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