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