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