assay_engine/state.rs
1//! Composed engine state.
2//!
3//! `EngineState<S>` bundles the per-module contexts. Phase 3 composed
4//! workflow + dashboard. Phase 8 adds an optional `AuthCtx` field
5//! (gated on the `auth` Cargo feature) and an `axum::FromRef` impl so
6//! the auth router's handlers can extract `AuthCtx` directly via
7//! `State<AuthCtx>`. The `AdminApiKeys` value flows through the same
8//! seam so admin endpoints (`/admin/oidc/*`, `/admin/auth/*`) can
9//! enforce the configured bearer-token allowlist.
10
11use std::sync::Arc;
12
13use assay_auth::AuthCtx;
14use assay_dashboard::DashboardCtx;
15#[cfg(feature = "vault")]
16use assay_vault::VaultCtx;
17use assay_workflow::{WorkflowCtx, WorkflowStore};
18
19pub use assay_auth::state::AdminApiKeys;
20
21use crate::config::EngineConfig;
22
23#[derive(Clone)]
24pub struct EngineState<S: WorkflowStore> {
25 pub workflow: Arc<WorkflowCtx<S>>,
26 pub dashboard: Arc<DashboardCtx>,
27 /// Composed auth context — present iff the runtime
28 /// `engine.modules.auth.enabled` row is TRUE. `axum::FromRef`
29 /// extracts it for the auth router's handlers.
30 pub auth: Option<AuthCtx>,
31 /// Composed vault context — present iff the `vault` Cargo feature
32 /// is on AND the runtime `engine.modules.vault.enabled` row is
33 /// TRUE. Plan 17's v0.3.0 module — KV / transit / collections /
34 /// share / sealing / dynamic creds / BW-compat all hang off this.
35 #[cfg(feature = "vault")]
36 pub vault: Option<VaultCtx>,
37 /// Admin API keys for the `/admin/*` HTTP surface — checked by
38 /// auth handlers via `axum::extract::FromRef<EngineState<S>>` so
39 /// the same value flows from `engine.toml` through to per-request
40 /// auth gating without a global static. Empty when no admin
41 /// surface is configured.
42 pub admin_api_keys: Arc<Vec<String>>,
43 /// Names of modules attached/loaded during boot — surfaced through
44 /// `/healthz` for ops visibility (which functional modules this
45 /// engine instance has wired up).
46 pub modules: Arc<Vec<String>>,
47 /// This instance's row in `engine.instances`. Lets `/healthz` and
48 /// future visibility endpoints identify which engine process is
49 /// answering.
50 pub instance_id: uuid::Uuid,
51 /// `assay-engine` crate version. Returned in
52 /// `/api/v1/engine/core/health` so external monitors can correlate
53 /// health checks with deployments.
54 pub engine_version: &'static str,
55 /// Wall-clock seconds since the UNIX epoch when this engine process
56 /// finished booting. Surfaced through `/api/v1/engine/core/info` so the
57 /// engine console can display "uptime" without an extra DB lookup
58 /// per request — matches the value the engine wrote to its
59 /// `engine.instances` row at boot.
60 pub started_at: f64,
61 /// Parsed `engine.toml` snapshot — admin endpoints serialise this
62 /// (with secrets redacted) so the engine console's "Config" pane
63 /// shows the operator exactly what the running engine is using.
64 /// `Arc` so cloning state per-request stays cheap.
65 pub engine_config: Arc<EngineConfig>,
66}
67
68impl<S: WorkflowStore> axum::extract::FromRef<EngineState<S>> for AuthCtx {
69 /// FromRef impl so auth handlers can extract the resolved AuthCtx
70 /// via `State<AuthCtx>`. Panics when the engine binary mounted the
71 /// auth router without composing an AuthCtx — a misconfiguration
72 /// the engine boot path is responsible for preventing (the auth
73 /// router is only mounted when `state.auth.is_some()`).
74 fn from_ref(s: &EngineState<S>) -> Self {
75 s.auth
76 .clone()
77 .expect("auth router mounted without an AuthCtx — engine boot bug")
78 }
79}
80
81impl<S: WorkflowStore> axum::extract::FromRef<EngineState<S>> for AdminApiKeys {
82 fn from_ref(s: &EngineState<S>) -> Self {
83 AdminApiKeys(Arc::clone(&s.admin_api_keys))
84 }
85}
86
87#[cfg(feature = "vault")]
88impl<S: WorkflowStore> axum::extract::FromRef<EngineState<S>> for VaultCtx {
89 /// FromRef impl so vault handlers (added Phase 1+) can extract the
90 /// resolved VaultCtx via `State<VaultCtx>`. Panics when the vault
91 /// router is mounted without composing a VaultCtx — same engine-
92 /// boot-bug surface AuthCtx uses.
93 fn from_ref(s: &EngineState<S>) -> Self {
94 s.vault
95 .clone()
96 .expect("vault router mounted without a VaultCtx — engine boot bug")
97 }
98}