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