1mod workflows;
4pub use workflows::*;
5mod artifact_router;
6pub use artifact_router::*;
7pub mod data_store;
8pub mod durable_orchestration;
10pub mod events;
11pub mod executor;
12#[cfg(feature = "native-inference")]
16pub mod inference;
17pub mod parallel_proposal;
18pub mod placement;
19pub mod proposal;
20pub mod router;
21pub mod security;
22pub mod trace;
23
24use chrono::Utc;
25use events::{
26 EventBroker, EventCatalog, EventError, InProcessBroker, NoopRuntimeEventSink, RuntimeEventSink,
27 Subscription, SubscriptionPoll, TraverseEvent,
28};
29use executor::{
30 ArtifactType, CapabilityExecutor, ExecutorCapability, ExecutorError, ExecutorOutput,
31};
32use placement::{PlacementConstraintEvaluator, RuntimeSnapshot};
33use router::{CapabilityExecutorRegistry, PlacementRouter, RouterError, RouterRequest};
34use security::{
35 ArtifactVerificationFailure, ArtifactVerificationRecord, RuntimeIdentity,
36 RuntimeSecurityConfig, RuntimeWarning, derive_identity_from_jwt, verify_artifact,
37};
38use semver::Version;
39use serde::{Deserialize, Serialize};
40use serde_json::{Map, Value, json};
41use std::fmt;
42use std::fs;
43use std::path::Path;
44use std::sync::{Arc, Mutex};
45use trace::TraceStore;
46use traverse_contracts::{
47 EventReference, ExecutionTarget, HostApiAccess, Lifecycle, NetworkAccess,
48 NoOpUsageTelemetrySink, ServiceType, UsageTelemetrySink, ViolationRecord,
49};
50use traverse_registry::{
51 CapabilityRegistration, CapabilityRegistry, DiscoveryQuery, ImplementationKind, LookupScope,
52 ModelResolutionEvidence, RegistrationOutcome, RegistryFailure, RegistryScope, ResolutionError,
53 ResolveTelemetry, ResolvedCapability, WorkflowFailure, WorkflowRegistration,
54 WorkflowRegistrationOutcome, WorkflowRegistry, WorkspaceAppStateFailure,
55 WorkspaceApplicationRegistration, load_workspace_application_registries, resolve_dependencies,
56 resolve_version_range,
57};
58use uuid::Uuid;
59
60const RUNTIME_REQUEST_KIND: &str = "runtime_request";
61const RUNTIME_RESULT_KIND: &str = "runtime_result";
62const RUNTIME_STATE_EVENT_KIND: &str = "runtime_state_event";
63const RUNTIME_TRACE_KIND: &str = "runtime_trace";
64const RUNTIME_STATE_MACHINE_VALIDATION_KIND: &str = "runtime_state_machine_validation";
65const BROWSER_SUBSCRIPTION_REQUEST_KIND: &str = "browser_runtime_subscription_request";
66const BROWSER_SUBSCRIPTION_ERROR_KIND: &str = "browser_runtime_subscription_error";
67const BROWSER_SUBSCRIPTION_LIFECYCLE_KIND: &str = "browser_runtime_subscription_lifecycle";
68const BROWSER_SUBSCRIPTION_STATE_KIND: &str = "browser_runtime_subscription_state";
69const BROWSER_SUBSCRIPTION_TRACE_KIND: &str = "browser_runtime_subscription_trace_artifact";
70const BROWSER_SUBSCRIPTION_TERMINAL_KIND: &str = "browser_runtime_subscription_terminal";
71const SUPPORTED_SCHEMA_VERSION: &str = "1.0.0";
72const GOVERNING_SPEC: &str = "006-runtime-request-execution";
73const STATE_MACHINE_GOVERNING_SPEC: &str = "010-runtime-state-machine";
74const BROWSER_SUBSCRIPTION_GOVERNING_SPEC: &str = "013-browser-runtime-subscription";
75const EXECUTION_PREFIX: &str = "exec_";
76const TRACE_PREFIX: &str = "trace_";
77const RUNTIME_EXECUTION_EVENT_TYPE: &str = "dev.traverse.runtime.execution.completed";
78
79pub struct Runtime<E> {
80 registry: CapabilityRegistry,
81 workflow_registry: WorkflowRegistry,
82 applications: Vec<WorkspaceApplicationRegistration>,
83 executor: Arc<E>,
84 observability: RuntimeObservabilityConfig,
85 security: RuntimeSecurityConfig,
86 event_sink: Arc<dyn RuntimeEventSink>,
87 trace_store: Arc<Mutex<TraceStore>>,
88 event_broker: Arc<dyn EventBroker>,
89 usage_telemetry_sink: Arc<dyn UsageTelemetrySink>,
90}
91
92impl<E: Clone> Clone for Runtime<E> {
93 fn clone(&self) -> Self {
94 Self {
95 registry: self.registry.clone(),
96 workflow_registry: self.workflow_registry.clone(),
97 applications: self.applications.clone(),
98 executor: Arc::clone(&self.executor),
99 observability: self.observability.clone(),
100 security: self.security.clone(),
101 event_sink: Arc::clone(&self.event_sink),
102 trace_store: Arc::clone(&self.trace_store),
103 event_broker: Arc::clone(&self.event_broker),
104 usage_telemetry_sink: Arc::clone(&self.usage_telemetry_sink),
105 }
106 }
107}
108
109impl<E: fmt::Debug> fmt::Debug for Runtime<E> {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.debug_struct("Runtime")
112 .field("registry", &self.registry)
113 .field("workflow_registry", &self.workflow_registry)
114 .field("applications", &self.applications)
115 .field("executor", &self.executor)
116 .field("observability", &self.observability)
117 .field("security", &self.security)
118 .field("event_sink", &self.event_sink)
119 .field("trace_store", &self.trace_store)
120 .field("event_broker", &"Arc<dyn EventBroker>")
121 .field("usage_telemetry_sink", &"Arc<dyn UsageTelemetrySink>")
122 .finish()
123 }
124}
125
126impl<E> Runtime<E> {
127 #[must_use]
128 pub fn new(registry: CapabilityRegistry, executor: E) -> Self {
129 Self {
130 registry,
131 workflow_registry: WorkflowRegistry::new(),
132 applications: Vec::new(),
133 executor: Arc::new(executor),
134 observability: RuntimeObservabilityConfig::default(),
135 security: RuntimeSecurityConfig::default(),
136 event_sink: Arc::new(NoopRuntimeEventSink),
137 trace_store: Arc::new(Mutex::new(TraceStore::new())),
138 event_broker: default_event_broker(),
139 usage_telemetry_sink: Arc::new(NoOpUsageTelemetrySink),
140 }
141 }
142
143 #[must_use]
144 pub fn with_workflow_registry(mut self, workflow_registry: WorkflowRegistry) -> Self {
145 self.workflow_registry = workflow_registry;
146 self
147 }
148
149 #[must_use]
150 pub fn with_workspace_applications(
151 mut self,
152 applications: Vec<WorkspaceApplicationRegistration>,
153 ) -> Self {
154 self.applications = applications;
155 self
156 }
157
158 pub fn from_workspace_app_state(
166 workspace_root: &Path,
167 workspace_id: &str,
168 executor: E,
169 validator_version: &str,
170 ) -> Result<Self, WorkspaceAppStateFailure> {
171 let loaded =
172 load_workspace_application_registries(workspace_root, workspace_id, validator_version)?;
173 Ok(Self::new(loaded.capability_registry, executor)
174 .with_workflow_registry(loaded.workflow_registry)
175 .with_workspace_applications(loaded.applications))
176 }
177
178 #[must_use]
179 pub fn with_observability_config(mut self, observability: RuntimeObservabilityConfig) -> Self {
180 self.observability = observability;
181 self
182 }
183
184 #[must_use]
185 pub fn observability_config(&self) -> &RuntimeObservabilityConfig {
186 &self.observability
187 }
188
189 #[must_use]
190 pub fn with_security_config(mut self, security: RuntimeSecurityConfig) -> Self {
191 self.security = security;
192 self
193 }
194
195 #[must_use]
197 pub fn with_event_sink(mut self, event_sink: Arc<dyn RuntimeEventSink>) -> Self {
198 self.event_sink = event_sink;
199 self
200 }
201
202 #[must_use]
204 pub fn with_event_broker(mut self, event_broker: Arc<dyn EventBroker>) -> Self {
205 self.event_broker = event_broker;
206 self
207 }
208
209 #[must_use]
216 pub fn with_usage_telemetry_sink(mut self, sink: Arc<dyn UsageTelemetrySink>) -> Self {
217 self.usage_telemetry_sink = sink;
218 self
219 }
220
221 #[must_use]
223 pub fn with_trace_store(mut self, trace_store: Arc<Mutex<TraceStore>>) -> Self {
224 self.trace_store = trace_store;
225 self
226 }
227
228 #[must_use]
230 pub fn event_broker(&self) -> Arc<dyn EventBroker> {
231 Arc::clone(&self.event_broker)
232 }
233
234 #[must_use]
236 pub fn trace_store(&self) -> Arc<Mutex<TraceStore>> {
237 Arc::clone(&self.trace_store)
238 }
239
240 #[must_use]
241 pub fn security_config(&self) -> &RuntimeSecurityConfig {
242 &self.security
243 }
244
245 #[must_use]
247 pub fn capability_registry(&self) -> &CapabilityRegistry {
248 &self.registry
249 }
250
251 pub fn register_capability(
261 &mut self,
262 registration: CapabilityRegistration,
263 ) -> Result<RegistrationOutcome, RegistryFailure> {
264 self.registry.register(registration)
265 }
266
267 #[must_use]
269 pub fn workflow_registry(&self) -> &WorkflowRegistry {
270 &self.workflow_registry
271 }
272
273 #[must_use]
275 pub fn workspace_applications(&self) -> &[WorkspaceApplicationRegistration] {
276 self.applications.as_slice()
277 }
278
279 #[must_use]
281 pub fn workflow_registry_mut(&mut self) -> &mut WorkflowRegistry {
282 &mut self.workflow_registry
283 }
284
285 pub fn register_workflow(
292 &mut self,
293 registration: WorkflowRegistration,
294 ) -> Result<WorkflowRegistrationOutcome, WorkflowFailure> {
295 self.workflow_registry
296 .register(&self.registry, registration)
297 }
298
299 #[cfg(feature = "native-inference")]
307 pub fn execute_governed_model_dependency(
308 &self,
309 app_id: &str,
310 app_version: &str,
311 request: &inference::GovernedModelExecutionRequest,
312 ) -> Result<inference::GovernedModelExecutionOutcome, inference::GovernedModelExecutionError>
313 {
314 let Some(application) = self.applications.iter().find(|application| {
315 application.app_id == app_id && application.app_version == app_version
316 }) else {
317 return Err(inference::GovernedModelExecutionError::new(
318 inference::GovernedModelExecutionErrorCode::InterfaceNotDeclared,
319 "application registration was not loaded into this runtime",
320 ));
321 };
322 let Some(dependency) = application
323 .model_dependencies
324 .iter()
325 .find(|dependency| dependency.interface_id == request.interface_id)
326 else {
327 return Err(inference::GovernedModelExecutionError::new(
328 inference::GovernedModelExecutionErrorCode::InterfaceNotDeclared,
329 "requested inference interface is not declared by this application",
330 ));
331 };
332
333 inference::execute_governed_ollama_model_dependency(dependency, request)
334 }
335}
336
337pub trait LocalExecutor: Send + Sync {
338 fn execute(
345 &self,
346 capability: &ResolvedCapability,
347 input: &Value,
348 ) -> Result<LocalExecutionOutput, LocalExecutionFailure>;
349}
350
351#[derive(Debug, Clone, PartialEq)]
355pub struct LocalExecutionOutput {
356 pub value: Value,
357 pub emitted_events: Vec<TraverseEvent>,
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct LocalExecutionFailure {
362 pub code: LocalExecutionFailureCode,
363 pub message: String,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub enum LocalExecutionFailureCode {
368 ExecutionFailed,
370 Timeout,
373 InvalidInput,
376 ResourceExhausted,
379 ConstraintViolated,
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
385pub struct RuntimeRequest {
386 pub kind: String,
387 pub schema_version: String,
388 pub request_id: String,
389 pub intent: RuntimeIntent,
390 pub input: Value,
391 pub lookup: RuntimeLookup,
392 pub context: RuntimeContext,
393 pub governing_spec: String,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct RuntimeIntent {
398 #[serde(default)]
399 pub capability_id: Option<String>,
400 #[serde(default)]
401 pub capability_version: Option<String>,
402 #[serde(default)]
406 pub version_range: Option<String>,
407 #[serde(default)]
408 pub intent_key: Option<String>,
409}
410
411#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
412pub struct RuntimeLookup {
413 pub scope: RuntimeLookupScope,
414 pub allow_ambiguity: bool,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "snake_case")]
419pub enum RuntimeLookupScope {
420 PublicOnly,
421 PreferPrivate,
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425pub struct RuntimeContext {
426 pub requested_target: PlacementTarget,
427 #[serde(default)]
428 pub correlation_id: Option<String>,
429 #[serde(default)]
430 pub caller: Option<String>,
431 #[serde(default)]
432 pub traceparent: Option<String>,
433 #[serde(default)]
434 pub tracestate: Option<String>,
435 #[serde(default)]
436 pub metadata: Option<Value>,
437 #[serde(default)]
438 pub identity: Option<RuntimeIdentity>,
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
442pub struct RuntimeObservabilityConfig {
443 pub signals: OTelSignalConfig,
444 pub exporter: OTelExporterConfig,
445 pub deterministic_ids: bool,
446 #[serde(default)]
447 pub deterministic_seed: Option<String>,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451pub struct OTelSignalConfig {
452 pub traces_enabled: bool,
453 pub logs_enabled: bool,
454 pub metrics_enabled: bool,
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458pub struct OTelExporterConfig {
459 #[serde(default)]
460 pub endpoint: Option<String>,
461 pub protocol: OtlpProtocol,
462}
463
464impl Default for RuntimeObservabilityConfig {
465 fn default() -> Self {
466 Self {
467 signals: OTelSignalConfig {
468 traces_enabled: true,
469 logs_enabled: false,
470 metrics_enabled: false,
471 },
472 exporter: OTelExporterConfig {
473 endpoint: None,
474 protocol: OtlpProtocol::Http,
475 },
476 deterministic_ids: false,
477 deterministic_seed: None,
478 }
479 }
480}
481
482impl RuntimeObservabilityConfig {
483 #[must_use]
484 pub fn deterministic_test(seed: &str) -> Self {
485 Self {
486 deterministic_ids: true,
487 deterministic_seed: Some(seed.to_string()),
488 ..Self::default()
489 }
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
494#[serde(rename_all = "snake_case")]
495pub enum OtlpProtocol {
496 Http,
497 Grpc,
498}
499
500#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
501#[serde(rename_all = "snake_case")]
502pub enum PlacementTarget {
503 Local,
504 Browser,
505 Edge,
506 Cloud,
507 Worker,
508 Device,
509}
510
511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
512pub struct PlacementDecisionRecord {
513 pub requested_target: PlacementTarget,
514 #[serde(default)]
515 pub selected_target: Option<PlacementTarget>,
516 pub status: PlacementDecisionStatus,
517 pub reason: PlacementDecisionReason,
518 pub supported_executor_targets: Vec<PlacementTarget>,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(rename_all = "snake_case")]
523pub enum PlacementDecisionStatus {
524 NotAttempted,
525 Selected,
526 Unsupported,
527}
528
529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
530#[serde(rename_all = "snake_case")]
531pub enum PlacementDecisionReason {
532 SelectionNotReached,
533 RequestedTargetSelected,
534 RequestedTargetUnsupported,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538pub struct RuntimeStateEvent {
539 pub kind: String,
540 pub schema_version: String,
541 pub event_id: String,
542 pub execution_id: String,
543 pub request_id: String,
544 pub state: RuntimeState,
545 pub entered_at: String,
546 pub details: Value,
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(rename_all = "snake_case")]
551pub enum RuntimeState {
552 Idle,
553 LoadingRegistry,
554 Ready,
555 Discovering,
556 EvaluatingConstraints,
557 Selecting,
558 Executing,
559 EmittingEvents,
560 Completed,
561 Error,
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
565#[serde(rename_all = "snake_case")]
566pub enum RuntimeTransitionReasonCode {
567 RuntimeInitializationStarted,
568 RegistryLoaded,
569 RegistryLoadFailed,
570 RequestStarted,
571 CandidatesCollected,
572 NoMatch,
573 ConstraintsEvaluated,
574 ConstraintValidationFailed,
575 CandidateSelected,
576 SelectionFailed,
577 ExecutionSucceededWithEvents,
578 ExecutionSucceeded,
579 ExecutionFailed,
580 EventsEmitted,
581 EventEmissionFailed,
582 ExecutionClosed,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
586pub struct RuntimeTransitionRecord {
587 pub from_state: RuntimeState,
588 pub to_state: RuntimeState,
589 pub reason_code: RuntimeTransitionReasonCode,
590 pub occurred_at: String,
591 #[serde(default)]
592 pub request_id: Option<String>,
593 #[serde(default)]
594 pub execution_id: Option<String>,
595 #[serde(default)]
596 pub details: Option<Value>,
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
600pub struct RuntimeStateMachineValidationEvidence {
601 pub kind: String,
602 pub schema_version: String,
603 pub governing_spec: String,
604 pub validated_at: String,
605 pub status: RuntimeStateMachineValidationStatus,
606 pub checked_states: Vec<RuntimeState>,
607 pub checked_transitions: Vec<String>,
608 pub violations: Vec<Value>,
609}
610
611#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(rename_all = "snake_case")]
613pub enum RuntimeStateMachineValidationStatus {
614 Passed,
615 Failed,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct RuntimeTrace {
620 pub kind: String,
621 pub schema_version: String,
622 pub trace_id: String,
623 pub execution_id: String,
624 pub request_id: String,
625 pub governing_spec: String,
626 pub request: RuntimeRequest,
627 pub decision_evidence: TraceDecisionEvidence,
628 pub state_progression: TraceStateProgression,
629 pub terminal_outcome: TraceTerminalOutcome,
630 pub emitted_events: Vec<traverse_contracts::EventReference>,
631 #[serde(default)]
632 pub workflow_evidence: Option<WorkflowTraversalEvidence>,
633 #[serde(default)]
634 pub model_resolution: Vec<ModelResolutionEvidence>,
635 pub state_transitions: Vec<RuntimeTransitionRecord>,
636 pub state_machine_validation: RuntimeStateMachineValidationEvidence,
637 pub candidate_collection: CandidateCollectionRecord,
638 pub selection: SelectionRecord,
639 pub execution: ExecutionRecord,
640 pub result: TraceResultRecord,
641 pub otel_trace: OTelTraceRecord,
642}
643
644#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
645pub struct OTelTraceRecord {
646 pub trace_id: String,
647 #[serde(default)]
648 pub parent_traceparent: Option<String>,
649 #[serde(default)]
650 pub tracestate: Option<String>,
651 pub exporter: OTelExporterRecord,
652 pub spans: Vec<OTelSpanRecord>,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct OTelExporterRecord {
657 pub enabled: bool,
658 #[serde(default)]
659 pub endpoint: Option<String>,
660 pub protocol: OtlpProtocol,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664pub struct OTelSpanRecord {
665 pub trace_id: String,
666 pub span_id: String,
667 #[serde(default)]
668 pub parent_span_id: Option<String>,
669 pub name: String,
670 pub kind: OTelSpanKind,
671 pub status: OTelSpanStatus,
672 pub started_at: String,
673 pub ended_at: String,
674 pub attributes: Vec<OTelAttribute>,
675 #[serde(default)]
676 pub events: Vec<OTelSpanEvent>,
677}
678
679#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
680#[serde(rename_all = "snake_case")]
681pub enum OTelSpanKind {
682 Internal,
683}
684
685#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
686#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
687pub enum OTelSpanStatus {
688 Ok,
689 Error,
690}
691
692#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693pub struct OTelAttribute {
694 pub key: String,
695 pub value: Value,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct OTelSpanEvent {
700 pub name: String,
701 pub timestamp: String,
702 pub attributes: Vec<OTelAttribute>,
703}
704
705impl RuntimeTrace {
706 #[must_use]
708 pub fn with_model_resolution(mut self, evidence: Vec<ModelResolutionEvidence>) -> Self {
709 self.decision_evidence
710 .model_resolution
711 .clone_from(&evidence);
712 self.model_resolution = evidence;
713 self
714 }
715
716 #[must_use]
718 pub fn selected_capability_id(&self) -> Option<&str> {
719 self.selection.selected_capability_id.as_deref()
720 }
721
722 #[must_use]
724 pub fn errors(&self) -> Option<&RuntimeError> {
725 self.terminal_outcome.error.as_ref()
726 }
727
728 #[must_use]
730 pub fn emitted_events(&self) -> &[traverse_contracts::EventReference] {
731 self.emitted_events.as_slice()
732 }
733
734 #[must_use]
736 pub fn output(&self) -> Option<&serde_json::Value> {
737 self.result.output.as_ref()
738 }
739
740 #[must_use]
742 pub fn is_success(&self) -> bool {
743 self.terminal_outcome.runtime_status == RuntimeResultStatus::Completed
744 }
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748pub struct TraceDecisionEvidence {
749 pub candidate_collection: CandidateCollectionRecord,
750 pub selection: SelectionRecord,
751 #[serde(default)]
752 pub model_resolution: Vec<ModelResolutionEvidence>,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct TraceStateProgression {
757 pub state_events: Vec<RuntimeStateEvent>,
758 pub transitions: Vec<RuntimeTransitionRecord>,
759 pub validation: RuntimeStateMachineValidationEvidence,
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
763pub struct TraceTerminalOutcome {
764 pub runtime_status: RuntimeResultStatus,
765 pub execution_status: ExecutionStatus,
766 #[serde(default)]
767 pub failure_reason: Option<ExecutionFailureReason>,
768 #[serde(default)]
769 pub error: Option<RuntimeError>,
770}
771
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773pub struct CandidateCollectionRecord {
774 pub lookup_scope: RuntimeLookupScope,
775 pub candidates: Vec<RuntimeCandidate>,
776 pub rejected_candidates: Vec<RejectedRuntimeCandidate>,
777}
778
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
780pub struct RuntimeCandidate {
781 pub scope: RuntimeRegistryScope,
782 pub capability_id: String,
783 pub capability_version: String,
784 pub artifact_ref: String,
785 pub implementation_kind: RuntimeImplementationKind,
786 pub lifecycle: RuntimeLifecycle,
787 pub reason: CandidateReason,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791pub struct RejectedRuntimeCandidate {
792 pub capability_id: String,
793 pub capability_version: String,
794 pub scope: RuntimeRegistryScope,
795 pub reason: RejectedCandidateReason,
796}
797
798#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(rename_all = "snake_case")]
800pub enum CandidateReason {
801 ExactMatch,
802 IntentMatch,
803}
804
805#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
806#[serde(rename_all = "snake_case")]
807pub enum RejectedCandidateReason {
808 WrongScope,
809 NotRunnableLocally,
810 LifecycleNotRunnable,
811 InputContractInvalid,
812 ArtifactMissing,
813 SupersededByPrivateOverlay,
814 NotSelectedAfterOrdering,
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818pub struct SelectionRecord {
819 pub status: SelectionStatus,
820 #[serde(default)]
821 pub selected_capability_id: Option<String>,
822 #[serde(default)]
823 pub selected_capability_version: Option<String>,
824 #[serde(default)]
825 pub failure_reason: Option<SelectionFailureReason>,
826 #[serde(default)]
827 pub remaining_candidates: Vec<RuntimeCandidate>,
828}
829
830#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
831#[serde(rename_all = "snake_case")]
832pub enum SelectionStatus {
833 Selected,
834 NoMatch,
835 Ambiguous,
836 InvalidRequest,
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
840#[serde(rename_all = "snake_case")]
841pub enum SelectionFailureReason {
842 InvalidRequest,
843 NoMatch,
844 Ambiguous,
845 NotRunnable,
846}
847
848#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
849pub struct ExecutionRecord {
850 pub placement: PlacementDecisionRecord,
851 pub placement_target: PlacementTarget,
852 pub status: ExecutionStatus,
853 #[serde(default)]
854 pub artifact_ref: Option<String>,
855 #[serde(default)]
856 pub started_at: Option<String>,
857 #[serde(default)]
858 pub completed_at: Option<String>,
859 #[serde(default)]
860 pub output_digest: Option<String>,
861 #[serde(default)]
862 pub failure_reason: Option<ExecutionFailureReason>,
863 #[serde(default)]
864 pub artifact_verification: Option<ArtifactVerificationRecord>,
865 #[serde(default)]
866 pub identity: Option<RuntimeIdentity>,
867}
868
869#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
870#[serde(rename_all = "snake_case")]
871pub enum ExecutionStatus {
872 NotStarted,
873 Succeeded,
874 Failed,
875}
876
877#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
878#[serde(rename_all = "snake_case")]
879pub enum ExecutionFailureReason {
880 ContractInputInvalid,
881 ArtifactMissing,
882 ArtifactNotRunnable,
883 PlacementUnsupported,
884 ExecutionFailed,
885 ContractOutputInvalid,
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
889pub struct TraceResultRecord {
890 pub status: RuntimeResultStatus,
891 #[serde(default)]
892 pub output: Option<serde_json::Value>,
893 #[serde(default)]
894 pub error: Option<RuntimeError>,
895 #[serde(default)]
896 pub warnings: Vec<RuntimeWarning>,
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
900pub struct RuntimeResult {
901 pub kind: String,
902 pub schema_version: String,
903 pub execution_id: String,
904 pub request_id: String,
905 pub status: RuntimeResultStatus,
906 pub trace_ref: String,
907 #[serde(default)]
908 pub output: Option<Value>,
909 #[serde(default)]
910 pub error: Option<RuntimeError>,
911 #[serde(default)]
912 pub warnings: Vec<RuntimeWarning>,
913}
914
915#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
916#[serde(rename_all = "snake_case")]
917pub enum RuntimeResultStatus {
918 Completed,
919 Error,
920}
921
922#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
923pub struct RuntimeError {
924 pub code: RuntimeErrorCode,
925 pub message: String,
926 pub details: Value,
927}
928
929#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
930#[serde(rename_all = "snake_case")]
931pub enum RuntimeErrorCode {
932 RequestInvalid,
933 CapabilityNotFound,
934 CapabilityAmbiguous,
935 CapabilityNotRunnable,
936 PlacementUnsupported,
937 ArtifactMissing,
938 ExecutionFailed,
939 OutputValidationFailed,
940 ContractViolation,
941}
942
943#[derive(Debug, Clone, PartialEq, Eq)]
944pub struct RuntimeExecutionOutcome {
945 pub result: RuntimeResult,
946 pub trace: RuntimeTrace,
947 pub state_events: Vec<RuntimeStateEvent>,
948}
949
950#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
951pub struct BrowserRuntimeSubscriptionRequest {
952 pub kind: String,
953 pub schema_version: String,
954 pub governing_spec: String,
955 #[serde(default)]
956 pub request_id: Option<String>,
957 #[serde(default)]
958 pub execution_id: Option<String>,
959}
960
961#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
962pub struct BrowserRuntimeSubscriptionErrorMessage {
963 pub kind: String,
964 pub schema_version: String,
965 pub sequence: u64,
966 pub code: BrowserRuntimeSubscriptionErrorCode,
967 pub message: String,
968}
969
970#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
971#[serde(rename_all = "snake_case")]
972pub enum BrowserRuntimeSubscriptionErrorCode {
973 InvalidRequest,
974 NotFound,
975 UnsupportedOperation,
976}
977
978#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
979pub struct BrowserRuntimeSubscriptionLifecycleMessage {
980 pub kind: String,
981 pub schema_version: String,
982 pub sequence: u64,
983 pub request_id: String,
984 pub execution_id: String,
985 pub status: BrowserRuntimeSubscriptionLifecycleStatus,
986}
987
988#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
989#[serde(rename_all = "snake_case")]
990pub enum BrowserRuntimeSubscriptionLifecycleStatus {
991 SubscriptionEstablished,
992 StreamCompleted,
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
996pub struct BrowserRuntimeSubscriptionStateMessage {
997 pub kind: String,
998 pub schema_version: String,
999 pub sequence: u64,
1000 pub state_event: RuntimeStateEvent,
1001}
1002
1003#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1004pub struct BrowserRuntimeSubscriptionTraceArtifactMessage {
1005 pub kind: String,
1006 pub schema_version: String,
1007 pub sequence: u64,
1008 pub trace: RuntimeTrace,
1009}
1010
1011#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1012pub struct BrowserRuntimeSubscriptionTerminalMessage {
1013 pub kind: String,
1014 pub schema_version: String,
1015 pub sequence: u64,
1016 pub result: RuntimeResult,
1017}
1018
1019#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1020pub enum BrowserRuntimeSubscriptionMessage {
1021 Error(BrowserRuntimeSubscriptionErrorMessage),
1022 Lifecycle(Box<BrowserRuntimeSubscriptionLifecycleMessage>),
1023 State(Box<BrowserRuntimeSubscriptionStateMessage>),
1024 TraceArtifact(Box<BrowserRuntimeSubscriptionTraceArtifactMessage>),
1025 StreamTerminal(Box<BrowserRuntimeSubscriptionTerminalMessage>),
1026}
1027
1028#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1029#[serde(rename_all = "snake_case")]
1030pub enum RuntimeRegistryScope {
1031 Public,
1032 Private,
1033}
1034
1035#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1036#[serde(rename_all = "snake_case")]
1037pub enum RuntimeImplementationKind {
1038 Executable,
1039 Workflow,
1040}
1041
1042#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1043#[serde(rename_all = "snake_case")]
1044pub enum RuntimeLifecycle {
1045 Draft,
1046 Active,
1047 Deprecated,
1048 Retired,
1049 Archived,
1050}
1051
1052#[derive(Debug, Clone, PartialEq, Eq)]
1053pub struct RequestParseFailure {
1054 pub message: String,
1055}
1056
1057impl fmt::Display for RequestParseFailure {
1058 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1059 formatter.write_str(&self.message)
1060 }
1061}
1062
1063impl std::error::Error for RequestParseFailure {}
1064
1065pub fn parse_runtime_request(json: &str) -> Result<RuntimeRequest, RequestParseFailure> {
1072 serde_json::from_str::<RuntimeRequest>(json).map_err(|error| RequestParseFailure {
1073 message: error.to_string(),
1074 })
1075}
1076
1077#[must_use]
1078pub fn browser_subscription_messages(
1079 request: &BrowserRuntimeSubscriptionRequest,
1080 outcome: &RuntimeExecutionOutcome,
1081) -> Vec<BrowserRuntimeSubscriptionMessage> {
1082 if let Some(error) = validate_browser_subscription_request(request) {
1083 return vec![BrowserRuntimeSubscriptionMessage::Error(error)];
1084 }
1085
1086 if !subscription_targets_outcome(request, outcome) {
1087 return vec![BrowserRuntimeSubscriptionMessage::Error(
1088 BrowserRuntimeSubscriptionErrorMessage {
1089 kind: BROWSER_SUBSCRIPTION_ERROR_KIND.to_string(),
1090 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1091 sequence: 0,
1092 code: BrowserRuntimeSubscriptionErrorCode::NotFound,
1093 message: "subscription target did not match the supplied execution outcome"
1094 .to_string(),
1095 },
1096 )];
1097 }
1098
1099 let mut sequence = 0_u64;
1100 let mut messages = Vec::new();
1101 messages.push(BrowserRuntimeSubscriptionMessage::Lifecycle(Box::new(
1102 BrowserRuntimeSubscriptionLifecycleMessage {
1103 kind: BROWSER_SUBSCRIPTION_LIFECYCLE_KIND.to_string(),
1104 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1105 sequence,
1106 request_id: outcome.result.request_id.clone(),
1107 execution_id: outcome.result.execution_id.clone(),
1108 status: BrowserRuntimeSubscriptionLifecycleStatus::SubscriptionEstablished,
1109 },
1110 )));
1111 sequence += 1;
1112
1113 for state_event in &outcome.state_events {
1114 messages.push(BrowserRuntimeSubscriptionMessage::State(Box::new(
1115 BrowserRuntimeSubscriptionStateMessage {
1116 kind: BROWSER_SUBSCRIPTION_STATE_KIND.to_string(),
1117 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1118 sequence,
1119 state_event: state_event.clone(),
1120 },
1121 )));
1122 sequence += 1;
1123 }
1124
1125 messages.push(BrowserRuntimeSubscriptionMessage::TraceArtifact(Box::new(
1126 BrowserRuntimeSubscriptionTraceArtifactMessage {
1127 kind: BROWSER_SUBSCRIPTION_TRACE_KIND.to_string(),
1128 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1129 sequence,
1130 trace: outcome.trace.clone(),
1131 },
1132 )));
1133 sequence += 1;
1134
1135 messages.push(BrowserRuntimeSubscriptionMessage::StreamTerminal(Box::new(
1136 BrowserRuntimeSubscriptionTerminalMessage {
1137 kind: BROWSER_SUBSCRIPTION_TERMINAL_KIND.to_string(),
1138 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1139 sequence,
1140 result: outcome.result.clone(),
1141 },
1142 )));
1143 sequence += 1;
1144
1145 messages.push(BrowserRuntimeSubscriptionMessage::Lifecycle(Box::new(
1146 BrowserRuntimeSubscriptionLifecycleMessage {
1147 kind: BROWSER_SUBSCRIPTION_LIFECYCLE_KIND.to_string(),
1148 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1149 sequence,
1150 request_id: outcome.result.request_id.clone(),
1151 execution_id: outcome.result.execution_id.clone(),
1152 status: BrowserRuntimeSubscriptionLifecycleStatus::StreamCompleted,
1153 },
1154 )));
1155
1156 messages
1157}
1158
1159fn validate_browser_subscription_request(
1160 request: &BrowserRuntimeSubscriptionRequest,
1161) -> Option<BrowserRuntimeSubscriptionErrorMessage> {
1162 if request.kind != BROWSER_SUBSCRIPTION_REQUEST_KIND {
1163 return Some(browser_subscription_error(
1164 BrowserRuntimeSubscriptionErrorCode::InvalidRequest,
1165 "kind must equal browser_runtime_subscription_request",
1166 ));
1167 }
1168 if request.schema_version != SUPPORTED_SCHEMA_VERSION {
1169 return Some(browser_subscription_error(
1170 BrowserRuntimeSubscriptionErrorCode::InvalidRequest,
1171 "schema_version must equal 1.0.0",
1172 ));
1173 }
1174 if request.governing_spec != BROWSER_SUBSCRIPTION_GOVERNING_SPEC {
1175 return Some(browser_subscription_error(
1176 BrowserRuntimeSubscriptionErrorCode::InvalidRequest,
1177 "governing_spec must equal 013-browser-runtime-subscription",
1178 ));
1179 }
1180
1181 match (&request.request_id, &request.execution_id) {
1182 (Some(request_id), None) if non_empty(request_id) => None,
1183 (None, Some(execution_id)) if non_empty(execution_id) => None,
1184 (Some(_), Some(_)) => Some(browser_subscription_error(
1185 BrowserRuntimeSubscriptionErrorCode::InvalidRequest,
1186 "exactly one target selector must be supplied",
1187 )),
1188 _ => Some(browser_subscription_error(
1189 BrowserRuntimeSubscriptionErrorCode::InvalidRequest,
1190 "subscription request must include request_id or execution_id",
1191 )),
1192 }
1193}
1194
1195fn subscription_targets_outcome(
1196 request: &BrowserRuntimeSubscriptionRequest,
1197 outcome: &RuntimeExecutionOutcome,
1198) -> bool {
1199 match (&request.request_id, &request.execution_id) {
1200 (Some(request_id), None) => request_id == &outcome.result.request_id,
1201 (None, Some(execution_id)) => execution_id == &outcome.result.execution_id,
1202 _ => false,
1203 }
1204}
1205
1206fn browser_subscription_error(
1207 code: BrowserRuntimeSubscriptionErrorCode,
1208 message: &str,
1209) -> BrowserRuntimeSubscriptionErrorMessage {
1210 BrowserRuntimeSubscriptionErrorMessage {
1211 kind: BROWSER_SUBSCRIPTION_ERROR_KIND.to_string(),
1212 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1213 sequence: 0,
1214 code,
1215 message: message.to_string(),
1216 }
1217}
1218
1219impl<E> Runtime<E>
1220where
1221 E: LocalExecutor,
1222{
1223 #[must_use]
1225 pub fn execute(&self, request: RuntimeRequest) -> RuntimeExecutionOutcome {
1226 let identity = request.context.identity.clone();
1227 let (attempt, mut emitter) = begin_attempt(request, self.observability.clone());
1228 emitter.push(
1229 RuntimeState::Discovering,
1230 RuntimeTransitionReasonCode::RequestStarted,
1231 json!({
1232 "lookup_scope": attempt.request.lookup.scope,
1233 "identity": attempt.request.context.identity,
1234 }),
1235 );
1236
1237 let mut outcome = if let Some(error) = validate_request(&attempt.request) {
1238 invalid_request_outcome(attempt, emitter, error)
1239 } else {
1240 let resolution = self.resolve_candidates(&attempt.request, &mut emitter);
1241
1242 if resolution.eligible.is_empty() {
1243 no_eligible_outcome(attempt, emitter, resolution.collection)
1244 } else if resolution.eligible.len() > 1 {
1245 ambiguous_outcome(attempt, emitter, resolution)
1246 } else {
1247 let mut eligible = resolution.eligible;
1248 let selected = eligible.remove(0);
1249 let selection = SelectionRecord {
1250 status: SelectionStatus::Selected,
1251 selected_capability_id: Some(selected.record.id.clone()),
1252 selected_capability_version: Some(selected.record.version.clone()),
1253 failure_reason: None,
1254 remaining_candidates: Vec::new(),
1255 };
1256
1257 self.execute_selected(
1258 attempt,
1259 emitter,
1260 resolution.collection,
1261 selection,
1262 &selected,
1263 )
1264 }
1265 };
1266
1267 self.emit_execution_lifecycle_event(&mut outcome, identity.as_ref());
1268 outcome
1269 }
1270
1271 fn emit_execution_lifecycle_event(
1272 &self,
1273 outcome: &mut RuntimeExecutionOutcome,
1274 identity: Option<&RuntimeIdentity>,
1275 ) {
1276 let event = TraverseEvent {
1277 id: Uuid::new_v4().to_string(),
1278 source: "traverse-runtime".to_string(),
1279 event_type: RUNTIME_EXECUTION_EVENT_TYPE.to_string(),
1280 datacontenttype: "application/json".to_string(),
1281 time: Utc::now().to_rfc3339(),
1282 data: json!({
1283 "execution_id": outcome.result.execution_id,
1284 "request_id": outcome.result.request_id,
1285 "status": outcome.result.status,
1286 "trace_ref": outcome.result.trace_ref,
1287 }),
1288 owner: "traverse-runtime".to_string(),
1289 version: SUPPORTED_SCHEMA_VERSION.to_string(),
1290 lifecycle_status: events::LifecycleStatus::Active,
1291 deduplication_id: Some(outcome.result.execution_id.clone()),
1292 ordering_scope: Some(outcome.result.request_id.clone()),
1293 correlation_id: Some(outcome.result.request_id.clone()),
1294 causation_id: Some(outcome.result.request_id.clone()),
1295 subject_id: identity.map(|identity| identity.subject_id.clone()),
1296 actor_id: identity.and_then(|identity| identity.actor_id.clone()),
1297 };
1298
1299 if let Err(error) = self.event_sink.emit(event) {
1300 let warning = RuntimeWarning {
1301 code: "runtime_event_sink_delivery_failed".to_string(),
1302 message: error.to_string(),
1303 };
1304 outcome.result.warnings.push(warning.clone());
1305 outcome.trace.result.warnings.push(warning);
1306 }
1307 }
1308
1309 fn collect_candidates(
1310 &self,
1311 request: &RuntimeRequest,
1312 _reason: CandidateReason,
1313 ) -> Vec<ResolvedCapability> {
1314 let lookup_scope = map_lookup_scope(request.lookup.scope);
1315
1316 if is_exact_target(&request.intent) {
1318 return request
1319 .intent
1320 .capability_id
1321 .as_deref()
1322 .zip(request.intent.capability_version.as_deref())
1323 .and_then(|(id, version)| self.registry.find_exact(lookup_scope, id, version))
1324 .into_iter()
1325 .collect();
1326 }
1327
1328 if let (Some(capability_id), Some(range_str)) = (
1330 request.intent.capability_id.as_deref(),
1331 request.intent.version_range.as_deref(),
1332 ) && non_empty(capability_id)
1333 && non_empty(range_str)
1334 {
1335 let resolved_at = Utc::now().to_rfc3339();
1336 return match resolve_version_range(
1337 &self.registry,
1338 capability_id,
1339 range_str,
1340 lookup_scope,
1341 Some(ResolveTelemetry {
1342 sink: self.usage_telemetry_sink.as_ref(),
1343 resolved_at: &resolved_at,
1344 }),
1345 ) {
1346 Ok(resolved) => {
1347 let entry_lookup = match resolved.scope {
1348 RegistryScope::Public => LookupScope::PublicOnly,
1349 RegistryScope::Private => LookupScope::PreferPrivate,
1350 };
1351 self.registry
1352 .find_exact(entry_lookup, &resolved.capability_id, &resolved.version)
1353 .into_iter()
1354 .collect()
1355 }
1356 Err(_) => Vec::new(),
1357 };
1358 }
1359
1360 let target = request
1362 .intent
1363 .capability_id
1364 .as_deref()
1365 .or(request.intent.intent_key.as_deref())
1366 .unwrap_or_default();
1367
1368 self.registry
1369 .discover(lookup_scope, &DiscoveryQuery::default())
1370 .into_iter()
1371 .filter(|entry| entry.id == target)
1372 .filter_map(|entry| {
1373 let scope = match entry.scope {
1374 traverse_registry::RegistryScope::Public => LookupScope::PublicOnly,
1375 traverse_registry::RegistryScope::Private => LookupScope::PreferPrivate,
1376 };
1377 self.registry.find_exact(scope, &entry.id, &entry.version)
1378 })
1379 .collect()
1380 }
1381
1382 fn resolve_candidates(
1383 &self,
1384 request: &RuntimeRequest,
1385 emitter: &mut StateEmitter,
1386 ) -> CandidateResolution {
1387 let candidate_reason = if is_exact_target(&request.intent) {
1388 CandidateReason::ExactMatch
1389 } else {
1390 CandidateReason::IntentMatch
1391 };
1392
1393 let discovered = self.collect_candidates(request, candidate_reason);
1394 if !discovered.is_empty() {
1395 emitter.push(
1396 RuntimeState::EvaluatingConstraints,
1397 RuntimeTransitionReasonCode::CandidatesCollected,
1398 json!({"candidate_count": discovered.len()}),
1399 );
1400 }
1401
1402 let mut eligible = Vec::new();
1403 let mut rejected = Vec::new();
1404 for candidate in discovered {
1405 match evaluate_candidate(candidate) {
1406 CandidateEvaluation::Eligible(capability) => eligible.push(capability),
1407 CandidateEvaluation::Rejected(candidate, reason) => {
1408 rejected.push(RejectedRuntimeCandidate {
1409 capability_id: candidate.record.id.clone(),
1410 capability_version: candidate.record.version.clone(),
1411 scope: map_registry_scope(candidate.record.scope),
1412 reason,
1413 });
1414 }
1415 }
1416 }
1417
1418 if !eligible.is_empty() {
1419 emitter.push(
1420 RuntimeState::Selecting,
1421 RuntimeTransitionReasonCode::ConstraintsEvaluated,
1422 json!({
1423 "eligible_candidates": eligible.len(),
1424 "rejected_candidates": rejected.len()
1425 }),
1426 );
1427 }
1428
1429 CandidateResolution {
1430 eligible: eligible.clone(),
1431 collection: CandidateCollectionRecord {
1432 lookup_scope: request.lookup.scope,
1433 candidates: eligible
1434 .iter()
1435 .map(|capability| runtime_candidate(capability, candidate_reason))
1436 .collect(),
1437 rejected_candidates: rejected,
1438 },
1439 candidate_reason,
1440 }
1441 }
1442
1443 #[allow(clippy::too_many_lines)]
1444 fn execute_selected(
1445 &self,
1446 attempt: AttemptContext,
1447 emitter: StateEmitter,
1448 candidate_collection: CandidateCollectionRecord,
1449 selection: SelectionRecord,
1450 selected: &ResolvedCapability,
1451 ) -> RuntimeExecutionOutcome {
1452 let mut context = ExecutionContext {
1453 attempt,
1454 emitter,
1455 candidate_collection,
1456 selection,
1457 };
1458 let requested_target = context.attempt.request.context.requested_target;
1459
1460 if contains_drafts_segment(&selected.record.contract_path) {
1461 let violation = ViolationRecord::new(
1462 "draft_artifact_not_executable",
1463 selected.record.contract_path.clone(),
1464 "draft artifacts are quarantined under drafts/ and must not be executable",
1465 );
1466 let error = runtime_error(
1467 RuntimeErrorCode::ContractViolation,
1468 "draft artifacts are not executable",
1469 json!({"violations": [violation]}),
1470 );
1471 return pre_execution_failure_outcome(
1472 context,
1473 PreExecutionFailure {
1474 artifact_ref: Some(selected.record.artifact_ref.clone()),
1475 failure_reason: ExecutionFailureReason::ArtifactNotRunnable,
1476 placement: placement_not_attempted(
1477 requested_target,
1478 PlacementDecisionReason::SelectionNotReached,
1479 ),
1480 error,
1481 artifact_verification: None,
1482 },
1483 );
1484 }
1485
1486 let placement = match resolve_placement(requested_target) {
1487 Ok(placement) => placement,
1488 Err(error) => {
1489 return pre_execution_failure_outcome(
1490 context,
1491 PreExecutionFailure {
1492 artifact_ref: Some(selected.record.artifact_ref.clone()),
1493 failure_reason: ExecutionFailureReason::PlacementUnsupported,
1494 placement: placement_not_attempted(
1495 requested_target,
1496 PlacementDecisionReason::RequestedTargetUnsupported,
1497 ),
1498 error,
1499 artifact_verification: None,
1500 },
1501 );
1502 }
1503 };
1504
1505 let artifact_bytes = match load_artifact_bytes_for_verification(selected) {
1506 Ok(bytes) => bytes,
1507 Err(error) => {
1508 return pre_execution_failure_outcome(
1509 context,
1510 PreExecutionFailure {
1511 artifact_ref: Some(selected.record.artifact_ref.clone()),
1512 failure_reason: ExecutionFailureReason::ArtifactMissing,
1513 placement,
1514 error,
1515 artifact_verification: None,
1516 },
1517 );
1518 }
1519 };
1520
1521 match verify_artifact(selected, &artifact_bytes, &self.security) {
1522 Ok(record) => {
1523 if record.warning_code.is_some() {
1524 context.attempt.warnings.push(RuntimeWarning {
1525 code: record.warning_code.clone().unwrap_or_default(),
1526 message: "unsigned local/dev artifact allowed by development security mode"
1527 .to_string(),
1528 });
1529 }
1530 context.attempt.artifact_verification = Some(record);
1531 }
1532 Err(error) => {
1533 let record = error.record().clone();
1534 let runtime_error = artifact_verification_runtime_error(&error);
1535 return pre_execution_failure_outcome(
1536 context,
1537 PreExecutionFailure {
1538 artifact_ref: Some(selected.record.artifact_ref.clone()),
1539 failure_reason: ExecutionFailureReason::ArtifactNotRunnable,
1540 placement,
1541 error: runtime_error,
1542 artifact_verification: Some(record),
1543 },
1544 );
1545 }
1546 }
1547
1548 let lookup_scope = map_lookup_scope(context.attempt.request.lookup.scope);
1551 if let Err(dep_error) = resolve_dependencies(
1552 &self.registry,
1553 &selected.record.id,
1554 &selected.contract.dependencies,
1555 lookup_scope,
1556 ) {
1557 let (detail_id, detail_version) = match &dep_error {
1558 ResolutionError::MissingDependency {
1559 capability_id,
1560 required_version,
1561 } => (capability_id.clone(), required_version.clone()),
1562 ResolutionError::CircularDependency { cycle } => {
1563 (cycle.join(" -> "), String::new())
1564 }
1565 ResolutionError::MaxTransitiveDepthExceeded { depth, chain } => {
1566 (format!("depth={depth}"), chain.join(" -> "))
1567 }
1568 };
1569 let error = runtime_error(
1570 RuntimeErrorCode::CapabilityNotFound,
1571 "dependency resolution failed before execution",
1572 serde_json::json!({
1573 "dependency_id": detail_id,
1574 "required_version": detail_version,
1575 }),
1576 );
1577 let artifact_verification = context.attempt.artifact_verification.clone();
1578 return pre_execution_failure_outcome(
1579 context,
1580 PreExecutionFailure {
1581 artifact_ref: Some(selected.record.artifact_ref.clone()),
1582 failure_reason: ExecutionFailureReason::ArtifactMissing,
1583 placement,
1584 error,
1585 artifact_verification,
1586 },
1587 );
1588 }
1589
1590 if let Err(error) = validate_payload_against_contract(
1591 &context.attempt.request.input,
1592 &selected.contract.inputs.schema,
1593 RuntimeErrorCode::RequestInvalid,
1594 "runtime request input does not satisfy the selected capability input contract",
1595 ) {
1596 let artifact_verification = context.attempt.artifact_verification.clone();
1597 return pre_execution_failure_outcome(
1598 context,
1599 PreExecutionFailure {
1600 artifact_ref: Some(selected.record.artifact_ref.clone()),
1601 failure_reason: ExecutionFailureReason::ContractInputInvalid,
1602 placement,
1603 error,
1604 artifact_verification,
1605 },
1606 );
1607 }
1608
1609 self.execute_started_selection(context, selected, placement)
1610 }
1611
1612 fn execute_started_selection(
1613 &self,
1614 mut context: ExecutionContext,
1615 selected: &ResolvedCapability,
1616 placement: PlacementDecisionRecord,
1617 ) -> RuntimeExecutionOutcome {
1618 let identity = context.attempt.request.context.identity.clone();
1619 let started_execution =
1620 start_selected_execution(&mut context.emitter, selected, placement, identity.as_ref());
1621 if selected.record.implementation_kind == ImplementationKind::Workflow {
1622 return self.execute_workflow_capability(context, selected, started_execution);
1623 }
1624
1625 let artifact_type = artifact_type_for(selected);
1626 let executor_capability = executor_capability_for(selected, artifact_type.clone());
1627 let bridge = BoundLocalExecutor {
1628 executor: Arc::clone(&self.executor),
1629 selected: selected.clone(),
1630 };
1631
1632 let router = PlacementRouter::new(
1633 PlacementConstraintEvaluator,
1634 CapabilityExecutorRegistry::new(),
1635 Arc::clone(&self.trace_store),
1636 Arc::clone(&self.event_broker),
1637 );
1638
1639 let target_hint = execution_target_from_placement(
1640 started_execution
1641 .placement
1642 .selected_target
1643 .unwrap_or(started_execution.placement.requested_target),
1644 );
1645
1646 let router_request = RouterRequest {
1647 capability_id: selected.record.id.clone(),
1648 artifact_type,
1649 contract: selected.contract.clone(),
1650 target_hint: Some(target_hint),
1651 runtime_snapshot: idle_runtime_snapshot(),
1652 input: context.attempt.request.input.clone(),
1653 executor_capability,
1654 trace_id_override: Some(context.attempt.trace_id.clone()),
1655 };
1656
1657 let router_result = router.execute_with_executor(router_request, &bridge);
1658 match router_result {
1659 Ok(response) => {
1660 if let Err(error) = validate_payload_against_contract(
1661 &response.output,
1662 &selected.contract.outputs.schema,
1663 RuntimeErrorCode::OutputValidationFailed,
1664 "executor output does not satisfy the selected capability output contract",
1665 ) {
1666 return execution_failure_outcome(
1667 context,
1668 ExecutionFailureState {
1669 artifact_ref: selected.record.artifact_ref.clone(),
1670 started_at: started_execution.started_at,
1671 placement: started_execution.placement,
1672 failure_reason: ExecutionFailureReason::ContractOutputInvalid,
1673 },
1674 error,
1675 Vec::new(),
1676 None,
1677 );
1678 }
1679
1680 let emitted_events: Vec<EventReference> = response
1686 .emitted_events
1687 .iter()
1688 .map(|event| EventReference {
1689 event_id: event.event_type.clone(),
1690 version: event.version.clone(),
1691 })
1692 .collect();
1693 successful_execution_outcome(
1694 context,
1695 selected,
1696 started_execution,
1697 response.output,
1698 emitted_events,
1699 None,
1700 )
1701 }
1702 Err(error) => {
1703 let (runtime_error, failure_reason, emitted_events) = map_router_error(&error);
1704 execution_failure_outcome(
1705 context,
1706 ExecutionFailureState {
1707 artifact_ref: selected.record.artifact_ref.clone(),
1708 started_at: started_execution.started_at,
1709 placement: started_execution.placement,
1710 failure_reason,
1711 },
1712 runtime_error,
1713 emitted_events,
1714 None,
1715 )
1716 }
1717 }
1718 }
1719}
1720
1721struct BoundLocalExecutor<E> {
1723 executor: Arc<E>,
1724 selected: ResolvedCapability,
1725}
1726
1727impl<E> CapabilityExecutor for BoundLocalExecutor<E>
1728where
1729 E: LocalExecutor,
1730{
1731 fn execute(
1732 &self,
1733 _capability: &ExecutorCapability,
1734 input: &Value,
1735 ) -> Result<ExecutorOutput, ExecutorError> {
1736 let output = self
1737 .executor
1738 .execute(&self.selected, input)
1739 .map_err(|failure| ExecutorError::ExecutionFailed(failure.message))?;
1740 validate_natively_emitted_events(&self.selected.contract, &output.emitted_events)
1741 .map_err(ExecutorError::ExecutionFailed)?;
1742 Ok(ExecutorOutput {
1743 value: output.value,
1744 emitted_events: output.emitted_events,
1745 connector_invocation_evidence: Vec::new(),
1746 })
1747 }
1748}
1749
1750fn validate_natively_emitted_events(
1760 contract: &traverse_contracts::CapabilityContract,
1761 emitted_events: &[TraverseEvent],
1762) -> Result<(), String> {
1763 if emitted_events.is_empty() {
1764 return Ok(());
1765 }
1766 if contract.service_type != ServiceType::Subscribable {
1767 return Err(format!(
1768 "capability '{}' emitted events but its service_type is not Subscribable",
1769 contract.id
1770 ));
1771 }
1772 for event in emitted_events {
1773 let declared = contract
1774 .emits
1775 .iter()
1776 .any(|decl| decl.event_id == event.event_type && decl.version == event.version);
1777 if !declared {
1778 return Err(format!(
1779 "capability '{}' emitted an undeclared event {}@{}",
1780 contract.id, event.event_type, event.version
1781 ));
1782 }
1783 }
1784 Ok(())
1785}
1786
1787#[derive(Debug, Default)]
1789struct DiscardEventBroker;
1790
1791impl EventBroker for DiscardEventBroker {
1792 fn publish(&self, _event: TraverseEvent) -> Result<(), EventError> {
1793 Ok(())
1794 }
1795
1796 fn subscribe(&self, event_type: &str, _from_cursor: &str) -> Result<Subscription, EventError> {
1797 Err(EventError::UnregisteredEventType(event_type.to_string()))
1798 }
1799
1800 fn subscribe_for_subject(
1801 &self,
1802 event_type: &str,
1803 _from_cursor: &str,
1804 _subject_id: Option<&str>,
1805 ) -> Result<Subscription, EventError> {
1806 Err(EventError::UnregisteredEventType(event_type.to_string()))
1807 }
1808
1809 fn poll(
1810 &self,
1811 subscription_id: &str,
1812 _max_events: usize,
1813 ) -> Result<SubscriptionPoll, EventError> {
1814 Err(EventError::SubscriptionNotFound(
1815 subscription_id.to_string(),
1816 ))
1817 }
1818
1819 fn cancel(&self, subscription_id: &str) -> Result<(), EventError> {
1820 Err(EventError::SubscriptionNotFound(
1821 subscription_id.to_string(),
1822 ))
1823 }
1824}
1825
1826fn default_event_broker() -> Arc<dyn EventBroker> {
1827 event_broker_or_discard(InProcessBroker::new(Arc::new(EventCatalog::new())))
1828}
1829
1830fn event_broker_or_discard(result: Result<InProcessBroker, EventError>) -> Arc<dyn EventBroker> {
1831 match result {
1832 Ok(broker) => Arc::new(broker),
1833 Err(_) => Arc::new(DiscardEventBroker),
1834 }
1835}
1836
1837fn idle_runtime_snapshot() -> RuntimeSnapshot {
1838 RuntimeSnapshot {
1839 target_loads: [
1840 (ExecutionTarget::Local, 0.0),
1841 (ExecutionTarget::Browser, 0.0),
1842 (ExecutionTarget::Edge, 0.0),
1843 (ExecutionTarget::Cloud, 0.0),
1844 (ExecutionTarget::Worker, 0.0),
1845 (ExecutionTarget::Device, 0.0),
1846 ]
1847 .into_iter()
1848 .collect(),
1849 }
1850}
1851
1852fn artifact_type_for(selected: &ResolvedCapability) -> ArtifactType {
1853 if selected.artifact.binary.is_some() {
1854 ArtifactType::Wasm
1855 } else {
1856 ArtifactType::Native
1857 }
1858}
1859
1860fn executor_capability_for(
1861 selected: &ResolvedCapability,
1862 artifact_type: ArtifactType,
1863) -> ExecutorCapability {
1864 let binary = selected.artifact.binary.as_ref();
1865 ExecutorCapability {
1866 capability_id: selected.contract.id.clone(),
1867 artifact_type,
1868 wasm_binary_path: binary.map(|binary| binary.location.clone()),
1869 wasm_checksum: selected
1870 .artifact
1871 .digests
1872 .binary_digest
1873 .as_deref()
1874 .and_then(|digest| digest.strip_prefix("sha256:"))
1875 .map(str::to_string),
1876 host_abi_version: None,
1877 emits: selected.contract.emits.clone(),
1878 service_type: selected.contract.service_type.clone(),
1879 }
1880}
1881
1882fn execution_target_from_placement(target: PlacementTarget) -> ExecutionTarget {
1883 match target {
1884 PlacementTarget::Local => ExecutionTarget::Local,
1885 PlacementTarget::Browser => ExecutionTarget::Browser,
1886 PlacementTarget::Edge => ExecutionTarget::Edge,
1887 PlacementTarget::Cloud => ExecutionTarget::Cloud,
1888 PlacementTarget::Worker => ExecutionTarget::Worker,
1889 PlacementTarget::Device => ExecutionTarget::Device,
1890 }
1891}
1892
1893fn map_router_error(
1894 error: &RouterError,
1895) -> (RuntimeError, ExecutionFailureReason, Vec<EventReference>) {
1896 match error {
1897 RouterError::PlacementFailed(placement_error) => (
1898 runtime_error(
1899 RuntimeErrorCode::PlacementUnsupported,
1900 "placement constraints rejected the capability execution request",
1901 json!({"placement_error": format!("{placement_error:?}")}),
1902 ),
1903 ExecutionFailureReason::PlacementUnsupported,
1904 Vec::new(),
1905 ),
1906 RouterError::ExecutorNotFound(artifact_type) => (
1907 runtime_error(
1908 RuntimeErrorCode::CapabilityNotRunnable,
1909 "no executor is registered for the capability artifact type",
1910 json!({"artifact_type": artifact_type}),
1911 ),
1912 ExecutionFailureReason::ArtifactNotRunnable,
1913 Vec::new(),
1914 ),
1915 RouterError::ExecutionFailed(message) => (
1916 runtime_error(
1917 RuntimeErrorCode::ExecutionFailed,
1918 message,
1919 json!({"code": "execution_failed"}),
1920 ),
1921 ExecutionFailureReason::ExecutionFailed,
1922 Vec::new(),
1923 ),
1924 RouterError::ContractViolation(violations) => (
1925 runtime_error(
1926 RuntimeErrorCode::ContractViolation,
1927 "capability execution violated its governed contract",
1928 json!({"violations": violations}),
1929 ),
1930 ExecutionFailureReason::ExecutionFailed,
1931 Vec::new(),
1932 ),
1933 RouterError::TraceLockPoisoned => (
1934 runtime_error(
1935 RuntimeErrorCode::ExecutionFailed,
1936 "trace store lock is poisoned",
1937 json!({"code": "trace_lock_poisoned"}),
1938 ),
1939 ExecutionFailureReason::ExecutionFailed,
1940 Vec::new(),
1941 ),
1942 RouterError::DurableTraceWriteFailed(message) => (
1943 runtime_error(
1944 RuntimeErrorCode::ExecutionFailed,
1945 "the execution trace could not be durably written",
1946 json!({"code": "durable_trace_write_failed", "detail": message}),
1947 ),
1948 ExecutionFailureReason::ExecutionFailed,
1949 Vec::new(),
1950 ),
1951 }
1952}
1953
1954fn terminal_failure(context: FailureContext) -> RuntimeExecutionOutcome {
1955 let result_record = TraceResultRecord {
1956 status: RuntimeResultStatus::Error,
1957 output: None,
1958 error: Some(context.error.clone()),
1959 warnings: context.attempt.warnings.clone(),
1960 };
1961 let otel_trace = otel_trace_record(
1962 &context.attempt,
1963 &context.state_transitions,
1964 &context.selection,
1965 &context.execution,
1966 &result_record,
1967 );
1968 let trace = RuntimeTrace {
1969 kind: RUNTIME_TRACE_KIND.to_string(),
1970 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
1971 trace_id: context.attempt.trace_id.clone(),
1972 execution_id: context.attempt.execution_id.clone(),
1973 request_id: context.attempt.request.request_id.clone(),
1974 governing_spec: GOVERNING_SPEC.to_string(),
1975 request: context.attempt.request.clone(),
1976 decision_evidence: TraceDecisionEvidence {
1977 candidate_collection: context.candidate_collection.clone(),
1978 selection: context.selection.clone(),
1979 model_resolution: Vec::new(),
1980 },
1981 state_progression: TraceStateProgression {
1982 state_events: context.state_events.clone(),
1983 transitions: context.state_transitions.clone(),
1984 validation: context.state_machine_validation.clone(),
1985 },
1986 terminal_outcome: TraceTerminalOutcome {
1987 runtime_status: RuntimeResultStatus::Error,
1988 execution_status: context.execution.status,
1989 failure_reason: context.execution.failure_reason,
1990 error: Some(context.error.clone()),
1991 },
1992 emitted_events: context.emitted_events,
1993 workflow_evidence: context.workflow_evidence,
1994 model_resolution: Vec::new(),
1995 state_transitions: context.state_transitions,
1996 state_machine_validation: context.state_machine_validation,
1997 candidate_collection: context.candidate_collection,
1998 selection: context.selection,
1999 execution: context.execution,
2000 result: result_record,
2001 otel_trace,
2002 };
2003
2004 let result = RuntimeResult {
2005 kind: RUNTIME_RESULT_KIND.to_string(),
2006 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
2007 execution_id: context.attempt.execution_id,
2008 request_id: context.attempt.request.request_id,
2009 status: RuntimeResultStatus::Error,
2010 trace_ref: context.attempt.trace_id,
2011 output: None,
2012 error: Some(context.error),
2013 warnings: context.attempt.warnings,
2014 };
2015
2016 RuntimeExecutionOutcome {
2017 result,
2018 trace,
2019 state_events: context.state_events,
2020 }
2021}
2022
2023fn supported_executor_targets() -> Vec<PlacementTarget> {
2024 vec![PlacementTarget::Local]
2025}
2026
2027fn placement_not_attempted(
2028 requested_target: PlacementTarget,
2029 reason: PlacementDecisionReason,
2030) -> PlacementDecisionRecord {
2031 PlacementDecisionRecord {
2032 requested_target,
2033 selected_target: None,
2034 status: PlacementDecisionStatus::NotAttempted,
2035 reason,
2036 supported_executor_targets: supported_executor_targets(),
2037 }
2038}
2039
2040fn resolve_placement(
2041 requested_target: PlacementTarget,
2042) -> Result<PlacementDecisionRecord, RuntimeError> {
2043 if requested_target == PlacementTarget::Local {
2044 return Ok(PlacementDecisionRecord {
2045 requested_target,
2046 selected_target: Some(PlacementTarget::Local),
2047 status: PlacementDecisionStatus::Selected,
2048 reason: PlacementDecisionReason::RequestedTargetSelected,
2049 supported_executor_targets: supported_executor_targets(),
2050 });
2051 }
2052
2053 Err(runtime_error(
2054 RuntimeErrorCode::PlacementUnsupported,
2055 "requested placement target is not supported by the available executor set",
2056 json!({
2057 "requested_target": requested_target,
2058 "supported_executor_targets": supported_executor_targets(),
2059 }),
2060 ))
2061}
2062
2063fn sanitized_request(mut request: RuntimeRequest) -> RuntimeRequest {
2064 if let Some(token) = request.context.caller.take() {
2065 if let Some(identity) = derive_identity_from_jwt(&token) {
2066 request.context.identity = Some(identity);
2067 } else {
2068 request.context.caller = Some(token);
2069 }
2070 }
2071 request
2072}
2073
2074fn load_artifact_bytes_for_verification(
2075 selected: &ResolvedCapability,
2076) -> Result<Vec<u8>, RuntimeError> {
2077 let Some(binary) = selected.artifact.binary.as_ref() else {
2078 return Ok(Vec::new());
2079 };
2080 match fs::read(&binary.location) {
2081 Ok(bytes) => Ok(bytes),
2082 Err(_) if binary.signature.is_none() => Ok(selected
2083 .artifact
2084 .digests
2085 .binary_digest
2086 .clone()
2087 .unwrap_or_else(|| selected.record.artifact_ref.clone())
2088 .into_bytes()),
2089 Err(error) => Err(runtime_error(
2090 RuntimeErrorCode::ArtifactMissing,
2091 "artifact bytes could not be loaded for signature verification",
2092 json!({
2093 "artifact_ref": selected.record.artifact_ref,
2094 "location": binary.location,
2095 "code": "artifact_load_failed",
2096 "message": error.to_string(),
2097 }),
2098 )),
2099 }
2100}
2101
2102fn artifact_verification_runtime_error(error: &ArtifactVerificationFailure) -> RuntimeError {
2103 runtime_error(
2104 RuntimeErrorCode::ContractViolation,
2105 "artifact signature verification failed before execution",
2106 json!({
2107 "code": error.code(),
2108 "artifact_verification": error.record(),
2109 }),
2110 )
2111}
2112
2113fn begin_attempt(
2114 request: RuntimeRequest,
2115 observability: RuntimeObservabilityConfig,
2116) -> (AttemptContext, StateEmitter) {
2117 let request = sanitized_request(request);
2118 let request_id = request.request_id.clone();
2119 let execution_id = format!("{EXECUTION_PREFIX}{request_id}");
2120 let trace_id = format!("{TRACE_PREFIX}{execution_id}");
2121 let mut emitter = StateEmitter::new(&execution_id, &request_id);
2122 emitter.push(
2123 RuntimeState::LoadingRegistry,
2124 RuntimeTransitionReasonCode::RuntimeInitializationStarted,
2125 json!({
2126 "registry_status": "available",
2127 "identity": request.context.identity,
2128 }),
2129 );
2130 emitter.push(
2131 RuntimeState::Ready,
2132 RuntimeTransitionReasonCode::RegistryLoaded,
2133 json!({"governing_spec": GOVERNING_SPEC}),
2134 );
2135
2136 (
2137 AttemptContext {
2138 request,
2139 execution_id,
2140 trace_id,
2141 observability,
2142 artifact_verification: None,
2143 warnings: Vec::new(),
2144 },
2145 emitter,
2146 )
2147}
2148
2149fn invalid_request_outcome(
2150 attempt: AttemptContext,
2151 mut emitter: StateEmitter,
2152 error: RuntimeError,
2153) -> RuntimeExecutionOutcome {
2154 let placement = placement_not_attempted(
2155 attempt.request.context.requested_target,
2156 PlacementDecisionReason::SelectionNotReached,
2157 );
2158 emitter.push(
2159 RuntimeState::EvaluatingConstraints,
2160 RuntimeTransitionReasonCode::CandidatesCollected,
2161 json!({"candidate_count": 0}),
2162 );
2163 emitter.push(
2164 RuntimeState::Error,
2165 RuntimeTransitionReasonCode::ConstraintValidationFailed,
2166 json!({"code": error.code, "message": error.message}),
2167 );
2168 emitter.push(
2169 RuntimeState::Ready,
2170 RuntimeTransitionReasonCode::ExecutionClosed,
2171 json!({"terminal_state": RuntimeState::Error}),
2172 );
2173 let finished = emitter.finish();
2174 let identity = attempt.request.context.identity.clone();
2175 terminal_failure(FailureContext {
2176 attempt,
2177 state_events: finished.events,
2178 state_transitions: finished.transitions,
2179 state_machine_validation: finished.validation,
2180 candidate_collection: CandidateCollectionRecord {
2181 lookup_scope: RuntimeLookupScope::PreferPrivate,
2182 candidates: Vec::new(),
2183 rejected_candidates: Vec::new(),
2184 },
2185 selection: SelectionRecord {
2186 status: SelectionStatus::InvalidRequest,
2187 selected_capability_id: None,
2188 selected_capability_version: None,
2189 failure_reason: Some(SelectionFailureReason::InvalidRequest),
2190 remaining_candidates: Vec::new(),
2191 },
2192 execution: ExecutionRecord {
2193 placement: placement.clone(),
2194 placement_target: placement.requested_target,
2195 status: ExecutionStatus::NotStarted,
2196 artifact_ref: None,
2197 started_at: None,
2198 completed_at: None,
2199 output_digest: None,
2200 failure_reason: Some(ExecutionFailureReason::ContractInputInvalid),
2201 artifact_verification: None,
2202 identity,
2203 },
2204 error,
2205 emitted_events: Vec::new(),
2206 workflow_evidence: None,
2207 })
2208}
2209
2210fn no_eligible_outcome(
2211 attempt: AttemptContext,
2212 mut emitter: StateEmitter,
2213 candidate_collection: CandidateCollectionRecord,
2214) -> RuntimeExecutionOutcome {
2215 let placement = placement_not_attempted(
2216 attempt.request.context.requested_target,
2217 PlacementDecisionReason::SelectionNotReached,
2218 );
2219 let error = if candidate_collection.rejected_candidates.is_empty() {
2220 runtime_error(
2221 RuntimeErrorCode::CapabilityNotFound,
2222 "no eligible capability matched the runtime request",
2223 json!({"request_id": attempt.request.request_id}),
2224 )
2225 } else {
2226 runtime_error(
2227 RuntimeErrorCode::CapabilityNotRunnable,
2228 "matching capabilities were found but none were runnable locally",
2229 json!({"rejected_candidates": candidate_collection.rejected_candidates}),
2230 )
2231 };
2232 let reason = if candidate_collection.rejected_candidates.is_empty() {
2233 RuntimeTransitionReasonCode::NoMatch
2234 } else {
2235 RuntimeTransitionReasonCode::ConstraintValidationFailed
2236 };
2237 emitter.push(RuntimeState::Error, reason, json!({"code": error.code}));
2238 emitter.push(
2239 RuntimeState::Ready,
2240 RuntimeTransitionReasonCode::ExecutionClosed,
2241 json!({"terminal_state": RuntimeState::Error}),
2242 );
2243 let failure_reason = if error.code == RuntimeErrorCode::CapabilityNotFound {
2244 SelectionFailureReason::NoMatch
2245 } else {
2246 SelectionFailureReason::NotRunnable
2247 };
2248 let finished = emitter.finish();
2249 let identity = attempt.request.context.identity.clone();
2250
2251 terminal_failure(FailureContext {
2252 attempt,
2253 state_events: finished.events,
2254 state_transitions: finished.transitions,
2255 state_machine_validation: finished.validation,
2256 candidate_collection,
2257 selection: SelectionRecord {
2258 status: SelectionStatus::NoMatch,
2259 selected_capability_id: None,
2260 selected_capability_version: None,
2261 failure_reason: Some(failure_reason),
2262 remaining_candidates: Vec::new(),
2263 },
2264 execution: ExecutionRecord {
2265 placement: placement.clone(),
2266 placement_target: placement.requested_target,
2267 status: ExecutionStatus::NotStarted,
2268 artifact_ref: None,
2269 started_at: None,
2270 completed_at: None,
2271 output_digest: None,
2272 failure_reason: Some(ExecutionFailureReason::ArtifactNotRunnable),
2273 artifact_verification: None,
2274 identity,
2275 },
2276 error,
2277 emitted_events: Vec::new(),
2278 workflow_evidence: None,
2279 })
2280}
2281
2282fn ambiguous_outcome(
2283 attempt: AttemptContext,
2284 mut emitter: StateEmitter,
2285 resolution: CandidateResolution,
2286) -> RuntimeExecutionOutcome {
2287 let placement = placement_not_attempted(
2288 attempt.request.context.requested_target,
2289 PlacementDecisionReason::SelectionNotReached,
2290 );
2291 let remaining_candidates = resolution
2292 .eligible
2293 .iter()
2294 .map(|candidate| runtime_candidate(candidate, resolution.candidate_reason))
2295 .collect::<Vec<_>>();
2296 let error = runtime_error(
2297 RuntimeErrorCode::CapabilityAmbiguous,
2298 "runtime request matched more than one eligible capability",
2299 json!({"remaining_candidates": remaining_candidates}),
2300 );
2301 emitter.push(
2302 RuntimeState::Error,
2303 RuntimeTransitionReasonCode::SelectionFailed,
2304 json!({"code": error.code}),
2305 );
2306 emitter.push(
2307 RuntimeState::Ready,
2308 RuntimeTransitionReasonCode::ExecutionClosed,
2309 json!({"terminal_state": RuntimeState::Error}),
2310 );
2311 let finished = emitter.finish();
2312 let identity = attempt.request.context.identity.clone();
2313
2314 terminal_failure(FailureContext {
2315 attempt,
2316 state_events: finished.events,
2317 state_transitions: finished.transitions,
2318 state_machine_validation: finished.validation,
2319 candidate_collection: resolution.collection,
2320 selection: SelectionRecord {
2321 status: SelectionStatus::Ambiguous,
2322 selected_capability_id: None,
2323 selected_capability_version: None,
2324 failure_reason: Some(SelectionFailureReason::Ambiguous),
2325 remaining_candidates,
2326 },
2327 execution: ExecutionRecord {
2328 placement: placement.clone(),
2329 placement_target: placement.requested_target,
2330 status: ExecutionStatus::NotStarted,
2331 artifact_ref: None,
2332 started_at: None,
2333 completed_at: None,
2334 output_digest: None,
2335 failure_reason: Some(ExecutionFailureReason::ArtifactNotRunnable),
2336 artifact_verification: None,
2337 identity,
2338 },
2339 error,
2340 emitted_events: Vec::new(),
2341 workflow_evidence: None,
2342 })
2343}
2344
2345fn pre_execution_failure_outcome(
2346 context: ExecutionContext,
2347 failure: PreExecutionFailure,
2348) -> RuntimeExecutionOutcome {
2349 let ExecutionContext {
2350 attempt,
2351 mut emitter,
2352 candidate_collection,
2353 selection,
2354 } = context;
2355 let reason = if emitter.current_state == RuntimeState::Selecting {
2356 RuntimeTransitionReasonCode::SelectionFailed
2357 } else {
2358 RuntimeTransitionReasonCode::ConstraintValidationFailed
2359 };
2360 emitter.push(
2361 RuntimeState::Error,
2362 reason,
2363 json!({"code": failure.error.code, "details": failure.error.details}),
2364 );
2365 emitter.push(
2366 RuntimeState::Ready,
2367 RuntimeTransitionReasonCode::ExecutionClosed,
2368 json!({"terminal_state": RuntimeState::Error}),
2369 );
2370 let finished = emitter.finish();
2371 let identity = attempt.request.context.identity.clone();
2372 terminal_failure(FailureContext {
2373 attempt,
2374 state_events: finished.events,
2375 state_transitions: finished.transitions,
2376 state_machine_validation: finished.validation,
2377 candidate_collection,
2378 selection,
2379 execution: ExecutionRecord {
2380 placement: failure.placement.clone(),
2381 placement_target: failure
2382 .placement
2383 .selected_target
2384 .unwrap_or(failure.placement.requested_target),
2385 status: ExecutionStatus::NotStarted,
2386 artifact_ref: failure.artifact_ref,
2387 started_at: None,
2388 completed_at: None,
2389 output_digest: None,
2390 failure_reason: Some(failure.failure_reason),
2391 artifact_verification: failure.artifact_verification,
2392 identity,
2393 },
2394 error: failure.error,
2395 emitted_events: Vec::new(),
2396 workflow_evidence: None,
2397 })
2398}
2399
2400#[allow(clippy::too_many_arguments)]
2401fn execution_failure_outcome(
2402 context: ExecutionContext,
2403 failure: ExecutionFailureState,
2404 error: RuntimeError,
2405 emitted_events: Vec<traverse_contracts::EventReference>,
2406 workflow_evidence: Option<WorkflowTraversalEvidence>,
2407) -> RuntimeExecutionOutcome {
2408 let ExecutionContext {
2409 attempt,
2410 mut emitter,
2411 candidate_collection,
2412 selection,
2413 } = context;
2414 emitter.push(
2415 RuntimeState::Error,
2416 RuntimeTransitionReasonCode::ExecutionFailed,
2417 json!({"code": error.code, "details": error.details}),
2418 );
2419 let completed_at = emitter.next_timestamp();
2420 emitter.push(
2421 RuntimeState::Ready,
2422 RuntimeTransitionReasonCode::ExecutionClosed,
2423 json!({"terminal_state": RuntimeState::Error}),
2424 );
2425 let finished = emitter.finish();
2426 let identity = attempt.request.context.identity.clone();
2427 let artifact_verification = attempt.artifact_verification.clone();
2428
2429 terminal_failure(FailureContext {
2430 attempt,
2431 state_events: finished.events,
2432 state_transitions: finished.transitions,
2433 state_machine_validation: finished.validation,
2434 candidate_collection,
2435 selection,
2436 execution: ExecutionRecord {
2437 placement: failure.placement.clone(),
2438 placement_target: failure
2439 .placement
2440 .selected_target
2441 .unwrap_or(failure.placement.requested_target),
2442 status: ExecutionStatus::Failed,
2443 artifact_ref: Some(failure.artifact_ref),
2444 started_at: Some(failure.started_at),
2445 completed_at: Some(completed_at),
2446 output_digest: None,
2447 failure_reason: Some(failure.failure_reason),
2448 artifact_verification,
2449 identity,
2450 },
2451 error,
2452 emitted_events,
2453 workflow_evidence,
2454 })
2455}
2456
2457#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2458fn successful_execution_outcome(
2459 context: ExecutionContext,
2460 selected: &ResolvedCapability,
2461 started_execution: StartedExecution,
2462 execution_output: Value,
2463 emitted_events: Vec<traverse_contracts::EventReference>,
2464 workflow_evidence: Option<WorkflowTraversalEvidence>,
2465) -> RuntimeExecutionOutcome {
2466 let ExecutionContext {
2467 attempt,
2468 mut emitter,
2469 candidate_collection,
2470 selection,
2471 } = context;
2472 let completed_at = emitter.next_timestamp();
2473 let emits_events = selected.record.implementation_kind == ImplementationKind::Workflow
2474 || !selected.contract.emits.is_empty();
2475 if emits_events {
2476 emitter.push(
2477 RuntimeState::EmittingEvents,
2478 RuntimeTransitionReasonCode::ExecutionSucceededWithEvents,
2479 json!({
2480 "capability_id": selected.record.id,
2481 "capability_version": selected.record.version,
2482 "declared_event_count": selected.contract.emits.len(),
2483 }),
2484 );
2485 emitter.push(
2486 RuntimeState::Completed,
2487 RuntimeTransitionReasonCode::EventsEmitted,
2488 json!({
2489 "capability_id": selected.record.id,
2490 "capability_version": selected.record.version,
2491 }),
2492 );
2493 } else {
2494 emitter.push(
2495 RuntimeState::Completed,
2496 RuntimeTransitionReasonCode::ExecutionSucceeded,
2497 json!({
2498 "capability_id": selected.record.id,
2499 "capability_version": selected.record.version,
2500 }),
2501 );
2502 }
2503 emitter.push(
2504 RuntimeState::Ready,
2505 RuntimeTransitionReasonCode::ExecutionClosed,
2506 json!({"terminal_state": RuntimeState::Completed}),
2507 );
2508 let finished = emitter.finish();
2509
2510 let execution = ExecutionRecord {
2511 placement: started_execution.placement.clone(),
2512 placement_target: started_execution
2513 .placement
2514 .selected_target
2515 .unwrap_or(started_execution.placement.requested_target),
2516 status: ExecutionStatus::Succeeded,
2517 artifact_ref: Some(selected.record.artifact_ref.clone()),
2518 started_at: Some(started_execution.started_at),
2519 completed_at: Some(completed_at),
2520 output_digest: Some(content_digest(&execution_output)),
2521 failure_reason: None,
2522 artifact_verification: attempt.artifact_verification.clone(),
2523 identity: attempt.request.context.identity.clone(),
2524 };
2525 let result_record = TraceResultRecord {
2526 status: RuntimeResultStatus::Completed,
2527 output: Some(execution_output.clone()),
2528 error: None,
2529 warnings: attempt.warnings.clone(),
2530 };
2531 let otel_trace = otel_trace_record(
2532 &attempt,
2533 &finished.transitions,
2534 &selection,
2535 &execution,
2536 &result_record,
2537 );
2538
2539 let trace = RuntimeTrace {
2540 kind: RUNTIME_TRACE_KIND.to_string(),
2541 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
2542 trace_id: attempt.trace_id.clone(),
2543 execution_id: attempt.execution_id.clone(),
2544 request_id: attempt.request.request_id.clone(),
2545 governing_spec: GOVERNING_SPEC.to_string(),
2546 request: attempt.request.clone(),
2547 decision_evidence: TraceDecisionEvidence {
2548 candidate_collection: candidate_collection.clone(),
2549 selection: selection.clone(),
2550 model_resolution: Vec::new(),
2551 },
2552 state_progression: TraceStateProgression {
2553 state_events: finished.events.clone(),
2554 transitions: finished.transitions.clone(),
2555 validation: finished.validation.clone(),
2556 },
2557 terminal_outcome: TraceTerminalOutcome {
2558 runtime_status: RuntimeResultStatus::Completed,
2559 execution_status: execution.status,
2560 failure_reason: None,
2561 error: None,
2562 },
2563 emitted_events,
2564 workflow_evidence,
2565 model_resolution: Vec::new(),
2566 state_transitions: finished.transitions.clone(),
2567 state_machine_validation: finished.validation.clone(),
2568 candidate_collection,
2569 selection,
2570 execution,
2571 result: result_record,
2572 otel_trace,
2573 };
2574
2575 let result = RuntimeResult {
2576 kind: RUNTIME_RESULT_KIND.to_string(),
2577 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
2578 execution_id: attempt.execution_id,
2579 request_id: attempt.request.request_id,
2580 status: RuntimeResultStatus::Completed,
2581 trace_ref: attempt.trace_id,
2582 output: Some(execution_output),
2583 error: None,
2584 warnings: attempt.warnings,
2585 };
2586
2587 RuntimeExecutionOutcome {
2588 result,
2589 trace,
2590 state_events: finished.events,
2591 }
2592}
2593
2594fn validate_request(request: &RuntimeRequest) -> Option<RuntimeError> {
2595 if request.kind != RUNTIME_REQUEST_KIND {
2596 return Some(runtime_error(
2597 RuntimeErrorCode::RequestInvalid,
2598 "kind must equal runtime_request",
2599 json!({"path": "$.kind"}),
2600 ));
2601 }
2602 if request.schema_version != SUPPORTED_SCHEMA_VERSION {
2603 return Some(runtime_error(
2604 RuntimeErrorCode::RequestInvalid,
2605 "schema_version must equal 1.0.0",
2606 json!({"path": "$.schema_version"}),
2607 ));
2608 }
2609 if request.governing_spec != GOVERNING_SPEC {
2610 return Some(runtime_error(
2611 RuntimeErrorCode::RequestInvalid,
2612 "governing_spec must equal 006-runtime-request-execution",
2613 json!({"path": "$.governing_spec"}),
2614 ));
2615 }
2616 if request.request_id.trim().is_empty() {
2617 return Some(runtime_error(
2618 RuntimeErrorCode::RequestInvalid,
2619 "request_id must be non-empty",
2620 json!({"path": "$.request_id"}),
2621 ));
2622 }
2623 if request.lookup.allow_ambiguity {
2624 return Some(runtime_error(
2625 RuntimeErrorCode::RequestInvalid,
2626 "allow_ambiguity must be false in this runtime slice",
2627 json!({"path": "$.lookup.allow_ambiguity"}),
2628 ));
2629 }
2630 if request
2631 .intent
2632 .capability_version
2633 .as_deref()
2634 .is_some_and(|version| Version::parse(version).is_err())
2635 {
2636 return Some(runtime_error(
2637 RuntimeErrorCode::RequestInvalid,
2638 "capability_version must be valid semantic versioning",
2639 json!({"path": "$.intent.capability_version"}),
2640 ));
2641 }
2642
2643 let exact_id = request
2644 .intent
2645 .capability_id
2646 .as_deref()
2647 .is_some_and(non_empty);
2648 let exact_version = request
2649 .intent
2650 .capability_version
2651 .as_deref()
2652 .is_some_and(non_empty);
2653 let intent_key = request.intent.intent_key.as_deref().is_some_and(non_empty);
2654
2655 if !(exact_id || intent_key) {
2656 return Some(runtime_error(
2657 RuntimeErrorCode::RequestInvalid,
2658 "runtime intent must include capability_id or intent_key",
2659 json!({"path": "$.intent"}),
2660 ));
2661 }
2662
2663 if exact_version && !exact_id {
2664 return Some(runtime_error(
2665 RuntimeErrorCode::RequestInvalid,
2666 "capability_version requires capability_id",
2667 json!({"path": "$.intent.capability_version"}),
2668 ));
2669 }
2670
2671 let has_version_range = request
2672 .intent
2673 .version_range
2674 .as_deref()
2675 .is_some_and(non_empty);
2676
2677 if has_version_range && !exact_id {
2678 return Some(runtime_error(
2679 RuntimeErrorCode::RequestInvalid,
2680 "version_range requires capability_id",
2681 json!({"path": "$.intent.version_range"}),
2682 ));
2683 }
2684
2685 if has_version_range && exact_version {
2686 return Some(runtime_error(
2687 RuntimeErrorCode::RequestInvalid,
2688 "version_range and capability_version are mutually exclusive",
2689 json!({"path": "$.intent.version_range"}),
2690 ));
2691 }
2692
2693 None
2694}
2695
2696fn is_exact_target(intent: &RuntimeIntent) -> bool {
2697 intent.capability_id.as_deref().is_some_and(non_empty)
2698 && intent.capability_version.as_deref().is_some_and(non_empty)
2699}
2700
2701fn non_empty(value: &str) -> bool {
2702 !value.trim().is_empty()
2703}
2704
2705fn map_lookup_scope(scope: RuntimeLookupScope) -> LookupScope {
2706 match scope {
2707 RuntimeLookupScope::PublicOnly => LookupScope::PublicOnly,
2708 RuntimeLookupScope::PreferPrivate => LookupScope::PreferPrivate,
2709 }
2710}
2711
2712fn evaluate_candidate(candidate: ResolvedCapability) -> CandidateEvaluation {
2713 if !candidate.contract.lifecycle.is_runtime_eligible() {
2714 return CandidateEvaluation::Rejected(
2715 candidate,
2716 RejectedCandidateReason::LifecycleNotRunnable,
2717 );
2718 }
2719 if candidate.record.implementation_kind == ImplementationKind::Workflow {
2720 if candidate.artifact.workflow_ref.is_some() {
2721 return CandidateEvaluation::Eligible(candidate);
2722 }
2723 return CandidateEvaluation::Rejected(candidate, RejectedCandidateReason::ArtifactMissing);
2724 }
2725
2726 let execution = &candidate.contract.execution;
2727 if !execution
2728 .preferred_targets
2729 .contains(&ExecutionTarget::Local)
2730 || execution.constraints.host_api_access != HostApiAccess::None
2731 || execution.constraints.network_access != NetworkAccess::Forbidden
2732 {
2733 return CandidateEvaluation::Rejected(
2734 candidate,
2735 RejectedCandidateReason::NotRunnableLocally,
2736 );
2737 }
2738
2739 match candidate.artifact.binary.as_ref() {
2740 Some(binary) if binary.location.trim().is_empty() => {
2741 CandidateEvaluation::Rejected(candidate, RejectedCandidateReason::ArtifactMissing)
2742 }
2743 None | Some(_) => CandidateEvaluation::Eligible(candidate),
2745 }
2746}
2747
2748fn validate_payload_against_contract(
2749 payload: &Value,
2750 schema: &Value,
2751 code: RuntimeErrorCode,
2752 message: &str,
2753) -> Result<(), RuntimeError> {
2754 let mut errors = Vec::new();
2755 validate_value_against_schema(payload, schema, "$", &mut errors);
2756 if errors.is_empty() {
2757 Ok(())
2758 } else {
2759 Err(runtime_error(
2760 code,
2761 message,
2762 json!({ "violations": errors }),
2763 ))
2764 }
2765}
2766
2767pub(crate) fn validate_value_against_schema(
2768 value: &Value,
2769 schema: &Value,
2770 path: &str,
2771 errors: &mut Vec<Value>,
2772) {
2773 let Some(schema_object) = schema.as_object() else {
2774 errors.push(json!({
2775 "path": path,
2776 "message": "schema must be an object"
2777 }));
2778 return;
2779 };
2780
2781 if let Some(schema_type) = schema_object.get("type").and_then(Value::as_str) {
2782 match schema_type {
2783 "object" => {
2784 let Some(instance) = value.as_object() else {
2785 errors.push(type_error(path, "object"));
2786 return;
2787 };
2788 validate_required(instance, schema_object, path, errors);
2789 validate_properties(instance, schema_object, path, errors);
2790 }
2791 "array" => {
2792 let Some(items) = value.as_array() else {
2793 errors.push(type_error(path, "array"));
2794 return;
2795 };
2796 if let Some(item_schema) = schema_object.get("items") {
2797 for (index, item) in items.iter().enumerate() {
2798 validate_value_against_schema(
2799 item,
2800 item_schema,
2801 &format!("{path}[{index}]"),
2802 errors,
2803 );
2804 }
2805 }
2806 }
2807 "string" if !value.is_string() => errors.push(type_error(path, "string")),
2808 "integer" if value.as_i64().is_none() && value.as_u64().is_none() => {
2809 errors.push(type_error(path, "integer"));
2810 }
2811 "number" if !value.is_number() => errors.push(type_error(path, "number")),
2812 "boolean" if !value.is_boolean() => errors.push(type_error(path, "boolean")),
2813 "null" if !value.is_null() => errors.push(type_error(path, "null")),
2814 _ => {}
2815 }
2816 }
2817}
2818
2819fn validate_required(
2820 instance: &Map<String, Value>,
2821 schema_object: &Map<String, Value>,
2822 path: &str,
2823 errors: &mut Vec<Value>,
2824) {
2825 let Some(required) = schema_object.get("required").and_then(Value::as_array) else {
2826 return;
2827 };
2828
2829 for required_field in required.iter().filter_map(Value::as_str) {
2830 if !instance.contains_key(required_field) {
2831 errors.push(json!({
2832 "path": format!("{path}.{required_field}"),
2833 "message": "required property is missing"
2834 }));
2835 }
2836 }
2837}
2838
2839fn validate_properties(
2840 instance: &Map<String, Value>,
2841 schema_object: &Map<String, Value>,
2842 path: &str,
2843 errors: &mut Vec<Value>,
2844) {
2845 let Some(properties) = schema_object.get("properties").and_then(Value::as_object) else {
2846 return;
2847 };
2848
2849 for (key, value) in instance {
2850 if let Some(property_schema) = properties.get(key) {
2851 validate_value_against_schema(value, property_schema, &format!("{path}.{key}"), errors);
2852 }
2853 }
2854}
2855
2856fn type_error(path: &str, expected: &str) -> Value {
2857 json!({
2858 "path": path,
2859 "message": format!("expected {expected}")
2860 })
2861}
2862
2863fn runtime_candidate(capability: &ResolvedCapability, reason: CandidateReason) -> RuntimeCandidate {
2864 RuntimeCandidate {
2865 scope: map_registry_scope(capability.record.scope),
2866 capability_id: capability.record.id.clone(),
2867 capability_version: capability.record.version.clone(),
2868 artifact_ref: capability.record.artifact_ref.clone(),
2869 implementation_kind: map_implementation_kind(capability.record.implementation_kind),
2870 lifecycle: map_lifecycle(&capability.record.lifecycle),
2871 reason,
2872 }
2873}
2874
2875fn map_registry_scope(scope: RegistryScope) -> RuntimeRegistryScope {
2876 match scope {
2877 RegistryScope::Public => RuntimeRegistryScope::Public,
2878 RegistryScope::Private => RuntimeRegistryScope::Private,
2879 }
2880}
2881
2882fn map_implementation_kind(kind: ImplementationKind) -> RuntimeImplementationKind {
2883 match kind {
2884 ImplementationKind::Executable => RuntimeImplementationKind::Executable,
2885 ImplementationKind::Workflow => RuntimeImplementationKind::Workflow,
2886 }
2887}
2888
2889fn map_lifecycle(lifecycle: &Lifecycle) -> RuntimeLifecycle {
2890 match lifecycle {
2891 Lifecycle::Draft => RuntimeLifecycle::Draft,
2892 Lifecycle::Active => RuntimeLifecycle::Active,
2893 Lifecycle::Deprecated => RuntimeLifecycle::Deprecated,
2894 Lifecycle::Retired => RuntimeLifecycle::Retired,
2895 Lifecycle::Archived => RuntimeLifecycle::Archived,
2896 }
2897}
2898
2899fn runtime_error(code: RuntimeErrorCode, message: &str, details: Value) -> RuntimeError {
2900 RuntimeError {
2901 code,
2902 message: message.to_string(),
2903 details,
2904 }
2905}
2906
2907fn content_digest(value: &Value) -> String {
2908 let json = value.to_string();
2909 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
2910 for byte in json.as_bytes() {
2911 hash ^= u64::from(*byte);
2912 hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
2913 }
2914 format!("0.1.0:{hash:016x}")
2915}
2916
2917fn otel_trace_record(
2918 attempt: &AttemptContext,
2919 state_transitions: &[RuntimeTransitionRecord],
2920 selection: &SelectionRecord,
2921 execution: &ExecutionRecord,
2922 result: &TraceResultRecord,
2923) -> OTelTraceRecord {
2924 let trace_id = otel_trace_id(attempt);
2925 let root_span_id = otel_span_id(attempt, "runtime.request", 0);
2926 let mut spans = vec![otel_span(OTelSpanInput {
2927 trace_id: &trace_id,
2928 span_id: &root_span_id,
2929 parent_span_id: None,
2930 name: "traverse.runtime.request",
2931 status: span_status(result.status),
2932 started_at: first_transition_time(state_transitions),
2933 ended_at: last_transition_time(state_transitions),
2934 attributes: base_otel_attributes(attempt, selection, execution),
2935 events: error_events(result),
2936 })];
2937
2938 for (index, phase) in otel_phase_names().iter().enumerate() {
2939 spans.push(otel_span(OTelSpanInput {
2940 trace_id: &trace_id,
2941 span_id: &otel_span_id(attempt, phase, index + 1),
2942 parent_span_id: Some(root_span_id.clone()),
2943 name: phase,
2944 status: phase_status(phase, result.status),
2945 started_at: phase_started_at(state_transitions, index),
2946 ended_at: phase_ended_at(state_transitions, index),
2947 attributes: base_otel_attributes(attempt, selection, execution),
2948 events: if result.status == RuntimeResultStatus::Error
2949 && *phase == "traverse.trace.assembly"
2950 {
2951 error_events(result)
2952 } else {
2953 Vec::new()
2954 },
2955 }));
2956 }
2957
2958 OTelTraceRecord {
2959 trace_id,
2960 parent_traceparent: attempt.request.context.traceparent.clone(),
2961 tracestate: attempt.request.context.tracestate.clone(),
2962 exporter: OTelExporterRecord {
2963 enabled: attempt.observability.exporter.endpoint.is_some(),
2964 endpoint: attempt.observability.exporter.endpoint.clone(),
2965 protocol: attempt.observability.exporter.protocol,
2966 },
2967 spans,
2968 }
2969}
2970
2971fn otel_phase_names() -> [&'static str; 5] {
2972 [
2973 "traverse.request.intake",
2974 "traverse.registry.lookup",
2975 "traverse.contract.validation",
2976 "traverse.capability.execution",
2977 "traverse.trace.assembly",
2978 ]
2979}
2980
2981fn otel_trace_id(attempt: &AttemptContext) -> String {
2982 if attempt.observability.deterministic_ids {
2983 let seed = attempt
2984 .observability
2985 .deterministic_seed
2986 .as_deref()
2987 .unwrap_or("traverse-test");
2988 return deterministic_hex(seed, &attempt.trace_id, 32);
2989 }
2990 deterministic_hex("traverse-runtime", &attempt.trace_id, 32)
2991}
2992
2993fn otel_span_id(attempt: &AttemptContext, name: &str, index: usize) -> String {
2994 let seed = attempt
2995 .observability
2996 .deterministic_seed
2997 .as_deref()
2998 .unwrap_or("traverse-runtime");
2999 deterministic_hex(
3000 seed,
3001 &format!("{}:{name}:{index}", attempt.execution_id),
3002 16,
3003 )
3004}
3005
3006fn deterministic_hex(seed: &str, value: &str, len: usize) -> String {
3007 let mut hash: u128 = 0x6c62_272e_07bb_0142_62b8_2175_6295_c58d;
3008 for byte in seed.as_bytes().iter().chain(value.as_bytes()) {
3009 hash ^= u128::from(*byte);
3010 hash = hash.wrapping_mul(0x0000_0000_0100_0000_0000_0000_0000_013b);
3011 }
3012 format!("{hash:032x}").chars().take(len).collect()
3013}
3014
3015struct OTelSpanInput<'a> {
3016 trace_id: &'a str,
3017 span_id: &'a str,
3018 parent_span_id: Option<String>,
3019 name: &'a str,
3020 status: OTelSpanStatus,
3021 started_at: String,
3022 ended_at: String,
3023 attributes: Vec<OTelAttribute>,
3024 events: Vec<OTelSpanEvent>,
3025}
3026
3027fn otel_span(input: OTelSpanInput<'_>) -> OTelSpanRecord {
3028 OTelSpanRecord {
3029 trace_id: input.trace_id.to_string(),
3030 span_id: input.span_id.to_string(),
3031 parent_span_id: input.parent_span_id,
3032 name: input.name.to_string(),
3033 kind: OTelSpanKind::Internal,
3034 status: input.status,
3035 started_at: input.started_at,
3036 ended_at: input.ended_at,
3037 attributes: input.attributes,
3038 events: input.events,
3039 }
3040}
3041
3042fn base_otel_attributes(
3043 attempt: &AttemptContext,
3044 selection: &SelectionRecord,
3045 execution: &ExecutionRecord,
3046) -> Vec<OTelAttribute> {
3047 let mut attributes = vec![
3048 otel_attr("traverse.request.id", json!(attempt.request.request_id)),
3049 otel_attr("traverse.execution.id", json!(attempt.execution_id)),
3050 otel_attr("traverse.lookup.scope", json!(attempt.request.lookup.scope)),
3051 otel_attr(
3052 "traverse.runtime.placement.target",
3053 json!(execution.placement_target),
3054 ),
3055 ];
3056 if let Some(correlation_id) = &attempt.request.context.correlation_id {
3057 attributes.push(otel_attr("traverse.correlation.id", json!(correlation_id)));
3058 }
3059 if let Some(capability_id) = &selection.selected_capability_id {
3060 attributes.push(otel_attr("traverse.capability.id", json!(capability_id)));
3061 }
3062 if let Some(capability_version) = &selection.selected_capability_version {
3063 attributes.push(otel_attr(
3064 "traverse.capability.version",
3065 json!(capability_version),
3066 ));
3067 }
3068 attributes
3069}
3070
3071fn otel_attr(key: &str, value: Value) -> OTelAttribute {
3072 OTelAttribute {
3073 key: key.to_string(),
3074 value,
3075 }
3076}
3077
3078fn error_events(result: &TraceResultRecord) -> Vec<OTelSpanEvent> {
3079 result
3080 .error
3081 .as_ref()
3082 .map(|error| {
3083 vec![OTelSpanEvent {
3084 name: "exception".to_string(),
3085 timestamp: "1970-01-01T00:00:00Z".to_string(),
3086 attributes: vec![
3087 otel_attr("traverse.error.classification", json!(error.code)),
3088 otel_attr("traverse.error.message", json!(error.message)),
3089 ],
3090 }]
3091 })
3092 .unwrap_or_default()
3093}
3094
3095fn span_status(status: RuntimeResultStatus) -> OTelSpanStatus {
3096 match status {
3097 RuntimeResultStatus::Completed => OTelSpanStatus::Ok,
3098 RuntimeResultStatus::Error => OTelSpanStatus::Error,
3099 }
3100}
3101
3102fn phase_status(phase: &str, status: RuntimeResultStatus) -> OTelSpanStatus {
3103 if status == RuntimeResultStatus::Error && phase == "traverse.trace.assembly" {
3104 OTelSpanStatus::Error
3105 } else {
3106 OTelSpanStatus::Ok
3107 }
3108}
3109
3110fn first_transition_time(transitions: &[RuntimeTransitionRecord]) -> String {
3111 transitions.first().map_or_else(
3112 || "1970-01-01T00:00:00Z".to_string(),
3113 |transition| transition.occurred_at.clone(),
3114 )
3115}
3116
3117fn last_transition_time(transitions: &[RuntimeTransitionRecord]) -> String {
3118 transitions.last().map_or_else(
3119 || "1970-01-01T00:00:00Z".to_string(),
3120 |transition| transition.occurred_at.clone(),
3121 )
3122}
3123
3124fn phase_started_at(transitions: &[RuntimeTransitionRecord], index: usize) -> String {
3125 transitions.get(index).map_or_else(
3126 || first_transition_time(transitions),
3127 |transition| transition.occurred_at.clone(),
3128 )
3129}
3130
3131fn phase_ended_at(transitions: &[RuntimeTransitionRecord], index: usize) -> String {
3132 transitions.get(index + 1).map_or_else(
3133 || last_transition_time(transitions),
3134 |transition| transition.occurred_at.clone(),
3135 )
3136}
3137
3138fn contains_drafts_segment(path: &str) -> bool {
3139 path.replace('\\', "/")
3140 .split('/')
3141 .any(|segment| segment == "drafts")
3142}
3143
3144struct AttemptContext {
3145 request: RuntimeRequest,
3146 execution_id: String,
3147 trace_id: String,
3148 observability: RuntimeObservabilityConfig,
3149 artifact_verification: Option<ArtifactVerificationRecord>,
3150 warnings: Vec<RuntimeWarning>,
3151}
3152
3153struct CandidateResolution {
3154 eligible: Vec<ResolvedCapability>,
3155 collection: CandidateCollectionRecord,
3156 candidate_reason: CandidateReason,
3157}
3158
3159struct FailureContext {
3160 attempt: AttemptContext,
3161 state_events: Vec<RuntimeStateEvent>,
3162 state_transitions: Vec<RuntimeTransitionRecord>,
3163 state_machine_validation: RuntimeStateMachineValidationEvidence,
3164 candidate_collection: CandidateCollectionRecord,
3165 selection: SelectionRecord,
3166 execution: ExecutionRecord,
3167 error: RuntimeError,
3168 emitted_events: Vec<traverse_contracts::EventReference>,
3169 workflow_evidence: Option<WorkflowTraversalEvidence>,
3170}
3171
3172struct ExecutionFailureState {
3173 artifact_ref: String,
3174 started_at: String,
3175 placement: PlacementDecisionRecord,
3176 failure_reason: ExecutionFailureReason,
3177}
3178
3179struct ExecutionContext {
3180 attempt: AttemptContext,
3181 emitter: StateEmitter,
3182 candidate_collection: CandidateCollectionRecord,
3183 selection: SelectionRecord,
3184}
3185
3186struct StartedExecution {
3187 started_at: String,
3188 placement: PlacementDecisionRecord,
3189}
3190
3191struct PreExecutionFailure {
3192 artifact_ref: Option<String>,
3193 failure_reason: ExecutionFailureReason,
3194 placement: PlacementDecisionRecord,
3195 error: RuntimeError,
3196 artifact_verification: Option<ArtifactVerificationRecord>,
3197}
3198
3199enum CandidateEvaluation {
3200 Eligible(ResolvedCapability),
3201 Rejected(ResolvedCapability, RejectedCandidateReason),
3202}
3203
3204struct StateEmitter {
3205 execution_id: String,
3206 request_id: String,
3207 next_second: u32,
3208 next_event_index: u32,
3209 current_state: RuntimeState,
3210 events: Vec<RuntimeStateEvent>,
3211 transitions: Vec<RuntimeTransitionRecord>,
3212 violations: Vec<Value>,
3213}
3214
3215struct FinishedStateMachineArtifacts {
3216 events: Vec<RuntimeStateEvent>,
3217 transitions: Vec<RuntimeTransitionRecord>,
3218 validation: RuntimeStateMachineValidationEvidence,
3219}
3220
3221fn start_selected_execution(
3222 emitter: &mut StateEmitter,
3223 selected: &ResolvedCapability,
3224 placement: PlacementDecisionRecord,
3225 identity: Option<&RuntimeIdentity>,
3226) -> StartedExecution {
3227 let started_at = emitter.next_timestamp();
3228 emitter.push(
3229 RuntimeState::Executing,
3230 RuntimeTransitionReasonCode::CandidateSelected,
3231 json!({
3232 "capability_id": selected.record.id,
3233 "capability_version": selected.record.version,
3234 "artifact_ref": selected.record.artifact_ref,
3235 "requested_target": placement.requested_target,
3236 "selected_target": placement.selected_target,
3237 "placement_status": placement.status,
3238 "placement_reason": placement.reason,
3239 "identity": identity,
3240 }),
3241 );
3242 StartedExecution {
3243 started_at,
3244 placement,
3245 }
3246}
3247
3248impl StateEmitter {
3249 fn new(execution_id: &str, request_id: &str) -> Self {
3250 Self {
3251 execution_id: execution_id.to_string(),
3252 request_id: request_id.to_string(),
3253 next_second: 0,
3254 next_event_index: 0,
3255 current_state: RuntimeState::Idle,
3256 events: Vec::new(),
3257 transitions: Vec::new(),
3258 violations: Vec::new(),
3259 }
3260 }
3261
3262 fn push(&mut self, state: RuntimeState, reason: RuntimeTransitionReasonCode, details: Value) {
3263 let transitioned = self.try_push(state, reason, details);
3264 debug_assert!(transitioned, "runtime state transition must be spec-valid");
3265 }
3266
3267 fn try_push(
3268 &mut self,
3269 state: RuntimeState,
3270 reason: RuntimeTransitionReasonCode,
3271 details: Value,
3272 ) -> bool {
3273 let from_state = self.current_state;
3274 if !is_allowed_transition(from_state, state, reason) {
3275 self.violations.push(json!({
3276 "from_state": from_state,
3277 "to_state": state,
3278 "reason_code": reason,
3279 "message": "unexpected runtime state transition"
3280 }));
3281 return false;
3282 }
3283 let entered_at = self.next_timestamp();
3284 let mut event_details = detail_object(details);
3285 event_details.insert(
3286 "transition_reason".to_string(),
3287 serde_json::to_value(reason)
3288 .unwrap_or_else(|_| Value::String("serialization_failed".to_string())),
3289 );
3290 let event = RuntimeStateEvent {
3291 kind: RUNTIME_STATE_EVENT_KIND.to_string(),
3292 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
3293 event_id: format!("rse_{}_{:04}", self.execution_id, self.next_event_index),
3294 execution_id: self.execution_id.clone(),
3295 request_id: self.request_id.clone(),
3296 state,
3297 entered_at: entered_at.clone(),
3298 details: Value::Object(event_details.clone()),
3299 };
3300 self.next_event_index += 1;
3301 self.events.push(event);
3302 self.transitions.push(RuntimeTransitionRecord {
3303 from_state,
3304 to_state: state,
3305 reason_code: reason,
3306 occurred_at: entered_at,
3307 request_id: Some(self.request_id.clone()),
3308 execution_id: Some(self.execution_id.clone()),
3309 details: Some(Value::Object(event_details)),
3310 });
3311 self.current_state = state;
3312 true
3313 }
3314
3315 fn next_timestamp(&mut self) -> String {
3316 let timestamp = format!("1970-01-01T00:00:{:02}Z", self.next_second);
3317 self.next_second += 1;
3318 timestamp
3319 }
3320
3321 fn finish(self) -> FinishedStateMachineArtifacts {
3322 let checked_states = vec![
3323 RuntimeState::Idle,
3324 RuntimeState::LoadingRegistry,
3325 RuntimeState::Ready,
3326 RuntimeState::Discovering,
3327 RuntimeState::EvaluatingConstraints,
3328 RuntimeState::Selecting,
3329 RuntimeState::Executing,
3330 RuntimeState::EmittingEvents,
3331 RuntimeState::Completed,
3332 RuntimeState::Error,
3333 ];
3334 let checked_transitions = self
3335 .transitions
3336 .iter()
3337 .map(|transition| {
3338 format!(
3339 "{}->{}",
3340 runtime_state_name(transition.from_state),
3341 runtime_state_name(transition.to_state)
3342 )
3343 })
3344 .collect();
3345 let validation = RuntimeStateMachineValidationEvidence {
3346 kind: RUNTIME_STATE_MACHINE_VALIDATION_KIND.to_string(),
3347 schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
3348 governing_spec: STATE_MACHINE_GOVERNING_SPEC.to_string(),
3349 validated_at: format!(
3350 "1970-01-01T00:00:{:02}Z",
3351 self.next_second.saturating_sub(1)
3352 ),
3353 status: if self.violations.is_empty() {
3354 RuntimeStateMachineValidationStatus::Passed
3355 } else {
3356 RuntimeStateMachineValidationStatus::Failed
3357 },
3358 checked_states,
3359 checked_transitions,
3360 violations: self.violations,
3361 };
3362 FinishedStateMachineArtifacts {
3363 events: self.events,
3364 transitions: self.transitions,
3365 validation,
3366 }
3367 }
3368}
3369
3370fn is_allowed_transition(
3371 from: RuntimeState,
3372 to: RuntimeState,
3373 reason: RuntimeTransitionReasonCode,
3374) -> bool {
3375 matches!(
3376 (from, to, reason),
3377 (
3378 RuntimeState::Idle,
3379 RuntimeState::LoadingRegistry,
3380 RuntimeTransitionReasonCode::RuntimeInitializationStarted
3381 ) | (
3382 RuntimeState::LoadingRegistry,
3383 RuntimeState::Ready,
3384 RuntimeTransitionReasonCode::RegistryLoaded
3385 ) | (
3386 RuntimeState::LoadingRegistry,
3387 RuntimeState::Error,
3388 RuntimeTransitionReasonCode::RegistryLoadFailed
3389 ) | (
3390 RuntimeState::Ready,
3391 RuntimeState::Discovering,
3392 RuntimeTransitionReasonCode::RequestStarted
3393 ) | (
3394 RuntimeState::Discovering,
3395 RuntimeState::EvaluatingConstraints,
3396 RuntimeTransitionReasonCode::CandidatesCollected
3397 ) | (
3398 RuntimeState::Discovering,
3399 RuntimeState::Error,
3400 RuntimeTransitionReasonCode::NoMatch
3401 ) | (
3402 RuntimeState::EvaluatingConstraints,
3403 RuntimeState::Selecting,
3404 RuntimeTransitionReasonCode::ConstraintsEvaluated
3405 ) | (
3406 RuntimeState::EvaluatingConstraints,
3407 RuntimeState::Error,
3408 RuntimeTransitionReasonCode::ConstraintValidationFailed
3409 ) | (
3410 RuntimeState::Selecting,
3411 RuntimeState::Executing,
3412 RuntimeTransitionReasonCode::CandidateSelected
3413 ) | (
3414 RuntimeState::Selecting,
3415 RuntimeState::Error,
3416 RuntimeTransitionReasonCode::SelectionFailed
3417 ) | (
3418 RuntimeState::Executing,
3419 RuntimeState::EmittingEvents,
3420 RuntimeTransitionReasonCode::ExecutionSucceededWithEvents
3421 ) | (
3422 RuntimeState::Executing,
3423 RuntimeState::Completed,
3424 RuntimeTransitionReasonCode::ExecutionSucceeded
3425 ) | (
3426 RuntimeState::Executing,
3427 RuntimeState::Error,
3428 RuntimeTransitionReasonCode::ExecutionFailed
3429 ) | (
3430 RuntimeState::EmittingEvents,
3431 RuntimeState::Completed,
3432 RuntimeTransitionReasonCode::EventsEmitted
3433 ) | (
3434 RuntimeState::EmittingEvents,
3435 RuntimeState::Error,
3436 RuntimeTransitionReasonCode::EventEmissionFailed
3437 ) | (
3438 RuntimeState::Completed | RuntimeState::Error,
3439 RuntimeState::Ready,
3440 RuntimeTransitionReasonCode::ExecutionClosed
3441 )
3442 )
3443}
3444
3445fn detail_object(details: Value) -> Map<String, Value> {
3446 match details {
3447 Value::Object(map) => map,
3448 other => {
3449 let mut map = Map::new();
3450 map.insert("value".to_string(), other);
3451 map
3452 }
3453 }
3454}
3455
3456fn runtime_state_name(state: RuntimeState) -> &'static str {
3457 match state {
3458 RuntimeState::Idle => "idle",
3459 RuntimeState::LoadingRegistry => "loading_registry",
3460 RuntimeState::Ready => "ready",
3461 RuntimeState::Discovering => "discovering",
3462 RuntimeState::EvaluatingConstraints => "evaluating_constraints",
3463 RuntimeState::Selecting => "selecting",
3464 RuntimeState::Executing => "executing",
3465 RuntimeState::EmittingEvents => "emitting_events",
3466 RuntimeState::Completed => "completed",
3467 RuntimeState::Error => "error",
3468 }
3469}
3470
3471#[cfg(test)]
3472mod tests {
3473 #![allow(clippy::expect_used)]
3474
3475 use std::fmt::Write as _;
3476
3477 use super::security::{
3478 ArtifactVerificationFailure, ArtifactVerificationScheme, ArtifactVerificationStatus,
3479 RuntimeSecurityConfig, derive_identity_from_jwt, verify_artifact,
3480 };
3481 use super::{
3482 BrowserRuntimeSubscriptionErrorCode, BrowserRuntimeSubscriptionMessage,
3483 BrowserRuntimeSubscriptionRequest, CandidateEvaluation, CandidateReason, LocalExecutor,
3484 PlacementTarget, RejectedCandidateReason, Runtime, RuntimeContext, RuntimeIntent,
3485 RuntimeLookup, RuntimeLookupScope, RuntimeLookupScope::*, RuntimeRequest,
3486 RuntimeResultStatus, RuntimeState, RuntimeTransitionReasonCode,
3487 browser_subscription_messages, evaluate_candidate, map_implementation_kind, map_lifecycle,
3488 map_registry_scope, parse_runtime_request, runtime_candidate, subscription_targets_outcome,
3489 validate_browser_subscription_request, validate_payload_against_contract, validate_request,
3490 };
3491 use ed25519_dalek::{Signer, SigningKey};
3492 use serde_json::json;
3493 use sha2::{Digest, Sha256};
3494 use std::collections::BTreeMap;
3495 use std::fs;
3496 use std::path::{Path, PathBuf};
3497 use std::sync::atomic::{AtomicU64, Ordering};
3498 use std::sync::{Arc, Mutex};
3499 use traverse_contracts::{
3500 BinaryFormat as ContractBinaryFormat, Entrypoint, EntrypointKind, Execution,
3501 ExecutionConstraints, ExecutionTarget, FilesystemAccess, HostApiAccess, Lifecycle,
3502 NetworkAccess, Owner, Provenance, ProvenanceSource, SchemaContainer, ServiceType,
3503 };
3504 use traverse_registry::{
3505 ArtifactDigests, ArtifactSignature, ArtifactSignatureScheme, BinaryFormat, BinaryReference,
3506 CapabilityArtifactRecord, CapabilityRegistration, CapabilityRegistry,
3507 CapabilityRegistryRecord, ComposabilityMetadata, CompositionKind, CompositionPattern,
3508 DiscoveryIndexEntry, ImplementationKind, ModelCandidateReadiness,
3509 ModelCandidateRejectionCode, ModelResolutionEvidence, ModelResolutionPhase,
3510 RegistryProvenance, RegistryScope, ResolvedCapability, SelectedModelCandidate, SourceKind,
3511 SourceReference, WorkspaceAppStateErrorCode,
3512 };
3513
3514 const HEX_TABLE: &[u8; 16] = b"0123456789abcdef";
3515 static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
3516
3517 #[derive(Debug, Default)]
3518 struct RecordingEventSink {
3519 events: Mutex<Vec<super::events::TraverseEvent>>,
3520 }
3521
3522 impl super::events::RuntimeEventSink for RecordingEventSink {
3523 fn emit(
3524 &self,
3525 event: super::events::TraverseEvent,
3526 ) -> Result<(), super::events::EventError> {
3527 self.events
3528 .lock()
3529 .expect("recording sink lock must not be poisoned")
3530 .push(event);
3531 Ok(())
3532 }
3533 }
3534
3535 #[derive(Debug)]
3536 struct FailingEventSink;
3537
3538 impl super::events::RuntimeEventSink for FailingEventSink {
3539 fn emit(
3540 &self,
3541 _event: super::events::TraverseEvent,
3542 ) -> Result<(), super::events::EventError> {
3543 Err(super::events::EventError::JournalWrite(
3544 "sink unavailable".to_string(),
3545 ))
3546 }
3547 }
3548
3549 #[test]
3550 fn missing_binary_metadata_is_eligible_for_native_host_execution() {
3551 let capability = resolved_capability(None, Lifecycle::Active);
3552
3553 let evaluation = evaluate_candidate(capability);
3554
3555 assert!(matches!(evaluation, CandidateEvaluation::Eligible(_)));
3556 }
3557
3558 #[test]
3559 fn live_wiring_helpers_support_native_and_wasm_artifact_types() {
3560 use super::executor::ArtifactType;
3561 use super::placement::PlacementError;
3562 use super::router::RouterError;
3563 use super::{ExecutionFailureReason, RuntimeErrorCode};
3564
3565 let native = resolved_capability(None, Lifecycle::Active);
3566 assert_eq!(super::artifact_type_for(&native), ArtifactType::Native);
3567 assert_eq!(
3568 super::executor_capability_for(&native, ArtifactType::Native).artifact_type,
3569 ArtifactType::Native
3570 );
3571
3572 let wasm = resolved_capability(
3573 Some(traverse_registry::BinaryReference {
3574 format: traverse_registry::BinaryFormat::Wasm,
3575 location: "artifact.wasm".to_string(),
3576 signature: None,
3577 }),
3578 Lifecycle::Active,
3579 );
3580 assert_eq!(super::artifact_type_for(&wasm), ArtifactType::Wasm);
3581
3582 let (code, reason, _) =
3583 super::map_router_error(&RouterError::ExecutorNotFound("Native".to_string()));
3584 assert_eq!(code.code, RuntimeErrorCode::CapabilityNotRunnable);
3585 assert_eq!(reason, ExecutionFailureReason::ArtifactNotRunnable);
3586
3587 let (code, reason, _) = super::map_router_error(&RouterError::TraceLockPoisoned);
3588 assert_eq!(code.code, RuntimeErrorCode::ExecutionFailed);
3589 assert_eq!(reason, ExecutionFailureReason::ExecutionFailed);
3590
3591 let (code, reason, _) = super::map_router_error(&RouterError::PlacementFailed(
3592 PlacementError::NoEligibleTarget,
3593 ));
3594 assert_eq!(code.code, RuntimeErrorCode::PlacementUnsupported);
3595 assert_eq!(reason, ExecutionFailureReason::PlacementUnsupported);
3596
3597 let (code, reason, _) =
3598 super::map_router_error(&RouterError::ExecutionFailed("boom".to_string()));
3599 assert_eq!(code.code, RuntimeErrorCode::ExecutionFailed);
3600 assert_eq!(reason, ExecutionFailureReason::ExecutionFailed);
3601
3602 let (code, reason, _) = super::map_router_error(&RouterError::ContractViolation(vec![
3608 traverse_contracts::ViolationRecord::new(
3609 "undeclared_event_emission",
3610 "test.cap",
3611 "test message",
3612 ),
3613 ]));
3614 assert_eq!(code.code, RuntimeErrorCode::ContractViolation);
3615 assert_eq!(reason, ExecutionFailureReason::ExecutionFailed);
3616
3617 let (code, reason, _) = super::map_router_error(&RouterError::DurableTraceWriteFailed(
3618 "simulated durable trace write failure".to_string(),
3619 ));
3620 assert_eq!(code.code, RuntimeErrorCode::ExecutionFailed);
3621 assert_eq!(reason, ExecutionFailureReason::ExecutionFailed);
3622 }
3623
3624 #[test]
3625 fn live_wiring_runtime_surfaces_and_fallback_broker_are_covered() {
3626 use super::PlacementTarget;
3627 use super::events::{EventError, LifecycleStatus, TraverseEvent};
3628 use traverse_contracts::ExecutionTarget;
3629
3630 let runtime = Runtime::new(CapabilityRegistry::new(), NoopExecutor);
3631 let _ = runtime.clone();
3632 let debug = format!("{runtime:?}");
3633 assert!(debug.contains("Runtime"));
3634 assert!(Arc::ptr_eq(
3635 &runtime.event_broker(),
3636 &runtime.event_broker()
3637 ));
3638 assert!(Arc::ptr_eq(&runtime.trace_store(), &runtime.trace_store()));
3639
3640 let mapped = [
3641 (PlacementTarget::Local, ExecutionTarget::Local),
3642 (PlacementTarget::Browser, ExecutionTarget::Browser),
3643 (PlacementTarget::Edge, ExecutionTarget::Edge),
3644 (PlacementTarget::Cloud, ExecutionTarget::Cloud),
3645 (PlacementTarget::Worker, ExecutionTarget::Worker),
3646 (PlacementTarget::Device, ExecutionTarget::Device),
3647 ];
3648 for (placement, expected) in mapped {
3649 assert_eq!(super::execution_target_from_placement(placement), expected);
3650 }
3651
3652 let discard = super::event_broker_or_discard(Err(EventError::InvalidRetentionWindow(
3653 "forced".to_string(),
3654 )));
3655 assert!(
3656 discard
3657 .publish(TraverseEvent {
3658 id: "evt".to_string(),
3659 source: "test".to_string(),
3660 event_type: "dev.traverse.discard".to_string(),
3661 datacontenttype: "application/json".to_string(),
3662 time: "2026-08-06T00:00:00Z".to_string(),
3663 data: json!({}),
3664 owner: "test".to_string(),
3665 version: "1.0.0".to_string(),
3666 lifecycle_status: LifecycleStatus::Active,
3667 deduplication_id: None,
3668 ordering_scope: None,
3669 correlation_id: None,
3670 causation_id: None,
3671 subject_id: None,
3672 actor_id: None,
3673 })
3674 .is_ok()
3675 );
3676 assert!(discard.subscribe("dev.traverse.discard", "0").is_err());
3677 assert!(
3678 discard
3679 .subscribe_for_subject("dev.traverse.discard", "0", None)
3680 .is_err()
3681 );
3682 assert!(discard.poll("missing", 1).is_err());
3683 assert!(discard.cancel("missing").is_err());
3684 }
3685
3686 fn run_native_executor_through_placement_router<E: super::LocalExecutor + 'static>(
3693 executor: E,
3694 ) -> (
3695 Result<super::router::RouterResponse, super::router::RouterError>,
3696 Arc<super::events::InProcessBroker>,
3697 String,
3698 ) {
3699 use super::events::{
3700 EventBroker, EventCatalog, EventCatalogEntry, InProcessBroker, LifecycleStatus,
3701 };
3702 use super::executor::ArtifactType;
3703 use super::placement::PlacementConstraintEvaluator;
3704 use super::router::{CapabilityExecutorRegistry, PlacementRouter, RouterRequest};
3705 use super::trace::TraceStore;
3706
3707 let event_type = "dev.traverse.native.live-emitted";
3708 let catalog = Arc::new(EventCatalog::new());
3709 catalog
3710 .register(EventCatalogEntry {
3711 event_type: event_type.to_string(),
3712 owner: "native.live".to_string(),
3713 version: "1.0.0".to_string(),
3714 lifecycle_status: LifecycleStatus::Active,
3715 consumer_count: 0,
3716 })
3717 .expect("catalog entry should register");
3718 let broker = Arc::new(InProcessBroker::new(catalog).expect("broker should construct"));
3719 let subscription = broker
3720 .subscribe(event_type, "0")
3721 .expect("subscribe should succeed");
3722 let trace_store = Arc::new(Mutex::new(TraceStore::new()));
3723
3724 let mut selected = resolved_capability(None, Lifecycle::Active);
3725 selected.contract.service_type = ServiceType::Subscribable;
3726 selected.contract.event_trigger = Some("dev.traverse.native.trigger".to_string());
3727 selected.contract.emits = vec![traverse_contracts::EventReference {
3728 event_id: event_type.to_string(),
3729 version: "1.0.0".to_string(),
3730 }];
3731 selected.contract.permitted_targets = vec![ExecutionTarget::Local, ExecutionTarget::Cloud];
3732
3733 let mut registry = CapabilityExecutorRegistry::new();
3734 registry.insert(
3735 ArtifactType::Native,
3736 Box::new(super::BoundLocalExecutor {
3737 executor: Arc::new(executor),
3738 selected: selected.clone(),
3739 }),
3740 );
3741 let router = PlacementRouter::new(
3742 PlacementConstraintEvaluator,
3743 registry,
3744 Arc::clone(&trace_store),
3745 broker.clone(),
3746 );
3747
3748 let response = router.execute(RouterRequest {
3749 capability_id: selected.record.id.clone(),
3750 artifact_type: ArtifactType::Native,
3751 contract: selected.contract.clone(),
3752 target_hint: Some(ExecutionTarget::Local),
3753 runtime_snapshot: super::idle_runtime_snapshot(),
3754 input: json!({}),
3755 executor_capability: super::executor_capability_for(&selected, ArtifactType::Native),
3756 trace_id_override: Some("trace_native_live".to_string()),
3757 });
3758
3759 (response, broker, subscription.subscription_id)
3760 }
3761
3762 fn native_traverse_event(event_type: &str) -> super::events::TraverseEvent {
3763 super::events::TraverseEvent {
3764 id: "native-event-1".to_string(),
3765 source: "traverse-runtime/test.native".to_string(),
3766 event_type: event_type.to_string(),
3767 datacontenttype: "application/json".to_string(),
3768 time: "2026-01-01T00:00:00Z".to_string(),
3769 data: json!({}),
3770 owner: "test.native".to_string(),
3771 version: "1.0.0".to_string(),
3772 lifecycle_status: super::events::LifecycleStatus::Active,
3773 deduplication_id: Some("native-event-1".to_string()),
3774 ordering_scope: Some("test.native".to_string()),
3775 correlation_id: None,
3776 causation_id: None,
3777 subject_id: None,
3778 actor_id: None,
3779 }
3780 }
3781
3782 #[test]
3783 fn bound_local_executor_publishes_declared_native_events_through_placement_router() {
3784 use super::events::EventBroker;
3785 use serde_json::Value;
3786 struct NativeEmitExecutor;
3793 impl super::LocalExecutor for NativeEmitExecutor {
3794 fn execute(
3795 &self,
3796 _capability: &ResolvedCapability,
3797 _input: &Value,
3798 ) -> Result<super::LocalExecutionOutput, super::LocalExecutionFailure> {
3799 Ok(super::LocalExecutionOutput {
3800 value: json!({ "draft_id": "native-1" }),
3801 emitted_events: vec![native_traverse_event("dev.traverse.native.live-emitted")],
3802 })
3803 }
3804 }
3805
3806 let (response, broker, subscription_id) =
3807 run_native_executor_through_placement_router(NativeEmitExecutor);
3808 let response = response.expect("native placement router execution should succeed");
3809
3810 assert_eq!(response.trace_id, "trace_native_live");
3811 assert_eq!(response.emitted_events.len(), 1);
3812 let poll = broker
3813 .poll(&subscription_id, 10)
3814 .expect("poll should succeed");
3815 assert_eq!(poll.events.len(), 1);
3816 assert_eq!(
3817 poll.events[0].event.event_type,
3818 "dev.traverse.native.live-emitted"
3819 );
3820 }
3821
3822 #[test]
3823 fn bound_local_executor_rejects_undeclared_native_event() {
3824 use super::events::EventBroker;
3825 use serde_json::Value;
3826 struct UndeclaredEmitExecutor;
3831 impl super::LocalExecutor for UndeclaredEmitExecutor {
3832 fn execute(
3833 &self,
3834 _capability: &ResolvedCapability,
3835 _input: &Value,
3836 ) -> Result<super::LocalExecutionOutput, super::LocalExecutionFailure> {
3837 Ok(super::LocalExecutionOutput {
3838 value: json!({ "draft_id": "native-1" }),
3839 emitted_events: vec![native_traverse_event("dev.traverse.native.undeclared")],
3840 })
3841 }
3842 }
3843
3844 let (response, broker, subscription_id) =
3845 run_native_executor_through_placement_router(UndeclaredEmitExecutor);
3846 assert!(response.is_err());
3847 let poll = broker
3848 .poll(&subscription_id, 10)
3849 .expect("poll should succeed");
3850 assert!(poll.events.is_empty());
3851 }
3852
3853 #[test]
3854 fn bound_local_executor_rejects_native_event_from_non_subscribable_capability() {
3855 use serde_json::Value;
3856 struct NonSubscribableEmitExecutor;
3860 impl super::LocalExecutor for NonSubscribableEmitExecutor {
3861 fn execute(
3862 &self,
3863 _capability: &ResolvedCapability,
3864 _input: &Value,
3865 ) -> Result<super::LocalExecutionOutput, super::LocalExecutionFailure> {
3866 Ok(super::LocalExecutionOutput {
3867 value: json!({ "draft_id": "native-1" }),
3868 emitted_events: vec![native_traverse_event("dev.traverse.native.live-emitted")],
3869 })
3870 }
3871 }
3872
3873 use super::events::{
3874 EventBroker, EventCatalog, EventCatalogEntry, InProcessBroker, LifecycleStatus,
3875 };
3876 use super::executor::ArtifactType;
3877 use super::placement::PlacementConstraintEvaluator;
3878 use super::router::{CapabilityExecutorRegistry, PlacementRouter, RouterRequest};
3879 use super::trace::TraceStore;
3880
3881 let event_type = "dev.traverse.native.live-emitted";
3882 let catalog = Arc::new(EventCatalog::new());
3883 catalog
3884 .register(EventCatalogEntry {
3885 event_type: event_type.to_string(),
3886 owner: "native.live".to_string(),
3887 version: "1.0.0".to_string(),
3888 lifecycle_status: LifecycleStatus::Active,
3889 consumer_count: 0,
3890 })
3891 .expect("catalog entry should register");
3892 let broker = Arc::new(InProcessBroker::new(catalog).expect("broker should construct"));
3893 let subscription = broker
3894 .subscribe(event_type, "0")
3895 .expect("subscribe should succeed");
3896 let trace_store = Arc::new(Mutex::new(TraceStore::new()));
3897
3898 let mut selected = resolved_capability(None, Lifecycle::Active);
3899 selected.contract.service_type = ServiceType::Stateless;
3900 selected.contract.emits = vec![traverse_contracts::EventReference {
3901 event_id: event_type.to_string(),
3902 version: "1.0.0".to_string(),
3903 }];
3904 selected.contract.permitted_targets = vec![ExecutionTarget::Local, ExecutionTarget::Cloud];
3905
3906 let mut registry = CapabilityExecutorRegistry::new();
3907 registry.insert(
3908 ArtifactType::Native,
3909 Box::new(super::BoundLocalExecutor {
3910 executor: Arc::new(NonSubscribableEmitExecutor),
3911 selected: selected.clone(),
3912 }),
3913 );
3914 let router = PlacementRouter::new(
3915 PlacementConstraintEvaluator,
3916 registry,
3917 Arc::clone(&trace_store),
3918 broker.clone(),
3919 );
3920
3921 let response = router.execute(RouterRequest {
3922 capability_id: selected.record.id.clone(),
3923 artifact_type: ArtifactType::Native,
3924 contract: selected.contract.clone(),
3925 target_hint: Some(ExecutionTarget::Local),
3926 runtime_snapshot: super::idle_runtime_snapshot(),
3927 input: json!({}),
3928 executor_capability: super::executor_capability_for(&selected, ArtifactType::Native),
3929 trace_id_override: Some("trace_native_live_non_subscribable".to_string()),
3930 });
3931
3932 assert!(response.is_err());
3933 let poll = broker
3934 .poll(&subscription.subscription_id, 10)
3935 .expect("poll should succeed");
3936 assert!(poll.events.is_empty());
3937 }
3938
3939 #[test]
3940 fn invalid_json_request_reports_parse_error_text() {
3941 let error = parse_runtime_request("{invalid").err();
3942
3943 assert!(error.is_some());
3944 let message = error.map(|item| item.to_string()).unwrap_or_default();
3945 assert!(!message.is_empty());
3946 }
3947
3948 #[test]
3949 fn runtime_loads_durable_workspace_app_state() {
3950 let workspace_root = unique_workspace_state_dir();
3951 write_runtime_workspace_app_state_fixture(&workspace_root, "local");
3952
3953 let runtime = Runtime::from_workspace_app_state(
3954 &workspace_root,
3955 "local",
3956 NoopExecutor,
3957 "test-runtime",
3958 )
3959 .expect("workspace app state should load");
3960
3961 assert!(
3962 runtime
3963 .capability_registry()
3964 .find_exact(
3965 traverse_registry::LookupScope::PreferPrivate,
3966 "expedition.planning.validate-team-readiness",
3967 "1.0.0"
3968 )
3969 .is_some()
3970 );
3971 assert!(
3972 runtime
3973 .workflow_registry()
3974 .find_exact(
3975 traverse_registry::LookupScope::PreferPrivate,
3976 "expedition.planning.plan-expedition",
3977 "1.0.0"
3978 )
3979 .is_some()
3980 );
3981 assert_eq!(
3982 runtime.workspace_applications()[0].model_dependencies[0].interface_id,
3983 "traverse.inference.generate"
3984 );
3985 }
3986
3987 #[test]
3988 fn governed_model_execution_resolves_from_loaded_app_declaration() {
3989 let workspace_root = unique_workspace_state_dir();
3990 write_runtime_workspace_app_state_fixture(&workspace_root, "local");
3991 let runtime = Runtime::from_workspace_app_state(
3992 &workspace_root,
3993 "local",
3994 NoopExecutor,
3995 "test-runtime",
3996 )
3997 .expect("workspace app state should load");
3998 let mut provider_configs = BTreeMap::new();
3999 provider_configs.insert(
4000 "ollama.local.generate".to_string(),
4001 crate::inference::OllamaProviderConfig {
4002 base_url: "http://127.0.0.1:9".to_string(),
4003 request_timeout_ms: Some(50),
4004 max_response_bytes: None,
4005 },
4006 );
4007
4008 let error = runtime
4009 .execute_governed_model_dependency(
4010 "expedition.readiness",
4011 "1.0.0",
4012 &crate::inference::GovernedModelExecutionRequest {
4013 interface_id: "traverse.inference.generate".to_string(),
4014 prompt: "Summarize readiness.".to_string(),
4015 system_prompt: None,
4016 options: json!({}),
4017 requested_placement: ExecutionTarget::Local,
4018 provider_configs,
4019 },
4020 )
4021 .expect_err("unavailable local provider should fail before output");
4022
4023 assert_eq!(
4024 error.code,
4025 crate::inference::GovernedModelExecutionErrorCode::ModelDependencyUnsatisfied
4026 );
4027 let evidence = error
4028 .model_resolution
4029 .expect("failed model execution should include resolution evidence");
4030 assert_eq!(
4031 evidence.requested_interface_id,
4032 "traverse.inference.generate"
4033 );
4034 assert_eq!(
4035 evidence.machine_failure_code(),
4036 Some("model_dependency_unsatisfied")
4037 );
4038 }
4039
4040 #[test]
4041 fn governed_model_execution_rejects_missing_app_or_interface() {
4042 let workspace_root = unique_workspace_state_dir();
4043 write_runtime_workspace_app_state_fixture(&workspace_root, "local");
4044 let runtime = Runtime::from_workspace_app_state(
4045 &workspace_root,
4046 "local",
4047 NoopExecutor,
4048 "test-runtime",
4049 )
4050 .expect("workspace app state should load");
4051 let request = crate::inference::GovernedModelExecutionRequest {
4052 interface_id: "traverse.inference.embed".to_string(),
4053 prompt: "Summarize readiness.".to_string(),
4054 system_prompt: None,
4055 options: json!({}),
4056 requested_placement: ExecutionTarget::Local,
4057 provider_configs: BTreeMap::new(),
4058 };
4059
4060 let missing_app = runtime
4061 .execute_governed_model_dependency("missing.app", "1.0.0", &request)
4062 .expect_err("unknown app should fail");
4063 assert_eq!(
4064 missing_app.code,
4065 crate::inference::GovernedModelExecutionErrorCode::InterfaceNotDeclared
4066 );
4067
4068 let missing_interface = runtime
4069 .execute_governed_model_dependency("expedition.readiness", "1.0.0", &request)
4070 .expect_err("undeclared interface should fail");
4071 assert_eq!(
4072 missing_interface.code,
4073 crate::inference::GovernedModelExecutionErrorCode::InterfaceNotDeclared
4074 );
4075 }
4076
4077 #[test]
4078 fn runtime_reports_missing_workspace_app_state() {
4079 let workspace_root = unique_workspace_state_dir();
4080
4081 let failure = Runtime::from_workspace_app_state(
4082 &workspace_root,
4083 "local",
4084 NoopExecutor,
4085 "test-runtime",
4086 )
4087 .expect_err("missing workspace app state should fail");
4088
4089 assert_eq!(
4090 failure.errors[0].code,
4091 WorkspaceAppStateErrorCode::MissingWorkspaceState
4092 );
4093 }
4094
4095 #[test]
4096 fn request_validation_rejects_all_invalid_request_guards() {
4097 let mut request = valid_request();
4098 request.kind = "wrong".to_string();
4099 assert_eq!(
4100 validate_request(&request).map(|error| error.code),
4101 Some(super::RuntimeErrorCode::RequestInvalid)
4102 );
4103
4104 let mut request = valid_request();
4105 request.schema_version = "9.9.9".to_string();
4106 assert_eq!(
4107 validate_request(&request).map(|error| error.code),
4108 Some(super::RuntimeErrorCode::RequestInvalid)
4109 );
4110
4111 let mut request = valid_request();
4112 request.governing_spec = "wrong-spec".to_string();
4113 assert_eq!(
4114 validate_request(&request).map(|error| error.code),
4115 Some(super::RuntimeErrorCode::RequestInvalid)
4116 );
4117
4118 let mut request = valid_request();
4119 request.request_id.clear();
4120 assert_eq!(
4121 validate_request(&request).map(|error| error.code),
4122 Some(super::RuntimeErrorCode::RequestInvalid)
4123 );
4124
4125 let mut request = valid_request();
4126 request.lookup.allow_ambiguity = true;
4127 assert_eq!(
4128 validate_request(&request).map(|error| error.code),
4129 Some(super::RuntimeErrorCode::RequestInvalid)
4130 );
4131
4132 let mut request = valid_request();
4133 request.context.requested_target = PlacementTarget::Local;
4134 request.intent.capability_version = Some("bad".to_string());
4135 assert_eq!(
4136 validate_request(&request).map(|error| error.code),
4137 Some(super::RuntimeErrorCode::RequestInvalid)
4138 );
4139
4140 let mut request = valid_request();
4141 request.intent.capability_id = None;
4142 request.intent.intent_key = None;
4143 request.intent.capability_version = None;
4144 assert_eq!(
4145 validate_request(&request).map(|error| error.code),
4146 Some(super::RuntimeErrorCode::RequestInvalid)
4147 );
4148
4149 let mut request = valid_request();
4150 request.intent.capability_id = None;
4151 request.intent.capability_version = Some("1.0.0".to_string());
4152 assert_eq!(
4153 validate_request(&request).map(|error| error.code),
4154 Some(super::RuntimeErrorCode::RequestInvalid)
4155 );
4156 }
4157
4158 #[test]
4159 fn candidate_evaluation_covers_local_runnability_branches() {
4160 let mut capability = resolved_capability(
4161 Some(traverse_registry::BinaryReference {
4162 format: traverse_registry::BinaryFormat::Wasm,
4163 location: "artifact.wasm".to_string(),
4164 signature: None,
4165 }),
4166 Lifecycle::Active,
4167 );
4168 capability.record.implementation_kind = ImplementationKind::Workflow;
4169 assert!(matches!(
4170 evaluate_candidate(capability.clone()),
4171 CandidateEvaluation::Rejected(_, RejectedCandidateReason::ArtifactMissing)
4172 ));
4173 capability.artifact.workflow_ref = Some(traverse_registry::WorkflowReference {
4174 workflow_id: "workflow".to_string(),
4175 workflow_version: "1.0.0".to_string(),
4176 });
4177 assert!(matches!(
4178 evaluate_candidate(capability),
4179 CandidateEvaluation::Eligible(_)
4180 ));
4181
4182 let capability = resolved_capability(
4183 Some(traverse_registry::BinaryReference {
4184 format: traverse_registry::BinaryFormat::Wasm,
4185 location: String::new(),
4186 signature: None,
4187 }),
4188 Lifecycle::Active,
4189 );
4190 assert!(matches!(
4191 evaluate_candidate(capability),
4192 CandidateEvaluation::Rejected(_, RejectedCandidateReason::ArtifactMissing)
4193 ));
4194
4195 let mut capability = resolved_capability(
4196 Some(traverse_registry::BinaryReference {
4197 format: traverse_registry::BinaryFormat::Wasm,
4198 location: "artifact.wasm".to_string(),
4199 signature: None,
4200 }),
4201 Lifecycle::Active,
4202 );
4203 capability.contract.execution.preferred_targets = vec![ExecutionTarget::Cloud];
4204 assert!(matches!(
4205 evaluate_candidate(capability),
4206 CandidateEvaluation::Rejected(_, RejectedCandidateReason::NotRunnableLocally)
4207 ));
4208
4209 let mut capability = resolved_capability(
4210 Some(traverse_registry::BinaryReference {
4211 format: traverse_registry::BinaryFormat::Wasm,
4212 location: "artifact.wasm".to_string(),
4213 signature: None,
4214 }),
4215 Lifecycle::Active,
4216 );
4217 capability.contract.execution.constraints.host_api_access =
4218 HostApiAccess::ExceptionRequired;
4219 assert!(matches!(
4220 evaluate_candidate(capability),
4221 CandidateEvaluation::Rejected(_, RejectedCandidateReason::NotRunnableLocally)
4222 ));
4223
4224 let mut capability = resolved_capability(
4225 Some(traverse_registry::BinaryReference {
4226 format: traverse_registry::BinaryFormat::Wasm,
4227 location: "artifact.wasm".to_string(),
4228 signature: None,
4229 }),
4230 Lifecycle::Active,
4231 );
4232 capability.contract.execution.constraints.network_access = NetworkAccess::Required;
4233 assert!(matches!(
4234 evaluate_candidate(capability),
4235 CandidateEvaluation::Rejected(_, RejectedCandidateReason::NotRunnableLocally)
4236 ));
4237
4238 let capability = resolved_capability(
4239 Some(traverse_registry::BinaryReference {
4240 format: traverse_registry::BinaryFormat::Wasm,
4241 location: "artifact.wasm".to_string(),
4242 signature: None,
4243 }),
4244 Lifecycle::Active,
4245 );
4246 assert!(matches!(
4247 evaluate_candidate(capability),
4248 CandidateEvaluation::Eligible(_)
4249 ));
4250 }
4251
4252 #[test]
4253 fn payload_validation_covers_schema_branches() {
4254 let invalid_schema = validate_payload_against_contract(
4255 &json!({"field": "value"}),
4256 &json!("bad-schema"),
4257 super::RuntimeErrorCode::RequestInvalid,
4258 "invalid schema",
4259 );
4260 assert!(invalid_schema.is_err());
4261
4262 let wrong_object = validate_payload_against_contract(
4263 &json!("value"),
4264 &json!({"type": "object"}),
4265 super::RuntimeErrorCode::RequestInvalid,
4266 "wrong object",
4267 );
4268 assert!(wrong_object.is_err());
4269
4270 let wrong_array = validate_payload_against_contract(
4271 &json!("value"),
4272 &json!({"type": "array"}),
4273 super::RuntimeErrorCode::RequestInvalid,
4274 "wrong array",
4275 );
4276 assert!(wrong_array.is_err());
4277
4278 let typed_array = validate_payload_against_contract(
4279 &json!(["value", 2]),
4280 &json!({"type": "array", "items": {"type": "string"}}),
4281 super::RuntimeErrorCode::RequestInvalid,
4282 "typed array",
4283 );
4284 assert!(typed_array.is_err());
4285
4286 for (value, schema) in [
4287 (json!("value"), json!({"type": "integer"})),
4288 (json!("value"), json!({"type": "number"})),
4289 (json!("value"), json!({"type": "boolean"})),
4290 (json!("value"), json!({"type": "null"})),
4291 ] {
4292 let result = validate_payload_against_contract(
4293 &value,
4294 &schema,
4295 super::RuntimeErrorCode::RequestInvalid,
4296 "typed validation",
4297 );
4298 assert!(result.is_err());
4299 }
4300
4301 let missing_required = validate_payload_against_contract(
4302 &json!({}),
4303 &json!({"type": "object", "required": ["draft_id"]}),
4304 super::RuntimeErrorCode::RequestInvalid,
4305 "required field",
4306 );
4307 assert!(missing_required.is_err());
4308
4309 let property_mismatch = validate_payload_against_contract(
4310 &json!({"draft_id": 3}),
4311 &json!({"type": "object", "properties": {"draft_id": {"type": "string"}}}),
4312 super::RuntimeErrorCode::RequestInvalid,
4313 "property mismatch",
4314 );
4315 assert!(property_mismatch.is_err());
4316
4317 let array_without_item_schema = validate_payload_against_contract(
4318 &json!(["draft-1"]),
4319 &json!({"type": "array"}),
4320 super::RuntimeErrorCode::RequestInvalid,
4321 "array without item schema",
4322 );
4323 assert!(array_without_item_schema.is_ok());
4324
4325 let object_without_type = validate_payload_against_contract(
4326 &json!({"draft_id": "draft-1"}),
4327 &json!({}),
4328 super::RuntimeErrorCode::RequestInvalid,
4329 "object without type",
4330 );
4331 assert!(object_without_type.is_ok());
4332 }
4333
4334 #[test]
4335 fn runtime_mapping_helpers_cover_all_variants() {
4336 assert_eq!(
4337 map_registry_scope(RegistryScope::Public),
4338 super::RuntimeRegistryScope::Public
4339 );
4340 assert_eq!(
4341 map_registry_scope(RegistryScope::Private),
4342 super::RuntimeRegistryScope::Private
4343 );
4344 assert_eq!(
4345 map_implementation_kind(ImplementationKind::Executable),
4346 super::RuntimeImplementationKind::Executable
4347 );
4348 assert_eq!(
4349 map_implementation_kind(ImplementationKind::Workflow),
4350 super::RuntimeImplementationKind::Workflow
4351 );
4352 assert_eq!(
4353 map_lifecycle(&Lifecycle::Draft),
4354 super::RuntimeLifecycle::Draft
4355 );
4356 assert_eq!(
4357 map_lifecycle(&Lifecycle::Active),
4358 super::RuntimeLifecycle::Active
4359 );
4360 assert_eq!(
4361 map_lifecycle(&Lifecycle::Deprecated),
4362 super::RuntimeLifecycle::Deprecated
4363 );
4364 assert_eq!(
4365 map_lifecycle(&Lifecycle::Retired),
4366 super::RuntimeLifecycle::Retired
4367 );
4368 assert_eq!(
4369 map_lifecycle(&Lifecycle::Archived),
4370 super::RuntimeLifecycle::Archived
4371 );
4372 }
4373
4374 #[test]
4375 fn runtime_candidate_helper_copies_registry_shape() {
4376 let capability = resolved_capability(
4377 Some(traverse_registry::BinaryReference {
4378 format: traverse_registry::BinaryFormat::Wasm,
4379 location: "artifact.wasm".to_string(),
4380 signature: None,
4381 }),
4382 Lifecycle::Deprecated,
4383 );
4384
4385 let candidate = runtime_candidate(&capability, CandidateReason::IntentMatch);
4386
4387 assert_eq!(candidate.reason, CandidateReason::IntentMatch);
4388 assert_eq!(candidate.lifecycle, super::RuntimeLifecycle::Deprecated);
4389 assert_eq!(
4390 candidate.implementation_kind,
4391 super::RuntimeImplementationKind::Executable
4392 );
4393 }
4394
4395 #[test]
4396 fn successful_runtime_execution_reports_completed_result_status() {
4397 let mut events = super::StateEmitter::new("exec_1", "req_1");
4398 events.push(
4399 RuntimeState::LoadingRegistry,
4400 RuntimeTransitionReasonCode::RuntimeInitializationStarted,
4401 json!({}),
4402 );
4403 events.push(
4404 RuntimeState::Ready,
4405 RuntimeTransitionReasonCode::RegistryLoaded,
4406 json!({}),
4407 );
4408 events.push(
4409 RuntimeState::Discovering,
4410 RuntimeTransitionReasonCode::RequestStarted,
4411 json!({}),
4412 );
4413 events.push(
4414 RuntimeState::EvaluatingConstraints,
4415 RuntimeTransitionReasonCode::CandidatesCollected,
4416 json!({"candidate_count": 1}),
4417 );
4418 events.push(
4419 RuntimeState::Selecting,
4420 RuntimeTransitionReasonCode::ConstraintsEvaluated,
4421 json!({"eligible_candidates": 1}),
4422 );
4423 events.push(
4424 RuntimeState::Executing,
4425 RuntimeTransitionReasonCode::CandidateSelected,
4426 json!({"capability_id": "content.comments.create-comment-draft"}),
4427 );
4428 let attempt = super::AttemptContext {
4429 request: valid_request(),
4430 execution_id: "exec_1".to_string(),
4431 trace_id: "trace_exec_1".to_string(),
4432 observability: super::RuntimeObservabilityConfig::default(),
4433 artifact_verification: None,
4434 warnings: Vec::new(),
4435 };
4436 let capability = resolved_capability(
4437 Some(traverse_registry::BinaryReference {
4438 format: traverse_registry::BinaryFormat::Wasm,
4439 location: "artifact.wasm".to_string(),
4440 signature: None,
4441 }),
4442 Lifecycle::Active,
4443 );
4444
4445 let outcome = super::successful_execution_outcome(
4446 super::ExecutionContext {
4447 attempt,
4448 emitter: events,
4449 candidate_collection: super::CandidateCollectionRecord {
4450 lookup_scope: PreferPrivate,
4451 candidates: vec![runtime_candidate(&capability, CandidateReason::ExactMatch)],
4452 rejected_candidates: Vec::new(),
4453 },
4454 selection: super::SelectionRecord {
4455 status: super::SelectionStatus::Selected,
4456 selected_capability_id: Some(capability.record.id.clone()),
4457 selected_capability_version: Some(capability.record.version.clone()),
4458 failure_reason: None,
4459 remaining_candidates: Vec::new(),
4460 },
4461 },
4462 &capability,
4463 super::StartedExecution {
4464 started_at: "1970-01-01T00:00:00Z".to_string(),
4465 placement: super::resolve_placement(PlacementTarget::Local)
4466 .unwrap_or_else(|_| unreachable!("local placement should resolve")),
4467 },
4468 json!({"draft_id": "draft-1"}),
4469 capability.contract.emits.clone(),
4470 None,
4471 );
4472
4473 assert_eq!(outcome.result.status, RuntimeResultStatus::Completed);
4474 assert_eq!(
4475 outcome.state_events.last().map(|event| event.state),
4476 Some(RuntimeState::Ready)
4477 );
4478 assert_eq!(
4479 outcome.trace.decision_evidence.selection.status,
4480 super::SelectionStatus::Selected
4481 );
4482 assert_eq!(
4483 outcome.trace.state_progression.state_events,
4484 outcome.state_events
4485 );
4486 assert_eq!(
4487 outcome.trace.terminal_outcome.runtime_status,
4488 RuntimeResultStatus::Completed
4489 );
4490 assert_eq!(outcome.trace.emitted_events, capability.contract.emits);
4491 assert_eq!(
4492 outcome.trace.state_machine_validation.status,
4493 super::RuntimeStateMachineValidationStatus::Passed
4494 );
4495 }
4496
4497 #[test]
4498 fn runtime_emits_a_token_free_terminal_event_for_invalid_requests() {
4499 let sink = Arc::new(RecordingEventSink::default());
4500 let mut request = valid_request();
4501 request.kind = "unsupported_runtime_request".to_string();
4502 request.context.identity = Some(super::security::RuntimeIdentity {
4503 subject_id: "subject_123".to_string(),
4504 actor_id: Some("actor_456".to_string()),
4505 token_reference_hash: "must-not-leak".to_string(),
4506 });
4507
4508 let outcome = Runtime::new(CapabilityRegistry::new(), NoopExecutor)
4509 .with_event_sink(sink.clone())
4510 .execute(request);
4511
4512 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4513 let events = sink
4514 .events
4515 .lock()
4516 .expect("recording sink lock must not be poisoned");
4517 assert_eq!(events.len(), 1);
4518 assert_eq!(events[0].event_type, super::RUNTIME_EXECUTION_EVENT_TYPE);
4519 assert_eq!(events[0].subject_id.as_deref(), Some("subject_123"));
4520 assert_eq!(events[0].actor_id.as_deref(), Some("actor_456"));
4521 let serialized = serde_json::to_string(&events[0]).expect("event must serialize");
4522 assert!(!serialized.contains("must-not-leak"));
4523 }
4524
4525 #[test]
4526 fn runtime_records_sink_delivery_failures_without_changing_execution_status() {
4527 let outcome = Runtime::new(CapabilityRegistry::new(), NoopExecutor)
4528 .with_event_sink(Arc::new(FailingEventSink))
4529 .execute(valid_request());
4530
4531 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4532 assert_eq!(outcome.result.warnings.len(), 1);
4533 assert_eq!(
4534 outcome.result.warnings[0].code,
4535 "runtime_event_sink_delivery_failed"
4536 );
4537 assert_eq!(outcome.trace.result.warnings, outcome.result.warnings);
4538 }
4539
4540 #[test]
4541 fn runtime_execution_produces_otel_phase_spans() {
4542 let mut registry = CapabilityRegistry::new();
4543 assert!(registry.register(public_registration()).is_ok());
4544 let runtime = Runtime::new(registry, NoopExecutor)
4545 .with_security_config(RuntimeSecurityConfig::development());
4546 let outcome = runtime.execute(valid_request());
4547 let spans = &outcome.trace.otel_trace.spans;
4548 let names: Vec<&str> = spans.iter().map(|span| span.name.as_str()).collect();
4549
4550 assert_eq!(spans.len(), 6);
4551 assert!(names.contains(&"traverse.runtime.request"));
4552 assert!(names.contains(&"traverse.request.intake"));
4553 assert!(names.contains(&"traverse.registry.lookup"));
4554 assert!(names.contains(&"traverse.contract.validation"));
4555 assert!(names.contains(&"traverse.capability.execution"));
4556 assert!(names.contains(&"traverse.trace.assembly"));
4557 assert!(
4558 spans
4559 .iter()
4560 .all(|span| span.status == super::OTelSpanStatus::Ok)
4561 );
4562 assert!(spans.iter().all(|span| {
4563 span.attributes
4564 .iter()
4565 .all(|attr| attr.key.starts_with("traverse.") || attr.key == "service.name")
4566 }));
4567 }
4568
4569 #[test]
4570 fn runtime_otel_trace_propagates_w3c_context_and_exporter_config() {
4571 let mut registry = CapabilityRegistry::new();
4572 assert!(registry.register(public_registration()).is_ok());
4573 let runtime = Runtime::new(registry, NoopExecutor)
4574 .with_security_config(RuntimeSecurityConfig::development())
4575 .with_observability_config(super::RuntimeObservabilityConfig {
4576 exporter: super::OTelExporterConfig {
4577 endpoint: Some("http://collector:4318".to_string()),
4578 protocol: super::OtlpProtocol::Http,
4579 },
4580 ..super::RuntimeObservabilityConfig::deterministic_test("seed-1")
4581 });
4582 let mut request = valid_request();
4583 request.context.traceparent =
4584 Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string());
4585 request.context.tracestate = Some("vendor=value".to_string());
4586
4587 let first = runtime.execute(request.clone()).trace.otel_trace;
4588 let second = runtime.execute(request).trace.otel_trace;
4589
4590 assert_eq!(first.trace_id, second.trace_id);
4591 assert_eq!(first.spans[0].span_id, second.spans[0].span_id);
4592 assert_eq!(
4593 first.parent_traceparent.as_deref(),
4594 Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
4595 );
4596 assert_eq!(first.tracestate.as_deref(), Some("vendor=value"));
4597 assert!(first.exporter.enabled);
4598 assert_eq!(
4599 first.exporter.endpoint.as_deref(),
4600 Some("http://collector:4318")
4601 );
4602 assert_eq!(
4603 runtime.observability_config().exporter.endpoint.as_deref(),
4604 Some("http://collector:4318")
4605 );
4606 }
4607
4608 #[test]
4609 fn governed_artifact_with_valid_ed25519_signature_executes() {
4610 let artifact_bytes = b"governed wasm bytes";
4611 let path = temp_artifact_path("ed25519-valid");
4612 assert!(fs::write(&path, artifact_bytes).is_ok());
4613 let signature = ed25519_signature_for(artifact_bytes);
4614 let mut registry = CapabilityRegistry::new();
4615 assert!(
4616 registry
4617 .register(governed_registration(&path, Some(signature)))
4618 .is_ok()
4619 );
4620 let runtime = Runtime::new(registry, NoopExecutor)
4621 .with_security_config(RuntimeSecurityConfig::development());
4622
4623 let outcome = runtime.execute(valid_request());
4624
4625 assert_eq!(outcome.result.status, RuntimeResultStatus::Completed);
4626 assert_eq!(
4627 outcome
4628 .trace
4629 .execution
4630 .artifact_verification
4631 .as_ref()
4632 .map(|record| record.status),
4633 Some(ArtifactVerificationStatus::Verified)
4634 );
4635 assert_eq!(
4636 outcome
4637 .trace
4638 .execution
4639 .artifact_verification
4640 .as_ref()
4641 .and_then(|record| record.scheme),
4642 Some(ArtifactVerificationScheme::Ed25519)
4643 );
4644 }
4645
4646 #[test]
4647 fn governed_artifact_without_signature_is_rejected_in_production() {
4648 let path = temp_artifact_path("missing-signature");
4649 assert!(fs::write(&path, b"unsigned governed bytes").is_ok());
4650 let mut registry = CapabilityRegistry::new();
4651 assert!(
4652 registry
4653 .register(governed_registration(&path, None))
4654 .is_ok()
4655 );
4656 let runtime = Runtime::new(registry, NoopExecutor)
4657 .with_security_config(RuntimeSecurityConfig::development());
4658
4659 let outcome = runtime.execute(valid_request());
4660
4661 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4662 assert_eq!(
4663 outcome
4664 .result
4665 .error
4666 .as_ref()
4667 .and_then(|error| error.details.get("code"))
4668 .and_then(serde_json::Value::as_str),
4669 Some("missing_signature")
4670 );
4671 assert_eq!(
4672 outcome
4673 .trace
4674 .execution
4675 .artifact_verification
4676 .as_ref()
4677 .and_then(|record| record.error_code.as_deref()),
4678 Some("missing_signature")
4679 );
4680 }
4681
4682 #[test]
4683 fn governed_artifact_without_checksum_is_rejected_before_execution() {
4684 let bytes = b"governed checksum missing";
4685 let path = temp_artifact_path("missing-checksum");
4686 assert!(fs::write(&path, bytes).is_ok());
4687 let mut registration = governed_registration(&path, Some(ed25519_signature_for(bytes)));
4688 registration.artifact.digests.binary_digest = None;
4689 let mut registry = CapabilityRegistry::new();
4690 assert!(registry.register(registration).is_ok());
4691
4692 let outcome = Runtime::new(registry, NoopExecutor).execute(valid_request());
4693
4694 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4695 assert_eq!(
4696 outcome
4697 .result
4698 .error
4699 .as_ref()
4700 .and_then(|error| error.details.get("code"))
4701 .and_then(serde_json::Value::as_str),
4702 Some("missing_checksum")
4703 );
4704 }
4705
4706 #[test]
4707 fn governed_artifact_with_mismatched_checksum_is_rejected_before_execution() {
4708 let bytes = b"governed checksum mismatch";
4709 let path = temp_artifact_path("checksum-mismatch");
4710 assert!(fs::write(&path, bytes).is_ok());
4711 let mut registration = governed_registration(&path, Some(ed25519_signature_for(bytes)));
4712 registration.artifact.digests.binary_digest = Some(
4713 "sha256:0000000000000000000000000000000000000000000000000000000000000000".to_string(),
4714 );
4715 let mut registry = CapabilityRegistry::new();
4716 assert!(registry.register(registration).is_ok());
4717
4718 let outcome = Runtime::new(registry, NoopExecutor).execute(valid_request());
4719
4720 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4721 assert_eq!(
4722 outcome
4723 .result
4724 .error
4725 .as_ref()
4726 .and_then(|error| error.details.get("code"))
4727 .and_then(serde_json::Value::as_str),
4728 Some("checksum_mismatch")
4729 );
4730 }
4731
4732 #[test]
4733 fn local_artifact_checksum_does_not_override_local_development_policy() {
4734 let bytes = b"local checksum is advisory";
4735 let path = temp_artifact_path("local-checksum");
4736 assert!(fs::write(&path, bytes).is_ok());
4737 let mut registration = governed_registration(&path, Some(ed25519_signature_for(bytes)));
4738 registration.artifact.source.kind = SourceKind::Local;
4739 registration.artifact.digests.binary_digest = Some(
4740 "sha256:0000000000000000000000000000000000000000000000000000000000000000".to_string(),
4741 );
4742 let mut registry = CapabilityRegistry::new();
4743 assert!(registry.register(registration).is_ok());
4744
4745 let outcome = Runtime::new(registry, NoopExecutor).execute(valid_request());
4746
4747 assert_eq!(outcome.result.status, RuntimeResultStatus::Completed);
4748 }
4749
4750 #[test]
4751 fn local_dev_unsigned_artifact_warns_and_executes_in_development_mode() {
4752 let mut registry = CapabilityRegistry::new();
4753 assert!(registry.register(public_registration()).is_ok());
4754 let runtime = Runtime::new(registry, NoopExecutor)
4755 .with_security_config(RuntimeSecurityConfig::development());
4756
4757 let outcome = runtime.execute(valid_request());
4758
4759 assert_eq!(outcome.result.status, RuntimeResultStatus::Completed);
4760 assert_eq!(
4761 outcome
4762 .result
4763 .warnings
4764 .first()
4765 .map(|warning| warning.code.as_str()),
4766 Some("unsigned_local_dev_artifact")
4767 );
4768 assert_eq!(
4769 outcome
4770 .trace
4771 .execution
4772 .artifact_verification
4773 .as_ref()
4774 .map(|record| record.status),
4775 Some(ArtifactVerificationStatus::Warning)
4776 );
4777 }
4778
4779 #[test]
4780 fn default_security_config_is_production() {
4781 assert_eq!(
4782 RuntimeSecurityConfig::default(),
4783 RuntimeSecurityConfig::production()
4784 );
4785 assert_ne!(
4786 RuntimeSecurityConfig::default(),
4787 RuntimeSecurityConfig::development()
4788 );
4789 }
4790
4791 #[test]
4792 fn unsigned_local_artifact_rejected_under_default_security_config() {
4793 let mut registry = CapabilityRegistry::new();
4794 assert!(registry.register(public_registration()).is_ok());
4795 let runtime = Runtime::new(registry, NoopExecutor);
4798
4799 let outcome = runtime.execute(valid_request());
4800
4801 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4802 assert_eq!(
4803 outcome
4804 .result
4805 .error
4806 .as_ref()
4807 .and_then(|error| error.details.get("code"))
4808 .and_then(serde_json::Value::as_str),
4809 Some("missing_signature")
4810 );
4811 }
4812
4813 #[test]
4814 fn governed_artifact_rejects_placeholder_sigstore_bundle() {
4815 let path = temp_artifact_path("sigstore-valid");
4816 assert!(fs::write(&path, b"sigstore governed bytes").is_ok());
4817 let signature = ArtifactSignature {
4818 scheme: ArtifactSignatureScheme::Sigstore,
4819 public_key_hex: None,
4820 signature_hex: None,
4821 sigstore_bundle_ref: Some("verified://bundle/comment-draft".to_string()),
4822 };
4823 let mut registry = CapabilityRegistry::new();
4824 assert!(
4825 registry
4826 .register(governed_registration(&path, Some(signature)))
4827 .is_ok()
4828 );
4829 let runtime = Runtime::new(registry, NoopExecutor)
4830 .with_security_config(RuntimeSecurityConfig::production());
4831
4832 let outcome = runtime.execute(valid_request());
4833
4834 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
4835 assert_eq!(
4836 outcome
4837 .result
4838 .error
4839 .as_ref()
4840 .and_then(|error| error.details.get("code"))
4841 .and_then(serde_json::Value::as_str),
4842 Some("sigstore_unreachable")
4843 );
4844 }
4845
4846 #[test]
4847 fn jwt_identity_is_derived_and_raw_token_is_not_traced() {
4848 let mut registry = CapabilityRegistry::new();
4849 assert!(registry.register(public_registration()).is_ok());
4850 let runtime = Runtime::new(registry, NoopExecutor)
4851 .with_security_config(RuntimeSecurityConfig::development());
4852 let mut request = valid_request();
4853 let token = make_jwt_with_actor("alice", "workflow-agent");
4854 request.context.caller = Some(token.clone());
4855
4856 let outcome = runtime.execute(request);
4857 let trace_json = serde_json::to_string(&outcome.trace).unwrap_or_default();
4858
4859 assert_eq!(outcome.result.status, RuntimeResultStatus::Completed);
4860 assert!(!trace_json.contains(&token));
4861 assert_eq!(
4862 outcome
4863 .trace
4864 .request
4865 .context
4866 .identity
4867 .as_ref()
4868 .map(|identity| identity.subject_id.as_str()),
4869 Some("alice")
4870 );
4871 assert_eq!(
4872 outcome
4873 .trace
4874 .execution
4875 .identity
4876 .as_ref()
4877 .and_then(|identity| identity.actor_id.as_deref()),
4878 Some("workflow-agent")
4879 );
4880 assert!(
4881 outcome
4882 .state_events
4883 .iter()
4884 .any(|event| event.details.to_string().contains("alice"))
4885 );
4886 }
4887
4888 #[test]
4889 #[allow(clippy::too_many_lines)]
4890 fn security_branch_guards_cover_malformed_signatures_sigstore_and_jwts() {
4891 let capability = governed_resolved_capability(None);
4892 let missing_public_key = ArtifactSignature {
4893 scheme: ArtifactSignatureScheme::Ed25519,
4894 public_key_hex: None,
4895 signature_hex: Some("00".to_string()),
4896 sigstore_bundle_ref: None,
4897 };
4898 let missing_signature = ArtifactSignature {
4899 scheme: ArtifactSignatureScheme::Ed25519,
4900 public_key_hex: Some("00".to_string()),
4901 signature_hex: None,
4902 sigstore_bundle_ref: None,
4903 };
4904 let bad_public_hex = ArtifactSignature {
4905 scheme: ArtifactSignatureScheme::Ed25519,
4906 public_key_hex: Some("abc".to_string()),
4907 signature_hex: Some("00".to_string()),
4908 sigstore_bundle_ref: None,
4909 };
4910 let bad_signature_hex = ArtifactSignature {
4911 scheme: ArtifactSignatureScheme::Ed25519,
4912 public_key_hex: Some("00".to_string()),
4913 signature_hex: Some("zz".to_string()),
4914 sigstore_bundle_ref: None,
4915 };
4916 let short_public_key = ArtifactSignature {
4917 scheme: ArtifactSignatureScheme::Ed25519,
4918 public_key_hex: Some("00".to_string()),
4919 signature_hex: Some("00".repeat(64)),
4920 sigstore_bundle_ref: None,
4921 };
4922 let short_signature = ArtifactSignature {
4923 scheme: ArtifactSignatureScheme::Ed25519,
4924 public_key_hex: Some("00".repeat(32)),
4925 signature_hex: Some("00".to_string()),
4926 sigstore_bundle_ref: None,
4927 };
4928 let invalid_public_key = ArtifactSignature {
4929 scheme: ArtifactSignatureScheme::Ed25519,
4930 public_key_hex: Some("ff".repeat(32)),
4931 signature_hex: Some("00".repeat(64)),
4932 sigstore_bundle_ref: None,
4933 };
4934 let mismatch = ed25519_signature_for(b"other bytes");
4935 for signature in [
4936 missing_public_key,
4937 missing_signature,
4938 bad_public_hex,
4939 bad_signature_hex,
4940 short_public_key,
4941 short_signature,
4942 invalid_public_key,
4943 mismatch,
4944 ] {
4945 let mut capability = capability.clone();
4946 capability.artifact.binary = Some(BinaryReference {
4947 format: BinaryFormat::Wasm,
4948 location: "unused.wasm".to_string(),
4949 signature: Some(signature),
4950 });
4951 let error = verify_artifact(
4952 &capability,
4953 b"artifact bytes",
4954 &RuntimeSecurityConfig::production(),
4955 );
4956 assert!(matches!(
4957 error,
4958 Err(ArtifactVerificationFailure::SignatureVerificationFailed(_))
4959 ));
4960 let failure = error.err();
4961 assert_eq!(
4962 failure.as_ref().map(ArtifactVerificationFailure::code),
4963 Some("signature_verification_failed")
4964 );
4965 assert_eq!(
4966 failure
4967 .as_ref()
4968 .and_then(|item| item.record().error_code.as_deref()),
4969 Some("signature_verification_failed")
4970 );
4971 }
4972
4973 let mut capability = capability.clone();
4974 capability.artifact.binary = Some(BinaryReference {
4975 format: BinaryFormat::Wasm,
4976 location: "unused.wasm".to_string(),
4977 signature: Some(ArtifactSignature {
4978 scheme: ArtifactSignatureScheme::Sigstore,
4979 public_key_hex: None,
4980 signature_hex: None,
4981 sigstore_bundle_ref: Some("https://rekor.example/bundle".to_string()),
4982 }),
4983 });
4984 let error = verify_artifact(
4985 &capability,
4986 b"artifact bytes",
4987 &RuntimeSecurityConfig::production(),
4988 );
4989 assert!(matches!(
4990 error,
4991 Err(ArtifactVerificationFailure::SigstoreUnreachable(_))
4992 ));
4993 assert_eq!(
4994 error.err().as_ref().map(ArtifactVerificationFailure::code),
4995 Some("sigstore_unreachable")
4996 );
4997
4998 let bad_payload = base64url_encode(b"{");
4999 let no_subject = base64url_encode(b"{}");
5000 assert!(derive_identity_from_jwt("not-a-jwt").is_none());
5001 assert!(derive_identity_from_jwt("a.b.c.d").is_none());
5002 assert!(derive_identity_from_jwt("a.abc=.c").is_none());
5003 assert!(derive_identity_from_jwt("a.*.c").is_none());
5004 assert!(derive_identity_from_jwt("a.a.c").is_none());
5005 assert!(derive_identity_from_jwt("a.-___.c").is_none());
5006 assert!(derive_identity_from_jwt(&format!("a.{bad_payload}.c")).is_none());
5007 assert!(derive_identity_from_jwt(&format!("a.{no_subject}.c")).is_none());
5008 assert_eq!(base64url_encode(b""), "");
5009 }
5010
5011 #[test]
5012 fn runtime_security_config_accessor_returns_current_config() {
5013 let runtime = Runtime::new(CapabilityRegistry::new(), NoopExecutor)
5014 .with_security_config(RuntimeSecurityConfig::production());
5015
5016 assert_eq!(
5017 runtime.security_config(),
5018 &RuntimeSecurityConfig::production()
5019 );
5020 }
5021
5022 #[test]
5023 fn signed_artifact_missing_from_disk_fails_before_execution() {
5024 let path = temp_artifact_path("missing-from-disk");
5025 let signature = ed25519_signature_for(b"governed wasm bytes");
5026 let mut registry = CapabilityRegistry::new();
5027 assert!(
5028 registry
5029 .register(governed_registration(&path, Some(signature)))
5030 .is_ok()
5031 );
5032 let runtime = Runtime::new(registry, NoopExecutor)
5033 .with_security_config(RuntimeSecurityConfig::production());
5034
5035 let outcome = runtime.execute(valid_request());
5036
5037 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
5038 assert_eq!(
5039 outcome
5040 .result
5041 .error
5042 .as_ref()
5043 .and_then(|error| error.details.get("code"))
5044 .and_then(serde_json::Value::as_str),
5045 Some("artifact_load_failed")
5046 );
5047 }
5048
5049 #[test]
5050 fn otel_timestamp_helpers_default_without_transitions() {
5051 let transitions = Vec::new();
5052
5053 assert_eq!(
5054 super::first_transition_time(&transitions),
5055 "1970-01-01T00:00:00Z"
5056 );
5057 assert_eq!(
5058 super::last_transition_time(&transitions),
5059 "1970-01-01T00:00:00Z"
5060 );
5061 assert_eq!(
5062 super::phase_started_at(&transitions, 0),
5063 "1970-01-01T00:00:00Z"
5064 );
5065 assert_eq!(
5066 super::phase_ended_at(&transitions, 0),
5067 "1970-01-01T00:00:00Z"
5068 );
5069 }
5070
5071 #[test]
5072 fn state_emitter_records_transition_validation_and_rejects_invalid_moves() {
5073 let mut events = super::StateEmitter::new("exec_1", "req_1");
5074
5075 assert!(events.try_push(
5076 RuntimeState::LoadingRegistry,
5077 RuntimeTransitionReasonCode::RuntimeInitializationStarted,
5078 json!({})
5079 ));
5080 assert!(!events.try_push(
5081 RuntimeState::Completed,
5082 RuntimeTransitionReasonCode::ExecutionSucceeded,
5083 json!({})
5084 ));
5085
5086 let finished = events.finish();
5087
5088 assert_eq!(finished.events.len(), 1);
5089 assert_eq!(finished.transitions.len(), 1);
5090 assert_eq!(
5091 finished.validation.status,
5092 super::RuntimeStateMachineValidationStatus::Failed
5093 );
5094 assert_eq!(finished.validation.violations.len(), 1);
5095 }
5096
5097 #[test]
5098 fn pre_execution_failure_from_constraint_phase_uses_constraint_reason() {
5099 let mut events = super::StateEmitter::new("exec_1", "req_1");
5100 events.push(
5101 RuntimeState::LoadingRegistry,
5102 RuntimeTransitionReasonCode::RuntimeInitializationStarted,
5103 json!({}),
5104 );
5105 events.push(
5106 RuntimeState::Ready,
5107 RuntimeTransitionReasonCode::RegistryLoaded,
5108 json!({}),
5109 );
5110 events.push(
5111 RuntimeState::Discovering,
5112 RuntimeTransitionReasonCode::RequestStarted,
5113 json!({}),
5114 );
5115 events.push(
5116 RuntimeState::EvaluatingConstraints,
5117 RuntimeTransitionReasonCode::CandidatesCollected,
5118 json!({"candidate_count": 1}),
5119 );
5120
5121 let outcome = super::pre_execution_failure_outcome(
5122 super::ExecutionContext {
5123 attempt: super::AttemptContext {
5124 request: valid_request(),
5125 execution_id: "exec_1".to_string(),
5126 trace_id: "trace_exec_1".to_string(),
5127 observability: super::RuntimeObservabilityConfig::default(),
5128 artifact_verification: None,
5129 warnings: Vec::new(),
5130 },
5131 emitter: events,
5132 candidate_collection: super::CandidateCollectionRecord {
5133 lookup_scope: PreferPrivate,
5134 candidates: Vec::new(),
5135 rejected_candidates: Vec::new(),
5136 },
5137 selection: super::SelectionRecord {
5138 status: super::SelectionStatus::NoMatch,
5139 selected_capability_id: None,
5140 selected_capability_version: None,
5141 failure_reason: Some(super::SelectionFailureReason::NotRunnable),
5142 remaining_candidates: Vec::new(),
5143 },
5144 },
5145 super::PreExecutionFailure {
5146 artifact_ref: None,
5147 failure_reason: super::ExecutionFailureReason::ArtifactMissing,
5148 placement: super::placement_not_attempted(
5149 PlacementTarget::Local,
5150 super::PlacementDecisionReason::SelectionNotReached,
5151 ),
5152 error: super::runtime_error(
5153 super::RuntimeErrorCode::CapabilityNotRunnable,
5154 "not runnable",
5155 json!({}),
5156 ),
5157 artifact_verification: None,
5158 },
5159 );
5160
5161 assert_eq!(
5162 outcome.trace.state_transitions[4].reason_code,
5163 RuntimeTransitionReasonCode::ConstraintValidationFailed
5164 );
5165 }
5166
5167 #[test]
5168 fn detail_object_wraps_non_object_values() {
5169 let wrapped = super::detail_object(json!("value"));
5170
5171 assert_eq!(wrapped.get("value"), Some(&json!("value")));
5172 }
5173
5174 #[test]
5175 fn collect_candidates_handles_missing_target_and_public_discovery() {
5176 let runtime = super::Runtime::new(CapabilityRegistry::new(), NoopExecutor)
5177 .with_security_config(RuntimeSecurityConfig::development());
5178 let mut request = valid_request();
5179 request.intent.capability_id = None;
5180 request.intent.capability_version = None;
5181 request.intent.intent_key = None;
5182
5183 assert!(
5184 runtime
5185 .collect_candidates(&request, CandidateReason::IntentMatch)
5186 .is_empty()
5187 );
5188
5189 let mut registry = CapabilityRegistry::new();
5190 let outcome = registry.register(public_registration());
5191 assert!(outcome.is_ok());
5192
5193 let runtime = super::Runtime::new(registry, NoopExecutor)
5194 .with_security_config(RuntimeSecurityConfig::development());
5195 let mut request = valid_request();
5196 request.lookup.scope = PublicOnly;
5197 request.intent.capability_id = None;
5198 request.intent.capability_version = None;
5199 request.intent.intent_key = Some("content.comments.create-comment-draft".to_string());
5200
5201 let candidates = runtime.collect_candidates(&request, CandidateReason::IntentMatch);
5202
5203 assert_eq!(candidates.len(), 1);
5204 assert_eq!(candidates[0].record.scope, RegistryScope::Public);
5205 }
5206
5207 #[test]
5208 fn noop_executor_returns_structured_output() {
5209 let executor = NoopExecutor;
5210 let capability = resolved_capability(
5211 Some(BinaryReference {
5212 format: BinaryFormat::Wasm,
5213 location: "artifact.wasm".to_string(),
5214 signature: None,
5215 }),
5216 Lifecycle::Active,
5217 );
5218
5219 let result = executor.execute(&capability, &json!({}));
5220
5221 assert_eq!(
5222 result,
5223 Ok(super::LocalExecutionOutput {
5224 value: json!({"draft_id": "draft"}),
5225 emitted_events: Vec::new(),
5226 })
5227 );
5228 }
5229
5230 #[test]
5231 fn browser_subscription_validation_covers_guard_branches() {
5232 let mut request = valid_browser_subscription_request();
5233 request.kind = "wrong".to_string();
5234 assert_eq!(
5235 validate_browser_subscription_request(&request).map(|error| error.code),
5236 Some(BrowserRuntimeSubscriptionErrorCode::InvalidRequest)
5237 );
5238
5239 let mut request = valid_browser_subscription_request();
5240 request.schema_version = "9.9.9".to_string();
5241 assert_eq!(
5242 validate_browser_subscription_request(&request).map(|error| error.code),
5243 Some(BrowserRuntimeSubscriptionErrorCode::InvalidRequest)
5244 );
5245
5246 let mut request = valid_browser_subscription_request();
5247 request.governing_spec = "wrong-spec".to_string();
5248 assert_eq!(
5249 validate_browser_subscription_request(&request).map(|error| error.code),
5250 Some(BrowserRuntimeSubscriptionErrorCode::InvalidRequest)
5251 );
5252 }
5253
5254 #[test]
5255 fn browser_subscription_reports_not_found_for_mismatched_target() {
5256 let outcome = runtime_outcome_for_browser_subscription();
5257 let request = BrowserRuntimeSubscriptionRequest {
5258 request_id: Some("req_other".to_string()),
5259 execution_id: None,
5260 ..valid_browser_subscription_request()
5261 };
5262
5263 let messages = browser_subscription_messages(&request, &outcome);
5264 assert_eq!(
5265 messages,
5266 vec![BrowserRuntimeSubscriptionMessage::Error(
5267 super::BrowserRuntimeSubscriptionErrorMessage {
5268 kind: "browser_runtime_subscription_error".to_string(),
5269 schema_version: "1.0.0".to_string(),
5270 sequence: 0,
5271 code: BrowserRuntimeSubscriptionErrorCode::NotFound,
5272 message: "subscription target did not match the supplied execution outcome"
5273 .to_string(),
5274 }
5275 )]
5276 );
5277 }
5278
5279 #[test]
5280 fn browser_subscription_target_helper_covers_fallback_branch() {
5281 let outcome = runtime_outcome_for_browser_subscription();
5282 let invalid_request = BrowserRuntimeSubscriptionRequest {
5283 request_id: Some("req_123".to_string()),
5284 execution_id: Some(outcome.result.execution_id.clone()),
5285 ..valid_browser_subscription_request()
5286 };
5287
5288 assert!(!subscription_targets_outcome(&invalid_request, &outcome));
5289 }
5290
5291 fn valid_request() -> RuntimeRequest {
5292 RuntimeRequest {
5293 kind: "runtime_request".to_string(),
5294 schema_version: "1.0.0".to_string(),
5295 request_id: "req_123".to_string(),
5296 intent: RuntimeIntent {
5297 capability_id: Some("content.comments.create-comment-draft".to_string()),
5298 capability_version: Some("1.0.0".to_string()),
5299 version_range: None,
5300 intent_key: Some("content.comments.create-comment-draft".to_string()),
5301 },
5302 input: json!({"comment_text": "Hello", "resource_id": "res-1"}),
5303 lookup: RuntimeLookup {
5304 scope: RuntimeLookupScope::PreferPrivate,
5305 allow_ambiguity: false,
5306 },
5307 context: RuntimeContext {
5308 requested_target: PlacementTarget::Local,
5309 correlation_id: None,
5310 caller: None,
5311 traceparent: None,
5312 tracestate: None,
5313 metadata: None,
5314 identity: None,
5315 },
5316 governing_spec: "006-runtime-request-execution".to_string(),
5317 }
5318 }
5319
5320 fn valid_browser_subscription_request() -> BrowserRuntimeSubscriptionRequest {
5321 BrowserRuntimeSubscriptionRequest {
5322 kind: "browser_runtime_subscription_request".to_string(),
5323 schema_version: "1.0.0".to_string(),
5324 governing_spec: "013-browser-runtime-subscription".to_string(),
5325 request_id: Some("req_123".to_string()),
5326 execution_id: None,
5327 }
5328 }
5329
5330 fn runtime_outcome_for_browser_subscription() -> super::RuntimeExecutionOutcome {
5331 let mut registry = CapabilityRegistry::new();
5332 assert!(registry.register(public_registration()).is_ok());
5333 let runtime = Runtime::new(registry, NoopExecutor)
5334 .with_security_config(RuntimeSecurityConfig::development());
5335 runtime.execute(valid_request())
5336 }
5337
5338 fn governed_registration(
5339 path: &std::path::Path,
5340 signature: Option<ArtifactSignature>,
5341 ) -> CapabilityRegistration {
5342 let mut registration = public_registration();
5343 registration.contract_path = "contracts/approved/comment-draft.json".to_string();
5344 registration.artifact.source = SourceReference {
5345 kind: SourceKind::Git,
5346 location: "https://github.com/enricopiovesan/Traverse".to_string(),
5347 };
5348 registration.artifact.binary = Some(BinaryReference {
5349 format: BinaryFormat::Wasm,
5350 location: path.display().to_string(),
5351 signature,
5352 });
5353 let bytes = fs::read(path).unwrap_or_default();
5354 let hex_digest = Sha256::digest(bytes)
5355 .iter()
5356 .fold(String::new(), |mut acc, byte| {
5357 let _ = write!(acc, "{byte:02x}");
5358 acc
5359 });
5360 registration.artifact.digests.binary_digest = Some(format!("sha256:{hex_digest}"));
5361 registration
5362 }
5363
5364 fn governed_resolved_capability(signature: Option<ArtifactSignature>) -> ResolvedCapability {
5365 let mut capability = resolved_capability(
5366 Some(BinaryReference {
5367 format: BinaryFormat::Wasm,
5368 location: "unused.wasm".to_string(),
5369 signature,
5370 }),
5371 Lifecycle::Active,
5372 );
5373 capability.record.contract_path = "contracts/approved/comment-draft.json".to_string();
5374 capability.artifact.source = SourceReference {
5375 kind: SourceKind::Git,
5376 location: "https://github.com/enricopiovesan/Traverse".to_string(),
5377 };
5378 capability
5379 }
5380
5381 fn ed25519_signature_for(bytes: &[u8]) -> ArtifactSignature {
5382 let signing_key = SigningKey::from_bytes(&[7_u8; 32]);
5383 let signature = signing_key.sign(bytes);
5384 ArtifactSignature {
5385 scheme: ArtifactSignatureScheme::Ed25519,
5386 public_key_hex: Some(hex_encode(signing_key.verifying_key().as_bytes())),
5387 signature_hex: Some(hex_encode(&signature.to_bytes())),
5388 sigstore_bundle_ref: None,
5389 }
5390 }
5391
5392 fn temp_artifact_path(name: &str) -> std::path::PathBuf {
5393 std::env::temp_dir().join(format!(
5394 "traverse-runtime-{name}-{}-{}.wasm",
5395 std::process::id(),
5396 "req_123"
5397 ))
5398 }
5399
5400 fn make_jwt_with_actor(subject_id: &str, actor_id: &str) -> String {
5401 let header = base64url_encode(br#"{"alg":"none","typ":"JWT"}"#);
5402 let payload = serde_json::json!({
5403 "sub": subject_id,
5404 "act": {"sub": actor_id}
5405 });
5406 format!(
5407 "{}.{}.signature",
5408 header,
5409 base64url_encode(payload.to_string().as_bytes())
5410 )
5411 }
5412
5413 fn hex_encode(bytes: &[u8]) -> String {
5414 let mut output = String::with_capacity(bytes.len() * 2);
5415 for byte in bytes {
5416 output.push(char::from(HEX_TABLE[(byte >> 4) as usize]));
5417 output.push(char::from(HEX_TABLE[(byte & 0x0f) as usize]));
5418 }
5419 output
5420 }
5421
5422 fn base64url_encode(bytes: &[u8]) -> String {
5423 const TABLE: &[u8; 64] =
5424 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5425 let mut out = String::new();
5426 let mut index = 0;
5427 while index + 3 <= bytes.len() {
5428 let chunk = &bytes[index..index + 3];
5429 let n = (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]);
5430 out.push(char::from(TABLE[((n >> 18) & 0x3f) as usize]));
5431 out.push(char::from(TABLE[((n >> 12) & 0x3f) as usize]));
5432 out.push(char::from(TABLE[((n >> 6) & 0x3f) as usize]));
5433 out.push(char::from(TABLE[(n & 0x3f) as usize]));
5434 index += 3;
5435 }
5436 match bytes.len() - index {
5437 1 => {
5438 let n = u32::from(bytes[index]) << 16;
5439 out.push(char::from(TABLE[((n >> 18) & 0x3f) as usize]));
5440 out.push(char::from(TABLE[((n >> 12) & 0x3f) as usize]));
5441 }
5442 2 => {
5443 let n = (u32::from(bytes[index]) << 16) | (u32::from(bytes[index + 1]) << 8);
5444 out.push(char::from(TABLE[((n >> 18) & 0x3f) as usize]));
5445 out.push(char::from(TABLE[((n >> 12) & 0x3f) as usize]));
5446 out.push(char::from(TABLE[((n >> 6) & 0x3f) as usize]));
5447 }
5448 _ => {}
5449 }
5450 out
5451 }
5452
5453 fn public_registration() -> CapabilityRegistration {
5454 CapabilityRegistration {
5455 scope: RegistryScope::Public,
5456 contract: test_contract(Lifecycle::Active),
5457 contract_path: "registry/contract.json".to_string(),
5458 artifact: test_artifact(Some(BinaryReference {
5459 format: BinaryFormat::Wasm,
5460 location: "artifact.wasm".to_string(),
5461 signature: None,
5462 })),
5463 registered_at: "2026-03-27T00:00:00Z".to_string(),
5464 tags: vec!["comments".to_string()],
5465 composability: ComposabilityMetadata {
5466 kind: CompositionKind::Atomic,
5467 patterns: vec![CompositionPattern::Sequential],
5468 provides: vec!["draft".to_string()],
5469 requires: vec!["authenticated-user".to_string()],
5470 },
5471 governing_spec: "005-capability-registry".to_string(),
5472 validator_version: "0.1.0".to_string(),
5473 }
5474 }
5475
5476 fn resolved_capability(
5477 binary: Option<traverse_registry::BinaryReference>,
5478 lifecycle: Lifecycle,
5479 ) -> ResolvedCapability {
5480 ResolvedCapability {
5481 contract: test_contract(lifecycle.clone()),
5482 record: test_record(lifecycle.clone()),
5483 artifact: test_artifact(binary),
5484 index_entry: test_index_entry(lifecycle),
5485 }
5486 }
5487
5488 fn test_contract(lifecycle: Lifecycle) -> traverse_contracts::CapabilityContract {
5489 traverse_contracts::CapabilityContract {
5490 kind: "capability_contract".to_string(),
5491 schema_version: "1.0.0".to_string(),
5492 id: "content.comments.create-comment-draft".to_string(),
5493 namespace: "content.comments".to_string(),
5494 name: "create-comment-draft".to_string(),
5495 version: "1.0.0".to_string(),
5496 lifecycle,
5497 owner: Owner {
5498 team: "comments".to_string(),
5499 contact: "comments@example.com".to_string(),
5500 },
5501 summary: "Create a comment draft for a resource".to_string(),
5502 description: "Creates a draft comment and returns the generated draft identifier."
5503 .to_string(),
5504 inputs: SchemaContainer {
5505 schema: json!({"type": "object"}),
5506 },
5507 outputs: SchemaContainer {
5508 schema: json!({"type": "object"}),
5509 },
5510 preconditions: Vec::new(),
5511 postconditions: Vec::new(),
5512 side_effects: vec![traverse_contracts::SideEffect {
5513 kind: traverse_contracts::SideEffectKind::MemoryOnly,
5514 description: "Produces a draft representation in memory.".to_string(),
5515 }],
5516 emits: Vec::new(),
5517 consumes: Vec::new(),
5518 permissions: Vec::new(),
5519 execution: Execution {
5520 binary_format: ContractBinaryFormat::Wasm,
5521 entrypoint: Entrypoint {
5522 kind: EntrypointKind::WasiCommand,
5523 command: "run".to_string(),
5524 },
5525 preferred_targets: vec![ExecutionTarget::Local],
5526 constraints: ExecutionConstraints {
5527 host_api_access: HostApiAccess::None,
5528 network_access: NetworkAccess::Forbidden,
5529 filesystem_access: FilesystemAccess::None,
5530 },
5531 },
5532 policies: Vec::new(),
5533 dependencies: Vec::new(),
5534 provenance: Provenance {
5535 source: ProvenanceSource::Greenfield,
5536 author: "Enrico Piovesan".to_string(),
5537 created_at: "2026-03-27T00:00:00Z".to_string(),
5538 spec_ref: Some("006-runtime-request-execution".to_string()),
5539 adr_refs: Vec::new(),
5540 exception_refs: Vec::new(),
5541 },
5542 evidence: Vec::new(),
5543 service_type: ServiceType::Stateless,
5544 permitted_targets: vec![
5545 ExecutionTarget::Local,
5546 ExecutionTarget::Cloud,
5547 ExecutionTarget::Edge,
5548 ExecutionTarget::Device,
5549 ],
5550 event_trigger: None,
5551 connector_requirements: Vec::new(),
5552 state_schema: None,
5553 use_cases: Vec::new(),
5554 risk: traverse_contracts::default_risk_metadata(),
5555 }
5556 }
5557
5558 fn test_record(lifecycle: Lifecycle) -> CapabilityRegistryRecord {
5559 CapabilityRegistryRecord {
5560 scope: RegistryScope::Private,
5561 id: "content.comments.create-comment-draft".to_string(),
5562 version: "1.0.0".to_string(),
5563 lifecycle,
5564 owner: Owner {
5565 team: "comments".to_string(),
5566 contact: "comments@example.com".to_string(),
5567 },
5568 contract_path: "registry/contract.json".to_string(),
5569 contract_digest: "digest".to_string(),
5570 implementation_kind: ImplementationKind::Executable,
5571 artifact_ref: "artifact:content.comments.create-comment-draft:1.0.0".to_string(),
5572 registered_at: "2026-03-27T00:00:00Z".to_string(),
5573 provenance: RegistryProvenance {
5574 source: "test".to_string(),
5575 author: "Enrico Piovesan".to_string(),
5576 created_at: "2026-03-27T00:00:00Z".to_string(),
5577 },
5578 evidence: traverse_registry::RegistrationEvidence {
5579 evidence_id: "evidence".to_string(),
5580 artifact_ref: "artifact:content.comments.create-comment-draft:1.0.0".to_string(),
5581 capability_id: "content.comments.create-comment-draft".to_string(),
5582 capability_version: "1.0.0".to_string(),
5583 scope: RegistryScope::Private,
5584 governing_spec: "005-capability-registry".to_string(),
5585 validator_version: "0.1.0".to_string(),
5586 produced_at: "2026-03-27T00:00:00Z".to_string(),
5587 result: traverse_registry::RegistrationResult::Passed,
5588 },
5589 }
5590 }
5591
5592 fn test_artifact(
5593 binary: Option<traverse_registry::BinaryReference>,
5594 ) -> CapabilityArtifactRecord {
5595 CapabilityArtifactRecord {
5596 artifact_ref: "artifact:content.comments.create-comment-draft:1.0.0".to_string(),
5597 implementation_kind: ImplementationKind::Executable,
5598 source: SourceReference {
5599 kind: SourceKind::Git,
5600 location: "https://github.com/enricopiovesan/cogolo".to_string(),
5601 },
5602 binary,
5603 workflow_ref: None,
5604 digests: ArtifactDigests {
5605 source_digest: "src-digest".to_string(),
5606 binary_digest: Some("bin-digest".to_string()),
5607 },
5608 provenance: RegistryProvenance {
5609 source: "test".to_string(),
5610 author: "Enrico Piovesan".to_string(),
5611 created_at: "2026-03-27T00:00:00Z".to_string(),
5612 },
5613 }
5614 }
5615
5616 fn test_index_entry(lifecycle: Lifecycle) -> DiscoveryIndexEntry {
5617 DiscoveryIndexEntry {
5618 scope: RegistryScope::Private,
5619 id: "content.comments.create-comment-draft".to_string(),
5620 version: "1.0.0".to_string(),
5621 lifecycle,
5622 owner: Owner {
5623 team: "comments".to_string(),
5624 contact: "comments@example.com".to_string(),
5625 },
5626 summary: "Create a comment draft for a resource".to_string(),
5627 tags: vec!["comments".to_string()],
5628 permissions: Vec::new(),
5629 emits: Vec::new(),
5630 consumes: Vec::new(),
5631 implementation_kind: ImplementationKind::Executable,
5632 composability: traverse_registry::ComposabilityMetadata {
5633 kind: traverse_registry::CompositionKind::Atomic,
5634 patterns: vec![traverse_registry::CompositionPattern::Sequential],
5635 provides: vec!["draft".to_string()],
5636 requires: vec!["authenticated-user".to_string()],
5637 },
5638 artifact_ref: "artifact:content.comments.create-comment-draft:1.0.0".to_string(),
5639 registered_at: "2026-03-27T00:00:00Z".to_string(),
5640 }
5641 }
5642
5643 fn write_runtime_workspace_app_state_fixture(workspace_root: &Path, workspace_id: &str) {
5644 let repo = repo_root();
5645 let state_path = workspace_root
5646 .join(".traverse/workspaces")
5647 .join(workspace_id)
5648 .join("apps/expedition.readiness/1.0.0/registration.json");
5649 fs::create_dir_all(state_path.parent().expect("state path must have parent"))
5650 .expect("workspace state parent should create");
5651 fs::write(
5652 state_path,
5653 serde_json::to_string_pretty(&serde_json::json!({
5654 "status": "registered",
5655 "workspace_id": workspace_id,
5656 "app_id": "expedition.readiness",
5657 "app_version": "1.0.0",
5658 "schema_version": "1.0.0",
5659 "manifest_path": repo.join("examples/applications/expedition-readiness/app.manifest.json").display().to_string(),
5660 "manifest_digest": "sha256:test-manifest",
5661 "bundle_digest": "sha256:test-bundle",
5662 "component_ids": [
5663 "expedition.readiness.capture-expedition-objective-component",
5664 "expedition.readiness.interpret-expedition-intent-component",
5665 "expedition.readiness.assess-conditions-summary-component",
5666 "expedition.readiness.validate-team-readiness-component",
5667 "expedition.readiness.assemble-expedition-plan-component"
5668 ],
5669 "workflow_ids": ["expedition.planning.plan-expedition"],
5670 "components": runtime_workspace_components_json(&repo),
5671 "workflows": [{
5672 "workflow_id": "expedition.planning.plan-expedition",
5673 "workflow_version": "1.0.0",
5674 "workflow_digest": "sha256:test-workflow",
5675 "path": repo.join("workflows/examples/expedition/plan-expedition/workflow.json").display().to_string()
5676 }],
5677 "model_dependencies": [{
5678 "interface_id": "traverse.inference.generate",
5679 "version_range": "^1.0",
5680 "selection_policy": {
5681 "strategy": "priority",
5682 "allow_fallback": true
5683 },
5684 "required_capabilities": ["text_generation"],
5685 "minimum_context_window": 8192,
5686 "candidates": [{
5687 "candidate_id": "ollama-llama-3-2-readiness",
5688 "provider_capability_id": "traverse.inference.generate",
5689 "provider_implementation_id": "ollama.local.generate",
5690 "model_identifier": "llama3.2:3b",
5691 "placement_target": "local",
5692 "priority": 10,
5693 "required_provider_config_keys": ["ollama_base_url"],
5694 "metadata": {
5695 "implementation_kind": "real_local_provider",
5696 "provider": "ollama",
5697 "model_context_window": 8192
5698 }
5699 }]
5700 }],
5701 "effective_config": {
5702 "values": {
5703 "workspace_id": "expedition-local",
5704 "readiness_mode": "deterministic"
5705 },
5706 "redacted_secret_keys": []
5707 },
5708 "state_scope": "workspace_persisted",
5709 "registration_fingerprint": {
5710 "app_id": "expedition.readiness",
5711 "app_version": "1.0.0",
5712 "manifest_digest": "sha256:test-manifest"
5713 }
5714 }))
5715 .expect("workspace app state should serialize"),
5716 )
5717 .expect("workspace app state should write");
5718 }
5719
5720 fn runtime_workspace_components_json(repo: &Path) -> Vec<serde_json::Value> {
5721 [
5722 (
5723 "capture-expedition-objective",
5724 "expedition.planning.capture-expedition-objective",
5725 ),
5726 (
5727 "interpret-expedition-intent",
5728 "expedition.planning.interpret-expedition-intent",
5729 ),
5730 (
5731 "assess-conditions-summary",
5732 "expedition.planning.assess-conditions-summary",
5733 ),
5734 (
5735 "validate-team-readiness",
5736 "expedition.planning.validate-team-readiness",
5737 ),
5738 (
5739 "assemble-expedition-plan",
5740 "expedition.planning.assemble-expedition-plan",
5741 ),
5742 ]
5743 .into_iter()
5744 .map(|(leaf, capability_id)| {
5745 serde_json::json!({
5746 "component_id": format!("expedition.readiness.{leaf}-component"),
5747 "component_version": "1.0.0",
5748 "capability_id": capability_id,
5749 "capability_version": "1.0.0",
5750 "wasm_digest": "sha256:5647c39a1d25d8728350f9619025292a62e78a602068a2ad9b6f075751c93d99",
5751 "manifest_path": repo.join("examples/applications/expedition-readiness/components/validate-team-readiness/component.manifest.json").display().to_string(),
5752 "contract_path": repo.join(format!("contracts/examples/expedition/capabilities/{leaf}/contract.json")).display().to_string(),
5753 "artifact_ref": repo.join("examples/capabilities/team-readiness-agent/artifacts/validate-team-readiness-agent.wasm").display().to_string()
5754 })
5755 })
5756 .collect()
5757 }
5758
5759 fn repo_root() -> PathBuf {
5760 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
5761 }
5762
5763 fn unique_workspace_state_dir() -> PathBuf {
5764 let nanos = std::time::SystemTime::now()
5765 .duration_since(std::time::UNIX_EPOCH)
5766 .unwrap_or_default()
5767 .as_nanos();
5768 let counter = TEMP_COUNTER.fetch_add(1, Ordering::SeqCst);
5769 let path = std::env::temp_dir().join(format!(
5770 "traverse-runtime-workspace-state-test-{}-{nanos}-{counter}",
5771 std::process::id()
5772 ));
5773 fs::create_dir_all(&path).expect("temporary workspace should create");
5774 path
5775 }
5776
5777 #[derive(Debug, Clone)]
5778 struct NoopExecutor;
5779
5780 impl super::LocalExecutor for NoopExecutor {
5781 fn execute(
5782 &self,
5783 _capability: &ResolvedCapability,
5784 _input: &serde_json::Value,
5785 ) -> Result<super::LocalExecutionOutput, super::LocalExecutionFailure> {
5786 Ok(super::LocalExecutionOutput {
5787 value: json!({"draft_id": "draft"}),
5788 emitted_events: Vec::new(),
5789 })
5790 }
5791 }
5792
5793 struct FailingExecutor;
5794
5795 impl super::LocalExecutor for FailingExecutor {
5796 fn execute(
5797 &self,
5798 _capability: &ResolvedCapability,
5799 _input: &serde_json::Value,
5800 ) -> Result<super::LocalExecutionOutput, super::LocalExecutionFailure> {
5801 Err(super::LocalExecutionFailure {
5802 code: super::LocalExecutionFailureCode::ExecutionFailed,
5803 message: "forced failure".to_string(),
5804 })
5805 }
5806 }
5807
5808 fn successful_trace() -> super::RuntimeTrace {
5809 let mut registry = CapabilityRegistry::new();
5810 assert!(registry.register(public_registration()).is_ok());
5811 let runtime = Runtime::new(registry, NoopExecutor)
5812 .with_security_config(RuntimeSecurityConfig::development());
5813 runtime.execute(valid_request()).trace
5814 }
5815
5816 fn failed_trace() -> super::RuntimeTrace {
5817 let mut registry = CapabilityRegistry::new();
5818 assert!(registry.register(public_registration()).is_ok());
5819 let runtime = Runtime::new(registry, FailingExecutor)
5820 .with_security_config(RuntimeSecurityConfig::development());
5821 runtime.execute(valid_request()).trace
5822 }
5823
5824 #[test]
5825 fn selected_capability_id_returns_id_on_success() {
5826 let trace = successful_trace();
5827 assert_eq!(
5828 trace.selected_capability_id(),
5829 Some("content.comments.create-comment-draft")
5830 );
5831 }
5832
5833 #[test]
5834 fn selected_capability_id_returns_none_when_no_selection() {
5835 let registry = CapabilityRegistry::new();
5836 let runtime = Runtime::new(registry, NoopExecutor)
5838 .with_security_config(RuntimeSecurityConfig::development());
5839 let trace = runtime.execute(valid_request()).trace;
5840 assert!(trace.selected_capability_id().is_none());
5841 }
5842
5843 #[test]
5844 fn errors_returns_none_on_success() {
5845 let trace = successful_trace();
5846 assert!(trace.errors().is_none());
5847 }
5848
5849 #[test]
5850 fn errors_returns_error_on_failure() {
5851 let trace = failed_trace();
5852 assert!(trace.errors().is_some());
5853 }
5854
5855 #[test]
5856 fn emitted_events_returns_slice() {
5857 let trace = successful_trace();
5858 let _ = trace.emitted_events();
5860 }
5861
5862 #[test]
5863 fn runtime_trace_exposes_non_sensitive_model_resolution_evidence() {
5864 let trace = successful_trace().with_model_resolution(vec![model_resolution_evidence()]);
5865 let serialized = serde_json::to_string(&trace).unwrap_or_default();
5866
5867 assert_eq!(trace.model_resolution.len(), 1);
5868 assert_eq!(
5869 trace.decision_evidence.model_resolution,
5870 trace.model_resolution
5871 );
5872 assert!(serialized.contains("model_resolution"));
5873 assert!(serialized.contains("ollama.local.generate"));
5874 assert!(serialized.contains("llama3.2:3b"));
5875 assert!(!serialized.contains("private prompt"));
5876 assert!(!serialized.contains("raw source text"));
5877 assert!(!serialized.contains("sk-local-secret"));
5878 }
5879
5880 #[test]
5881 fn output_returns_value_on_success() {
5882 let trace = successful_trace();
5883 assert_eq!(trace.output(), Some(&json!({"draft_id": "draft"})));
5884 }
5885
5886 #[test]
5887 fn output_returns_none_on_failure() {
5888 let trace = failed_trace();
5889 assert!(trace.output().is_none());
5890 }
5891
5892 #[test]
5893 fn is_success_true_on_completed() {
5894 let trace = successful_trace();
5895 assert!(trace.is_success());
5896 }
5897
5898 #[test]
5899 fn is_success_false_on_error() {
5900 let trace = failed_trace();
5901 assert!(!trace.is_success());
5902 }
5903
5904 fn model_resolution_evidence() -> ModelResolutionEvidence {
5905 ModelResolutionEvidence {
5906 phase: ModelResolutionPhase::Execution,
5907 interface_id: "traverse.inference.generate".to_string(),
5908 requested_interface_id: "traverse.inference.generate".to_string(),
5909 requested_placement: ExecutionTarget::Local,
5910 selected: Some(SelectedModelCandidate {
5911 candidate_id: "ollama-llama-3-2".to_string(),
5912 provider_capability_id: "traverse.inference.generate".to_string(),
5913 provider_implementation_id: "ollama.local.generate".to_string(),
5914 model_identifier: "llama3.2:3b".to_string(),
5915 placement_target: ExecutionTarget::Local,
5916 priority: 10,
5917 selection_reason: "selected highest-priority passing candidate".to_string(),
5918 }),
5919 candidates: vec![traverse_registry::ModelCandidateEvaluation {
5920 candidate_id: "ollama-llama-3-2".to_string(),
5921 provider_capability_id: "traverse.inference.generate".to_string(),
5922 provider_implementation_id: "ollama.local.generate".to_string(),
5923 model_identifier: "llama3.2:3b".to_string(),
5924 placement_target: ExecutionTarget::Local,
5925 priority: 10,
5926 readiness: ModelCandidateReadiness::Ready,
5927 rejection_code: Option::<ModelCandidateRejectionCode>::None,
5928 reason: "candidate passed availability, interface, placement, and context checks"
5929 .to_string(),
5930 manifest_order: 0,
5931 }],
5932 failure_code: Option::<ModelCandidateRejectionCode>::None,
5933 }
5934 }
5935}