Skip to main content

aion_server/
state.rs

1//! Shared server state constructed once at startup.
2
3/// The boot path's required-no-default config set: the probe, the declared
4/// upgrade defaults the boot-side config heal mints, and the declared
5/// not-upgrade-healable list — co-located with the requirement functions in
6/// this module that define the set.
7mod boot_required;
8
9pub(crate) use boot_required::{
10    BOOT_REQUIRED_FIELD_DEFAULTS, NOT_UPGRADE_HEALABLE, RequiredFieldDefault, boot_required_probe,
11};
12
13use std::{path::PathBuf, sync::Arc};
14
15use aion::{
16    ActivityDispatcher, EngineBuilder, RuntimeHandle, SignalRouter, signal::ConcreteSignalRouter,
17};
18use aion_store::{EventStore, NamespaceStore, OutboxStore, WorkerDeploymentStore};
19
20use crate::dev_ui::{ActivityMockRegistry, DevMockingDispatcher};
21
22#[cfg(feature = "auth")]
23use crate::auth::JwksCache;
24use crate::{
25    config::{RuntimeConfig, ServerConfig, StoreBackend, StoreConfig},
26    error::ServerError,
27    namespace::{NamespaceGuard, NamespaceMinter, resolver::NamespaceResolver},
28    observability::{
29        Metrics, health::HealthState, instrumented_store::InstrumentedEventStore,
30        metrics::MetricsError,
31    },
32    shutdown::DrainState,
33    worker::{
34        ConnectedWorkerRegistry, HeartbeatTracker, PendingActivities, WorkerActivityDispatcher,
35        supervisor::WorkerSupervisor,
36    },
37};
38
39/// The address a locally-spawned assistant agent dials this server back on.
40///
41/// Independent of `[mcp] enabled`. That switch governs whether the general
42/// WORKFLOW tool catalogue is mounted; the address is the same address either
43/// way, and the assistant's own `/assistant/mcp` route is served whatever the
44/// general surface does — which is why the flag travels ON the value rather than
45/// deciding whether there is one.
46///
47/// `None` means this server cannot state a dialable address at all, and then no
48/// MCP server of ours is handed to any agent.
49///
50/// # The listen address is TRANSLATED, and this is the honest caveat
51///
52/// The value is read from the CONFIGURED listen address, before bind — the same
53/// limitation `assistant/install.rs` records for the worker-listener advice. A
54/// wildcard (`0.0.0.0`, `[::]`) is not an address anything can dial, so it is
55/// translated to loopback: the agent this URL is handed to is a subprocess of
56/// THIS server on THIS host, so loopback is where it should reach us, and a
57/// wildcard bind always accepts a loopback connection. A configured port of
58/// zero cannot be translated at all — the real port is only known after bind —
59/// and yields `None` rather than a URL that would silently dial port zero.
60fn assistant_mcp_endpoint(
61    runtime: &RuntimeConfig,
62) -> Option<crate::assistant::sessions::launch::AssistantEndpoints> {
63    let configured = runtime.listen.http;
64    if configured.port() == 0 {
65        tracing::warn!(
66            "`server.listen_address` names port 0, so this server cannot state an address for an \
67             assistant agent to dial before it has bound; assistant sessions will be opened with \
68             no MCP server of ours — including the `assistant_context` tool, so an agent will not \
69             be able to read what is on the operator's screen"
70        );
71        return None;
72    }
73    let host = if configured.ip().is_unspecified() {
74        std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
75    } else {
76        configured.ip()
77    };
78    let authority = std::net::SocketAddr::new(host, configured.port());
79    Some(crate::assistant::sessions::launch::AssistantEndpoints {
80        base: format!("http://{authority}"),
81        aion_mcp_enabled: runtime.mcp.enabled,
82    })
83}
84
85/// The assistant session registry over one store — the ONE assembly of the
86/// three inputs every constructor needs, so a fifth constructor cannot wire
87/// them differently from the other four.
88fn build_assistant_sessions(
89    store: Arc<dyn aion_store::assistant::AssistantSessionStore>,
90    runtime: &RuntimeConfig,
91) -> crate::assistant::sessions::AssistantSessions {
92    crate::assistant::sessions::AssistantSessions::new(
93        store,
94        runtime.assistant.clone(),
95        assistant_mcp_endpoint(runtime),
96    )
97}
98
99/// Build an uncommissioned supervisor over one durable deployment store and
100/// the state's cluster channel.
101///
102/// Every state constructor routes through this, so no construction path can
103/// accidentally hand the supervisor a DIFFERENT store from the one the
104/// management API writes through — desired state written in one place and read
105/// in another is exactly the drift this single call site removes. The same
106/// argument holds for the publisher: it is the state's own cluster channel,
107/// so a desired-state write made by the supervisor reaches the same live feed
108/// as one made by the worker-deployment endpoints.
109fn new_supervisor(
110    store: &Arc<dyn WorkerDeploymentStore>,
111    publisher: &crate::cluster_publisher::ClusterEventPublisher,
112) -> Arc<WorkerSupervisor> {
113    Arc::new(WorkerSupervisor::new(Arc::clone(store), publisher.clone()))
114}
115
116/// Cloneable shared state passed to all server transports.
117#[derive(Clone)]
118pub struct ServerState {
119    inner: Arc<ServerStateInner>,
120}
121
122struct ServerStateInner {
123    namespace_guard: NamespaceGuard,
124    runtime: RuntimeConfig,
125    worker_registry: ConnectedWorkerRegistry,
126    pending_activities: PendingActivities,
127    heartbeat_tracker: HeartbeatTracker,
128    /// Correlation registry joining the liveness probe's pushed gRPC pings to
129    /// the answers that come back up each worker's inbound stream (#197).
130    ///
131    /// Held on the state because the two halves live in tasks that cannot see
132    /// each other: the probe owns a timer loop, and each answer arrives inside
133    /// the tonic stream handler for one worker.
134    grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters,
135    drain_state: DrainState,
136    metrics: Option<Metrics>,
137    health: Option<HealthState>,
138    /// Shared per-run activity-mock registry. Present only when the dev surface
139    /// is commissioned; the engine's dispatcher consults this exact instance.
140    activity_mock_registry: Option<ActivityMockRegistry>,
141    /// The durable store cast as an [`OutboxStore`]. `None` for the in-memory
142    /// backend, which intentionally has no outbox.
143    outbox_store: Option<Arc<dyn OutboxStore>>,
144    /// The durable namespace registry, captured from the SAME concrete leaf
145    /// backend as the engine's `EventStore` BEFORE that leaf is wrapped in the
146    /// decorator chain (`PublishingEventStore` → `InstrumentedEventStore`),
147    /// which do not implement [`NamespaceStore`]. The haematite backend supplies
148    /// the quorum-replicated implementation; the in-memory backend supplies a
149    /// local-only one. Always present so the control-plane mint
150    /// (Phase 1 S5) and `GET /namespaces` (S7) can reach a real store on every
151    /// boot. Mirrors the `cluster_store` retention pattern.
152    namespace_store: Arc<dyn NamespaceStore>,
153    /// Durable worker-deployment records captured from the same leaf backend.
154    worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
155    /// Managed-worker supervision (W-1..W-4). Always present and always
156    /// UNCOMMISSIONED at construction: it supervises nothing until
157    /// [`crate::run`] installs the operator's `[worker_supervision]` policy, so
158    /// state construction can never start a process by itself.
159    worker_supervisor: Arc<WorkerSupervisor>,
160    /// Advisory outbox wake (LSUB-2): the in-process `Notify` shared by the
161    /// engine's stage seam (the `InstrumentedEventStore`'s `append_with_outbox`)
162    /// and the [`OutboxDispatcher`](crate::worker::OutboxDispatcher) run loop, so
163    /// a committed fan-out row wakes the dispatcher in ~RTT instead of waiting up
164    /// to one poll interval. Always present (cheap, no `Option`): the handle is
165    /// harmless when the outbox is not commissioned, since nothing pulses it.
166    outbox_wake: Arc<tokio::sync::Notify>,
167    /// WS3 cluster topology/ownership publisher. Always present: the ops console's
168    /// cluster channel is served on every boot (calm state with no peers on a
169    /// single-node server). Sized from `websocket.cluster_broadcast_capacity`.
170    cluster_publisher: crate::cluster_publisher::ClusterEventPublisher,
171    /// NOI-5b agent-observability transcript sequencer + live fan-out. Always
172    /// present: the transcript channel is served on every boot. The backing
173    /// [`ObservabilityStore`](aion_store::ObservabilityStore) is the durable
174    /// `O`-keyspace impl on a haematite boot and an in-memory impl on the memory
175    /// backend (which has no `O` keyspace), so the transcript path is uniform
176    /// across backends while only haematite persists across restart.
177    /// Sized from `websocket.cluster_broadcast_capacity` (the same deployment-wide
178    /// real-time channel capacity the cluster tail uses).
179    transcript_publisher: crate::activity_publisher::ActivityEventPublisher,
180    /// NOI-6 server-side intervention routing: the `attempt -> owning-worker`
181    /// back-index the intervention router resolves a command's target through.
182    /// Always present (cheap, no `Option`): the agent-dispatch path binds an owner
183    /// when it dispatches an agent attempt and releases it on completion, so the
184    /// router resolves the CURRENT owner. Empty until an agent attempt is
185    /// dispatched — a command to an unbound attempt is the attempt-scoped no-op.
186    attempt_owners: crate::worker::AttemptOwnerIndex,
187    /// R1 live unserved-queue state. Always present (cheap, no `Option`): the
188    /// bridge dispatcher publishes every parked dispatch into THIS instance, so
189    /// `unserved_queues()` answers "which addresses are unserved, why, and which
190    /// runs are waiting on them" without reading logs. Empty whenever nothing is
191    /// parked.
192    queue_service_state: crate::worker::QueueServiceState,
193    /// R1 queue-declaration source, filled in with the engine-backed reader once
194    /// the engine exists. Held so surfaces (and the bridge) share ONE reader
195    /// rather than each building their own view of the deployed contracts.
196    queue_declarations: crate::worker::QueueDeclarationSource,
197    /// The declared bodies THIS server is executing right now, shared with the
198    /// [`crate::worker::DeclaredCommandDispatcher`] that registers each attempt
199    /// for the life of its command. Always present (cheap, no `Option`): the
200    /// cancel path signals through it on every boot. Empty whenever no
201    /// server-run command is in flight — which, on a `from_parts*` state that
202    /// builds no declared-body dispatcher, is always.
203    declared_attempts: crate::worker::DeclaredCommandAttempts,
204    /// The declared-body catalog source BOTH dispatch paths consult — the
205    /// direct decorator over the engine's dispatch hook and the outbox row
206    /// decorator (aion#193). One source, installed once the engine exists;
207    /// empty and staying empty on a `from_parts*` state, which builds neither.
208    declared_bodies: crate::worker::DeclaredBodySource,
209    /// The last completed update check (#189 slice one), shared with the
210    /// [`crate::update_check::UpdateCheckObserver`] decorator that writes it
211    /// on the full boot path. Always present and always EMPTY at
212    /// construction: a server that has never checked says so, and nothing
213    /// here ever fetches anything — every check is an explicit operator act.
214    update_status: crate::update_check::UpdateStatusState,
215    /// The server-resolved workspace root for declared action bodies (#139):
216    /// the aion home's `clones/` directory, resolved ONCE at state
217    /// construction. The declared-body dispatcher expands `{workspace_root}`
218    /// with THIS value and the startup banner reports it, so composition
219    /// points read the value instead of re-deriving it. A resolution failure
220    /// is held here — boot proceeds, and the failure surfaces as a terminal
221    /// refusal when a placeholder-bearing body dispatches.
222    workspace_root: crate::worker::WorkspaceRoot,
223    /// This node's distribution name for the WS3 cluster snapshot self-identity.
224    /// `Some` on a distributed haematite boot (the configured `store.cluster.node_id`),
225    /// `None` on a single-node boot — the snapshot then reports the standalone
226    /// self-label so the ops console still has a node to render.
227    cluster_self_node: Option<String>,
228    /// Owns the distributed haematite inbound-write responder thread, kept alive
229    /// for the server's lifetime so a cluster node keeps answering peers'
230    /// replication/election traffic. `None` for non-distributed boots. Dropping
231    /// the state stops the responder.
232    cluster_responder: Option<aion_store_haematite::ClusterResponder>,
233    /// The concrete distributed haematite store the SS-5b supervisor polls for
234    /// peer liveness. `None` for every non-distributed boot.
235    cluster_store: Option<Arc<aion_store_haematite::HaematiteStore>>,
236    /// The peers the SS-5b supervisor watches (each with the shards this node
237    /// adopts on its death). Empty for non-distributed boots.
238    watched_peers: Vec<crate::cluster::WatchedPeer>,
239    /// The request-routing shard directory (R-2), built over the cluster store +
240    /// static peer config. `None` for every non-distributed boot, so the routing
241    /// edge falls back to the bare R-1 ownership check (and the default path is a
242    /// no-op).
243    shard_directory: Option<Arc<crate::routing::StaticShardDirectory>>,
244    /// The request forwarder (R-3): relays a non-local signal/query/cancel to the
245    /// shard owner's gRPC address. `None` for non-distributed boots. The trait
246    /// object makes the liminal forwarder a one-line swap when 13-L0/L1 land (R-6).
247    request_forwarder: Option<Arc<dyn crate::routing::RequestForwarder>>,
248    /// Server-owned assistant sessions: the live harness processes, and the
249    /// durable store their records and transcripts live in.
250    ///
251    /// Always present. The registry is cheap and empty on a server whose
252    /// `[assistant]` section declares no harness; asking it whether sessions are
253    /// available is how the descriptor answers, and an `Option` here would make
254    /// "dark" and "not built yet" the same shape.
255    assistant_sessions: crate::assistant::sessions::AssistantSessions,
256    #[cfg(feature = "auth")]
257    jwks_cache: Option<JwksCache>,
258}
259
260impl ServerState {
261    /// Fallback cluster broadcast capacity for the `from_parts*` embedder/test
262    /// constructors, which bypass config validation. The config-driven
263    /// [`Self::build`] path always sizes the publisher from the validated
264    /// `websocket.cluster_broadcast_capacity` instead.
265    ///
266    /// `NonZeroUsize::new(64)` is statically non-`None`, so the
267    /// [`Option::unwrap`]-free `match` keeps the value `const` without tripping
268    /// the workspace `unwrap_used`/`expect_used` deny lints.
269    const FALLBACK_CLUSTER_BROADCAST_CAPACITY: std::num::NonZeroUsize =
270        match std::num::NonZeroUsize::new(64) {
271            Some(value) => value,
272            None => std::num::NonZeroUsize::MIN,
273        };
274
275    /// Build shared state from operator configuration.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`ServerError`] if the store cannot connect or the engine cannot
280    /// be constructed.
281    pub async fn build(
282        config: ServerConfig,
283        stage: &crate::control::StageReporter,
284    ) -> Result<Self, ServerError> {
285        let (store_config, runtime) = config.into_parts();
286        let connected = connect_store(store_config, stage).await?;
287        Self::build_with_connected_store(connected, runtime, stage).await
288    }
289
290    /// Build shared state from an already-constructed store.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`ServerError::EngineCall`] if the engine cannot be constructed.
295    pub async fn build_with_store<S>(store: S, runtime: RuntimeConfig) -> Result<Self, ServerError>
296    where
297        S: EventStore
298            + NamespaceStore
299            + WorkerDeploymentStore
300            + aion_store::workloop::WorkloopStore
301            + aion_store::visibility::VisibilityStore
302            + aion_store::AssistantSessionStore,
303    {
304        // Capture the concrete leaf as the event store, the namespace registry,
305        // the workloop registration store and the visibility projection before
306        // it is wrapped in the (decorator-unaware) chain — one leaf, several
307        // trait objects.
308        let leaf = Arc::new(store);
309        let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
310        let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
311        let workloop_store: Arc<dyn aion_store::workloop::WorkloopStore> = leaf.clone();
312        let visibility_store: Arc<dyn aion_store::visibility::VisibilityStore> = leaf.clone();
313        let assistant_store: Arc<dyn aion_store::AssistantSessionStore> = leaf.clone();
314        // An embedder that hands us a store it already opened is not the boot
315        // path that claims a home: there is no pid record to narrate into, so
316        // the stages are logged and dropped.
317        Self::build_with_connected_store(
318            ConnectedStore::local(
319                leaf,
320                None,
321                namespace_store,
322                worker_deployment_store,
323                workloop_store,
324                visibility_store,
325                assistant_store,
326            ),
327            runtime,
328            &crate::control::StageReporter::detached(),
329        )
330        .await
331    }
332
333    async fn build_with_connected_store(
334        connected: ConnectedStore,
335        runtime: RuntimeConfig,
336        stage: &crate::control::StageReporter,
337    ) -> Result<Self, ServerError> {
338        let cluster_self_node = connected.cluster_self_node();
339        let outbox_store = connected.outbox_store;
340        let bootstrap_coordinator = connected.bootstrap_coordinator;
341        let cluster_responder = connected.cluster_responder;
342        let cluster_store = connected.cluster_store;
343        let watched_peers = connected.watched_peers;
344        // Build the R-2 directory + R-3 forwarder over the (live, failover-aware)
345        // cluster store and static peer config. Both present only for a
346        // distributed boot; `None` otherwise leaves the routing edge a no-op.
347        let RoutingState {
348            shard_directory,
349            request_forwarder,
350            mint_routing,
351        } = build_routing_state(
352            cluster_store.as_ref(),
353            connected.directory_peers,
354            connected.self_node_id,
355        );
356        let (event_broadcast_capacity, query_timeout, workloop_sweep_interval) =
357            required_engine_seams(&runtime)?;
358        let (cluster_publisher, transcript_publisher) =
359            build_real_time_publishers(&runtime, connected.observability_store)?;
360        let (metrics, outbox_wake, instrumented_store) =
361            build_instrumented_store(&runtime, connected.event_store)?;
362        let exported_metrics = runtime.metrics.enabled.then_some(metrics.clone());
363        let seams = build_worker_seams(
364            &runtime,
365            &cluster_publisher,
366            &metrics,
367            &connected.namespace_store,
368            &connected.worker_deployment_store,
369            mint_routing,
370        );
371        let (
372            activity_dispatcher,
373            activity_mock_registry,
374            attempt_owners,
375            workspace_root,
376            update_status,
377        ) = build_decorated_dispatcher(&runtime, &seams, transcript_publisher.clone());
378
379        let engine = boot_engine(EngineAssembly {
380            seams: &seams,
381            instrumented_store: &instrumented_store,
382            event_broadcast_capacity,
383            query_timeout,
384            workloop_store: Arc::clone(&connected.workloop_store),
385            visibility_store: Arc::clone(&connected.visibility_store),
386            workloop_sweep_interval,
387            activity_dispatcher,
388            active_registry: Arc::new(aion::Registry::default()),
389            bootstrap_coordinator,
390            runtime: &runtime,
391            stage,
392        })
393        .await?;
394        let resolver = NamespaceResolver::from_config(runtime.namespace.clone(), engine);
395        let worker_supervisor =
396            new_supervisor(&connected.worker_deployment_store, &cluster_publisher);
397        #[cfg(feature = "auth")]
398        let jwks_cache = build_jwks_cache(&runtime).await?;
399        let assistant_sessions =
400            build_assistant_sessions(Arc::clone(&connected.assistant_store), &runtime);
401        Ok(Self {
402            inner: Arc::new(ServerStateInner {
403                namespace_guard: NamespaceGuard::new(resolver),
404                assistant_sessions,
405                runtime,
406                metrics: exported_metrics,
407                worker_registry: seams.worker_registry,
408                pending_activities: seams.pending_activities,
409                heartbeat_tracker: seams.heartbeat_tracker,
410                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
411                drain_state: seams.drain_state,
412                health: Some(HealthState::new(instrumented_store, true)),
413                activity_mock_registry,
414                outbox_store,
415                namespace_store: connected.namespace_store,
416                worker_supervisor,
417                worker_deployment_store: connected.worker_deployment_store,
418                outbox_wake,
419                cluster_publisher,
420                transcript_publisher,
421                attempt_owners,
422                queue_service_state: seams.queue_service_state,
423                queue_declarations: seams.queue_declarations,
424                declared_attempts: seams.declared_attempts,
425                declared_bodies: seams.declared_bodies.clone(),
426                update_status,
427                workspace_root,
428                cluster_self_node,
429                cluster_responder,
430                cluster_store,
431                watched_peers,
432                shard_directory,
433                request_forwarder,
434                #[cfg(feature = "auth")]
435                jwks_cache,
436            }),
437        })
438    }
439
440    /// Build shared state from explicit parts with a default worker registry.
441    #[must_use]
442    pub fn from_parts(namespace_resolver: NamespaceResolver, runtime: RuntimeConfig) -> Self {
443        // No durable store was supplied (this constructor builds state from a
444        // resolver only), so the registry is a local-only in-memory store —
445        // present so `namespace_store()` is always reachable, never mutating any
446        // durable backend.
447        Self::from_parts_with_namespace_store(
448            namespace_resolver,
449            runtime,
450            Arc::new(aion_store::InMemoryStore::default()),
451        )
452    }
453
454    /// Build shared state from explicit parts with one caller-supplied durable
455    /// namespace and worker-deployment leaf.
456    ///
457    /// Identical to [`Self::from_parts`] except both control-plane dimensions are
458    /// derived from `store`: namespace registry reads/writes and durable worker
459    /// deployments use the same supplied leaf rather than fresh in-memory state.
460    #[must_use]
461    pub fn from_parts_with_namespace_store<S>(
462        namespace_resolver: NamespaceResolver,
463        runtime: RuntimeConfig,
464        store: Arc<S>,
465    ) -> Self
466    where
467        S: NamespaceStore + WorkerDeploymentStore,
468    {
469        let namespace_store: Arc<dyn NamespaceStore> = store.clone();
470        let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = store;
471        Self::from_parts_with_control_stores(
472            namespace_resolver,
473            runtime,
474            namespace_store,
475            worker_deployment_store,
476        )
477    }
478
479    /// Build shared state with caller-supplied namespace and worker-deployment stores.
480    ///
481    /// This is the explicit embedder/test seam for retaining both control-plane
482    /// contracts from one durable leaf without constructing the full engine boot.
483    #[must_use]
484    pub fn from_parts_with_control_stores(
485        namespace_resolver: NamespaceResolver,
486        runtime: RuntimeConfig,
487        namespace_store: Arc<dyn NamespaceStore>,
488        worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
489    ) -> Self {
490        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
491        // Bound here, beside the tracker, for the same reason: `runtime` moves
492        // into the state below, and the transport-loss budget must be read from
493        // the operator's heartbeat window BEFORE it does. The window is a
494        // constructor parameter, so an embedder-booted server cannot reach the
495        // state carrying a budget the operator never declared.
496        let pending_activities = PendingActivities::new(runtime.worker.heartbeat_window);
497        // Computed before `runtime` moves into the state: the retention bounds
498        // flow from `[observability]` config on the embedder path too, so a
499        // from-parts server enforces the same truncation/cap as a full boot.
500        let bounds = transcript_bounds(&runtime);
501        // These constructors bypass config validation and cannot fail, so an
502        // unruled flush policy falls back to the identity (unbatched) one rather
503        // than inventing a tuning value; see `TranscriptBatchPolicy::UNBATCHED`.
504        let batch = required_transcript_batch_policy(&runtime)
505            .unwrap_or(crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED);
506        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
507            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
508        );
509        let drain_state = DrainState::default();
510        let assistant_sessions =
511            build_assistant_sessions(Arc::new(aion_store::InMemoryStore::default()), &runtime);
512        Self {
513            inner: Arc::new(ServerStateInner {
514                namespace_guard: NamespaceGuard::new(namespace_resolver),
515                assistant_sessions,
516                runtime,
517                worker_registry: ConnectedWorkerRegistry::default()
518                    .with_worker_deployment_store(worker_deployment_store.clone())
519                    .with_cluster_publisher(cluster_publisher.clone()),
520                pending_activities,
521                heartbeat_tracker,
522                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
523                drain_state: drain_state.clone(),
524                metrics: None,
525                health: None,
526                activity_mock_registry: None,
527                outbox_store: None,
528                namespace_store,
529                worker_supervisor: new_supervisor(&worker_deployment_store, &cluster_publisher),
530                worker_deployment_store,
531                outbox_wake: Arc::new(tokio::sync::Notify::new()),
532                cluster_publisher,
533                // NOI-5b: a from-parts / embedder state has no durable store, so
534                // the transcript sequencer runs over an in-memory `O`-keyspace
535                // impl — the transcript channel is served on every boot.
536                transcript_publisher: build_transcript_publisher(
537                    None,
538                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
539                    bounds,
540                    batch,
541                ),
542                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
543                queue_service_state: crate::worker::QueueServiceState::default(),
544                queue_declarations: crate::worker::QueueDeclarationSource::default(),
545                // No declared-body dispatcher is built on this path, so nothing
546                // ever registers here; present so the cancel path reads one
547                // registry on every construction rather than an `Option`.
548                declared_attempts: crate::worker::DeclaredCommandAttempts::new(drain_state.clone()),
549                declared_bodies: crate::worker::DeclaredBodySource::default(),
550                // Empty and STAYING empty: this constructor builds no
551                // dispatcher, so no observer ever writes it — the honest
552                // answer for an embedder/test state is "never checked".
553                update_status: crate::update_check::UpdateStatusState::default(),
554                // Inert here: this constructor builds no declared-body
555                // dispatcher, so nothing expands `{workspace_root}` with this
556                // value — it exists so `workspace_root()` is always readable.
557                workspace_root: crate::worker::WorkspaceRoot::resolve(),
558                cluster_self_node: None,
559                cluster_responder: None,
560                cluster_store: None,
561                watched_peers: Vec::new(),
562                shard_directory: None,
563                request_forwarder: None,
564                #[cfg(feature = "auth")]
565                jwks_cache: None,
566            }),
567        }
568    }
569
570    /// Build shared state from explicit parts with one caller-supplied durable
571    /// namespace/worker-deployment leaf and a caller-supplied JWKS cache.
572    ///
573    /// The combined seam of [`Self::from_parts_with_namespace_store`] (seed the
574    /// durable registry the control-plane read/create paths observe) and
575    /// [`Self::from_parts_with_jwks`] (validate bearer tokens against an injected
576    /// issuer): an enumerated caller can exercise the real JWT authorization path
577    /// against a seeded registry without a full [`Self::build`] boot.
578    #[cfg(feature = "auth")]
579    #[must_use]
580    pub fn from_parts_with_namespace_store_and_jwks<S>(
581        namespace_resolver: NamespaceResolver,
582        runtime: RuntimeConfig,
583        store: Arc<S>,
584        jwks_cache: JwksCache,
585    ) -> Self
586    where
587        S: NamespaceStore + WorkerDeploymentStore,
588    {
589        let namespace_store: Arc<dyn NamespaceStore> = store.clone();
590        let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = store;
591        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
592        // Bound here, beside the tracker, for the same reason: `runtime` moves
593        // into the state below, and the transport-loss budget must be read from
594        // the operator's heartbeat window BEFORE it does. The window is a
595        // constructor parameter, so an embedder-booted server cannot reach the
596        // state carrying a budget the operator never declared.
597        let pending_activities = PendingActivities::new(runtime.worker.heartbeat_window);
598        // Computed before `runtime` moves into the state: the retention bounds
599        // flow from `[observability]` config on the embedder path too, so a
600        // from-parts server enforces the same truncation/cap as a full boot.
601        let bounds = transcript_bounds(&runtime);
602        // These constructors bypass config validation and cannot fail, so an
603        // unruled flush policy falls back to the identity (unbatched) one rather
604        // than inventing a tuning value; see `TranscriptBatchPolicy::UNBATCHED`.
605        let batch = required_transcript_batch_policy(&runtime)
606            .unwrap_or(crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED);
607        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
608            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
609        );
610        let drain_state = DrainState::default();
611        let assistant_sessions =
612            build_assistant_sessions(Arc::new(aion_store::InMemoryStore::default()), &runtime);
613        Self {
614            inner: Arc::new(ServerStateInner {
615                namespace_guard: NamespaceGuard::new(namespace_resolver),
616                assistant_sessions,
617                runtime,
618                worker_registry: ConnectedWorkerRegistry::default()
619                    .with_worker_deployment_store(worker_deployment_store.clone())
620                    .with_cluster_publisher(cluster_publisher.clone()),
621                pending_activities,
622                heartbeat_tracker,
623                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
624                drain_state: drain_state.clone(),
625                metrics: None,
626                health: None,
627                activity_mock_registry: None,
628                outbox_store: None,
629                namespace_store,
630                worker_supervisor: new_supervisor(&worker_deployment_store, &cluster_publisher),
631                worker_deployment_store,
632                outbox_wake: Arc::new(tokio::sync::Notify::new()),
633                cluster_publisher,
634                // NOI-5b: a from-parts / embedder state has no durable store, so
635                // the transcript sequencer runs over an in-memory `O`-keyspace
636                // impl — the transcript channel is served on every boot.
637                transcript_publisher: build_transcript_publisher(
638                    None,
639                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
640                    bounds,
641                    batch,
642                ),
643                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
644                queue_service_state: crate::worker::QueueServiceState::default(),
645                queue_declarations: crate::worker::QueueDeclarationSource::default(),
646                // No declared-body dispatcher is built on this path, so nothing
647                // ever registers here; present so the cancel path reads one
648                // registry on every construction rather than an `Option`.
649                declared_attempts: crate::worker::DeclaredCommandAttempts::new(drain_state.clone()),
650                declared_bodies: crate::worker::DeclaredBodySource::default(),
651                // Empty and STAYING empty: this constructor builds no
652                // dispatcher, so no observer ever writes it — the honest
653                // answer for an embedder/test state is "never checked".
654                update_status: crate::update_check::UpdateStatusState::default(),
655                // Inert here: this constructor builds no declared-body
656                // dispatcher, so nothing expands `{workspace_root}` with this
657                // value — it exists so `workspace_root()` is always readable.
658                workspace_root: crate::worker::WorkspaceRoot::resolve(),
659                cluster_self_node: None,
660                cluster_responder: None,
661                cluster_store: None,
662                watched_peers: Vec::new(),
663                shard_directory: None,
664                request_forwarder: None,
665                jwks_cache: Some(jwks_cache),
666            }),
667        }
668    }
669
670    /// Build shared state from explicit parts with a caller-supplied JWKS cache.
671    ///
672    /// Embedders that construct their own [`JwksCache`] (for example against a
673    /// private issuer) can install it here; transports then validate bearer
674    /// tokens against it exactly as with a [`Self::build`]-constructed state.
675    #[cfg(feature = "auth")]
676    #[must_use]
677    pub fn from_parts_with_jwks(
678        namespace_resolver: NamespaceResolver,
679        runtime: RuntimeConfig,
680        jwks_cache: JwksCache,
681    ) -> Self {
682        // No durable store was supplied, so the deployment records and the
683        // supervisor built over them share ONE in-memory leaf: two leaves
684        // would let the supervisor read a record the API never wrote.
685        let fallback_deployment_store: Arc<dyn WorkerDeploymentStore> =
686            Arc::new(aion_store::InMemoryStore::default());
687        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
688        // Bound here, beside the tracker, for the same reason: `runtime` moves
689        // into the state below, and the transport-loss budget must be read from
690        // the operator's heartbeat window BEFORE it does. The window is a
691        // constructor parameter, so an embedder-booted server cannot reach the
692        // state carrying a budget the operator never declared.
693        let pending_activities = PendingActivities::new(runtime.worker.heartbeat_window);
694        // Computed before `runtime` moves into the state: the retention bounds
695        // flow from `[observability]` config on the embedder path too, so a
696        // from-parts server enforces the same truncation/cap as a full boot.
697        let bounds = transcript_bounds(&runtime);
698        // These constructors bypass config validation and cannot fail, so an
699        // unruled flush policy falls back to the identity (unbatched) one rather
700        // than inventing a tuning value; see `TranscriptBatchPolicy::UNBATCHED`.
701        let batch = required_transcript_batch_policy(&runtime)
702            .unwrap_or(crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED);
703        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
704            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
705        );
706        let drain_state = DrainState::default();
707        let assistant_sessions =
708            build_assistant_sessions(Arc::new(aion_store::InMemoryStore::default()), &runtime);
709        Self {
710            inner: Arc::new(ServerStateInner {
711                namespace_guard: NamespaceGuard::new(namespace_resolver),
712                assistant_sessions,
713                runtime,
714                worker_registry: ConnectedWorkerRegistry::default(),
715                pending_activities,
716                heartbeat_tracker,
717                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
718                drain_state: drain_state.clone(),
719                metrics: None,
720                health: None,
721                activity_mock_registry: None,
722                outbox_store: None,
723                // No durable store was supplied (these constructors build state
724                // from a resolver only), so the registry is a local-only
725                // in-memory store — present so `namespace_store()` is always
726                // reachable, never mutating any durable backend.
727                namespace_store: Arc::new(aion_store::InMemoryStore::default()),
728                worker_supervisor: new_supervisor(&fallback_deployment_store, &cluster_publisher),
729                worker_deployment_store: fallback_deployment_store,
730                outbox_wake: Arc::new(tokio::sync::Notify::new()),
731                cluster_publisher,
732                // NOI-5b: a from-parts / embedder state has no durable store, so
733                // the transcript sequencer runs over an in-memory `O`-keyspace
734                // impl — the transcript channel is served on every boot.
735                transcript_publisher: build_transcript_publisher(
736                    None,
737                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
738                    bounds,
739                    batch,
740                ),
741                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
742                queue_service_state: crate::worker::QueueServiceState::default(),
743                queue_declarations: crate::worker::QueueDeclarationSource::default(),
744                // No declared-body dispatcher is built on this path, so nothing
745                // ever registers here; present so the cancel path reads one
746                // registry on every construction rather than an `Option`.
747                declared_attempts: crate::worker::DeclaredCommandAttempts::new(drain_state.clone()),
748                declared_bodies: crate::worker::DeclaredBodySource::default(),
749                // Empty and STAYING empty: this constructor builds no
750                // dispatcher, so no observer ever writes it — the honest
751                // answer for an embedder/test state is "never checked".
752                update_status: crate::update_check::UpdateStatusState::default(),
753                // Inert here: this constructor builds no declared-body
754                // dispatcher, so nothing expands `{workspace_root}` with this
755                // value — it exists so `workspace_root()` is always readable.
756                workspace_root: crate::worker::WorkspaceRoot::resolve(),
757                cluster_self_node: None,
758                cluster_responder: None,
759                cluster_store: None,
760                watched_peers: Vec::new(),
761                shard_directory: None,
762                request_forwarder: None,
763                jwks_cache: Some(jwks_cache),
764            }),
765        }
766    }
767
768    /// Build shared state from explicit parts with a caller-supplied registry.
769    #[must_use]
770    pub fn from_parts_with_registry(
771        namespace_resolver: NamespaceResolver,
772        runtime: RuntimeConfig,
773        worker_registry: ConnectedWorkerRegistry,
774    ) -> Self {
775        // No durable store was supplied, so the deployment records and the
776        // supervisor built over them share ONE in-memory leaf: two leaves
777        // would let the supervisor read a record the API never wrote.
778        let fallback_deployment_store: Arc<dyn WorkerDeploymentStore> =
779            Arc::new(aion_store::InMemoryStore::default());
780        let heartbeat_tracker = HeartbeatTracker::new(runtime.worker.heartbeat_window);
781        // Bound here, beside the tracker, for the same reason: `runtime` moves
782        // into the state below, and the transport-loss budget must be read from
783        // the operator's heartbeat window BEFORE it does. The window is a
784        // constructor parameter, so an embedder-booted server cannot reach the
785        // state carrying a budget the operator never declared.
786        let pending_activities = PendingActivities::new(runtime.worker.heartbeat_window);
787        // Computed before `runtime` moves into the state: the retention bounds
788        // flow from `[observability]` config on the embedder path too, so a
789        // from-parts server enforces the same truncation/cap as a full boot.
790        let bounds = transcript_bounds(&runtime);
791        // These constructors bypass config validation and cannot fail, so an
792        // unruled flush policy falls back to the identity (unbatched) one rather
793        // than inventing a tuning value; see `TranscriptBatchPolicy::UNBATCHED`.
794        let batch = required_transcript_batch_policy(&runtime)
795            .unwrap_or(crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED);
796        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
797            Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
798        );
799        let drain_state = DrainState::default();
800        let assistant_sessions =
801            build_assistant_sessions(Arc::new(aion_store::InMemoryStore::default()), &runtime);
802        Self {
803            inner: Arc::new(ServerStateInner {
804                namespace_guard: NamespaceGuard::new(namespace_resolver),
805                assistant_sessions,
806                runtime,
807                worker_registry,
808                pending_activities,
809                heartbeat_tracker,
810                grpc_liveness_waiters: crate::worker::GrpcLivenessWaiters::new(),
811                drain_state: drain_state.clone(),
812                metrics: None,
813                health: None,
814                activity_mock_registry: None,
815                outbox_store: None,
816                // No durable store was supplied (these constructors build state
817                // from a resolver only), so the registry is a local-only
818                // in-memory store — present so `namespace_store()` is always
819                // reachable, never mutating any durable backend.
820                namespace_store: Arc::new(aion_store::InMemoryStore::default()),
821                worker_supervisor: new_supervisor(&fallback_deployment_store, &cluster_publisher),
822                worker_deployment_store: fallback_deployment_store,
823                outbox_wake: Arc::new(tokio::sync::Notify::new()),
824                cluster_publisher,
825                // NOI-5b: a from-parts / embedder state has no durable store, so
826                // the transcript sequencer runs over an in-memory `O`-keyspace
827                // impl — the transcript channel is served on every boot.
828                transcript_publisher: build_transcript_publisher(
829                    None,
830                    Self::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
831                    bounds,
832                    batch,
833                ),
834                attempt_owners: crate::worker::AttemptOwnerIndex::new(),
835                queue_service_state: crate::worker::QueueServiceState::default(),
836                queue_declarations: crate::worker::QueueDeclarationSource::default(),
837                // No declared-body dispatcher is built on this path, so nothing
838                // ever registers here; present so the cancel path reads one
839                // registry on every construction rather than an `Option`.
840                declared_attempts: crate::worker::DeclaredCommandAttempts::new(drain_state.clone()),
841                declared_bodies: crate::worker::DeclaredBodySource::default(),
842                // Empty and STAYING empty: this constructor builds no
843                // dispatcher, so no observer ever writes it — the honest
844                // answer for an embedder/test state is "never checked".
845                update_status: crate::update_check::UpdateStatusState::default(),
846                // Inert here: this constructor builds no declared-body
847                // dispatcher, so nothing expands `{workspace_root}` with this
848                // value — it exists so `workspace_root()` is always readable.
849                workspace_root: crate::worker::WorkspaceRoot::resolve(),
850                cluster_self_node: None,
851                cluster_responder: None,
852                cluster_store: None,
853                watched_peers: Vec::new(),
854                shard_directory: None,
855                request_forwarder: None,
856                #[cfg(feature = "auth")]
857                jwks_cache: None,
858            }),
859        }
860    }
861
862    /// Borrow the namespace guard shared by all transports.
863    #[must_use]
864    pub fn namespace_guard(&self) -> &NamespaceGuard {
865        &self.inner.namespace_guard
866    }
867
868    /// Build the deploy authorization guard over the shared resolver.
869    #[must_use]
870    pub fn deploy_guard(&self) -> crate::deploy::DeployGuard {
871        crate::deploy::DeployGuard::new(self.inner.namespace_guard.resolver().clone())
872    }
873
874    /// Borrow non-secret runtime settings needed by transports.
875    #[must_use]
876    pub fn runtime_config(&self) -> &RuntimeConfig {
877        &self.inner.runtime
878    }
879
880    /// Borrow the last-completed-update-check slot (#189 slice one).
881    ///
882    /// Written only by the [`crate::update_check::UpdateCheckObserver`] on the
883    /// full boot path; read by `GET /update-status`. Empty until a check has
884    /// genuinely completed — a fresh server has honestly never checked.
885    /// Crate-visible only: the slot's type is implementation detail behind
886    /// the route, not `aion-server` public API.
887    #[must_use]
888    pub(crate) fn update_status(&self) -> &crate::update_check::UpdateStatusState {
889        &self.inner.update_status
890    }
891
892    /// Borrow the server-resolved workspace root declared bodies expand
893    /// `{workspace_root}` with (#139).
894    ///
895    /// The startup banner reads this so composition points learn the value
896    /// from the server that will use it, rather than re-deriving it. That
897    /// banner claim holds for [`Self::build`]-constructed states, where this
898    /// same value is threaded into the declared-body dispatcher; the
899    /// `from_parts*` constructors build no such dispatcher, so their copy is
900    /// inert — readable, but expanded by nothing.
901    #[must_use]
902    pub fn workspace_root(&self) -> &crate::worker::WorkspaceRoot {
903        &self.inner.workspace_root
904    }
905
906    /// Borrow the server-owned assistant sessions.
907    ///
908    /// Always present, and empty on a server whose `[assistant]` section
909    /// declares no harness: the registry's own
910    /// [`availability`](crate::assistant::sessions::AssistantSessions::availability)
911    /// is what says whether a session can be opened, and it says WHY when it
912    /// cannot.
913    #[must_use]
914    pub fn assistant_sessions(&self) -> &crate::assistant::sessions::AssistantSessions {
915        &self.inner.assistant_sessions
916    }
917
918    /// Borrow the connected-worker registry shared by worker transports and dispatch.
919    #[must_use]
920    pub fn worker_registry(&self) -> &ConnectedWorkerRegistry {
921        &self.inner.worker_registry
922    }
923
924    /// Stop every in-flight activity of `workflow_id`, by whichever path is
925    /// executing it (#233).
926    ///
927    /// Joins the three pieces of live state this node holds — the heartbeat
928    /// tracker (which worker holds which activity), the connected-worker
929    /// registry (how to reach that worker), and the declared-attempt registry
930    /// (which commands this server is running itself) — so the cancel handler
931    /// needs only a `ServerState` and never learns the routing itself.
932    ///
933    /// Returns one record per tracked in-flight activity, INCLUDING the ones
934    /// that could not be asked, plus the server-executed declared bodies that
935    /// were signalled. A worker cancel is a request, not a guarantee; a
936    /// declared body's signal reaches its process group.
937    ///
938    /// # Errors
939    ///
940    /// Returns [`ServerError::LockPoisoned`] when the tracker's, the registry's,
941    /// or the declared-attempt registry's state cannot be read.
942    pub fn cancel_in_flight_activities(
943        &self,
944        workflow_id: &aion_core::WorkflowId,
945    ) -> Result<crate::worker::InFlightCancellation, ServerError> {
946        crate::worker::cancel_in_flight_activities(
947            self.heartbeat_tracker(),
948            self.worker_registry(),
949            self.declared_attempts(),
950            workflow_id,
951        )
952    }
953
954    /// Borrow the registry of declared bodies this server is executing.
955    ///
956    /// The declared-body dispatcher registers each attempt here for the life of
957    /// its command; [`Self::cancel_in_flight_activities`] signals through it.
958    #[must_use]
959    pub fn declared_attempts(&self) -> &crate::worker::DeclaredCommandAttempts {
960        &self.inner.declared_attempts
961    }
962
963    /// Borrow the declared-body catalog source both dispatch paths consult —
964    /// the direct decorator over the engine's dispatch hook, and the outbox
965    /// row decorator (aion#193). One source, so a body is either declared on
966    /// every path or on none.
967    #[must_use]
968    pub fn declared_bodies(&self) -> &crate::worker::DeclaredBodySource {
969        &self.inner.declared_bodies
970    }
971
972    /// Borrow the WS3 cluster-event publisher shared by the cluster state-change
973    /// sites (supervisor, worker registry) and the cluster subscription endpoint.
974    /// Always present, on every boot.
975    #[must_use]
976    pub fn cluster_publisher(&self) -> &crate::cluster_publisher::ClusterEventPublisher {
977        &self.inner.cluster_publisher
978    }
979
980    /// Borrow the NOI-5b transcript sequencer shared by the worker->server
981    /// ingestion seam (which publishes a running activity's `ActivityEvent`s) and
982    /// the transcript subscription endpoint (which tails + resumes them). Always
983    /// present, on every boot.
984    #[must_use]
985    pub fn transcript_publisher(&self) -> &crate::activity_publisher::ActivityEventPublisher {
986        &self.inner.transcript_publisher
987    }
988
989    /// Borrow the NOI-6 `attempt -> owning-worker` back-index. The agent-dispatch
990    /// path binds an owner when it dispatches an agent attempt and releases it on
991    /// completion, so the intervention router always resolves the CURRENT owner.
992    #[must_use]
993    pub fn attempt_owners(&self) -> &crate::worker::AttemptOwnerIndex {
994        &self.inner.attempt_owners
995    }
996
997    /// Borrow the R1 live unserved-queue state — the same instance the bridge
998    /// dispatcher publishes every parked dispatch into.
999    #[must_use]
1000    pub fn queue_service_state(&self) -> &crate::worker::QueueServiceState {
1001        &self.inner.queue_service_state
1002    }
1003
1004    /// Borrow the R1 queue-declaration source — the engine-backed reader the
1005    /// bridge classifies against. Answers `Unknown` on a state built without an
1006    /// engine, which never refuses anything.
1007    #[must_use]
1008    pub fn queue_declarations(&self) -> &crate::worker::QueueDeclarationSource {
1009        &self.inner.queue_declarations
1010    }
1011
1012    /// Every queue address currently unserved, with its taxonomy reason, the
1013    /// policy it is held under, the live poller census behind the verdict, and
1014    /// the runs parked on it.
1015    ///
1016    /// This is the server-side answer to "is anything stuck, and on what" — the
1017    /// question the pre-R1 seam could only be asked by reading logs.
1018    ///
1019    /// # Errors
1020    ///
1021    /// Returns [`ServerError::LockPoisoned`] if the state lock is poisoned.
1022    pub fn unserved_queues(&self) -> Result<Vec<crate::worker::UnservedQueue>, ServerError> {
1023        self.inner.queue_service_state.unserved()
1024    }
1025
1026    /// Every run this engine process could not make resident, with the reason it
1027    /// could not and when the failure was observed (#117).
1028    ///
1029    /// This is the fleet half of the degraded-residency question. `POST
1030    /// /workflows/describe` answers it for a run an operator can already name;
1031    /// this answers it for the operator who cannot, which is the case that made
1032    /// the original defect unrecoverable in practice — the id was only ever
1033    /// printed in a boot log line that had scrolled away.
1034    ///
1035    /// The set is per-process and self-clearing: an entry disappears the moment
1036    /// the engine observes that run resident, so an EMPTY list is the healthy
1037    /// answer and never a stale one.
1038    ///
1039    /// # Errors
1040    ///
1041    /// Returns [`ServerError`] when the state carries no engine handle, or when
1042    /// the registry lock is poisoned.
1043    pub fn unrecoverable_runs(
1044        &self,
1045    ) -> Result<Vec<(aion_core::WorkflowId, aion::registry::UnrecoverableRun)>, ServerError> {
1046        self.engine()?
1047            .registry()
1048            .unrecoverable()
1049            .list()
1050            .map_err(ServerError::from)
1051    }
1052
1053    /// Build the NOI-6 intervention router over the connected-worker registry, the
1054    /// attempt-owner back-index, and the active intervention transport.
1055    ///
1056    /// The transport is the liminal server-push
1057    /// ([`LiminalInterventionTransport`](crate::worker::LiminalInterventionTransport))
1058    /// when the `liminal-transport` feature is compiled in — the production path
1059    /// that pushes a routed command out on the owning worker's connection — and a
1060    /// null transport otherwise, which reports the target unreachable so every
1061    /// command NACKs the attempt-scoped no-op rather than silently vanishing. The
1062    /// router is cheap to build (it clones cloneable handles), so it is constructed
1063    /// per request at the endpoint rather than stored.
1064    #[must_use]
1065    pub fn intervention_router(&self) -> crate::worker::InterventionRouter {
1066        let transport: std::sync::Arc<dyn crate::worker::InterventionTransport> = {
1067            #[cfg(feature = "liminal-transport")]
1068            {
1069                std::sync::Arc::new(crate::worker::LiminalInterventionTransport)
1070            }
1071            #[cfg(not(feature = "liminal-transport"))]
1072            {
1073                std::sync::Arc::new(NullInterventionTransport)
1074            }
1075        };
1076        crate::worker::InterventionRouter::new(
1077            self.inner.worker_registry.clone(),
1078            self.inner.attempt_owners.clone(),
1079            transport,
1080        )
1081        // Lane #229: an APPLIED InjectMessage is teed into the durable
1082        // transcript, so the retained record holds the operator's words.
1083        .with_transcript_publisher(self.inner.transcript_publisher.clone())
1084    }
1085
1086    /// This node's configured cluster distribution name for the WS3 snapshot
1087    /// self-identity, or `None` on a single-node boot (the snapshot then reports
1088    /// the standalone self-label).
1089    #[must_use]
1090    pub fn cluster_self_node(&self) -> Option<&str> {
1091        self.inner.cluster_self_node.as_deref()
1092    }
1093
1094    /// Clone the live engine handle the completion path records terminals through.
1095    ///
1096    /// This is the SAME `Arc<Engine>` the gRPC completion callback is built over
1097    /// (state.rs installs `ServerOutboxDeliveryCallback::new(engine)` on the
1098    /// pending tracker when `outbox.enabled`), so the liminal completion path
1099    /// re-enters worker results through the identical `record_fan_out_completion`
1100    /// seam rather than inventing a second one.
1101    ///
1102    /// # Errors
1103    ///
1104    /// Returns [`ServerError`] when the namespace resolver has no engine handle
1105    /// (a state built from parts without an engine).
1106    pub fn engine(&self) -> Result<Arc<aion::Engine>, ServerError> {
1107        self.inner
1108            .namespace_guard
1109            .resolver()
1110            .engine()
1111            .map(Arc::clone)
1112    }
1113
1114    /// Borrow the pending-activities tracker shared by the NIF bridge and worker stream handler.
1115    #[must_use]
1116    pub fn pending_activities(&self) -> &PendingActivities {
1117        &self.inner.pending_activities
1118    }
1119
1120    /// What THIS install says about itself on every describe and list
1121    /// response (ADR-016, WA-010 R4): the leases it knew and failed to record
1122    /// since boot, read live from the dispatchers' shared ledger.
1123    #[must_use]
1124    pub fn read_provenance(&self) -> aion_core::ReadProvenance {
1125        aion_core::ReadProvenance::new(
1126            self.inner
1127                .pending_activities
1128                .lease_recorder()
1129                .ledger()
1130                .failures(),
1131        )
1132    }
1133
1134    /// Borrow the heartbeat/liveness tracker shared by dispatch and worker streams.
1135    #[must_use]
1136    pub fn heartbeat_tracker(&self) -> &HeartbeatTracker {
1137        &self.inner.heartbeat_tracker
1138    }
1139
1140    /// Borrow the gRPC liveness answer-correlation registry (#197).
1141    ///
1142    /// The worker stream handler delivers each `LivenessAnswer` into it; the
1143    /// liveness probe arms and awaits through the SAME handle. There is exactly
1144    /// one per server, for the same reason there is exactly one probe.
1145    #[must_use]
1146    pub fn grpc_liveness_waiters(&self) -> &crate::worker::GrpcLivenessWaiters {
1147        &self.inner.grpc_liveness_waiters
1148    }
1149
1150    /// Borrow the drain gate shared by transports and worker dispatch.
1151    #[must_use]
1152    pub fn drain_state(&self) -> &DrainState {
1153        &self.inner.drain_state
1154    }
1155
1156    /// Borrow the prometheus metrics handle when this state was built with a store.
1157    #[must_use]
1158    pub fn metrics(&self) -> Option<&Metrics> {
1159        self.inner.metrics.as_ref()
1160    }
1161
1162    /// Borrow health probe state when this state was built with a store.
1163    #[must_use]
1164    pub fn health(&self) -> Option<&HealthState> {
1165        self.inner.health.as_ref()
1166    }
1167
1168    /// Borrow the shared per-run activity-mock registry when the dev surface is
1169    /// commissioned. Returns [`None`] on a server with the dev surface dark, so
1170    /// the dev handlers refuse cleanly rather than mocking on a production
1171    /// server.
1172    #[must_use]
1173    pub fn activity_mock_registry(&self) -> Option<&ActivityMockRegistry> {
1174        self.inner.activity_mock_registry.as_ref()
1175    }
1176
1177    /// Borrow the outbox store the dispatcher claims rows from, when the durable
1178    /// (haematite) backend is in use. This is the SAME leaf `Arc<HaematiteStore>` the
1179    /// engine writes through, so the dispatcher shares its single
1180    /// `haematite::Connection` rather than opening a second contending one. Returns
1181    /// [`None`] for the in-memory backend, which has no outbox table.
1182    #[must_use]
1183    pub fn outbox_store(&self) -> Option<Arc<dyn OutboxStore>> {
1184        self.inner.outbox_store.clone()
1185    }
1186
1187    /// Borrow the durable namespace registry shared by the control plane.
1188    ///
1189    /// This is the SAME concrete leaf backend the engine writes events through
1190    /// (haematite quorum-replicated, or in-memory local-only),
1191    /// captured as a [`NamespaceStore`] before the decorator chain wrapped it.
1192    /// Always present on every boot, so the mint-on-register path (Phase 1 S5)
1193    /// and `GET /namespaces` (S7) can reach a real registry regardless of
1194    /// backend.
1195    #[must_use]
1196    pub fn namespace_store(&self) -> &Arc<dyn NamespaceStore> {
1197        &self.inner.namespace_store
1198    }
1199
1200    /// Borrow the durable worker-deployment store shared by the control plane.
1201    #[must_use]
1202    pub fn worker_deployment_store(&self) -> &Arc<dyn WorkerDeploymentStore> {
1203        &self.inner.worker_deployment_store
1204    }
1205
1206    /// Borrow the managed-worker supervisor.
1207    ///
1208    /// Built over the SAME durable deployment store as
1209    /// [`Self::worker_deployment_store`], so desired state written through the
1210    /// API is the desired state the supervisor converges on. Uncommissioned
1211    /// until [`crate::run`] installs an operator policy.
1212    #[must_use]
1213    pub fn worker_supervisor(&self) -> &Arc<WorkerSupervisor> {
1214        &self.inner.worker_supervisor
1215    }
1216
1217    /// Build the shared minted-on-use hook over the durable namespace store and
1218    /// the configured [`AutoCreate`](crate::config::AutoCreate) policy.
1219    ///
1220    /// This is the SAME policy logic the worker-registration seam applies (S5);
1221    /// the workflow-start safety net (S6) calls it after authorization so a
1222    /// client that starts a workflow before any worker registers still gets a
1223    /// durable namespace record. Cheap to build (clones an `Arc` + a `Copy`
1224    /// policy), so transports construct it per request rather than holding it.
1225    #[must_use]
1226    pub fn namespace_minter(&self) -> NamespaceMinter {
1227        let minter = NamespaceMinter::new(
1228            Arc::clone(&self.inner.namespace_store),
1229            self.inner.runtime.auto_create,
1230        )
1231        // Thread the deployment-global cluster channel so the start-time safety
1232        // net (S6) and the explicit `POST /namespaces` path (S7) emit the same
1233        // live "namespace created" delta the worker-mint seam (S5) does — all
1234        // three mint choke-points surface on the one ops-console push channel.
1235        .with_cluster_publisher(self.inner.cluster_publisher.clone());
1236        // And the namespace-mint routing context on a clustered boot, so a
1237        // namespace whose registry shard this node does not own is minted by the
1238        // node that does rather than being fenced forever. `None` off-cluster,
1239        // where the minter is byte-identical to before routing existed.
1240        match self.namespace_routing() {
1241            Some(routing) => minter.with_routing(routing),
1242            None => minter,
1243        }
1244    }
1245
1246    /// The namespace-mint routing context for this boot, or `None` when there is
1247    /// nothing to route to.
1248    ///
1249    /// Present only when ALL THREE handles exist: the distributed store (which
1250    /// hashes a namespace to its registry shard), the R-2 shard directory (which
1251    /// resolves that shard's current owner), and the R-3 request forwarder (which
1252    /// dials it). Those three are populated together by `build_routing_state` on
1253    /// a `[store.cluster]` boot and are all `None` otherwise, so a partial
1254    /// context can never arise — but each is checked rather than assumed.
1255    #[must_use]
1256    pub fn namespace_routing(&self) -> Option<crate::namespace::NamespaceRouting> {
1257        build_namespace_routing(
1258            self.cluster_store(),
1259            self.shard_directory(),
1260            self.request_forwarder(),
1261        )
1262    }
1263
1264    /// Clone the advisory outbox wake (LSUB-2) shared with the engine's stage
1265    /// seam. The outbox dispatcher installs this handle so a committed fan-out row
1266    /// wakes its run loop in ~RTT rather than waiting for the next poll tick. The
1267    /// handle is always present; it is simply never pulsed when the outbox is not
1268    /// commissioned, so wiring it is free and behaviour is unchanged.
1269    #[must_use]
1270    pub fn outbox_wake(&self) -> Arc<tokio::sync::Notify> {
1271        Arc::clone(&self.inner.outbox_wake)
1272    }
1273
1274    /// Whether this server is a node in a distributed haematite cluster.
1275    ///
1276    /// `true` when boot constructed the distributed backend (a `[store.cluster]`
1277    /// section was present) and is holding its inbound-write responder alive;
1278    /// `false` for every single-node / non-haematite boot.
1279    #[must_use]
1280    pub fn is_clustered(&self) -> bool {
1281        self.inner.cluster_responder.is_some()
1282    }
1283
1284    /// The concrete distributed haematite store the request-routing edge consults
1285    /// for shard ownership (`shard_for_workflow` / `owns_workflow_shard`) and
1286    /// unsteered-start remint. `None` for every single-node / non-clustered boot,
1287    /// so the routing pre-step is a no-op and the default path is unchanged.
1288    #[must_use]
1289    pub fn cluster_store(&self) -> Option<&Arc<aion_store_haematite::HaematiteStore>> {
1290        self.inner.cluster_store.as_ref()
1291    }
1292
1293    /// The request-routing shard directory (R-2) the edge consults to resolve a
1294    /// non-owned shard's owner. `None` for single-node / non-clustered boots, so
1295    /// the edge falls back to the bare R-1 ownership check.
1296    #[must_use]
1297    pub fn shard_directory(&self) -> Option<&Arc<crate::routing::StaticShardDirectory>> {
1298        self.inner.shard_directory.as_ref()
1299    }
1300
1301    /// The R-3 request forwarder used to relay a non-local signal/query/cancel to
1302    /// the shard owner. `None` for single-node / non-clustered boots.
1303    #[must_use]
1304    pub fn request_forwarder(&self) -> Option<&Arc<dyn crate::routing::RequestForwarder>> {
1305        self.inner.request_forwarder.as_ref()
1306    }
1307
1308    /// Spawn the worker heartbeat expiry sweeper (#176): the production driver
1309    /// of [`HeartbeatTracker::fail_expired_workers`], failing every worker with
1310    /// an in-flight task beyond the operator's `worker.heartbeat_window` and
1311    /// deregistering it with the provable
1312    /// [`WorkerDeathReason::Timeout`](aion_core::WorkerDeathReason::Timeout).
1313    ///
1314    /// Always spawned on the server boot path — dead-worker detection is a
1315    /// liveness correctness property, not an opt-in feature. The cadence is
1316    /// derived from the heartbeat window
1317    /// ([`sweep_interval`](crate::worker::sweep_interval): a quarter of the
1318    /// window clamped to `[1s, window]`, so the default 30s window sweeps every
1319    /// 7.5s); there is deliberately no separate config knob. The task exits
1320    /// when `shutdown` flips to `true`, exactly like the transports; the
1321    /// returned handle may be dropped to detach it (dropping a tokio
1322    /// `JoinHandle` never cancels the task) and is returned so tests can await
1323    /// clean shutdown.
1324    #[must_use]
1325    pub fn spawn_heartbeat_sweeper(
1326        &self,
1327        shutdown: tokio::sync::watch::Receiver<bool>,
1328    ) -> tokio::task::JoinHandle<()> {
1329        let sweeper = crate::worker::HeartbeatSweeper::new(
1330            self.inner.heartbeat_tracker.clone(),
1331            self.inner.worker_registry.clone(),
1332            self.inner.pending_activities.clone(),
1333            self.inner.drain_state.clone(),
1334            self.inner.runtime.worker.heartbeat_window,
1335        )
1336        // The SAME queue-service state the bridge parks dispatches into, so a
1337        // deregistration names how many dispatches are already stranded on the
1338        // queue the dead worker was serving.
1339        .with_queue_state(self.inner.queue_service_state.clone());
1340        tokio::spawn(sweeper.run(shutdown))
1341    }
1342
1343    /// Spawn the startup catch-up legs — owed timer fires, schedule-
1344    /// coordinator catch-up, schedule recovery — behind already-open doors.
1345    ///
1346    /// [`boot_engine`] runs only the workflow-residency recovery leg before
1347    /// the transports bind, because the catch-up backlog has no upper bound
1348    /// (an estate watcher down for hours owes thousands of fires) and a boot
1349    /// that blocks on it keeps every door shut for the whole sweep. Every
1350    /// catch-up fire is the same idempotent record-once delivery the live
1351    /// timer wheel performs against a serving engine, so running it
1352    /// concurrently with the transports is the steady-state contract.
1353    ///
1354    /// The task stops when `shutdown` flips: catch-up left unfinished at
1355    /// shutdown is exactly a boot interrupted mid-sweep, and the next boot's
1356    /// sweep re-derives the remaining owed fires from durable state. A
1357    /// catch-up error is reported with its remedy and does NOT kill the
1358    /// serving process — the doors stay open, and a restart re-runs the
1359    /// sweep from durable state.
1360    ///
1361    /// # Errors
1362    ///
1363    /// Returns [`ServerError`] when the state holds no engine handle (a state
1364    /// built from parts without an engine) — such a state also never deferred
1365    /// any recovery, so there is nothing to catch up.
1366    pub fn spawn_startup_catchup(
1367        &self,
1368        mut shutdown: tokio::sync::watch::Receiver<bool>,
1369    ) -> Result<tokio::task::JoinHandle<()>, ServerError> {
1370        let engine = self.engine()?;
1371        Ok(tokio::spawn(async move {
1372            tracing::info!(
1373                "startup catch-up running behind open doors: owed timer fires, \
1374                 schedule catch-up"
1375            );
1376            let catchup = engine.run_startup_catchup();
1377            tokio::pin!(catchup);
1378            tokio::select! {
1379                result = &mut catchup => match result {
1380                    Ok(()) => {
1381                        tracing::info!("startup catch-up complete");
1382                    }
1383                    Err(error) => {
1384                        tracing::error!(
1385                            %error,
1386                            "startup catch-up failed; owed timer fires and schedule \
1387                             catch-up remain undelivered — restart the server to re-run \
1388                             the sweep from durable state"
1389                        );
1390                    }
1391                },
1392                _ = shutdown.changed() => {
1393                    tracing::info!(
1394                        "startup catch-up interrupted by shutdown; the next boot's \
1395                         sweep resumes from durable state"
1396                    );
1397                }
1398            }
1399        }))
1400    }
1401
1402    /// Spawn the liminal connection dead-man switch (the liveness probe) over
1403    /// `notifier`.
1404    ///
1405    /// Always spawned on a boot that hosts the liminal worker listener:
1406    /// connection liveness is a correctness property of the transport, not an
1407    /// opt-in feature. Both timings derive from the operator's
1408    /// `worker.heartbeat_window` — see
1409    /// [`LivenessProbe`](crate::worker::LivenessProbe) — so there is no separate
1410    /// knob. The task exits when `shutdown` flips to `true`, exactly like the
1411    /// heartbeat sweeper and the transports.
1412    #[cfg(feature = "liminal-transport")]
1413    #[must_use]
1414    pub fn spawn_liminal_liveness_probe(
1415        &self,
1416        notifier: std::sync::Arc<crate::worker::LiminalConnectionNotifier>,
1417        shutdown: tokio::sync::watch::Receiver<bool>,
1418    ) -> tokio::task::JoinHandle<()> {
1419        let probe = crate::worker::LivenessProbe::across_transports(
1420            Some(notifier),
1421            // #197: the SAME probe covers gRPC-delivered workers. One probe,
1422            // one probation, one eligibility set — two probes would each
1423            // publish a whole verdict over the other's, because publication is
1424            // a replacement rather than a merge.
1425            Some(self.inner.grpc_liveness_waiters.clone()),
1426            self.inner.heartbeat_tracker.clone(),
1427            self.inner.worker_registry.clone(),
1428            self.inner.runtime.worker.heartbeat_window,
1429        );
1430        tokio::spawn(probe.run(shutdown))
1431    }
1432
1433    /// Spawn the SS-5b cluster supervisor: a background task that watches every
1434    /// declared peer's replication liveness and, on a confirmed peer death,
1435    /// calls `adopt_shards` for that peer's shards on THIS node's live engine —
1436    /// automatic failover with no manual trigger.
1437    ///
1438    /// Does nothing (returns `Ok(())` without spawning) unless this is a
1439    /// distributed boot whose cluster config declared at least one peer with
1440    /// `owned_shards`. A single-node / non-clustered server therefore never runs
1441    /// a supervisor, so default behaviour is unchanged.
1442    ///
1443    /// The spawned task drains on `shutdown` exactly like the transports.
1444    ///
1445    /// # Errors
1446    ///
1447    /// Returns [`ServerError`] when the engine handle cannot be resolved.
1448    pub fn spawn_cluster_supervisor(
1449        &self,
1450        config: crate::cluster::SupervisorConfig,
1451        shutdown: tokio::sync::watch::Receiver<bool>,
1452    ) -> Result<bool, ServerError> {
1453        let Some(cluster_store) = self.inner.cluster_store.clone() else {
1454            return Ok(false);
1455        };
1456        if self.inner.watched_peers.is_empty() {
1457            return Ok(false);
1458        }
1459        let engine = Arc::clone(self.inner.namespace_guard.resolver().engine()?);
1460        // WS3: feed cluster topology deltas from the supervisor's existing
1461        // decision points into the ops console channel. `self_node` is the
1462        // configured distribution name (already captured for the snapshot).
1463        let publisher = Arc::new(self.inner.cluster_publisher.clone());
1464        let self_node = self.inner.cluster_self_node.clone().unwrap_or_default();
1465        // #253: adoption re-runs the terminal-workflow outbox settlement sweep
1466        // over the widened owned-shard scope, so a dead peer's stranded row for
1467        // a terminal workflow is settled — never re-armed — by its adopter.
1468        // With no outbox commissioned there is nothing to settle and the
1469        // adopter delegates straight to the engine.
1470        let adopter = Arc::new(crate::cluster::OutboxSettlingAdopter::new(
1471            engine,
1472            self.inner.outbox_store.clone(),
1473        ));
1474        let supervisor = crate::cluster::ClusterSupervisor::new(
1475            cluster_store,
1476            adopter,
1477            self.inner.watched_peers.clone(),
1478            config,
1479        )
1480        .with_publisher(publisher, self_node);
1481        if !supervisor.watches_any() {
1482            return Ok(false);
1483        }
1484        tokio::spawn(supervisor.run(shutdown));
1485        Ok(true)
1486    }
1487
1488    /// Borrow the shared JWKS cache when authentication is enabled.
1489    #[cfg(feature = "auth")]
1490    #[must_use]
1491    pub fn jwks_cache(&self) -> Option<&JwksCache> {
1492        self.inner.jwks_cache.as_ref()
1493    }
1494
1495    /// Shut down the embedded engine so in-flight durable appends can finish.
1496    ///
1497    /// # Errors
1498    ///
1499    /// Returns [`ServerError`] if the namespace resolver has no engine handle or the engine rejects
1500    /// shutdown.
1501    pub fn shutdown(&self) -> Result<(), ServerError> {
1502        self.inner.namespace_guard.resolver().shutdown_engine()
1503    }
1504}
1505
1506#[cfg(feature = "auth")]
1507async fn build_jwks_cache(runtime: &RuntimeConfig) -> Result<Option<JwksCache>, ServerError> {
1508    if !runtime.auth.enabled {
1509        return Ok(None);
1510    }
1511    let Some(url) = runtime.auth.jwks_url.clone() else {
1512        return Err(ServerError::Config {
1513            message: "auth.jwks_url must not be empty when auth.enabled is true".to_owned(),
1514        });
1515    };
1516    let interval = std::time::Duration::from_secs(runtime.auth.jwks_refresh_seconds);
1517    let cache = JwksCache::new(url, interval)
1518        .await
1519        .map_err(|error| ServerError::Config {
1520            message: format!("auth jwks initial fetch failed: {error}"),
1521        })?;
1522    Ok(Some(cache))
1523}
1524
1525fn metrics_config_error(error: &MetricsError) -> ServerError {
1526    ServerError::Config {
1527        message: error.to_string(),
1528    }
1529}
1530
1531/// Borrowed inputs assembled into the embedded engine by [`boot_engine`].
1532struct EngineAssembly<'a> {
1533    /// The worker seams installed onto the engine before startup recovery runs.
1534    seams: &'a WorkerSeams,
1535    /// The metrics-instrumented store the engine writes through.
1536    instrumented_store: &'a Arc<InstrumentedEventStore>,
1537    /// Explicitly-sized broadcast channel capacity for `/events/stream`.
1538    event_broadcast_capacity: std::num::NonZeroUsize,
1539    /// Explicit workflow-query reply deadline for `/workflows/query`.
1540    query_timeout: std::time::Duration,
1541    /// The registration store the workloop cadence service sweeps, captured
1542    /// from the SAME concrete leaf as the event store before the decorator
1543    /// chain wrapped it.
1544    workloop_store: Arc<dyn aion_store::workloop::WorkloopStore>,
1545    /// The visibility projection the engine's Recorders write and every list
1546    /// surface reads — the SAME concrete leaf as the event store, so the
1547    /// projection is as durable as the histories it projects.
1548    visibility_store: Arc<dyn aion_store::visibility::VisibilityStore>,
1549    /// Explicit workloop sweep interval, which bounds dead-man detection
1550    /// latency for every registered loop.
1551    workloop_sweep_interval: std::time::Duration,
1552    /// The activity dispatcher (optionally dev-mock-decorated) the engine uses.
1553    activity_dispatcher: Arc<dyn ActivityDispatcher>,
1554    /// The shared active-workflow registry server dispatchers correlate against.
1555    active_registry: Arc<aion::Registry>,
1556    /// Whether THIS node seeds the schedule coordinator (SS-2 ownership gate).
1557    bootstrap_coordinator: bool,
1558    /// Non-secret runtime settings driving scheduler/outbox/package/shard knobs.
1559    runtime: &'a RuntimeConfig,
1560    /// Where this boot narrates its stages. Startup workflow recovery is the
1561    /// last unbounded leg before the doors open, and on a large estate it is
1562    /// long enough that an operator needs to be told it is happening.
1563    stage: &'a crate::control::StageReporter,
1564}
1565
1566/// The search-attribute schema every server-embedded engine runs with.
1567///
1568/// The engine REFUSES an append carrying an unregistered attribute, and this is
1569/// the only place the server registers any. So every attribute name a server
1570/// writer records must appear here or the write it rides on fails outright — a
1571/// missing `display_name` registration does not lose the label, it fails every
1572/// named start (#211). Kept as its own function so that coupling is testable
1573/// against the actual writers rather than only observable at boot.
1574fn server_search_attribute_schema() -> Result<aion_core::SearchAttributeSchema, ServerError> {
1575    let mut schema = aion_core::SearchAttributeSchema::new();
1576    for (name, label) in [
1577        (crate::namespace::NAMESPACE_ATTRIBUTE, "namespace"),
1578        (crate::namespace::TASK_QUEUE_ATTRIBUTE, "task_queue"),
1579        (crate::namespace::DISPLAY_NAME_ATTRIBUTE, "display_name"),
1580    ] {
1581        schema
1582            .register(name, aion_core::SearchAttributeType::String)
1583            .map_err(|error| ServerError::Config {
1584                message: format!("failed to register {label} search attribute: {error}"),
1585            })?;
1586    }
1587    Ok(schema)
1588}
1589
1590/// Assemble the embedded engine from the server's runtime configuration and
1591/// bring it to serving state: build, install every engine-backed seam, then run
1592/// startup recovery — in that order, enforced here by construction rather than
1593/// by call-site discipline.
1594///
1595/// Factored out of [`ServerState::build_with_connected_store`]; it carries the
1596/// SS-2 wiring — the coordinator bootstrap gate fed from real ownership and the
1597/// `owned_shards` hook that drives both scoping and the per-shard election
1598/// before recovery.
1599async fn boot_engine(assembly: EngineAssembly<'_>) -> Result<Arc<aion::Engine>, ServerError> {
1600    let search_attribute_schema = server_search_attribute_schema()?;
1601    let runtime = assembly.runtime;
1602    let builder = EngineBuilder::new()
1603        .store_arc(assembly.instrumented_store.clone())
1604        .event_streaming(assembly.event_broadcast_capacity)
1605        .visibility_store_arc(assembly.visibility_store)
1606        .search_attribute_schema(search_attribute_schema)
1607        .scheduler_threads(runtime.scheduler_threads)
1608        // AE-017: the operator's no-progress stop bound, validated non-zero
1609        // at load; re-checked here so a state built around the loader cannot
1610        // hand the engine an absent bound.
1611        .stop_drain_timeout(
1612            runtime
1613                .stop_drain_timeout
1614                .ok_or_else(|| ServerError::Config {
1615                    message: String::from(crate::config::STOP_DRAIN_TIMEOUT_REQUIRED),
1616                })?,
1617        )
1618        .outbox_enabled(runtime.outbox.enabled)
1619        .activity_dispatcher(assembly.activity_dispatcher)
1620        .active_registry(assembly.active_registry)
1621        .production_recovery_seam()
1622        // #266: recovery replay re-dispatches every in-flight activity through
1623        // the dispatcher decorated above, and that dispatcher consults seams
1624        // (the declared-body source foremost) that are only installed once the
1625        // engine exists. Defer recovery out of `build()`;
1626        // `build_with_connected_store` runs it via `run_startup_recovery`
1627        // immediately after `install_engine_backed_seams`.
1628        .defer_startup_recovery()
1629        .signal_router_factory(|runtime: Arc<RuntimeHandle>, handoff| {
1630            Arc::new(ConcreteSignalRouter::new(runtime, handoff)) as Arc<dyn SignalRouter>
1631        })
1632        .query_timeout(assembly.query_timeout)
1633        // 🔴 THE WORKLOOP SERVICE. Without this call the engine's `workloop`
1634        // slot stays `None`, the `close_iteration/3` NIF bridge is never
1635        // installed, and every workloop verb refuses by name — so a correctly
1636        // compiled loop would deploy, start, run one iteration and be REFUSED
1637        // at its first generation boundary. It is wired UNCONDITIONALLY
1638        // because both store backends implement `WorkloopStore`: whether a
1639        // loop can park must not depend on which store the operator chose.
1640        .with_workloop_service(
1641            Arc::clone(&assembly.workloop_store),
1642            assembly.workloop_sweep_interval,
1643        )
1644        // SS-2: only the node owning the schedule-coordinator's shard seeds and
1645        // serves it. `true` for every non-distributed boot (owns all shards); a
1646        // distributed non-owner passes `false` so it does not fence the
1647        // coordinator stream (AA-4-4). Default `true`, so a single-node boot is
1648        // byte-identical to today.
1649        .bootstrap_schedule_coordinator(assembly.bootstrap_coordinator)
1650        .load_workflow_sources(runtime.workflow_packages.iter().map(PathBuf::as_path));
1651    // Owned-shard assignment: when the operator pins this node to a shard subset,
1652    // scope the engine to it AND (SS-2) elect those shards before recovery — the
1653    // builder's `owned_shards` hook drives both. Empty (the default) leaves the
1654    // builder untouched, so single-node boot owns ALL shards, elects nothing, and
1655    // is byte-identical to today.
1656    let builder = if runtime.owned_shards.is_empty() {
1657        builder
1658    } else {
1659        builder.owned_shards(runtime.owned_shards.iter().copied())
1660    };
1661    // JIT compilation threshold: applied ONLY when the operator named one, so
1662    // absent stays absent all the way down to beamr rather than becoming a
1663    // value the server picked. Same shape as `owned_shards` above — an unset
1664    // knob leaves the builder untouched and the boot is byte-identical to a
1665    // tree without this field.
1666    let builder = match runtime.jit_threshold {
1667        Some(threshold) => builder.scheduler_jit_threshold(threshold),
1668        None => builder,
1669    };
1670    let engine = Arc::new(builder.build().await.map_err(ServerError::from)?);
1671    install_engine_backed_seams(assembly.seams, &engine, runtime.outbox.enabled);
1672    // #266: workflow recovery runs HERE — after every seam the decorated
1673    // activity dispatcher consults is installed, never before. The build
1674    // above deferred it; running it earlier re-dispatched adopted
1675    // in-flight declared-body activities into an empty body source, and
1676    // they parked forever on their own queue (the fleet e2e pins this).
1677    //
1678    // Instant doors: ONLY the workflow-residency leg blocks the boot. The
1679    // catch-up legs (owed timer fires, schedule catch-up) have no upper
1680    // bound — an estate watcher can owe thousands of fires — and every fire
1681    // goes through the same idempotent record-once path the live wheel
1682    // uses, so the run loop starts them behind already-open doors via
1683    // [`ServerState::spawn_startup_catchup`].
1684    assembly.stage.report(
1685        crate::control::stage::STAGE_ENGINE_RECOVERY,
1686        "recovering resident workflows from durable state".to_owned(),
1687    );
1688    engine
1689        .recover_workflows_on_startup()
1690        .await
1691        .map_err(ServerError::from)?;
1692    assembly.stage.report(
1693        crate::control::stage::STAGE_ENGINE_RECOVERY,
1694        "resident workflows recovered".to_owned(),
1695    );
1696    Ok(engine)
1697}
1698
1699/// Validate the two engine seams the server unconditionally mounts: the event
1700/// broadcast channel capacity (`/events/stream`) and the query reply deadline
1701/// (`/workflows/query`). Both are explicit-no-default — a mounted-but-
1702/// unconfigured surface is never acceptable.
1703fn required_engine_seams(
1704    runtime: &RuntimeConfig,
1705) -> Result<
1706    (
1707        std::num::NonZeroUsize,
1708        std::time::Duration,
1709        std::time::Duration,
1710    ),
1711    ServerError,
1712> {
1713    let event_broadcast_capacity = runtime
1714        .websocket
1715        .event_broadcast_capacity
1716        .and_then(std::num::NonZeroUsize::new)
1717        .ok_or_else(|| ServerError::Config {
1718            message: crate::config::EVENT_BROADCAST_CAPACITY_REQUIRED.to_owned(),
1719        })?;
1720    let query_timeout = runtime
1721        .query_timeout
1722        .filter(|timeout| !timeout.is_zero())
1723        .ok_or_else(|| ServerError::Config {
1724            message: crate::config::QUERY_TIMEOUT_REQUIRED.to_owned(),
1725        })?;
1726    // 🔴 RE-VALIDATED AT BOOT, not trusted from the config load. The engine's
1727    // own `WorkloopService::new` refuses a zero interval, and an engine that
1728    // refuses to assemble takes the whole server down with a message naming a
1729    // Rust constructor. Refusing here names the KEY the operator set.
1730    let workloop_sweep_interval = runtime
1731        .workloop_sweep_interval
1732        .filter(|interval| !interval.is_zero())
1733        .ok_or_else(|| ServerError::Config {
1734            message: crate::config::WORKLOOP_SWEEP_INTERVAL_REQUIRED.to_owned(),
1735        })?;
1736    Ok((
1737        event_broadcast_capacity,
1738        query_timeout,
1739        workloop_sweep_interval,
1740    ))
1741}
1742
1743/// Install the outbox delivery callback when the durable outbox is commissioned.
1744///
1745/// Routes unmatched worker completions arriving at the sink into the live
1746/// workflow's mailbox. Flag-off, no callback is installed and the sink's
1747/// unmatched branch stays a silent drop. The dispatcher is not rebuilt — it
1748/// shares this exact pending tracker.
1749fn install_outbox_delivery(
1750    pending_activities: &PendingActivities,
1751    engine: &Arc<aion::Engine>,
1752    outbox_enabled: bool,
1753) {
1754    if outbox_enabled {
1755        let callback = Arc::new(crate::worker::ServerOutboxDeliveryCallback::new(
1756            Arc::clone(engine),
1757        ));
1758        pending_activities.set_outbox_delivery(callback);
1759    }
1760}
1761
1762/// The per-boot worker-side seams every dispatch path shares.
1763struct WorkerSeams {
1764    worker_registry: ConnectedWorkerRegistry,
1765    pending_activities: PendingActivities,
1766    heartbeat_tracker: HeartbeatTracker,
1767    drain_state: DrainState,
1768    queue_declarations: crate::worker::QueueDeclarationSource,
1769    queue_service_state: crate::worker::QueueServiceState,
1770    declared_bodies: crate::worker::DeclaredBodySource,
1771    /// The declared bodies this server is EXECUTING, as opposed to the ones it
1772    /// can look up. The declared-body dispatcher registers each attempt here for
1773    /// the life of its command and the cancel path signals through it, so a
1774    /// cancelled run's server-run command is stopped rather than left holding
1775    /// the machine.
1776    declared_attempts: crate::worker::DeclaredCommandAttempts,
1777    /// The boot's metrics handle, so the engine-backed seams installed after
1778    /// boot can export what they count.
1779    metrics: Metrics,
1780    /// The boot's one cluster-event publisher, kept here so the dispatchers
1781    /// built over these seams announce an unbounded park on the operator's
1782    /// real-time channel (#266 T4).
1783    cluster_publisher: crate::cluster_publisher::ClusterEventPublisher,
1784}
1785
1786/// Build the worker-side seams: the connected-worker registry (WS3 topology
1787/// deltas + the Control-Plane Phase 1 mint hook), the shared completion tracker,
1788/// worker liveness, the drain gate, and the two R1 queue-service handles the
1789/// bridge publishes into and the state reads from.
1790fn build_worker_seams(
1791    runtime: &RuntimeConfig,
1792    cluster_publisher: &crate::cluster_publisher::ClusterEventPublisher,
1793    metrics: &Metrics,
1794    namespace_store: &Arc<dyn NamespaceStore>,
1795    worker_deployment_store: &Arc<dyn WorkerDeploymentStore>,
1796    mint_routing: Option<crate::namespace::NamespaceRouting>,
1797) -> WorkerSeams {
1798    let worker_registry = ConnectedWorkerRegistry::default()
1799        .with_cluster_publisher(cluster_publisher.clone())
1800        .with_namespace_minting(namespace_store.clone(), runtime.auto_create)
1801        .with_worker_deployment_store(worker_deployment_store.clone());
1802    // The worker-registration mint is the OTHER `mint_or_gate` choke-point, so
1803    // it gets the same routing the start seams get: a worker registering for a
1804    // namespace whose registry shard this node does not own must not be refused
1805    // `NotOwner` forever either. `None` off-cluster leaves the registry
1806    // byte-identical.
1807    let worker_registry = match mint_routing {
1808        Some(routing) => worker_registry.with_namespace_routing(routing),
1809        None => worker_registry,
1810    };
1811    let drain_state = DrainState::default();
1812    WorkerSeams {
1813        worker_registry,
1814        // The transport-loss budget derives from the operator's heartbeat
1815        // window (the one value declaring what silence means), so a worker that
1816        // keeps dying under an activity is re-dispatched attempt-neutrally for
1817        // a bounded span and then terminates naming the TRANSPORT.
1818        pending_activities: PendingActivities::new(runtime.worker.heartbeat_window),
1819        heartbeat_tracker: HeartbeatTracker::new(runtime.worker.heartbeat_window),
1820        declared_attempts: crate::worker::DeclaredCommandAttempts::new(drain_state.clone()),
1821        drain_state,
1822        queue_declarations: crate::worker::QueueDeclarationSource::default(),
1823        queue_service_state: crate::worker::QueueServiceState::default(),
1824        declared_bodies: crate::worker::DeclaredBodySource::default(),
1825        metrics: metrics.clone(),
1826        cluster_publisher: cluster_publisher.clone(),
1827    }
1828}
1829
1830/// Point the bridge's R1 classifier at the engine's live workflow catalog.
1831///
1832/// Installed only after the engine exists (the dispatcher is built before it),
1833/// exactly like the outbox delivery callback: the dispatcher is not rebuilt, it
1834/// shares this handle. Until this runs — and on any state built without an
1835/// engine — the classifier answers `Unknown`, which never refuses anything.
1836fn install_queue_declarations(
1837    queue_declarations: &crate::worker::QueueDeclarationSource,
1838    engine: &Arc<aion::Engine>,
1839) {
1840    queue_declarations.install(Arc::new(crate::worker::EngineQueueDeclarations::new(
1841        Arc::clone(engine),
1842    )));
1843}
1844
1845/// Point the declared-body executor at the engine's live workflow catalog.
1846///
1847/// Installed only after the engine exists, exactly like the queue-declaration
1848/// reader above: the dispatcher already holds a clone of this handle. Until
1849/// this runs, every lookup answers `None` and every dispatch takes the worker
1850/// path — and that window is why the boot defers startup recovery (#266):
1851/// deploys are durable, so a restarted server DOES have declared bodies its
1852/// recovered runs depend on before this install, and recovery replay
1853/// re-dispatching an adopted in-flight activity inside `EngineBuilder::build()`
1854/// fell through the uninstalled source to a queue with no pollers and parked
1855/// forever. [`boot_engine`] therefore runs `Engine::run_startup_recovery()`
1856/// only AFTER `install_engine_backed_seams`, enforced by construction.
1857/// The one dispatch source that legitimately precedes this install is a
1858/// fresh start arriving over the API — and the API is not serving yet.
1859fn install_declared_bodies(
1860    declared_bodies: &crate::worker::DeclaredBodySource,
1861    engine: &Arc<aion::Engine>,
1862) {
1863    declared_bodies.install(Arc::new(crate::worker::EngineDeclaredBodies::new(
1864        Arc::clone(engine),
1865    )));
1866}
1867
1868/// Hand every engine-backed seam its reader once the engine exists.
1869///
1870/// One boot-path step for the three handles built before the engine and
1871/// filled in after it: outbox completion delivery, the R1 queue-declaration
1872/// classifier, and the declared-body executor.
1873fn install_engine_backed_seams(seams: &WorkerSeams, engine: &Arc<aion::Engine>, outbox: bool) {
1874    install_outbox_delivery(&seams.pending_activities, engine, outbox);
1875    // WA-010 R3: every dispatch path records an accepted delivery's lease
1876    // through the engine's single writer, and counts a lost one on the same
1877    // ledger the metrics surface and the API provenance read.
1878    seams.pending_activities.set_lease_recorder(
1879        Arc::new(crate::worker::EngineLeaseRecorder::new(Arc::clone(engine))),
1880        Some(seams.metrics.clone()),
1881    );
1882    install_queue_declarations(&seams.queue_declarations, engine);
1883    install_declared_bodies(&seams.declared_bodies, engine);
1884}
1885
1886/// Decorate the worker activity dispatcher with the per-run activity-mock layer
1887/// when the dev surface is commissioned, returning the dispatcher and the shared
1888/// mock registry (if any).
1889///
1890/// Dark by default: with the dev surface off the engine gets the bare production
1891/// dispatcher and there is no mocking path at all (CN4).
1892/// Compose the engine-seam bridge dispatcher over the state's shared parts.
1893///
1894/// Also mints the NOI-6 attempt→owner index and returns it alongside: the
1895/// bridge binds each liminal-delivered attempt into it for the dispatch's
1896/// lifetime, and the state stores the SAME instance for the intervention
1897/// router to read, so the ops console can enumerate and target live attempts.
1898fn build_bridge_dispatcher(
1899    runtime: &RuntimeConfig,
1900    seams: &WorkerSeams,
1901) -> (WorkerActivityDispatcher, crate::worker::AttemptOwnerIndex) {
1902    let attempt_owners = crate::worker::AttemptOwnerIndex::new();
1903    let dispatcher = WorkerActivityDispatcher::new(
1904        seams.worker_registry.clone(),
1905        runtime.default_namespace.clone(),
1906        seams.heartbeat_tracker.clone(),
1907    )
1908    .with_pending(seams.pending_activities.clone())
1909    .with_drain_state(seams.drain_state.clone())
1910    .with_tokio_handle(tokio::runtime::Handle::current())
1911    .with_attempt_owners(attempt_owners.clone())
1912    .with_queue_service(runtime.worker.queue_service.clone())
1913    .with_queue_declarations(seams.queue_declarations.clone())
1914    .with_queue_state(seams.queue_service_state.clone())
1915    .with_cluster_publisher(seams.cluster_publisher.clone());
1916    (dispatcher, attempt_owners)
1917}
1918
1919/// Build the process metrics, the LSUB-2 advisory outbox wake, and the
1920/// instrumented event store wired to pulse it — the storage-side trio the
1921/// full boot path assembles before the engine exists.
1922///
1923/// The wake is one process-wide `Notify` shared by the engine's stage seam
1924/// and the outbox dispatcher. A single handle is correct here because there
1925/// is exactly one in-process dispatcher that sweeps all owned shards per tick
1926/// — a wake just means "something was staged; sweep".
1927///
1928/// # Errors
1929///
1930/// Returns [`ServerError`] when the metrics registry cannot be constructed.
1931fn build_instrumented_store(
1932    runtime: &RuntimeConfig,
1933    event_store: Arc<dyn EventStore>,
1934) -> Result<
1935    (
1936        Metrics,
1937        Arc<tokio::sync::Notify>,
1938        Arc<InstrumentedEventStore>,
1939    ),
1940    ServerError,
1941> {
1942    let metrics = Metrics::new().map_err(|error| metrics_config_error(&error))?;
1943    let outbox_wake = Arc::new(tokio::sync::Notify::new());
1944    let instrumented_store = Arc::new(
1945        InstrumentedEventStore::new(
1946            event_store,
1947            metrics.clone(),
1948            runtime.default_namespace.clone(),
1949        )
1950        .with_outbox_wake(Arc::clone(&outbox_wake)),
1951    );
1952    Ok((metrics, outbox_wake, instrumented_store))
1953}
1954
1955/// Build the production bridge dispatcher and its decoration stack in one
1956/// step: declared-body execution always, the update-check observer above it,
1957/// the dev mock when commissioned. Also mints the update-status slot the
1958/// observer writes, returned so the state serves the same slot.
1959fn build_decorated_dispatcher(
1960    runtime: &RuntimeConfig,
1961    seams: &WorkerSeams,
1962    transcript: crate::activity_publisher::ActivityEventPublisher,
1963) -> (
1964    Arc<dyn ActivityDispatcher>,
1965    Option<ActivityMockRegistry>,
1966    crate::worker::AttemptOwnerIndex,
1967    crate::worker::WorkspaceRoot,
1968    crate::update_check::UpdateStatusState,
1969) {
1970    // #139: THE one resolution of the declared-body workspace root. The
1971    // dispatcher expands `{workspace_root}` with it and the returned value is
1972    // what the state exposes for the startup banner; nothing else derives it.
1973    let workspace_root = crate::worker::WorkspaceRoot::resolve();
1974    // #189 slice one: the update-status slot is created beside the observer
1975    // that writes it and returned so the state serves the SAME slot.
1976    let update_status = crate::update_check::UpdateStatusState::default();
1977    let (dispatcher, attempt_owners) = build_bridge_dispatcher(runtime, seams);
1978    let (activity_dispatcher, activity_mock_registry) = decorate_activity_dispatcher(
1979        dispatcher,
1980        seams.declared_bodies.clone(),
1981        seams.declared_attempts.clone(),
1982        workspace_root.clone(),
1983        transcript,
1984        update_status.clone(),
1985        runtime.dev.enabled,
1986    );
1987    (
1988        activity_dispatcher,
1989        activity_mock_registry,
1990        attempt_owners,
1991        workspace_root,
1992        update_status,
1993    )
1994}
1995
1996fn decorate_activity_dispatcher(
1997    dispatcher: WorkerActivityDispatcher,
1998    declared_bodies: crate::worker::DeclaredBodySource,
1999    declared_attempts: crate::worker::DeclaredCommandAttempts,
2000    workspace_root: crate::worker::WorkspaceRoot,
2001    transcript: crate::activity_publisher::ActivityEventPublisher,
2002    update_status: crate::update_check::UpdateStatusState,
2003    dev_enabled: bool,
2004) -> (Arc<dyn ActivityDispatcher>, Option<ActivityMockRegistry>) {
2005    // The declared-body layer wraps the production dispatcher UNCONDITIONALLY:
2006    // an action whose deployed contract declares a body executes at the
2007    // server, everything else falls through to the worker path untouched. The
2008    // update-check observer wraps THAT layer so it sees each completed
2009    // declared execution's result (it records completed checks and touches
2010    // nothing else). The dev mock (when commissioned) stays outermost so a
2011    // mocked activity short-circuits before either real execution path.
2012    let declared = crate::worker::DeclaredCommandDispatcher::new(
2013        Arc::new(dispatcher),
2014        declared_bodies.clone(),
2015        declared_attempts,
2016        tokio::runtime::Handle::current(),
2017        workspace_root,
2018        transcript,
2019    );
2020    let observed = crate::update_check::UpdateCheckObserver::new(
2021        Arc::new(declared),
2022        declared_bodies,
2023        update_status,
2024    );
2025    if dev_enabled {
2026        let registry = ActivityMockRegistry::new();
2027        let decorated = DevMockingDispatcher::new(Arc::new(observed), registry.clone());
2028        (Arc::new(decorated), Some(registry))
2029    } else {
2030        (Arc::new(observed), None)
2031    }
2032}
2033
2034/// Validate the WS3 cluster broadcast capacity the server unconditionally mounts
2035/// (the `cluster` subscription on `/events/stream`). Explicit-no-default with the
2036/// same non-zero startup guard as the workflow event channel: the lag contract
2037/// has no buffer to lag against unless sized.
2038fn required_cluster_broadcast_capacity(
2039    runtime: &RuntimeConfig,
2040) -> Result<std::num::NonZeroUsize, ServerError> {
2041    runtime
2042        .websocket
2043        .cluster_broadcast_capacity
2044        .and_then(std::num::NonZeroUsize::new)
2045        .ok_or_else(|| ServerError::Config {
2046            message: crate::config::CLUSTER_BROADCAST_CAPACITY_REQUIRED.to_owned(),
2047        })
2048}
2049
2050/// Build the deployment-wide real-time publishers the server mounts on every
2051/// boot — the WS3 cluster topology channel and the NOI-5b agent-observability
2052/// transcript channel — from the validated `websocket.cluster_broadcast_capacity`.
2053///
2054/// The transcript sequencer runs over `observability_store` (the durable
2055/// `O`-keyspace impl on a haematite boot) or an in-memory impl when the backend
2056/// has none — see [`build_transcript_publisher`].
2057///
2058/// # Errors
2059///
2060/// Returns [`ServerError`] when `websocket.cluster_broadcast_capacity` is unset
2061/// or zero (the same explicit-no-default guard the cluster channel already had).
2062fn build_real_time_publishers(
2063    runtime: &RuntimeConfig,
2064    observability_store: Option<Arc<dyn aion_store::ObservabilityStore>>,
2065) -> Result<
2066    (
2067        crate::cluster_publisher::ClusterEventPublisher,
2068        crate::activity_publisher::ActivityEventPublisher,
2069    ),
2070    ServerError,
2071> {
2072    let capacity = required_cluster_broadcast_capacity(runtime)?;
2073    let batch = required_transcript_batch_policy(runtime)?;
2074    Ok((
2075        crate::cluster_publisher::ClusterEventPublisher::new(capacity),
2076        build_transcript_publisher(
2077            observability_store,
2078            capacity,
2079            transcript_bounds(runtime),
2080            batch,
2081        ),
2082    ))
2083}
2084
2085/// The operator's node-cache byte budget, or a refusal that names the key.
2086///
2087/// There is no default to fall back to — not here, and not in haematite, which
2088/// refuses a `DatabaseConfig` that does not carry one. A node is not a
2089/// fixed-size thing, so a cache bounded only by its entry count is unbounded in
2090/// BYTES, and how much resident memory this process may spend on cached nodes is
2091/// a deployment decision. `"unlimited"` is available as an explicit choice, so
2092/// the pre-budget behaviour is still reachable — by NAME, visible in a diff.
2093///
2094/// # Errors
2095///
2096/// Returns [`ServerError::Config`] carrying
2097/// [`STORE_NODE_CACHE_BUDGET_REQUIRED`](crate::config::STORE_NODE_CACHE_BUDGET_REQUIRED)
2098/// when `store.node_cache_budget` is unset.
2099///
2100/// `pub(crate)` so the shipped-config sweep
2101/// (`config::shipped_configs_tests`) holds every shipped TOML to this exact
2102/// requirement instead of enumerating a copy of it.
2103pub(crate) fn required_node_cache_budget(
2104    config: &StoreConfig,
2105) -> Result<haematite::NodeCacheBudget, ServerError> {
2106    config.node_cache_budget.ok_or_else(|| ServerError::Config {
2107        message: crate::config::STORE_NODE_CACHE_BUDGET_REQUIRED.to_owned(),
2108    })
2109}
2110
2111/// The lock-acquisition keys are RETIRED (ruled 2026-08-24): the writer lock
2112/// is a kernel-released flock, so its holder is either live mid-handover
2113/// (boot now waits indefinitely, reporting while it waits) or dead (the
2114/// kernel already released it). A configuration still carrying the keys
2115/// stays legal — warned and ignored, never refused — so existing estates
2116/// boot unchanged.
2117fn warn_retired_lock_acquisition_keys(config: &StoreConfig) {
2118    if config.lock_acquisition_patience_ms.is_some()
2119        || config.lock_acquisition_retry_cadence_ms.is_some()
2120    {
2121        tracing::warn!(
2122            "store.lock_acquisition_patience_ms and store.lock_acquisition_retry_cadence_ms \
2123             are retired and ignored: the server waits indefinitely for the data-directory \
2124             writer lock (a kernel-released flock whose holder is either live mid-handover \
2125             or already gone) and reports while it waits. Remove the keys"
2126        );
2127    }
2128}
2129
2130/// The operator's transcript flush policy, or a startup refusal naming the key
2131/// they have not ruled on.
2132///
2133/// `observability.max_batch_events` and `observability.max_batch_hold_ms` have
2134/// NO shipped default (see [`crate::config::ObservabilityConfig`]): the trade
2135/// between store cost and how much not-yet-durable transcript a crash can lose
2136/// belongs to the deployment, and a guessed value would make it silently. This
2137/// is the same explicit-no-default guard [`required_cluster_broadcast_capacity`]
2138/// applies one line above, for the same reason.
2139///
2140/// # Errors
2141///
2142/// Returns [`ServerError`] when either key is unset, when `max_batch_events` is
2143/// zero (a batch of no events would commit nothing, forever), or when the hold
2144/// in milliseconds does not fit a [`Duration`](std::time::Duration).
2145///
2146/// `pub(crate)` so the shipped-config sweep
2147/// (`config::shipped_configs_tests`) holds every shipped TOML to this exact
2148/// requirement instead of enumerating a copy of it.
2149pub(crate) fn required_transcript_batch_policy(
2150    runtime: &RuntimeConfig,
2151) -> Result<crate::activity_publisher::TranscriptBatchPolicy, ServerError> {
2152    let max_batch_events = runtime
2153        .observability
2154        .max_batch_events
2155        .and_then(std::num::NonZeroUsize::new)
2156        .ok_or_else(|| ServerError::Config {
2157            message: crate::config::OBSERVABILITY_MAX_BATCH_EVENTS_REQUIRED.to_owned(),
2158        })?;
2159    // Zero is a MEANINGFUL setting here (never hold a partial batch open), so
2160    // absence — not zero — is what fails.
2161    let max_batch_hold_ms =
2162        runtime
2163            .observability
2164            .max_batch_hold_ms
2165            .ok_or_else(|| ServerError::Config {
2166                message: crate::config::OBSERVABILITY_MAX_BATCH_HOLD_MS_REQUIRED.to_owned(),
2167            })?;
2168    Ok(crate::activity_publisher::TranscriptBatchPolicy {
2169        max_batch_events,
2170        max_hold: std::time::Duration::from_millis(max_batch_hold_ms),
2171    })
2172}
2173
2174/// The operator-configured transcript retention bounds from `[observability]`.
2175fn transcript_bounds(runtime: &RuntimeConfig) -> crate::activity_bounds::TranscriptBounds {
2176    crate::activity_bounds::TranscriptBounds {
2177        max_event_bytes: runtime.observability.max_event_bytes,
2178        max_stream_events: runtime.observability.max_stream_events,
2179    }
2180}
2181
2182/// Build the NOI-5b transcript sequencer over `observability_store` (the durable
2183/// `O`-keyspace impl when the backend has one, an in-memory impl otherwise) with
2184/// a live-tail buffer of `capacity` and the `[observability]` retention bounds.
2185///
2186/// The publisher is ALWAYS constructed (the transcript channel is served on every
2187/// boot); only the durability of the backing store varies by backend. The memory
2188/// backend, which has no `O` keyspace, gets the in-memory
2189/// [`InMemoryObservabilityStore`](aion_store::InMemoryObservabilityStore), so the
2190/// live-tail + resume path behaves identically and only cross-restart durability
2191/// differs — exactly the "keep the no-observability path uniform" contract.
2192fn build_transcript_publisher(
2193    observability_store: Option<Arc<dyn aion_store::ObservabilityStore>>,
2194    capacity: std::num::NonZeroUsize,
2195    bounds: crate::activity_bounds::TranscriptBounds,
2196    batch: crate::activity_publisher::TranscriptBatchPolicy,
2197) -> crate::activity_publisher::ActivityEventPublisher {
2198    let store = observability_store
2199        .unwrap_or_else(|| Arc::new(aion_store::InMemoryObservabilityStore::default()));
2200    crate::activity_publisher::ActivityEventPublisher::new(store, capacity, batch)
2201        .with_bounds(bounds)
2202}
2203
2204/// The request-routing pieces built from the cluster store + peer config.
2205struct RoutingState {
2206    shard_directory: Option<Arc<crate::routing::StaticShardDirectory>>,
2207    request_forwarder: Option<Arc<dyn crate::routing::RequestForwarder>>,
2208    /// The namespace-mint routing context assembled from the two handles above
2209    /// plus the cluster store, so the boot path can thread it into the worker
2210    /// registry's minter (the second of the two minter construction sites)
2211    /// without re-deriving it.
2212    mint_routing: Option<crate::namespace::NamespaceRouting>,
2213}
2214
2215/// Build the R-2 shard directory and R-3 request forwarder over the cluster
2216/// store and static peer config, or all-`None` when this is not a distributed
2217/// boot (no cluster store) so the routing edge is a no-op (default path).
2218fn build_routing_state(
2219    cluster_store: Option<&Arc<aion_store_haematite::HaematiteStore>>,
2220    directory_peers: Vec<crate::routing::DirectoryPeer>,
2221    self_node_id: Option<String>,
2222) -> RoutingState {
2223    let Some(store) = cluster_store else {
2224        return RoutingState {
2225            shard_directory: None,
2226            request_forwarder: None,
2227            mint_routing: None,
2228        };
2229    };
2230    let shard_directory = Arc::new(crate::routing::StaticShardDirectory::new(
2231        Arc::clone(store),
2232        directory_peers,
2233        self_node_id,
2234    ));
2235    let request_forwarder: Arc<dyn crate::routing::RequestForwarder> =
2236        Arc::new(crate::routing::GrpcRequestForwarder::new());
2237    let mint_routing = build_namespace_routing(
2238        Some(store),
2239        Some(&shard_directory),
2240        Some(&request_forwarder),
2241    );
2242    RoutingState {
2243        shard_directory: Some(shard_directory),
2244        request_forwarder: Some(request_forwarder),
2245        mint_routing,
2246    }
2247}
2248
2249/// Assemble the namespace-mint routing context from the three handles a
2250/// distributed boot produces, or `None` when any is absent.
2251///
2252/// The single place the context is built, shared by the boot path (which wires
2253/// it into the worker registry's minter before the state exists) and
2254/// [`ServerState::namespace_routing`] (which serves the per-request minters).
2255/// Each handle is checked rather than assumed present: `build_routing_state`
2256/// populates them together, but a partial context would silently route mints to
2257/// nowhere.
2258fn build_namespace_routing(
2259    cluster_store: Option<&Arc<aion_store_haematite::HaematiteStore>>,
2260    shard_directory: Option<&Arc<crate::routing::StaticShardDirectory>>,
2261    request_forwarder: Option<&Arc<dyn crate::routing::RequestForwarder>>,
2262) -> Option<crate::namespace::NamespaceRouting> {
2263    use crate::namespace::{
2264        GrpcMintForwarder, MintForwarder, MintShardOwners, NamespaceRouting, NamespaceShardResolver,
2265    };
2266    let store = Arc::clone(cluster_store?);
2267    let directory = Arc::clone(shard_directory?);
2268    let shards: Arc<dyn NamespaceShardResolver> = store;
2269    let owners: Arc<dyn MintShardOwners> = directory;
2270    let forwarder: Arc<dyn MintForwarder> =
2271        Arc::new(GrpcMintForwarder::new(Arc::clone(request_forwarder?)));
2272    Some(NamespaceRouting::new(shards, owners, forwarder))
2273}
2274
2275/// A connected durable store plus the lifecycle pieces the boot path needs.
2276///
2277/// `outbox_store` is the SAME leaf store cast as an [`OutboxStore`] on the
2278/// haematite backend, which is the one that has a durable outbox; the in-memory
2279/// backend yields `None`. `bootstrap_coordinator` gates the schedule-coordinator seed on real
2280/// ownership (SS-2 / AA-4-4): `true` for every non-distributed boot (single-node
2281/// owns the coordinator's shard), and for a distributed node only when it owns
2282/// that shard. `cluster_responder` owns the distributed inbound-write responder
2283/// thread, kept alive for the server's lifetime; `None` for non-distributed boots.
2284struct ConnectedStore {
2285    event_store: Arc<dyn EventStore>,
2286    outbox_store: Option<Arc<dyn OutboxStore>>,
2287    /// The SAME concrete leaf store as `event_store`, captured as a
2288    /// [`NamespaceStore`] before the decorator chain wraps it (the decorators
2289    /// are `NamespaceStore`-unaware). The control plane mints and lists through
2290    /// this handle. Every backend populates it: distributed haematite supplies
2291    /// the quorum-replicated implementation, single-node haematite and the
2292    /// in-memory store the local-only one.
2293    namespace_store: Arc<dyn NamespaceStore>,
2294    /// Same concrete leaf captured as the deployment-store contract.
2295    worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
2296    /// The SAME concrete leaf captured as the workloop registration store.
2297    ///
2298    /// Captured HERE, beside `namespace_store`, for the same reason: the
2299    /// decorator chain that wraps the event store is `WorkloopStore`-unaware,
2300    /// so by the time the engine's `store_arc` handle exists the leaf's
2301    /// workloop implementation is no longer reachable through it. Both
2302    /// backends implement the trait, so this is never `None` — a workloop
2303    /// service is available on every boot, and a loop that cannot park is
2304    /// never a silent consequence of which store was selected.
2305    workloop_store: Arc<dyn aion_store::workloop::WorkloopStore>,
2306    /// The SAME concrete leaf captured as the visibility projection, for the
2307    /// same reason as `workloop_store`: the decorator chain is
2308    /// `VisibilityStore`-unaware. Every backend implements the trait, so the
2309    /// engine never lists from a projection that dies with the process.
2310    visibility_store: Arc<dyn aion_store::visibility::VisibilityStore>,
2311    /// The SAME concrete leaf captured as the assistant-session store, for the
2312    /// same reason as `workloop_store`: the decorator chain is
2313    /// `AssistantSessionStore`-unaware. Every backend implements the trait, so
2314    /// an assistant session's record and transcript are as durable as whichever
2315    /// store the deployment chose — never a file beside it and never lost with
2316    /// the process.
2317    assistant_store: Arc<dyn aion_store::AssistantSessionStore>,
2318    /// NOI-5b: the SAME concrete leaf store captured as an
2319    /// [`ObservabilityStore`](aion_store::ObservabilityStore) when the backend
2320    /// implements the durable `O` keyspace (haematite). `None` for backends with
2321    /// no `O` keyspace (haematite / in-memory), where the transcript sequencer runs
2322    /// over an in-memory impl instead. Captured before the leaf is wrapped in the
2323    /// (`ObservabilityStore`-unaware) decorator chain, exactly like
2324    /// `namespace_store`.
2325    observability_store: Option<Arc<dyn aion_store::ObservabilityStore>>,
2326    bootstrap_coordinator: bool,
2327    cluster_responder: Option<aion_store_haematite::ClusterResponder>,
2328    /// The concrete distributed haematite store (the SAME leaf as `event_store`),
2329    /// retained for the SS-5b cluster supervisor's peer-liveness polling. `None`
2330    /// for every non-distributed boot.
2331    cluster_store: Option<Arc<aion_store_haematite::HaematiteStore>>,
2332    /// The peers the SS-5b supervisor watches, each with the shards this node
2333    /// adopts on its death. Empty for non-distributed boots.
2334    watched_peers: Vec<crate::cluster::WatchedPeer>,
2335    /// The static shard-directory peer entries (name + declared shards + gRPC
2336    /// forward address) used to build the request-routing directory (R-2). Empty
2337    /// for non-distributed boots.
2338    directory_peers: Vec<crate::routing::DirectoryPeer>,
2339    /// This node's own distribution name (cluster `node_id`), so the SS-3
2340    /// directory can resolve a shard-owner record naming THIS node to `Local`.
2341    /// `None` for non-distributed boots.
2342    self_node_id: Option<String>,
2343}
2344
2345impl ConnectedStore {
2346    /// A non-distributed connected store: owns the coordinator's shard (so it
2347    /// bootstraps the coordinator) and has no cluster responder.
2348    ///
2349    /// `namespace_store` is the SAME concrete leaf as `event_store`, captured as
2350    /// a [`NamespaceStore`] by the caller (where the concrete type is still
2351    /// known) before the decorator chain wraps the event store.
2352    fn local(
2353        event_store: Arc<dyn EventStore>,
2354        outbox_store: Option<Arc<dyn OutboxStore>>,
2355        namespace_store: Arc<dyn NamespaceStore>,
2356        worker_deployment_store: Arc<dyn WorkerDeploymentStore>,
2357        workloop_store: Arc<dyn aion_store::workloop::WorkloopStore>,
2358        visibility_store: Arc<dyn aion_store::visibility::VisibilityStore>,
2359        assistant_store: Arc<dyn aion_store::AssistantSessionStore>,
2360    ) -> Self {
2361        Self {
2362            event_store,
2363            outbox_store,
2364            namespace_store,
2365            worker_deployment_store,
2366            workloop_store,
2367            visibility_store,
2368            assistant_store,
2369            // `local` is the memory backend and the embedder's own-store path.
2370            // A haematite boot never arrives here — `connect_store` routes it to
2371            // `connect_haematite_store`, which sets `observability_store`. So a
2372            // `local` store has no durable `O` keyspace and the transcript
2373            // sequencer falls back to an in-memory impl (NOI-5b).
2374            observability_store: None,
2375            bootstrap_coordinator: true,
2376            cluster_responder: None,
2377            cluster_store: None,
2378            watched_peers: Vec::new(),
2379            directory_peers: Vec::new(),
2380            self_node_id: None,
2381        }
2382    }
2383
2384    /// Capture this node's self-identity for the cluster snapshot before the
2385    /// distributed boot moves it into routing state.
2386    fn cluster_self_node(&self) -> Option<String> {
2387        self.self_node_id.clone()
2388    }
2389}
2390
2391/// Connect the selected store. Haematite supplies both durable events and the
2392/// durable outbox; the in-memory development backend has no outbox.
2393async fn connect_store(
2394    config: StoreConfig,
2395    stage: &crate::control::StageReporter,
2396) -> Result<ConnectedStore, ServerError> {
2397    match config.backend {
2398        StoreBackend::Memory => {
2399            // One leaf store, captured as both the engine's event store and the
2400            // namespace registry (in-memory backends have no outbox table).
2401            let leaf = Arc::new(aion_store::InMemoryStore::default());
2402            let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
2403            let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
2404            let workloop_store: Arc<dyn aion_store::workloop::WorkloopStore> = leaf.clone();
2405            let visibility_store: Arc<dyn aion_store::visibility::VisibilityStore> = leaf.clone();
2406            let assistant_store: Arc<dyn aion_store::AssistantSessionStore> = leaf.clone();
2407            Ok(ConnectedStore::local(
2408                leaf,
2409                None,
2410                namespace_store,
2411                worker_deployment_store,
2412                workloop_store,
2413                visibility_store,
2414                assistant_store,
2415            ))
2416        }
2417        StoreBackend::Haematite => connect_haematite_store(config, stage).await,
2418    }
2419}
2420
2421/// Connect the haematite backend, opening the on-disk database if `store.data_dir`
2422/// already holds one and otherwise creating it with `store.shard_count` shards.
2423///
2424/// Without a `[store.cluster]` section this is the SINGLE-NODE path
2425/// ([`HaematiteStore::open`] / [`create_with_shard_count`]), byte-identical to
2426/// before: no endpoint, no election, owns everything, bootstraps the coordinator.
2427/// With a cluster section this is the DISTRIBUTED path
2428/// ([`HaematiteStore::open_or_create_distributed`]): it binds the replication
2429/// endpoint, builds the quorum membership, dials peers, starts the responder, and
2430/// computes whether THIS node owns the schedule-coordinator's shard so the engine
2431/// boot path seeds the coordinator on exactly one owner cluster-wide (SS-2).
2432///
2433/// The SAME leaf `Arc<HaematiteStore>` is shared as both the engine's
2434/// [`EventStore`] and the dispatcher's [`OutboxStore`] (one inner haematite
2435/// database), mirroring the haematite backend.
2436///
2437/// [`HaematiteStore::open`]: aion_store_haematite::HaematiteStore::open
2438/// [`create_with_shard_count`]: aion_store_haematite::HaematiteStore::create_with_shard_count
2439/// [`HaematiteStore::open_or_create_distributed`]: aion_store_haematite::HaematiteStore::open_or_create_distributed
2440async fn connect_haematite_store(
2441    config: StoreConfig,
2442    stage: &crate::control::StageReporter,
2443) -> Result<ConnectedStore, ServerError> {
2444    // Required HERE, at the only seam that consumes it: haematite is the only
2445    // backend with a node cache, so a `memory` deployment is never asked to
2446    // rule on a cache it does not have. Same explicit-no-default guard
2447    // `observability.max_batch_events` uses, at the same kind of seam. Read
2448    // before `config` is partially moved below.
2449    let node_cache_budget = required_node_cache_budget(&config)?;
2450    warn_retired_lock_acquisition_keys(&config);
2451    let Some(data_dir) = config.data_dir else {
2452        return Err(ServerError::Config {
2453            message: "store.data_dir must not be empty when store.backend is haematite".to_owned(),
2454        });
2455    };
2456    let shard_count = config.shard_count;
2457    let owned_shards = config.owned_shards.clone();
2458    let cluster = config.cluster.clone();
2459    // The peers the SS-5b supervisor watches, captured before `cluster` is moved
2460    // into the blocking build. A peer with declared `owned_shards` becomes a
2461    // watch target; peers without are kept out of the watch set (the supervisor
2462    // would have nothing to adopt for them).
2463    let watched_peers: Vec<crate::cluster::WatchedPeer> = cluster
2464        .as_ref()
2465        .map(|cluster| {
2466            cluster
2467                .peers
2468                .iter()
2469                .map(|peer| crate::cluster::WatchedPeer {
2470                    name: peer.name.clone(),
2471                    owned_shards: peer.owned_shards.clone(),
2472                })
2473                .collect()
2474        })
2475        .unwrap_or_default();
2476    // The static shard-directory entries (R-2): each peer's declared shards plus
2477    // its gRPC forward address. Built from the same config the supervisor uses.
2478    let directory_peers: Vec<crate::routing::DirectoryPeer> = cluster
2479        .as_ref()
2480        .map(|cluster| {
2481            cluster
2482                .peers
2483                .iter()
2484                .map(|peer| crate::routing::DirectoryPeer {
2485                    name: peer.name.clone(),
2486                    owned_shards: peer.owned_shards.clone(),
2487                    grpc_addr: peer.grpc_address,
2488                })
2489                .collect()
2490        })
2491        .unwrap_or_default();
2492    // This node's own distribution name, so the SS-3 directory resolves a
2493    // shard-owner record naming THIS node to `Local`.
2494    let self_node_id: Option<String> = cluster.as_ref().map(|cluster| cluster.node_id.clone());
2495    // Construction (and, for the distributed path, the off-runtime endpoint bind)
2496    // must not stall the async runtime, so run it on the blocking pool. The
2497    // distributed constructor itself steps onto a bare thread for the bind.
2498    stage.report(
2499        crate::control::stage::STAGE_STORE_OPEN,
2500        format!("opening the haematite store at {data_dir}"),
2501    );
2502    // The reporter is cloned INTO the blocking closure: the store build runs
2503    // on the blocking pool, and the two legs that take minutes — the writer-
2504    // lock wait and WAL recovery — report from that thread. A handle that
2505    // could not cross the boundary would leave exactly those two legs silent.
2506    let build_stage = stage.clone();
2507    let (store, responder) = tokio::task::spawn_blocking(move || {
2508        build_haematite_store(
2509            &data_dir,
2510            shard_count,
2511            cluster,
2512            node_cache_budget,
2513            &build_stage,
2514        )
2515    })
2516    .await
2517    .map_err(|error| ServerError::Config {
2518        message: format!("haematite store initialization task failed: {error}"),
2519    })??;
2520
2521    // Gate the coordinator bootstrap on real ownership: a distributed node that
2522    // does NOT own the coordinator's shard must not seed/fence it (AA-4-4). A
2523    // single-node boot owns all shards, so it always bootstraps.
2524    let bootstrap_coordinator = if owned_shards.is_empty() {
2525        true
2526    } else {
2527        store.set_owned_shards(owned_shards.iter().copied());
2528        store.owns_workflow_shard(&aion::schedule_coordinator_workflow_id())
2529    };
2530
2531    let leaf = Arc::new(store);
2532    let event_store: Arc<dyn EventStore> = leaf.clone();
2533    let outbox_store: Arc<dyn OutboxStore> = leaf.clone();
2534    // The namespace registry is the SAME concrete `HaematiteStore` leaf (the
2535    // quorum-replicated implementation), captured before the leaf is moved into
2536    // the cluster-store retention below.
2537    let namespace_store: Arc<dyn NamespaceStore> = leaf.clone();
2538    let worker_deployment_store: Arc<dyn WorkerDeploymentStore> = leaf.clone();
2539    let workloop_store: Arc<dyn aion_store::workloop::WorkloopStore> = leaf.clone();
2540    // The visibility projection lives in the leaf's `v` keyspace — durable,
2541    // rebuilt from owned histories at boot and adoption, never process-local.
2542    let visibility_store: Arc<dyn aion_store::visibility::VisibilityStore> = leaf.clone();
2543    let assistant_store: Arc<dyn aion_store::AssistantSessionStore> = leaf.clone();
2544    // NOI-5b: the SAME concrete leaf captured as the durable `O`-keyspace
2545    // observability store, so the transcript sequencer persists to haematite and
2546    // survives restart/failover. Captured here (before the decorator chain wraps
2547    // the event store) exactly like the namespace registry.
2548    let observability_store: Arc<dyn aion_store::ObservabilityStore> = leaf.clone();
2549    // Retain the concrete store ONLY for a distributed boot (responder present),
2550    // where the SS-5b supervisor will poll it for peer liveness. A single-node
2551    // boot has no peers, so it carries no cluster store and never supervises.
2552    let cluster_store = responder.as_ref().map(|_| leaf);
2553    let (watched_peers, directory_peers, self_node_id) = if cluster_store.is_some() {
2554        (watched_peers, directory_peers, self_node_id)
2555    } else {
2556        (Vec::new(), Vec::new(), None)
2557    };
2558    Ok(ConnectedStore {
2559        event_store,
2560        outbox_store: Some(outbox_store),
2561        namespace_store,
2562        worker_deployment_store,
2563        workloop_store,
2564        visibility_store,
2565        assistant_store,
2566        observability_store: Some(observability_store),
2567        bootstrap_coordinator,
2568        cluster_responder: responder,
2569        cluster_store,
2570        watched_peers,
2571        directory_peers,
2572        self_node_id,
2573    })
2574}
2575
2576/// Build the haematite store: the distributed path when a cluster section is
2577/// present, otherwise the single-node path. Returns the store and (for the
2578/// distributed path) its inbound-write responder. Restart-safe: an existing
2579/// on-disk database is reused (its shard count wins) rather than re-created.
2580///
2581/// Linux/Android give Haematite a descriptor-authoritative `/proc/self/fd` path.
2582/// On path-ambient Unix targets such as macOS, startup instead resolves the held
2583/// descriptor's current path and refuses any ancestor owned by an unprivileged
2584/// principal other than the server euid or writable by group/world. That policy
2585/// prevents a second principal from renaming a parent after startup and replacing
2586/// the old name with a symlink that redirects Haematite's normal reads/commits.
2587/// Every shard is still eagerly materialized and the capability retained, but on
2588/// those targets neither action confines later pathname I/O. A descriptor-relative
2589/// Haematite constructor and backend I/O remain the long-term fix.
2590fn build_haematite_store(
2591    data_dir: &str,
2592    shard_count: usize,
2593    cluster: Option<crate::config::ClusterConfig>,
2594    node_cache_budget: haematite::NodeCacheBudget,
2595    stage: &crate::control::StageReporter,
2596) -> Result<
2597    (
2598        aion_store_haematite::HaematiteStore,
2599        Option<aion_store_haematite::ClusterResponder>,
2600    ),
2601    ServerError,
2602> {
2603    build_haematite_store_with_hook(
2604        data_dir,
2605        shard_count,
2606        cluster,
2607        node_cache_budget,
2608        stage,
2609        || Ok(()),
2610    )
2611}
2612
2613fn build_haematite_store_with_hook(
2614    data_dir: &str,
2615    shard_count: usize,
2616    cluster: Option<crate::config::ClusterConfig>,
2617    node_cache_budget: haematite::NodeCacheBudget,
2618    stage: &crate::control::StageReporter,
2619    before_backend_touch: impl FnOnce() -> Result<(), std::io::Error>,
2620) -> Result<
2621    (
2622        aion_store_haematite::HaematiteStore,
2623        Option<aion_store_haematite::ClusterResponder>,
2624    ),
2625    ServerError,
2626> {
2627    use aion_store_haematite::{ClusterBootstrap, HaematiteStore};
2628
2629    // Acquire the data root through the same no-follow component walk used by
2630    // authoring. New components are created 0700 on Unix, and an existing root
2631    // that the server's own user owns is tightened to 0700 rather than refused —
2632    // provisioning our own directory is Aion's job, not the operator's. Only a
2633    // root Aion cannot make safe (foreign owner, a filesystem without Unix
2634    // modes) is a loud startup failure here; an unsafe ANCESTOR is caught
2635    // separately below and is never repaired.
2636    let private_root = crate::filesystem::ConfinedDir::open_or_create(std::path::Path::new(
2637        data_dir,
2638    ))
2639    .map_err(|error| ServerError::Config {
2640        message: format!("unsafe store.data_dir `{data_dir}`: {error}"),
2641    })?;
2642
2643    // Haematite creates shard directories lazily. Pre-create every configured
2644    // directory descriptor-relatively and check its mode — the root and the
2645    // shard directories are the whole of what a boot inspects. Nothing below
2646    // them is walked: every file haematite writes is private from its own
2647    // creation (0.12.1), and a store older than that is the operator verb
2648    // `aion store harden`'s to bring across, once. The walk that used to run
2649    // here — twice per boot, over every object file — is what turned a
2650    // 30-second boot into a 3-minute silence on a million-file store.
2651    stage.report(
2652        crate::control::stage::STAGE_STORE_OPEN,
2653        format!("checking the data root and {shard_count} shard directories under {data_dir}"),
2654    );
2655    for shard in 0..shard_count {
2656        let relative = std::path::PathBuf::from(format!("shard-{shard}"));
2657        private_root
2658            .create_dir_all(&relative)
2659            .map_err(|error| ServerError::Config {
2660                message: format!(
2661                    "failed to materialize shard-{shard} under store.data_dir `{data_dir}`: {error}"
2662                ),
2663            })?;
2664        private_root
2665            .ensure_child_dir_private(&relative)
2666            .map_err(|error| private_store_mode_error(data_dir, &error))?;
2667    }
2668
2669    // Deterministic regression seam: the capability and shard directories exist,
2670    // but Haematite has not touched any path yet.
2671    before_backend_touch().map_err(|error| ServerError::Config {
2672        message: format!("store.data_dir pre-open hook failed: {error}"),
2673    })?;
2674
2675    #[cfg(unix)]
2676    let backend_path = private_root
2677        .backend_path()
2678        .map_err(|error| ServerError::Config {
2679            message: format!("failed to resolve held store.data_dir `{data_dir}`: {error}"),
2680        })?;
2681    #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
2682    crate::filesystem::validate_ambient_backend_ancestors(&backend_path).map_err(|error| {
2683        let (component, reason) = error.into_parts();
2684        ServerError::UnsafeDataRootAncestor {
2685            data_root: backend_path.clone(),
2686            component,
2687            reason,
2688        }
2689    })?;
2690    #[cfg(not(unix))]
2691    let backend_path = std::path::PathBuf::from(data_dir);
2692
2693    let Some(cluster) = cluster else {
2694        let store = if backend_path.join("config.json").exists() {
2695            // The configured budget rides along as the migration ruling for a
2696            // store that predates the field (aion#75: an upgrade must never
2697            // present as data loss); a store that already rules keeps its
2698            // recorded ruling and this value is ignored.
2699            HaematiteStore::open(
2700                &backend_path,
2701                node_cache_budget,
2702                writer_lock_wait_reporter(stage),
2703            )
2704            .map_err(ServerError::from)?
2705        } else {
2706            HaematiteStore::create_with_shard_count(&backend_path, shard_count, node_cache_budget)
2707                .map_err(ServerError::from)?
2708        };
2709        materialize_narrated(&store, shard_count, stage)?;
2710        let store = store.retain_data_root_capability(private_root);
2711        return Ok((store, None));
2712    };
2713
2714    let boot = ClusterBootstrap {
2715        node_id: cluster.node_id,
2716        bind_address: cluster.bind_address,
2717        members: cluster.members,
2718        peers: cluster
2719            .peers
2720            .into_iter()
2721            .map(|peer| (peer.name, peer.address))
2722            .collect(),
2723        timeout: HAEMATITE_CLUSTER_OP_TIMEOUT,
2724    };
2725    let (store, responder) = HaematiteStore::open_or_create_distributed(
2726        &backend_path,
2727        shard_count,
2728        boot,
2729        node_cache_budget,
2730    )
2731    .map_err(ServerError::from)?;
2732    materialize_narrated(&store, shard_count, stage)?;
2733    let store = store.retain_data_root_capability(private_root);
2734    Ok((store, Some(responder)))
2735}
2736
2737/// The writer-lock wait observer the store build hands haematite: every
2738/// still-waiting report becomes a boot stage in the home's pid record.
2739///
2740/// This is the leg that made the 2026-08-26 stacking invisible. A second
2741/// server on one home blocks HERE, inside `Database::open`, with nothing to
2742/// see; recording it means `aion server status` can say who is waiting on
2743/// what, and for how long.
2744fn writer_lock_wait_reporter(
2745    stage: &crate::control::StageReporter,
2746) -> impl FnMut(&std::path::Path, std::time::Duration) + use<'_> {
2747    move |lock_path, waited| {
2748        stage.report(
2749            crate::control::stage::STAGE_WRITER_LOCK_WAIT,
2750            format!(
2751                "waiting {}s on the store writer lock at {} — another live process \
2752                 holds it (a draining predecessor, or another server on this data \
2753                 directory); this boot proceeds the moment it is released",
2754                waited.as_secs(),
2755                lock_path.display()
2756            ),
2757        );
2758    }
2759}
2760
2761/// Materialize every shard with a stage line written when each shard's
2762/// open STARTS — naming the shard id, not its position — and one when the
2763/// last has finished.
2764///
2765/// Materialization is where a shard's index is rebuilt and its WAL replayed,
2766/// and on a large store it is where a boot spends its time. A line at the
2767/// start of each shard means a stage label can never outlive its work by
2768/// more than one shard's open, and `stage_seq` advances with every shard,
2769/// which is how a reader tells a boot that is WORKING from one that is
2770/// WEDGED — and, now, WHICH shard it is working on.
2771fn materialize_narrated(
2772    store: &aion_store_haematite::HaematiteStore,
2773    shard_count: usize,
2774    stage: &crate::control::StageReporter,
2775) -> Result<(), ServerError> {
2776    stage.report(
2777        crate::control::stage::STAGE_STORE_OPEN,
2778        format!("haematite store opened; materializing {shard_count} shards"),
2779    );
2780    store
2781        .materialize_all_shards(|shard, materialized, total| {
2782            stage.report(
2783                crate::control::stage::STAGE_WAL_RECOVERY,
2784                format!("materializing shard {shard} ({materialized} of {total})"),
2785            );
2786        })
2787        .map_err(ServerError::from)?;
2788    stage.report(
2789        crate::control::stage::STAGE_WAL_RECOVERY,
2790        format!("all {shard_count} shards materialized"),
2791    );
2792    Ok(())
2793}
2794
2795fn private_store_mode_error(data_dir: &str, error: &std::io::Error) -> ServerError {
2796    ServerError::Config {
2797        message: format!(
2798            "failed to apply private modes under store.data_dir `{data_dir}`: {error}"
2799        ),
2800    }
2801}
2802
2803/// Per-operation quorum/election timeout for the distributed haematite backend.
2804const HAEMATITE_CLUSTER_OP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
2805
2806/// The NOI-6 intervention transport used when no push transport is compiled in.
2807///
2808/// Without the `liminal-transport` feature there is no way to reach a worker's
2809/// out-of-band connection, so every routed command reports the owning worker
2810/// unreachable — which the router maps onto the attempt-scoped stale-target no-op.
2811/// This keeps the intervention endpoint honest on a transport-less build (an
2812/// operator gets a NACK, never a false "applied") without gating the endpoint on a
2813/// feature.
2814#[cfg(not(feature = "liminal-transport"))]
2815#[derive(Clone, Debug)]
2816struct NullInterventionTransport;
2817
2818#[cfg(not(feature = "liminal-transport"))]
2819#[async_trait::async_trait]
2820impl crate::worker::InterventionTransport for NullInterventionTransport {
2821    async fn push(
2822        &self,
2823        _worker: &crate::worker::WorkerHandle,
2824        _command: aion_core::InterventionCommand,
2825    ) -> Result<aion_core::InterventionOutcome, ServerError> {
2826        Err(ServerError::worker_connection_lost(
2827            "intervention",
2828            "no intervention push transport is compiled in".to_owned(),
2829        ))
2830    }
2831}
2832
2833#[cfg(test)]
2834mod tests {
2835    use std::{net::SocketAddr, time::Duration};
2836
2837    use aion_store::InMemoryStore;
2838
2839    use super::ServerState;
2840    use crate::config::{
2841        AuthConfig, AuthoringConfig, DeployConfig, DevConfig, ListenConfig, MetricsConfig,
2842        NamespaceConfig, NamespaceMode, OpsConsoleAssetSource, OpsConsoleConfig, OutboxConfig,
2843        RuntimeConfig, WebSocketConfig, WorkerConfig,
2844    };
2845
2846    fn runtime_config() -> RuntimeConfig {
2847        RuntimeConfig {
2848            listen: ListenConfig {
2849                grpc: SocketAddr::from(([127, 0, 0, 1], 50051)),
2850                http: SocketAddr::from(([127, 0, 0, 1], 8080)),
2851            },
2852            tls: None,
2853            auth: AuthConfig {
2854                enabled: false,
2855                jwks_url: None,
2856                jwks_refresh_seconds: 300,
2857            },
2858            ops_console: OpsConsoleConfig {
2859                source: OpsConsoleAssetSource::Embedded,
2860            },
2861            namespace: NamespaceConfig {
2862                mode: NamespaceMode::SharedEngine,
2863            },
2864            worker: WorkerConfig {
2865                heartbeat_window: Duration::from_secs(30),
2866                ..WorkerConfig::default()
2867            },
2868            websocket: WebSocketConfig {
2869                outbound_buffer_bound: 32,
2870                event_broadcast_capacity: Some(64),
2871                cluster_broadcast_capacity: Some(64),
2872            },
2873            workflow_packages: Vec::new(),
2874            deploy: DeployConfig::default(),
2875            authoring: AuthoringConfig::default(),
2876            dev: DevConfig::default(),
2877            outbox: OutboxConfig::default(),
2878            observability: crate::config::ObservabilityConfig::with_flush_policy(64, 0),
2879            mcp: crate::config::ResolvedMcpConfig::default(),
2880            assistant: crate::config::ResolvedAssistantConfig::default(),
2881            scheduler_threads: 1,
2882            stop_drain_timeout: Some(std::time::Duration::from_secs(5)),
2883            jit_threshold: None,
2884            query_timeout: Some(Duration::from_secs(10)),
2885            workloop_sweep_interval: Some(Duration::from_millis(50)),
2886            default_namespace: "default".to_owned(),
2887            auto_create: crate::config::AutoCreate::Open,
2888            max_in_flight_activities: crate::config::DEFAULT_MAX_IN_FLIGHT_ACTIVITIES,
2889            drain_timeout: Duration::from_secs(30),
2890            metrics: MetricsConfig { enabled: true },
2891            owned_shards: Vec::new(),
2892            cors_allowed_origins: Vec::new(),
2893        }
2894    }
2895
2896    /// The flush policy is REQUIRED and has no default: a runtime that has not
2897    /// ruled on it refuses to build the transcript publisher, and the refusal
2898    /// names the key the operator must set. This is the loud-at-startup half of
2899    /// "no invented tuning values".
2900    #[test]
2901    fn an_unruled_flush_policy_refuses_to_build_the_publisher() {
2902        for (mutate, expected_key) in [
2903            (
2904                Box::new(|runtime: &mut RuntimeConfig| {
2905                    runtime.observability.max_batch_events = None;
2906                }) as Box<dyn Fn(&mut RuntimeConfig)>,
2907                "observability.max_batch_events",
2908            ),
2909            (
2910                Box::new(|runtime: &mut RuntimeConfig| {
2911                    runtime.observability.max_batch_events = Some(0);
2912                }),
2913                "observability.max_batch_events",
2914            ),
2915            (
2916                Box::new(|runtime: &mut RuntimeConfig| {
2917                    runtime.observability.max_batch_hold_ms = None;
2918                }),
2919                "observability.max_batch_hold_ms",
2920            ),
2921        ] {
2922            let mut runtime = runtime_config();
2923            mutate(&mut runtime);
2924            let message = super::required_transcript_batch_policy(&runtime)
2925                .err()
2926                .map_or_else(String::new, |error| error.to_string());
2927            assert!(
2928                message.contains(expected_key),
2929                "the refusal must name {expected_key}: {message}"
2930            );
2931            assert!(
2932                message.contains("no default"),
2933                "and must say the key has no default: {message}"
2934            );
2935        }
2936    }
2937
2938    /// A stated policy is carried through verbatim — including a hold of ZERO,
2939    /// which is a real ruling ("never wait"), not a missing one.
2940    #[test]
2941    fn a_stated_flush_policy_is_carried_through_verbatim() -> Result<(), Box<dyn std::error::Error>>
2942    {
2943        let mut runtime = runtime_config();
2944        runtime.observability = crate::config::ObservabilityConfig::with_flush_policy(32, 0);
2945        let policy = super::required_transcript_batch_policy(&runtime)?;
2946        assert_eq!(policy.max_batch_events.get(), 32);
2947        assert_eq!(policy.max_hold, Duration::ZERO);
2948
2949        runtime.observability = crate::config::ObservabilityConfig::with_flush_policy(8, 250);
2950        let policy = super::required_transcript_batch_policy(&runtime)?;
2951        assert_eq!(policy.max_batch_events.get(), 8);
2952        assert_eq!(policy.max_hold, Duration::from_millis(250));
2953        Ok(())
2954    }
2955
2956    /// The engine's schema must accept EVERY attribute the server's start
2957    /// writer actually records — the invariant, not an enumeration of names.
2958    ///
2959    /// The two sides are genuinely coupled at runtime: the recorder validates
2960    /// each attribute against this schema before appending, so an attribute the
2961    /// writer produces and the schema does not register fails the START, not
2962    /// just the label (#211). The map is taken from the production
2963    /// `start_search_attributes` with every optional field populated, so any
2964    /// future attribute the writer learns to record is covered here without
2965    /// this test being edited.
2966    #[test]
2967    fn engine_schema_accepts_every_attribute_the_start_writer_records()
2968    -> Result<(), Box<dyn std::error::Error>> {
2969        let schema = super::server_search_attribute_schema()?;
2970        let recorded = crate::api::handlers::workflows::start_search_attributes(
2971            "tenant-a",
2972            Some("gpu"),
2973            Some("Nightly settlement"),
2974        );
2975
2976        assert!(
2977            recorded.contains_key(crate::namespace::DISPLAY_NAME_ATTRIBUTE),
2978            "the fixture must exercise the display-name attribute, or this test \
2979             cannot see its registration go missing"
2980        );
2981        for (name, value) in &recorded {
2982            schema.validate(name, value).map_err(|error| {
2983                format!(
2984                    "the start writer records {name}, but the engine's schema refuses it: {error}"
2985                )
2986            })?;
2987        }
2988        Ok(())
2989    }
2990
2991    #[tokio::test]
2992    async fn builds_state_with_in_memory_store() -> Result<(), Box<dyn std::error::Error>> {
2993        let state =
2994            ServerState::build_with_store(InMemoryStore::default(), runtime_config()).await?;
2995
2996        std::hint::black_box(state.namespace_guard());
2997        std::hint::black_box(state.worker_registry());
2998
2999        Ok(())
3000    }
3001
3002    /// R1 surfacing: a real boot exposes the unserved-queue state, the bridge
3003    /// publishes parked dispatches into THAT instance, and the address leaves
3004    /// the state the moment the dispatch resolves.
3005    ///
3006    /// The dispatch is driven through a dispatcher built over the state's OWN
3007    /// registry, queue state, and engine-backed declaration source — the same
3008    /// three handles `build_bridge_dispatcher` hands the production bridge.
3009    #[tokio::test]
3010    async fn unserved_queues_surfaces_a_parked_dispatch_and_clears_it()
3011    -> Result<(), Box<dyn std::error::Error>> {
3012        use aion::{ActivityDispatch, ActivityDispatcher as _};
3013        use aion_core::{ActivityId, RunId, WorkflowId};
3014        use std::sync::Arc;
3015
3016        let state =
3017            ServerState::build_with_store(InMemoryStore::default(), runtime_config()).await?;
3018        assert!(
3019            state.unserved_queues()?.is_empty(),
3020            "a calm boot has no unserved queues"
3021        );
3022        // The engine-backed reader IS installed on a real boot; with no
3023        // queue-declaring package deployed it can contradict nothing, so it must
3024        // answer Unknown rather than manufacture a structural refusal.
3025        assert!(state.queue_declarations().is_installed());
3026        assert_eq!(
3027            state
3028                .queue_declarations()
3029                .declaration_for("nobody-serves-this"),
3030            crate::worker::QueueDeclaration::Unknown
3031        );
3032
3033        let dispatcher = Arc::new(
3034            crate::worker::WorkerActivityDispatcher::new(
3035                state.worker_registry().clone(),
3036                "default",
3037                crate::worker::HeartbeatTracker::new(Duration::from_secs(5)),
3038            )
3039            .with_queue_state(state.queue_service_state().clone())
3040            .with_queue_declarations(state.queue_declarations().clone()),
3041        );
3042        let workflow_id = WorkflowId::new_v4();
3043        let request = ActivityDispatch {
3044            namespace: "default".to_owned(),
3045            task_queue: "nobody-serves-this".to_owned(),
3046            node: None,
3047            workflow_id: workflow_id.clone(),
3048            run_id: RunId::new_v4(),
3049            activity_id: ActivityId::from_sequence_position(0),
3050            name: "greet".to_owned(),
3051            input: "{}".to_owned(),
3052            config: "{}".to_owned(),
3053            attempt: 1,
3054            advisory: false,
3055            labels: std::collections::BTreeMap::new(),
3056        };
3057        let parked = std::thread::spawn(move || dispatcher.dispatch(request));
3058
3059        let mut unserved = Vec::new();
3060        for _ in 0..30 {
3061            unserved = state.unserved_queues()?;
3062            if !unserved.is_empty() {
3063                break;
3064            }
3065            tokio::time::sleep(Duration::from_millis(100)).await;
3066        }
3067        assert_eq!(unserved.len(), 1, "the parked dispatch is not surfaced");
3068        assert_eq!(
3069            unserved[0].reason,
3070            crate::worker::QueueServiceReason::NoLivePollers,
3071            "an empty catalog must not be read as a structural refusal"
3072        );
3073        assert_eq!(unserved[0].key.task_queue, "nobody-serves-this");
3074        assert_eq!(unserved[0].waiting.len(), 1);
3075        assert_eq!(unserved[0].waiting[0].workflow_id, workflow_id);
3076
3077        // Release the dispatch: a worker arrives whose receiver is already gone.
3078        let (worker_tx, worker_rx) = tokio::sync::mpsc::channel(1);
3079        drop(worker_rx);
3080        let registration = state.worker_registry().register_namespaces(
3081            [String::from("default")],
3082            "nobody-serves-this",
3083            None,
3084            [String::from("greet")].iter(),
3085            worker_tx,
3086        )?;
3087        let outcome = parked.join().map_err(|_| "parked dispatch panicked")?;
3088        assert!(outcome.is_err(), "the released dispatch must resolve");
3089        assert!(
3090            state.unserved_queues()?.is_empty(),
3091            "a resolved dispatch must leave the unserved state"
3092        );
3093        registration.deregister()?;
3094        Ok(())
3095    }
3096
3097    #[tokio::test]
3098    async fn namespace_store_is_reachable_and_functional_after_default_boot()
3099    -> Result<(), Box<dyn std::error::Error>> {
3100        use aion_store::{MintOutcome, NamespaceOrigin};
3101
3102        // A default single-node (in-memory) boot must expose a real, functional
3103        // namespace registry through `state.namespace_store()` — the control
3104        // plane's mint (S5) and `GET /namespaces` (S7) reach the store this way.
3105        let state =
3106            ServerState::build_with_store(InMemoryStore::default(), runtime_config()).await?;
3107
3108        let store = state.namespace_store();
3109
3110        // Mint a fresh namespace: the first reference creates it.
3111        let outcome = store
3112            .register_namespace("orders", NamespaceOrigin::WorkerMint)
3113            .await?;
3114        assert_eq!(
3115            outcome,
3116            MintOutcome::Created,
3117            "the first reference to a namespace mints it"
3118        );
3119
3120        // Re-referencing is idempotent: the record already exists.
3121        let again = store
3122            .register_namespace("orders", NamespaceOrigin::WorkerMint)
3123            .await?;
3124        assert_eq!(
3125            again,
3126            MintOutcome::AlreadyExisted,
3127            "a second reference touches the existing record rather than re-creating it"
3128        );
3129
3130        // Single lookup returns the durable record.
3131        let fetched = store.get_namespace("orders").await?;
3132        let record = fetched.ok_or("registered namespace must be retrievable via get_namespace")?;
3133        assert_eq!(record.name, "orders");
3134        assert_eq!(record.origin, NamespaceOrigin::WorkerMint);
3135
3136        // The live set lists the namespace.
3137        let listed = store.list_namespaces().await?;
3138        assert!(
3139            listed.iter().any(|record| record.name == "orders"),
3140            "list_namespaces returns the minted namespace"
3141        );
3142
3143        Ok(())
3144    }
3145
3146    #[tokio::test(flavor = "multi_thread")]
3147    async fn connect_store_haematite_round_trips_through_event_store()
3148    -> Result<(), Box<dyn std::error::Error>> {
3149        use aion_core::{ContentType, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId};
3150        use aion_store::WriteToken;
3151        use chrono::Utc;
3152
3153        use crate::config::{StoreBackend, StoreConfig};
3154
3155        let data_dir = crate::test_support::private_tempdir()?;
3156        // Single shard, a fresh temp data_dir: the production connect path opens
3157        // an existing haematite database or creates one, then shares the leaf as
3158        // both the engine EventStore and the dispatcher OutboxStore.
3159        let connected = super::connect_store(
3160            StoreConfig {
3161                backend: StoreBackend::Haematite,
3162                owned_shards: Vec::new(),
3163                data_dir: Some(data_dir.path().to_string_lossy().into_owned()),
3164                shard_count: 1,
3165                cluster: None,
3166                node_cache_budget: Some(test_node_cache_budget()?),
3167                ..StoreConfig::default()
3168            },
3169            &crate::control::StageReporter::detached(),
3170        )
3171        .await?;
3172        let event_store = connected.event_store;
3173        assert!(
3174            connected.outbox_store.is_some(),
3175            "the haematite backend shares its leaf store as the dispatcher's outbox store"
3176        );
3177        assert!(
3178            connected.bootstrap_coordinator,
3179            "a single-node haematite boot owns all shards and bootstraps the coordinator"
3180        );
3181        assert!(
3182            connected.cluster_responder.is_none(),
3183            "a single-node (no [cluster]) haematite boot has no distributed responder"
3184        );
3185
3186        let workflow_id = WorkflowId::new_v4();
3187        let event = aion_core::Event::WorkflowStarted {
3188            envelope: EventEnvelope {
3189                seq: 1,
3190                recorded_at: Utc::now(),
3191                workflow_id: workflow_id.clone(),
3192            },
3193            workflow_type: String::from("checkout"),
3194            input: Payload::new(ContentType::Json, b"{}".to_vec()),
3195            run_id: RunId::new_v4(),
3196            parent_run_id: None,
3197            parent_workflow_id: None,
3198            package_version: PackageVersion::new("a".repeat(64)),
3199        };
3200        event_store
3201            .append(
3202                WriteToken::recorder(),
3203                &workflow_id,
3204                std::slice::from_ref(&event),
3205                0,
3206            )
3207            .await?;
3208        let history = event_store.read_history(&workflow_id).await?;
3209        assert_eq!(
3210            history.len(),
3211            1,
3212            "an event appended through the server's dyn EventStore reads back"
3213        );
3214        Ok(())
3215    }
3216
3217    /// A generous node-cache byte budget (1 GiB) for the haematite fixtures.
3218    ///
3219    /// Roomy enough that no fixture here can reach it, so these stay boot-path
3220    /// and data-root tests rather than accidental cache-eviction tests. It is a
3221    /// TEST value, not a default: production reads the operator's ruling.
3222    fn test_node_cache_budget() -> Result<haematite::NodeCacheBudget, Box<dyn std::error::Error>> {
3223        Ok(haematite::NodeCacheBudget::bytes(1 << 30)?)
3224    }
3225
3226    /// How many object files the boot-cost specimen store carries. It only
3227    /// has to be large enough that a per-file walk is unmistakable next to
3228    /// the per-shard budget below — Tom's store has 1.18 M; a few thousand
3229    /// discriminates the same way at test speed.
3230    const BOOT_COST_WORKFLOWS: usize = 800;
3231
3232    /// The boot's open budget, per shard directory and for the root: the
3233    /// component walk that acquires the root, one open to create-or-open each
3234    /// shard directory and one to check its mode. Anything close to the
3235    /// number of OBJECT FILES is a walk, and a walk is what this pin exists
3236    /// to keep out.
3237    const BOOT_OPENS_PER_DIRECTORY: u64 = 8;
3238
3239    /// R4 of the boot-latency brief: opening a store does per-DIRECTORY work,
3240    /// never per-object-file work.
3241    ///
3242    /// Tom's 2026-08-27 boot spent 171 s in silence because the server walked
3243    /// every one of 1.18 M object files, twice, to `chmod` each one. This
3244    /// specimen builds a store with thousands of object files, then counts
3245    /// the opens the server's own capability layer issues while booting over
3246    /// it. A per-file walk fails by COUNT — the number of opens tracks the
3247    /// number of files — independent of how fast the disk happened to be.
3248    ///
3249    /// Two counters, both bounded by the same per-directory budget: this
3250    /// crate's [`crate::filesystem::capability_opens`] (the walk that was
3251    /// deleted lived here) and haematite's `store::file_opens` (0.12.1),
3252    /// which counts what the shard open itself does — measured at 17 opens
3253    /// for 1,200 object files when this pin was written, so the bound holds
3254    /// today and fails the day a shard open starts reading per object.
3255    #[cfg(unix)]
3256    #[tokio::test]
3257    async fn booting_over_a_store_of_many_object_files_opens_per_shard_not_per_file()
3258    -> Result<(), Box<dyn std::error::Error>> {
3259        let sandbox = crate::test_support::private_tempdir()?;
3260        let data_root = sandbox.path().join("data");
3261        let data_dir = data_root
3262            .to_str()
3263            .ok_or("temporary data path was not UTF-8")?
3264            .to_owned();
3265        let shard_count = 4;
3266
3267        populate_boot_cost_store(&data_root, shard_count).await?;
3268        let object_files = count_regular_files(&data_root)?;
3269        let directories = u64::try_from(shard_count)? + 1;
3270        let budget = BOOT_OPENS_PER_DIRECTORY * directories;
3271        assert!(
3272            object_files > budget * 4,
3273            "the specimen must carry far more object files ({object_files}) than the open \
3274             budget ({budget}), or a walk would fit inside the budget and this pin proves \
3275             nothing"
3276        );
3277
3278        crate::filesystem::reset_capability_opens();
3279        haematite::store::reset_file_opens();
3280        let (store, responder) = tokio::task::spawn_blocking(move || {
3281            super::build_haematite_store_with_hook(
3282                &data_dir,
3283                shard_count,
3284                None,
3285                haematite::NodeCacheBudget::bytes(1 << 30).map_err(|error| error.to_string())?,
3286                &crate::control::StageReporter::detached(),
3287                || Ok(()),
3288            )
3289            .map_err(|error| error.to_string())
3290        })
3291        .await??;
3292        let opens = crate::filesystem::capability_opens();
3293        let haematite_opens = haematite::store::file_opens();
3294        assert!(responder.is_none());
3295        assert_post_boot_node_is_private(&store, &data_root).await?;
3296        drop(store);
3297        println!(
3298            "boot-cost pin: {object_files} object files in {shard_count} shards; {opens} opens \
3299             through the server's capability layer and {haematite_opens} inside haematite during \
3300             the open (budget {budget} each)"
3301        );
3302
3303        assert!(
3304            opens <= budget,
3305            "booting over {object_files} object files in {shard_count} shards issued {opens} \
3306             opens through the server's capability layer; the budget is {budget} \
3307             ({BOOT_OPENS_PER_DIRECTORY} per directory) — the boot is walking the tree again"
3308        );
3309        assert!(
3310            haematite_opens <= budget,
3311            "opening the store over {object_files} object files issued {haematite_opens} opens \
3312             inside haematite; the budget is {budget} — the shard open is doing per-object work"
3313        );
3314        Ok(())
3315    }
3316
3317    /// R2 of the boot-latency brief: a shard directory the server's own user
3318    /// left too loose is tightened at boot, the way the root is — by its own
3319    /// `fstat`, without a walk beneath it.
3320    #[cfg(unix)]
3321    #[test]
3322    fn a_permissive_shard_directory_we_own_is_tightened_at_boot()
3323    -> Result<(), Box<dyn std::error::Error>> {
3324        use std::os::unix::fs::PermissionsExt as _;
3325
3326        let sandbox = crate::test_support::private_tempdir()?;
3327        let data_root = sandbox.path().join("data");
3328        let loose = data_root.join("shard-2");
3329        std::fs::create_dir_all(&loose)?;
3330        std::fs::set_permissions(&data_root, std::fs::Permissions::from_mode(0o700))?;
3331        std::fs::set_permissions(&loose, std::fs::Permissions::from_mode(0o755))?;
3332        let data_dir = data_root
3333            .to_str()
3334            .ok_or("temporary data path was not UTF-8")?;
3335
3336        let (captured, built) = crate::test_support::CapturedLogs::capture(|| {
3337            super::build_haematite_store_with_hook(
3338                data_dir,
3339                4,
3340                None,
3341                haematite::NodeCacheBudget::bytes(1 << 30).map_err(|error| error.to_string())?,
3342                &crate::control::StageReporter::detached(),
3343                || Ok(()),
3344            )
3345            .map_err(|error| error.to_string())
3346        });
3347        let (store, _) = built?;
3348        drop(store);
3349
3350        assert_eq!(
3351            std::fs::metadata(&loose)?.permissions().mode() & 0o777,
3352            0o700,
3353            "the loose shard directory is tightened by the boot"
3354        );
3355        let text = captured.text()?;
3356        assert!(
3357            text.contains("tightened a sensitive root") && text.contains("shard-2"),
3358            "the repair is logged and names the directory: {text}"
3359        );
3360        Ok(())
3361    }
3362
3363    /// R5 of the boot-latency brief: every unit of boot work is announced
3364    /// when it STARTS, naming what it is — so a stage label can never outlive
3365    /// its work by more than one shard's open, and the reader of a stuck boot
3366    /// knows which shard it is stuck in.
3367    ///
3368    /// The store build is bracketed by "checking the data root", "store
3369    /// opened; materializing N shards", one line per shard NAMING ITS ID at
3370    /// the start of that shard's open, and "all N shards materialized" — in
3371    /// that order.
3372    #[test]
3373    fn the_store_build_narrates_each_shard_by_id_before_opening_it()
3374    -> Result<(), Box<dyn std::error::Error>> {
3375        const SHARDS: usize = 6;
3376        let sandbox = crate::test_support::private_tempdir()?;
3377        let data_root = sandbox.path().join("data");
3378        let data_dir = data_root
3379            .to_str()
3380            .ok_or("temporary data path was not UTF-8")?;
3381
3382        let (captured, built) = crate::test_support::CapturedLogs::capture(|| {
3383            super::build_haematite_store_with_hook(
3384                data_dir,
3385                SHARDS,
3386                None,
3387                haematite::NodeCacheBudget::bytes(1 << 30).map_err(|error| error.to_string())?,
3388                &crate::control::StageReporter::detached(),
3389                || Ok(()),
3390            )
3391            .map_err(|error| error.to_string())
3392        });
3393        let (store, _) = built?;
3394        drop(store);
3395
3396        let text = captured.text()?;
3397        let position = |needle: &str| -> Result<usize, String> {
3398            text.find(needle)
3399                .ok_or_else(|| format!("missing stage line `{needle}` in:\n{text}"))
3400        };
3401        let checking = position(&format!(
3402            "checking the data root and {SHARDS} shard directories"
3403        ))?;
3404        let opened = position(&format!(
3405            "haematite store opened; materializing {SHARDS} shards"
3406        ))?;
3407        let done = position(&format!("all {SHARDS} shards materialized"))?;
3408        assert!(
3409            checking < opened && opened < done,
3410            "stage lines out of order:\n{text}"
3411        );
3412        for shard in 0..SHARDS {
3413            let line = position(&format!("materializing shard {shard} ("))?;
3414            assert!(
3415                opened < line && line < done,
3416                "shard {shard}'s start line must sit between open and done:\n{text}"
3417            );
3418        }
3419        assert!(
3420            !text.contains(&format!("materializing shard {SHARDS} (")),
3421            "a line names a shard id; ids run 0..{SHARDS}, never the count"
3422        );
3423        Ok(())
3424    }
3425
3426    /// One workflow per append, each landing its own object files — the
3427    /// store shape the boot-cost pin boots over.
3428    async fn populate_boot_cost_store(
3429        data_root: &std::path::Path,
3430        shard_count: usize,
3431    ) -> Result<(), Box<dyn std::error::Error>> {
3432        use aion_core::{ContentType, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId};
3433        use aion_store::{WritableEventStore as _, WriteToken};
3434        use chrono::Utc;
3435
3436        let store = aion_store_haematite::HaematiteStore::create_with_shard_count(
3437            data_root,
3438            shard_count,
3439            test_node_cache_budget()?,
3440        )?;
3441        for index in 0..BOOT_COST_WORKFLOWS {
3442            let workflow_id = WorkflowId::new_v4();
3443            let event = aion_core::Event::WorkflowStarted {
3444                envelope: EventEnvelope {
3445                    seq: 1,
3446                    recorded_at: Utc::now(),
3447                    workflow_id: workflow_id.clone(),
3448                },
3449                workflow_type: format!("boot-cost-{index}"),
3450                input: Payload::new(ContentType::Json, b"{}".to_vec()),
3451                run_id: RunId::new_v4(),
3452                parent_run_id: None,
3453                parent_workflow_id: None,
3454                package_version: PackageVersion::new("a".repeat(64)),
3455            };
3456            store
3457                .append(
3458                    WriteToken::recorder(),
3459                    &workflow_id,
3460                    std::slice::from_ref(&event),
3461                    0,
3462                )
3463                .await?;
3464        }
3465        Ok(())
3466    }
3467
3468    /// Pin A1: a node written AFTER boot is private by its own creation —
3469    /// 0600 in a 0700 prefix directory — with no repair walk having run
3470    /// (the open count is the proof no walk ran). The harness umask (022
3471    /// under nextest) would leave a umask-derived file at 0644, so the
3472    /// assertion discriminates without a spawned umask-000 probe;
3473    /// haematite's own `every_created_path_is_private_under_umask_000`
3474    /// covers the 000 case at the create sites themselves.
3475    async fn assert_post_boot_node_is_private(
3476        store: &aion_store_haematite::HaematiteStore,
3477        data_root: &std::path::Path,
3478    ) -> Result<(), Box<dyn std::error::Error>> {
3479        use std::os::unix::fs::PermissionsExt as _;
3480
3481        use aion_core::{ContentType, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId};
3482        use aion_store::{WritableEventStore as _, WriteToken};
3483        use chrono::Utc;
3484
3485        let workflow_id = WorkflowId::new_v4();
3486        let event = aion_core::Event::WorkflowStarted {
3487            envelope: EventEnvelope {
3488                seq: 1,
3489                recorded_at: Utc::now(),
3490                workflow_id: workflow_id.clone(),
3491            },
3492            workflow_type: String::from("boot-cost-after-boot"),
3493            input: Payload::new(ContentType::Json, b"{}".to_vec()),
3494            run_id: RunId::new_v4(),
3495            parent_run_id: None,
3496            parent_workflow_id: None,
3497            package_version: PackageVersion::new("a".repeat(64)),
3498        };
3499        store
3500            .append(
3501                WriteToken::recorder(),
3502                &workflow_id,
3503                std::slice::from_ref(&event),
3504                0,
3505            )
3506            .await?;
3507        let (newest, parent) = newest_regular_file(data_root)?
3508            .ok_or("the post-boot append must have written a node file")?;
3509        assert_eq!(
3510            std::fs::metadata(&newest)?.permissions().mode() & 0o777,
3511            0o600,
3512            "a node written after boot is private by creation: {}",
3513            newest.display()
3514        );
3515        assert_eq!(
3516            std::fs::metadata(&parent)?.permissions().mode() & 0o777,
3517            0o700,
3518            "its prefix directory is private by creation: {}",
3519            parent.display()
3520        );
3521        Ok(())
3522    }
3523
3524    /// The most recently modified regular file under `root`, with its parent.
3525    fn newest_regular_file(
3526        root: &std::path::Path,
3527    ) -> std::io::Result<Option<(std::path::PathBuf, std::path::PathBuf)>> {
3528        let mut newest: Option<(std::time::SystemTime, std::path::PathBuf)> = None;
3529        let mut pending = vec![root.to_path_buf()];
3530        while let Some(dir) = pending.pop() {
3531            for entry in std::fs::read_dir(&dir)? {
3532                let entry = entry?;
3533                let file_type = entry.file_type()?;
3534                if file_type.is_dir() {
3535                    pending.push(entry.path());
3536                } else if file_type.is_file() {
3537                    let modified = entry.metadata()?.modified()?;
3538                    if newest.as_ref().is_none_or(|(when, _)| modified > *when) {
3539                        newest = Some((modified, entry.path()));
3540                    }
3541                }
3542            }
3543        }
3544        Ok(newest.and_then(|(_, path)| {
3545            let parent = path.parent()?.to_path_buf();
3546            Some((path, parent))
3547        }))
3548    }
3549
3550    fn count_regular_files(root: &std::path::Path) -> std::io::Result<u64> {
3551        let mut total = 0;
3552        for entry in std::fs::read_dir(root)? {
3553            let entry = entry?;
3554            let file_type = entry.file_type()?;
3555            if file_type.is_dir() {
3556                total += count_regular_files(&entry.path())?;
3557            } else if file_type.is_file() {
3558                total += 1;
3559            }
3560        }
3561        Ok(total)
3562    }
3563
3564    #[cfg(unix)]
3565    #[test]
3566    fn haematite_root_swap_before_first_backend_touch_cannot_redirect_writes()
3567    -> Result<(), Box<dyn std::error::Error>> {
3568        use std::os::unix::fs::symlink;
3569
3570        let sandbox = crate::test_support::private_tempdir()?;
3571        let configured_root = sandbox.path().join("data");
3572        let held_root = sandbox.path().join("held-data");
3573        let outside = sandbox.path().join("outside");
3574        std::fs::create_dir(&outside)?;
3575        let configured = configured_root
3576            .to_str()
3577            .ok_or("temporary data path was not UTF-8")?;
3578
3579        let (store, responder) = super::build_haematite_store_with_hook(
3580            configured,
3581            4,
3582            None,
3583            test_node_cache_budget()?,
3584            &crate::control::StageReporter::detached(),
3585            || {
3586                // The server has acquired and hardened `configured_root`, but
3587                // Haematite has not opened or created anything. Replace the
3588                // ambient name with an attacker-controlled symlink at exactly
3589                // the old check/use boundary.
3590                std::fs::rename(&configured_root, &held_root)?;
3591                symlink(&outside, &configured_root)?;
3592                Ok(())
3593            },
3594        )?;
3595        assert!(responder.is_none());
3596
3597        let outside_entries = std::fs::read_dir(&outside)?.collect::<Result<Vec<_>, _>>()?;
3598        assert!(
3599            outside_entries.is_empty(),
3600            "Haematite followed the replaced ambient root and wrote outside"
3601        );
3602        assert!(held_root.join("config.json").is_file());
3603        for shard in 0..4 {
3604            let shard_path = held_root.join(format!("shard-{shard}"));
3605            assert!(shard_path.is_dir(), "shard {shard} was not materialized");
3606            assert!(
3607                std::fs::read_dir(&shard_path)?
3608                    .next()
3609                    .transpose()?
3610                    .is_some(),
3611                "shard {shard} did not run Haematite's materialization path"
3612            );
3613        }
3614
3615        drop(store);
3616        Ok(())
3617    }
3618
3619    #[cfg(any(target_os = "linux", target_os = "android"))]
3620    #[tokio::test]
3621    async fn proc_fd_backend_path_survives_a_post_startup_root_swap()
3622    -> Result<(), Box<dyn std::error::Error>> {
3623        use std::os::unix::fs::symlink;
3624
3625        use aion_core::{ContentType, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId};
3626        use aion_store::{WritableEventStore as _, WriteToken};
3627        use chrono::Utc;
3628
3629        let sandbox = crate::test_support::private_tempdir()?;
3630        let configured_root = sandbox.path().join("data");
3631        let held_root = sandbox.path().join("held-data");
3632        let capture = sandbox.path().join("capture");
3633        std::fs::create_dir(&capture)?;
3634        let configured = configured_root
3635            .to_str()
3636            .ok_or("temporary data path was not UTF-8")?;
3637
3638        let (store, responder) = super::build_haematite_store(
3639            configured,
3640            4,
3641            None,
3642            test_node_cache_budget()?,
3643            &crate::control::StageReporter::detached(),
3644        )?;
3645        assert!(responder.is_none());
3646        std::fs::rename(&configured_root, &held_root)?;
3647        symlink(&capture, &configured_root)?;
3648
3649        let workflow_id = WorkflowId::new_v4();
3650        let event = aion_core::Event::WorkflowStarted {
3651            envelope: EventEnvelope {
3652                seq: 1,
3653                recorded_at: Utc::now(),
3654                workflow_id: workflow_id.clone(),
3655            },
3656            workflow_type: String::from("post-startup-root-swap"),
3657            input: Payload::new(ContentType::Json, b"{}".to_vec()),
3658            run_id: RunId::new_v4(),
3659            parent_run_id: None,
3660            parent_workflow_id: None,
3661            package_version: PackageVersion::new("a".repeat(64)),
3662        };
3663        store
3664            .append(
3665                WriteToken::recorder(),
3666                &workflow_id,
3667                std::slice::from_ref(&event),
3668                0,
3669            )
3670            .await?;
3671
3672        let captured = std::fs::read_dir(&capture)?.collect::<Result<Vec<_>, _>>()?;
3673        assert!(
3674            captured.is_empty(),
3675            "post-startup append followed the replacement symlink into capture"
3676        );
3677        assert!(held_root.join("config.json").is_file());
3678        drop(store);
3679        Ok(())
3680    }
3681
3682    #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
3683    #[test]
3684    fn path_ambient_haematite_refuses_group_or_world_writable_ancestors()
3685    -> Result<(), Box<dyn std::error::Error>> {
3686        use std::os::unix::fs::PermissionsExt as _;
3687
3688        let sandbox = crate::test_support::private_tempdir()?;
3689        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3690
3691        for mode in [0o770, 0o1777] {
3692            let shared = sandbox.path().join(format!("shared-{mode:o}"));
3693            let data_root = shared.join("data");
3694            std::fs::create_dir(&shared)?;
3695            std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(mode))?;
3696            std::fs::create_dir(&data_root)?;
3697            std::fs::set_permissions(&data_root, std::fs::Permissions::from_mode(0o700))?;
3698            let configured = data_root
3699                .to_str()
3700                .ok_or("temporary data path was not UTF-8")?;
3701
3702            let Err(error) = super::build_haematite_store(
3703                configured,
3704                4,
3705                None,
3706                test_node_cache_budget()?,
3707                &crate::control::StageReporter::detached(),
3708            ) else {
3709                return Err(format!("mode {mode:04o} ancestor was accepted").into());
3710            };
3711            let message = error.to_string();
3712            let crate::ServerError::UnsafeDataRootAncestor {
3713                data_root: resolved_root,
3714                component,
3715                reason,
3716            } = error
3717            else {
3718                return Err(format!("expected typed unsafe-ancestor error, got {message}").into());
3719            };
3720            assert_eq!(resolved_root, std::fs::canonicalize(&data_root)?);
3721            assert_eq!(component, std::fs::canonicalize(&shared)?);
3722            assert!(
3723                reason.contains(&format!("mode {mode:04o}")),
3724                "unexpected reason: {reason}"
3725            );
3726            if mode & 0o1000 != 0 {
3727                assert!(reason.contains("sticky bit is not accepted"));
3728            }
3729            assert!(message.contains("private Aion home"));
3730            assert!(
3731                !data_root.join("config.json").exists(),
3732                "Haematite touched its ambient path before the refusal"
3733            );
3734        }
3735        Ok(())
3736    }
3737
3738    #[cfg(target_os = "macos")]
3739    #[test]
3740    fn path_ambient_haematite_refuses_mutating_allow_acl_ancestor()
3741    -> Result<(), Box<dyn std::error::Error>> {
3742        use std::os::unix::fs::PermissionsExt as _;
3743
3744        let sandbox = crate::test_support::private_tempdir()?;
3745        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3746        let shared = sandbox.path().join("acl-shared");
3747        let data_root = shared.join("data");
3748        std::fs::create_dir(&shared)?;
3749        std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o700))?;
3750        let acl = "everyone allow list,search,add_file,add_subdirectory,delete_child";
3751        let status = std::process::Command::new("chmod")
3752            .arg("+a")
3753            .arg(acl)
3754            .arg(&shared)
3755            .status()?;
3756        assert!(status.success(), "failed to install Darwin regression ACL");
3757        let configured = data_root
3758            .to_str()
3759            .ok_or("temporary data path was not UTF-8")?;
3760
3761        let result = super::build_haematite_store(
3762            configured,
3763            4,
3764            None,
3765            test_node_cache_budget()?,
3766            &crate::control::StageReporter::detached(),
3767        );
3768        let cleanup = std::process::Command::new("chmod")
3769            .arg("-RN")
3770            .arg(&shared)
3771            .status()?;
3772        assert!(cleanup.success(), "failed to clean Darwin regression ACL");
3773
3774        let Err(error) = result else {
3775            return Err("mutating non-euid allow ACL ancestor was accepted".into());
3776        };
3777        let message = error.to_string();
3778        let crate::ServerError::UnsafeDataRootAncestor {
3779            component, reason, ..
3780        } = error
3781        else {
3782            return Err(format!("expected typed unsafe-ancestor error, got {message}").into());
3783        };
3784        assert_eq!(component, std::fs::canonicalize(&shared)?);
3785        assert!(
3786            reason.contains("allow"),
3787            "reason did not name the ACE: {reason}"
3788        );
3789        assert!(
3790            reason.contains("everyone"),
3791            "reason did not name the ACE principal: {reason}"
3792        );
3793        assert!(
3794            !data_root.join("config.json").exists(),
3795            "Haematite touched its ambient path before the ACL refusal"
3796        );
3797        Ok(())
3798    }
3799
3800    #[cfg(target_os = "macos")]
3801    #[test]
3802    fn path_ambient_haematite_accepts_the_euid_uuid_allow_ace()
3803    -> Result<(), Box<dyn std::error::Error>> {
3804        use std::os::unix::fs::PermissionsExt as _;
3805
3806        use exacl::{AclEntry, AclOption, Perm};
3807
3808        let sandbox = crate::test_support::private_tempdir()?;
3809        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3810        let private_parent = sandbox.path().join("euid-uuid-allow");
3811        let data_root = private_parent.join("data");
3812        std::fs::create_dir(&private_parent)?;
3813        std::fs::set_permissions(&private_parent, std::fs::Permissions::from_mode(0o700))?;
3814
3815        let server_uid = rustix::process::geteuid().as_raw();
3816        let ace_qualifier = crate::filesystem::darwin_user_uuid_for_test(server_uid)?;
3817        let entry = AclEntry::allow_user(
3818            &ace_qualifier.to_string(),
3819            Perm::EXECUTE | Perm::WRITE | Perm::APPEND | Perm::DELETE_CHILD,
3820            None,
3821        );
3822        exacl::setfacl(
3823            &[private_parent.as_path()],
3824            &[entry],
3825            AclOption::SYMLINK_ACL,
3826        )?;
3827        let configured = data_root
3828            .to_str()
3829            .ok_or("temporary data path was not UTF-8")?;
3830
3831        let result = super::build_haematite_store(
3832            configured,
3833            4,
3834            None,
3835            test_node_cache_budget()?,
3836            &crate::control::StageReporter::detached(),
3837        );
3838        let cleanup = std::process::Command::new("chmod")
3839            .arg("-RN")
3840            .arg(&private_parent)
3841            .status()?;
3842        assert!(cleanup.success(), "failed to clean euid UUID allow ACL");
3843
3844        let (store, responder) = result?;
3845        assert!(responder.is_none());
3846        assert!(data_root.join("config.json").is_file());
3847        drop(store);
3848        Ok(())
3849    }
3850
3851    #[cfg(target_os = "macos")]
3852    #[test]
3853    fn path_ambient_haematite_refuses_a_non_euid_user_uuid_allow_ace()
3854    -> Result<(), Box<dyn std::error::Error>> {
3855        use std::os::unix::fs::PermissionsExt as _;
3856
3857        use exacl::{AclEntry, AclOption, Perm};
3858
3859        let sandbox = crate::test_support::private_tempdir()?;
3860        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3861        let shared = sandbox.path().join("non-euid-uuid-allow");
3862        let data_root = shared.join("data");
3863        std::fs::create_dir(&shared)?;
3864        std::fs::set_permissions(&shared, std::fs::Permissions::from_mode(0o700))?;
3865
3866        let server_uid = rustix::process::geteuid().as_raw();
3867        let foreign_uid = u32::from(server_uid == 0);
3868        let foreign_qualifier = crate::filesystem::darwin_user_uuid_for_test(foreign_uid)?;
3869        let entry = AclEntry::allow_user(
3870            &foreign_qualifier.to_string(),
3871            Perm::EXECUTE | Perm::WRITE | Perm::APPEND | Perm::DELETE_CHILD,
3872            None,
3873        );
3874        exacl::setfacl(&[shared.as_path()], &[entry], AclOption::SYMLINK_ACL)?;
3875        let configured = data_root
3876            .to_str()
3877            .ok_or("temporary data path was not UTF-8")?;
3878
3879        let result = super::build_haematite_store(
3880            configured,
3881            4,
3882            None,
3883            test_node_cache_budget()?,
3884            &crate::control::StageReporter::detached(),
3885        );
3886        let cleanup = std::process::Command::new("chmod")
3887            .arg("-RN")
3888            .arg(&shared)
3889            .status()?;
3890        assert!(cleanup.success(), "failed to clean non-euid UUID allow ACL");
3891
3892        let Err(error) = result else {
3893            return Err("mutating non-euid user UUID allow ACE was accepted".into());
3894        };
3895        let message = error.to_string();
3896        let crate::ServerError::UnsafeDataRootAncestor {
3897            component, reason, ..
3898        } = error
3899        else {
3900            return Err(format!("expected typed unsafe-ancestor error, got {message}").into());
3901        };
3902        assert_eq!(component, std::fs::canonicalize(&shared)?);
3903        assert!(
3904            reason.contains("allow") && reason.contains(&format!("server euid {server_uid}")),
3905            "reason did not name the rejected ACE: {reason}"
3906        );
3907        assert!(
3908            !data_root.join("config.json").exists(),
3909            "Haematite touched its ambient path before the UUID ACL refusal"
3910        );
3911        Ok(())
3912    }
3913
3914    #[cfg(target_os = "macos")]
3915    #[test]
3916    fn path_ambient_haematite_accepts_a_deny_only_acl_ancestor()
3917    -> Result<(), Box<dyn std::error::Error>> {
3918        use std::os::unix::fs::PermissionsExt as _;
3919
3920        let sandbox = crate::test_support::private_tempdir()?;
3921        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3922        let private_parent = sandbox.path().join("deny-only");
3923        let data_root = private_parent.join("data");
3924        std::fs::create_dir(&private_parent)?;
3925        std::fs::set_permissions(&private_parent, std::fs::Permissions::from_mode(0o700))?;
3926        let status = std::process::Command::new("chmod")
3927            .arg("+a")
3928            .arg("everyone deny delete")
3929            .arg(&private_parent)
3930            .status()?;
3931        assert!(status.success(), "failed to install Darwin deny-only ACL");
3932        let configured = data_root
3933            .to_str()
3934            .ok_or("temporary data path was not UTF-8")?;
3935
3936        let result = super::build_haematite_store(
3937            configured,
3938            4,
3939            None,
3940            test_node_cache_budget()?,
3941            &crate::control::StageReporter::detached(),
3942        );
3943        let cleanup = std::process::Command::new("chmod")
3944            .arg("-RN")
3945            .arg(&private_parent)
3946            .status()?;
3947        assert!(cleanup.success(), "failed to clean Darwin deny-only ACL");
3948
3949        let (store, responder) = result?;
3950        assert!(responder.is_none());
3951        assert!(data_root.join("config.json").is_file());
3952        drop(store);
3953        Ok(())
3954    }
3955
3956    #[cfg(target_os = "macos")]
3957    #[test]
3958    fn path_ambient_haematite_accepts_the_stock_home_acl_chain()
3959    -> Result<(), Box<dyn std::error::Error>> {
3960        use std::os::unix::fs::PermissionsExt as _;
3961        use users::os::unix::UserExt as _;
3962
3963        let effective_uid = rustix::process::geteuid().as_raw();
3964        let effective_user = users::get_user_by_uid(effective_uid)
3965            .ok_or_else(|| format!("server euid {effective_uid} has no account record"))?;
3966        let sandbox = tempfile::Builder::new()
3967            .prefix(".aion-acl-home-proof-")
3968            .tempdir_in(effective_user.home_dir())?;
3969        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3970        let data_root = sandbox.path().join("data");
3971        let configured = data_root
3972            .to_str()
3973            .ok_or("temporary data path was not UTF-8")?;
3974
3975        let (store, responder) = super::build_haematite_store(
3976            configured,
3977            4,
3978            None,
3979            test_node_cache_budget()?,
3980            &crate::control::StageReporter::detached(),
3981        )?;
3982        assert!(responder.is_none());
3983        assert!(data_root.join("config.json").is_file());
3984        drop(store);
3985        Ok(())
3986    }
3987
3988    #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
3989    #[test]
3990    fn path_ambient_haematite_accepts_an_owner_controlled_chain()
3991    -> Result<(), Box<dyn std::error::Error>> {
3992        use std::os::unix::fs::PermissionsExt as _;
3993
3994        let sandbox = crate::test_support::private_tempdir()?;
3995        std::fs::set_permissions(sandbox.path(), std::fs::Permissions::from_mode(0o700))?;
3996        let private_parent = sandbox.path().join("private");
3997        let data_root = private_parent.join("data");
3998        std::fs::create_dir(&private_parent)?;
3999        std::fs::set_permissions(&private_parent, std::fs::Permissions::from_mode(0o700))?;
4000        let configured = data_root
4001            .to_str()
4002            .ok_or("temporary data path was not UTF-8")?;
4003
4004        let (store, responder) = super::build_haematite_store(
4005            configured,
4006            4,
4007            None,
4008            test_node_cache_budget()?,
4009            &crate::control::StageReporter::detached(),
4010        )?;
4011        assert!(responder.is_none());
4012        assert!(data_root.join("config.json").is_file());
4013        for shard in 0..4 {
4014            assert!(data_root.join(format!("shard-{shard}")).is_dir());
4015        }
4016        drop(store);
4017        Ok(())
4018    }
4019
4020    #[tokio::test]
4021    async fn connect_store_memory_backend_exposes_no_outbox_store()
4022    -> Result<(), Box<dyn std::error::Error>> {
4023        use crate::config::{StoreBackend, StoreConfig};
4024
4025        // Memory backend: no durable outbox table, so no outbox store handle —
4026        // and `outbox.enabled` over memory is rejected at dispatcher commission.
4027        let connected = super::connect_store(
4028            StoreConfig {
4029                backend: StoreBackend::Memory,
4030                owned_shards: Vec::new(),
4031                data_dir: None,
4032                shard_count: 1,
4033                cluster: None,
4034                node_cache_budget: None,
4035                ..StoreConfig::default()
4036            },
4037            &crate::control::StageReporter::detached(),
4038        )
4039        .await?;
4040        assert!(
4041            connected.outbox_store.is_none(),
4042            "the in-memory backend exposes no outbox store"
4043        );
4044        Ok(())
4045    }
4046
4047    /// The haematite boot path REFUSES a store config that does not rule on the
4048    /// node cache's byte budget, naming the missing key — the same
4049    /// explicit-no-default guard `observability.max_batch_events` uses, applied
4050    /// where the value is used (haematite is the only backend that has a node
4051    /// cache, so this is the seam that consumes the ruling).
4052    #[tokio::test]
4053    async fn haematite_boot_refuses_without_a_node_cache_budget()
4054    -> Result<(), Box<dyn std::error::Error>> {
4055        use crate::ServerError;
4056        use crate::config::{StoreBackend, StoreConfig};
4057
4058        let sandbox = crate::test_support::private_tempdir()?;
4059        let data_dir = sandbox.path().join("data");
4060        let error = super::connect_haematite_store(
4061            StoreConfig {
4062                backend: StoreBackend::Haematite,
4063                data_dir: Some(
4064                    data_dir
4065                        .to_str()
4066                        .ok_or("temporary data path was not UTF-8")?
4067                        .to_owned(),
4068                ),
4069                shard_count: 4,
4070                ..StoreConfig::default()
4071            },
4072            &crate::control::StageReporter::detached(),
4073        )
4074        .await
4075        .err()
4076        .ok_or("the haematite boot path must refuse a store config with no node_cache_budget")?;
4077        let ServerError::Config { message } = error else {
4078            return Err(format!("expected a config refusal, got {error:?}").into());
4079        };
4080        assert!(
4081            message.contains("store.node_cache_budget"),
4082            "the refusal must name the missing key, got: {message}"
4083        );
4084        assert!(
4085            message.contains("AION_STORE_NODE_CACHE_BUDGET"),
4086            "the refusal must name the environment override, got: {message}"
4087        );
4088        Ok(())
4089    }
4090
4091    /// The operator's configured budget reaches the constructed
4092    /// [`haematite::DatabaseConfig`] — observed where haematite records it, in
4093    /// the created database's own `config.json`, so the assertion cannot pass by
4094    /// a value that stopped short of `Database::create`.
4095    #[tokio::test]
4096    async fn configured_node_cache_budget_reaches_the_created_database()
4097    -> Result<(), Box<dyn std::error::Error>> {
4098        use crate::config::{StoreBackend, StoreConfig};
4099
4100        const ONE_GIB: usize = 1 << 30;
4101
4102        let sandbox = crate::test_support::private_tempdir()?;
4103        let data_dir = sandbox.path().join("data");
4104        let connected = super::connect_haematite_store(
4105            StoreConfig {
4106                backend: StoreBackend::Haematite,
4107                data_dir: Some(
4108                    data_dir
4109                        .to_str()
4110                        .ok_or("temporary data path was not UTF-8")?
4111                        .to_owned(),
4112                ),
4113                shard_count: 4,
4114                node_cache_budget: Some(haematite::NodeCacheBudget::bytes(ONE_GIB)?),
4115                ..StoreConfig::default()
4116            },
4117            &crate::control::StageReporter::detached(),
4118        )
4119        .await?;
4120        drop(connected);
4121
4122        let recorded: serde_json::Value =
4123            serde_json::from_slice(&std::fs::read(data_dir.join("config.json"))?)?;
4124        assert_eq!(
4125            recorded.get("node_cache_budget"),
4126            Some(&serde_json::json!({ "bytes": ONE_GIB })),
4127            "the operator's budget must be the one haematite created the database with"
4128        );
4129        Ok(())
4130    }
4131
4132    #[tokio::test]
4133    async fn state_build_fails_without_event_broadcast_capacity()
4134    -> Result<(), Box<dyn std::error::Error>> {
4135        let mut runtime = runtime_config();
4136        runtime.websocket.event_broadcast_capacity = None;
4137
4138        let error = ServerState::build_with_store(InMemoryStore::default(), runtime)
4139            .await
4140            .err()
4141            .ok_or("state build must fail when event streaming is unsized")?;
4142
4143        assert!(error.is_config(), "expected a config error, got {error}");
4144        assert!(
4145            error
4146                .to_string()
4147                .contains("websocket.event_broadcast_capacity"),
4148            "error must name the missing key: {error}"
4149        );
4150        Ok(())
4151    }
4152
4153    #[tokio::test]
4154    async fn state_build_fails_without_query_timeout() -> Result<(), Box<dyn std::error::Error>> {
4155        let mut runtime = runtime_config();
4156        runtime.query_timeout = None;
4157
4158        let error = ServerState::build_with_store(InMemoryStore::default(), runtime)
4159            .await
4160            .err()
4161            .ok_or("state build must fail when the query reply deadline is unset")?;
4162
4163        assert!(error.is_config(), "expected a config error, got {error}");
4164        assert!(
4165            error.to_string().contains("runtime.query_timeout_ms"),
4166            "error must name the missing key: {error}"
4167        );
4168        assert!(
4169            error.to_string().contains("AION_RUNTIME_QUERY_TIMEOUT_MS"),
4170            "error must name the environment override: {error}"
4171        );
4172        Ok(())
4173    }
4174
4175    #[tokio::test]
4176    async fn state_build_fails_with_zero_query_timeout() -> Result<(), Box<dyn std::error::Error>> {
4177        let mut runtime = runtime_config();
4178        runtime.query_timeout = Some(Duration::ZERO);
4179
4180        let error = ServerState::build_with_store(InMemoryStore::default(), runtime)
4181            .await
4182            .err()
4183            .ok_or("state build must fail when the query reply deadline is zero")?;
4184
4185        assert!(error.is_config(), "expected a config error, got {error}");
4186        assert!(
4187            error.to_string().contains("runtime.query_timeout_ms"),
4188            "error must name the zero-valued key: {error}"
4189        );
4190        Ok(())
4191    }
4192
4193    /// THE #189 WIRING PIN (r1 Blocker B1): a completed update check driven
4194    /// through the dispatcher stack `build_decorated_dispatcher` actually
4195    /// builds lands in the update-status slot that same call RETURNS — the
4196    /// slot the boot path stores and `GET /update-status` serves.
4197    ///
4198    /// Everything between the dispatch and the slot is the production object:
4199    /// the real `DeclaredCommandDispatcher` executes a real server-run command
4200    /// through the real `ShellAction` (transcript pump and all), the real
4201    /// `UpdateCheckObserver` sits in its real position, and the assertion
4202    /// reads the slot off the function's own return value. The one test
4203    /// double is the `DeclaredBodies` source — the seam production code
4204    /// installs after the engine exists — and it is SEQUENCED because the
4205    /// gates run offline: the observer's verification (first resolution)
4206    /// sees the genuine `FETCH_COMMAND`, and the executor (second
4207    /// resolution) is handed a local `cat` of the captured real index body,
4208    /// standing in for the network transfer the genuine curl would perform.
4209    ///
4210    /// The r1 review proved the absence of this pin by mutation: returning a
4211    /// FRESH slot instead of the observer's left all 1268 tests green while
4212    /// `/update-status` would answer null forever. Under this test that
4213    /// exact mutation goes red: the returned slot stays empty and the
4214    /// assertion below names it.
4215    #[tokio::test(flavor = "multi_thread")]
4216    async fn a_completed_check_through_the_built_dispatcher_lands_in_the_returned_slot()
4217    -> Result<(), Box<dyn std::error::Error>> {
4218        use std::collections::{BTreeMap, VecDeque};
4219        use std::sync::{Arc, Mutex};
4220
4221        use aion::ActivityDispatch;
4222        use aion_core::{ActivityId, RunId, WorkflowId};
4223        use aion_package::ActionBodyContract;
4224
4225        use crate::update_check::document::{FETCH_ACTION, FETCH_COMMAND, UPDATE_CHECK_QUEUE};
4226        use crate::worker::{DeclaredBodies, DeclaredBodyLookup, DispatchingRun};
4227
4228        /// Hands out one scripted resolution per call, in order. Documented
4229        /// above: first the observer's verification, then the executor's.
4230        struct SequencedBodies {
4231            replies: Mutex<VecDeque<DeclaredBodyLookup>>,
4232        }
4233
4234        impl DeclaredBodies for SequencedBodies {
4235            fn body_for(
4236                &self,
4237                _task_queue: &str,
4238                _action: &str,
4239                _run: DispatchingRun<'_>,
4240            ) -> DeclaredBodyLookup {
4241                let mut replies = match self.replies.lock() {
4242                    Ok(replies) => replies,
4243                    Err(poisoned) => poisoned.into_inner(),
4244                };
4245                replies.pop_front().unwrap_or(DeclaredBodyLookup::None)
4246            }
4247        }
4248
4249        let runtime = runtime_config();
4250        let cluster_publisher = crate::cluster_publisher::ClusterEventPublisher::new(
4251            ServerState::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
4252        );
4253        let namespace_store: Arc<dyn aion_store::NamespaceStore> =
4254            Arc::new(InMemoryStore::default());
4255        let worker_deployment_store: Arc<dyn aion_store::WorkerDeploymentStore> =
4256            Arc::new(InMemoryStore::default());
4257        let metrics = crate::observability::Metrics::new()?;
4258        let seams = super::build_worker_seams(
4259            &runtime,
4260            &cluster_publisher,
4261            &metrics,
4262            &namespace_store,
4263            &worker_deployment_store,
4264            None,
4265        );
4266
4267        // The captured REAL index body, served to the executor by a local
4268        // command instead of the network (gates run offline).
4269        let fixture = concat!(
4270            env!("CARGO_MANIFEST_DIR"),
4271            "/src/update_check/fixtures/aion-cli-index.jsonl"
4272        );
4273        seams.declared_bodies.install(Arc::new(SequencedBodies {
4274            replies: Mutex::new(VecDeque::from([
4275                DeclaredBodyLookup::Declared(ActionBodyContract::Run {
4276                    command: FETCH_COMMAND.to_owned(),
4277                }),
4278                DeclaredBodyLookup::Declared(ActionBodyContract::Run {
4279                    command: format!("cat {fixture}"),
4280                }),
4281            ])),
4282        }));
4283
4284        let transcript = crate::activity_publisher::ActivityEventPublisher::new(
4285            Arc::new(aion_store::InMemoryObservabilityStore::default()),
4286            ServerState::FALLBACK_CLUSTER_BROADCAST_CAPACITY,
4287            crate::activity_publisher::TranscriptBatchPolicy::UNBATCHED,
4288        );
4289        let (dispatcher, _mock_registry, _attempt_owners, _workspace_root, update_status) =
4290            super::build_decorated_dispatcher(&runtime, &seams, transcript);
4291
4292        assert_eq!(
4293            update_status.last(),
4294            None,
4295            "the returned slot must start honestly empty"
4296        );
4297
4298        let dispatch = ActivityDispatch {
4299            namespace: "default".to_owned(),
4300            task_queue: UPDATE_CHECK_QUEUE.to_owned(),
4301            node: None,
4302            workflow_id: WorkflowId::new_v4(),
4303            run_id: RunId::new_v4(),
4304            activity_id: ActivityId::from_sequence_position(1),
4305            name: FETCH_ACTION.to_owned(),
4306            input: "{}".to_owned(),
4307            config: "{}".to_owned(),
4308            attempt: 1,
4309            labels: BTreeMap::new(),
4310            advisory: false,
4311        };
4312        let handle = tokio::task::spawn_blocking(move || dispatcher.dispatch(dispatch));
4313        let encoded = handle
4314            .await?
4315            .map_err(|error| format!("the check dispatch failed: {error}"))?;
4316        let outcome: serde_json::Value = serde_json::from_str(&encoded)?;
4317        assert_eq!(outcome["exit_code"], 0, "the local stand-in command ran");
4318
4319        let recorded = update_status.last().ok_or(
4320            "the completed check must land in the RETURNED slot — the one the boot path \
4321             stores and /update-status serves; an empty slot here is the disconnected-\
4322             producer mis-wire the r1 review proved unmeasured",
4323        )?;
4324        assert_eq!(recorded.latest_known, "0.13.7");
4325        Ok(())
4326    }
4327}