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 test_double;
85
86pub use test_double::EmbedderTestDouble;
87
88use serde_json::{Value, json};
89use std::collections::BTreeMap;
90use std::path::{Path, PathBuf};
91use traverse_registry::{
92    ApplicationRegistrationFailure, ApplicationRegistrationRequest, ApplicationRegistry,
93    CapabilityRegistry, ComponentExecutionMode, EventRegistry, RegistryScope, WorkflowRegistry,
94    load_application_bundle_manifest,
95};
96use traverse_runtime::{
97    ArtifactRouter, PlacementTarget, Runtime, RuntimeContext, RuntimeError, RuntimeErrorCode,
98    RuntimeIntent, RuntimeLookup, RuntimeLookupScope, RuntimeRequest, RuntimeResultStatus,
99    WorkflowExecutionRequest, WorkflowLookupScope, WorkflowTraversalStatus,
100    WorkflowTraversalStepStatus,
101};
102
103/// Implemented embedder API version (spec 057 IDL `$id` suffix).
104pub const EMBEDDER_API_VERSION: &str = "1.0.0";
105
106/// Conformance suite revision this package certifies against (spec 057).
107pub const EMBEDDER_CONFORMANCE_VERSION: &str = "1.0.0";
108
109/// Application bundle manifest `schema_version` values this package accepts.
110pub const SUPPORTED_BUNDLE_SCHEMA_VERSIONS: &[&str] = &["1.0.0"];
111
112const EVENT_SCHEMA_VERSION: &str = "1.0.0";
113const DEFAULT_WORKSPACE_ID: &str = "local-default";
114
115/// Runtime artifact verification posture for the embedded runtime.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
117pub enum SecurityPosture {
118    /// Reject unsigned bundle artifacts (spec 030 FR-013). Default.
119    #[default]
120    Production,
121    /// Allow locally built unsigned bundle artifacts with a runtime warning.
122    Development,
123}
124
125/// Configuration for [`BundleEmbedder::init`] (`runtime.init` input).
126#[derive(Debug, Clone)]
127pub struct EmbedderConfig {
128    /// Path to the application bundle's `app.manifest.json`.
129    pub manifest_bundle_path: PathBuf,
130    /// Workspace identity recorded on registrations and events.
131    pub workspace_id: String,
132    /// Platform identity checked against compatible-capability allowlists.
133    pub platform: String,
134    /// Artifact verification posture.
135    pub security: SecurityPosture,
136}
137
138impl EmbedderConfig {
139    /// Creates a config with IDL defaults: workspace `local-default`, the
140    /// compiling platform's OS identifier, and the production security
141    /// posture.
142    #[must_use]
143    pub fn new(manifest_bundle_path: impl Into<PathBuf>) -> Self {
144        Self {
145            manifest_bundle_path: manifest_bundle_path.into(),
146            workspace_id: DEFAULT_WORKSPACE_ID.to_string(),
147            platform: std::env::consts::OS.to_string(),
148            security: SecurityPosture::Production,
149        }
150    }
151}
152
153/// Stable embedder-boundary error codes.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum EmbedderErrorCode {
156    /// The bundle failed to load, validate, or register.
157    BundleLoadFailed,
158    /// The bundle declares a manifest schema version this package does not support.
159    UnsupportedBundleSchema,
160    /// The bundle path could not be resolved to an absolute path.
161    BundlePathInvalid,
162    /// The embedded WASM executor could not initialize.
163    ExecutorUnavailable,
164    /// The runtime was shut down; no further operations are accepted.
165    RuntimeStopped,
166    /// The submitted target is neither a bundled workflow nor a bundled capability.
167    TargetNotFound,
168    /// The target is a compatible-mode capability; use the compatible lifecycle.
169    CompatibleLifecycleRequired,
170    /// The capability exists but is not a compatible-mode capability.
171    CapabilityNotCompatible,
172    /// The capability's platform allowlist does not include this platform.
173    PlatformNotSupported,
174    /// No instance with the given id exists for the capability.
175    InstanceNotFound,
176    /// No running instance matches the request.
177    InstanceNotRunning,
178}
179
180impl EmbedderErrorCode {
181    /// Stable `snake_case` wire representation of the code.
182    #[must_use]
183    pub fn as_str(self) -> &'static str {
184        match self {
185            Self::BundleLoadFailed => "bundle_load_failed",
186            Self::UnsupportedBundleSchema => "unsupported_bundle_schema",
187            Self::BundlePathInvalid => "bundle_path_invalid",
188            Self::ExecutorUnavailable => "executor_unavailable",
189            Self::RuntimeStopped => "runtime_stopped",
190            Self::TargetNotFound => "target_not_found",
191            Self::CompatibleLifecycleRequired => "compatible_lifecycle_required",
192            Self::CapabilityNotCompatible => "capability_not_compatible",
193            Self::PlatformNotSupported => "platform_not_supported",
194            Self::InstanceNotFound => "instance_not_found",
195            Self::InstanceNotRunning => "instance_not_running",
196        }
197    }
198}
199
200/// A structured embedder-boundary error.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct EmbedderError {
203    /// Stable error code.
204    pub code: EmbedderErrorCode,
205    /// Human-readable, secret-free explanation.
206    pub message: String,
207}
208
209impl EmbedderError {
210    fn new(code: EmbedderErrorCode, message: impl Into<String>) -> Self {
211        Self {
212            code,
213            message: message.into(),
214        }
215    }
216
217    fn as_value(&self) -> Value {
218        json!({ "code": self.code.as_str(), "message": self.message })
219    }
220}
221
222/// `runtime.submit` acceptance status.
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum SubmitStatus {
225    /// The submission was accepted and executed; results arrived as events.
226    Accepted,
227    /// The submission was rejected at the embedder boundary.
228    Rejected,
229}
230
231/// `runtime.submit` output.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SubmitOutcome {
234    /// Session identifier (`null` in the IDL when rejected).
235    pub session_id: Option<String>,
236    /// Acceptance status.
237    pub status: SubmitStatus,
238    /// Boundary rejection error, when rejected.
239    pub error: Option<EmbedderError>,
240}
241
242/// `compatible.start` output.
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct CompatibleStartOutcome {
245    /// Instance identifier (`null` in the IDL on error).
246    pub instance_id: Option<String>,
247    /// `started` on success.
248    pub status: CompatibleLifecycleStatus,
249    /// Boundary error, when not started.
250    pub error: Option<EmbedderError>,
251}
252
253/// `compatible.stop` / `compatible.kill` output.
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct CompatibleLifecycleOutcome {
256    /// `stopped` or `killed` on success.
257    pub status: CompatibleLifecycleStatus,
258    /// Boundary error, when the lifecycle change did not happen.
259    pub error: Option<EmbedderError>,
260}
261
262/// Compatible-capability lifecycle statuses.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum CompatibleLifecycleStatus {
265    /// The instance is running.
266    Started,
267    /// The instance stopped gracefully.
268    Stopped,
269    /// The instance was force-terminated.
270    Killed,
271    /// The lifecycle operation failed.
272    Error,
273}
274
275/// `runtime.shutdown` output (always `stopped`).
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub struct ShutdownOutcome {
278    /// Number of compatible instances force-terminated by this call.
279    pub killed_instances: usize,
280}
281
282/// Ordered, synchronous event subscriber.
283pub type EventCallback = Box<dyn FnMut(&Value) + Send>;
284
285/// The uniform `embedder-api/1.0.0` operation surface (spec 057 FR-003).
286///
287/// [`BundleEmbedder`] is the production implementation;
288/// [`EmbedderTestDouble`] is the deterministic in-memory test double
289/// required by spec 068 FR-006. Both emit identical event envelopes.
290pub trait TraverseEmbedderApi {
291    /// `runtime.submit`: execute a bundled workflow or WASM capability.
292    fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome;
293
294    /// `runtime.subscribe`: register an ordered event callback. Previously
295    /// emitted events are replayed to the new subscriber first, so late
296    /// subscribers observe the identical ordered stream.
297    fn subscribe(&mut self, callback: EventCallback);
298
299    /// `compatible.start`: start a compatible-mode capability instance.
300    fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome;
301
302    /// `compatible.stop`: gracefully stop one instance (`instance_id`) or
303    /// every running instance of the capability (`None`).
304    fn stop_compatible(
305        &mut self,
306        capability_id: &str,
307        instance_id: Option<&str>,
308    ) -> CompatibleLifecycleOutcome;
309
310    /// `compatible.kill`: force-terminate one instance (`instance_id`) or
311    /// every running instance of the capability (`None`).
312    fn kill_compatible(
313        &mut self,
314        capability_id: &str,
315        instance_id: Option<&str>,
316    ) -> CompatibleLifecycleOutcome;
317
318    /// `runtime.shutdown`: kill running compatible instances and stop
319    /// accepting operations. Idempotent.
320    fn shutdown(&mut self) -> ShutdownOutcome;
321
322    /// Release evidence connecting this embedder to its package version,
323    /// linked runtime, conformance version, and bundle digests (spec 068
324    /// FR-008, NFR-002).
325    fn release_evidence(&self) -> Value;
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329enum InstanceState {
330    Started,
331    Stopped,
332    Killed,
333}
334
335impl InstanceState {
336    fn as_str(self) -> &'static str {
337        match self {
338            Self::Started => "started",
339            Self::Stopped => "stopped",
340            Self::Killed => "killed",
341        }
342    }
343}
344
345#[derive(Debug, Clone)]
346struct CompatibleInstance {
347    capability_id: String,
348    state: InstanceState,
349}
350
351/// Shared deterministic embedder state: identity, counters, subscribers,
352/// event history, and the compatible-capability lifecycle table. Both the
353/// production embedder and the test double delegate here so their public
354/// boundary behavior is identical.
355pub(crate) struct EmbedderCore {
356    workspace_id: String,
357    app_id: String,
358    app_version: String,
359    platform: String,
360    compatible_targets: BTreeMap<String, Vec<String>>,
361    instances: BTreeMap<String, CompatibleInstance>,
362    subscribers: Vec<EventCallback>,
363    history: Vec<Value>,
364    next_event: u64,
365    next_session: u64,
366    next_request: u64,
367    next_instance: u64,
368    stopped: bool,
369}
370
371impl EmbedderCore {
372    pub(crate) fn new(
373        workspace_id: String,
374        app_id: String,
375        app_version: String,
376        platform: String,
377        compatible_targets: BTreeMap<String, Vec<String>>,
378    ) -> Self {
379        Self {
380            workspace_id,
381            app_id,
382            app_version,
383            platform,
384            compatible_targets,
385            instances: BTreeMap::new(),
386            subscribers: Vec::new(),
387            history: Vec::new(),
388            next_event: 0,
389            next_session: 0,
390            next_request: 0,
391            next_instance: 0,
392            stopped: false,
393        }
394    }
395
396    fn next_session_id(&mut self) -> String {
397        self.next_session += 1;
398        format!("sess-{:08}", self.next_session)
399    }
400
401    fn next_request_id(&mut self) -> String {
402        self.next_request += 1;
403        format!("req-{:08}", self.next_request)
404    }
405
406    fn next_instance_id(&mut self) -> String {
407        self.next_instance += 1;
408        format!("inst-{:08}", self.next_instance)
409    }
410
411    fn emit(&mut self, event_type: &str, session_id: Option<&str>, data: Value) {
412        self.next_event += 1;
413        let mut event = json!({
414            "kind": "embedder_event",
415            "schema_version": EVENT_SCHEMA_VERSION,
416            "embedder_api_version": EMBEDDER_API_VERSION,
417            "event_id": format!("evt-{:08}", self.next_event),
418            "sequence": self.next_event,
419            "event_type": event_type,
420            "workspace_id": self.workspace_id,
421            "app_id": self.app_id,
422            "session_id": session_id,
423        });
424        event["data"] = data;
425        for subscriber in &mut self.subscribers {
426            subscriber(&event);
427        }
428        self.history.push(event);
429    }
430
431    fn subscribe(&mut self, mut callback: EventCallback) {
432        for event in &self.history {
433            callback(event);
434        }
435        self.subscribers.push(callback);
436    }
437
438    fn emit_error_event(&mut self, session_id: Option<&str>, error: &EmbedderError, data: Value) {
439        let mut payload = data;
440        payload["error"] = error.as_value();
441        self.emit("error", session_id, payload);
442    }
443
444    fn rejected_submit(&mut self, target_id: &str, error: EmbedderError) -> SubmitOutcome {
445        self.emit_error_event(None, &error, json!({ "target_id": target_id }));
446        SubmitOutcome {
447            session_id: None,
448            status: SubmitStatus::Rejected,
449            error: Some(error),
450        }
451    }
452
453    fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
454        let error = if self.stopped {
455            Some(runtime_stopped_error())
456        } else {
457            match self.compatible_targets.get(capability_id) {
458                None => Some(EmbedderError::new(
459                    EmbedderErrorCode::CapabilityNotCompatible,
460                    format!(
461                        "capability '{capability_id}' is not a compatible-mode capability in this bundle"
462                    ),
463                )),
464                Some(platforms) if !platforms.iter().any(|p| p == &self.platform) => {
465                    Some(EmbedderError::new(
466                        EmbedderErrorCode::PlatformNotSupported,
467                        format!(
468                            "capability '{capability_id}' permits platforms [{}] but this embedder runs on '{}'",
469                            platforms.join(", "),
470                            self.platform
471                        ),
472                    ))
473                }
474                Some(_) => None,
475            }
476        };
477        if let Some(error) = error {
478            self.emit_error_event(None, &error, json!({ "capability_id": capability_id }));
479            return CompatibleStartOutcome {
480                instance_id: None,
481                status: CompatibleLifecycleStatus::Error,
482                error: Some(error),
483            };
484        }
485
486        let instance_id = self.next_instance_id();
487        self.instances.insert(
488            instance_id.clone(),
489            CompatibleInstance {
490                capability_id: capability_id.to_string(),
491                state: InstanceState::Started,
492            },
493        );
494        self.emit(
495            "state_changed",
496            None,
497            json!({
498                "capability_id": capability_id,
499                "instance_id": instance_id,
500                "state": InstanceState::Started.as_str(),
501                "previous_state": null,
502                "input": input,
503            }),
504        );
505        CompatibleStartOutcome {
506            instance_id: Some(instance_id),
507            status: CompatibleLifecycleStatus::Started,
508            error: None,
509        }
510    }
511
512    fn transition_compatible(
513        &mut self,
514        capability_id: &str,
515        instance_id: Option<&str>,
516        target_state: InstanceState,
517    ) -> CompatibleLifecycleOutcome {
518        if self.stopped {
519            let error = runtime_stopped_error();
520            self.emit_error_event(None, &error, json!({ "capability_id": capability_id }));
521            return CompatibleLifecycleOutcome {
522                status: CompatibleLifecycleStatus::Error,
523                error: Some(error),
524            };
525        }
526
527        let selected: Vec<String> = match instance_id {
528            Some(requested) => match self.instances.get(requested) {
529                Some(instance) if instance.capability_id == capability_id => {
530                    if instance.state == InstanceState::Started {
531                        vec![requested.to_string()]
532                    } else {
533                        let error = EmbedderError::new(
534                            EmbedderErrorCode::InstanceNotRunning,
535                            format!(
536                                "instance '{requested}' of capability '{capability_id}' is not running"
537                            ),
538                        );
539                        self.emit_error_event(
540                            None,
541                            &error,
542                            json!({ "capability_id": capability_id, "instance_id": requested }),
543                        );
544                        return CompatibleLifecycleOutcome {
545                            status: CompatibleLifecycleStatus::Error,
546                            error: Some(error),
547                        };
548                    }
549                }
550                _ => {
551                    let error = EmbedderError::new(
552                        EmbedderErrorCode::InstanceNotFound,
553                        format!(
554                            "no instance '{requested}' exists for capability '{capability_id}'"
555                        ),
556                    );
557                    self.emit_error_event(
558                        None,
559                        &error,
560                        json!({ "capability_id": capability_id, "instance_id": requested }),
561                    );
562                    return CompatibleLifecycleOutcome {
563                        status: CompatibleLifecycleStatus::Error,
564                        error: Some(error),
565                    };
566                }
567            },
568            None => self
569                .instances
570                .iter()
571                .filter(|(_, instance)| {
572                    instance.capability_id == capability_id
573                        && instance.state == InstanceState::Started
574                })
575                .map(|(id, _)| id.clone())
576                .collect(),
577        };
578
579        if selected.is_empty() {
580            let error = EmbedderError::new(
581                EmbedderErrorCode::InstanceNotRunning,
582                format!("capability '{capability_id}' has no running instances"),
583            );
584            self.emit_error_event(None, &error, json!({ "capability_id": capability_id }));
585            return CompatibleLifecycleOutcome {
586                status: CompatibleLifecycleStatus::Error,
587                error: Some(error),
588            };
589        }
590
591        for id in selected {
592            self.set_instance_state(&id, target_state);
593        }
594        CompatibleLifecycleOutcome {
595            status: match target_state {
596                InstanceState::Stopped => CompatibleLifecycleStatus::Stopped,
597                _ => CompatibleLifecycleStatus::Killed,
598            },
599            error: None,
600        }
601    }
602
603    fn set_instance_state(&mut self, instance_id: &str, target_state: InstanceState) {
604        let Some(instance) = self.instances.get_mut(instance_id) else {
605            return;
606        };
607        let previous = instance.state;
608        instance.state = target_state;
609        let capability_id = instance.capability_id.clone();
610        self.emit(
611            "state_changed",
612            None,
613            json!({
614                "capability_id": capability_id,
615                "instance_id": instance_id,
616                "state": target_state.as_str(),
617                "previous_state": previous.as_str(),
618            }),
619        );
620    }
621
622    fn shutdown(&mut self) -> ShutdownOutcome {
623        if self.stopped {
624            return ShutdownOutcome {
625                killed_instances: 0,
626            };
627        }
628        let running: Vec<String> = self
629            .instances
630            .iter()
631            .filter(|(_, instance)| instance.state == InstanceState::Started)
632            .map(|(id, _)| id.clone())
633            .collect();
634        let killed_instances = running.len();
635        for id in running {
636            self.set_instance_state(&id, InstanceState::Killed);
637        }
638        self.stopped = true;
639        ShutdownOutcome { killed_instances }
640    }
641
642    fn evidence(&self, runtime_implementation: &str, wasm_components: Value) -> Value {
643        let mut evidence = json!({
644            "kind": "embedder_release_evidence",
645            "schema_version": EVENT_SCHEMA_VERSION,
646            "package": {
647                "name": env!("CARGO_PKG_NAME"),
648                "version": env!("CARGO_PKG_VERSION"),
649            },
650            "embedder_api_version": EMBEDDER_API_VERSION,
651            "conformance_version": EMBEDDER_CONFORMANCE_VERSION,
652            "runtime": {
653                "implementation": runtime_implementation,
654                "version": env!("CARGO_PKG_VERSION"),
655                "linkage": "native-static",
656            },
657            "supported_bundle_schema_versions": SUPPORTED_BUNDLE_SCHEMA_VERSIONS,
658            "bundle": {
659                "app_id": self.app_id,
660                "app_version": self.app_version,
661            },
662            "workspace_id": self.workspace_id,
663            "platform": self.platform,
664        });
665        evidence["bundle"]["wasm_components"] = wasm_components;
666        evidence
667    }
668}
669
670#[derive(Debug, Clone)]
671struct WasmTarget {
672    capability_version: String,
673}
674
675#[derive(Debug, Clone)]
676struct WorkflowTarget {
677    workflow_version: String,
678}
679
680/// Submittable targets and evidence derived from a loaded bundle manifest.
681struct BundleTargets {
682    wasm: BTreeMap<String, WasmTarget>,
683    compatible: BTreeMap<String, Vec<String>>,
684    workflows: BTreeMap<String, WorkflowTarget>,
685    wasm_component_evidence: Vec<Value>,
686}
687
688impl BundleTargets {
689    fn from_manifest(manifest: &traverse_registry::ApplicationBundleManifest) -> Self {
690        let mut wasm = BTreeMap::new();
691        let mut compatible = BTreeMap::new();
692        let mut wasm_component_evidence = Vec::new();
693        for component in &manifest.components {
694            match component.manifest.execution_mode {
695                ComponentExecutionMode::Wasm => {
696                    wasm.insert(
697                        component.manifest.capability_id.clone(),
698                        WasmTarget {
699                            capability_version: component.manifest.capability_version.clone(),
700                        },
701                    );
702                    wasm_component_evidence.push(json!({
703                        "component_id": component.manifest.component_id,
704                        "capability_id": component.manifest.capability_id,
705                        "wasm_digest": component.verified_wasm_digest,
706                    }));
707                }
708                ComponentExecutionMode::Compatible => {
709                    compatible.insert(
710                        component.manifest.capability_id.clone(),
711                        component.manifest.platforms.clone(),
712                    );
713                }
714            }
715        }
716        let workflows = manifest
717            .workflows
718            .iter()
719            .map(|workflow| {
720                (
721                    workflow.workflow_id.clone(),
722                    WorkflowTarget {
723                        workflow_version: workflow.workflow_version.clone(),
724                    },
725                )
726            })
727            .collect();
728        Self {
729            wasm,
730            compatible,
731            workflows,
732            wasm_component_evidence,
733        }
734    }
735}
736
737/// Production embedder: loads an application-owned bundle and executes it
738/// through the natively linked Traverse runtime.
739pub struct BundleEmbedder {
740    core: EmbedderCore,
741    runtime: Runtime<ArtifactRouter>,
742    wasm_targets: BTreeMap<String, WasmTarget>,
743    workflow_targets: BTreeMap<String, WorkflowTarget>,
744    wasm_component_evidence: Value,
745}
746
747impl BundleEmbedder {
748    /// `runtime.init`: load, verify, and register the application bundle.
749    ///
750    /// # Errors
751    ///
752    /// Returns an [`EmbedderError`] with a stable code when the bundle path
753    /// cannot be resolved, the bundle schema version is unsupported, the
754    /// bundle fails validation or registration, or the WASM executor cannot
755    /// initialize. Rejections are deterministic and never fall back to a
756    /// sidecar (spec 068 NFR-001).
757    #[allow(unexpected_cfgs)]
758    pub fn init(config: EmbedderConfig) -> Result<Self, EmbedderError> {
759        let manifest_path = absolute_bundle_path(&config.manifest_bundle_path)?;
760        let manifest = load_application_bundle_manifest(&manifest_path).map_err(|failure| {
761            EmbedderError::new(
762                EmbedderErrorCode::BundleLoadFailed,
763                format!(
764                    "application bundle failed to load: {}",
765                    manifest_failure_messages(
766                        &failure
767                            .errors
768                            .iter()
769                            .map(|e| e.message.clone())
770                            .collect::<Vec<_>>()
771                    )
772                ),
773            )
774        })?;
775        ensure_supported_bundle_schema(&manifest.schema_version)?;
776
777        let mut capabilities = CapabilityRegistry::new();
778        let events = EventRegistry::new();
779        let mut workflows = WorkflowRegistry::new();
780        let mut applications = ApplicationRegistry::new();
781        applications
782            .register_bundle(
783                &mut capabilities,
784                &events,
785                &mut workflows,
786                &ApplicationRegistrationRequest {
787                    scope: RegistryScope::Private,
788                    workspace_id: config.workspace_id.clone(),
789                    manifest_path: manifest_path.clone(),
790                    registered_at: format!("bundle:{}@{}", manifest.app_id, manifest.version),
791                    validator_version: env!("CARGO_PKG_VERSION").to_string(),
792                },
793            )
794            .map_err(|failure| registration_failure_error(&failure))?;
795
796        #[cfg(coverage)]
797        let executor = ArtifactRouter::new()
798            .expect("the bounded Wasmtime configuration initializes under coverage");
799        #[cfg(not(coverage))]
800        let executor = ArtifactRouter::new().map_err(|failure| {
801            EmbedderError::new(EmbedderErrorCode::ExecutorUnavailable, failure.message)
802        })?;
803
804        let security = match config.security {
805            SecurityPosture::Production => {
806                traverse_runtime::security::RuntimeSecurityConfig::production()
807            }
808            SecurityPosture::Development => {
809                traverse_runtime::security::RuntimeSecurityConfig::development()
810            }
811        };
812        let runtime = Runtime::new(capabilities, executor)
813            .with_workflow_registry(workflows)
814            .with_security_config(security);
815
816        let targets = BundleTargets::from_manifest(&manifest);
817        Ok(Self {
818            core: EmbedderCore::new(
819                config.workspace_id,
820                manifest.app_id,
821                manifest.version,
822                config.platform,
823                targets.compatible,
824            ),
825            runtime,
826            wasm_targets: targets.wasm,
827            workflow_targets: targets.workflows,
828            wasm_component_evidence: Value::Array(targets.wasm_component_evidence),
829        })
830    }
831
832    fn submit_workflow(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
833        let workflow_version = self.workflow_targets[target_id].workflow_version.clone();
834        let session_id = self.core.next_session_id();
835        let request_id = self.core.next_request_id();
836        let outcome = self.runtime.execute_workflow(WorkflowExecutionRequest {
837            kind: "workflow_execution_request".to_string(),
838            schema_version: "1.0.0".to_string(),
839            request_id: request_id.clone(),
840            workflow_id: target_id.to_string(),
841            workflow_version: workflow_version.clone(),
842            scope: WorkflowLookupScope::PreferPrivate,
843            input: input.clone(),
844            governing_spec: "007-workflow-registry-traversal".to_string(),
845        });
846
847        for step in &outcome.evidence.visited_nodes {
848            self.core.emit(
849                "capability_invoked",
850                Some(&session_id),
851                json!({
852                    "request_id": request_id,
853                    "workflow_id": target_id,
854                    "workflow_version": workflow_version,
855                    "step_index": step.step_index,
856                    "node_id": step.node_id,
857                    "capability_id": step.capability_id,
858                    "capability_version": step.capability_version,
859                    "status": workflow_step_status_str(step.status),
860                }),
861            );
862        }
863        match outcome.result.status {
864            WorkflowTraversalStatus::Completed => {
865                self.core.emit(
866                    "capability_result",
867                    Some(&session_id),
868                    json!({
869                        "request_id": request_id,
870                        "workflow_id": target_id,
871                        "workflow_version": workflow_version,
872                        "status": "completed",
873                        "output": outcome.result.output,
874                    }),
875                );
876            }
877            WorkflowTraversalStatus::Error => {
878                self.core.emit(
879                    "error",
880                    Some(&session_id),
881                    json!({
882                        "request_id": request_id,
883                        "workflow_id": target_id,
884                        "workflow_version": workflow_version,
885                        "status": "error",
886                        "error": outcome.result.error.as_ref().map(runtime_error_value),
887                    }),
888                );
889            }
890        }
891        SubmitOutcome {
892            session_id: Some(session_id),
893            status: SubmitStatus::Accepted,
894            error: None,
895        }
896    }
897
898    fn submit_capability(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
899        let capability_version = self.wasm_targets[target_id].capability_version.clone();
900        let session_id = self.core.next_session_id();
901        let request_id = self.core.next_request_id();
902        let outcome = self.runtime.execute(RuntimeRequest {
903            kind: "runtime_request".to_string(),
904            schema_version: "1.0.0".to_string(),
905            request_id,
906            intent: RuntimeIntent {
907                capability_id: Some(target_id.to_string()),
908                capability_version: Some(capability_version.clone()),
909                version_range: None,
910                intent_key: None,
911            },
912            input: input.clone(),
913            lookup: RuntimeLookup {
914                scope: RuntimeLookupScope::PreferPrivate,
915                allow_ambiguity: false,
916            },
917            context: RuntimeContext {
918                requested_target: PlacementTarget::Local,
919                correlation_id: Some(session_id.clone()),
920                caller: None,
921                traceparent: None,
922                tracestate: None,
923                metadata: None,
924                identity: None,
925            },
926            governing_spec: "006-runtime-request-execution".to_string(),
927        });
928
929        let execution_id = outcome.result.execution_id.clone();
930        self.core.emit(
931            "capability_invoked",
932            Some(&session_id),
933            json!({
934                "execution_id": execution_id,
935                "capability_id": target_id,
936                "capability_version": capability_version,
937            }),
938        );
939        match outcome.result.status {
940            RuntimeResultStatus::Completed => {
941                self.core.emit(
942                    "capability_result",
943                    Some(&session_id),
944                    json!({
945                        "execution_id": execution_id,
946                        "capability_id": target_id,
947                        "status": "completed",
948                        "output": outcome.result.output,
949                    }),
950                );
951            }
952            RuntimeResultStatus::Error => {
953                self.core.emit(
954                    "error",
955                    Some(&session_id),
956                    json!({
957                        "execution_id": execution_id,
958                        "capability_id": target_id,
959                        "status": "error",
960                        "error": outcome.result.error.as_ref().map(runtime_error_value),
961                    }),
962                );
963            }
964        }
965        SubmitOutcome {
966            session_id: Some(session_id),
967            status: SubmitStatus::Accepted,
968            error: None,
969        }
970    }
971}
972
973impl TraverseEmbedderApi for BundleEmbedder {
974    fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome {
975        if self.core.stopped {
976            let error = runtime_stopped_error();
977            return self.core.rejected_submit(target_id, error);
978        }
979        if self.workflow_targets.contains_key(target_id) {
980            return self.submit_workflow(target_id, input);
981        }
982        if self.wasm_targets.contains_key(target_id) {
983            return self.submit_capability(target_id, input);
984        }
985        if self.core.compatible_targets.contains_key(target_id) {
986            let error = EmbedderError::new(
987                EmbedderErrorCode::CompatibleLifecycleRequired,
988                format!(
989                    "capability '{target_id}' is a compatible-mode capability; use compatible.start/stop/kill"
990                ),
991            );
992            return self.core.rejected_submit(target_id, error);
993        }
994        let error = EmbedderError::new(
995            EmbedderErrorCode::TargetNotFound,
996            format!("'{target_id}' is neither a bundled workflow nor a bundled capability"),
997        );
998        self.core.rejected_submit(target_id, error)
999    }
1000
1001    fn subscribe(&mut self, callback: EventCallback) {
1002        self.core.subscribe(callback);
1003    }
1004
1005    fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome {
1006        self.core.start_compatible(capability_id, input)
1007    }
1008
1009    fn stop_compatible(
1010        &mut self,
1011        capability_id: &str,
1012        instance_id: Option<&str>,
1013    ) -> CompatibleLifecycleOutcome {
1014        self.core
1015            .transition_compatible(capability_id, instance_id, InstanceState::Stopped)
1016    }
1017
1018    fn kill_compatible(
1019        &mut self,
1020        capability_id: &str,
1021        instance_id: Option<&str>,
1022    ) -> CompatibleLifecycleOutcome {
1023        self.core
1024            .transition_compatible(capability_id, instance_id, InstanceState::Killed)
1025    }
1026
1027    fn shutdown(&mut self) -> ShutdownOutcome {
1028        self.core.shutdown()
1029    }
1030
1031    fn release_evidence(&self) -> Value {
1032        self.core
1033            .evidence("traverse-runtime", self.wasm_component_evidence.clone())
1034    }
1035}
1036
1037fn runtime_stopped_error() -> EmbedderError {
1038    EmbedderError::new(
1039        EmbedderErrorCode::RuntimeStopped,
1040        "the embedded runtime was shut down and accepts no further operations",
1041    )
1042}
1043
1044fn absolute_bundle_path(path: &Path) -> Result<PathBuf, EmbedderError> {
1045    std::path::absolute(path).map_err(|error| {
1046        EmbedderError::new(
1047            EmbedderErrorCode::BundlePathInvalid,
1048            format!(
1049                "bundle path '{}' could not be resolved: {error}",
1050                path.display()
1051            ),
1052        )
1053    })
1054}
1055
1056fn ensure_supported_bundle_schema(schema_version: &str) -> Result<(), EmbedderError> {
1057    if SUPPORTED_BUNDLE_SCHEMA_VERSIONS.contains(&schema_version) {
1058        return Ok(());
1059    }
1060    Err(EmbedderError::new(
1061        EmbedderErrorCode::UnsupportedBundleSchema,
1062        format!(
1063            "bundle declares schema_version '{schema_version}' but this package supports [{}]; \
1064             no sidecar fallback is attempted",
1065            SUPPORTED_BUNDLE_SCHEMA_VERSIONS.join(", ")
1066        ),
1067    ))
1068}
1069
1070fn registration_failure_error(failure: &ApplicationRegistrationFailure) -> EmbedderError {
1071    EmbedderError::new(
1072        EmbedderErrorCode::BundleLoadFailed,
1073        format!(
1074            "application bundle failed to register: {}",
1075            manifest_failure_messages(
1076                &failure
1077                    .errors
1078                    .iter()
1079                    .map(|error| error.message.clone())
1080                    .collect::<Vec<_>>()
1081            )
1082        ),
1083    )
1084}
1085
1086fn manifest_failure_messages(messages: &[String]) -> String {
1087    messages.join("; ")
1088}
1089
1090fn runtime_error_value(error: &RuntimeError) -> Value {
1091    json!({
1092        "code": runtime_error_code_str(error.code),
1093        "message": error.message,
1094        "details": error.details,
1095    })
1096}
1097
1098fn runtime_error_code_str(code: RuntimeErrorCode) -> &'static str {
1099    match code {
1100        RuntimeErrorCode::RequestInvalid => "request_invalid",
1101        RuntimeErrorCode::CapabilityNotFound => "capability_not_found",
1102        RuntimeErrorCode::CapabilityAmbiguous => "capability_ambiguous",
1103        RuntimeErrorCode::CapabilityNotRunnable => "capability_not_runnable",
1104        RuntimeErrorCode::PlacementUnsupported => "placement_unsupported",
1105        RuntimeErrorCode::ArtifactMissing => "artifact_missing",
1106        RuntimeErrorCode::ExecutionFailed => "execution_failed",
1107        RuntimeErrorCode::OutputValidationFailed => "output_validation_failed",
1108        RuntimeErrorCode::ContractViolation => "contract_violation",
1109    }
1110}
1111
1112fn workflow_step_status_str(status: WorkflowTraversalStepStatus) -> &'static str {
1113    match status {
1114        WorkflowTraversalStepStatus::Entered => "entered",
1115        WorkflowTraversalStepStatus::Completed => "completed",
1116        WorkflowTraversalStepStatus::Failed => "failed",
1117    }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use super::*;
1123
1124    #[test]
1125    fn error_codes_render_stable_snake_case_strings() {
1126        let codes = [
1127            (EmbedderErrorCode::BundleLoadFailed, "bundle_load_failed"),
1128            (
1129                EmbedderErrorCode::UnsupportedBundleSchema,
1130                "unsupported_bundle_schema",
1131            ),
1132            (EmbedderErrorCode::BundlePathInvalid, "bundle_path_invalid"),
1133            (
1134                EmbedderErrorCode::ExecutorUnavailable,
1135                "executor_unavailable",
1136            ),
1137            (EmbedderErrorCode::RuntimeStopped, "runtime_stopped"),
1138            (EmbedderErrorCode::TargetNotFound, "target_not_found"),
1139            (
1140                EmbedderErrorCode::CompatibleLifecycleRequired,
1141                "compatible_lifecycle_required",
1142            ),
1143            (
1144                EmbedderErrorCode::CapabilityNotCompatible,
1145                "capability_not_compatible",
1146            ),
1147            (
1148                EmbedderErrorCode::PlatformNotSupported,
1149                "platform_not_supported",
1150            ),
1151            (EmbedderErrorCode::InstanceNotFound, "instance_not_found"),
1152            (
1153                EmbedderErrorCode::InstanceNotRunning,
1154                "instance_not_running",
1155            ),
1156        ];
1157        for (code, expected) in codes {
1158            assert_eq!(code.as_str(), expected);
1159        }
1160    }
1161
1162    #[test]
1163    fn runtime_error_codes_render_stable_snake_case_strings() {
1164        let codes = [
1165            (RuntimeErrorCode::RequestInvalid, "request_invalid"),
1166            (RuntimeErrorCode::CapabilityNotFound, "capability_not_found"),
1167            (
1168                RuntimeErrorCode::CapabilityAmbiguous,
1169                "capability_ambiguous",
1170            ),
1171            (
1172                RuntimeErrorCode::CapabilityNotRunnable,
1173                "capability_not_runnable",
1174            ),
1175            (
1176                RuntimeErrorCode::PlacementUnsupported,
1177                "placement_unsupported",
1178            ),
1179            (RuntimeErrorCode::ArtifactMissing, "artifact_missing"),
1180            (RuntimeErrorCode::ExecutionFailed, "execution_failed"),
1181            (
1182                RuntimeErrorCode::OutputValidationFailed,
1183                "output_validation_failed",
1184            ),
1185            (RuntimeErrorCode::ContractViolation, "contract_violation"),
1186        ];
1187        for (code, expected) in codes {
1188            assert_eq!(runtime_error_code_str(code), expected);
1189        }
1190    }
1191
1192    #[test]
1193    fn workflow_step_statuses_render_stable_strings() {
1194        assert_eq!(
1195            workflow_step_status_str(WorkflowTraversalStepStatus::Entered),
1196            "entered"
1197        );
1198        assert_eq!(
1199            workflow_step_status_str(WorkflowTraversalStepStatus::Completed),
1200            "completed"
1201        );
1202        assert_eq!(
1203            workflow_step_status_str(WorkflowTraversalStepStatus::Failed),
1204            "failed"
1205        );
1206    }
1207
1208    #[test]
1209    fn instance_states_render_stable_strings() {
1210        assert_eq!(InstanceState::Started.as_str(), "started");
1211        assert_eq!(InstanceState::Stopped.as_str(), "stopped");
1212        assert_eq!(InstanceState::Killed.as_str(), "killed");
1213    }
1214
1215    #[test]
1216    fn runtime_errors_map_to_structured_values() {
1217        let value = runtime_error_value(&RuntimeError {
1218            code: RuntimeErrorCode::ExecutionFailed,
1219            message: "capability failed".to_string(),
1220            details: json!({ "path": "$" }),
1221        });
1222        assert_eq!(
1223            value,
1224            json!({
1225                "code": "execution_failed",
1226                "message": "capability failed",
1227                "details": { "path": "$" },
1228            })
1229        );
1230    }
1231
1232    #[test]
1233    fn unsupported_bundle_schema_is_rejected_deterministically() -> Result<(), String> {
1234        let error = ensure_supported_bundle_schema("9.9.9")
1235            .err()
1236            .ok_or("schema 9.9.9 should be rejected")?;
1237        assert_eq!(error.code, EmbedderErrorCode::UnsupportedBundleSchema);
1238        assert!(error.message.contains("9.9.9"));
1239        assert!(error.message.contains("1.0.0"));
1240        ensure_supported_bundle_schema("1.0.0").map_err(|error| error.message)
1241    }
1242
1243    #[test]
1244    fn empty_bundle_path_is_rejected() -> Result<(), String> {
1245        let error = absolute_bundle_path(Path::new(""))
1246            .err()
1247            .ok_or("empty path should be rejected")?;
1248        assert_eq!(error.code, EmbedderErrorCode::BundlePathInvalid);
1249        Ok(())
1250    }
1251
1252    #[test]
1253    fn set_instance_state_ignores_unknown_instances() {
1254        let mut core = EmbedderCore::new(
1255            "local-default".to_string(),
1256            "app".to_string(),
1257            "1.0.0".to_string(),
1258            "linux".to_string(),
1259            BTreeMap::new(),
1260        );
1261        core.set_instance_state("inst-missing", InstanceState::Killed);
1262        assert!(core.history.is_empty());
1263    }
1264}