Skip to main content

traverse_embedder/
lib.rs

1//! Public Traverse platform embedder SDK for Rust hosts.
2//!
3//! This crate is the Linux GTK / CLI delivery of spec
4//! `068-public-platform-embedder-packages`: a versioned public package that
5//! implements every `embedder-api/1.0.0` operation (spec
6//! `057-embeddable-runtime-host`) against an application-owned bundle,
7//! without any production dependency on `traverse-cli serve` or
8//! `.traverse/server.json` discovery.
9//!
10//! # Bundle input shape
11//!
12//! [`BundleEmbedder::init`] consumes the application bundle manifest defined
13//! by spec `044-application-bundle-manifest`: an `app.manifest.json` whose
14//! directory contains the referenced component manifests, capability
15//! contracts, WASM artifacts, and workflow definitions. The bundle is
16//! digest-verified at load; an invalid or incompatible bundle is rejected
17//! deterministically with a stable error code and never falls back to a
18//! network sidecar (spec 068 NFR-001).
19//!
20//! # Operation mapping
21//!
22//! | `embedder-api/1.0.0` operation | Rust surface |
23//! | --- | --- |
24//! | `runtime.init` | [`BundleEmbedder::init`] (`Result` replaces `status: ready \| error`) |
25//! | `runtime.shutdown` | [`TraverseEmbedderApi::shutdown`] |
26//! | `runtime.submit` | [`TraverseEmbedderApi::submit`] |
27//! | `runtime.subscribe` | [`TraverseEmbedderApi::subscribe`] |
28//! | `compatible.start` | [`TraverseEmbedderApi::start_compatible`] |
29//! | `compatible.stop` | [`TraverseEmbedderApi::stop_compatible`] |
30//! | `compatible.kill` | [`TraverseEmbedderApi::kill_compatible`] |
31//!
32//! # Event and error mapping
33//!
34//! Events are delivered synchronously, in emission order, as JSON values
35//! with a stable envelope (`kind: "embedder_event"`, `schema_version`,
36//! `event_id`, `sequence`, `event_type`, `workspace_id`, `app_id`,
37//! `session_id`, `data`). Event types are exactly the `embedder-api/1.0.0`
38//! set the runtime produces here: `state_changed`, `capability_invoked`,
39//! `capability_result`, and `error`. Runtime execution errors surface inside
40//! `error` events with the runtime's stable `snake_case` error codes;
41//! embedder-boundary failures use [`EmbedderErrorCode`] codes. Identifiers
42//! (`sess-*`, `req-*`, `evt-*`, `inst-*`) are deterministic counters so the
43//! same bundled input produces identical event JSON on a fresh embedder.
44//!
45//! # Shutdown and cancellation behavior
46//!
47//! [`TraverseEmbedderApi::shutdown`] force-terminates every running
48//! compatible capability instance (emitting a `state_changed` event per
49//! instance, state `killed`), then stops accepting work: every later
50//! `submit`, `start_compatible`, `stop_compatible`, or `kill_compatible`
51//! call is rejected with `runtime_stopped`. Shutdown is idempotent.
52//!
53//! # Compatibility and upgrade policy
54//!
55//! * Embedder API: `1.0.0` (`https://traverse.dev/embedder-api/1.0.0`).
56//!   A new IDL version requires a new conformance suite revision and a
57//!   minor (pre-1.0: patch-compatible) crate release that states the new
58//!   version in its release evidence.
59//! * Bundle schema: [`SUPPORTED_BUNDLE_SCHEMA_VERSIONS`]. Bundles declaring
60//!   any other `schema_version` are rejected at `init` with
61//!   `unsupported_bundle_schema` and the mismatch is spelled out in the
62//!   error message.
63//! * Runtime: the Traverse runtime is linked natively into this crate at
64//!   the same workspace version; there is no separately shipped
65//!   runtime-WASM artifact for the Rust package. Release evidence
66//!   ([`TraverseEmbedderApi::release_evidence`]) records the package
67//!   version, the linked runtime version, the embedder API and conformance
68//!   versions, and the digest of every bundled WASM component so a
69//!   downstream binary can be connected to its inputs (spec 068 NFR-002).
70//! * Semantic versioning: breaking public-API changes require a major
71//!   version bump once the crate reaches 1.0.0; until then the whole
72//!   workspace versions in lockstep.
73//!
74//! # Security posture
75//!
76//! [`SecurityPosture::Production`] (the default) rejects unsigned bundle
77//! artifacts per spec `030-security-identity-model` FR-013.
78//! [`SecurityPosture::Development`] permits locally built unsigned bundles
79//! for development and conformance fixtures, exactly like the dev sidecar's
80//! loopback modes. Secrets never appear in events, errors, or release
81//! evidence: the embedder emits only runtime-owned outputs and stable
82//! error metadata (spec 068 NFR-004).
83
84mod registry_cache;
85mod test_double;
86
87pub use registry_cache::{
88    HostRegistryCache, RegistryArtifactFetcher, RegistryCacheError, RegistryCacheErrorCode,
89    RegistryPrepareEvidence, VerifiedRegistryDependency, prepare as prepare_registry_dependency,
90    resolve_component as resolve_registry_component,
91    resolve_offline as resolve_registry_dependency_offline,
92};
93pub use test_double::EmbedderTestDouble;
94
95use serde_json::{Value, json};
96use std::collections::{BTreeMap, VecDeque};
97use std::path::{Path, PathBuf};
98use std::sync::atomic::{AtomicU64, Ordering};
99use traverse_registry::{
100    ApplicationManifestError, ApplicationManifestErrorCode, ApplicationManifestFailure,
101    ApplicationRegistrationFailure, ApplicationRegistrationRequest, ApplicationRegistry,
102    CapabilityRegistry, ComponentExecutionMode, EventRegistry, RegistryComponentResolver,
103    RegistryReference, RegistryScope, ResolvedRegistryComponent, WorkflowRegistry,
104    load_application_bundle_manifest, load_application_bundle_manifest_with_resolver,
105};
106use traverse_runtime::data_store::{
107    DataStore, DataStoreError, DataStoreErrorCode, LocalDataClassification, StateRecord,
108};
109use traverse_runtime::{
110    ArtifactRouter, ExecutionFailureReason, PlacementTarget, Runtime, RuntimeContext, RuntimeError,
111    RuntimeErrorCode, RuntimeExecutionOutcome, RuntimeIntent, RuntimeLookup, RuntimeLookupScope,
112    RuntimeRequest, RuntimeResultStatus, WorkflowExecutionOutcome, WorkflowExecutionRequest,
113    WorkflowLookupScope, WorkflowTraversalStatus, WorkflowTraversalStepStatus,
114};
115
116/// Implemented embedder API version (spec 057 IDL `$id` suffix).
117pub const EMBEDDER_API_VERSION: &str = "1.0.0";
118
119/// Conformance suite revision this package certifies against (spec 057).
120pub const EMBEDDER_CONFORMANCE_VERSION: &str = "1.0.0";
121
122/// Implemented companion Trace API version (spec 517).
123pub const EMBEDDED_TRACE_API_VERSION: &str = "1.0.0";
124
125/// Maximum number of public trace records retained by one embedded session.
126pub const EMBEDDED_TRACE_RETENTION_LIMIT: usize = 100;
127
128/// Largest page the public embedded Trace API returns in one call.
129pub const EMBEDDED_TRACE_MAX_PAGE_SIZE: usize = 100;
130
131/// Application bundle manifest `schema_version` values this package accepts.
132pub const SUPPORTED_BUNDLE_SCHEMA_VERSIONS: &[&str] = &["1.0.0"];
133
134const EVENT_SCHEMA_VERSION: &str = "1.0.0";
135const DEFAULT_WORKSPACE_ID: &str = "local-default";
136static NEXT_EMBEDDED_TRACE_SESSION: AtomicU64 = AtomicU64::new(1);
137
138/// Stable machine-readable public Trace API failure codes (spec 517 FR-010).
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum EmbeddedTraceApiErrorCode {
141    /// The cursor is malformed, stale, or belongs to another embedder session.
142    InvalidCursor,
143    /// The requested trace is no longer retained by this session.
144    TraceNotFound,
145    /// The embedder has been stopped and cannot serve local diagnostics.
146    TraceApiUnavailable,
147    /// The caller requested a companion API version this package does not support.
148    IncompatibleVersion,
149}
150
151impl EmbeddedTraceApiErrorCode {
152    /// Returns the stable wire representation of this code.
153    #[must_use]
154    pub const fn as_str(self) -> &'static str {
155        match self {
156            Self::InvalidCursor => "invalid_cursor",
157            Self::TraceNotFound => "trace_not_found",
158            Self::TraceApiUnavailable => "trace_api_unavailable",
159            Self::IncompatibleVersion => "incompatible_version",
160        }
161    }
162}
163
164/// A public Trace API failure with a stable code and deliberately generic text.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct EmbeddedTraceApiError {
167    /// Machine-readable failure classification.
168    pub code: EmbeddedTraceApiErrorCode,
169    /// Safe explanatory text. It never contains runtime error details.
170    pub message: &'static str,
171}
172
173impl EmbeddedTraceApiError {
174    fn new(code: EmbeddedTraceApiErrorCode) -> Self {
175        let message = match code {
176            EmbeddedTraceApiErrorCode::InvalidCursor => {
177                "the trace cursor is invalid for this embedded session"
178            }
179            EmbeddedTraceApiErrorCode::TraceNotFound => {
180                "the requested trace is not retained by this embedded session"
181            }
182            EmbeddedTraceApiErrorCode::TraceApiUnavailable => {
183                "the embedded Trace API is unavailable because the host is stopped"
184            }
185            EmbeddedTraceApiErrorCode::IncompatibleVersion => {
186                "the requested embedded Trace API version is not supported"
187            }
188        };
189        Self { code, message }
190    }
191}
192
193/// The safe terminal outcome exposed by the public embedded Trace API.
194#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum EmbeddedTraceOutcome {
196    /// Runtime execution completed successfully.
197    Completed,
198    /// Runtime execution produced a stable failure classification.
199    Error,
200}
201
202/// One public phase code in a safe trace projection.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct EmbeddedTracePhase {
205    /// Stable phase classification; no phase payload or telemetry is exposed.
206    pub code: String,
207}
208
209/// Safe selected-target evidence for a public trace detail.
210#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct EmbeddedTraceSelectedTarget {
212    /// Runtime-selected capability or workflow identity.
213    pub target_id: String,
214    /// Selected target version when runtime evidence has one.
215    pub target_version: Option<String>,
216}
217
218/// Safe placement evidence for a public trace detail.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct EmbeddedTracePlacement {
221    /// Placement selected by the runtime, rendered as a stable code.
222    pub target: String,
223}
224
225/// Safe list-oriented record for one completed local execution.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct EmbeddedTraceSummary {
228    /// Opaque public identifier scoped to this embedded session.
229    pub trace_id: String,
230    /// Runtime execution identifier, or a derived workflow execution identifier.
231    pub execution_id: String,
232    /// Submitted bundled target identity.
233    pub target_id: String,
234    /// Deterministic session-local completion time in UTC representation.
235    pub completed_at: String,
236    /// Monotonic completion evidence used for deterministic ordering.
237    pub completion_sequence: u64,
238    /// Safe terminal outcome.
239    pub outcome: EmbeddedTraceOutcome,
240}
241
242/// Safe public diagnostic detail for one retained local trace.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct EmbeddedTraceDetail {
245    /// The corresponding list summary.
246    pub summary: EmbeddedTraceSummary,
247    /// Safe runtime or workflow phase classifications.
248    pub phases: Vec<EmbeddedTracePhase>,
249    /// Selected target evidence when runtime evidence reached selection.
250    pub selected_target: Option<EmbeddedTraceSelectedTarget>,
251    /// Selected placement evidence when available.
252    pub placement: Option<EmbeddedTracePlacement>,
253    /// Stable runtime or traversal failure classification, never error text.
254    pub failure_code: Option<String>,
255    /// Whether runtime state-machine evidence has no recorded violations.
256    pub state_machine_valid: Option<bool>,
257}
258
259/// A bounded, cursor-paged public Trace API response.
260#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct EmbeddedTracePage {
262    /// Newest-first public trace summaries.
263    pub summaries: Vec<EmbeddedTraceSummary>,
264    /// Opaque cursor for the following page, if retained results remain.
265    pub next_cursor: Option<String>,
266    /// Fixed process-local retention capacity advertised to consumers.
267    pub retention_limit: usize,
268}
269
270/// The additive `embedded-trace-api/1.0.0` companion surface (spec 517).
271///
272/// This trait intentionally does not alter [`TraverseEmbedderApi`]. A host
273/// that implements it provides `trace.list` and `trace.get` over a bounded,
274/// process-local safe projection; callers must request the advertised version.
275pub trait EmbeddedTraceApi {
276    /// Returns the companion API version advertised by this host.
277    fn embedded_trace_api_version(&self) -> &'static str;
278
279    /// `trace.list`: returns a deterministic page of safe local summaries.
280    ///
281    /// # Errors
282    ///
283    /// Returns a stable API error when the requested version is unsupported,
284    /// the page size is invalid, or the opaque cursor is malformed or belongs
285    /// to another host session.
286    fn trace_list(
287        &self,
288        requested_version: &str,
289        page_size: usize,
290        cursor: Option<&str>,
291    ) -> Result<EmbeddedTracePage, EmbeddedTraceApiError>;
292
293    /// `trace.get`: returns one safe retained local trace detail.
294    ///
295    /// # Errors
296    ///
297    /// Returns a stable API error when the requested version is unsupported,
298    /// the trace identifier is malformed, or the retained trace is absent.
299    fn trace_get(
300        &self,
301        requested_version: &str,
302        trace_id: &str,
303    ) -> Result<EmbeddedTraceDetail, EmbeddedTraceApiError>;
304}
305
306/// Runtime artifact verification posture for the embedded runtime.
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
308pub enum SecurityPosture {
309    /// Reject unsigned bundle artifacts (spec 030 FR-013). Default.
310    #[default]
311    Production,
312    /// Allow locally built unsigned bundle artifacts with a runtime warning.
313    Development,
314}
315
316/// Configuration for [`BundleEmbedder::init`] (`runtime.init` input).
317#[derive(Debug, Clone)]
318pub struct EmbedderConfig {
319    /// Path to the application bundle's `app.manifest.json`.
320    pub manifest_bundle_path: PathBuf,
321    /// Workspace identity recorded on registrations and events.
322    pub workspace_id: String,
323    /// Platform identity checked against compatible-capability allowlists.
324    pub platform: String,
325    /// Artifact verification posture.
326    pub security: SecurityPosture,
327    /// Optional host-owned verified registry cache used to resolve
328    /// `registry_ref` components offline at `init` (Spec 080).
329    pub registry_cache: Option<HostRegistryCache>,
330}
331
332impl EmbedderConfig {
333    /// Creates a config with IDL defaults: workspace `local-default`, the
334    /// compiling platform's OS identifier, and the production security
335    /// posture.
336    #[must_use]
337    pub fn new(manifest_bundle_path: impl Into<PathBuf>) -> Self {
338        Self {
339            manifest_bundle_path: manifest_bundle_path.into(),
340            workspace_id: DEFAULT_WORKSPACE_ID.to_string(),
341            platform: std::env::consts::OS.to_string(),
342            security: SecurityPosture::Production,
343            registry_cache: None,
344        }
345    }
346
347    /// Attach a host-owned verified registry cache for offline `registry_ref`
348    /// resolution during `init`.
349    #[must_use]
350    pub fn with_registry_cache(mut self, cache: HostRegistryCache) -> Self {
351        self.registry_cache = Some(cache);
352        self
353    }
354}
355
356/// An explicitly host-owned local store that may be injected into a
357/// [`BundleEmbedder`]. The host selects its root and lifecycle before
358/// constructing this wrapper; Traverse never receives a root path.
359pub struct HostDataStore {
360    adapter: Box<dyn DataStore>,
361    classification: LocalDataClassification,
362}
363
364impl HostDataStore {
365    /// Wrap a host-created adapter with its fixed data classification.
366    #[must_use]
367    pub fn new<A>(adapter: A, classification: LocalDataClassification) -> Self
368    where
369        A: DataStore + 'static,
370    {
371        Self {
372            adapter: Box::new(adapter),
373            classification,
374        }
375    }
376}
377
378/// Safe public projection of a `DataStore` failure.
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct EmbeddedDataStoreError {
381    /// Stable machine-readable failure code.
382    pub code: &'static str,
383    /// Safe operation metadata; it never contains a key, value, or host path.
384    pub operation: &'static str,
385}
386
387impl EmbeddedDataStoreError {
388    fn not_configured(operation: &'static str) -> Self {
389        Self {
390            code: "data_store_not_configured",
391            operation,
392        }
393    }
394
395    fn from_error(operation: &'static str, error: &DataStoreError) -> Self {
396        let code = match error.code {
397            DataStoreErrorCode::IntegrityCheckFailed => "integrity_check_failed",
398            DataStoreErrorCode::StoreLocked => "store_locked",
399            DataStoreErrorCode::DurabilityCommitFailed => "durability_commit_failed",
400            DataStoreErrorCode::IoFailure => "storage_io_failed",
401            DataStoreErrorCode::InvalidKey => "invalid_key",
402            DataStoreErrorCode::SerializationFailure => "serialization_failed",
403            DataStoreErrorCode::SchemaValidationError => "schema_validation_failed",
404            DataStoreErrorCode::NoStateSchemaDeclared => "state_schema_unavailable",
405            DataStoreErrorCode::LamportClockOverflow => "lamport_clock_overflow",
406            DataStoreErrorCode::SyncFailure => "sync_failed",
407            DataStoreErrorCode::KeyProviderRequired => "key_provider_required",
408            DataStoreErrorCode::KeyNotFound => "key_not_found",
409            DataStoreErrorCode::KeyExpired => "key_expired",
410            DataStoreErrorCode::KeyProviderFailure => "key_provider_failed",
411            DataStoreErrorCode::CryptoFailure => "crypto_failed",
412            DataStoreErrorCode::ClassificationChangeNotAllowed => {
413                "classification_change_not_allowed"
414            }
415            DataStoreErrorCode::RemoteConflict => "remote_conflict",
416            DataStoreErrorCode::RemoteUnavailable => "remote_unavailable",
417            DataStoreErrorCode::RemoteTimeout => "remote_timeout",
418            DataStoreErrorCode::RemoteOutcomeUnknown => "remote_outcome_unknown",
419            DataStoreErrorCode::RemoteUnauthorized => "remote_unauthorized",
420            DataStoreErrorCode::RemoteScopeDenied => "remote_scope_denied",
421            DataStoreErrorCode::RemoteIntegrityFailed => "remote_integrity_failed",
422            DataStoreErrorCode::RemoteBackendFailed => "remote_backend_failed",
423        };
424        Self { code, operation }
425    }
426}
427
428/// Stable embedder-boundary error codes.
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
430pub enum EmbedderErrorCode {
431    /// The bundle failed to load, validate, or register.
432    BundleLoadFailed,
433    /// The bundle declares a manifest schema version this package does not support.
434    UnsupportedBundleSchema,
435    /// The bundle path could not be resolved to an absolute path.
436    BundlePathInvalid,
437    /// The embedded WASM executor could not initialize.
438    ExecutorUnavailable,
439    /// The runtime was shut down; no further operations are accepted.
440    RuntimeStopped,
441    /// The submitted target is neither a bundled workflow nor a bundled capability.
442    TargetNotFound,
443    /// The target is a compatible-mode capability; use the compatible lifecycle.
444    CompatibleLifecycleRequired,
445    /// The capability exists but is not a compatible-mode capability.
446    CapabilityNotCompatible,
447    /// The capability's platform allowlist does not include this platform.
448    PlatformNotSupported,
449    /// No instance with the given id exists for the capability.
450    InstanceNotFound,
451    /// No running instance matches the request.
452    InstanceNotRunning,
453}
454
455impl EmbedderErrorCode {
456    /// Stable `snake_case` wire representation of the code.
457    #[must_use]
458    pub fn as_str(self) -> &'static str {
459        match self {
460            Self::BundleLoadFailed => "bundle_load_failed",
461            Self::UnsupportedBundleSchema => "unsupported_bundle_schema",
462            Self::BundlePathInvalid => "bundle_path_invalid",
463            Self::ExecutorUnavailable => "executor_unavailable",
464            Self::RuntimeStopped => "runtime_stopped",
465            Self::TargetNotFound => "target_not_found",
466            Self::CompatibleLifecycleRequired => "compatible_lifecycle_required",
467            Self::CapabilityNotCompatible => "capability_not_compatible",
468            Self::PlatformNotSupported => "platform_not_supported",
469            Self::InstanceNotFound => "instance_not_found",
470            Self::InstanceNotRunning => "instance_not_running",
471        }
472    }
473}
474
475/// A structured embedder-boundary error.
476#[derive(Debug, Clone, PartialEq, Eq)]
477pub struct EmbedderError {
478    /// Stable error code.
479    pub code: EmbedderErrorCode,
480    /// Human-readable, secret-free explanation.
481    pub message: String,
482}
483
484impl EmbedderError {
485    fn new(code: EmbedderErrorCode, message: impl Into<String>) -> Self {
486        Self {
487            code,
488            message: message.into(),
489        }
490    }
491
492    fn as_value(&self) -> Value {
493        json!({ "code": self.code.as_str(), "message": self.message })
494    }
495}
496
497/// `runtime.submit` acceptance status.
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum SubmitStatus {
500    /// The submission was accepted and executed; results arrived as events.
501    Accepted,
502    /// The submission was rejected at the embedder boundary.
503    Rejected,
504}
505
506/// `runtime.submit` output.
507#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct SubmitOutcome {
509    /// Session identifier (`null` in the IDL when rejected).
510    pub session_id: Option<String>,
511    /// Acceptance status.
512    pub status: SubmitStatus,
513    /// Boundary rejection error, when rejected.
514    pub error: Option<EmbedderError>,
515}
516
517/// `compatible.start` output.
518#[derive(Debug, Clone, PartialEq, Eq)]
519pub struct CompatibleStartOutcome {
520    /// Instance identifier (`null` in the IDL on error).
521    pub instance_id: Option<String>,
522    /// `started` on success.
523    pub status: CompatibleLifecycleStatus,
524    /// Boundary error, when not started.
525    pub error: Option<EmbedderError>,
526}
527
528/// `compatible.stop` / `compatible.kill` output.
529#[derive(Debug, Clone, PartialEq, Eq)]
530pub struct CompatibleLifecycleOutcome {
531    /// `stopped` or `killed` on success.
532    pub status: CompatibleLifecycleStatus,
533    /// Boundary error, when the lifecycle change did not happen.
534    pub error: Option<EmbedderError>,
535}
536
537/// Compatible-capability lifecycle statuses.
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum CompatibleLifecycleStatus {
540    /// The instance is running.
541    Started,
542    /// The instance stopped gracefully.
543    Stopped,
544    /// The instance was force-terminated.
545    Killed,
546    /// The lifecycle operation failed.
547    Error,
548}
549
550/// `runtime.shutdown` output (always `stopped`).
551#[derive(Debug, Clone, Copy, PartialEq, Eq)]
552pub struct ShutdownOutcome {
553    /// Number of compatible instances force-terminated by this call.
554    pub killed_instances: usize,
555}
556
557/// Ordered, synchronous event subscriber.
558pub type EventCallback = Box<dyn FnMut(&Value) + Send>;
559
560/// The uniform `embedder-api/1.0.0` operation surface (spec 057 FR-003).
561///
562/// [`BundleEmbedder`] is the production implementation;
563/// [`EmbedderTestDouble`] is the deterministic in-memory test double
564/// required by spec 068 FR-006. Both emit identical event envelopes.
565pub trait TraverseEmbedderApi {
566    /// `runtime.submit`: execute a bundled workflow or WASM capability.
567    fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome;
568
569    /// `runtime.subscribe`: register an ordered event callback. Previously
570    /// emitted events are replayed to the new subscriber first, so late
571    /// subscribers observe the identical ordered stream.
572    fn subscribe(&mut self, callback: EventCallback);
573
574    /// `compatible.start`: start a compatible-mode capability instance.
575    fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome;
576
577    /// `compatible.stop`: gracefully stop one instance (`instance_id`) or
578    /// every running instance of the capability (`None`).
579    fn stop_compatible(
580        &mut self,
581        capability_id: &str,
582        instance_id: Option<&str>,
583    ) -> CompatibleLifecycleOutcome;
584
585    /// `compatible.kill`: force-terminate one instance (`instance_id`) or
586    /// every running instance of the capability (`None`).
587    fn kill_compatible(
588        &mut self,
589        capability_id: &str,
590        instance_id: Option<&str>,
591    ) -> CompatibleLifecycleOutcome;
592
593    /// `runtime.shutdown`: kill running compatible instances and stop
594    /// accepting operations. Idempotent.
595    fn shutdown(&mut self) -> ShutdownOutcome;
596
597    /// Release evidence connecting this embedder to its package version,
598    /// linked runtime, conformance version, and bundle digests (spec 068
599    /// FR-008, NFR-002).
600    fn release_evidence(&self) -> Value;
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604enum InstanceState {
605    Started,
606    Stopped,
607    Killed,
608}
609
610impl InstanceState {
611    fn as_str(self) -> &'static str {
612        match self {
613            Self::Started => "started",
614            Self::Stopped => "stopped",
615            Self::Killed => "killed",
616        }
617    }
618}
619
620#[derive(Debug, Clone)]
621struct CompatibleInstance {
622    capability_id: String,
623    state: InstanceState,
624}
625
626struct EmbeddedTraceRecordInput {
627    execution_id: String,
628    target_id: String,
629    outcome: EmbeddedTraceOutcome,
630    phases: Vec<EmbeddedTracePhase>,
631    selected_target: Option<EmbeddedTraceSelectedTarget>,
632    placement: Option<EmbeddedTracePlacement>,
633    failure_code: Option<String>,
634    state_machine_valid: Option<bool>,
635}
636
637fn logical_completion_time(sequence: u64) -> String {
638    let minute = (sequence / 60) % 60;
639    let second = sequence % 60;
640    format!("1970-01-01T00:{minute:02}:{second:02}Z")
641}
642
643/// Shared deterministic embedder state: identity, counters, subscribers,
644/// event history, and the compatible-capability lifecycle table. Both the
645/// production embedder and the test double delegate here so their public
646/// boundary behavior is identical.
647pub(crate) struct EmbedderCore {
648    workspace_id: String,
649    app_id: String,
650    app_version: String,
651    platform: String,
652    compatible_targets: BTreeMap<String, Vec<String>>,
653    instances: BTreeMap<String, CompatibleInstance>,
654    subscribers: Vec<EventCallback>,
655    history: Vec<Value>,
656    next_event: u64,
657    next_session: u64,
658    next_request: u64,
659    next_instance: u64,
660    trace_session: u64,
661    next_trace: u64,
662    traces: VecDeque<EmbeddedTraceDetail>,
663    stopped: bool,
664}
665
666impl EmbedderCore {
667    pub(crate) fn new(
668        workspace_id: String,
669        app_id: String,
670        app_version: String,
671        platform: String,
672        compatible_targets: BTreeMap<String, Vec<String>>,
673    ) -> Self {
674        Self {
675            workspace_id,
676            app_id,
677            app_version,
678            platform,
679            compatible_targets,
680            instances: BTreeMap::new(),
681            subscribers: Vec::new(),
682            history: Vec::new(),
683            next_event: 0,
684            next_session: 0,
685            next_request: 0,
686            next_instance: 0,
687            trace_session: NEXT_EMBEDDED_TRACE_SESSION.fetch_add(1, Ordering::Relaxed),
688            next_trace: 0,
689            traces: VecDeque::new(),
690            stopped: false,
691        }
692    }
693
694    fn next_session_id(&mut self) -> String {
695        self.next_session += 1;
696        format!("sess-{:08}", self.next_session)
697    }
698
699    fn next_request_id(&mut self) -> String {
700        self.next_request += 1;
701        format!("req-{:08}", self.next_request)
702    }
703
704    fn next_instance_id(&mut self) -> String {
705        self.next_instance += 1;
706        format!("inst-{:08}", self.next_instance)
707    }
708
709    fn record_trace(&mut self, input: EmbeddedTraceRecordInput) {
710        self.next_trace += 1;
711        let sequence = self.next_trace;
712        let summary = EmbeddedTraceSummary {
713            trace_id: format!("embedded-trace-{:08}-{:08}", self.trace_session, sequence),
714            execution_id: input.execution_id,
715            target_id: input.target_id,
716            completed_at: logical_completion_time(sequence),
717            completion_sequence: sequence,
718            outcome: input.outcome,
719        };
720        self.traces.push_back(EmbeddedTraceDetail {
721            summary,
722            phases: input.phases,
723            selected_target: input.selected_target,
724            placement: input.placement,
725            failure_code: input.failure_code,
726            state_machine_valid: input.state_machine_valid,
727        });
728        if self.traces.len() > EMBEDDED_TRACE_RETENTION_LIMIT {
729            let _ = self.traces.pop_front();
730        }
731    }
732
733    fn trace_list(
734        &self,
735        requested_version: &str,
736        page_size: usize,
737        cursor: Option<&str>,
738    ) -> Result<EmbeddedTracePage, EmbeddedTraceApiError> {
739        self.ensure_trace_api_available(requested_version)?;
740        let traces = self.newest_traces();
741        let start = match cursor {
742            None => 0,
743            Some(cursor) => self.cursor_start(cursor, &traces)?,
744        };
745        let page_size = page_size.clamp(1, EMBEDDED_TRACE_MAX_PAGE_SIZE);
746        let end = start.saturating_add(page_size).min(traces.len());
747        let summaries = traces[start..end]
748            .iter()
749            .map(|detail| detail.summary.clone())
750            .collect::<Vec<_>>();
751        let next_cursor =
752            (end < traces.len()).then(|| self.cursor_for(&traces[end - 1].summary.trace_id));
753        Ok(EmbeddedTracePage {
754            summaries,
755            next_cursor,
756            retention_limit: EMBEDDED_TRACE_RETENTION_LIMIT,
757        })
758    }
759
760    fn trace_get(
761        &self,
762        requested_version: &str,
763        trace_id: &str,
764    ) -> Result<EmbeddedTraceDetail, EmbeddedTraceApiError> {
765        self.ensure_trace_api_available(requested_version)?;
766        self.traces
767            .iter()
768            .find(|detail| detail.summary.trace_id == trace_id)
769            .cloned()
770            .ok_or_else(|| EmbeddedTraceApiError::new(EmbeddedTraceApiErrorCode::TraceNotFound))
771    }
772
773    fn ensure_trace_api_available(
774        &self,
775        requested_version: &str,
776    ) -> Result<(), EmbeddedTraceApiError> {
777        if self.stopped {
778            return Err(EmbeddedTraceApiError::new(
779                EmbeddedTraceApiErrorCode::TraceApiUnavailable,
780            ));
781        }
782        if requested_version != EMBEDDED_TRACE_API_VERSION {
783            return Err(EmbeddedTraceApiError::new(
784                EmbeddedTraceApiErrorCode::IncompatibleVersion,
785            ));
786        }
787        Ok(())
788    }
789
790    fn newest_traces(&self) -> Vec<&EmbeddedTraceDetail> {
791        let mut traces = self.traces.iter().collect::<Vec<_>>();
792        traces.sort_by(|left, right| {
793            right
794                .summary
795                .completion_sequence
796                .cmp(&left.summary.completion_sequence)
797                .then_with(|| left.summary.trace_id.cmp(&right.summary.trace_id))
798        });
799        traces
800    }
801
802    fn cursor_for(&self, trace_id: &str) -> String {
803        format!("embedded-trace-cursor:{}:{trace_id}", self.trace_session)
804    }
805
806    fn cursor_start(
807        &self,
808        cursor: &str,
809        traces: &[&EmbeddedTraceDetail],
810    ) -> Result<usize, EmbeddedTraceApiError> {
811        let Some((prefix, trace_id)) = cursor.rsplit_once(':') else {
812            return Err(EmbeddedTraceApiError::new(
813                EmbeddedTraceApiErrorCode::InvalidCursor,
814            ));
815        };
816        let expected_prefix = format!("embedded-trace-cursor:{}", self.trace_session);
817        if prefix != expected_prefix {
818            return Err(EmbeddedTraceApiError::new(
819                EmbeddedTraceApiErrorCode::InvalidCursor,
820            ));
821        }
822        traces
823            .iter()
824            .position(|detail| detail.summary.trace_id == trace_id)
825            .map(|position| position + 1)
826            .ok_or_else(|| EmbeddedTraceApiError::new(EmbeddedTraceApiErrorCode::InvalidCursor))
827    }
828
829    fn emit(&mut self, event_type: &str, session_id: Option<&str>, data: Value) {
830        self.next_event += 1;
831        let mut event = json!({
832            "kind": "embedder_event",
833            "schema_version": EVENT_SCHEMA_VERSION,
834            "embedder_api_version": EMBEDDER_API_VERSION,
835            "event_id": format!("evt-{:08}", self.next_event),
836            "sequence": self.next_event,
837            "event_type": event_type,
838            "workspace_id": self.workspace_id,
839            "app_id": self.app_id,
840            "session_id": session_id,
841        });
842        event["data"] = data;
843        for subscriber in &mut self.subscribers {
844            subscriber(&event);
845        }
846        self.history.push(event);
847    }
848
849    fn subscribe(&mut self, mut callback: EventCallback) {
850        for event in &self.history {
851            callback(event);
852        }
853        self.subscribers.push(callback);
854    }
855
856    fn emit_error_event(&mut self, session_id: Option<&str>, error: &EmbedderError, data: Value) {
857        let mut payload = data;
858        payload["error"] = error.as_value();
859        self.emit("error", session_id, payload);
860    }
861
862    fn rejected_submit(&mut self, target_id: &str, error: EmbedderError) -> SubmitOutcome {
863        self.emit_error_event(None, &error, json!({ "target_id": target_id }));
864        SubmitOutcome {
865            session_id: None,
866            status: SubmitStatus::Rejected,
867            error: Some(error),
868        }
869    }
870
871    fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
872        let error = if self.stopped {
873            Some(runtime_stopped_error())
874        } else {
875            match self.compatible_targets.get(capability_id) {
876                None => Some(EmbedderError::new(
877                    EmbedderErrorCode::CapabilityNotCompatible,
878                    format!(
879                        "capability '{capability_id}' is not a compatible-mode capability in this bundle"
880                    ),
881                )),
882                Some(platforms) if !platforms.iter().any(|p| p == &self.platform) => {
883                    Some(EmbedderError::new(
884                        EmbedderErrorCode::PlatformNotSupported,
885                        format!(
886                            "capability '{capability_id}' permits platforms [{}] but this embedder runs on '{}'",
887                            platforms.join(", "),
888                            self.platform
889                        ),
890                    ))
891                }
892                Some(_) => None,
893            }
894        };
895        if let Some(error) = error {
896            self.emit_error_event(None, &error, json!({ "capability_id": capability_id }));
897            return CompatibleStartOutcome {
898                instance_id: None,
899                status: CompatibleLifecycleStatus::Error,
900                error: Some(error),
901            };
902        }
903
904        let instance_id = self.next_instance_id();
905        self.instances.insert(
906            instance_id.clone(),
907            CompatibleInstance {
908                capability_id: capability_id.to_string(),
909                state: InstanceState::Started,
910            },
911        );
912        self.emit(
913            "state_changed",
914            None,
915            json!({
916                "capability_id": capability_id,
917                "instance_id": instance_id,
918                "state": InstanceState::Started.as_str(),
919                "previous_state": null,
920                "input": input,
921            }),
922        );
923        CompatibleStartOutcome {
924            instance_id: Some(instance_id),
925            status: CompatibleLifecycleStatus::Started,
926            error: None,
927        }
928    }
929
930    fn transition_compatible(
931        &mut self,
932        capability_id: &str,
933        instance_id: Option<&str>,
934        target_state: InstanceState,
935    ) -> CompatibleLifecycleOutcome {
936        if self.stopped {
937            let error = runtime_stopped_error();
938            self.emit_error_event(None, &error, json!({ "capability_id": capability_id }));
939            return CompatibleLifecycleOutcome {
940                status: CompatibleLifecycleStatus::Error,
941                error: Some(error),
942            };
943        }
944
945        let selected: Vec<String> = match instance_id {
946            Some(requested) => match self.instances.get(requested) {
947                Some(instance) if instance.capability_id == capability_id => {
948                    if instance.state == InstanceState::Started {
949                        vec![requested.to_string()]
950                    } else {
951                        let error = EmbedderError::new(
952                            EmbedderErrorCode::InstanceNotRunning,
953                            format!(
954                                "instance '{requested}' of capability '{capability_id}' is not running"
955                            ),
956                        );
957                        self.emit_error_event(
958                            None,
959                            &error,
960                            json!({ "capability_id": capability_id, "instance_id": requested }),
961                        );
962                        return CompatibleLifecycleOutcome {
963                            status: CompatibleLifecycleStatus::Error,
964                            error: Some(error),
965                        };
966                    }
967                }
968                _ => {
969                    let error = EmbedderError::new(
970                        EmbedderErrorCode::InstanceNotFound,
971                        format!(
972                            "no instance '{requested}' exists for capability '{capability_id}'"
973                        ),
974                    );
975                    self.emit_error_event(
976                        None,
977                        &error,
978                        json!({ "capability_id": capability_id, "instance_id": requested }),
979                    );
980                    return CompatibleLifecycleOutcome {
981                        status: CompatibleLifecycleStatus::Error,
982                        error: Some(error),
983                    };
984                }
985            },
986            None => self
987                .instances
988                .iter()
989                .filter(|(_, instance)| {
990                    instance.capability_id == capability_id
991                        && instance.state == InstanceState::Started
992                })
993                .map(|(id, _)| id.clone())
994                .collect(),
995        };
996
997        if selected.is_empty() {
998            let error = EmbedderError::new(
999                EmbedderErrorCode::InstanceNotRunning,
1000                format!("capability '{capability_id}' has no running instances"),
1001            );
1002            self.emit_error_event(None, &error, json!({ "capability_id": capability_id }));
1003            return CompatibleLifecycleOutcome {
1004                status: CompatibleLifecycleStatus::Error,
1005                error: Some(error),
1006            };
1007        }
1008
1009        for id in selected {
1010            self.set_instance_state(&id, target_state);
1011        }
1012        CompatibleLifecycleOutcome {
1013            status: match target_state {
1014                InstanceState::Stopped => CompatibleLifecycleStatus::Stopped,
1015                _ => CompatibleLifecycleStatus::Killed,
1016            },
1017            error: None,
1018        }
1019    }
1020
1021    fn set_instance_state(&mut self, instance_id: &str, target_state: InstanceState) {
1022        let Some(instance) = self.instances.get_mut(instance_id) else {
1023            return;
1024        };
1025        let previous = instance.state;
1026        instance.state = target_state;
1027        let capability_id = instance.capability_id.clone();
1028        self.emit(
1029            "state_changed",
1030            None,
1031            json!({
1032                "capability_id": capability_id,
1033                "instance_id": instance_id,
1034                "state": target_state.as_str(),
1035                "previous_state": previous.as_str(),
1036            }),
1037        );
1038    }
1039
1040    fn shutdown(&mut self) -> ShutdownOutcome {
1041        if self.stopped {
1042            return ShutdownOutcome {
1043                killed_instances: 0,
1044            };
1045        }
1046        let running: Vec<String> = self
1047            .instances
1048            .iter()
1049            .filter(|(_, instance)| instance.state == InstanceState::Started)
1050            .map(|(id, _)| id.clone())
1051            .collect();
1052        let killed_instances = running.len();
1053        for id in running {
1054            self.set_instance_state(&id, InstanceState::Killed);
1055        }
1056        self.stopped = true;
1057        self.traces.clear();
1058        ShutdownOutcome { killed_instances }
1059    }
1060
1061    fn evidence(&self, runtime_implementation: &str, wasm_components: Value) -> Value {
1062        let mut evidence = json!({
1063            "kind": "embedder_release_evidence",
1064            "schema_version": EVENT_SCHEMA_VERSION,
1065            "package": {
1066                "name": env!("CARGO_PKG_NAME"),
1067                "version": env!("CARGO_PKG_VERSION"),
1068            },
1069            "embedder_api_version": EMBEDDER_API_VERSION,
1070            "companion_apis": {
1071                "embedded-trace-api": EMBEDDED_TRACE_API_VERSION,
1072            },
1073            "conformance_version": EMBEDDER_CONFORMANCE_VERSION,
1074            "runtime": {
1075                "implementation": runtime_implementation,
1076                "version": env!("CARGO_PKG_VERSION"),
1077                "linkage": "native-static",
1078            },
1079            "supported_bundle_schema_versions": SUPPORTED_BUNDLE_SCHEMA_VERSIONS,
1080            "bundle": {
1081                "app_id": self.app_id,
1082                "app_version": self.app_version,
1083            },
1084            "workspace_id": self.workspace_id,
1085            "platform": self.platform,
1086        });
1087        evidence["bundle"]["wasm_components"] = wasm_components;
1088        evidence
1089    }
1090}
1091
1092#[derive(Debug, Clone)]
1093struct WasmTarget {
1094    capability_version: String,
1095}
1096
1097#[derive(Debug, Clone)]
1098struct WorkflowTarget {
1099    workflow_version: String,
1100}
1101
1102/// Submittable targets and evidence derived from a loaded bundle manifest.
1103struct BundleTargets {
1104    wasm: BTreeMap<String, WasmTarget>,
1105    compatible: BTreeMap<String, Vec<String>>,
1106    workflows: BTreeMap<String, WorkflowTarget>,
1107    wasm_component_evidence: Vec<Value>,
1108}
1109
1110impl BundleTargets {
1111    fn from_manifest(manifest: &traverse_registry::ApplicationBundleManifest) -> Self {
1112        let mut wasm = BTreeMap::new();
1113        let mut compatible = BTreeMap::new();
1114        let mut wasm_component_evidence = Vec::new();
1115        for component in &manifest.components {
1116            match component.manifest.execution_mode {
1117                ComponentExecutionMode::Wasm => {
1118                    wasm.insert(
1119                        component.manifest.capability_id.clone(),
1120                        WasmTarget {
1121                            capability_version: component.manifest.capability_version.clone(),
1122                        },
1123                    );
1124                    wasm_component_evidence.push(json!({
1125                        "component_id": component.manifest.component_id,
1126                        "capability_id": component.manifest.capability_id,
1127                        "wasm_digest": component.verified_wasm_digest,
1128                    }));
1129                }
1130                ComponentExecutionMode::Compatible => {
1131                    compatible.insert(
1132                        component.manifest.capability_id.clone(),
1133                        component.manifest.platforms.clone(),
1134                    );
1135                }
1136            }
1137        }
1138        let workflows = manifest
1139            .workflows
1140            .iter()
1141            .map(|workflow| {
1142                (
1143                    workflow.workflow_id.clone(),
1144                    WorkflowTarget {
1145                        workflow_version: workflow.workflow_version.clone(),
1146                    },
1147                )
1148            })
1149            .collect();
1150        Self {
1151            wasm,
1152            compatible,
1153            workflows,
1154            wasm_component_evidence,
1155        }
1156    }
1157}
1158
1159/// Production embedder: loads an application-owned bundle and executes it
1160/// through the natively linked Traverse runtime.
1161pub struct BundleEmbedder {
1162    core: EmbedderCore,
1163    runtime: Runtime<ArtifactRouter>,
1164    wasm_targets: BTreeMap<String, WasmTarget>,
1165    workflow_targets: BTreeMap<String, WorkflowTarget>,
1166    wasm_component_evidence: Value,
1167    data_store: Option<HostDataStore>,
1168}
1169
1170impl BundleEmbedder {
1171    /// `runtime.init`: load, verify, and register the application bundle.
1172    ///
1173    /// # Errors
1174    ///
1175    /// Returns an [`EmbedderError`] with a stable code when the bundle path
1176    /// cannot be resolved, the bundle schema version is unsupported, the
1177    /// bundle fails validation or registration, or the WASM executor cannot
1178    /// initialize. Rejections are deterministic and never fall back to a
1179    /// sidecar (spec 068 NFR-001).
1180    #[allow(unexpected_cfgs)]
1181    pub fn init(config: EmbedderConfig) -> Result<Self, EmbedderError> {
1182        let manifest_path = absolute_bundle_path(&config.manifest_bundle_path)?;
1183        let manifest = match config.registry_cache.as_ref() {
1184            Some(cache) => {
1185                let resolver = OfflineRegistryCacheResolver { cache };
1186                load_application_bundle_manifest_with_resolver(&manifest_path, Some(&resolver))
1187            }
1188            None => load_application_bundle_manifest(&manifest_path),
1189        }
1190        .map_err(|failure| map_manifest_failure(&failure))?;
1191        ensure_supported_bundle_schema(&manifest.schema_version)?;
1192
1193        let mut capabilities = CapabilityRegistry::new();
1194        let events = EventRegistry::new();
1195        let mut workflows = WorkflowRegistry::new();
1196        let mut applications = ApplicationRegistry::new();
1197        applications
1198            .register_bundle(
1199                &mut capabilities,
1200                &events,
1201                &mut workflows,
1202                &ApplicationRegistrationRequest {
1203                    scope: RegistryScope::Private,
1204                    workspace_id: config.workspace_id.clone(),
1205                    manifest_path: manifest_path.clone(),
1206                    registered_at: format!("bundle:{}@{}", manifest.app_id, manifest.version),
1207                    validator_version: env!("CARGO_PKG_VERSION").to_string(),
1208                },
1209            )
1210            .map_err(|failure| registration_failure_error(&failure))?;
1211
1212        #[cfg(coverage)]
1213        let executor = ArtifactRouter::new()
1214            .expect("the bounded Wasmtime configuration initializes under coverage");
1215        #[cfg(not(coverage))]
1216        let executor = ArtifactRouter::new().map_err(|failure| {
1217            EmbedderError::new(EmbedderErrorCode::ExecutorUnavailable, failure.message)
1218        })?;
1219
1220        let security = match config.security {
1221            SecurityPosture::Production => {
1222                traverse_runtime::security::RuntimeSecurityConfig::production()
1223            }
1224            SecurityPosture::Development => {
1225                traverse_runtime::security::RuntimeSecurityConfig::development()
1226            }
1227        };
1228        let runtime = Runtime::new(capabilities, executor)
1229            .with_workflow_registry(workflows)
1230            .with_security_config(security);
1231
1232        let targets = BundleTargets::from_manifest(&manifest);
1233        Ok(Self {
1234            core: EmbedderCore::new(
1235                config.workspace_id,
1236                manifest.app_id,
1237                manifest.version,
1238                config.platform,
1239                targets.compatible,
1240            ),
1241            runtime,
1242            wasm_targets: targets.wasm,
1243            workflow_targets: targets.workflows,
1244            wasm_component_evidence: Value::Array(targets.wasm_component_evidence),
1245            data_store: None,
1246        })
1247    }
1248
1249    /// Explicitly injects a host-owned `DataStore`.
1250    ///
1251    /// This additive host surface is deliberately separate from capability
1252    /// execution. It accepts neither a root path nor a capability identity,
1253    /// so the runtime cannot derive storage locations or grant capability
1254    /// code direct access.
1255    pub fn inject_data_store(&mut self, store: HostDataStore) {
1256        self.data_store = Some(store);
1257    }
1258
1259    /// Reads one host-owned state record from the injected `DataStore`.
1260    ///
1261    /// Returns `None` when the injected store has no record for `key`.
1262    ///
1263    /// # Errors
1264    ///
1265    /// Returns a safe typed failure when no store was injected or its read
1266    /// operation fails.
1267    pub fn data_store_read(
1268        &mut self,
1269        key: &str,
1270    ) -> Result<Option<StateRecord>, EmbeddedDataStoreError> {
1271        let result = match self.data_store.as_ref() {
1272            Some(store) => store
1273                .adapter
1274                .read(key)
1275                .map_err(|error| EmbeddedDataStoreError::from_error("read", &error)),
1276            None => Err(EmbeddedDataStoreError::not_configured("read")),
1277        };
1278        self.record_data_store_operation("read", result.is_ok());
1279        result
1280    }
1281
1282    /// Writes one host-owned state record to the injected `DataStore`.
1283    ///
1284    /// # Errors
1285    ///
1286    /// Returns a safe typed failure when no store was injected or its write
1287    /// operation fails.
1288    pub fn data_store_write(&mut self, record: StateRecord) -> Result<(), EmbeddedDataStoreError> {
1289        let result = match self.data_store.as_mut() {
1290            Some(store) => store
1291                .adapter
1292                .write(record)
1293                .map_err(|error| EmbeddedDataStoreError::from_error("write", &error)),
1294            None => Err(EmbeddedDataStoreError::not_configured("write")),
1295        };
1296        self.record_data_store_operation("write", result.is_ok());
1297        result
1298    }
1299
1300    /// Deletes one host-owned state record from the injected `DataStore`.
1301    ///
1302    /// # Errors
1303    ///
1304    /// Returns a safe typed failure when no store was injected or its delete
1305    /// operation fails.
1306    pub fn data_store_delete(&mut self, key: &str) -> Result<(), EmbeddedDataStoreError> {
1307        let result = match self.data_store.as_mut() {
1308            Some(store) => store
1309                .adapter
1310                .delete(key)
1311                .map_err(|error| EmbeddedDataStoreError::from_error("delete", &error)),
1312            None => Err(EmbeddedDataStoreError::not_configured("delete")),
1313        };
1314        self.record_data_store_operation("delete", result.is_ok());
1315        result
1316    }
1317
1318    fn record_data_store_operation(&mut self, operation: &'static str, succeeded: bool) {
1319        let classification = self
1320            .data_store
1321            .as_ref()
1322            .map(|store| match store.classification {
1323                LocalDataClassification::Public => "public",
1324                LocalDataClassification::Private => "private",
1325            });
1326        self.core.emit(
1327            "data_store_operation",
1328            None,
1329            json!({
1330                "operation": operation,
1331                "outcome": if succeeded { "completed" } else { "failed" },
1332                "classification": classification,
1333            }),
1334        );
1335    }
1336
1337    fn submit_workflow(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
1338        let workflow_version = self.workflow_targets[target_id].workflow_version.clone();
1339        let session_id = self.core.next_session_id();
1340        let request_id = self.core.next_request_id();
1341        let outcome = self.runtime.execute_workflow(WorkflowExecutionRequest {
1342            kind: "workflow_execution_request".to_string(),
1343            schema_version: "1.0.0".to_string(),
1344            request_id: request_id.clone(),
1345            workflow_id: target_id.to_string(),
1346            workflow_version: workflow_version.clone(),
1347            scope: WorkflowLookupScope::PreferPrivate,
1348            input: input.clone(),
1349            governing_spec: "007-workflow-registry-traversal".to_string(),
1350        });
1351
1352        self.core
1353            .record_trace(workflow_trace_input(&outcome, target_id, &workflow_version));
1354
1355        for step in &outcome.evidence.visited_nodes {
1356            self.core.emit(
1357                "capability_invoked",
1358                Some(&session_id),
1359                json!({
1360                    "request_id": request_id,
1361                    "workflow_id": target_id,
1362                    "workflow_version": workflow_version,
1363                    "step_index": step.step_index,
1364                    "node_id": step.node_id,
1365                    "capability_id": step.capability_id,
1366                    "capability_version": step.capability_version,
1367                    "status": workflow_step_status_str(step.status),
1368                }),
1369            );
1370        }
1371        match outcome.result.status {
1372            WorkflowTraversalStatus::Completed => {
1373                self.core.emit(
1374                    "capability_result",
1375                    Some(&session_id),
1376                    json!({
1377                        "request_id": request_id,
1378                        "workflow_id": target_id,
1379                        "workflow_version": workflow_version,
1380                        "status": "completed",
1381                        "output": outcome.result.output,
1382                    }),
1383                );
1384            }
1385            WorkflowTraversalStatus::Error => {
1386                self.core.emit(
1387                    "error",
1388                    Some(&session_id),
1389                    json!({
1390                        "request_id": request_id,
1391                        "workflow_id": target_id,
1392                        "workflow_version": workflow_version,
1393                        "status": "error",
1394                        "error": outcome.result.error.as_ref().map(runtime_error_value),
1395                    }),
1396                );
1397            }
1398        }
1399        SubmitOutcome {
1400            session_id: Some(session_id),
1401            status: SubmitStatus::Accepted,
1402            error: None,
1403        }
1404    }
1405
1406    fn submit_capability(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
1407        let capability_version = self.wasm_targets[target_id].capability_version.clone();
1408        let session_id = self.core.next_session_id();
1409        let request_id = self.core.next_request_id();
1410        let outcome = self.runtime.execute(RuntimeRequest {
1411            kind: "runtime_request".to_string(),
1412            schema_version: "1.0.0".to_string(),
1413            request_id,
1414            intent: RuntimeIntent {
1415                capability_id: Some(target_id.to_string()),
1416                capability_version: Some(capability_version.clone()),
1417                version_range: None,
1418                intent_key: None,
1419            },
1420            input: input.clone(),
1421            lookup: RuntimeLookup {
1422                scope: RuntimeLookupScope::PreferPrivate,
1423                allow_ambiguity: false,
1424            },
1425            context: RuntimeContext {
1426                requested_target: PlacementTarget::Local,
1427                correlation_id: Some(session_id.clone()),
1428                caller: None,
1429                traceparent: None,
1430                tracestate: None,
1431                metadata: None,
1432                identity: None,
1433            },
1434            governing_spec: "006-runtime-request-execution".to_string(),
1435        });
1436
1437        self.core
1438            .record_trace(runtime_trace_input(&outcome, target_id));
1439
1440        let execution_id = outcome.result.execution_id.clone();
1441        self.core.emit(
1442            "capability_invoked",
1443            Some(&session_id),
1444            json!({
1445                "execution_id": execution_id,
1446                "capability_id": target_id,
1447                "capability_version": capability_version,
1448            }),
1449        );
1450        match outcome.result.status {
1451            RuntimeResultStatus::Completed => {
1452                self.core.emit(
1453                    "capability_result",
1454                    Some(&session_id),
1455                    json!({
1456                        "execution_id": execution_id,
1457                        "capability_id": target_id,
1458                        "status": "completed",
1459                        "output": outcome.result.output,
1460                    }),
1461                );
1462            }
1463            RuntimeResultStatus::Error => {
1464                self.core.emit(
1465                    "error",
1466                    Some(&session_id),
1467                    json!({
1468                        "execution_id": execution_id,
1469                        "capability_id": target_id,
1470                        "status": "error",
1471                        "error": outcome.result.error.as_ref().map(runtime_error_value),
1472                    }),
1473                );
1474            }
1475        }
1476        SubmitOutcome {
1477            session_id: Some(session_id),
1478            status: SubmitStatus::Accepted,
1479            error: None,
1480        }
1481    }
1482}
1483
1484impl EmbeddedTraceApi for BundleEmbedder {
1485    fn embedded_trace_api_version(&self) -> &'static str {
1486        EMBEDDED_TRACE_API_VERSION
1487    }
1488
1489    fn trace_list(
1490        &self,
1491        requested_version: &str,
1492        page_size: usize,
1493        cursor: Option<&str>,
1494    ) -> Result<EmbeddedTracePage, EmbeddedTraceApiError> {
1495        self.core.trace_list(requested_version, page_size, cursor)
1496    }
1497
1498    fn trace_get(
1499        &self,
1500        requested_version: &str,
1501        trace_id: &str,
1502    ) -> Result<EmbeddedTraceDetail, EmbeddedTraceApiError> {
1503        self.core.trace_get(requested_version, trace_id)
1504    }
1505}
1506
1507impl TraverseEmbedderApi for BundleEmbedder {
1508    fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
1509        if self.core.stopped {
1510            let error = runtime_stopped_error();
1511            return self.core.rejected_submit(target_id, error);
1512        }
1513        if self.workflow_targets.contains_key(target_id) {
1514            return self.submit_workflow(target_id, input);
1515        }
1516        if self.wasm_targets.contains_key(target_id) {
1517            return self.submit_capability(target_id, input);
1518        }
1519        if self.core.compatible_targets.contains_key(target_id) {
1520            let error = EmbedderError::new(
1521                EmbedderErrorCode::CompatibleLifecycleRequired,
1522                format!(
1523                    "capability '{target_id}' is a compatible-mode capability; use compatible.start/stop/kill"
1524                ),
1525            );
1526            return self.core.rejected_submit(target_id, error);
1527        }
1528        let error = EmbedderError::new(
1529            EmbedderErrorCode::TargetNotFound,
1530            format!("'{target_id}' is neither a bundled workflow nor a bundled capability"),
1531        );
1532        self.core.rejected_submit(target_id, error)
1533    }
1534
1535    fn subscribe(&mut self, callback: EventCallback) {
1536        self.core.subscribe(callback);
1537    }
1538
1539    fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
1540        self.core.start_compatible(capability_id, input)
1541    }
1542
1543    fn stop_compatible(
1544        &mut self,
1545        capability_id: &str,
1546        instance_id: Option<&str>,
1547    ) -> CompatibleLifecycleOutcome {
1548        self.core
1549            .transition_compatible(capability_id, instance_id, InstanceState::Stopped)
1550    }
1551
1552    fn kill_compatible(
1553        &mut self,
1554        capability_id: &str,
1555        instance_id: Option<&str>,
1556    ) -> CompatibleLifecycleOutcome {
1557        self.core
1558            .transition_compatible(capability_id, instance_id, InstanceState::Killed)
1559    }
1560
1561    fn shutdown(&mut self) -> ShutdownOutcome {
1562        self.core.shutdown()
1563    }
1564
1565    fn release_evidence(&self) -> Value {
1566        self.core
1567            .evidence("traverse-runtime", self.wasm_component_evidence.clone())
1568    }
1569}
1570
1571fn runtime_trace_input(
1572    outcome: &RuntimeExecutionOutcome,
1573    target_id: &str,
1574) -> EmbeddedTraceRecordInput {
1575    let phases = outcome
1576        .trace
1577        .state_progression
1578        .transitions
1579        .iter()
1580        .map(|transition| EmbeddedTracePhase {
1581            code: runtime_state_code(transition.to_state).to_string(),
1582        })
1583        .collect();
1584    let selected_target =
1585        outcome
1586            .trace
1587            .selection
1588            .selected_capability_id
1589            .as_ref()
1590            .map(|target_id| EmbeddedTraceSelectedTarget {
1591                target_id: target_id.clone(),
1592                target_version: outcome.trace.selection.selected_capability_version.clone(),
1593            });
1594    let placement = outcome
1595        .trace
1596        .execution
1597        .placement
1598        .selected_target
1599        .map(|target| EmbeddedTracePlacement {
1600            target: placement_target_code(target).to_string(),
1601        });
1602    let failure_code = outcome
1603        .result
1604        .error
1605        .as_ref()
1606        .map(|error| runtime_error_code_str(error.code).to_string())
1607        .or_else(|| {
1608            outcome
1609                .trace
1610                .execution
1611                .failure_reason
1612                .map(|reason| execution_failure_code(reason).to_string())
1613        });
1614    EmbeddedTraceRecordInput {
1615        execution_id: outcome.result.execution_id.clone(),
1616        target_id: target_id.to_string(),
1617        outcome: match outcome.result.status {
1618            RuntimeResultStatus::Completed => EmbeddedTraceOutcome::Completed,
1619            RuntimeResultStatus::Error => EmbeddedTraceOutcome::Error,
1620        },
1621        phases,
1622        selected_target,
1623        placement,
1624        failure_code,
1625        state_machine_valid: Some(outcome.trace.state_machine_validation.violations.is_empty()),
1626    }
1627}
1628
1629fn workflow_trace_input(
1630    outcome: &WorkflowExecutionOutcome,
1631    target_id: &str,
1632    workflow_version: &str,
1633) -> EmbeddedTraceRecordInput {
1634    let phases = outcome
1635        .evidence
1636        .visited_nodes
1637        .iter()
1638        .map(|step| EmbeddedTracePhase {
1639            code: format!("workflow_{}", workflow_step_status_str(step.status)),
1640        })
1641        .collect();
1642    EmbeddedTraceRecordInput {
1643        execution_id: format!("workflow-{}", outcome.result.request_id),
1644        target_id: target_id.to_string(),
1645        outcome: match outcome.result.status {
1646            WorkflowTraversalStatus::Completed => EmbeddedTraceOutcome::Completed,
1647            WorkflowTraversalStatus::Error => EmbeddedTraceOutcome::Error,
1648        },
1649        phases,
1650        selected_target: Some(EmbeddedTraceSelectedTarget {
1651            target_id: target_id.to_string(),
1652            target_version: Some(workflow_version.to_string()),
1653        }),
1654        placement: None,
1655        failure_code: outcome
1656            .result
1657            .error
1658            .as_ref()
1659            .map(|error| runtime_error_code_str(error.code).to_string()),
1660        state_machine_valid: None,
1661    }
1662}
1663
1664fn runtime_state_code(state: traverse_runtime::RuntimeState) -> &'static str {
1665    match state {
1666        traverse_runtime::RuntimeState::Idle => "idle",
1667        traverse_runtime::RuntimeState::LoadingRegistry => "loading_registry",
1668        traverse_runtime::RuntimeState::Ready => "ready",
1669        traverse_runtime::RuntimeState::Discovering => "discovering",
1670        traverse_runtime::RuntimeState::EvaluatingConstraints => "evaluating_constraints",
1671        traverse_runtime::RuntimeState::Selecting => "selecting",
1672        traverse_runtime::RuntimeState::Executing => "executing",
1673        traverse_runtime::RuntimeState::EmittingEvents => "emitting_events",
1674        traverse_runtime::RuntimeState::Completed => "completed",
1675        traverse_runtime::RuntimeState::Error => "error",
1676    }
1677}
1678
1679fn placement_target_code(target: PlacementTarget) -> &'static str {
1680    match target {
1681        PlacementTarget::Local => "local",
1682        PlacementTarget::Browser => "browser",
1683        PlacementTarget::Edge => "edge",
1684        PlacementTarget::Cloud => "cloud",
1685        PlacementTarget::Worker => "worker",
1686        PlacementTarget::Device => "device",
1687    }
1688}
1689
1690fn execution_failure_code(reason: ExecutionFailureReason) -> &'static str {
1691    match reason {
1692        ExecutionFailureReason::ContractInputInvalid => "contract_input_invalid",
1693        ExecutionFailureReason::ArtifactMissing => "artifact_missing",
1694        ExecutionFailureReason::ArtifactNotRunnable => "artifact_not_runnable",
1695        ExecutionFailureReason::PlacementUnsupported => "placement_unsupported",
1696        ExecutionFailureReason::ExecutionFailed => "execution_failed",
1697        ExecutionFailureReason::ContractOutputInvalid => "contract_output_invalid",
1698    }
1699}
1700
1701fn runtime_stopped_error() -> EmbedderError {
1702    EmbedderError::new(
1703        EmbedderErrorCode::RuntimeStopped,
1704        "the embedded runtime was shut down and accepts no further operations",
1705    )
1706}
1707
1708fn map_manifest_failure(failure: &ApplicationManifestFailure) -> EmbedderError {
1709    let message = manifest_failure_messages(
1710        &failure
1711            .errors
1712            .iter()
1713            .map(|error| error.message.clone())
1714            .collect::<Vec<_>>(),
1715    );
1716    let code_hint = failure.errors.first().map(|error| error.code);
1717    let message = match code_hint {
1718        Some(ApplicationManifestErrorCode::RegistryReferenceRequiresResolution) => {
1719            format!("application bundle failed to load (registry_cache_entry_missing): {message}")
1720        }
1721        _ => format!("application bundle failed to load: {message}"),
1722    };
1723    EmbedderError::new(EmbedderErrorCode::BundleLoadFailed, message)
1724}
1725
1726struct OfflineRegistryCacheResolver<'a> {
1727    cache: &'a HostRegistryCache,
1728}
1729
1730impl RegistryComponentResolver for OfflineRegistryCacheResolver<'_> {
1731    fn resolve(
1732        &self,
1733        reference: &RegistryReference,
1734    ) -> Result<ResolvedRegistryComponent, ApplicationManifestFailure> {
1735        crate::registry_cache::resolve_component(self.cache, reference).map_err(|failure| {
1736            ApplicationManifestFailure {
1737                errors: vec![ApplicationManifestError {
1738                    code: ApplicationManifestErrorCode::RegistryReferenceRequiresResolution,
1739                    path: "$.registry_ref".to_string(),
1740                    message: format!("{}: {}", failure.code.as_str(), failure.message),
1741                }],
1742            }
1743        })
1744    }
1745}
1746
1747fn absolute_bundle_path(path: &Path) -> Result<PathBuf, EmbedderError> {
1748    std::path::absolute(path).map_err(|error| {
1749        EmbedderError::new(
1750            EmbedderErrorCode::BundlePathInvalid,
1751            format!(
1752                "bundle path '{}' could not be resolved: {error}",
1753                path.display()
1754            ),
1755        )
1756    })
1757}
1758
1759fn ensure_supported_bundle_schema(schema_version: &str) -> Result<(), EmbedderError> {
1760    if SUPPORTED_BUNDLE_SCHEMA_VERSIONS.contains(&schema_version) {
1761        return Ok(());
1762    }
1763    Err(EmbedderError::new(
1764        EmbedderErrorCode::UnsupportedBundleSchema,
1765        format!(
1766            "bundle declares schema_version '{schema_version}' but this package supports [{}]; \
1767             no sidecar fallback is attempted",
1768            SUPPORTED_BUNDLE_SCHEMA_VERSIONS.join(", ")
1769        ),
1770    ))
1771}
1772
1773fn registration_failure_error(failure: &ApplicationRegistrationFailure) -> EmbedderError {
1774    EmbedderError::new(
1775        EmbedderErrorCode::BundleLoadFailed,
1776        format!(
1777            "application bundle failed to register: {}",
1778            manifest_failure_messages(
1779                &failure
1780                    .errors
1781                    .iter()
1782                    .map(|error| error.message.clone())
1783                    .collect::<Vec<_>>()
1784            )
1785        ),
1786    )
1787}
1788
1789fn manifest_failure_messages(messages: &[String]) -> String {
1790    messages.join("; ")
1791}
1792
1793fn runtime_error_value(error: &RuntimeError) -> Value {
1794    json!({
1795        "code": runtime_error_code_str(error.code),
1796        "message": error.message,
1797        "details": error.details,
1798    })
1799}
1800
1801fn runtime_error_code_str(code: RuntimeErrorCode) -> &'static str {
1802    match code {
1803        RuntimeErrorCode::RequestInvalid => "request_invalid",
1804        RuntimeErrorCode::CapabilityNotFound => "capability_not_found",
1805        RuntimeErrorCode::CapabilityAmbiguous => "capability_ambiguous",
1806        RuntimeErrorCode::CapabilityNotRunnable => "capability_not_runnable",
1807        RuntimeErrorCode::PlacementUnsupported => "placement_unsupported",
1808        RuntimeErrorCode::ArtifactMissing => "artifact_missing",
1809        RuntimeErrorCode::ExecutionFailed => "execution_failed",
1810        RuntimeErrorCode::OutputValidationFailed => "output_validation_failed",
1811        RuntimeErrorCode::ContractViolation => "contract_violation",
1812    }
1813}
1814
1815fn workflow_step_status_str(status: WorkflowTraversalStepStatus) -> &'static str {
1816    match status {
1817        WorkflowTraversalStepStatus::Entered => "entered",
1818        WorkflowTraversalStepStatus::Completed => "completed",
1819        WorkflowTraversalStepStatus::Failed => "failed",
1820    }
1821}
1822
1823#[cfg(test)]
1824mod tests {
1825    #![allow(clippy::expect_used, clippy::unwrap_used)]
1826
1827    use super::*;
1828
1829    #[test]
1830    fn error_codes_render_stable_snake_case_strings() {
1831        let codes = [
1832            (EmbedderErrorCode::BundleLoadFailed, "bundle_load_failed"),
1833            (
1834                EmbedderErrorCode::UnsupportedBundleSchema,
1835                "unsupported_bundle_schema",
1836            ),
1837            (EmbedderErrorCode::BundlePathInvalid, "bundle_path_invalid"),
1838            (
1839                EmbedderErrorCode::ExecutorUnavailable,
1840                "executor_unavailable",
1841            ),
1842            (EmbedderErrorCode::RuntimeStopped, "runtime_stopped"),
1843            (EmbedderErrorCode::TargetNotFound, "target_not_found"),
1844            (
1845                EmbedderErrorCode::CompatibleLifecycleRequired,
1846                "compatible_lifecycle_required",
1847            ),
1848            (
1849                EmbedderErrorCode::CapabilityNotCompatible,
1850                "capability_not_compatible",
1851            ),
1852            (
1853                EmbedderErrorCode::PlatformNotSupported,
1854                "platform_not_supported",
1855            ),
1856            (EmbedderErrorCode::InstanceNotFound, "instance_not_found"),
1857            (
1858                EmbedderErrorCode::InstanceNotRunning,
1859                "instance_not_running",
1860            ),
1861        ];
1862        for (code, expected) in codes {
1863            assert_eq!(code.as_str(), expected);
1864        }
1865    }
1866
1867    #[test]
1868    fn datastore_errors_map_to_safe_stable_codes() {
1869        let codes = [
1870            (
1871                DataStoreErrorCode::IntegrityCheckFailed,
1872                "integrity_check_failed",
1873            ),
1874            (DataStoreErrorCode::StoreLocked, "store_locked"),
1875            (
1876                DataStoreErrorCode::DurabilityCommitFailed,
1877                "durability_commit_failed",
1878            ),
1879            (DataStoreErrorCode::IoFailure, "storage_io_failed"),
1880            (DataStoreErrorCode::InvalidKey, "invalid_key"),
1881            (
1882                DataStoreErrorCode::SerializationFailure,
1883                "serialization_failed",
1884            ),
1885            (
1886                DataStoreErrorCode::SchemaValidationError,
1887                "schema_validation_failed",
1888            ),
1889            (
1890                DataStoreErrorCode::NoStateSchemaDeclared,
1891                "state_schema_unavailable",
1892            ),
1893            (
1894                DataStoreErrorCode::LamportClockOverflow,
1895                "lamport_clock_overflow",
1896            ),
1897            (DataStoreErrorCode::SyncFailure, "sync_failed"),
1898            (
1899                DataStoreErrorCode::KeyProviderRequired,
1900                "key_provider_required",
1901            ),
1902            (DataStoreErrorCode::KeyNotFound, "key_not_found"),
1903            (DataStoreErrorCode::KeyExpired, "key_expired"),
1904            (
1905                DataStoreErrorCode::KeyProviderFailure,
1906                "key_provider_failed",
1907            ),
1908            (DataStoreErrorCode::CryptoFailure, "crypto_failed"),
1909            (
1910                DataStoreErrorCode::ClassificationChangeNotAllowed,
1911                "classification_change_not_allowed",
1912            ),
1913            (DataStoreErrorCode::RemoteConflict, "remote_conflict"),
1914            (DataStoreErrorCode::RemoteUnavailable, "remote_unavailable"),
1915            (DataStoreErrorCode::RemoteTimeout, "remote_timeout"),
1916            (
1917                DataStoreErrorCode::RemoteOutcomeUnknown,
1918                "remote_outcome_unknown",
1919            ),
1920            (
1921                DataStoreErrorCode::RemoteUnauthorized,
1922                "remote_unauthorized",
1923            ),
1924            (DataStoreErrorCode::RemoteScopeDenied, "remote_scope_denied"),
1925            (
1926                DataStoreErrorCode::RemoteIntegrityFailed,
1927                "remote_integrity_failed",
1928            ),
1929            (
1930                DataStoreErrorCode::RemoteBackendFailed,
1931                "remote_backend_failed",
1932            ),
1933        ];
1934        for (code, expected) in codes {
1935            let error = EmbeddedDataStoreError::from_error(
1936                "read",
1937                &DataStoreError {
1938                    code,
1939                    message: "host details must not cross the boundary".to_string(),
1940                    details: json!({ "path": "/host/private" }),
1941                },
1942            );
1943            assert_eq!(error.code, expected);
1944            assert_eq!(error.operation, "read");
1945        }
1946    }
1947
1948    #[test]
1949    fn runtime_error_codes_render_stable_snake_case_strings() {
1950        let codes = [
1951            (RuntimeErrorCode::RequestInvalid, "request_invalid"),
1952            (RuntimeErrorCode::CapabilityNotFound, "capability_not_found"),
1953            (
1954                RuntimeErrorCode::CapabilityAmbiguous,
1955                "capability_ambiguous",
1956            ),
1957            (
1958                RuntimeErrorCode::CapabilityNotRunnable,
1959                "capability_not_runnable",
1960            ),
1961            (
1962                RuntimeErrorCode::PlacementUnsupported,
1963                "placement_unsupported",
1964            ),
1965            (RuntimeErrorCode::ArtifactMissing, "artifact_missing"),
1966            (RuntimeErrorCode::ExecutionFailed, "execution_failed"),
1967            (
1968                RuntimeErrorCode::OutputValidationFailed,
1969                "output_validation_failed",
1970            ),
1971            (RuntimeErrorCode::ContractViolation, "contract_violation"),
1972        ];
1973        for (code, expected) in codes {
1974            assert_eq!(runtime_error_code_str(code), expected);
1975        }
1976    }
1977
1978    #[test]
1979    fn workflow_step_statuses_render_stable_strings() {
1980        assert_eq!(
1981            workflow_step_status_str(WorkflowTraversalStepStatus::Entered),
1982            "entered"
1983        );
1984        assert_eq!(
1985            workflow_step_status_str(WorkflowTraversalStepStatus::Completed),
1986            "completed"
1987        );
1988        assert_eq!(
1989            workflow_step_status_str(WorkflowTraversalStepStatus::Failed),
1990            "failed"
1991        );
1992    }
1993
1994    #[test]
1995    fn instance_states_render_stable_strings() {
1996        assert_eq!(InstanceState::Started.as_str(), "started");
1997        assert_eq!(InstanceState::Stopped.as_str(), "stopped");
1998        assert_eq!(InstanceState::Killed.as_str(), "killed");
1999    }
2000
2001    #[test]
2002    fn runtime_errors_map_to_structured_values() {
2003        let value = runtime_error_value(&RuntimeError {
2004            code: RuntimeErrorCode::ExecutionFailed,
2005            message: "capability failed".to_string(),
2006            details: json!({ "path": "$" }),
2007        });
2008        assert_eq!(
2009            value,
2010            json!({
2011                "code": "execution_failed",
2012                "message": "capability failed",
2013                "details": { "path": "$" },
2014            })
2015        );
2016    }
2017
2018    #[test]
2019    fn unsupported_bundle_schema_is_rejected_deterministically() -> Result<(), String> {
2020        let error = ensure_supported_bundle_schema("9.9.9")
2021            .err()
2022            .ok_or("schema 9.9.9 should be rejected")?;
2023        assert_eq!(error.code, EmbedderErrorCode::UnsupportedBundleSchema);
2024        assert!(error.message.contains("9.9.9"));
2025        assert!(error.message.contains("1.0.0"));
2026        ensure_supported_bundle_schema("1.0.0").map_err(|error| error.message)
2027    }
2028
2029    #[test]
2030    fn empty_bundle_path_is_rejected() -> Result<(), String> {
2031        let error = absolute_bundle_path(Path::new(""))
2032            .err()
2033            .ok_or("empty path should be rejected")?;
2034        assert_eq!(error.code, EmbedderErrorCode::BundlePathInvalid);
2035        Ok(())
2036    }
2037
2038    #[test]
2039    fn set_instance_state_ignores_unknown_instances() {
2040        let mut core = EmbedderCore::new(
2041            "local-default".to_string(),
2042            "app".to_string(),
2043            "1.0.0".to_string(),
2044            "linux".to_string(),
2045            BTreeMap::new(),
2046        );
2047        core.set_instance_state("inst-missing", InstanceState::Killed);
2048        assert!(core.history.is_empty());
2049    }
2050
2051    #[test]
2052    fn embedded_trace_api_pages_safe_test_double_records() -> Result<(), String> {
2053        let secret_input = "input-secret-never-public";
2054        let secret_output = "output-secret-never-public";
2055        let secret_error = "error-secret-never-public";
2056        let mut embedder = EmbedderTestDouble::new("workspace", "app", "1.0.0", "web")
2057            .with_target_output("demo.success", json!({ "secret": secret_output }))
2058            .with_target_error("demo.failure", "execution_failed", secret_error);
2059        assert_eq!(
2060            embedder.embedded_trace_api_version(),
2061            EMBEDDED_TRACE_API_VERSION
2062        );
2063
2064        let accepted = embedder.submit("demo.success", &json!({ "secret": secret_input }));
2065        assert_eq!(accepted.status, SubmitStatus::Accepted);
2066        let accepted = embedder.submit("demo.failure", &json!({ "secret": secret_input }));
2067        assert_eq!(accepted.status, SubmitStatus::Accepted);
2068
2069        let first_page = embedder
2070            .trace_list(EMBEDDED_TRACE_API_VERSION, 1, None)
2071            .map_err(|error| error.message.to_string())?;
2072        assert_eq!(first_page.retention_limit, EMBEDDED_TRACE_RETENTION_LIMIT);
2073        assert_eq!(first_page.summaries.len(), 1);
2074        assert_eq!(first_page.summaries[0].target_id, "demo.failure");
2075        assert_eq!(first_page.summaries[0].outcome, EmbeddedTraceOutcome::Error);
2076        let cursor = first_page
2077            .next_cursor
2078            .ok_or("the first page should have a continuation cursor")?;
2079        let failure = embedder
2080            .trace_get(
2081                EMBEDDED_TRACE_API_VERSION,
2082                &first_page.summaries[0].trace_id,
2083            )
2084            .map_err(|error| error.message.to_string())?;
2085        assert_eq!(failure.failure_code.as_deref(), Some("execution_failed"));
2086        assert_eq!(failure.phases[0].code, "error");
2087        let safe_debug = format!("{failure:?}");
2088        assert!(!safe_debug.contains(secret_input));
2089        assert!(!safe_debug.contains(secret_output));
2090        assert!(!safe_debug.contains(secret_error));
2091
2092        let second_page = embedder
2093            .trace_list(EMBEDDED_TRACE_API_VERSION, 1, Some(&cursor))
2094            .map_err(|error| error.message.to_string())?;
2095        assert_eq!(second_page.summaries.len(), 1);
2096        assert_eq!(second_page.summaries[0].target_id, "demo.success");
2097        assert!(second_page.next_cursor.is_none());
2098        Ok(())
2099    }
2100
2101    #[test]
2102    fn embedded_trace_api_rejects_stale_versions_cursors_and_stopped_hosts() -> Result<(), String> {
2103        let mut embedder = EmbedderTestDouble::new("workspace", "app", "1.0.0", "web")
2104            .with_target_output("demo.success", json!({ "value": "safe" }));
2105        let _ = embedder.submit("demo.success", &json!({}));
2106        let _ = embedder.submit("demo.success", &json!({}));
2107        let cursor = embedder
2108            .trace_list(EMBEDDED_TRACE_API_VERSION, 1, None)
2109            .map_err(|error| error.message.to_string())?
2110            .next_cursor
2111            .ok_or("two retained traces should produce a cursor")?;
2112
2113        let version_error = embedder
2114            .trace_list("2.0.0", 10, None)
2115            .err()
2116            .ok_or("an incompatible version should fail")?;
2117        assert_eq!(
2118            version_error.code,
2119            EmbeddedTraceApiErrorCode::IncompatibleVersion
2120        );
2121        let cursor_error = embedder
2122            .trace_list(EMBEDDED_TRACE_API_VERSION, 10, Some("not-a-cursor"))
2123            .err()
2124            .ok_or("a malformed cursor should fail")?;
2125        assert_eq!(cursor_error.code, EmbeddedTraceApiErrorCode::InvalidCursor);
2126
2127        let mut other_session = EmbedderTestDouble::new("workspace", "app", "1.0.0", "web")
2128            .with_target_output("demo.success", json!({ "value": "safe" }));
2129        let _ = other_session.submit("demo.success", &json!({}));
2130        let foreign_cursor_error = other_session
2131            .trace_list(EMBEDDED_TRACE_API_VERSION, 10, Some(&cursor))
2132            .err()
2133            .ok_or("a cursor from another session should fail")?;
2134        assert_eq!(
2135            foreign_cursor_error.code,
2136            EmbeddedTraceApiErrorCode::InvalidCursor
2137        );
2138
2139        let _ = embedder.shutdown();
2140        let stopped_error = embedder
2141            .trace_list(EMBEDDED_TRACE_API_VERSION, 10, None)
2142            .err()
2143            .ok_or("a stopped host should be unavailable")?;
2144        assert_eq!(
2145            stopped_error.code,
2146            EmbeddedTraceApiErrorCode::TraceApiUnavailable
2147        );
2148        Ok(())
2149    }
2150
2151    #[test]
2152    fn embedded_trace_api_evicts_oldest_records_deterministically() -> Result<(), String> {
2153        let mut embedder = EmbedderTestDouble::new("workspace", "app", "1.0.0", "web")
2154            .with_target_output("demo.success", json!({ "value": "safe" }));
2155        let mut first_trace_id = None;
2156        for index in 0..=EMBEDDED_TRACE_RETENTION_LIMIT {
2157            let _ = embedder.submit("demo.success", &json!({ "index": index }));
2158            if index == 0 {
2159                first_trace_id = embedder
2160                    .trace_list(EMBEDDED_TRACE_API_VERSION, 1, None)
2161                    .map_err(|error| error.message.to_string())?
2162                    .summaries
2163                    .first()
2164                    .map(|summary| summary.trace_id.clone());
2165            }
2166        }
2167        let first_trace_id =
2168            first_trace_id.ok_or("the first trace should be retained initially")?;
2169        let retained = embedder
2170            .trace_list(
2171                EMBEDDED_TRACE_API_VERSION,
2172                EMBEDDED_TRACE_RETENTION_LIMIT,
2173                None,
2174            )
2175            .map_err(|error| error.message.to_string())?;
2176        assert_eq!(retained.summaries.len(), EMBEDDED_TRACE_RETENTION_LIMIT);
2177        assert_eq!(retained.summaries[0].completion_sequence, 101);
2178        let evicted = embedder
2179            .trace_get(EMBEDDED_TRACE_API_VERSION, &first_trace_id)
2180            .err()
2181            .ok_or("the oldest record should have been evicted")?;
2182        assert_eq!(evicted.code, EmbeddedTraceApiErrorCode::TraceNotFound);
2183        Ok(())
2184    }
2185
2186    #[test]
2187    fn embedded_trace_error_codes_render_stably() {
2188        let codes = [
2189            (EmbeddedTraceApiErrorCode::InvalidCursor, "invalid_cursor"),
2190            (EmbeddedTraceApiErrorCode::TraceNotFound, "trace_not_found"),
2191            (
2192                EmbeddedTraceApiErrorCode::TraceApiUnavailable,
2193                "trace_api_unavailable",
2194            ),
2195            (
2196                EmbeddedTraceApiErrorCode::IncompatibleVersion,
2197                "incompatible_version",
2198            ),
2199        ];
2200        for (code, expected) in codes {
2201            assert_eq!(code.as_str(), expected);
2202        }
2203    }
2204
2205    #[test]
2206    fn trace_projection_codes_cover_public_runtime_enums() {
2207        let states = [
2208            (traverse_runtime::RuntimeState::Idle, "idle"),
2209            (
2210                traverse_runtime::RuntimeState::LoadingRegistry,
2211                "loading_registry",
2212            ),
2213            (traverse_runtime::RuntimeState::Ready, "ready"),
2214            (traverse_runtime::RuntimeState::Discovering, "discovering"),
2215            (
2216                traverse_runtime::RuntimeState::EvaluatingConstraints,
2217                "evaluating_constraints",
2218            ),
2219            (traverse_runtime::RuntimeState::Selecting, "selecting"),
2220            (traverse_runtime::RuntimeState::Executing, "executing"),
2221            (
2222                traverse_runtime::RuntimeState::EmittingEvents,
2223                "emitting_events",
2224            ),
2225            (traverse_runtime::RuntimeState::Completed, "completed"),
2226            (traverse_runtime::RuntimeState::Error, "error"),
2227        ];
2228        for (state, expected) in states {
2229            assert_eq!(runtime_state_code(state), expected);
2230        }
2231
2232        let placements = [
2233            (PlacementTarget::Local, "local"),
2234            (PlacementTarget::Browser, "browser"),
2235            (PlacementTarget::Edge, "edge"),
2236            (PlacementTarget::Cloud, "cloud"),
2237            (PlacementTarget::Worker, "worker"),
2238            (PlacementTarget::Device, "device"),
2239        ];
2240        for (target, expected) in placements {
2241            assert_eq!(placement_target_code(target), expected);
2242        }
2243
2244        let failures = [
2245            (
2246                ExecutionFailureReason::ContractInputInvalid,
2247                "contract_input_invalid",
2248            ),
2249            (ExecutionFailureReason::ArtifactMissing, "artifact_missing"),
2250            (
2251                ExecutionFailureReason::ArtifactNotRunnable,
2252                "artifact_not_runnable",
2253            ),
2254            (
2255                ExecutionFailureReason::PlacementUnsupported,
2256                "placement_unsupported",
2257            ),
2258            (ExecutionFailureReason::ExecutionFailed, "execution_failed"),
2259            (
2260                ExecutionFailureReason::ContractOutputInvalid,
2261                "contract_output_invalid",
2262            ),
2263        ];
2264        for (reason, expected) in failures {
2265            assert_eq!(execution_failure_code(reason), expected);
2266        }
2267    }
2268
2269    #[test]
2270    fn offline_registry_resolver_loads_prepared_contract_and_reports_missing() {
2271        use crate::registry_cache::{
2272            HostRegistryCache, RegistryArtifactFetcher, prepare, resolve_component,
2273        };
2274        use sha2::{Digest, Sha256};
2275        use std::collections::HashMap;
2276        use std::fmt::Write as _;
2277        use traverse_registry::{
2278            PublicRegistryCapabilityRecord, RegistryComponentResolver, RegistryReference,
2279            SyncedPublicRegistryState,
2280        };
2281
2282        struct MapFetcher {
2283            assets: HashMap<String, Vec<u8>>,
2284        }
2285        impl RegistryArtifactFetcher for MapFetcher {
2286            fn fetch(&self, url: &str) -> Result<Vec<u8>, String> {
2287                self.assets
2288                    .get(url)
2289                    .cloned()
2290                    .ok_or_else(|| "missing".to_string())
2291            }
2292        }
2293
2294        fn sha256_hex(bytes: &[u8]) -> String {
2295            let digest = Sha256::digest(bytes);
2296            let mut value = String::with_capacity(digest.len() * 2);
2297            for byte in digest {
2298                let _ = write!(value, "{byte:02x}");
2299            }
2300            value
2301        }
2302
2303        let contract = include_str!(
2304            "../../../contracts/examples/traverse-starter/capabilities/process/contract.json"
2305        )
2306        .as_bytes()
2307        .to_vec();
2308        let artifact = b"\0asm\x01\0\0\0".to_vec();
2309        let artifact_digest = format!("sha256:{}", sha256_hex(&artifact));
2310        let contract_digest = format!("sha256:{}", sha256_hex(&contract));
2311        let record = PublicRegistryCapabilityRecord {
2312            namespace: "traverse-starter".to_string(),
2313            id: "process".to_string(),
2314            version: "1.0.0".to_string(),
2315            digest: artifact_digest.clone(),
2316            artifact_url: "https://example.test/process.wasm".to_string(),
2317            contract_digest: contract_digest.clone(),
2318            contract_url: "https://example.test/process.json".to_string(),
2319            deprecated: false,
2320        };
2321        let snapshot = SyncedPublicRegistryState {
2322            schema_version: "1".to_string(),
2323            workspace_id: "ws".to_string(),
2324            state_scope: "public".to_string(),
2325            source_repo: "traverse-framework/registry".to_string(),
2326            release_tag: "index-v1".to_string(),
2327            index_version: 1,
2328            generated_at: "2026-07-29T00:00:00Z".to_string(),
2329            source_commit: None,
2330            synced_at: "2026-07-29T00:00:00Z".to_string(),
2331            record_count: 1,
2332            validation_status: "valid".to_string(),
2333            governing_spec: "055-registry-sync".to_string(),
2334            capabilities: vec![record.clone()],
2335        };
2336        let mut assets = HashMap::new();
2337        assets.insert(record.artifact_url.clone(), artifact);
2338        assets.insert(record.contract_url.clone(), contract);
2339        let fetcher = MapFetcher { assets };
2340        let reference = RegistryReference {
2341            namespace: "traverse-starter".to_string(),
2342            id: "process".to_string(),
2343            version_range: "^1.0.0".to_string(),
2344        };
2345        let root = std::env::temp_dir().join(format!(
2346            "traverse-embedder-resolver-{}-{}",
2347            std::process::id(),
2348            std::time::SystemTime::now()
2349                .duration_since(std::time::UNIX_EPOCH)
2350                .expect("clock")
2351                .as_nanos()
2352        ));
2353        std::fs::create_dir_all(&root).expect("root");
2354        let cache = HostRegistryCache::new(root);
2355        prepare(&cache, &snapshot, &reference, &fetcher).expect("prepare");
2356        let resolver = OfflineRegistryCacheResolver { cache: &cache };
2357        let loaded = resolver.resolve(&reference).expect("resolve");
2358        assert_eq!(loaded.wasm_digest, artifact_digest);
2359        assert_eq!(loaded.contract.id, "traverse-starter.process");
2360
2361        let missing_ref = RegistryReference {
2362            namespace: "missing".to_string(),
2363            id: "capability".to_string(),
2364            version_range: "^1.0.0".to_string(),
2365        };
2366        let missing = resolver.resolve(&missing_ref).expect_err("missing");
2367        assert!(
2368            missing.errors[0]
2369                .message
2370                .contains("registry_cache_entry_missing")
2371        );
2372        let _ = resolve_component(&cache, &reference);
2373    }
2374
2375    #[test]
2376    fn map_manifest_failure_marks_registry_cache_misses() {
2377        let failure = ApplicationManifestFailure {
2378            errors: vec![ApplicationManifestError {
2379                code: ApplicationManifestErrorCode::RegistryReferenceRequiresResolution,
2380                path: "$.registry_ref".to_string(),
2381                message: "registry_cache_entry_missing: absent".to_string(),
2382            }],
2383        };
2384        let mapped = map_manifest_failure(&failure);
2385        assert_eq!(mapped.code, EmbedderErrorCode::BundleLoadFailed);
2386        assert!(mapped.message.contains("registry_cache_entry_missing"));
2387
2388        let other = ApplicationManifestFailure {
2389            errors: vec![ApplicationManifestError {
2390                code: ApplicationManifestErrorCode::ManifestReadFailed,
2391                path: "$".to_string(),
2392                message: "boom".to_string(),
2393            }],
2394        };
2395        let mapped_other = map_manifest_failure(&other);
2396        assert!(
2397            mapped_other
2398                .message
2399                .contains("application bundle failed to load")
2400        );
2401        assert!(
2402            !mapped_other
2403                .message
2404                .contains("registry_cache_entry_missing):")
2405        );
2406    }
2407}