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