1mod 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
103pub const EMBEDDER_API_VERSION: &str = "1.0.0";
105
106pub const EMBEDDER_CONFORMANCE_VERSION: &str = "1.0.0";
108
109pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
117pub enum SecurityPosture {
118 #[default]
120 Production,
121 Development,
123}
124
125#[derive(Debug, Clone)]
127pub struct EmbedderConfig {
128 pub manifest_bundle_path: PathBuf,
130 pub workspace_id: String,
132 pub platform: String,
134 pub security: SecurityPosture,
136}
137
138impl EmbedderConfig {
139 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum EmbedderErrorCode {
156 BundleLoadFailed,
158 UnsupportedBundleSchema,
160 BundlePathInvalid,
162 ExecutorUnavailable,
164 RuntimeStopped,
166 TargetNotFound,
168 CompatibleLifecycleRequired,
170 CapabilityNotCompatible,
172 PlatformNotSupported,
174 InstanceNotFound,
176 InstanceNotRunning,
178}
179
180impl EmbedderErrorCode {
181 #[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#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct EmbedderError {
203 pub code: EmbedderErrorCode,
205 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum SubmitStatus {
225 Accepted,
227 Rejected,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct SubmitOutcome {
234 pub session_id: Option<String>,
236 pub status: SubmitStatus,
238 pub error: Option<EmbedderError>,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct CompatibleStartOutcome {
245 pub instance_id: Option<String>,
247 pub status: CompatibleLifecycleStatus,
249 pub error: Option<EmbedderError>,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct CompatibleLifecycleOutcome {
256 pub status: CompatibleLifecycleStatus,
258 pub error: Option<EmbedderError>,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum CompatibleLifecycleStatus {
265 Started,
267 Stopped,
269 Killed,
271 Error,
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub struct ShutdownOutcome {
278 pub killed_instances: usize,
280}
281
282pub type EventCallback = Box<dyn FnMut(&Value) + Send>;
284
285pub trait TraverseEmbedderApi {
291 fn submit(&mut self, target_id: &str, input: &Value) -> SubmitOutcome;
293
294 fn subscribe(&mut self, callback: EventCallback);
298
299 fn start_compatible(&mut self, capability_id: &str, input: &Value) -> CompatibleStartOutcome;
301
302 fn stop_compatible(
305 &mut self,
306 capability_id: &str,
307 instance_id: Option<&str>,
308 ) -> CompatibleLifecycleOutcome;
309
310 fn kill_compatible(
313 &mut self,
314 capability_id: &str,
315 instance_id: Option<&str>,
316 ) -> CompatibleLifecycleOutcome;
317
318 fn shutdown(&mut self) -> ShutdownOutcome;
321
322 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
351pub(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
680struct 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
737pub 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 #[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}