Skip to main content

aion_server/
state.rs

1//! Shared server state constructed once at startup.
2
3use std::{path::PathBuf, sync::Arc};
4
5use aion::{
6    ActivityDispatcher, EngineBuilder, RuntimeHandle, SignalRouter, signal::ConcreteSignalRouter,
7};
8use aion_store::{EventStore, NamespaceStore, OutboxStore, WorkerDeploymentStore};
9#[cfg(feature = "libsql-backend")]
10use aion_store_libsql::LibSqlStore;
11
12use crate::dev_ui::{ActivityMockRegistry, DevMockingDispatcher};
13
14#[cfg(feature = "auth")]
15use crate::auth::JwksCache;
16use crate::{
17    config::{RuntimeConfig, ServerConfig, StoreBackend, StoreConfig},
18    error::ServerError,
19    namespace::{NamespaceGuard, NamespaceMinter, resolver::NamespaceResolver},
20    observability::{
21        Metrics, health::HealthState, instrumented_store::InstrumentedEventStore,
22        metrics::MetricsError,
23    },
24    shutdown::DrainState,
25    worker::{
26        ConnectedWorkerRegistry, HeartbeatTracker, PendingActivities, WorkerActivityDispatcher,
27        supervisor::WorkerSupervisor,
28    },
29};
30
31/// Build an uncommissioned supervisor over one durable deployment store and
32/// the state's cluster channel.
33///
34/// Every state constructor routes through this, so no construction path can
35/// accidentally hand the supervisor a DIFFERENT store from the one the
36/// management API writes through — desired state written in one place and read
37/// in another is exactly the drift this single call site removes. The same
38/// argument holds for the publisher: it is the state's own cluster channel,
39/// so a desired-state write made by the supervisor reaches the same live feed
40/// as one made by the worker-deployment endpoints.
41fn new_supervisor(
42    store: &Arc<dyn WorkerDeploymentStore>,
43    publisher: &crate::cluster_publisher::ClusterEventPublisher,
44) -> Arc<WorkerSupervisor> {
45    Arc::new(WorkerSupervisor::new(Arc::clone(store), publisher.clone()))
46}
47
48/// Cloneable shared state passed to all server transports.
49#[derive(Clone)]
50pub struct ServerState {
51    inner: Arc<ServerStateInner>,
52}
53
54struct ServerStateInner {
55    namespace_guard: NamespaceGuard,
56    runtime: RuntimeConfig,
57    worker_registry: ConnectedWorkerRegistry,
58    pending_activities: PendingActivities,
59    heartbeat_tracker: HeartbeatTracker,
60    /// Correlation registry joining the liveness probe's pushed gRPC pings to
61    /// the answers that come back up each worker's inbound stream (#197).
62    ///
63    /// Held on the state because the two halves live in tasks that cannot see
64    /// each other: the probe owns a timer loop, and each answer arrives inside
65    /// the tonic stream handler for one worker.
66    grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters,
67    drain_state: DrainState,
68    metrics: Option<Metrics>,
69    health: Option<HealthState>,
70    /// Shared per-run activity-mock registry. Present only when the dev surface
71    /// is commissioned; the engine's dispatcher consults this exact instance.
72    activity_mock_registry: Option<ActivityMockRegistry>,
73    /// The leaf libSQL store cast as an [`OutboxStore`], shared with the engine's
74    /// `EventStore` so the outbox dispatcher writes through the same single
75    /// `libsql::Connection`. `None` for the in-memory backend (no outbox table).
76    outbox_store: Option<Arc<dyn OutboxStore>>,
77    /// The durable namespace registry, captured from the SAME concrete leaf
78    /// backend as the engine's `EventStore` BEFORE that leaf is wrapped in the
79    /// decorator chain (`PublishingEventStore` → `InstrumentedEventStore`),
80    /// which do not implement [`NamespaceStore`]. The haematite backend supplies
81    /// the quorum-replicated implementation; the libSQL and in-memory backends
82    /// supply a local-only one. Always present so the control-plane mint
83    /// (Phase 1 S5) and `GET /namespaces` (S7) can reach a real store on every
84    /// boot. Mirrors the `cluster_store` retention pattern.
85    namespace_store: Arc<dyn NamespaceStore>,
86    /// Durable worker-deployment records captured from the same leaf backend.
87    worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
88    /// Managed-worker supervision (W-1..W-4). Always present and always
89    /// UNCOMMISSIONED at construction: it supervises nothing until
90    /// [`crate::run`] installs the operator's `[worker_supervision]` policy, so
91    /// state construction can never start a process by itself.
92    worker_supervisor: Arc<WorkerSupervisor>,
93    /// Advisory outbox wake (LSUB-2): the in-process `Notify` shared by the
94    /// engine's stage seam (the `InstrumentedEventStore`'s `append_with_outbox`)
95    /// and the [`OutboxDispatcher`](crate::worker::OutboxDispatcher) run loop, so
96    /// a committed fan-out row wakes the dispatcher in ~RTT instead of waiting up
97    /// to one poll interval. Always present (cheap, no `Option`): the handle is
98    /// harmless when the outbox is not commissioned, since nothing pulses it.
99    outbox_wake: Arc<tokio::sync::Notify>,
100    /// WS3 cluster topology/ownership publisher. Always present: the ops console's
101    /// cluster channel is served on every boot (calm state with no peers on a
102    /// single-node server). Sized from `websocket.cluster_broadcast_capacity`.
103    cluster_publisher: crate::cluster_publisher::ClusterEventPublisher,
104    /// NOI-5b agent-observability transcript sequencer + live fan-out. Always
105    /// present: the transcript channel is served on every boot. The backing
106    /// [`ObservabilityStore`](aion_store::ObservabilityStore) is the durable
107    /// `O`-keyspace impl on a haematite boot and an in-memory impl on every other
108    /// backend (libSQL / in-memory have no `O` keyspace), so the transcript path
109    /// is uniform across backends while only haematite persists across restart.
110    /// Sized from `websocket.cluster_broadcast_capacity` (the same deployment-wide
111    /// real-time channel capacity the cluster tail uses).
112    transcript_publisher: crate::activity_publisher::ActivityEventPublisher,
113    /// NOI-6 server-side intervention routing: the `attempt -> owning-worker`
114    /// back-index the intervention router resolves a command's target through.
115    /// Always present (cheap, no `Option`): the agent-dispatch path binds an owner
116    /// when it dispatches an agent attempt and releases it on completion, so the
117    /// router resolves the CURRENT owner. Empty until an agent attempt is
118    /// dispatched — a command to an unbound attempt is the attempt-scoped no-op.
119    attempt_owners: crate::worker::AttemptOwnerIndex,
120    /// R1 live unserved-queue state. Always present (cheap, no `Option`): the
121    /// bridge dispatcher publishes every parked dispatch into THIS instance, so
122    /// `unserved_queues()` answers "which addresses are unserved, why, and which
123    /// runs are waiting on them" without reading logs. Empty whenever nothing is
124    /// parked.
125    queue_service_state: crate::worker::QueueServiceState,
126    /// R1 queue-declaration source, filled in with the engine-backed reader once
127    /// the engine exists. Held so surfaces (and the bridge) share ONE reader
128    /// rather than each building their own view of the deployed contracts.
129    queue_declarations: crate::worker::QueueDeclarationSource,
130    /// The last completed update check (#189 slice one), shared with the
131    /// [`crate::update_check::UpdateCheckObserver`] decorator that writes it
132    /// on the full boot path. Always present and always EMPTY at
133    /// construction: a server that has never checked says so, and nothing
134    /// here ever fetches anything — every check is an explicit operator act.
135    update_status: crate::update_check::UpdateStatusState,
136    /// The server-resolved workspace root for declared action bodies (#139):
137    /// the aion home's `clones/` directory, resolved ONCE at state
138    /// construction. The declared-body dispatcher expands `{workspace_root}`
139    /// with THIS value and the startup banner reports it, so composition
140    /// points read the value instead of re-deriving it. A resolution failure
141    /// is held here — boot proceeds, and the failure surfaces as a terminal
142    /// refusal when a placeholder-bearing body dispatches.
143    workspace_root: crate::worker::WorkspaceRoot,
144    /// This node's distribution name for the WS3 cluster snapshot self-identity.
145    /// `Some` on a distributed haematite boot (the configured `store.cluster.node_id`),
146    /// `None` on a single-node boot — the snapshot then reports the standalone
147    /// self-label so the ops console still has a node to render.
148    cluster_self_node: Option<String>,
149    /// Owns the distributed haematite inbound-write responder thread, kept alive
150    /// for the server's lifetime so a cluster node keeps answering peers'
151    /// replication/election traffic. `None` for non-distributed boots. Dropping
152    /// the state stops the responder.
153    #[cfg(feature = "haematite-backend")]
154    cluster_responder: Option<aion_store_haematite::ClusterResponder>,
155    /// The concrete distributed haematite store the SS-5b supervisor polls for
156    /// peer liveness. `None` for every non-distributed boot.
157    #[cfg(feature = "haematite-backend")]
158    cluster_store: Option<Arc<aion_store_haematite::HaematiteStore>>,
159    /// The peers the SS-5b supervisor watches (each with the shards this node
160    /// adopts on its death). Empty for non-distributed boots.
161    #[cfg(feature = "haematite-backend")]
162    watched_peers: Vec<crate::cluster::WatchedPeer>,
163    /// The request-routing shard directory (R-2), built over the cluster store +
164    /// static peer config. `None` for every non-distributed boot, so the routing
165    /// edge falls back to the bare R-1 ownership check (and the default path is a
166    /// no-op).
167    #[cfg(feature = "haematite-backend")]
168    shard_directory: Option<Arc<crate::routing::StaticShardDirectory>>,
169    /// The request forwarder (R-3): relays a non-local signal/query/cancel to the
170    /// shard owner's gRPC address. `None` for non-distributed boots. The trait
171    /// object makes the liminal forwarder a one-line swap when 13-L0/L1 land (R-6).
172    #[cfg(feature = "haematite-backend")]
173    request_forwarder: Option<Arc<dyn crate::routing::RequestForwarder>>,
174    #[cfg(feature = "auth")]
175    jwks_cache: Option<JwksCache>,
176}
177
178impl ServerState {
179    /// Fallback cluster broadcast capacity for the `from_parts*` embedder/test
180    /// constructors, which bypass config validation. The config-driven
181    /// [`Self::build`] path always sizes the publisher from the validated
182    /// `websocket.cluster_broadcast_capacity` instead.
183    ///
184    /// `NonZeroUsize::new(64)` is statically non-`None`, so the
185    /// [`Option::unwrap`]-free `match` keeps the value `const` without tripping
186    /// the workspace `unwrap_used`/`expect_used` deny lints.
187    const FALLBACK_CLUSTER_BROADCAST_CAPACITY: std::num::NonZeroUsize =
188        match std::num::NonZeroUsize::new(64) {
189            Some(value) => value,
190            None => std::num::NonZeroUsize::MIN,
191        };
192
193    /// Build shared state from operator configuration.
194    ///
195    /// # Errors
196    ///
197    /// Returns [`ServerError`] if the store cannot connect or the engine cannot
198    /// be constructed.
199    pub async fn build(config: ServerConfig) -> Result<Self, ServerError> {
200        let (store_config, runtime) = config.into_parts();
201        let connected = connect_store(store_config).await?;
202        Self::build_with_connected_store(connected, runtime).await
203    }
204
205    /// Build shared state from an already-constructed store.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`ServerError::EngineCall`] if the engine cannot be constructed.
210    pub async fn build_with_store<S>(store: S, runtime: RuntimeConfig) -> Result<Self, ServerError>
211    where
212        S: EventStore + NamespaceStore + WorkerDeploymentStore,
213    {
214        // Capture the concrete leaf as BOTH the event store and the namespace
215        // registry before it is wrapped in the (NamespaceStore-unaware) decorator
216        // chain — the same leaf, two trait objects.
217        let leaf = Arc::new(store);
218        let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
219        let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
220        Self::build_with_connected_store(
221            ConnectedStore::local(leaf, None, namespace_store, worker_deployment_store),
222            runtime,
223        )
224        .await
225    }
226
227    async fn build_with_connected_store(
228        connected: ConnectedStore,
229        runtime: RuntimeConfig,
230    ) -> Result<Self, ServerError> {
231        let cluster_self_node = connected.cluster_self_node();
232        let outbox_store = connected.outbox_store;
233        let bootstrap_coordinator = connected.bootstrap_coordinator;
234        #[cfg(feature = "haematite-backend")]
235        let cluster_responder = connected.cluster_responder;
236        #[cfg(feature = "haematite-backend")]
237        let cluster_store = connected.cluster_store;
238        #[cfg(feature = "haematite-backend")]
239        let watched_peers = connected.watched_peers;
240        // Build the R-2 directory + R-3 forwarder over the (live, failover-aware)
241        // cluster store and static peer config. Both present only for a
242        // distributed boot; `None` otherwise leaves the routing edge a no-op.
243        #[cfg(feature = "haematite-backend")]
244        let RoutingState {
245            shard_directory,
246            request_forwarder,
247            mint_routing,
248        } = build_routing_state(
249            cluster_store.as_ref(),
250            connected.directory_peers,
251            connected.self_node_id,
252        );
253        let (event_broadcast_capacity, query_timeout) = required_engine_seams(&runtime)?;
254        let (cluster_publisher, transcript_publisher) =
255            build_real_time_publishers(&runtime, connected.observability_store)?;
256        let (metrics, outbox_wake, instrumented_store) =
257            build_instrumented_store(&runtime, connected.event_store)?;
258        let exported_metrics = runtime.metrics.enabled.then_some(metrics.clone());
259        #[cfg(not(feature = "haematite-backend"))]
260        let mint_routing: Option<crate::namespace::NamespaceRouting> = None;
261        let seams = build_worker_seams(
262            &runtime,
263            &cluster_publisher,
264            &connected.namespace_store,
265            &connected.worker_deployment_store,
266            mint_routing,
267        );
268        let (
269            activity_dispatcher,
270            activity_mock_registry,
271            attempt_owners,
272            workspace_root,
273            update_status,
274        ) = build_decorated_dispatcher(&runtime, &seams, transcript_publisher.clone());
275
276        let engine = build_engine(EngineAssembly {
277            instrumented_store: &instrumented_store,
278            event_broadcast_capacity,
279            query_timeout,
280            activity_dispatcher,
281            active_registry: Arc::new(aion::Registry::default()),
282            bootstrap_coordinator,
283            runtime: &runtime,
284        })
285        .await?;
286        let engine = Arc::new(engine);
287        install_engine_backed_seams(&seams, &engine, runtime.outbox.enabled);
288        let resolver = NamespaceResolver::from_config(runtime.namespace.clone(), engine);
289        let worker_supervisor =
290            new_supervisor(&connected.worker_deployment_store, &cluster_publisher);
291        #[cfg(feature = "auth")]
292        let jwks_cache = build_jwks_cache(&runtime).await?;
293        Ok(Self {
294            inner: Arc::new(ServerStateInner {
295                namespace_guard: NamespaceGuard::new(resolver),
296                runtime,
297                metrics: exported_metrics,
298                worker_registry: seams.worker_registry,
299                pending_activities: seams.pending_activities,
300                heartbeat_tracker: seams.heartbeat_tracker,
301                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
302                drain_state: seams.drain_state,
303                health: Some(HealthState::new(instrumented_store, true)),
304                activity_mock_registry,
305                outbox_store,
306                namespace_store: connected.namespace_store,
307                worker_supervisor,
308                worker_deployment_store: connected.worker_deployment_store,
309                outbox_wake,
310                cluster_publisher,
311                transcript_publisher,
312                attempt_owners,
313                queue_service_state: seams.queue_service_state,
314                queue_declarations: seams.queue_declarations,
315                update_status,
316                workspace_root,
317                cluster_self_node,
318                #[cfg(feature = "haematite-backend")]
319                cluster_responder,
320                #[cfg(feature = "haematite-backend")]
321                cluster_store,
322                #[cfg(feature = "haematite-backend")]
323                watched_peers,
324                #[cfg(feature = "haematite-backend")]
325                shard_directory,
326                #[cfg(feature = "haematite-backend")]
327                request_forwarder,
328                #[cfg(feature = "auth")]
329                jwks_cache,
330            }),
331        })
332    }
333
334    /// Build shared state from explicit parts with a default worker registry.
335    #[must_use]
336    pub fn from_parts(namespace_resolver: NamespaceResolver, runtime: RuntimeConfig) -> Self {
337        // No durable store was supplied (this constructor builds state from a
338        // resolver only), so the registry is a local-only in-memory store —
339        // present so `namespace_store()` is always reachable, never mutating any
340        // durable backend.
341        Self::from_parts_with_namespace_store(
342            namespace_resolver,
343            runtime,
344            Arc::new(aion_store::InMemoryStore::default()),
345        )
346    }
347
348    /// Build shared state from explicit parts with one caller-supplied durable
349    /// namespace and worker-deployment leaf.
350    ///
351    /// Identical to [`Self::from_parts`] except both control-plane dimensions are
352    /// derived from `store`: namespace registry reads/writes and durable worker
353    /// deployments use the same supplied leaf rather than fresh in-memory state.
354    #[must_use]
355    pub fn from_parts_with_namespace_store<S>(
356        namespace_resolver: NamespaceResolver,
357        runtime: RuntimeConfig,
358        store: Arc<S>,
359    ) -> Self
360    where
361        S: NamespaceStore + WorkerDeploymentStore,
362    {
363        let namespace_store: Arc<dyn NamespaceStore> = store.clone();
364        let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = store;
365        Self::from_parts_with_control_stores(
366            namespace_resolver,
367            runtime,
368            namespace_store,
369            worker_deployment_store,
370        )
371    }
372
373    /// Build shared state with caller-supplied namespace and worker-deployment stores.
374    ///
375    /// This is the explicit embedder/test seam for retaining both control-plane
376    /// contracts from one durable leaf without constructing the full engine boot.
377    #[must_use]
378    pub fn from_parts_with_control_stores(
379        namespace_resolver: NamespaceResolver,
380        runtime: RuntimeConfig,
381        namespace_store: Arc<dyn NamespaceStore>,
382        worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
383    ) -> Self {
384        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
385        // Bound here, beside the tracker, for the same reason: `runtime` moves
386        // into the state below, and the transport-loss budget must be read from
387        // the operator's heartbeat window BEFORE it does. A default ledger here
388        // would give an embedder-booted server a ZERO budget — exactly one
389        // re-dispatchable worker loss, then transport-exhausted — while the
390        // full-boot path got the real one.
391        let pending_activities =
392            PendingActivities::default().with_heartbeat_window(runtime.worker.heartbeat_window);
393        // Computed before `runtime` moves into the state: the retention bounds
394        // flow from `[observability]` config on the embedder path too, so a
395        // from-parts server enforces the same truncation/cap as a full boot.
396        let bounds = transcript_bounds(&runtime);
397        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
398            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
399        );
400        Self {
401            inner: Arc::new(ServerStateInner {
402                namespace_guard: NamespaceGuard::new(namespace_resolver),
403                runtime,
404                worker_registry: ConnectedWorkerRegistry::default()
405                    .with_worker_deployment_store(worker_deployment_store.clone())
406                    .with_cluster_publisher(cluster_publisher.clone()),
407                pending_activities,
408                heartbeat_tracker,
409                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
410                drain_state: DrainState::default(),
411                metrics: None,
412                health: None,
413                activity_mock_registry: None,
414                outbox_store: None,
415                namespace_store,
416                worker_supervisor: new_supervisor(&worker_deployment_store, &cluster_publisher),
417                worker_deployment_store,
418                outbox_wake: Arc::new(tokio::sync::Notify::new()),
419                cluster_publisher,
420                // NOI-5b: a from-parts / embedder state has no durable store, so
421                // the transcript sequencer runs over an in-memory `O`-keyspace
422                // impl — the transcript channel is served on every boot.
423                transcript_publisher: build_transcript_publisher(
424                    None,
425                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
426                    bounds,
427                ),
428                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
429                queue_service_state: crate::worker::QueueServiceState::default(),
430                queue_declarations: crate::worker::QueueDeclarationSource::default(),
431                // Empty and STAYING empty: this constructor builds no
432                // dispatcher, so no observer ever writes it — the honest
433                // answer for an embedder/test state is "never checked".
434                update_status: crate::update_check::UpdateStatusState::default(),
435                // Inert here: this constructor builds no declared-body
436                // dispatcher, so nothing expands `{workspace_root}` with this
437                // value — it exists so `workspace_root()` is always readable.
438                workspace_root: crate::worker::WorkspaceRoot::resolve(),
439                cluster_self_node: None,
440                #[cfg(feature = "haematite-backend")]
441                cluster_responder: None,
442                #[cfg(feature = "haematite-backend")]
443                cluster_store: None,
444                #[cfg(feature = "haematite-backend")]
445                watched_peers: Vec::new(),
446                #[cfg(feature = "haematite-backend")]
447                shard_directory: None,
448                #[cfg(feature = "haematite-backend")]
449                request_forwarder: None,
450                #[cfg(feature = "auth")]
451                jwks_cache: None,
452            }),
453        }
454    }
455
456    /// Build shared state from explicit parts with one caller-supplied durable
457    /// namespace/worker-deployment leaf and a caller-supplied JWKS cache.
458    ///
459    /// The combined seam of [`Self::from_parts_with_namespace_store`] (seed the
460    /// durable registry the control-plane read/create paths observe) and
461    /// [`Self::from_parts_with_jwks`] (validate bearer tokens against an injected
462    /// issuer): an enumerated caller can exercise the real JWT authorization path
463    /// against a seeded registry without a full [`Self::build`] boot.
464    #[cfg(feature = "auth")]
465    #[must_use]
466    pub fn from_parts_with_namespace_store_and_jwks<S>(
467        namespace_resolver: NamespaceResolver,
468        runtime: RuntimeConfig,
469        store: Arc<S>,
470        jwks_cache: JwksCache,
471    ) -> Self
472    where
473        S: NamespaceStore + WorkerDeploymentStore,
474    {
475        let namespace_store: Arc<dyn NamespaceStore> = store.clone();
476        let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = store;
477        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
478        // Bound here, beside the tracker, for the same reason: `runtime` moves
479        // into the state below, and the transport-loss budget must be read from
480        // the operator's heartbeat window BEFORE it does. A default ledger here
481        // would give an embedder-booted server a ZERO budget — exactly one
482        // re-dispatchable worker loss, then transport-exhausted — while the
483        // full-boot path got the real one.
484        let pending_activities =
485            PendingActivities::default().with_heartbeat_window(runtime.worker.heartbeat_window);
486        // Computed before `runtime` moves into the state: the retention bounds
487        // flow from `[observability]` config on the embedder path too, so a
488        // from-parts server enforces the same truncation/cap as a full boot.
489        let bounds = transcript_bounds(&runtime);
490        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
491            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
492        );
493        Self {
494            inner: Arc::new(ServerStateInner {
495                namespace_guard: NamespaceGuard::new(namespace_resolver),
496                runtime,
497                worker_registry: ConnectedWorkerRegistry::default()
498                    .with_worker_deployment_store(worker_deployment_store.clone())
499                    .with_cluster_publisher(cluster_publisher.clone()),
500                pending_activities,
501                heartbeat_tracker,
502                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
503                drain_state: DrainState::default(),
504                metrics: None,
505                health: None,
506                activity_mock_registry: None,
507                outbox_store: None,
508                namespace_store,
509                worker_supervisor: new_supervisor(&worker_deployment_store, &cluster_publisher),
510                worker_deployment_store,
511                outbox_wake: Arc::new(tokio::sync::Notify::new()),
512                cluster_publisher,
513                // NOI-5b: a from-parts / embedder state has no durable store, so
514                // the transcript sequencer runs over an in-memory `O`-keyspace
515                // impl — the transcript channel is served on every boot.
516                transcript_publisher: build_transcript_publisher(
517                    None,
518                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
519                    bounds,
520                ),
521                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
522                queue_service_state: crate::worker::QueueServiceState::default(),
523                queue_declarations: crate::worker::QueueDeclarationSource::default(),
524                // Empty and STAYING empty: this constructor builds no
525                // dispatcher, so no observer ever writes it — the honest
526                // answer for an embedder/test state is "never checked".
527                update_status: crate::update_check::UpdateStatusState::default(),
528                // Inert here: this constructor builds no declared-body
529                // dispatcher, so nothing expands `{workspace_root}` with this
530                // value — it exists so `workspace_root()` is always readable.
531                workspace_root: crate::worker::WorkspaceRoot::resolve(),
532                cluster_self_node: None,
533                #[cfg(feature = "haematite-backend")]
534                cluster_responder: None,
535                #[cfg(feature = "haematite-backend")]
536                cluster_store: None,
537                #[cfg(feature = "haematite-backend")]
538                watched_peers: Vec::new(),
539                #[cfg(feature = "haematite-backend")]
540                shard_directory: None,
541                #[cfg(feature = "haematite-backend")]
542                request_forwarder: None,
543                jwks_cache: Some(jwks_cache),
544            }),
545        }
546    }
547
548    /// Build shared state from explicit parts with a caller-supplied JWKS cache.
549    ///
550    /// Embedders that construct their own [`JwksCache`] (for example against a
551    /// private issuer) can install it here; transports then validate bearer
552    /// tokens against it exactly as with a [`Self::build`]-constructed state.
553    #[cfg(feature = "auth")]
554    #[must_use]
555    pub fn from_parts_with_jwks(
556        namespace_resolver: NamespaceResolver,
557        runtime: RuntimeConfig,
558        jwks_cache: JwksCache,
559    ) -> Self {
560        // No durable store was supplied, so the deployment records and the
561        // supervisor built over them share ONE in-memory leaf: two leaves
562        // would let the supervisor read a record the API never wrote.
563        let fallback_deployment_store: Arc<dyn WorkerDeploymentStore> =
564            Arc::new(aion_store::InMemoryStore::default());
565        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
566        // Bound here, beside the tracker, for the same reason: `runtime` moves
567        // into the state below, and the transport-loss budget must be read from
568        // the operator's heartbeat window BEFORE it does. A default ledger here
569        // would give an embedder-booted server a ZERO budget — exactly one
570        // re-dispatchable worker loss, then transport-exhausted — while the
571        // full-boot path got the real one.
572        let pending_activities =
573            PendingActivities::default().with_heartbeat_window(runtime.worker.heartbeat_window);
574        // Computed before `runtime` moves into the state: the retention bounds
575        // flow from `[observability]` config on the embedder path too, so a
576        // from-parts server enforces the same truncation/cap as a full boot.
577        let bounds = transcript_bounds(&runtime);
578        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
579            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
580        );
581        Self {
582            inner: Arc::new(ServerStateInner {
583                namespace_guard: NamespaceGuard::new(namespace_resolver),
584                runtime,
585                worker_registry: ConnectedWorkerRegistry::default(),
586                pending_activities,
587                heartbeat_tracker,
588                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
589                drain_state: DrainState::default(),
590                metrics: None,
591                health: None,
592                activity_mock_registry: None,
593                outbox_store: None,
594                // No durable store was supplied (these constructors build state
595                // from a resolver only), so the registry is a local-only
596                // in-memory store — present so `namespace_store()` is always
597                // reachable, never mutating any durable backend.
598                namespace_store: Arc::new(aion_store::InMemoryStore::default()),
599                worker_supervisor: new_supervisor(&fallback_deployment_store, &cluster_publisher),
600                worker_deployment_store: fallback_deployment_store,
601                outbox_wake: Arc::new(tokio::sync::Notify::new()),
602                cluster_publisher,
603                // NOI-5b: a from-parts / embedder state has no durable store, so
604                // the transcript sequencer runs over an in-memory `O`-keyspace
605                // impl — the transcript channel is served on every boot.
606                transcript_publisher: build_transcript_publisher(
607                    None,
608                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
609                    bounds,
610                ),
611                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
612                queue_service_state: crate::worker::QueueServiceState::default(),
613                queue_declarations: crate::worker::QueueDeclarationSource::default(),
614                // Empty and STAYING empty: this constructor builds no
615                // dispatcher, so no observer ever writes it — the honest
616                // answer for an embedder/test state is "never checked".
617                update_status: crate::update_check::UpdateStatusState::default(),
618                // Inert here: this constructor builds no declared-body
619                // dispatcher, so nothing expands `{workspace_root}` with this
620                // value — it exists so `workspace_root()` is always readable.
621                workspace_root: crate::worker::WorkspaceRoot::resolve(),
622                cluster_self_node: None,
623                #[cfg(feature = "haematite-backend")]
624                cluster_responder: None,
625                #[cfg(feature = "haematite-backend")]
626                cluster_store: None,
627                #[cfg(feature = "haematite-backend")]
628                watched_peers: Vec::new(),
629                #[cfg(feature = "haematite-backend")]
630                shard_directory: None,
631                #[cfg(feature = "haematite-backend")]
632                request_forwarder: None,
633                jwks_cache: Some(jwks_cache),
634            }),
635        }
636    }
637
638    /// Build shared state from explicit parts with a caller-supplied registry.
639    #[must_use]
640    pub fn from_parts_with_registry(
641        namespace_resolver: NamespaceResolver,
642        runtime: RuntimeConfig,
643        worker_registry: ConnectedWorkerRegistry,
644    ) -> Self {
645        // No durable store was supplied, so the deployment records and the
646        // supervisor built over them share ONE in-memory leaf: two leaves
647        // would let the supervisor read a record the API never wrote.
648        let fallback_deployment_store: Arc<dyn WorkerDeploymentStore> =
649            Arc::new(aion_store::InMemoryStore::default());
650        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
651        // Bound here, beside the tracker, for the same reason: `runtime` moves
652        // into the state below, and the transport-loss budget must be read from
653        // the operator's heartbeat window BEFORE it does. A default ledger here
654        // would give an embedder-booted server a ZERO budget — exactly one
655        // re-dispatchable worker loss, then transport-exhausted — while the
656        // full-boot path got the real one.
657        let pending_activities =
658            PendingActivities::default().with_heartbeat_window(runtime.worker.heartbeat_window);
659        // Computed before `runtime` moves into the state: the retention bounds
660        // flow from `[observability]` config on the embedder path too, so a
661        // from-parts server enforces the same truncation/cap as a full boot.
662        let bounds = transcript_bounds(&runtime);
663        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
664            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
665        );
666        Self {
667            inner: Arc::new(ServerStateInner {
668                namespace_guard: NamespaceGuard::new(namespace_resolver),
669                runtime,
670                worker_registry,
671                pending_activities,
672                heartbeat_tracker,
673                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
674                drain_state: DrainState::default(),
675                metrics: None,
676                health: None,
677                activity_mock_registry: None,
678                outbox_store: None,
679                // No durable store was supplied (these constructors build state
680                // from a resolver only), so the registry is a local-only
681                // in-memory store — present so `namespace_store()` is always
682                // reachable, never mutating any durable backend.
683                namespace_store: Arc::new(aion_store::InMemoryStore::default()),
684                worker_supervisor: new_supervisor(&fallback_deployment_store, &cluster_publisher),
685                worker_deployment_store: fallback_deployment_store,
686                outbox_wake: Arc::new(tokio::sync::Notify::new()),
687                cluster_publisher,
688                // NOI-5b: a from-parts / embedder state has no durable store, so
689                // the transcript sequencer runs over an in-memory `O`-keyspace
690                // impl — the transcript channel is served on every boot.
691                transcript_publisher: build_transcript_publisher(
692                    None,
693                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
694                    bounds,
695                ),
696                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
697                queue_service_state: crate::worker::QueueServiceState::default(),
698                queue_declarations: crate::worker::QueueDeclarationSource::default(),
699                // Empty and STAYING empty: this constructor builds no
700                // dispatcher, so no observer ever writes it — the honest
701                // answer for an embedder/test state is "never checked".
702                update_status: crate::update_check::UpdateStatusState::default(),
703                // Inert here: this constructor builds no declared-body
704                // dispatcher, so nothing expands `{workspace_root}` with this
705                // value — it exists so `workspace_root()` is always readable.
706                workspace_root: crate::worker::WorkspaceRoot::resolve(),
707                cluster_self_node: None,
708                #[cfg(feature = "haematite-backend")]
709                cluster_responder: None,
710                #[cfg(feature = "haematite-backend")]
711                cluster_store: None,
712                #[cfg(feature = "haematite-backend")]
713                watched_peers: Vec::new(),
714                #[cfg(feature = "haematite-backend")]
715                shard_directory: None,
716                #[cfg(feature = "haematite-backend")]
717                request_forwarder: None,
718                #[cfg(feature = "auth")]
719                jwks_cache: None,
720            }),
721        }
722    }
723
724    /// Borrow the namespace guard shared by all transports.
725    #[must_use]
726    pub fn namespace_guard(&self) -> &NamespaceGuard {
727        &self.inner.namespace_guard
728    }
729
730    /// Build the deploy authorization guard over the shared resolver.
731    #[must_use]
732    pub fn deploy_guard(&self) -> crate::deploy::DeployGuard {
733        crate::deploy::DeployGuard::new(self.inner.namespace_guard.resolver().clone())
734    }
735
736    /// Borrow non-secret runtime settings needed by transports.
737    #[must_use]
738    pub fn runtime_config(&self) -> &RuntimeConfig {
739        &self.inner.runtime
740    }
741
742    /// Borrow the last-completed-update-check slot (#189 slice one).
743    ///
744    /// Written only by the [`crate::update_check::UpdateCheckObserver`] on the
745    /// full boot path; read by `GET /update-status`. Empty until a check has
746    /// genuinely completed — a fresh server has honestly never checked.
747    /// Crate-visible only: the slot's type is implementation detail behind
748    /// the route, not `aion-server` public API.
749    #[must_use]
750    pub(crate) fn update_status(&self) -> &crate::update_check::UpdateStatusState {
751        &self.inner.update_status
752    }
753
754    /// Borrow the server-resolved workspace root declared bodies expand
755    /// `{workspace_root}` with (#139).
756    ///
757    /// The startup banner reads this so composition points learn the value
758    /// from the server that will use it, rather than re-deriving it. That
759    /// banner claim holds for [`Self::build`]-constructed states, where this
760    /// same value is threaded into the declared-body dispatcher; the
761    /// `from_parts*` constructors build no such dispatcher, so their copy is
762    /// inert — readable, but expanded by nothing.
763    #[must_use]
764    pub fn workspace_root(&self) -> &crate::worker::WorkspaceRoot {
765        &self.inner.workspace_root
766    }
767
768    /// Borrow the connected-worker registry shared by worker transports and dispatch.
769    #[must_use]
770    pub fn worker_registry(&self) -> &ConnectedWorkerRegistry {
771        &self.inner.worker_registry
772    }
773
774    /// Borrow the WS3 cluster-event publisher shared by the cluster state-change
775    /// sites (supervisor, worker registry) and the cluster subscription endpoint.
776    /// Always present, on every boot.
777    #[must_use]
778    pub fn cluster_publisher(&self) -> &crate::cluster_publisher::ClusterEventPublisher {
779        &self.inner.cluster_publisher
780    }
781
782    /// Borrow the NOI-5b transcript sequencer shared by the worker->server
783    /// ingestion seam (which publishes a running activity's `ActivityEvent`s) and
784    /// the transcript subscription endpoint (which tails + resumes them). Always
785    /// present, on every boot.
786    #[must_use]
787    pub fn transcript_publisher(&self) -> &crate::activity_publisher::ActivityEventPublisher {
788        &self.inner.transcript_publisher
789    }
790
791    /// Borrow the NOI-6 `attempt -> owning-worker` back-index. The agent-dispatch
792    /// path binds an owner when it dispatches an agent attempt and releases it on
793    /// completion, so the intervention router always resolves the CURRENT owner.
794    #[must_use]
795    pub fn attempt_owners(&self) -> &crate::worker::AttemptOwnerIndex {
796        &self.inner.attempt_owners
797    }
798
799    /// Borrow the R1 live unserved-queue state — the same instance the bridge
800    /// dispatcher publishes every parked dispatch into.
801    #[must_use]
802    pub fn queue_service_state(&self) -> &crate::worker::QueueServiceState {
803        &self.inner.queue_service_state
804    }
805
806    /// Borrow the R1 queue-declaration source — the engine-backed reader the
807    /// bridge classifies against. Answers `Unknown` on a state built without an
808    /// engine, which never refuses anything.
809    #[must_use]
810    pub fn queue_declarations(&self) -> &crate::worker::QueueDeclarationSource {
811        &self.inner.queue_declarations
812    }
813
814    /// Every queue address currently unserved, with its taxonomy reason, the
815    /// policy it is held under, the live poller census behind the verdict, and
816    /// the runs parked on it.
817    ///
818    /// This is the server-side answer to "is anything stuck, and on what" — the
819    /// question the pre-R1 seam could only be asked by reading logs.
820    ///
821    /// # Errors
822    ///
823    /// Returns [`ServerError::LockPoisoned`] if the state lock is poisoned.
824    pub fn unserved_queues(&self) -> Result<Vec<crate::worker::UnservedQueue>, ServerError> {
825        self.inner.queue_service_state.unserved()
826    }
827
828    /// Every run this engine process could not make resident, with the reason it
829    /// could not and when the failure was observed (#117).
830    ///
831    /// This is the fleet half of the degraded-residency question. `POST
832    /// /workflows/describe` answers it for a run an operator can already name;
833    /// this answers it for the operator who cannot, which is the case that made
834    /// the original defect unrecoverable in practice — the id was only ever
835    /// printed in a boot log line that had scrolled away.
836    ///
837    /// The set is per-process and self-clearing: an entry disappears the moment
838    /// the engine observes that run resident, so an EMPTY list is the healthy
839    /// answer and never a stale one.
840    ///
841    /// # Errors
842    ///
843    /// Returns [`ServerError`] when the state carries no engine handle, or when
844    /// the registry lock is poisoned.
845    pub fn unrecoverable_runs(
846        &self,
847    ) -> Result<Vec<(aion_core::WorkflowId, aion::registry::UnrecoverableRun)>, ServerError> {
848        self.engine()?
849            .registry()
850            .unrecoverable()
851            .list()
852            .map_err(ServerError::from)
853    }
854
855    /// Build the NOI-6 intervention router over the connected-worker registry, the
856    /// attempt-owner back-index, and the active intervention transport.
857    ///
858    /// The transport is the liminal server-push
859    /// ([`LiminalInterventionTransport`](crate::worker::LiminalInterventionTransport))
860    /// when the `liminal-transport` feature is compiled in — the production path
861    /// that pushes a routed command out on the owning worker's connection — and a
862    /// null transport otherwise, which reports the target unreachable so every
863    /// command NACKs the attempt-scoped no-op rather than silently vanishing. The
864    /// router is cheap to build (it clones cloneable handles), so it is constructed
865    /// per request at the endpoint rather than stored.
866    #[must_use]
867    pub fn intervention_router(&self) -> crate::worker::InterventionRouter {
868        let transport: std::sync::Arc<dyn crate::worker::InterventionTransport> = {
869            #[cfg(feature = "liminal-transport")]
870            {
871                std::sync::Arc::new(crate::worker::LiminalInterventionTransport)
872            }
873            #[cfg(not(feature = "liminal-transport"))]
874            {
875                std::sync::Arc::new(NullInterventionTransport)
876            }
877        };
878        crate::worker::InterventionRouter::new(
879            self.inner.worker_registry.clone(),
880            self.inner.attempt_owners.clone(),
881            transport,
882        )
883        // Lane #229: an APPLIED InjectMessage is teed into the durable
884        // transcript, so the retained record holds the operator's words.
885        .with_transcript_publisher(self.inner.transcript_publisher.clone())
886    }
887
888    /// This node's configured cluster distribution name for the WS3 snapshot
889    /// self-identity, or `None` on a single-node boot (the snapshot then reports
890    /// the standalone self-label).
891    #[must_use]
892    pub fn cluster_self_node(&self) -> Option<&str> {
893        self.inner.cluster_self_node.as_deref()
894    }
895
896    /// Clone the live engine handle the completion path records terminals through.
897    ///
898    /// This is the SAME `Arc<Engine>` the gRPC completion callback is built over
899    /// (state.rs installs `ServerOutboxDeliveryCallback::new(engine)` on the
900    /// pending tracker when `outbox.enabled`), so the liminal completion path
901    /// re-enters worker results through the identical `record_fan_out_completion`
902    /// seam rather than inventing a second one.
903    ///
904    /// # Errors
905    ///
906    /// Returns [`ServerError`] when the namespace resolver has no engine handle
907    /// (a state built from parts without an engine).
908    pub fn engine(&self) -> Result<Arc<aion::Engine>, ServerError> {
909        self.inner
910            .namespace_guard
911            .resolver()
912            .engine()
913            .map(Arc::clone)
914    }
915
916    /// Borrow the pending-activities tracker shared by the NIF bridge and worker stream handler.
917    #[must_use]
918    pub fn pending_activities(&self) -> &PendingActivities {
919        &self.inner.pending_activities
920    }
921
922    /// Borrow the heartbeat/liveness tracker shared by dispatch and worker streams.
923    #[must_use]
924    pub fn heartbeat_tracker(&self) -> &HeartbeatTracker {
925        &self.inner.heartbeat_tracker
926    }
927
928    /// Borrow the gRPC liveness answer-correlation registry (#197).
929    ///
930    /// The worker stream handler delivers each `LivenessAnswer` into it; the
931    /// liveness probe arms and awaits through the SAME handle. There is exactly
932    /// one per server, for the same reason there is exactly one probe.
933    #[must_use]
934    pub fn grpc_liveness_waiters(&self) -> &crate::worker::GrpcLivenessWaiters {
935        &self.inner.grpc_liveness_waiters
936    }
937
938    /// Borrow the drain gate shared by transports and worker dispatch.
939    #[must_use]
940    pub fn drain_state(&self) -> &DrainState {
941        &self.inner.drain_state
942    }
943
944    /// Borrow the prometheus metrics handle when this state was built with a store.
945    #[must_use]
946    pub fn metrics(&self) -> Option<&Metrics> {
947        self.inner.metrics.as_ref()
948    }
949
950    /// Borrow health probe state when this state was built with a store.
951    #[must_use]
952    pub fn health(&self) -> Option<&HealthState> {
953        self.inner.health.as_ref()
954    }
955
956    /// Borrow the shared per-run activity-mock registry when the dev surface is
957    /// commissioned. Returns [`None`] on a server with the dev surface dark, so
958    /// the dev handlers refuse cleanly rather than mocking on a production
959    /// server.
960    #[must_use]
961    pub fn activity_mock_registry(&self) -> Option<&ActivityMockRegistry> {
962        self.inner.activity_mock_registry.as_ref()
963    }
964
965    /// Borrow the outbox store the dispatcher claims rows from, when the durable
966    /// (libSQL) backend is in use. This is the SAME leaf `Arc<LibSqlStore>` the
967    /// engine writes through, so the dispatcher shares its single
968    /// `libsql::Connection` rather than opening a second contending one. Returns
969    /// [`None`] for the in-memory backend, which has no outbox table.
970    #[must_use]
971    pub fn outbox_store(&self) -> Option<Arc<dyn OutboxStore>> {
972        self.inner.outbox_store.clone()
973    }
974
975    /// Borrow the durable namespace registry shared by the control plane.
976    ///
977    /// This is the SAME concrete leaf backend the engine writes events through
978    /// (haematite quorum-replicated, or libSQL / in-memory local-only),
979    /// captured as a [`NamespaceStore`] before the decorator chain wrapped it.
980    /// Always present on every boot, so the mint-on-register path (Phase 1 S5)
981    /// and `GET /namespaces` (S7) can reach a real registry regardless of
982    /// backend.
983    #[must_use]
984    pub fn namespace_store(&self) -> &Arc<dyn NamespaceStore> {
985        &self.inner.namespace_store
986    }
987
988    /// Borrow the durable worker-deployment store shared by the control plane.
989    #[must_use]
990    pub fn worker_deployment_store(&self) -> &Arc<dyn WorkerDeploymentStore> {
991        &self.inner.worker_deployment_store
992    }
993
994    /// Borrow the managed-worker supervisor.
995    ///
996    /// Built over the SAME durable deployment store as
997    /// [`Self::worker_deployment_store`], so desired state written through the
998    /// API is the desired state the supervisor converges on. Uncommissioned
999    /// until [`crate::run`] installs an operator policy.
1000    #[must_use]
1001    pub fn worker_supervisor(&self) -> &Arc<WorkerSupervisor> {
1002        &self.inner.worker_supervisor
1003    }
1004
1005    /// Build the shared minted-on-use hook over the durable namespace store and
1006    /// the configured [`AutoCreate`](crate::config::AutoCreate) policy.
1007    ///
1008    /// This is the SAME policy logic the worker-registration seam applies (S5);
1009    /// the workflow-start safety net (S6) calls it after authorization so a
1010    /// client that starts a workflow before any worker registers still gets a
1011    /// durable namespace record. Cheap to build (clones an `Arc` + a `Copy`
1012    /// policy), so transports construct it per request rather than holding it.
1013    #[must_use]
1014    pub fn namespace_minter(&self) -> NamespaceMinter {
1015        let minter = NamespaceMinter::new(
1016            Arc::clone(&self.inner.namespace_store),
1017            self.inner.runtime.auto_create,
1018        )
1019        // Thread the deployment-global cluster channel so the start-time safety
1020        // net (S6) and the explicit `POST /namespaces` path (S7) emit the same
1021        // live "namespace created" delta the worker-mint seam (S5) does — all
1022        // three mint choke-points surface on the one ops-console push channel.
1023        .with_cluster_publisher(self.inner.cluster_publisher.clone());
1024        // And the namespace-mint routing context on a clustered boot, so a
1025        // namespace whose registry shard this node does not own is minted by the
1026        // node that does rather than being fenced forever. `None` off-cluster,
1027        // where the minter is byte-identical to before routing existed.
1028        match self.namespace_routing() {
1029            Some(routing) => minter.with_routing(routing),
1030            None => minter,
1031        }
1032    }
1033
1034    /// The namespace-mint routing context for this boot, or `None` when there is
1035    /// nothing to route to.
1036    ///
1037    /// Present only when ALL THREE handles exist: the distributed store (which
1038    /// hashes a namespace to its registry shard), the R-2 shard directory (which
1039    /// resolves that shard's current owner), and the R-3 request forwarder (which
1040    /// dials it). Those three are populated together by `build_routing_state` on
1041    /// a `[store.cluster]` boot and are all `None` otherwise, so a partial
1042    /// context can never arise — but each is checked rather than assumed.
1043    #[cfg(feature = "haematite-backend")]
1044    #[must_use]
1045    pub fn namespace_routing(&self) -> Option<crate::namespace::NamespaceRouting> {
1046        build_namespace_routing(
1047            self.cluster_store(),
1048            self.shard_directory(),
1049            self.request_forwarder(),
1050        )
1051    }
1052
1053    /// No cluster backend compiled in: nothing to route a mint to.
1054    #[cfg(not(feature = "haematite-backend"))]
1055    #[must_use]
1056    pub const fn namespace_routing(&self) -> Option<crate::namespace::NamespaceRouting> {
1057        None
1058    }
1059
1060    /// Clone the advisory outbox wake (LSUB-2) shared with the engine's stage
1061    /// seam. The outbox dispatcher installs this handle so a committed fan-out row
1062    /// wakes its run loop in ~RTT rather than waiting for the next poll tick. The
1063    /// handle is always present; it is simply never pulsed when the outbox is not
1064    /// commissioned, so wiring it is free and behaviour is unchanged.
1065    #[must_use]
1066    pub fn outbox_wake(&self) -> Arc<tokio::sync::Notify> {
1067        Arc::clone(&self.inner.outbox_wake)
1068    }
1069
1070    /// Whether this server is a node in a distributed haematite cluster.
1071    ///
1072    /// `true` when boot constructed the distributed backend (a `[store.cluster]`
1073    /// section was present) and is holding its inbound-write responder alive;
1074    /// `false` for every single-node / non-haematite boot.
1075    #[cfg(feature = "haematite-backend")]
1076    #[must_use]
1077    pub fn is_clustered(&self) -> bool {
1078        self.inner.cluster_responder.is_some()
1079    }
1080
1081    /// The concrete distributed haematite store the request-routing edge consults
1082    /// for shard ownership (`shard_for_workflow` / `owns_workflow_shard`) and
1083    /// unsteered-start remint. `None` for every single-node / non-clustered boot,
1084    /// so the routing pre-step is a no-op and the default path is unchanged.
1085    #[cfg(feature = "haematite-backend")]
1086    #[must_use]
1087    pub fn cluster_store(&self) -> Option<&Arc<aion_store_haematite::HaematiteStore>> {
1088        self.inner.cluster_store.as_ref()
1089    }
1090
1091    /// The request-routing shard directory (R-2) the edge consults to resolve a
1092    /// non-owned shard's owner. `None` for single-node / non-clustered boots, so
1093    /// the edge falls back to the bare R-1 ownership check.
1094    #[cfg(feature = "haematite-backend")]
1095    #[must_use]
1096    pub fn shard_directory(&self) -> Option<&Arc<crate::routing::StaticShardDirectory>> {
1097        self.inner.shard_directory.as_ref()
1098    }
1099
1100    /// The R-3 request forwarder used to relay a non-local signal/query/cancel to
1101    /// the shard owner. `None` for single-node / non-clustered boots.
1102    #[cfg(feature = "haematite-backend")]
1103    #[must_use]
1104    pub fn request_forwarder(&self) -> Option<&Arc<dyn crate::routing::RequestForwarder>> {
1105        self.inner.request_forwarder.as_ref()
1106    }
1107
1108    /// Spawn the worker heartbeat expiry sweeper (#176): the production driver
1109    /// of [`HeartbeatTracker::fail_expired_workers`], failing every worker with
1110    /// an in-flight task beyond the operator's `worker.heartbeat_window` and
1111    /// deregistering it with the provable
1112    /// [`WorkerDeathReason::Timeout`](aion_core::WorkerDeathReason::Timeout).
1113    ///
1114    /// Always spawned on the server boot path — dead-worker detection is a
1115    /// liveness correctness property, not an opt-in feature. The cadence is
1116    /// derived from the heartbeat window
1117    /// ([`sweep_interval`](crate::worker::sweep_interval): a quarter of the
1118    /// window clamped to `[1s, window]`, so the default 30s window sweeps every
1119    /// 7.5s); there is deliberately no separate config knob. The task exits
1120    /// when `shutdown` flips to `true`, exactly like the transports; the
1121    /// returned handle may be dropped to detach it (dropping a tokio
1122    /// `JoinHandle` never cancels the task) and is returned so tests can await
1123    /// clean shutdown.
1124    #[must_use]
1125    pub fn spawn_heartbeat_sweeper(
1126        &self,
1127        shutdown: tokio::sync::watch::Receiver<bool>,
1128    ) -> tokio::task::JoinHandle<()> {
1129        let sweeper = crate::worker::HeartbeatSweeper::new(
1130            self.inner.heartbeat_tracker.clone(),
1131            self.inner.worker_registry.clone(),
1132            self.inner.pending_activities.clone(),
1133            self.inner.drain_state.clone(),
1134            self.inner.runtime.worker.heartbeat_window,
1135        )
1136        // The SAME queue-service state the bridge parks dispatches into, so a
1137        // deregistration names how many dispatches are already stranded on the
1138        // queue the dead worker was serving.
1139        .with_queue_state(self.inner.queue_service_state.clone());
1140        tokio::spawn(sweeper.run(shutdown))
1141    }
1142
1143    /// Spawn the liminal connection dead-man switch (the liveness probe) over
1144    /// `notifier`.
1145    ///
1146    /// Always spawned on a boot that hosts the liminal worker listener:
1147    /// connection liveness is a correctness property of the transport, not an
1148    /// opt-in feature. Both timings derive from the operator's
1149    /// `worker.heartbeat_window` — see
1150    /// [`LivenessProbe`](crate::worker::LivenessProbe) — so there is no separate
1151    /// knob. The task exits when `shutdown` flips to `true`, exactly like the
1152    /// heartbeat sweeper and the transports.
1153    #[cfg(feature = "liminal-transport")]
1154    #[must_use]
1155    pub fn spawn_liminal_liveness_probe(
1156        &self,
1157        notifier: std::sync::Arc<crate::worker::LiminalConnectionNotifier>,
1158        shutdown: tokio::sync::watch::Receiver<bool>,
1159    ) -> tokio::task::JoinHandle<()> {
1160        let probe = crate::worker::LivenessProbe::across_transports(
1161            Some(notifier),
1162            // #197: the SAME probe covers gRPC-delivered workers. One probe,
1163            // one probation, one eligibility set — two probes would each
1164            // publish a whole verdict over the other's, because publication is
1165            // a replacement rather than a merge.
1166            Some(self.inner.grpc_liveness_waiters.clone()),
1167            self.inner.heartbeat_tracker.clone(),
1168            self.inner.worker_registry.clone(),
1169            self.inner.runtime.worker.heartbeat_window,
1170        );
1171        tokio::spawn(probe.run(shutdown))
1172    }
1173
1174    /// Spawn the SS-5b cluster supervisor: a background task that watches every
1175    /// declared peer's replication liveness and, on a confirmed peer death,
1176    /// calls `adopt_shards` for that peer's shards on THIS node's live engine —
1177    /// automatic failover with no manual trigger.
1178    ///
1179    /// Does nothing (returns `Ok(())` without spawning) unless this is a
1180    /// distributed boot whose cluster config declared at least one peer with
1181    /// `owned_shards`. A single-node / non-clustered server therefore never runs
1182    /// a supervisor, so default behaviour is unchanged.
1183    ///
1184    /// The spawned task drains on `shutdown` exactly like the transports.
1185    ///
1186    /// # Errors
1187    ///
1188    /// Returns [`ServerError`] when the engine handle cannot be resolved.
1189    #[cfg(feature = "haematite-backend")]
1190    pub fn spawn_cluster_supervisor(
1191        &self,
1192        config: crate::cluster::SupervisorConfig,
1193        shutdown: tokio::sync::watch::Receiver<bool>,
1194    ) -> Result<bool, ServerError> {
1195        let Some(cluster_store) = self.inner.cluster_store.clone() else {
1196            return Ok(false);
1197        };
1198        if self.inner.watched_peers.is_empty() {
1199            return Ok(false);
1200        }
1201        let engine = Arc::clone(self.inner.namespace_guard.resolver().engine()?);
1202        // WS3: feed cluster topology deltas from the supervisor's existing
1203        // decision points into the ops console channel. `self_node` is the
1204        // configured distribution name (already captured for the snapshot).
1205        let publisher = Arc::new(self.inner.cluster_publisher.clone());
1206        let self_node = self.inner.cluster_self_node.clone().unwrap_or_default();
1207        // #253: adoption re-runs the terminal-workflow outbox settlement sweep
1208        // over the widened owned-shard scope, so a dead peer's stranded row for
1209        // a terminal workflow is settled — never re-armed — by its adopter.
1210        // With no outbox commissioned there is nothing to settle and the
1211        // adopter delegates straight to the engine.
1212        let adopter = Arc::new(crate::cluster::OutboxSettlingAdopter::new(
1213            engine,
1214            self.inner.outbox_store.clone(),
1215        ));
1216        let supervisor = crate::cluster::ClusterSupervisor::new(
1217            cluster_store,
1218            adopter,
1219            self.inner.watched_peers.clone(),
1220            config,
1221        )
1222        .with_publisher(publisher, self_node);
1223        if !supervisor.watches_any() {
1224            return Ok(false);
1225        }
1226        tokio::spawn(supervisor.run(shutdown));
1227        Ok(true)
1228    }
1229
1230    /// Borrow the shared JWKS cache when authentication is enabled.
1231    #[cfg(feature = "auth")]
1232    #[must_use]
1233    pub fn jwks_cache(&self) -> Option<&JwksCache> {
1234        self.inner.jwks_cache.as_ref()
1235    }
1236
1237    /// Shut down the embedded engine so in-flight durable appends can finish.
1238    ///
1239    /// # Errors
1240    ///
1241    /// Returns [`ServerError`] if the namespace resolver has no engine handle or the engine rejects
1242    /// shutdown.
1243    pub fn shutdown(&self) -> Result<(), ServerError> {
1244        self.inner.namespace_guard.resolver().shutdown_engine()
1245    }
1246}
1247
1248#[cfg(feature = "auth")]
1249async fn build_jwks_cache(runtime: &RuntimeConfig) -> Result<Option<JwksCache>, ServerError> {
1250    if !runtime.auth.enabled {
1251        return Ok(None);
1252    }
1253    let Some(url) = runtime.auth.jwks_url.clone() else {
1254        return Err(ServerError::Config {
1255            message: "auth.jwks_url must not be empty when auth.enabled is true".to_owned(),
1256        });
1257    };
1258    let interval = std::time::Duration::from_secs(runtime.auth.jwks_refresh_seconds);
1259    let cache = JwksCache::new(url, interval)
1260        .await
1261        .map_err(|error| ServerError::Config {
1262            message: format!("auth jwks initial fetch failed: {error}"),
1263        })?;
1264    Ok(Some(cache))
1265}
1266
1267fn metrics_config_error(error: &MetricsError) -> ServerError {
1268    ServerError::Config {
1269        message: error.to_string(),
1270    }
1271}
1272
1273/// Borrowed inputs assembled into the embedded engine by [`build_engine`].
1274struct EngineAssembly<'a> {
1275    /// The metrics-instrumented store the engine writes through.
1276    instrumented_store: &'a Arc<InstrumentedEventStore>,
1277    /// Explicitly-sized broadcast channel capacity for `/events/stream`.
1278    event_broadcast_capacity: std::num::NonZeroUsize,
1279    /// Explicit workflow-query reply deadline for `/workflows/query`.
1280    query_timeout: std::time::Duration,
1281    /// The activity dispatcher (optionally dev-mock-decorated) the engine uses.
1282    activity_dispatcher: Arc<dyn ActivityDispatcher>,
1283    /// The shared active-workflow registry server dispatchers correlate against.
1284    active_registry: Arc<aion::Registry>,
1285    /// Whether THIS node seeds the schedule coordinator (SS-2 ownership gate).
1286    bootstrap_coordinator: bool,
1287    /// Non-secret runtime settings driving scheduler/outbox/package/shard knobs.
1288    runtime: &'a RuntimeConfig,
1289}
1290
1291/// The search-attribute schema every server-embedded engine runs with.
1292///
1293/// The engine REFUSES an append carrying an unregistered attribute, and this is
1294/// the only place the server registers any. So every attribute name a server
1295/// writer records must appear here or the write it rides on fails outright — a
1296/// missing `display_name` registration does not lose the label, it fails every
1297/// named start (#211). Kept as its own function so that coupling is testable
1298/// against the actual writers rather than only observable at boot.
1299fn server_search_attribute_schema() -> Result<aion_core::SearchAttributeSchema, ServerError> {
1300    let mut schema = aion_core::SearchAttributeSchema::new();
1301    for (name, label) in [
1302        (crate::namespace::NAMESPACE_ATTRIBUTE, "namespace"),
1303        (crate::namespace::TASK_QUEUE_ATTRIBUTE, "task_queue"),
1304        (crate::namespace::DISPLAY_NAME_ATTRIBUTE, "display_name"),
1305    ] {
1306        schema
1307            .register(name, aion_core::SearchAttributeType::String)
1308            .map_err(|error| ServerError::Config {
1309                message: format!("failed to register {label} search attribute: {error}"),
1310            })?;
1311    }
1312    Ok(schema)
1313}
1314
1315/// Assemble the embedded engine from the server's runtime configuration.
1316///
1317/// Factored out of [`ServerState::build_with_connected_store`] to keep that
1318/// method within length bounds; it carries the SS-2 wiring — the coordinator
1319/// bootstrap gate fed from real ownership and the `owned_shards` hook that drives
1320/// both scoping and the per-shard election before recovery.
1321async fn build_engine(assembly: EngineAssembly<'_>) -> Result<aion::Engine, ServerError> {
1322    let search_attribute_schema = server_search_attribute_schema()?;
1323    let runtime = assembly.runtime;
1324    let builder = EngineBuilder::new()
1325        .store_arc(assembly.instrumented_store.clone())
1326        .event_streaming(assembly.event_broadcast_capacity)
1327        .in_memory_visibility()
1328        .search_attribute_schema(search_attribute_schema)
1329        .scheduler_threads(runtime.scheduler_threads)
1330        .outbox_enabled(runtime.outbox.enabled)
1331        .activity_dispatcher(assembly.activity_dispatcher)
1332        .active_registry(assembly.active_registry)
1333        .production_recovery_seam()
1334        .signal_router_factory(|runtime: Arc<RuntimeHandle>, handoff| {
1335            Arc::new(ConcreteSignalRouter::new(runtime, handoff)) as Arc<dyn SignalRouter>
1336        })
1337        .query_timeout(assembly.query_timeout)
1338        // SS-2: only the node owning the schedule-coordinator's shard seeds and
1339        // serves it. `true` for every non-distributed boot (owns all shards); a
1340        // distributed non-owner passes `false` so it does not fence the
1341        // coordinator stream (AA-4-4). Default `true`, so a single-node boot is
1342        // byte-identical to today.
1343        .bootstrap_schedule_coordinator(assembly.bootstrap_coordinator)
1344        .load_workflow_sources(runtime.workflow_packages.iter().map(PathBuf::as_path));
1345    // Owned-shard assignment: when the operator pins this node to a shard subset,
1346    // scope the engine to it AND (SS-2) elect those shards before recovery — the
1347    // builder's `owned_shards` hook drives both. Empty (the default) leaves the
1348    // builder untouched, so single-node boot owns ALL shards, elects nothing, and
1349    // is byte-identical to today.
1350    let builder = if runtime.owned_shards.is_empty() {
1351        builder
1352    } else {
1353        builder.owned_shards(runtime.owned_shards.iter().copied())
1354    };
1355    builder.build().await.map_err(ServerError::from)
1356}
1357
1358/// Validate the two engine seams the server unconditionally mounts: the event
1359/// broadcast channel capacity (`/events/stream`) and the query reply deadline
1360/// (`/workflows/query`). Both are explicit-no-default — a mounted-but-
1361/// unconfigured surface is never acceptable.
1362fn required_engine_seams(
1363    runtime: &RuntimeConfig,
1364) -> Result<(std::num::NonZeroUsize, std::time::Duration), ServerError> {
1365    let event_broadcast_capacity = runtime
1366        .websocket
1367        .event_broadcast_capacity
1368        .and_then(std::num::NonZeroUsize::new)
1369        .ok_or_else(|| ServerError::Config {
1370            message: crate::config::EVENT_BROADCAST_CAPACITY_REQUIRED.to_owned(),
1371        })?;
1372    let query_timeout = runtime
1373        .query_timeout
1374        .filter(|timeout| !timeout.is_zero())
1375        .ok_or_else(|| ServerError::Config {
1376            message: crate::config::QUERY_TIMEOUT_REQUIRED.to_owned(),
1377        })?;
1378    Ok((event_broadcast_capacity, query_timeout))
1379}
1380
1381/// Install the outbox delivery callback when the durable outbox is commissioned.
1382///
1383/// Routes unmatched worker completions arriving at the sink into the live
1384/// workflow's mailbox. Flag-off, no callback is installed and the sink's
1385/// unmatched branch stays a silent drop. The dispatcher is not rebuilt — it
1386/// shares this exact pending tracker.
1387fn install_outbox_delivery(
1388    pending_activities: &PendingActivities,
1389    engine: &Arc<aion::Engine>,
1390    outbox_enabled: bool,
1391) {
1392    if outbox_enabled {
1393        let callback = Arc::new(crate::worker::ServerOutboxDeliveryCallback::new(
1394            Arc::clone(engine),
1395        ));
1396        pending_activities.set_outbox_delivery(callback);
1397    }
1398}
1399
1400/// The per-boot worker-side seams every dispatch path shares.
1401struct WorkerSeams {
1402    worker_registry: ConnectedWorkerRegistry,
1403    pending_activities: PendingActivities,
1404    heartbeat_tracker: HeartbeatTracker,
1405    drain_state: DrainState,
1406    queue_declarations: crate::worker::QueueDeclarationSource,
1407    queue_service_state: crate::worker::QueueServiceState,
1408    declared_bodies: crate::worker::DeclaredBodySource,
1409}
1410
1411/// Build the worker-side seams: the connected-worker registry (WS3 topology
1412/// deltas + the Control-Plane Phase 1 mint hook), the shared completion tracker,
1413/// worker liveness, the drain gate, and the two R1 queue-service handles the
1414/// bridge publishes into and the state reads from.
1415fn build_worker_seams(
1416    runtime: &RuntimeConfig,
1417    cluster_publisher: &crate::cluster_publisher::ClusterEventPublisher,
1418    namespace_store: &Arc<dyn NamespaceStore>,
1419    worker_deployment_store: &Arc<dyn WorkerDeploymentStore>,
1420    mint_routing: Option<crate::namespace::NamespaceRouting>,
1421) -> WorkerSeams {
1422    let worker_registry = ConnectedWorkerRegistry::default()
1423        .with_cluster_publisher(cluster_publisher.clone())
1424        .with_namespace_minting(namespace_store.clone(), runtime.auto_create)
1425        .with_worker_deployment_store(worker_deployment_store.clone());
1426    // The worker-registration mint is the OTHER `mint_or_gate` choke-point, so
1427    // it gets the same routing the start seams get: a worker registering for a
1428    // namespace whose registry shard this node does not own must not be refused
1429    // `NotOwner` forever either. `None` off-cluster leaves the registry
1430    // byte-identical.
1431    let worker_registry = match mint_routing {
1432        Some(routing) => worker_registry.with_namespace_routing(routing),
1433        None => worker_registry,
1434    };
1435    WorkerSeams {
1436        worker_registry,
1437        // The transport-loss budget derives from the operator's heartbeat
1438        // window (the one value declaring what silence means), so a worker that
1439        // keeps dying under an activity is re-dispatched attempt-neutrally for
1440        // a bounded span and then terminates naming the TRANSPORT.
1441        pending_activities: PendingActivities::default()
1442            .with_heartbeat_window(runtime.worker.heartbeat_window),
1443        heartbeat_tracker: HeartbeatTracker::new(runtime.worker.heartbeat_window),
1444        drain_state: DrainState::default(),
1445        queue_declarations: crate::worker::QueueDeclarationSource::default(),
1446        queue_service_state: crate::worker::QueueServiceState::default(),
1447        declared_bodies: crate::worker::DeclaredBodySource::default(),
1448    }
1449}
1450
1451/// Point the bridge's R1 classifier at the engine's live workflow catalog.
1452///
1453/// Installed only after the engine exists (the dispatcher is built before it),
1454/// exactly like the outbox delivery callback: the dispatcher is not rebuilt, it
1455/// shares this handle. Until this runs — and on any state built without an
1456/// engine — the classifier answers `Unknown`, which never refuses anything.
1457fn install_queue_declarations(
1458    queue_declarations: &crate::worker::QueueDeclarationSource,
1459    engine: &Arc<aion::Engine>,
1460) {
1461    queue_declarations.install(Arc::new(crate::worker::EngineQueueDeclarations::new(
1462        Arc::clone(engine),
1463    )));
1464}
1465
1466/// Point the declared-body executor at the engine's live workflow catalog.
1467///
1468/// Installed only after the engine exists, exactly like the queue-declaration
1469/// reader above: the dispatcher already holds a clone of this handle. Until
1470/// this runs, every lookup answers `None` and every dispatch takes the worker
1471/// path — nothing is deployed before the engine exists, so no declared body
1472/// can be missed.
1473fn install_declared_bodies(
1474    declared_bodies: &crate::worker::DeclaredBodySource,
1475    engine: &Arc<aion::Engine>,
1476) {
1477    declared_bodies.install(Arc::new(crate::worker::EngineDeclaredBodies::new(
1478        Arc::clone(engine),
1479    )));
1480}
1481
1482/// Hand every engine-backed seam its reader once the engine exists.
1483///
1484/// One boot-path step for the three handles built before the engine and
1485/// filled in after it: outbox completion delivery, the R1 queue-declaration
1486/// classifier, and the declared-body executor.
1487fn install_engine_backed_seams(seams: &WorkerSeams, engine: &Arc<aion::Engine>, outbox: bool) {
1488    install_outbox_delivery(&seams.pending_activities, engine, outbox);
1489    install_queue_declarations(&seams.queue_declarations, engine);
1490    install_declared_bodies(&seams.declared_bodies, engine);
1491}
1492
1493/// Decorate the worker activity dispatcher with the per-run activity-mock layer
1494/// when the dev surface is commissioned, returning the dispatcher and the shared
1495/// mock registry (if any).
1496///
1497/// Dark by default: with the dev surface off the engine gets the bare production
1498/// dispatcher and there is no mocking path at all (CN4).
1499/// Compose the engine-seam bridge dispatcher over the state's shared parts.
1500///
1501/// Also mints the NOI-6 attempt→owner index and returns it alongside: the
1502/// bridge binds each liminal-delivered attempt into it for the dispatch's
1503/// lifetime, and the state stores the SAME instance for the intervention
1504/// router to read, so the ops console can enumerate and target live attempts.
1505fn build_bridge_dispatcher(
1506    runtime: &RuntimeConfig,
1507    seams: &WorkerSeams,
1508) -> (WorkerActivityDispatcher, crate::worker::AttemptOwnerIndex) {
1509    let attempt_owners = crate::worker::AttemptOwnerIndex::new();
1510    let dispatcher = WorkerActivityDispatcher::new(
1511        seams.worker_registry.clone(),
1512        runtime.default_namespace.clone(),
1513        seams.heartbeat_tracker.clone(),
1514    )
1515    .with_pending(seams.pending_activities.clone())
1516    .with_drain_state(seams.drain_state.clone())
1517    .with_tokio_handle(tokio::runtime::Handle::current())
1518    .with_attempt_owners(attempt_owners.clone())
1519    .with_queue_service(runtime.worker.queue_service.clone())
1520    .with_queue_declarations(seams.queue_declarations.clone())
1521    .with_queue_state(seams.queue_service_state.clone());
1522    (dispatcher, attempt_owners)
1523}
1524
1525/// Build the process metrics, the LSUB-2 advisory outbox wake, and the
1526/// instrumented event store wired to pulse it — the storage-side trio the
1527/// full boot path assembles before the engine exists.
1528///
1529/// The wake is one process-wide `Notify` shared by the engine's stage seam
1530/// and the outbox dispatcher. A single handle is correct here because there
1531/// is exactly one in-process dispatcher that sweeps all owned shards per tick
1532/// — a wake just means "something was staged; sweep".
1533///
1534/// # Errors
1535///
1536/// Returns [`ServerError`] when the metrics registry cannot be constructed.
1537fn build_instrumented_store(
1538    runtime: &RuntimeConfig,
1539    event_store: Arc<dyn EventStore>,
1540) -> Result<
1541    (
1542        Metrics,
1543        Arc<tokio::sync::Notify>,
1544        Arc<InstrumentedEventStore>,
1545    ),
1546    ServerError,
1547> {
1548    let metrics = Metrics::new().map_err(|error| metrics_config_error(&error))?;
1549    let outbox_wake = Arc::new(tokio::sync::Notify::new());
1550    let instrumented_store = Arc::new(
1551        InstrumentedEventStore::new(
1552            event_store,
1553            metrics.clone(),
1554            runtime.default_namespace.clone(),
1555        )
1556        .with_outbox_wake(Arc::clone(&outbox_wake)),
1557    );
1558    Ok((metrics, outbox_wake, instrumented_store))
1559}
1560
1561/// Build the production bridge dispatcher and its decoration stack in one
1562/// step: declared-body execution always, the update-check observer above it,
1563/// the dev mock when commissioned. Also mints the update-status slot the
1564/// observer writes, returned so the state serves the same slot.
1565fn build_decorated_dispatcher(
1566    runtime: &RuntimeConfig,
1567    seams: &WorkerSeams,
1568    transcript: crate::activity_publisher::ActivityEventPublisher,
1569) -> (
1570    Arc<dyn ActivityDispatcher>,
1571    Option<ActivityMockRegistry>,
1572    crate::worker::AttemptOwnerIndex,
1573    crate::worker::WorkspaceRoot,
1574    crate::update_check::UpdateStatusState,
1575) {
1576    // #139: THE one resolution of the declared-body workspace root. The
1577    // dispatcher expands `{workspace_root}` with it and the returned value is
1578    // what the state exposes for the startup banner; nothing else derives it.
1579    let workspace_root = crate::worker::WorkspaceRoot::resolve();
1580    // #189 slice one: the update-status slot is created beside the observer
1581    // that writes it and returned so the state serves the SAME slot.
1582    let update_status = crate::update_check::UpdateStatusState::default();
1583    let (dispatcher, attempt_owners) = build_bridge_dispatcher(runtime, seams);
1584    let (activity_dispatcher, activity_mock_registry) = decorate_activity_dispatcher(
1585        dispatcher,
1586        seams.declared_bodies.clone(),
1587        workspace_root.clone(),
1588        transcript,
1589        update_status.clone(),
1590        runtime.dev.enabled,
1591    );
1592    (
1593        activity_dispatcher,
1594        activity_mock_registry,
1595        attempt_owners,
1596        workspace_root,
1597        update_status,
1598    )
1599}
1600
1601fn decorate_activity_dispatcher(
1602    dispatcher: WorkerActivityDispatcher,
1603    declared_bodies: crate::worker::DeclaredBodySource,
1604    workspace_root: crate::worker::WorkspaceRoot,
1605    transcript: crate::activity_publisher::ActivityEventPublisher,
1606    update_status: crate::update_check::UpdateStatusState,
1607    dev_enabled: bool,
1608) -> (Arc<dyn ActivityDispatcher>, Option<ActivityMockRegistry>) {
1609    // The declared-body layer wraps the production dispatcher UNCONDITIONALLY:
1610    // an action whose deployed contract declares a body executes at the
1611    // server, everything else falls through to the worker path untouched. The
1612    // update-check observer wraps THAT layer so it sees each completed
1613    // declared execution's result (it records completed checks and touches
1614    // nothing else). The dev mock (when commissioned) stays outermost so a
1615    // mocked activity short-circuits before either real execution path.
1616    let declared = crate::worker::DeclaredCommandDispatcher::new(
1617        Arc::new(dispatcher),
1618        declared_bodies.clone(),
1619        tokio::runtime::Handle::current(),
1620        workspace_root,
1621        transcript,
1622    );
1623    let observed = crate::update_check::UpdateCheckObserver::new(
1624        Arc::new(declared),
1625        declared_bodies,
1626        update_status,
1627    );
1628    if dev_enabled {
1629        let registry = ActivityMockRegistry::new();
1630        let decorated = DevMockingDispatcher::new(Arc::new(observed), registry.clone());
1631        (Arc::new(decorated), Some(registry))
1632    } else {
1633        (Arc::new(observed), None)
1634    }
1635}
1636
1637/// Validate the WS3 cluster broadcast capacity the server unconditionally mounts
1638/// (the `cluster` subscription on `/events/stream`). Explicit-no-default with the
1639/// same non-zero startup guard as the workflow event channel: the lag contract
1640/// has no buffer to lag against unless sized.
1641fn required_cluster_broadcast_capacity(
1642    runtime: &RuntimeConfig,
1643) -> Result<std::num::NonZeroUsize, ServerError> {
1644    runtime
1645        .websocket
1646        .cluster_broadcast_capacity
1647        .and_then(std::num::NonZeroUsize::new)
1648        .ok_or_else(|| ServerError::Config {
1649            message: crate::config::CLUSTER_BROADCAST_CAPACITY_REQUIRED.to_owned(),
1650        })
1651}
1652
1653/// Build the deployment-wide real-time publishers the server mounts on every
1654/// boot — the WS3 cluster topology channel and the NOI-5b agent-observability
1655/// transcript channel — from the validated `websocket.cluster_broadcast_capacity`.
1656///
1657/// The transcript sequencer runs over `observability_store` (the durable
1658/// `O`-keyspace impl on a haematite boot) or an in-memory impl when the backend
1659/// has none — see [`build_transcript_publisher`].
1660///
1661/// # Errors
1662///
1663/// Returns [`ServerError`] when `websocket.cluster_broadcast_capacity` is unset
1664/// or zero (the same explicit-no-default guard the cluster channel already had).
1665fn build_real_time_publishers(
1666    runtime: &RuntimeConfig,
1667    observability_store: Option<Arc<dyn aion_store::ObservabilityStore>>,
1668) -> Result<
1669    (
1670        crate::cluster_publisher::ClusterEventPublisher,
1671        crate::activity_publisher::ActivityEventPublisher,
1672    ),
1673    ServerError,
1674> {
1675    let capacity = required_cluster_broadcast_capacity(runtime)?;
1676    Ok((
1677        crate::cluster_publisher::ClusterEventPublisher::new(capacity),
1678        build_transcript_publisher(observability_store, capacity, transcript_bounds(runtime)),
1679    ))
1680}
1681
1682/// The operator-configured transcript retention bounds from `[observability]`.
1683fn transcript_bounds(runtime: &RuntimeConfig) -> crate::activity_bounds::TranscriptBounds {
1684    crate::activity_bounds::TranscriptBounds {
1685        max_event_bytes: runtime.observability.max_event_bytes,
1686        max_stream_events: runtime.observability.max_stream_events,
1687    }
1688}
1689
1690/// Build the NOI-5b transcript sequencer over `observability_store` (the durable
1691/// `O`-keyspace impl when the backend has one, an in-memory impl otherwise) with
1692/// a live-tail buffer of `capacity` and the `[observability]` retention bounds.
1693///
1694/// The publisher is ALWAYS constructed (the transcript channel is served on every
1695/// boot); only the durability of the backing store varies by backend. A backend
1696/// with no `O` keyspace (libSQL / in-memory) gets the in-memory
1697/// [`InMemoryObservabilityStore`](aion_store::InMemoryObservabilityStore), so the
1698/// live-tail + resume path behaves identically and only cross-restart durability
1699/// differs — exactly the "keep the no-observability path uniform" contract.
1700fn build_transcript_publisher(
1701    observability_store: Option<Arc<dyn aion_store::ObservabilityStore>>,
1702    capacity: std::num::NonZeroUsize,
1703    bounds: crate::activity_bounds::TranscriptBounds,
1704) -> crate::activity_publisher::ActivityEventPublisher {
1705    let store = observability_store
1706        .unwrap_or_else(|| Arc::new(aion_store::InMemoryObservabilityStore::default()));
1707    crate::activity_publisher::ActivityEventPublisher::new(store, capacity).with_bounds(bounds)
1708}
1709
1710/// The request-routing pieces built from the cluster store + peer config.
1711#[cfg(feature = "haematite-backend")]
1712struct RoutingState {
1713    shard_directory: Option<Arc<crate::routing::StaticShardDirectory>>,
1714    request_forwarder: Option<Arc<dyn crate::routing::RequestForwarder>>,
1715    /// The namespace-mint routing context assembled from the two handles above
1716    /// plus the cluster store, so the boot path can thread it into the worker
1717    /// registry's minter (the second of the two minter construction sites)
1718    /// without re-deriving it.
1719    mint_routing: Option<crate::namespace::NamespaceRouting>,
1720}
1721
1722/// Build the R-2 shard directory and R-3 request forwarder over the cluster
1723/// store and static peer config, or all-`None` when this is not a distributed
1724/// boot (no cluster store) so the routing edge is a no-op (default path).
1725#[cfg(feature = "haematite-backend")]
1726fn build_routing_state(
1727    cluster_store: Option<&Arc<aion_store_haematite::HaematiteStore>>,
1728    directory_peers: Vec<crate::routing::DirectoryPeer>,
1729    self_node_id: Option<String>,
1730) -> RoutingState {
1731    let Some(store) = cluster_store else {
1732        return RoutingState {
1733            shard_directory: None,
1734            request_forwarder: None,
1735            mint_routing: None,
1736        };
1737    };
1738    let shard_directory = Arc::new(crate::routing::StaticShardDirectory::new(
1739        Arc::clone(store),
1740        directory_peers,
1741        self_node_id,
1742    ));
1743    let request_forwarder: Arc<dyn crate::routing::RequestForwarder> =
1744        Arc::new(crate::routing::GrpcRequestForwarder::new());
1745    let mint_routing = build_namespace_routing(
1746        Some(store),
1747        Some(&shard_directory),
1748        Some(&request_forwarder),
1749    );
1750    RoutingState {
1751        shard_directory: Some(shard_directory),
1752        request_forwarder: Some(request_forwarder),
1753        mint_routing,
1754    }
1755}
1756
1757/// Assemble the namespace-mint routing context from the three handles a
1758/// distributed boot produces, or `None` when any is absent.
1759///
1760/// The single place the context is built, shared by the boot path (which wires
1761/// it into the worker registry's minter before the state exists) and
1762/// [`ServerState::namespace_routing`] (which serves the per-request minters).
1763/// Each handle is checked rather than assumed present: `build_routing_state`
1764/// populates them together, but a partial context would silently route mints to
1765/// nowhere.
1766#[cfg(feature = "haematite-backend")]
1767fn build_namespace_routing(
1768    cluster_store: Option<&Arc<aion_store_haematite::HaematiteStore>>,
1769    shard_directory: Option<&Arc<crate::routing::StaticShardDirectory>>,
1770    request_forwarder: Option<&Arc<dyn crate::routing::RequestForwarder>>,
1771) -> Option<crate::namespace::NamespaceRouting> {
1772    use crate::namespace::{
1773        GrpcMintForwarder, MintForwarder, MintShardOwners, NamespaceRouting, NamespaceShardResolver,
1774    };
1775    let store = Arc::clone(cluster_store?);
1776    let directory = Arc::clone(shard_directory?);
1777    let shards: Arc<dyn NamespaceShardResolver> = store;
1778    let owners: Arc<dyn MintShardOwners> = directory;
1779    let forwarder: Arc<dyn MintForwarder> =
1780        Arc::new(GrpcMintForwarder::new(Arc::clone(request_forwarder?)));
1781    Some(NamespaceRouting::new(shards, owners, forwarder))
1782}
1783
1784/// A connected durable store plus the lifecycle pieces the boot path needs.
1785///
1786/// `outbox_store` is the SAME leaf store cast as an [`OutboxStore`] for backends
1787/// with a durable outbox table (libSQL, haematite); the in-memory backend yields
1788/// `None`. `bootstrap_coordinator` gates the schedule-coordinator seed on real
1789/// ownership (SS-2 / AA-4-4): `true` for every non-distributed boot (single-node
1790/// owns the coordinator's shard), and for a distributed node only when it owns
1791/// that shard. `cluster_responder` owns the distributed inbound-write responder
1792/// thread, kept alive for the server's lifetime; `None` for non-distributed boots.
1793struct ConnectedStore {
1794    event_store: Arc<dyn EventStore>,
1795    outbox_store: Option<Arc<dyn OutboxStore>>,
1796    /// The SAME concrete leaf store as `event_store`, captured as a
1797    /// [`NamespaceStore`] before the decorator chain wraps it (the decorators
1798    /// are `NamespaceStore`-unaware). The control plane mints and lists through
1799    /// this handle. Every backend populates it: haematite supplies the
1800    /// quorum-replicated implementation, libSQL and in-memory the local-only
1801    /// one.
1802    namespace_store: Arc<dyn NamespaceStore>,
1803    /// Same concrete leaf captured as the deployment-store contract.
1804    worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
1805    /// NOI-5b: the SAME concrete leaf store captured as an
1806    /// [`ObservabilityStore`](aion_store::ObservabilityStore) when the backend
1807    /// implements the durable `O` keyspace (haematite). `None` for backends with
1808    /// no `O` keyspace (libSQL / in-memory), where the transcript sequencer runs
1809    /// over an in-memory impl instead. Captured before the leaf is wrapped in the
1810    /// (`ObservabilityStore`-unaware) decorator chain, exactly like
1811    /// `namespace_store`.
1812    observability_store: Option<Arc<dyn aion_store::ObservabilityStore>>,
1813    bootstrap_coordinator: bool,
1814    #[cfg(feature = "haematite-backend")]
1815    cluster_responder: Option<aion_store_haematite::ClusterResponder>,
1816    /// The concrete distributed haematite store (the SAME leaf as `event_store`),
1817    /// retained for the SS-5b cluster supervisor's peer-liveness polling. `None`
1818    /// for every non-distributed boot.
1819    #[cfg(feature = "haematite-backend")]
1820    cluster_store: Option<Arc<aion_store_haematite::HaematiteStore>>,
1821    /// The peers the SS-5b supervisor watches, each with the shards this node
1822    /// adopts on its death. Empty for non-distributed boots.
1823    #[cfg(feature = "haematite-backend")]
1824    watched_peers: Vec<crate::cluster::WatchedPeer>,
1825    /// The static shard-directory peer entries (name + declared shards + gRPC
1826    /// forward address) used to build the request-routing directory (R-2). Empty
1827    /// for non-distributed boots.
1828    #[cfg(feature = "haematite-backend")]
1829    directory_peers: Vec<crate::routing::DirectoryPeer>,
1830    /// This node's own distribution name (cluster `node_id`), so the SS-3
1831    /// directory can resolve a shard-owner record naming THIS node to `Local`.
1832    /// `None` for non-distributed boots.
1833    #[cfg(feature = "haematite-backend")]
1834    self_node_id: Option<String>,
1835}
1836
1837impl ConnectedStore {
1838    /// A non-distributed connected store: owns the coordinator's shard (so it
1839    /// bootstraps the coordinator) and has no cluster responder.
1840    ///
1841    /// `namespace_store` is the SAME concrete leaf as `event_store`, captured as
1842    /// a [`NamespaceStore`] by the caller (where the concrete type is still
1843    /// known) before the decorator chain wraps the event store.
1844    fn local(
1845        event_store: Arc<dyn EventStore>,
1846        outbox_store: Option<Arc<dyn OutboxStore>>,
1847        namespace_store: Arc<dyn NamespaceStore>,
1848        worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
1849    ) -> Self {
1850        Self {
1851            event_store,
1852            outbox_store,
1853            namespace_store,
1854            worker_deployment_store,
1855            // A `local` connected store is the memory / libSQL / embedder path,
1856            // none of which implement the durable `O` keyspace: the transcript
1857            // sequencer falls back to an in-memory impl (NOI-5b).
1858            observability_store: None,
1859            bootstrap_coordinator: true,
1860            #[cfg(feature = "haematite-backend")]
1861            cluster_responder: None,
1862            #[cfg(feature = "haematite-backend")]
1863            cluster_store: None,
1864            #[cfg(feature = "haematite-backend")]
1865            watched_peers: Vec::new(),
1866            #[cfg(feature = "haematite-backend")]
1867            directory_peers: Vec::new(),
1868            #[cfg(feature = "haematite-backend")]
1869            self_node_id: None,
1870        }
1871    }
1872
1873    /// Capture this node's self-identity for the cluster snapshot before the
1874    /// distributed boot moves it into routing state.
1875    #[cfg(feature = "haematite-backend")]
1876    fn cluster_self_node(&self) -> Option<String> {
1877        self.self_node_id.clone()
1878    }
1879
1880    /// Non-distributed connected stores have no cluster self-identity.
1881    #[cfg(not(feature = "haematite-backend"))]
1882    const fn cluster_self_node(&self) -> Option<String> {
1883        None
1884    }
1885}
1886
1887/// Connect the durable store, yielding the engine's [`EventStore`] handle and,
1888/// for the libSQL backend, the SAME leaf store cast as an [`OutboxStore`].
1889///
1890/// Both handles are clones of one `Arc<LibSqlStore>`, which holds a single
1891/// `libsql::Connection`. Sharing that connection with the outbox dispatcher
1892/// serializes the engine's `append_with_outbox` and the dispatcher's
1893/// `claim_outbox_rows` writes, so the two never contend across separate
1894/// connections and never raise `SQLITE_BUSY`. The in-memory backend has no
1895/// outbox table, so it yields `None`.
1896async fn connect_store(config: StoreConfig) -> Result<ConnectedStore, ServerError> {
1897    match config.backend {
1898        StoreBackend::Memory => {
1899            // One leaf store, captured as both the engine's event store and the
1900            // namespace registry (in-memory backends have no outbox table).
1901            let leaf = Arc::new(aion_store::InMemoryStore::default());
1902            let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
1903            let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
1904            Ok(ConnectedStore::local(
1905                leaf,
1906                None,
1907                namespace_store,
1908                worker_deployment_store,
1909            ))
1910        }
1911        StoreBackend::LibSql => {
1912            #[cfg(feature = "libsql-backend")]
1913            {
1914                connect_libsql_store(config).await
1915            }
1916            #[cfg(not(feature = "libsql-backend"))]
1917            {
1918                let _ = config;
1919                connect_libsql_store_unavailable()
1920            }
1921        }
1922        StoreBackend::Haematite => {
1923            #[cfg(feature = "haematite-backend")]
1924            {
1925                connect_haematite_store(config).await
1926            }
1927            #[cfg(not(feature = "haematite-backend"))]
1928            {
1929                let _ = config;
1930                connect_haematite_store_unavailable()
1931            }
1932        }
1933    }
1934}
1935
1936/// Connect the libSQL backend, opening the embedded database at `store.url` and
1937/// sharing the SAME leaf `Arc<LibSqlStore>` (one `libsql::Connection`) as both the
1938/// engine's [`EventStore`] and the dispatcher's [`OutboxStore`].
1939#[cfg(feature = "libsql-backend")]
1940async fn connect_libsql_store(config: StoreConfig) -> Result<ConnectedStore, ServerError> {
1941    let Some(url) = config.url else {
1942        return Err(ServerError::Config {
1943            message: "store.url must not be empty when store.backend is libsql".to_owned(),
1944        });
1945    };
1946    let store = LibSqlStore::open(url.clone())
1947        .await
1948        .map_err(ServerError::from)?;
1949    store
1950        .validate_event_compatibility()
1951        .await
1952        .map_err(|error| match error {
1953            aion_store::StoreError::Serialization(_) => ServerError::Config {
1954                message: format!(
1955                    "Database schema mismatch — delete {url} and restart, or run migrations."
1956                ),
1957            },
1958            other => ServerError::from(other),
1959        })?;
1960    let leaf = Arc::new(store);
1961    let event_store: Arc<dyn EventStore> = leaf.clone();
1962    let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
1963    let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
1964    let outbox_store: Arc<dyn OutboxStore> = leaf;
1965    Ok(ConnectedStore::local(
1966        event_store,
1967        Some(outbox_store),
1968        namespace_store,
1969        worker_deployment_store,
1970    ))
1971}
1972
1973/// Reject `backend = libsql` cleanly when the optional `libsql-backend` feature
1974/// is not compiled in, so a default (ablative-stack) build gives a precise
1975/// operator error instead of a silent fallthrough.
1976#[cfg(not(feature = "libsql-backend"))]
1977fn connect_libsql_store_unavailable() -> Result<ConnectedStore, ServerError> {
1978    Err(ServerError::Config {
1979        message: "store.backend = libsql requires the aion-server `libsql-backend` feature"
1980            .to_owned(),
1981    })
1982}
1983
1984/// Connect the haematite backend, opening the on-disk database if `store.data_dir`
1985/// already holds one and otherwise creating it with `store.shard_count` shards.
1986///
1987/// Without a `[store.cluster]` section this is the SINGLE-NODE path
1988/// ([`HaematiteStore::open`] / [`create_with_shard_count`]), byte-identical to
1989/// before: no endpoint, no election, owns everything, bootstraps the coordinator.
1990/// With a cluster section this is the DISTRIBUTED path
1991/// ([`HaematiteStore::open_or_create_distributed`]): it binds the replication
1992/// endpoint, builds the quorum membership, dials peers, starts the responder, and
1993/// computes whether THIS node owns the schedule-coordinator's shard so the engine
1994/// boot path seeds the coordinator on exactly one owner cluster-wide (SS-2).
1995///
1996/// The SAME leaf `Arc<HaematiteStore>` is shared as both the engine's
1997/// [`EventStore`] and the dispatcher's [`OutboxStore`] (one inner haematite
1998/// database), mirroring the libSQL backend.
1999///
2000/// [`HaematiteStore::open`]: aion_store_haematite::HaematiteStore::open
2001/// [`create_with_shard_count`]: aion_store_haematite::HaematiteStore::create_with_shard_count
2002/// [`HaematiteStore::open_or_create_distributed`]: aion_store_haematite::HaematiteStore::open_or_create_distributed
2003#[cfg(feature = "haematite-backend")]
2004async fn connect_haematite_store(config: StoreConfig) -> Result<ConnectedStore, ServerError> {
2005    let Some(data_dir) = config.data_dir else {
2006        return Err(ServerError::Config {
2007            message: "store.data_dir must not be empty when store.backend is haematite".to_owned(),
2008        });
2009    };
2010    let shard_count = config.shard_count;
2011    let owned_shards = config.owned_shards.clone();
2012    let cluster = config.cluster.clone();
2013    // The peers the SS-5b supervisor watches, captured before `cluster` is moved
2014    // into the blocking build. A peer with declared `owned_shards` becomes a
2015    // watch target; peers without are kept out of the watch set (the supervisor
2016    // would have nothing to adopt for them).
2017    let watched_peers: Vec<crate::cluster::WatchedPeer> = cluster
2018        .as_ref()
2019        .map(|cluster| {
2020            cluster
2021                .peers
2022                .iter()
2023                .map(|peer| crate::cluster::WatchedPeer {
2024                    name: peer.name.clone(),
2025                    owned_shards: peer.owned_shards.clone(),
2026                })
2027                .collect()
2028        })
2029        .unwrap_or_default();
2030    // The static shard-directory entries (R-2): each peer's declared shards plus
2031    // its gRPC forward address. Built from the same config the supervisor uses.
2032    let directory_peers: Vec<crate::routing::DirectoryPeer> = cluster
2033        .as_ref()
2034        .map(|cluster| {
2035            cluster
2036                .peers
2037                .iter()
2038                .map(|peer| crate::routing::DirectoryPeer {
2039                    name: peer.name.clone(),
2040                    owned_shards: peer.owned_shards.clone(),
2041                    grpc_addr: peer.grpc_address,
2042                })
2043                .collect()
2044        })
2045        .unwrap_or_default();
2046    // This node's own distribution name, so the SS-3 directory resolves a
2047    // shard-owner record naming THIS node to `Local`.
2048    let self_node_id: Option<String> = cluster.as_ref().map(|cluster| cluster.node_id.clone());
2049    // Construction (and, for the distributed path, the off-runtime endpoint bind)
2050    // must not stall the async runtime, so run it on the blocking pool. The
2051    // distributed constructor itself steps onto a bare thread for the bind.
2052    let (store, responder) =
2053        tokio::task::spawn_blocking(move || build_haematite_store(&data_dir, shard_count, cluster))
2054            .await
2055            .map_err(|error| ServerError::Config {
2056                message: format!("haematite store initialization task failed: {error}"),
2057            })??;
2058
2059    // Gate the coordinator bootstrap on real ownership: a distributed node that
2060    // does NOT own the coordinator's shard must not seed/fence it (AA-4-4). A
2061    // single-node boot owns all shards, so it always bootstraps.
2062    let bootstrap_coordinator = if owned_shards.is_empty() {
2063        true
2064    } else {
2065        store.set_owned_shards(owned_shards.iter().copied());
2066        store.owns_workflow_shard(&aion::schedule_coordinator_workflow_id())
2067    };
2068
2069    let leaf = Arc::new(store);
2070    let event_store: Arc<dyn EventStore> = leaf.clone();
2071    let outbox_store: Arc<dyn OutboxStore> = leaf.clone();
2072    // The namespace registry is the SAME concrete `HaematiteStore` leaf (the
2073    // quorum-replicated implementation), captured before the leaf is moved into
2074    // the cluster-store retention below.
2075    let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
2076    let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
2077    // NOI-5b: the SAME concrete leaf captured as the durable `O`-keyspace
2078    // observability store, so the transcript sequencer persists to haematite and
2079    // survives restart/failover. Captured here (before the decorator chain wraps
2080    // the event store) exactly like the namespace registry.
2081    let observability_store: Arc<dyn aion_store::ObservabilityStore> = leaf.clone();
2082    // Retain the concrete store ONLY for a distributed boot (responder present),
2083    // where the SS-5b supervisor will poll it for peer liveness. A single-node
2084    // boot has no peers, so it carries no cluster store and never supervises.
2085    let cluster_store = responder.as_ref().map(|_| leaf);
2086    let (watched_peers, directory_peers, self_node_id) = if cluster_store.is_some() {
2087        (watched_peers, directory_peers, self_node_id)
2088    } else {
2089        (Vec::new(), Vec::new(), None)
2090    };
2091    Ok(ConnectedStore {
2092        event_store,
2093        outbox_store: Some(outbox_store),
2094        namespace_store,
2095        worker_deployment_store,
2096        observability_store: Some(observability_store),
2097        bootstrap_coordinator,
2098        cluster_responder: responder,
2099        cluster_store,
2100        watched_peers,
2101        directory_peers,
2102        self_node_id,
2103    })
2104}
2105
2106/// Build the haematite store: the distributed path when a cluster section is
2107/// present, otherwise the single-node path. Returns the store and (for the
2108/// distributed path) its inbound-write responder. Restart-safe: an existing
2109/// on-disk database is reused (its shard count wins) rather than re-created.
2110///
2111/// Linux/Android give Haematite a descriptor-authoritative `/proc/self/fd` path.
2112/// On path-ambient Unix targets such as macOS, startup instead resolves the held
2113/// descriptor's current path and refuses any ancestor owned by an unprivileged
2114/// principal other than the server euid or writable by group/world. That policy
2115/// prevents a second principal from renaming a parent after startup and replacing
2116/// the old name with a symlink that redirects Haematite's normal reads/commits.
2117/// Every shard is still eagerly materialized and the capability retained, but on
2118/// those targets neither action confines later pathname I/O. A descriptor-relative
2119/// Haematite constructor and backend I/O remain the long-term fix.
2120#[cfg(feature = "haematite-backend")]
2121fn build_haematite_store(
2122    data_dir: &str,
2123    shard_count: usize,
2124    cluster: Option<crate::config::ClusterConfig>,
2125) -> Result<
2126    (
2127        aion_store_haematite::HaematiteStore,
2128        Option<aion_store_haematite::ClusterResponder>,
2129    ),
2130    ServerError,
2131> {
2132    build_haematite_store_with_hook(data_dir, shard_count, cluster, || Ok(()))
2133}
2134
2135#[cfg(feature = "haematite-backend")]
2136fn build_haematite_store_with_hook(
2137    data_dir: &str,
2138    shard_count: usize,
2139    cluster: Option<crate::config::ClusterConfig>,
2140    before_backend_touch: impl FnOnce() -> Result<(), std::io::Error>,
2141) -> Result<
2142    (
2143        aion_store_haematite::HaematiteStore,
2144        Option<aion_store_haematite::ClusterResponder>,
2145    ),
2146    ServerError,
2147> {
2148    use aion_store_haematite::{ClusterBootstrap, HaematiteStore};
2149
2150    // Acquire the data root through the same no-follow component walk used by
2151    // authoring. New components are created 0700 on Unix, and an existing root
2152    // that the server's own user owns is tightened to 0700 rather than refused —
2153    // provisioning our own directory is Aion's job, not the operator's. Only a
2154    // root Aion cannot make safe (foreign owner, a filesystem without Unix
2155    // modes) is a loud startup failure here; an unsafe ANCESTOR is caught
2156    // separately below and is never repaired.
2157    let private_root = crate::filesystem::ConfinedDir::open_or_create(std::path::Path::new(
2158        data_dir,
2159    ))
2160    .map_err(|error| ServerError::Config {
2161        message: format!("unsafe store.data_dir `{data_dir}`: {error}"),
2162    })?;
2163
2164    // Haematite 0.5 creates shard directories lazily. Pre-create every configured
2165    // directory descriptor-relatively, then force the backend's actual shard
2166    // spawn/recovery path below while this checked-and-hardened window is held.
2167    for shard in 0..shard_count {
2168        private_root
2169            .create_dir_all(std::path::Path::new(&format!("shard-{shard}")))
2170            .map_err(|error| ServerError::Config {
2171                message: format!(
2172                    "failed to materialize shard-{shard} under store.data_dir `{data_dir}`: {error}"
2173                ),
2174            })?;
2175    }
2176    private_root
2177        .harden_tree()
2178        .map_err(|error| private_store_mode_error(data_dir, &error))?;
2179
2180    // Deterministic regression seam: the capability and shard directories exist,
2181    // but Haematite has not touched any path yet.
2182    before_backend_touch().map_err(|error| ServerError::Config {
2183        message: format!("store.data_dir pre-open hook failed: {error}"),
2184    })?;
2185
2186    #[cfg(unix)]
2187    let backend_path = private_root
2188        .backend_path()
2189        .map_err(|error| ServerError::Config {
2190            message: format!("failed to resolve held store.data_dir `{data_dir}`: {error}"),
2191        })?;
2192    #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
2193    crate::filesystem::validate_ambient_backend_ancestors(&backend_path).map_err(|error| {
2194        let (component, reason) = error.into_parts();
2195        ServerError::UnsafeDataRootAncestor {
2196            data_root: backend_path.clone(),
2197            component,
2198            reason,
2199        }
2200    })?;
2201    #[cfg(not(unix))]
2202    let backend_path = std::path::PathBuf::from(data_dir);
2203
2204    let Some(cluster) = cluster else {
2205        let store = if backend_path.join("config.json").exists() {
2206            HaematiteStore::open(&backend_path).map_err(ServerError::from)?
2207        } else {
2208            HaematiteStore::create_with_shard_count(&backend_path, shard_count)
2209                .map_err(ServerError::from)?
2210        };
2211        store.materialize_all_shards().map_err(ServerError::from)?;
2212        private_root
2213            .harden_tree()
2214            .map_err(|error| private_store_mode_error(data_dir, &error))?;
2215        let store = store.retain_data_root_capability(private_root);
2216        return Ok((store, None));
2217    };
2218
2219    let boot = ClusterBootstrap {
2220        node_id: cluster.node_id,
2221        bind_address: cluster.bind_address,
2222        members: cluster.members,
2223        peers: cluster
2224            .peers
2225            .into_iter()
2226            .map(|peer| (peer.name, peer.address))
2227            .collect(),
2228        timeout: HAEMATITE_CLUSTER_OP_TIMEOUT,
2229    };
2230    let (store, responder) =
2231        HaematiteStore::open_or_create_distributed(&backend_path, shard_count, boot)
2232            .map_err(ServerError::from)?;
2233    store.materialize_all_shards().map_err(ServerError::from)?;
2234    private_root
2235        .harden_tree()
2236        .map_err(|error| private_store_mode_error(data_dir, &error))?;
2237    let store = store.retain_data_root_capability(private_root);
2238    Ok((store, Some(responder)))
2239}
2240
2241#[cfg(feature = "haematite-backend")]
2242fn private_store_mode_error(data_dir: &str, error: &std::io::Error) -> ServerError {
2243    ServerError::Config {
2244        message: format!(
2245            "failed to apply private modes under store.data_dir `{data_dir}`: {error}"
2246        ),
2247    }
2248}
2249
2250/// Per-operation quorum/election timeout for the distributed haematite backend.
2251#[cfg(feature = "haematite-backend")]
2252const HAEMATITE_CLUSTER_OP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
2253
2254/// Reject `backend = haematite` cleanly when the optional `haematite-backend`
2255/// feature is not compiled in, so a default build gives a precise operator
2256/// error instead of a silent fallthrough.
2257#[cfg(not(feature = "haematite-backend"))]
2258fn connect_haematite_store_unavailable() -> Result<ConnectedStore, ServerError> {
2259    Err(ServerError::Config {
2260        message: "store.backend = haematite requires the aion-server `haematite-backend` feature"
2261            .to_owned(),
2262    })
2263}
2264
2265/// The NOI-6 intervention transport used when no push transport is compiled in.
2266///
2267/// Without the `liminal-transport` feature there is no way to reach a worker's
2268/// out-of-band connection, so every routed command reports the owning worker
2269/// unreachable — which the router maps onto the attempt-scoped stale-target no-op.
2270/// This keeps the intervention endpoint honest on a transport-less build (an
2271/// operator gets a NACK, never a false "applied") without gating the endpoint on a
2272/// feature.
2273#[cfg(not(feature = "liminal-transport"))]
2274#[derive(Clone, Debug)]
2275struct NullInterventionTransport;
2276
2277#[cfg(not(feature = "liminal-transport"))]
2278#[async_trait::async_trait]
2279impl crate::worker::InterventionTransport for NullInterventionTransport {
2280    async fn push(
2281        &self,
2282        _worker: &crate::worker::WorkerHandle,
2283        _command: aion_core::InterventionCommand,
2284    ) -> Result<aion_core::InterventionOutcome, ServerError> {
2285        Err(ServerError::worker_connection_lost(
2286            "intervention",
2287            "no intervention push transport is compiled in".to_owned(),
2288        ))
2289    }
2290}
2291
2292#[cfg(test)]
2293mod tests {
2294    use std::{net::SocketAddr, time::Duration};
2295
2296    use aion_store::InMemoryStore;
2297
2298    use super::ServerState;
2299    use crate::config::{
2300        AuthConfig, AuthoringConfig, DeployConfig, DevConfig, ListenConfig, MetricsConfig,
2301        NamespaceConfig, NamespaceMode, OpsConsoleAssetSource, OpsConsoleConfig, OutboxConfig,
2302        RuntimeConfig, WebSocketConfig, WorkerConfig,
2303    };
2304
2305    fn runtime_config() -> RuntimeConfig {
2306        RuntimeConfig {
2307            listen: ListenConfig {
2308                grpc: SocketAddr::from(([127, 0, 0, 1], 50051)),
2309                http: SocketAddr::from(([127, 0, 0, 1], 8080)),
2310            },
2311            tls: None,
2312            auth: AuthConfig {
2313                enabled: false,
2314                jwks_url: None,
2315                jwks_refresh_seconds: 300,
2316            },
2317            ops_console: OpsConsoleConfig {
2318                source: OpsConsoleAssetSource::Embedded,
2319            },
2320            namespace: NamespaceConfig {
2321                mode: NamespaceMode::SharedEngine,
2322            },
2323            worker: WorkerConfig {
2324                heartbeat_window: Duration::from_secs(30),
2325                ..WorkerConfig::default()
2326            },
2327            websocket: WebSocketConfig {
2328                outbound_buffer_bound: 32,
2329                event_broadcast_capacity: Some(64),
2330                cluster_broadcast_capacity: Some(64),
2331            },
2332            workflow_packages: Vec::new(),
2333            deploy: DeployConfig::default(),
2334            authoring: AuthoringConfig::default(),
2335            dev: DevConfig::default(),
2336            outbox: OutboxConfig::default(),
2337            observability: crate::config::ObservabilityConfig::default(),
2338            mcp: crate::config::ResolvedMcpConfig::default(),
2339            scheduler_threads: 1,
2340            query_timeout: Some(Duration::from_secs(10)),
2341            default_namespace: "default".to_owned(),
2342            auto_create: crate::config::AutoCreate::Open,
2343            max_in_flight_activities: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
2344            drain_timeout: Duration::from_secs(30),
2345            metrics: MetricsConfig { enabled: true },
2346            owned_shards: Vec::new(),
2347            cors_allowed_origins: Vec::new(),
2348        }
2349    }
2350
2351    /// The engine's schema must accept EVERY attribute the server's start
2352    /// writer actually records — the invariant, not an enumeration of names.
2353    ///
2354    /// The two sides are genuinely coupled at runtime: the recorder validates
2355    /// each attribute against this schema before appending, so an attribute the
2356    /// writer produces and the schema does not register fails the START, not
2357    /// just the label (#211). The map is taken from the production
2358    /// `start_search_attributes` with every optional field populated, so any
2359    /// future attribute the writer learns to record is covered here without
2360    /// this test being edited.
2361    #[test]
2362    fn engine_schema_accepts_every_attribute_the_start_writer_records()
2363    -> Result<(), Box<dyn std::error::Error>> {
2364        let schema = super::server_search_attribute_schema()?;
2365        let recorded = crate::api::handlers::workflows::start_search_attributes(
2366            "tenant-a",
2367            Some("gpu"),
2368            Some("Nightly settlement"),
2369        );
2370
2371        assert!(
2372            recorded.contains_key(crate::namespace::DISPLAY_NAME_ATTRIBUTE),
2373            "the fixture must exercise the display-name attribute, or this test \
2374             cannot see its registration go missing"
2375        );
2376        for (name, value) in &recorded {
2377            schema.validate(name, value).map_err(|error| {
2378                format!(
2379                    "the start writer records {name}, but the engine's schema refuses it: {error}"
2380                )
2381            })?;
2382        }
2383        Ok(())
2384    }
2385
2386    #[tokio::test]
2387    async fn builds_state_with_in_memory_store() -> Result<(), Box<dyn std::error::Error>> {
2388        let state =
2389            ServerState::build_with_store(InMemoryStore::default(), runtime_config()).await?;
2390
2391        std::hint::black_box(state.namespace_guard());
2392        std::hint::black_box(state.worker_registry());
2393
2394        Ok(())
2395    }
2396
2397    /// R1 surfacing: a real boot exposes the unserved-queue state, the bridge
2398    /// publishes parked dispatches into THAT instance, and the address leaves
2399    /// the state the moment the dispatch resolves.
2400    ///
2401    /// The dispatch is driven through a dispatcher built over the state's OWN
2402    /// registry, queue state, and engine-backed declaration source — the same
2403    /// three handles `build_bridge_dispatcher` hands the production bridge.
2404    #[tokio::test]
2405    async fn unserved_queues_surfaces_a_parked_dispatch_and_clears_it()
2406    -> Result<(), Box<dyn std::error::Error>> {
2407        use aion::{ActivityDispatch, ActivityDispatcher as _};
2408        use aion_core::{ActivityId, RunId, WorkflowId};
2409        use std::sync::Arc;
2410
2411        let state =
2412            ServerState::build_with_store(InMemoryStore::default(), runtime_config()).await?;
2413        assert!(
2414            state.unserved_queues()?.is_empty(),
2415            "a calm boot has no unserved queues"
2416        );
2417        // The engine-backed reader IS installed on a real boot; with no
2418        // queue-declaring package deployed it can contradict nothing, so it must
2419        // answer Unknown rather than manufacture a structural refusal.
2420        assert!(state.queue_declarations().is_installed());
2421        assert_eq!(
2422            state
2423                .queue_declarations()
2424                .declaration_for("nobody-serves-this"),
2425            crate::worker::QueueDeclaration::Unknown
2426        );
2427
2428        let dispatcher = Arc::new(
2429            crate::worker::WorkerActivityDispatcher::new(
2430                state.worker_registry().clone(),
2431                "default",
2432                crate::worker::HeartbeatTracker::new(Duration::from_secs(5)),
2433            )
2434            .with_queue_state(state.queue_service_state().clone())
2435            .with_queue_declarations(state.queue_declarations().clone()),
2436        );
2437        let workflow_id = WorkflowId::new_v4();
2438        let request = ActivityDispatch {
2439            namespace: "default".to_owned(),
2440            task_queue: "nobody-serves-this".to_owned(),
2441            node: None,
2442            workflow_id: workflow_id.clone(),
2443            run_id: RunId::new_v4(),
2444            activity_id: ActivityId::from_sequence_position(0),
2445            name: "greet".to_owned(),
2446            input: "{}".to_owned(),
2447            config: "{}".to_owned(),
2448            attempt: 1,
2449            advisory: false,
2450            labels: std::collections::BTreeMap::new(),
2451        };
2452        let parked = std::thread::spawn(move || dispatcher.dispatch(request));
2453
2454        let mut unserved = Vec::new();
2455        for _ in 0..30 {
2456            unserved = state.unserved_queues()?;
2457            if !unserved.is_empty() {
2458                break;
2459            }
2460            tokio::time::sleep(Duration::from_millis(100)).await;
2461        }
2462        assert_eq!(unserved.len(), 1, "the parked dispatch is not surfaced");
2463        assert_eq!(
2464            unserved[0].reason,
2465            crate::worker::QueueServiceReason::NoLivePollers,
2466            "an empty catalog must not be read as a structural refusal"
2467        );
2468        assert_eq!(unserved[0].key.task_queue, "nobody-serves-this");
2469        assert_eq!(unserved[0].waiting.len(), 1);
2470        assert_eq!(unserved[0].waiting[0].workflow_id, workflow_id);
2471
2472        // Release the dispatch: a worker arrives whose receiver is already gone.
2473        let (worker_tx, worker_rx) = tokio::sync::mpsc::channel(1);
2474        drop(worker_rx);
2475        let registration = state.worker_registry().register_namespaces(
2476            [String::from("default")],
2477            "nobody-serves-this",
2478            None,
2479            [String::from("greet")].iter(),
2480            worker_tx,
2481        )?;
2482        let outcome = parked.join().map_err(|_| "parked dispatch panicked")?;
2483        assert!(outcome.is_err(), "the released dispatch must resolve");
2484        assert!(
2485            state.unserved_queues()?.is_empty(),
2486            "a resolved dispatch must leave the unserved state"
2487        );
2488        registration.deregister()?;
2489        Ok(())
2490    }
2491
2492    #[tokio::test]
2493    async fn namespace_store_is_reachable_and_functional_after_default_boot()
2494    -> Result<(), Box<dyn std::error::Error>> {
2495        use aion_store::{MintOutcome, NamespaceOrigin};
2496
2497        // A default single-node (in-memory) boot must expose a real, functional
2498        // namespace registry through `state.namespace_store()` — the control
2499        // plane's mint (S5) and `GET /namespaces` (S7) reach the store this way.
2500        let state =
2501            ServerState::build_with_store(InMemoryStore::default(), runtime_config()).await?;
2502
2503        let store = state.namespace_store();
2504
2505        // Mint a fresh namespace: the first reference creates it.
2506        let outcome = store
2507            .register_namespace("orders", NamespaceOrigin::WorkerMint)
2508            .await?;
2509        assert_eq!(
2510            outcome,
2511            MintOutcome::Created,
2512            "the first reference to a namespace mints it"
2513        );
2514
2515        // Re-referencing is idempotent: the record already exists.
2516        let again = store
2517            .register_namespace("orders", NamespaceOrigin::WorkerMint)
2518            .await?;
2519        assert_eq!(
2520            again,
2521            MintOutcome::AlreadyExisted,
2522            "a second reference touches the existing record rather than re-creating it"
2523        );
2524
2525        // Single lookup returns the durable record.
2526        let fetched = store.get_namespace("orders").await?;
2527        let record = fetched.ok_or("registered namespace must be retrievable via get_namespace")?;
2528        assert_eq!(record.name, "orders");
2529        assert_eq!(record.origin, NamespaceOrigin::WorkerMint);
2530
2531        // The live set lists the namespace.
2532        let listed = store.list_namespaces().await?;
2533        assert!(
2534            listed.iter().any(|record| record.name == "orders"),
2535            "list_namespaces returns the minted namespace"
2536        );
2537
2538        Ok(())
2539    }
2540
2541    #[cfg(feature = "haematite-backend")]
2542    #[tokio::test(flavor = "multi_thread")]
2543    async fn connect_store_haematite_round_trips_through_event_store()
2544    -> Result<(), Box<dyn std::error::Error>> {
2545        use aion_core::{ContentType, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId};
2546        use aion_store::WriteToken;
2547        use chrono::Utc;
2548
2549        use crate::config::{StoreBackend, StoreConfig};
2550
2551        let data_dir = crate::test_support::private_tempdir()?;
2552        // Single shard, a fresh temp data_dir: the production connect path opens
2553        // an existing haematite database or creates one, then shares the leaf as
2554        // both the engine EventStore and the dispatcher OutboxStore.
2555        let connected = super::connect_store(StoreConfig {
2556            backend: StoreBackend::Haematite,
2557            url: None,
2558            owned_shards: Vec::new(),
2559            data_dir: Some(data_dir.path().to_string_lossy().into_owned()),
2560            shard_count: 1,
2561            cluster: None,
2562        })
2563        .await?;
2564        let event_store = connected.event_store;
2565        assert!(
2566            connected.outbox_store.is_some(),
2567            "the haematite backend shares its leaf store as the dispatcher's outbox store"
2568        );
2569        assert!(
2570            connected.bootstrap_coordinator,
2571            "a single-node haematite boot owns all shards and bootstraps the coordinator"
2572        );
2573        assert!(
2574            connected.cluster_responder.is_none(),
2575            "a single-node (no [cluster]) haematite boot has no distributed responder"
2576        );
2577
2578        let workflow_id = WorkflowId::new_v4();
2579        let event = aion_core::Event::WorkflowStarted {
2580            envelope: EventEnvelope {
2581                seq: 1,
2582                recorded_at: Utc::now(),
2583                workflow_id: workflow_id.clone(),
2584            },
2585            workflow_type: String::from("checkout"),
2586            input: Payload::new(ContentType::Json, b"{}".to_vec()),
2587            run_id: RunId::new_v4(),
2588            parent_run_id: None,
2589            package_version: PackageVersion::new("a".repeat(64)),
2590        };
2591        event_store
2592            .append(
2593                WriteToken::recorder(),
2594                &workflow_id,
2595                std::slice::from_ref(&event),
2596                0,
2597            )
2598            .await?;
2599        let history = event_store.read_history(&workflow_id).await?;
2600        assert_eq!(
2601            history.len(),
2602            1,
2603            "an event appended through the server's dyn EventStore reads back"
2604        );
2605        Ok(())
2606    }
2607
2608    #[cfg(all(feature = "haematite-backend", unix))]
2609    #[test]
2610    fn haematite_root_swap_before_first_backend_touch_cannot_redirect_writes()
2611    -> Result<(), Box<dyn std::error::Error>> {
2612        use std::os::unix::fs::symlink;
2613
2614        let sandbox = crate::test_support::private_tempdir()?;
2615        let configured_root = sandbox.path().join("data");
2616        let held_root = sandbox.path().join("held-data");
2617        let outside = sandbox.path().join("outside");
2618        std::fs::create_dir(&outside)?;
2619        let configured = configured_root
2620            .to_str()
2621            .ok_or("temporary data path was not UTF-8")?;
2622
2623        let (store, responder) =
2624            super::build_haematite_store_with_hook(configured, 4, None, || {
2625                // The server has acquired and hardened `configured_root`, but
2626                // Haematite has not opened or created anything. Replace the
2627                // ambient name with an attacker-controlled symlink at exactly
2628                // the old check/use boundary.
2629                std::fs::rename(&configured_root, &held_root)?;
2630                symlink(&outside, &configured_root)?;
2631                Ok(())
2632            })?;
2633        assert!(responder.is_none());
2634
2635        let outside_entries = std::fs::read_dir(&outside)?.collect::<Result<Vec<_>, _>>()?;
2636        assert!(
2637            outside_entries.is_empty(),
2638            "Haematite followed the replaced ambient root and wrote outside"
2639        );
2640        assert!(held_root.join("config.json").is_file());
2641        for shard in 0..4 {
2642            let shard_path = held_root.join(format!("shard-{shard}"));
2643            assert!(shard_path.is_dir(), "shard {shard} was not materialized");
2644            assert!(
2645                std::fs::read_dir(&shard_path)?
2646                    .next()
2647                    .transpose()?
2648                    .is_some(),
2649                "shard {shard} did not run Haematite's materialization path"
2650            );
2651        }
2652
2653        drop(store);
2654        Ok(())
2655    }
2656
2657    #[cfg(all(
2658        feature = "haematite-backend",
2659        any(target_os = "linux", target_os = "android")
2660    ))]
2661    #[tokio::test]
2662    async fn proc_fd_backend_path_survives_a_post_startup_root_swap()
2663    -> Result<(), Box<dyn std::error::Error>> {
2664        use std::os::unix::fs::symlink;
2665
2666        use aion_core::{ContentType, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId};
2667        use aion_store::{WritableEventStore as _, WriteToken};
2668        use chrono::Utc;
2669
2670        let sandbox = crate::test_support::private_tempdir()?;
2671        let configured_root = sandbox.path().join("data");
2672        let held_root = sandbox.path().join("held-data");
2673        let capture = sandbox.path().join("capture");
2674        std::fs::create_dir(&capture)?;
2675        let configured = configured_root
2676            .to_str()
2677            .ok_or("temporary data path was not UTF-8")?;
2678
2679        let (store, responder) = super::build_haematite_store(configured, 4, None)?;
2680        assert!(responder.is_none());
2681        std::fs::rename(&configured_root, &held_root)?;
2682        symlink(&capture, &configured_root)?;
2683
2684        let workflow_id = WorkflowId::new_v4();
2685        let event = aion_core::Event::WorkflowStarted {
2686            envelope: EventEnvelope {
2687                seq: 1,
2688                recorded_at: Utc::now(),
2689                workflow_id: workflow_id.clone(),
2690            },
2691            workflow_type: String::from("post-startup-root-swap"),
2692            input: Payload::new(ContentType::Json, b"{}".to_vec()),
2693            run_id: RunId::new_v4(),
2694            parent_run_id: None,
2695            package_version: PackageVersion::new("a".repeat(64)),
2696        };
2697        store
2698            .append(
2699                WriteToken::recorder(),
2700                &workflow_id,
2701                std::slice::from_ref(&event),
2702                0,
2703            )
2704            .await?;
2705
2706        let captured = std::fs::read_dir(&capture)?.collect::<Result<Vec<_>, _>>()?;
2707        assert!(
2708            captured.is_empty(),
2709            "post-startup append followed the replacement symlink into capture"
2710        );
2711        assert!(held_root.join("config.json").is_file());
2712        drop(store);
2713        Ok(())
2714    }
2715
2716    #[cfg(all(
2717        feature = "haematite-backend",
2718        unix,
2719        not(any(target_os = "linux", target_os = "android"))
2720    ))]
2721    #[test]
2722    fn path_ambient_haematite_refuses_group_or_world_writable_ancestors()
2723    -> Result<(), Box<dyn std::error::Error>> {
2724        use std::os::unix::fs::PermissionsExt as _;
2725
2726        let sandbox = crate::test_support::private_tempdir()?;
2727        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
2728
2729        for mode in [0o770, 0o1777] {
2730            let shared = sandbox.path().join(format!("shared-{mode:o}"));
2731            let data_root = shared.join("data");
2732            std::fs::create_dir(&shared)?;
2733            std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(mode))?;
2734            std::fs::create_dir(&data_root)?;
2735            std::fs::set_permissions(&data_root, std::fs::Permissions::from_mode(0o700))?;
2736            let configured = data_root
2737                .to_str()
2738                .ok_or("temporary data path was not UTF-8")?;
2739
2740            let Err(error) = super::build_haematite_store(configured, 4, None) else {
2741                return Err(format!("mode {mode:04o} ancestor was accepted").into());
2742            };
2743            let message = error.to_string();
2744            let crate::ServerError::UnsafeDataRootAncestor {
2745                data_root: resolved_root,
2746                component,
2747                reason,
2748            } = error
2749            else {
2750                return Err(format!("expected typed unsafe-ancestor error, got {message}").into());
2751            };
2752            assert_eq!(resolved_root, std::fs::canonicalize(&data_root)?);
2753            assert_eq!(component, std::fs::canonicalize(&shared)?);
2754            assert!(
2755                reason.contains(&format!("mode {mode:04o}")),
2756                "unexpected reason: {reason}"
2757            );
2758            if mode & 0o1000 != 0 {
2759                assert!(reason.contains("sticky bit is not accepted"));
2760            }
2761            assert!(message.contains("private Aion home"));
2762            assert!(
2763                !data_root.join("config.json").exists(),
2764                "Haematite touched its ambient path before the refusal"
2765            );
2766        }
2767        Ok(())
2768    }
2769
2770    #[cfg(all(feature = "haematite-backend", target_os = "macos"))]
2771    #[test]
2772    fn path_ambient_haematite_refuses_mutating_allow_acl_ancestor()
2773    -> Result<(), Box<dyn std::error::Error>> {
2774        use std::os::unix::fs::PermissionsExt as _;
2775
2776        let sandbox = crate::test_support::private_tempdir()?;
2777        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
2778        let shared = sandbox.path().join("acl-shared");
2779        let data_root = shared.join("data");
2780        std::fs::create_dir(&shared)?;
2781        std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o700))?;
2782        let acl = "everyone allow list,search,add_file,add_subdirectory,delete_child";
2783        let status = std::process::Command::new("chmod")
2784            .arg("+a")
2785            .arg(acl)
2786            .arg(&shared)
2787            .status()?;
2788        assert!(status.success(), "failed to install Darwin regression ACL");
2789        let configured = data_root
2790            .to_str()
2791            .ok_or("temporary data path was not UTF-8")?;
2792
2793        let result = super::build_haematite_store(configured, 4, None);
2794        let cleanup = std::process::Command::new("chmod")
2795            .arg("-RN")
2796            .arg(&shared)
2797            .status()?;
2798        assert!(cleanup.success(), "failed to clean Darwin regression ACL");
2799
2800        let Err(error) = result else {
2801            return Err("mutating non-euid allow ACL ancestor was accepted".into());
2802        };
2803        let message = error.to_string();
2804        let crate::ServerError::UnsafeDataRootAncestor {
2805            component, reason, ..
2806        } = error
2807        else {
2808            return Err(format!("expected typed unsafe-ancestor error, got {message}").into());
2809        };
2810        assert_eq!(component, std::fs::canonicalize(&shared)?);
2811        assert!(
2812            reason.contains("allow"),
2813            "reason did not name the ACE: {reason}"
2814        );
2815        assert!(
2816            reason.contains("everyone"),
2817            "reason did not name the ACE principal: {reason}"
2818        );
2819        assert!(
2820            !data_root.join("config.json").exists(),
2821            "Haematite touched its ambient path before the ACL refusal"
2822        );
2823        Ok(())
2824    }
2825
2826    #[cfg(all(feature = "haematite-backend", target_os = "macos"))]
2827    #[test]
2828    fn path_ambient_haematite_accepts_the_euid_uuid_allow_ace()
2829    -> Result<(), Box<dyn std::error::Error>> {
2830        use std::os::unix::fs::PermissionsExt as _;
2831
2832        use exacl::{AclEntry, AclOption, Perm};
2833
2834        let sandbox = crate::test_support::private_tempdir()?;
2835        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
2836        let private_parent = sandbox.path().join("euid-uuid-allow");
2837        let data_root = private_parent.join("data");
2838        std::fs::create_dir(&private_parent)?;
2839        std::fs::set_permissions(&private_parent, std::fs::Permissions::from_mode(0o700))?;
2840
2841        let server_uid = rustix::process::geteuid().as_raw();
2842        let ace_qualifier = crate::filesystem::darwin_user_uuid_for_test(server_uid)?;
2843        let entry = AclEntry::allow_user(
2844            &ace_qualifier.to_string(),
2845            Perm::EXECUTE | Perm::WRITE | Perm::APPEND | Perm::DELETE_CHILD,
2846            None,
2847        );
2848        exacl::setfacl(
2849            &[private_parent.as_path()],
2850            &[entry],
2851            AclOption::SYMLINK_ACL,
2852        )?;
2853        let configured = data_root
2854            .to_str()
2855            .ok_or("temporary data path was not UTF-8")?;
2856
2857        let result = super::build_haematite_store(configured, 4, None);
2858        let cleanup = std::process::Command::new("chmod")
2859            .arg("-RN")
2860            .arg(&private_parent)
2861            .status()?;
2862        assert!(cleanup.success(), "failed to clean euid UUID allow ACL");
2863
2864        let (store, responder) = result?;
2865        assert!(responder.is_none());
2866        assert!(data_root.join("config.json").is_file());
2867        drop(store);
2868        Ok(())
2869    }
2870
2871    #[cfg(all(feature = "haematite-backend", target_os = "macos"))]
2872    #[test]
2873    fn path_ambient_haematite_refuses_a_non_euid_user_uuid_allow_ace()
2874    -> Result<(), Box<dyn std::error::Error>> {
2875        use std::os::unix::fs::PermissionsExt as _;
2876
2877        use exacl::{AclEntry, AclOption, Perm};
2878
2879        let sandbox = crate::test_support::private_tempdir()?;
2880        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
2881        let shared = sandbox.path().join("non-euid-uuid-allow");
2882        let data_root = shared.join("data");
2883        std::fs::create_dir(&shared)?;
2884        std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o700))?;
2885
2886        let server_uid = rustix::process::geteuid().as_raw();
2887        let foreign_uid = u32::from(server_uid == 0);
2888        let foreign_qualifier = crate::filesystem::darwin_user_uuid_for_test(foreign_uid)?;
2889        let entry = AclEntry::allow_user(
2890            &foreign_qualifier.to_string(),
2891            Perm::EXECUTE | Perm::WRITE | Perm::APPEND | Perm::DELETE_CHILD,
2892            None,
2893        );
2894        exacl::setfacl(&[shared.as_path()], &[entry], AclOption::SYMLINK_ACL)?;
2895        let configured = data_root
2896            .to_str()
2897            .ok_or("temporary data path was not UTF-8")?;
2898
2899        let result = super::build_haematite_store(configured, 4, None);
2900        let cleanup = std::process::Command::new("chmod")
2901            .arg("-RN")
2902            .arg(&shared)
2903            .status()?;
2904        assert!(cleanup.success(), "failed to clean non-euid UUID allow ACL");
2905
2906        let Err(error) = result else {
2907            return Err("mutating non-euid user UUID allow ACE was accepted".into());
2908        };
2909        let message = error.to_string();
2910        let crate::ServerError::UnsafeDataRootAncestor {
2911            component, reason, ..
2912        } = error
2913        else {
2914            return Err(format!("expected typed unsafe-ancestor error, got {message}").into());
2915        };
2916        assert_eq!(component, std::fs::canonicalize(&shared)?);
2917        assert!(
2918            reason.contains("allow") && reason.contains(&format!("server euid {server_uid}")),
2919            "reason did not name the rejected ACE: {reason}"
2920        );
2921        assert!(
2922            !data_root.join("config.json").exists(),
2923            "Haematite touched its ambient path before the UUID ACL refusal"
2924        );
2925        Ok(())
2926    }
2927
2928    #[cfg(all(feature = "haematite-backend", target_os = "macos"))]
2929    #[test]
2930    fn path_ambient_haematite_accepts_a_deny_only_acl_ancestor()
2931    -> Result<(), Box<dyn std::error::Error>> {
2932        use std::os::unix::fs::PermissionsExt as _;
2933
2934        let sandbox = crate::test_support::private_tempdir()?;
2935        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
2936        let private_parent = sandbox.path().join("deny-only");
2937        let data_root = private_parent.join("data");
2938        std::fs::create_dir(&private_parent)?;
2939        std::fs::set_permissions(&private_parent, std::fs::Permissions::from_mode(0o700))?;
2940        let status = std::process::Command::new("chmod")
2941            .arg("+a")
2942            .arg("everyone deny delete")
2943            .arg(&private_parent)
2944            .status()?;
2945        assert!(status.success(), "failed to install Darwin deny-only ACL");
2946        let configured = data_root
2947            .to_str()
2948            .ok_or("temporary data path was not UTF-8")?;
2949
2950        let result = super::build_haematite_store(configured, 4, None);
2951        let cleanup = std::process::Command::new("chmod")
2952            .arg("-RN")
2953            .arg(&private_parent)
2954            .status()?;
2955        assert!(cleanup.success(), "failed to clean Darwin deny-only ACL");
2956
2957        let (store, responder) = result?;
2958        assert!(responder.is_none());
2959        assert!(data_root.join("config.json").is_file());
2960        drop(store);
2961        Ok(())
2962    }
2963
2964    #[cfg(all(feature = "haematite-backend", target_os = "macos"))]
2965    #[test]
2966    fn path_ambient_haematite_accepts_the_stock_home_acl_chain()
2967    -> Result<(), Box<dyn std::error::Error>> {
2968        use std::os::unix::fs::PermissionsExt as _;
2969        use users::os::unix::UserExt as _;
2970
2971        let effective_uid = rustix::process::geteuid().as_raw();
2972        let effective_user = users::get_user_by_uid(effective_uid)
2973            .ok_or_else(|| format!("server euid {effective_uid} has no account record"))?;
2974        let sandbox = tempfile::Builder::new()
2975            .prefix(".aion-acl-home-proof-")
2976            .tempdir_in(effective_user.home_dir())?;
2977        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
2978        let data_root = sandbox.path().join("data");
2979        let configured = data_root
2980            .to_str()
2981            .ok_or("temporary data path was not UTF-8")?;
2982
2983        let (store, responder) = super::build_haematite_store(configured, 4, None)?;
2984        assert!(responder.is_none());
2985        assert!(data_root.join("config.json").is_file());
2986        drop(store);
2987        Ok(())
2988    }
2989
2990    #[cfg(all(
2991        feature = "haematite-backend",
2992        unix,
2993        not(any(target_os = "linux", target_os = "android"))
2994    ))]
2995    #[test]
2996    fn path_ambient_haematite_accepts_an_owner_controlled_chain()
2997    -> Result<(), Box<dyn std::error::Error>> {
2998        use std::os::unix::fs::PermissionsExt as _;
2999
3000        let sandbox = crate::test_support::private_tempdir()?;
3001        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3002        let private_parent = sandbox.path().join("private");
3003        let data_root = private_parent.join("data");
3004        std::fs::create_dir(&private_parent)?;
3005        std::fs::set_permissions(&private_parent, std::fs::Permissions::from_mode(0o700))?;
3006        let configured = data_root
3007            .to_str()
3008            .ok_or("temporary data path was not UTF-8")?;
3009
3010        let (store, responder) = super::build_haematite_store(configured, 4, None)?;
3011        assert!(responder.is_none());
3012        assert!(data_root.join("config.json").is_file());
3013        for shard in 0..4 {
3014            assert!(data_root.join(format!("shard-{shard}")).is_dir());
3015        }
3016        drop(store);
3017        Ok(())
3018    }
3019
3020    #[tokio::test]
3021    async fn connect_store_memory_backend_exposes_no_outbox_store()
3022    -> Result<(), Box<dyn std::error::Error>> {
3023        use crate::config::{StoreBackend, StoreConfig};
3024
3025        // Memory backend: no durable outbox table, so no outbox store handle —
3026        // and `outbox.enabled` over memory is rejected at dispatcher commission.
3027        let connected = super::connect_store(StoreConfig {
3028            backend: StoreBackend::Memory,
3029            url: None,
3030            owned_shards: Vec::new(),
3031            data_dir: None,
3032            shard_count: 1,
3033            cluster: None,
3034        })
3035        .await?;
3036        assert!(
3037            connected.outbox_store.is_none(),
3038            "the in-memory backend exposes no outbox store"
3039        );
3040        Ok(())
3041    }
3042
3043    // The libSQL connect path is now an opt-in backend (`libsql-backend`), so this
3044    // libSQL-specific outbox-sharing assertion compiles and runs only under that
3045    // feature. The memory case is covered above, unconditionally.
3046    #[cfg(feature = "libsql-backend")]
3047    #[tokio::test]
3048    async fn connect_store_shares_outbox_store_only_for_libsql()
3049    -> Result<(), Box<dyn std::error::Error>> {
3050        use crate::config::{StoreBackend, StoreConfig};
3051
3052        // LibSql backend: the leaf Arc<LibSqlStore> is shared as BOTH the engine's
3053        // EventStore and the dispatcher's OutboxStore (one libsql::Connection), so
3054        // the dispatcher reuses the engine's connection rather than opening a
3055        // second contending one (the inc-8 contention fix).
3056        let path = std::env::temp_dir().join(format!(
3057            "aion-connect-store-{}-{}.db",
3058            std::process::id(),
3059            std::time::SystemTime::now()
3060                .duration_since(std::time::UNIX_EPOCH)
3061                .map(|elapsed| elapsed.as_nanos())
3062                .unwrap_or_default()
3063        ));
3064        let connected = super::connect_store(StoreConfig {
3065            backend: StoreBackend::LibSql,
3066            url: Some(path.to_string_lossy().into_owned()),
3067            owned_shards: Vec::new(),
3068            data_dir: None,
3069            shard_count: 1,
3070            cluster: None,
3071        })
3072        .await?;
3073        assert!(
3074            connected.outbox_store.is_some(),
3075            "the libSQL backend shares its leaf store as the dispatcher's outbox store"
3076        );
3077        Ok(())
3078    }
3079
3080    #[tokio::test]
3081    async fn state_build_fails_without_event_broadcast_capacity()
3082    -> Result<(), Box<dyn std::error::Error>> {
3083        let mut runtime = runtime_config();
3084        runtime.websocket.event_broadcast_capacity = None;
3085
3086        let error = ServerState::build_with_store(InMemoryStore::default(), runtime)
3087            .await
3088            .err()
3089            .ok_or("state build must fail when event streaming is unsized")?;
3090
3091        assert!(error.is_config(), "expected a config error, got {error}");
3092        assert!(
3093            error
3094                .to_string()
3095                .contains("websocket.event_broadcast_capacity"),
3096            "error must name the missing key: {error}"
3097        );
3098        Ok(())
3099    }
3100
3101    #[tokio::test]
3102    async fn state_build_fails_without_query_timeout() -> Result<(), Box<dyn std::error::Error>> {
3103        let mut runtime = runtime_config();
3104        runtime.query_timeout = None;
3105
3106        let error = ServerState::build_with_store(InMemoryStore::default(), runtime)
3107            .await
3108            .err()
3109            .ok_or("state build must fail when the query reply deadline is unset")?;
3110
3111        assert!(error.is_config(), "expected a config error, got {error}");
3112        assert!(
3113            error.to_string().contains("runtime.query_timeout_ms"),
3114            "error must name the missing key: {error}"
3115        );
3116        assert!(
3117            error.to_string().contains("AION_RUNTIME_QUERY_TIMEOUT_MS"),
3118            "error must name the environment override: {error}"
3119        );
3120        Ok(())
3121    }
3122
3123    #[tokio::test]
3124    async fn state_build_fails_with_zero_query_timeout() -> Result<(), Box<dyn std::error::Error>> {
3125        let mut runtime = runtime_config();
3126        runtime.query_timeout = Some(Duration::ZERO);
3127
3128        let error = ServerState::build_with_store(InMemoryStore::default(), runtime)
3129            .await
3130            .err()
3131            .ok_or("state build must fail when the query reply deadline is zero")?;
3132
3133        assert!(error.is_config(), "expected a config error, got {error}");
3134        assert!(
3135            error.to_string().contains("runtime.query_timeout_ms"),
3136            "error must name the zero-valued key: {error}"
3137        );
3138        Ok(())
3139    }
3140
3141    /// THE #189 WIRING PIN (r1 Blocker B1): a completed update check driven
3142    /// through the dispatcher stack `build_decorated_dispatcher` actually
3143    /// builds lands in the update-status slot that same call RETURNS — the
3144    /// slot the boot path stores and `GET /update-status` serves.
3145    ///
3146    /// Everything between the dispatch and the slot is the production object:
3147    /// the real `DeclaredCommandDispatcher` executes a real server-run command
3148    /// through the real `ShellAction` (transcript pump and all), the real
3149    /// `UpdateCheckObserver` sits in its real position, and the assertion
3150    /// reads the slot off the function's own return value. The one test
3151    /// double is the `DeclaredBodies` source — the seam production code
3152    /// installs after the engine exists — and it is SEQUENCED because the
3153    /// gates run offline: the observer's verification (first resolution)
3154    /// sees the genuine `FETCH_COMMAND`, and the executor (second
3155    /// resolution) is handed a local `cat` of the captured real index body,
3156    /// standing in for the network transfer the genuine curl would perform.
3157    ///
3158    /// The r1 review proved the absence of this pin by mutation: returning a
3159    /// FRESH slot instead of the observer's left all 1268 tests green while
3160    /// `/update-status` would answer null forever. Under this test that
3161    /// exact mutation goes red: the returned slot stays empty and the
3162    /// assertion below names it.
3163    #[tokio::test(flavor = "multi_thread")]
3164    async fn a_completed_check_through_the_built_dispatcher_lands_in_the_returned_slot()
3165    -> Result<(), Box<dyn std::error::Error>> {
3166        use std::collections::{BTreeMap, VecDeque};
3167        use std::sync::{Arc, Mutex};
3168
3169        use aion::ActivityDispatch;
3170        use aion_core::{ActivityId, RunId, WorkflowId};
3171        use aion_package::ActionBodyContract;
3172
3173        use crate::update_check::document::{FETCH_ACTION, FETCH_COMMAND, UPDATE_CHECK_QUEUE};
3174        use crate::worker::{DeclaredBodies, DeclaredBodyLookup, DispatchingRun};
3175
3176        /// Hands out one scripted resolution per call, in order. Documented
3177        /// above: first the observer's verification, then the executor's.
3178        struct SequencedBodies {
3179            replies: Mutex<VecDeque<DeclaredBodyLookup>>,
3180        }
3181
3182        impl DeclaredBodies for SequencedBodies {
3183            fn body_for(
3184                &self,
3185                _task_queue: &str,
3186                _action: &str,
3187                _run: DispatchingRun<'_>,
3188            ) -> DeclaredBodyLookup {
3189                let mut replies = match self.replies.lock() {
3190                    Ok(replies) => replies,
3191                    Err(poisoned) => poisoned.into_inner(),
3192                };
3193                replies.pop_front().unwrap_or(DeclaredBodyLookup::None)
3194            }
3195        }
3196
3197        let runtime = runtime_config();
3198        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
3199            ServerState::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
3200        );
3201        let namespace_store: Arc<dyn aion_store::NamespaceStore> =
3202            Arc::new(InMemoryStore::default());
3203        let worker_deployment_store: Arc<dyn aion_store::WorkerDeploymentStore> =
3204            Arc::new(InMemoryStore::default());
3205        let seams = super::build_worker_seams(
3206            &runtime,
3207            &cluster_publisher,
3208            &namespace_store,
3209            &worker_deployment_store,
3210            None,
3211        );
3212
3213        // The captured REAL index body, served to the executor by a local
3214        // command instead of the network (gates run offline).
3215        let fixture = concat!(
3216            env!("CARGO_MANIFEST_DIR"),
3217            "/src/update_check/fixtures/aion-cli-index.jsonl"
3218        );
3219        seams.declared_bodies.install(Arc::new(SequencedBodies {
3220            replies: Mutex::new(VecDeque::from([
3221                DeclaredBodyLookup::Declared(ActionBodyContract::Run {
3222                    command: FETCH_COMMAND.to_owned(),
3223                }),
3224                DeclaredBodyLookup::Declared(ActionBodyContract::Run {
3225                    command: format!("cat {fixture}"),
3226                }),
3227            ])),
3228        }));
3229
3230        let transcript = crate::activity_publisher::ActivityEventPublisher::new(
3231            Arc::new(aion_store::InMemoryObservabilityStore::default()),
3232            ServerState::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
3233        );
3234        let (dispatcher, _mock_registry, _attempt_owners, _workspace_root, update_status) =
3235            super::build_decorated_dispatcher(&runtime, &seams, transcript);
3236
3237        assert_eq!(
3238            update_status.last(),
3239            None,
3240            "the returned slot must start honestly empty"
3241        );
3242
3243        let dispatch = ActivityDispatch {
3244            namespace: "default".to_owned(),
3245            task_queue: UPDATE_CHECK_QUEUE.to_owned(),
3246            node: None,
3247            workflow_id: WorkflowId::new_v4(),
3248            run_id: RunId::new_v4(),
3249            activity_id: ActivityId::from_sequence_position(1),
3250            name: FETCH_ACTION.to_owned(),
3251            input: "{}".to_owned(),
3252            config: "{}".to_owned(),
3253            attempt: 1,
3254            labels: BTreeMap::new(),
3255            advisory: false,
3256        };
3257        let handle = tokio::task::spawn_blocking(move || dispatcher.dispatch(dispatch));
3258        let encoded = handle
3259            .await?
3260            .map_err(|error| format!("the check dispatch failed: {error}"))?;
3261        let outcome: serde_json::Value = serde_json::from_str(&encoded)?;
3262        assert_eq!(outcome["exit_code"], 0, "the local stand-in command ran");
3263
3264        let recorded = update_status.last().ok_or(
3265            "the completed check must land in the RETURNED slot — the one the boot path \
3266             stores and /update-status serves; an empty slot here is the disconnected-\
3267             producer mis-wire the r1 review proved unmeasured",
3268        )?;
3269        assert_eq!(recorded.latest_known, "0.13.7");
3270        Ok(())
3271    }
3272}