1use crate::security::{RuntimeWarning, verify_artifact};
2use crate::{
3 ExecutionFailureReason, ExecutionFailureState, LocalExecutor, Runtime, RuntimeError,
4 RuntimeErrorCode, RuntimeExecutionOutcome, execution_failure_outcome, runtime_error,
5 successful_execution_outcome, validate_payload_against_contract,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value, json};
9use std::collections::BTreeSet;
10use traverse_contracts::EventReference;
11use traverse_registry::{
12 LookupScope, RegistryScope, ResolvedCapability, ResolvedWorkflow, WorkflowEdge,
13 WorkflowEdgePredicate, WorkflowEdgeTrigger, WorkflowNode,
14};
15
16const WORKFLOW_REQUEST_KIND: &str = "workflow_execution_request";
17const WORKFLOW_EVIDENCE_KIND: &str = "workflow_traversal_evidence";
18const WORKFLOW_SCHEMA_VERSION: &str = "1.0.0";
19const WORKFLOW_GOVERNING_SPEC: &str = "007-workflow-registry-traversal";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct WorkflowExecutionRequest {
23 pub kind: String,
24 pub schema_version: String,
25 pub request_id: String,
26 pub workflow_id: String,
27 pub workflow_version: String,
28 pub scope: WorkflowLookupScope,
29 pub input: Value,
30 pub governing_spec: String,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum WorkflowLookupScope {
36 PublicOnly,
37 PreferPrivate,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct WorkflowTraversalEvidence {
42 pub kind: String,
43 pub schema_version: String,
44 pub trace_id: String,
45 pub request_id: String,
46 pub workflow_id: String,
47 pub workflow_version: String,
48 pub governing_spec: String,
49 pub visited_nodes: Vec<WorkflowTraversalStepRecord>,
50 pub traversed_edges: Vec<WorkflowTraversalEdgeRecord>,
51 pub emitted_events: Vec<EventReference>,
52 #[serde(default)]
53 pub waiting_edges: Vec<WaitingWorkflowEdgeContext>,
54 #[serde(default)]
55 pub event_match_records: Vec<EventMatchRecord>,
56 #[serde(default)]
57 pub event_wake_decisions: Vec<EventWakeDecision>,
58 #[serde(default)]
59 pub event_consumptions: Vec<EventConsumptionRecord>,
60 pub result: WorkflowTraversalResult,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct WorkflowTraversalStepRecord {
65 pub step_index: usize,
66 pub node_id: String,
67 pub capability_id: String,
68 pub capability_version: String,
69 pub status: WorkflowTraversalStepStatus,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum WorkflowTraversalStepStatus {
75 Entered,
76 Completed,
77 Failed,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct WorkflowTraversalEdgeRecord {
82 pub edge_id: String,
83 pub from: String,
84 pub to: String,
85 pub trigger: WorkflowTraversalTrigger,
86 #[serde(default)]
87 pub event: Option<EventReference>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct WaitingWorkflowEdgeContext {
92 pub workflow_execution_id: String,
93 pub edge_id: String,
94 pub from_node_id: String,
95 pub to_node_id: String,
96 pub event_ref: EventReference,
97 #[serde(default)]
98 pub predicate: Option<WorkflowEdgePredicate>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct EventMatchRecord {
103 pub event_id: String,
104 pub event_version: String,
105 pub edge_id: String,
106 pub match_result: EventMatchResult,
107 #[serde(default)]
108 pub predicate_result: Option<EventPredicateResult>,
109 #[serde(default)]
110 pub rejection_reason: Option<String>,
111 pub recorded_at: String,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum EventMatchResult {
117 Matched,
118 NotMatched,
119 AlreadyConsumed,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum EventPredicateResult {
125 Passed,
126 Failed,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct EventWakeDecision {
131 pub decision_type: String,
132 pub event_id: String,
133 pub event_version: String,
134 pub edge_id: String,
135 pub workflow_execution_id: String,
136 pub wake_order: usize,
137 pub result: EventWakeDecisionResult,
138 pub recorded_at: String,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum EventWakeDecisionResult {
144 Taken,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct EventConsumptionRecord {
149 pub event_id: String,
150 pub event_version: String,
151 pub edge_id: String,
152 pub workflow_execution_id: String,
153 pub consumed_at: String,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum WorkflowTraversalTrigger {
159 Direct,
160 Event,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct WorkflowTraversalResult {
165 pub status: WorkflowTraversalStatus,
166 #[serde(default)]
167 pub failure_reason: Option<WorkflowTraversalFailureReason>,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum WorkflowTraversalStatus {
173 Completed,
174 Error,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum WorkflowTraversalFailureReason {
180 WorkflowNotFound,
181 WorkflowInvalid,
182 AmbiguousNextEdge,
183 MissingRequiredEvent,
184 TerminalNodeNotReached,
185 StepExecutionFailed,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct WorkflowExecutionResult {
190 pub kind: String,
191 pub schema_version: String,
192 pub request_id: String,
193 pub workflow_id: String,
194 pub workflow_version: String,
195 pub status: WorkflowTraversalStatus,
196 #[serde(default)]
197 pub output: Option<Value>,
198 #[serde(default)]
199 pub error: Option<RuntimeError>,
200 #[serde(default)]
201 pub warnings: Vec<RuntimeWarning>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct WorkflowExecutionOutcome {
206 pub result: WorkflowExecutionResult,
207 pub evidence: WorkflowTraversalEvidence,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211struct EmittedEventRecord {
212 record_id: String,
213 event: EventReference,
214 payload: Option<Value>,
215}
216
217#[derive(Debug, Clone, Default, PartialEq, Eq)]
218struct WorkflowEventEvidenceBundle {
219 waiting_edges: Vec<WaitingWorkflowEdgeContext>,
220 match_records: Vec<EventMatchRecord>,
221 wake_decisions: Vec<EventWakeDecision>,
222 consumptions: Vec<EventConsumptionRecord>,
223}
224
225#[derive(Debug, Clone, Default, PartialEq, Eq)]
226struct EventDrivenEvaluationOutcome {
227 taken_edge_ids: Vec<String>,
228 evidence: WorkflowEventEvidenceBundle,
229}
230
231impl<E> Runtime<E>
232where
233 E: LocalExecutor,
234{
235 #[must_use]
236 #[allow(clippy::needless_pass_by_value)]
237 pub fn execute_workflow(&self, request: WorkflowExecutionRequest) -> WorkflowExecutionOutcome {
238 if let Some(error) = validate_workflow_request(&request) {
239 return workflow_failure(
240 &request,
241 WorkflowTraversalFailureReason::WorkflowInvalid,
242 error,
243 Vec::new(),
244 Vec::new(),
245 Vec::new(),
246 WorkflowEventEvidenceBundle::default(),
247 Vec::new(),
248 );
249 }
250
251 let lookup_scope = map_workflow_lookup_scope(request.scope);
252 let Some(workflow) = self.workflow_registry.find_exact(
253 lookup_scope,
254 &request.workflow_id,
255 &request.workflow_version,
256 ) else {
257 return workflow_failure(
258 &request,
259 WorkflowTraversalFailureReason::WorkflowNotFound,
260 runtime_error(
261 RuntimeErrorCode::CapabilityNotFound,
262 "workflow definition was not found in the workflow registry",
263 json!({"workflow_id": request.workflow_id, "workflow_version": request.workflow_version}),
264 ),
265 Vec::new(),
266 Vec::new(),
267 Vec::new(),
268 WorkflowEventEvidenceBundle::default(),
269 Vec::new(),
270 );
271 };
272
273 if let Err(error) = validate_payload_against_contract(
274 &request.input,
275 &workflow.definition.inputs.schema,
276 RuntimeErrorCode::RequestInvalid,
277 "workflow request input does not satisfy the workflow input contract",
278 ) {
279 return workflow_failure(
280 &request,
281 WorkflowTraversalFailureReason::WorkflowInvalid,
282 error,
283 Vec::new(),
284 Vec::new(),
285 Vec::new(),
286 WorkflowEventEvidenceBundle::default(),
287 Vec::new(),
288 );
289 }
290
291 match self.traverse_workflow(&request, &workflow) {
292 Ok(success) => success,
293 Err(failure) => failure,
294 }
295 }
296
297 pub(crate) fn execute_workflow_capability(
298 &self,
299 mut context: crate::ExecutionContext,
300 selected: &ResolvedCapability,
301 started_execution: crate::StartedExecution,
302 ) -> RuntimeExecutionOutcome {
303 let Some(workflow_ref) = selected.artifact.workflow_ref.as_ref() else {
304 let error = runtime_error(
305 RuntimeErrorCode::ArtifactMissing,
306 "workflow-backed capability is missing its workflow reference",
307 json!({"artifact_ref": selected.record.artifact_ref}),
308 );
309 return execution_failure_outcome(
310 context,
311 ExecutionFailureState {
312 artifact_ref: selected.record.artifact_ref.clone(),
313 started_at: started_execution.started_at,
314 placement: started_execution.placement.clone(),
315 failure_reason: ExecutionFailureReason::ArtifactMissing,
316 },
317 error,
318 Vec::new(),
319 None,
320 );
321 };
322
323 let workflow_scope = match selected.record.scope {
324 RegistryScope::Public => WorkflowLookupScope::PublicOnly,
325 RegistryScope::Private => WorkflowLookupScope::PreferPrivate,
326 };
327 let workflow = self.execute_workflow(WorkflowExecutionRequest {
328 kind: WORKFLOW_REQUEST_KIND.to_string(),
329 schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
330 request_id: context.attempt.request.request_id.clone(),
331 workflow_id: workflow_ref.workflow_id.clone(),
332 workflow_version: workflow_ref.workflow_version.clone(),
333 scope: workflow_scope,
334 input: context.attempt.request.input.clone(),
335 governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
336 });
337 context
338 .attempt
339 .warnings
340 .extend(workflow.result.warnings.iter().cloned());
341
342 match workflow.result.status {
343 WorkflowTraversalStatus::Completed => {
344 let output = workflow.result.output.unwrap_or(Value::Object(Map::new()));
345 let workflow_evidence = workflow.evidence;
346 let emitted_events = workflow_evidence.emitted_events.clone();
347 successful_execution_outcome(
348 context,
349 selected,
350 started_execution,
351 output,
352 emitted_events,
353 Some(workflow_evidence),
354 )
355 }
356 WorkflowTraversalStatus::Error => {
357 let workflow_evidence = workflow.evidence;
358 let emitted_events = workflow_evidence.emitted_events.clone();
359 execution_failure_outcome(
360 context,
361 ExecutionFailureState {
362 artifact_ref: selected.record.artifact_ref.clone(),
363 started_at: started_execution.started_at,
364 placement: started_execution.placement,
365 failure_reason: ExecutionFailureReason::ExecutionFailed,
366 },
367 workflow.result.error.unwrap_or(runtime_error(
368 RuntimeErrorCode::ExecutionFailed,
369 "workflow-backed capability execution failed",
370 json!({}),
371 )),
372 emitted_events,
373 Some(workflow_evidence),
374 )
375 }
376 }
377 }
378
379 #[allow(clippy::result_large_err, clippy::too_many_lines)]
380 fn traverse_workflow(
381 &self,
382 request: &WorkflowExecutionRequest,
383 workflow: &ResolvedWorkflow,
384 ) -> Result<WorkflowExecutionOutcome, WorkflowExecutionOutcome> {
385 let mut state = workflow_state(&request.input);
386 let mut current = workflow.definition.start_node.clone();
387 let mut step_index = 0;
388 let mut visited = Vec::new();
389 let mut traversed = Vec::new();
390 let mut emitted = Vec::new();
391 let mut event_evidence = WorkflowEventEvidenceBundle::default();
392 let mut consumed_event_edges = BTreeSet::new();
393 let mut warnings: Vec<RuntimeWarning> = Vec::new();
394 let workflow_execution_id = format!("workflow_exec_{}", request.request_id);
395
396 loop {
397 let Some(node) = workflow
398 .definition
399 .nodes
400 .iter()
401 .find(|node| node.node_id == current)
402 else {
403 return Err(workflow_failure(
404 request,
405 WorkflowTraversalFailureReason::WorkflowInvalid,
406 runtime_error(
407 RuntimeErrorCode::ExecutionFailed,
408 "workflow node could not be resolved during traversal",
409 json!({"node_id": current}),
410 ),
411 visited,
412 traversed,
413 emitted,
414 event_evidence,
415 warnings,
416 ));
417 };
418
419 visited.push(WorkflowTraversalStepRecord {
420 step_index,
421 node_id: node.node_id.clone(),
422 capability_id: node.capability_id.clone(),
423 capability_version: node.capability_version.clone(),
424 status: WorkflowTraversalStepStatus::Entered,
425 });
426
427 let lookup_scope = map_workflow_lookup_scope(request.scope);
428 let Some(capability) = self.registry.find_exact(
429 lookup_scope,
430 &node.capability_id,
431 &node.capability_version,
432 ) else {
433 return Err(workflow_failure(
434 request,
435 WorkflowTraversalFailureReason::WorkflowInvalid,
436 runtime_error(
437 RuntimeErrorCode::CapabilityNotFound,
438 "workflow node capability was not found in the capability registry",
439 json!({"capability_id": node.capability_id, "capability_version": node.capability_version}),
440 ),
441 visited,
442 traversed,
443 emitted,
444 event_evidence,
445 warnings,
446 ));
447 };
448
449 let artifact_bytes = match crate::load_artifact_bytes_for_verification(&capability) {
454 Ok(bytes) => bytes,
455 Err(error) => {
456 let mut failed = visited;
457 if let Some(last) = failed.last_mut() {
458 last.status = WorkflowTraversalStepStatus::Failed;
459 }
460 return Err(workflow_failure(
461 request,
462 WorkflowTraversalFailureReason::StepExecutionFailed,
463 error,
464 failed,
465 traversed,
466 emitted,
467 event_evidence,
468 warnings,
469 ));
470 }
471 };
472 match verify_artifact(&capability, &artifact_bytes, &self.security) {
473 Ok(record) => {
474 if let Some(code) = record.warning_code {
475 warnings.push(RuntimeWarning {
476 code,
477 message:
478 "unsigned local/dev artifact allowed by development security mode"
479 .to_string(),
480 });
481 }
482 }
483 Err(failure) => {
484 let mut failed = visited;
485 if let Some(last) = failed.last_mut() {
486 last.status = WorkflowTraversalStepStatus::Failed;
487 }
488 return Err(workflow_failure(
489 request,
490 WorkflowTraversalFailureReason::StepExecutionFailed,
491 runtime_error(
492 RuntimeErrorCode::ContractViolation,
493 "artifact signature verification failed before workflow step execution",
494 json!({
495 "code": failure.code(),
496 "artifact_verification": failure.record(),
497 "node_id": node.node_id,
498 }),
499 ),
500 failed,
501 traversed,
502 emitted,
503 event_evidence,
504 warnings,
505 ));
506 }
507 }
508
509 let node_input = node_input(&state, node);
510 if let Err(error) = validate_payload_against_contract(
511 &node_input,
512 &capability.contract.inputs.schema,
513 RuntimeErrorCode::RequestInvalid,
514 "workflow node input does not satisfy the capability input contract",
515 ) {
516 let mut failed = visited;
517 if let Some(last) = failed.last_mut() {
518 last.status = WorkflowTraversalStepStatus::Failed;
519 }
520 return Err(workflow_failure(
521 request,
522 WorkflowTraversalFailureReason::StepExecutionFailed,
523 error,
524 failed,
525 traversed,
526 emitted,
527 event_evidence,
528 warnings,
529 ));
530 }
531
532 let output = match self.executor.execute(&capability, &node_input) {
533 Ok(output) => output,
534 Err(failure) => {
535 let mut failed = visited;
536 if let Some(last) = failed.last_mut() {
537 last.status = WorkflowTraversalStepStatus::Failed;
538 }
539 return Err(workflow_failure(
540 request,
541 WorkflowTraversalFailureReason::StepExecutionFailed,
542 runtime_error(
543 RuntimeErrorCode::ExecutionFailed,
544 &failure.message,
545 json!({"code": format!("{:?}", failure.code)}),
546 ),
547 failed,
548 traversed,
549 emitted,
550 event_evidence,
551 warnings,
552 ));
553 }
554 };
555
556 if let Err(error) = validate_payload_against_contract(
557 &output,
558 &capability.contract.outputs.schema,
559 RuntimeErrorCode::OutputValidationFailed,
560 "workflow node output does not satisfy the capability output contract",
561 ) {
562 let mut failed = visited;
563 if let Some(last) = failed.last_mut() {
564 last.status = WorkflowTraversalStepStatus::Failed;
565 }
566 return Err(workflow_failure(
567 request,
568 WorkflowTraversalFailureReason::StepExecutionFailed,
569 error,
570 failed,
571 traversed,
572 emitted,
573 event_evidence,
574 warnings,
575 ));
576 }
577
578 update_state(&mut state, node, &output);
579 let node_emitted = emitted_events(&output);
580 emitted.extend(node_emitted.iter().map(|record| record.event.clone()));
581 if let Some(last) = visited.last_mut() {
582 last.status = WorkflowTraversalStepStatus::Completed;
583 }
584
585 let outgoing = workflow
586 .definition
587 .edges
588 .iter()
589 .filter(|edge| edge.from == node.node_id)
590 .cloned()
591 .collect::<Vec<_>>();
592 let direct = outgoing
593 .iter()
594 .filter(|edge| edge.trigger == WorkflowEdgeTrigger::Direct)
595 .cloned()
596 .collect::<Vec<_>>();
597 if direct.len() > 1 {
598 return Err(workflow_failure(
599 request,
600 WorkflowTraversalFailureReason::AmbiguousNextEdge,
601 runtime_error(
602 RuntimeErrorCode::ExecutionFailed,
603 "workflow traversal found more than one direct next edge",
604 json!({"node_id": node.node_id}),
605 ),
606 visited,
607 traversed,
608 emitted,
609 event_evidence,
610 warnings,
611 ));
612 }
613 if let Some(edge) = direct.into_iter().next() {
614 traversed.push(edge_record(&edge));
615 current = edge.to;
616 step_index += 1;
617 continue;
618 }
619
620 let waiting_edges = waiting_edge_contexts(
621 &workflow_execution_id,
622 outgoing
623 .iter()
624 .filter(|edge| edge.trigger == WorkflowEdgeTrigger::Event)
625 .cloned()
626 .collect::<Vec<_>>()
627 .as_slice(),
628 );
629 if !waiting_edges.is_empty() {
630 event_evidence.waiting_edges.extend(waiting_edges.clone());
631 }
632 let evaluation = evaluate_event_driven_edges(
633 &waiting_edges,
634 &node_emitted,
635 &mut consumed_event_edges,
636 &format!("{}:step:{step_index}", request.request_id),
637 );
638 event_evidence
639 .match_records
640 .extend(evaluation.evidence.match_records.iter().cloned());
641 event_evidence
642 .wake_decisions
643 .extend(evaluation.evidence.wake_decisions.iter().cloned());
644 event_evidence
645 .consumptions
646 .extend(evaluation.evidence.consumptions.iter().cloned());
647 let matched_event_edges = outgoing
648 .iter()
649 .filter(|edge| {
650 evaluation
651 .taken_edge_ids
652 .iter()
653 .any(|edge_id| edge_id == &edge.edge_id)
654 })
655 .cloned()
656 .collect::<Vec<_>>();
657 if matched_event_edges.len() > 1 {
658 return Err(workflow_failure(
659 request,
660 WorkflowTraversalFailureReason::AmbiguousNextEdge,
661 runtime_error(
662 RuntimeErrorCode::ExecutionFailed,
663 "workflow traversal found more than one event next edge",
664 json!({"node_id": node.node_id}),
665 ),
666 visited,
667 traversed,
668 emitted,
669 event_evidence,
670 warnings,
671 ));
672 }
673 if let Some(edge) = matched_event_edges.into_iter().next() {
674 traversed.push(edge_record(&edge));
675 current = edge.to;
676 step_index += 1;
677 continue;
678 }
679
680 if workflow.definition.terminal_nodes.contains(&node.node_id) {
681 let final_output =
682 final_workflow_output(&state, &workflow.definition.output_projection);
683 if let Err(error) = validate_payload_against_contract(
684 &final_output,
685 &workflow.definition.outputs.schema,
686 RuntimeErrorCode::OutputValidationFailed,
687 "workflow output does not satisfy the workflow output contract",
688 ) {
689 return Err(workflow_failure(
690 request,
691 WorkflowTraversalFailureReason::WorkflowInvalid,
692 error,
693 visited,
694 traversed,
695 emitted,
696 event_evidence,
697 warnings,
698 ));
699 }
700
701 let evidence = WorkflowTraversalEvidence {
702 kind: WORKFLOW_EVIDENCE_KIND.to_string(),
703 schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
704 trace_id: format!("workflow_trace_{}", request.request_id),
705 request_id: request.request_id.clone(),
706 workflow_id: workflow.definition.id.clone(),
707 workflow_version: workflow.definition.version.clone(),
708 governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
709 visited_nodes: visited,
710 traversed_edges: traversed,
711 emitted_events: emitted,
712 waiting_edges: event_evidence.waiting_edges,
713 event_match_records: event_evidence.match_records,
714 event_wake_decisions: event_evidence.wake_decisions,
715 event_consumptions: event_evidence.consumptions,
716 result: WorkflowTraversalResult {
717 status: WorkflowTraversalStatus::Completed,
718 failure_reason: None,
719 },
720 };
721
722 return Ok(WorkflowExecutionOutcome {
723 result: WorkflowExecutionResult {
724 kind: WORKFLOW_REQUEST_KIND.to_string(),
725 schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
726 request_id: request.request_id.clone(),
727 workflow_id: workflow.definition.id.clone(),
728 workflow_version: workflow.definition.version.clone(),
729 status: WorkflowTraversalStatus::Completed,
730 output: Some(final_output),
731 error: None,
732 warnings,
733 },
734 evidence,
735 });
736 }
737
738 let failure_reason = if outgoing
739 .iter()
740 .any(|edge| edge.trigger == WorkflowEdgeTrigger::Event)
741 {
742 WorkflowTraversalFailureReason::MissingRequiredEvent
743 } else {
744 WorkflowTraversalFailureReason::TerminalNodeNotReached
745 };
746
747 return Err(workflow_failure(
748 request,
749 failure_reason,
750 runtime_error(
751 RuntimeErrorCode::ExecutionFailed,
752 "workflow traversal could not reach a valid next node",
753 json!({"node_id": node.node_id}),
754 ),
755 visited,
756 traversed,
757 emitted,
758 event_evidence,
759 warnings,
760 ));
761 }
762 }
763}
764
765fn validate_workflow_request(request: &WorkflowExecutionRequest) -> Option<RuntimeError> {
766 if request.kind != WORKFLOW_REQUEST_KIND {
767 return Some(runtime_error(
768 RuntimeErrorCode::RequestInvalid,
769 "kind must equal workflow_execution_request",
770 json!({"path": "$.kind"}),
771 ));
772 }
773 if request.schema_version != WORKFLOW_SCHEMA_VERSION {
774 return Some(runtime_error(
775 RuntimeErrorCode::RequestInvalid,
776 "schema_version must equal 1.0.0",
777 json!({"path": "$.schema_version"}),
778 ));
779 }
780 if request.governing_spec != WORKFLOW_GOVERNING_SPEC {
781 return Some(runtime_error(
782 RuntimeErrorCode::RequestInvalid,
783 "governing_spec must equal 007-workflow-registry-traversal",
784 json!({"path": "$.governing_spec"}),
785 ));
786 }
787 if request.request_id.trim().is_empty()
788 || request.workflow_id.trim().is_empty()
789 || request.workflow_version.trim().is_empty()
790 {
791 return Some(runtime_error(
792 RuntimeErrorCode::RequestInvalid,
793 "request_id, workflow_id, and workflow_version must be non-empty",
794 json!({"path": "$"}),
795 ));
796 }
797 None
798}
799
800fn map_workflow_lookup_scope(scope: WorkflowLookupScope) -> LookupScope {
801 match scope {
802 WorkflowLookupScope::PublicOnly => LookupScope::PublicOnly,
803 WorkflowLookupScope::PreferPrivate => LookupScope::PreferPrivate,
804 }
805}
806
807fn workflow_state(input: &Value) -> Map<String, Value> {
808 match input {
809 Value::Object(map) => map.clone(),
810 other => {
811 let mut map = Map::new();
812 map.insert("input".to_string(), other.clone());
813 map
814 }
815 }
816}
817
818fn node_input(state: &Map<String, Value>, node: &WorkflowNode) -> Value {
819 let mut input = Map::new();
820 for key in &node.input.from_workflow_input {
821 if let Some(value) = state.get(key) {
822 input.insert(key.clone(), value.clone());
823 }
824 }
825 Value::Object(input)
826}
827
828fn update_state(state: &mut Map<String, Value>, node: &WorkflowNode, output: &Value) {
829 let Value::Object(object) = output else {
830 return;
831 };
832 for key in &node.output.to_workflow_state {
833 if let Some(value) = object.get(key) {
834 state.insert(key.clone(), value.clone());
835 }
836 }
837 if let Some(namespace) = &node.output.publish_to_state_as {
838 state.insert(namespace.clone(), output.clone());
839 }
840}
841
842fn final_workflow_output(state: &Map<String, Value>, output_projection: &[String]) -> Value {
843 if output_projection.is_empty() {
844 return Value::Object(state.clone());
845 }
846 let mut projected = Map::new();
847 for key in output_projection {
848 if let Some(value) = state.get(key) {
849 projected.insert(key.clone(), value.clone());
850 }
851 }
852 Value::Object(projected)
853}
854
855fn emitted_events(output: &Value) -> Vec<EmittedEventRecord> {
856 let Value::Object(object) = output else {
857 return Vec::new();
858 };
859 let Some(Value::Array(events)) = object.get("emitted_events") else {
860 return Vec::new();
861 };
862 events
863 .iter()
864 .enumerate()
865 .filter_map(|(index, event)| {
866 let Value::Object(event) = event else {
867 return None;
868 };
869 Some(EmittedEventRecord {
870 record_id: format!("event_record_{index}"),
871 event: EventReference {
872 event_id: event.get("event_id")?.as_str()?.to_string(),
873 version: event.get("version")?.as_str()?.to_string(),
874 },
875 payload: event.get("payload").cloned(),
876 })
877 })
878 .collect()
879}
880
881fn waiting_edge_contexts(
882 workflow_execution_id: &str,
883 edges: &[WorkflowEdge],
884) -> Vec<WaitingWorkflowEdgeContext> {
885 edges
886 .iter()
887 .filter_map(|edge| {
888 Some(WaitingWorkflowEdgeContext {
889 workflow_execution_id: workflow_execution_id.to_string(),
890 edge_id: edge.edge_id.clone(),
891 from_node_id: edge.from.clone(),
892 to_node_id: edge.to.clone(),
893 event_ref: edge.event.clone()?,
894 predicate: edge.predicate.clone(),
895 })
896 })
897 .collect()
898}
899
900#[allow(clippy::too_many_lines)]
901fn evaluate_event_driven_edges(
902 waiting_edges: &[WaitingWorkflowEdgeContext],
903 emitted_events: &[EmittedEventRecord],
904 consumed_event_edges: &mut BTreeSet<String>,
905 record_prefix: &str,
906) -> EventDrivenEvaluationOutcome {
907 let mut ordered_waiting_edges = waiting_edges.to_vec();
908 ordered_waiting_edges.sort_by(|left, right| {
909 left.workflow_execution_id
910 .cmp(&right.workflow_execution_id)
911 .then_with(|| left.edge_id.cmp(&right.edge_id))
912 });
913
914 let mut outcome = EventDrivenEvaluationOutcome::default();
915 if emitted_events.is_empty() {
916 outcome
917 .evidence
918 .match_records
919 .extend(ordered_waiting_edges.iter().map(|edge| EventMatchRecord {
920 event_id: edge.event_ref.event_id.clone(),
921 event_version: edge.event_ref.version.clone(),
922 edge_id: edge.edge_id.clone(),
923 match_result: EventMatchResult::NotMatched,
924 predicate_result: None,
925 rejection_reason: Some("required event was not emitted".to_string()),
926 recorded_at: format!("{record_prefix}:no_event:{}", edge.edge_id),
927 }));
928 return outcome;
929 }
930
931 let mut wake_order = 1;
932 for (event_index, emitted_event) in emitted_events.iter().enumerate() {
933 for waiting_edge in &ordered_waiting_edges {
934 let match_recorded_at = format!(
935 "{record_prefix}:event:{event_index}:match:{}",
936 waiting_edge.edge_id
937 );
938 if emitted_event.event != waiting_edge.event_ref {
939 outcome.evidence.match_records.push(EventMatchRecord {
940 event_id: emitted_event.event.event_id.clone(),
941 event_version: emitted_event.event.version.clone(),
942 edge_id: waiting_edge.edge_id.clone(),
943 match_result: EventMatchResult::NotMatched,
944 predicate_result: None,
945 rejection_reason: Some(
946 "event id/version did not match the waiting edge".to_string(),
947 ),
948 recorded_at: match_recorded_at,
949 });
950 continue;
951 }
952
953 if let Some(predicate) = waiting_edge.predicate.as_ref() {
954 let predicate_passed =
955 event_payload_field(emitted_event.payload.as_ref(), &predicate.field)
956 .is_some_and(|value| value == &predicate.equals);
957 if !predicate_passed {
958 outcome.evidence.match_records.push(EventMatchRecord {
959 event_id: emitted_event.event.event_id.clone(),
960 event_version: emitted_event.event.version.clone(),
961 edge_id: waiting_edge.edge_id.clone(),
962 match_result: EventMatchResult::NotMatched,
963 predicate_result: Some(EventPredicateResult::Failed),
964 rejection_reason: Some(
965 "event predicate did not match the emitted payload".to_string(),
966 ),
967 recorded_at: match_recorded_at,
968 });
969 continue;
970 }
971 }
972
973 let consumption_key = format!(
974 "{}|{}|{}",
975 emitted_event.record_id, waiting_edge.workflow_execution_id, waiting_edge.edge_id
976 );
977 if consumed_event_edges.contains(&consumption_key) {
978 outcome.evidence.match_records.push(EventMatchRecord {
979 event_id: emitted_event.event.event_id.clone(),
980 event_version: emitted_event.event.version.clone(),
981 edge_id: waiting_edge.edge_id.clone(),
982 match_result: EventMatchResult::AlreadyConsumed,
983 predicate_result: waiting_edge
984 .predicate
985 .as_ref()
986 .map(|_| EventPredicateResult::Passed),
987 rejection_reason: Some(
988 "event record was already consumed for this waiting edge".to_string(),
989 ),
990 recorded_at: match_recorded_at,
991 });
992 continue;
993 }
994
995 consumed_event_edges.insert(consumption_key);
996 outcome.evidence.match_records.push(EventMatchRecord {
997 event_id: emitted_event.event.event_id.clone(),
998 event_version: emitted_event.event.version.clone(),
999 edge_id: waiting_edge.edge_id.clone(),
1000 match_result: EventMatchResult::Matched,
1001 predicate_result: waiting_edge
1002 .predicate
1003 .as_ref()
1004 .map(|_| EventPredicateResult::Passed),
1005 rejection_reason: None,
1006 recorded_at: match_recorded_at.clone(),
1007 });
1008 outcome.taken_edge_ids.push(waiting_edge.edge_id.clone());
1009 let wake_recorded_at = format!(
1010 "{record_prefix}:event:{event_index}:wake:{}",
1011 waiting_edge.edge_id
1012 );
1013 outcome.evidence.wake_decisions.push(EventWakeDecision {
1014 decision_type: "event_wake".to_string(),
1015 event_id: emitted_event.event.event_id.clone(),
1016 event_version: emitted_event.event.version.clone(),
1017 edge_id: waiting_edge.edge_id.clone(),
1018 workflow_execution_id: waiting_edge.workflow_execution_id.clone(),
1019 wake_order,
1020 result: EventWakeDecisionResult::Taken,
1021 recorded_at: wake_recorded_at.clone(),
1022 });
1023 outcome.evidence.consumptions.push(EventConsumptionRecord {
1024 event_id: emitted_event.event.event_id.clone(),
1025 event_version: emitted_event.event.version.clone(),
1026 edge_id: waiting_edge.edge_id.clone(),
1027 workflow_execution_id: waiting_edge.workflow_execution_id.clone(),
1028 consumed_at: wake_recorded_at,
1029 });
1030 wake_order += 1;
1031 }
1032 }
1033 outcome
1034}
1035
1036fn event_payload_field<'a>(payload: Option<&'a Value>, field: &str) -> Option<&'a Value> {
1037 let payload = payload?;
1038 let path = field.strip_prefix("payload.").unwrap_or(field);
1039 if path == "payload" || path.is_empty() {
1040 return Some(payload);
1041 }
1042
1043 let mut current = payload;
1044 for segment in path.split('.') {
1045 let Value::Object(map) = current else {
1046 return None;
1047 };
1048 current = map.get(segment)?;
1049 }
1050 Some(current)
1051}
1052
1053fn edge_record(edge: &WorkflowEdge) -> WorkflowTraversalEdgeRecord {
1054 WorkflowTraversalEdgeRecord {
1055 edge_id: edge.edge_id.clone(),
1056 from: edge.from.clone(),
1057 to: edge.to.clone(),
1058 trigger: match edge.trigger {
1059 WorkflowEdgeTrigger::Direct => WorkflowTraversalTrigger::Direct,
1060 WorkflowEdgeTrigger::Event => WorkflowTraversalTrigger::Event,
1061 },
1062 event: edge.event.clone(),
1063 }
1064}
1065
1066#[allow(clippy::too_many_arguments)]
1067fn workflow_failure(
1068 request: &WorkflowExecutionRequest,
1069 failure_reason: WorkflowTraversalFailureReason,
1070 error: RuntimeError,
1071 visited_nodes: Vec<WorkflowTraversalStepRecord>,
1072 traversed_edges: Vec<WorkflowTraversalEdgeRecord>,
1073 emitted_events: Vec<EventReference>,
1074 event_evidence: WorkflowEventEvidenceBundle,
1075 warnings: Vec<RuntimeWarning>,
1076) -> WorkflowExecutionOutcome {
1077 let evidence = WorkflowTraversalEvidence {
1078 kind: WORKFLOW_EVIDENCE_KIND.to_string(),
1079 schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
1080 trace_id: format!("workflow_trace_{}", request.request_id),
1081 request_id: request.request_id.clone(),
1082 workflow_id: request.workflow_id.clone(),
1083 workflow_version: request.workflow_version.clone(),
1084 governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
1085 visited_nodes,
1086 traversed_edges,
1087 emitted_events,
1088 waiting_edges: event_evidence.waiting_edges,
1089 event_match_records: event_evidence.match_records,
1090 event_wake_decisions: event_evidence.wake_decisions,
1091 event_consumptions: event_evidence.consumptions,
1092 result: WorkflowTraversalResult {
1093 status: WorkflowTraversalStatus::Error,
1094 failure_reason: Some(failure_reason),
1095 },
1096 };
1097
1098 WorkflowExecutionOutcome {
1099 result: WorkflowExecutionResult {
1100 kind: WORKFLOW_REQUEST_KIND.to_string(),
1101 schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
1102 request_id: request.request_id.clone(),
1103 workflow_id: request.workflow_id.clone(),
1104 workflow_version: request.workflow_version.clone(),
1105 status: WorkflowTraversalStatus::Error,
1106 output: None,
1107 error: Some(error),
1108 warnings,
1109 },
1110 evidence,
1111 }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117 use crate::security::RuntimeSecurityConfig;
1118 use crate::{
1119 CandidateCollectionRecord, LocalExecutionFailure, LocalExecutionFailureCode,
1120 RuntimeContext, RuntimeIntent, RuntimeLookup, RuntimeLookupScope, RuntimeRequest,
1121 RuntimeResultStatus, SelectionRecord,
1122 };
1123 use serde_json::json;
1124 use traverse_contracts::{
1125 BinaryFormat as ContractBinaryFormat, CapabilityContract, Condition, Entrypoint,
1126 EntrypointKind, EventReference, EvidenceStatus, EvidenceType, Execution,
1127 ExecutionConstraints, ExecutionTarget, FilesystemAccess, HostApiAccess, IdReference,
1128 Lifecycle, NetworkAccess, Owner, Provenance, ProvenanceSource, SchemaContainer,
1129 ServiceType, SideEffect, SideEffectKind, ValidationEvidence,
1130 };
1131 use traverse_registry::{
1132 ArtifactDigests, ArtifactSignature, ArtifactSignatureScheme, BinaryFormat, BinaryReference,
1133 CapabilityArtifactRecord, CapabilityRegistration, CapabilityRegistry,
1134 ComposabilityMetadata, CompositionKind, CompositionPattern, ImplementationKind,
1135 RegistryProvenance, RegistryScope, SourceKind, SourceReference, WorkflowDefinition,
1136 WorkflowEdge, WorkflowEdgeTrigger, WorkflowNode, WorkflowNodeInput, WorkflowNodeOutput,
1137 WorkflowRegistration, WorkflowRegistry, WorkflowRegistryRecord, workflow_artifact_record,
1138 };
1139
1140 #[test]
1141 fn workflow_request_validation_rejects_invalid_guards() {
1142 let mut request = valid_workflow_request();
1143 request.kind = "bad".to_string();
1144 assert_eq!(
1145 validate_workflow_request(&request).map(|error| error.code),
1146 Some(RuntimeErrorCode::RequestInvalid)
1147 );
1148
1149 let mut request = valid_workflow_request();
1150 request.schema_version = "2.0.0".to_string();
1151 assert_eq!(
1152 validate_workflow_request(&request).map(|error| error.code),
1153 Some(RuntimeErrorCode::RequestInvalid)
1154 );
1155
1156 let mut request = valid_workflow_request();
1157 request.governing_spec = "bad".to_string();
1158 assert_eq!(
1159 validate_workflow_request(&request).map(|error| error.code),
1160 Some(RuntimeErrorCode::RequestInvalid)
1161 );
1162
1163 let mut request = valid_workflow_request();
1164 request.request_id.clear();
1165 assert_eq!(
1166 validate_workflow_request(&request).map(|error| error.code),
1167 Some(RuntimeErrorCode::RequestInvalid)
1168 );
1169 }
1170
1171 #[test]
1172 fn workflow_helpers_cover_state_and_event_extraction_paths() {
1173 let scalar = workflow_state(&json!("value"));
1174 assert_eq!(scalar.get("input"), Some(&json!("value")));
1175
1176 let mut state = workflow_state(&json!({"comment_text": "hello"}));
1177 let node = WorkflowNode {
1178 node_id: "node".to_string(),
1179 capability_id: "content.comments.create-comment-draft".to_string(),
1180 capability_version: "1.0.0".to_string(),
1181 input: WorkflowNodeInput {
1182 from_workflow_input: vec!["comment_text".to_string(), "missing".to_string()],
1183 },
1184 output: WorkflowNodeOutput {
1185 to_workflow_state: vec!["draft_id".to_string()],
1186 publish_to_state_as: None,
1187 },
1188 };
1189 assert_eq!(node_input(&state, &node), json!({"comment_text": "hello"}));
1190 update_state(&mut state, &node, &json!({"draft_id": "draft-1"}));
1191 assert_eq!(state.get("draft_id"), Some(&json!("draft-1")));
1192 update_state(&mut state, &node, &json!("not-an-object"));
1193
1194 assert!(emitted_events(&json!("nope")).is_empty());
1195 assert_eq!(
1196 emitted_events(&json!({
1197 "emitted_events": [
1198 "bad",
1199 {
1200 "event_id": "content.comments.draft-created",
1201 "version": "1.0.0",
1202 "payload": {"severity": "normal"}
1203 },
1204 {"event_id": "bad"}
1205 ]
1206 })),
1207 vec![EmittedEventRecord {
1208 record_id: "event_record_1".to_string(),
1209 event: EventReference {
1210 event_id: "content.comments.draft-created".to_string(),
1211 version: "1.0.0".to_string(),
1212 },
1213 payload: Some(json!({"severity": "normal"})),
1214 }]
1215 );
1216
1217 let edge = WorkflowEdge {
1218 edge_id: "edge".to_string(),
1219 from: "a".to_string(),
1220 to: "b".to_string(),
1221 trigger: WorkflowEdgeTrigger::Event,
1222 event: Some(EventReference {
1223 event_id: "content.comments.draft-created".to_string(),
1224 version: "1.0.0".to_string(),
1225 }),
1226 predicate: None,
1227 };
1228 assert_eq!(edge_record(&edge).trigger, WorkflowTraversalTrigger::Event);
1229 assert_eq!(
1230 map_workflow_lookup_scope(WorkflowLookupScope::PreferPrivate),
1231 LookupScope::PreferPrivate
1232 );
1233 assert_eq!(
1234 event_payload_field(Some(&json!({"severity": "normal"})), "payload.severity"),
1235 Some(&json!("normal"))
1236 );
1237 assert_eq!(
1238 event_payload_field(Some(&json!({"severity": "normal"})), "payload"),
1239 Some(&json!({"severity": "normal"}))
1240 );
1241 assert_eq!(
1242 event_payload_field(Some(&json!("normal")), "payload.severity"),
1243 None
1244 );
1245 }
1246
1247 #[test]
1248 #[allow(clippy::too_many_lines)]
1249 fn executes_workflow_deterministically_and_supports_workflow_backed_capabilities() {
1250 let workflow_registry = workflow_registry_fixture();
1251 let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1252 .with_workflow_registry(workflow_registry)
1253 .with_security_config(RuntimeSecurityConfig::development());
1254
1255 let workflow = runtime.execute_workflow(valid_workflow_request());
1256 assert_eq!(workflow.result.status, WorkflowTraversalStatus::Completed);
1257 assert_eq!(
1258 workflow.result.output,
1259 Some(
1260 json!({"comment_text": "hello", "draft_id": "draft-1", "comment_id": "comment-1"})
1261 )
1262 );
1263 assert_eq!(workflow.evidence.visited_nodes.len(), 3);
1264 assert_eq!(workflow.evidence.traversed_edges.len(), 2);
1265 assert_eq!(workflow.evidence.waiting_edges.len(), 2);
1266 assert_eq!(workflow.evidence.event_wake_decisions.len(), 2);
1267 assert_eq!(workflow.evidence.event_consumptions.len(), 2);
1268 assert!(
1269 workflow
1270 .evidence
1271 .event_match_records
1272 .iter()
1273 .all(|record| record.match_result == EventMatchResult::Matched)
1274 );
1275
1276 let mut composed_registry = capability_registry_fixture();
1277 register_capability_ok(
1278 &mut composed_registry,
1279 CapabilityRegistration {
1280 scope: RegistryScope::Public,
1281 contract: capability_contract(
1282 "content.comments.publish-comment",
1283 vec![],
1284 json!({
1285 "type": "object",
1286 "properties": { "comment_text": { "type": "string" } },
1287 "required": ["comment_text"],
1288 "additionalProperties": true
1289 }),
1290 json!({
1291 "type": "object",
1292 "properties": { "comment_id": { "type": "string" } },
1293 "required": ["comment_id"],
1294 "additionalProperties": true
1295 }),
1296 ),
1297 contract_path: "contracts/publish-comment.json".to_string(),
1298 artifact: workflow_artifact_record(
1299 "content.comments.publish-comment",
1300 "1.0.0",
1301 "artifact-workflow",
1302 ),
1303 registered_at: "2026-03-27T00:10:00Z".to_string(),
1304 tags: vec!["comments".to_string()],
1305 composability: ComposabilityMetadata {
1306 kind: CompositionKind::Composite,
1307 patterns: vec![
1308 CompositionPattern::Sequential,
1309 CompositionPattern::EventDriven,
1310 ],
1311 provides: vec!["published-comment".to_string()],
1312 requires: vec!["draft".to_string()],
1313 },
1314 governing_spec: "005-capability-registry".to_string(),
1315 validator_version: "validator".to_string(),
1316 },
1317 );
1318
1319 let runtime = Runtime::new(composed_registry, WorkflowExecutor)
1320 .with_workflow_registry(workflow_registry_fixture())
1321 .with_security_config(RuntimeSecurityConfig::development());
1322 let result = runtime.execute(RuntimeRequest {
1323 kind: "runtime_request".to_string(),
1324 schema_version: "1.0.0".to_string(),
1325 request_id: "request-workflow".to_string(),
1326 intent: RuntimeIntent {
1327 capability_id: Some("content.comments.publish-comment".to_string()),
1328 capability_version: Some("1.0.0".to_string()),
1329 version_range: None,
1330 intent_key: None,
1331 },
1332 input: json!({"comment_text": "hello"}),
1333 lookup: RuntimeLookup {
1334 scope: RuntimeLookupScope::PublicOnly,
1335 allow_ambiguity: false,
1336 },
1337 context: RuntimeContext {
1338 requested_target: crate::PlacementTarget::Local,
1339 correlation_id: None,
1340 caller: None,
1341 traceparent: None,
1342 tracestate: None,
1343 metadata: None,
1344 identity: None,
1345 },
1346 governing_spec: "006-runtime-request-execution".to_string(),
1347 });
1348 assert_eq!(result.result.status, RuntimeResultStatus::Completed);
1349 assert_eq!(
1350 result.result.output,
1351 Some(
1352 json!({"comment_text": "hello", "draft_id": "draft-1", "comment_id": "comment-1"})
1353 )
1354 );
1355 assert!(
1356 result
1357 .result
1358 .warnings
1359 .iter()
1360 .any(|warning| warning.code == "unsigned_local_dev_artifact")
1361 );
1362 }
1363
1364 #[test]
1365 fn workflow_failures_cover_not_found_missing_events_and_step_failures() {
1366 let workflow_registry = workflow_registry_fixture();
1367 let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1368 .with_workflow_registry(workflow_registry);
1369
1370 let mut missing_request = valid_workflow_request();
1371 missing_request.workflow_id = "missing".to_string();
1372 let missing = runtime.execute_workflow(missing_request);
1373 assert_eq!(
1374 missing.evidence.result.failure_reason,
1375 Some(WorkflowTraversalFailureReason::WorkflowNotFound)
1376 );
1377
1378 let workflow_registry = workflow_registry_fixture();
1379 let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
1380 .with_workflow_registry(workflow_registry)
1381 .with_security_config(RuntimeSecurityConfig::development());
1382 let missing_event = runtime.execute_workflow(valid_workflow_request());
1383 assert_eq!(
1384 missing_event.evidence.result.failure_reason,
1385 Some(WorkflowTraversalFailureReason::MissingRequiredEvent)
1386 );
1387
1388 let runtime = Runtime::new(capability_registry_fixture(), FailingWorkflowExecutor)
1389 .with_workflow_registry(workflow_registry_fixture())
1390 .with_security_config(RuntimeSecurityConfig::development());
1391 let failed = runtime.execute_workflow(valid_workflow_request());
1392 assert_eq!(
1393 failed.evidence.result.failure_reason,
1394 Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1395 );
1396 }
1397
1398 #[test]
1399 fn event_driven_helpers_are_deterministic_and_prevent_duplicate_consumption() {
1400 let event = EmittedEventRecord {
1401 record_id: "event_record_0".to_string(),
1402 event: EventReference {
1403 event_id: "content.comments.validated".to_string(),
1404 version: "1.0.0".to_string(),
1405 },
1406 payload: Some(json!({"severity": "normal"})),
1407 };
1408 let waiting_edges = vec![
1409 WaitingWorkflowEdgeContext {
1410 workflow_execution_id: "wf_exec_b".to_string(),
1411 edge_id: "edge_b".to_string(),
1412 from_node_id: "from".to_string(),
1413 to_node_id: "to".to_string(),
1414 event_ref: event.event.clone(),
1415 predicate: Some(WorkflowEdgePredicate {
1416 field: "payload.severity".to_string(),
1417 equals: json!("normal"),
1418 }),
1419 },
1420 WaitingWorkflowEdgeContext {
1421 workflow_execution_id: "wf_exec_a".to_string(),
1422 edge_id: "edge_a".to_string(),
1423 from_node_id: "from".to_string(),
1424 to_node_id: "to".to_string(),
1425 event_ref: event.event.clone(),
1426 predicate: None,
1427 },
1428 ];
1429 let mut consumed = BTreeSet::new();
1430 let first = evaluate_event_driven_edges(
1431 &waiting_edges,
1432 std::slice::from_ref(&event),
1433 &mut consumed,
1434 "trace",
1435 );
1436 assert_eq!(
1437 first.taken_edge_ids,
1438 vec!["edge_a".to_string(), "edge_b".to_string()]
1439 );
1440 assert_eq!(
1441 first
1442 .evidence
1443 .wake_decisions
1444 .iter()
1445 .map(|decision| (&decision.workflow_execution_id, decision.wake_order))
1446 .collect::<Vec<_>>(),
1447 vec![(&"wf_exec_a".to_string(), 1), (&"wf_exec_b".to_string(), 2)]
1448 );
1449
1450 let second = evaluate_event_driven_edges(&waiting_edges, &[event], &mut consumed, "trace");
1451 assert!(second.taken_edge_ids.is_empty());
1452 assert!(
1453 second
1454 .evidence
1455 .match_records
1456 .iter()
1457 .all(|record| record.match_result == EventMatchResult::AlreadyConsumed)
1458 );
1459 }
1460
1461 #[test]
1462 fn event_driven_helpers_reject_non_matching_predicates() {
1463 let waiting_edges = vec![WaitingWorkflowEdgeContext {
1464 workflow_execution_id: "wf_exec_1".to_string(),
1465 edge_id: "edge_predicate".to_string(),
1466 from_node_id: "assess".to_string(),
1467 to_node_id: "validate".to_string(),
1468 event_ref: EventReference {
1469 event_id: "expedition.conditions.summary-assessed".to_string(),
1470 version: "1.0.0".to_string(),
1471 },
1472 predicate: Some(WorkflowEdgePredicate {
1473 field: "payload.severity".to_string(),
1474 equals: json!("high"),
1475 }),
1476 }];
1477 let emitted = vec![EmittedEventRecord {
1478 record_id: "event_record_0".to_string(),
1479 event: EventReference {
1480 event_id: "expedition.conditions.summary-assessed".to_string(),
1481 version: "1.0.0".to_string(),
1482 },
1483 payload: Some(json!({"severity": "normal"})),
1484 }];
1485 let outcome =
1486 evaluate_event_driven_edges(&waiting_edges, &emitted, &mut BTreeSet::new(), "trace");
1487 assert!(outcome.taken_edge_ids.is_empty());
1488 assert_eq!(
1489 outcome.evidence.match_records,
1490 vec![EventMatchRecord {
1491 event_id: "expedition.conditions.summary-assessed".to_string(),
1492 event_version: "1.0.0".to_string(),
1493 edge_id: "edge_predicate".to_string(),
1494 match_result: EventMatchResult::NotMatched,
1495 predicate_result: Some(EventPredicateResult::Failed),
1496 rejection_reason: Some(
1497 "event predicate did not match the emitted payload".to_string()
1498 ),
1499 recorded_at: "trace:event:0:match:edge_predicate".to_string(),
1500 }]
1501 );
1502 }
1503
1504 #[test]
1505 fn event_driven_helpers_record_non_matching_event_identity() {
1506 let waiting_edges = vec![WaitingWorkflowEdgeContext {
1507 workflow_execution_id: "wf_exec_1".to_string(),
1508 edge_id: "edge_identity".to_string(),
1509 from_node_id: "create".to_string(),
1510 to_node_id: "validate".to_string(),
1511 event_ref: EventReference {
1512 event_id: "content.comments.validated".to_string(),
1513 version: "1.0.0".to_string(),
1514 },
1515 predicate: None,
1516 }];
1517 let emitted = vec![EmittedEventRecord {
1518 record_id: "event_record_0".to_string(),
1519 event: EventReference {
1520 event_id: "content.comments.other".to_string(),
1521 version: "1.0.0".to_string(),
1522 },
1523 payload: None,
1524 }];
1525 let outcome =
1526 evaluate_event_driven_edges(&waiting_edges, &emitted, &mut BTreeSet::new(), "trace");
1527 assert!(outcome.taken_edge_ids.is_empty());
1528 assert_eq!(
1529 outcome.evidence.match_records,
1530 vec![EventMatchRecord {
1531 event_id: "content.comments.other".to_string(),
1532 event_version: "1.0.0".to_string(),
1533 edge_id: "edge_identity".to_string(),
1534 match_result: EventMatchResult::NotMatched,
1535 predicate_result: None,
1536 rejection_reason: Some(
1537 "event id/version did not match the waiting edge".to_string()
1538 ),
1539 recorded_at: "trace:event:0:match:edge_identity".to_string(),
1540 }]
1541 );
1542 }
1543
1544 #[test]
1545 #[allow(clippy::too_many_lines)]
1546 fn workflow_runtime_covers_additional_failure_and_helper_branches() {
1547 let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1548 .with_workflow_registry(workflow_registry_fixture())
1549 .with_security_config(RuntimeSecurityConfig::development());
1550
1551 let invalid_input = runtime.execute_workflow(WorkflowExecutionRequest {
1552 input: json!({}),
1553 ..valid_workflow_request()
1554 });
1555 assert_eq!(
1556 invalid_input.evidence.result.failure_reason,
1557 Some(WorkflowTraversalFailureReason::WorkflowInvalid)
1558 );
1559
1560 let invalid_request = runtime.execute_workflow(WorkflowExecutionRequest {
1561 kind: "bad".to_string(),
1562 ..valid_workflow_request()
1563 });
1564 assert_eq!(
1565 invalid_request.evidence.result.failure_reason,
1566 Some(WorkflowTraversalFailureReason::WorkflowInvalid)
1567 );
1568
1569 let missing_node = runtime.traverse_workflow(
1570 &valid_workflow_request(),
1571 &resolved_workflow(WorkflowDefinition {
1572 start_node: "missing".to_string(),
1573 ..workflow_definition_fixture(
1574 Some(EventReference {
1575 event_id: "content.comments.validated".to_string(),
1576 version: "1.0.0".to_string(),
1577 }),
1578 None,
1579 )
1580 }),
1581 );
1582 assert!(missing_node.is_err());
1583
1584 let missing_capability_runtime = Runtime::new(CapabilityRegistry::new(), WorkflowExecutor)
1585 .with_workflow_registry(workflow_registry_fixture());
1586 let missing_capability =
1587 missing_capability_runtime.execute_workflow(valid_workflow_request());
1588 assert_eq!(
1589 missing_capability.evidence.result.failure_reason,
1590 Some(WorkflowTraversalFailureReason::WorkflowInvalid)
1591 );
1592
1593 let strict_runtime =
1594 Runtime::new(strict_input_capability_registry_fixture(), WorkflowExecutor)
1595 .with_workflow_registry(workflow_registry_fixture())
1596 .with_security_config(RuntimeSecurityConfig::development());
1597 let input_mismatch = strict_runtime.traverse_workflow(
1598 &valid_workflow_request(),
1599 &resolved_workflow(WorkflowDefinition {
1600 nodes: vec![WorkflowNode {
1601 input: WorkflowNodeInput {
1602 from_workflow_input: vec!["missing".to_string()],
1603 },
1604 ..workflow_definition_fixture(
1605 Some(EventReference {
1606 event_id: "content.comments.validated".to_string(),
1607 version: "1.0.0".to_string(),
1608 }),
1609 None,
1610 )
1611 .nodes[0]
1612 .clone()
1613 }],
1614 edges: Vec::new(),
1615 start_node: "create_draft".to_string(),
1616 terminal_nodes: vec!["create_draft".to_string()],
1617 ..workflow_definition_fixture(
1618 Some(EventReference {
1619 event_id: "content.comments.validated".to_string(),
1620 version: "1.0.0".to_string(),
1621 }),
1622 None,
1623 )
1624 }),
1625 );
1626 assert!(input_mismatch.is_err());
1627
1628 let bad_output_runtime =
1629 Runtime::new(capability_registry_fixture(), BadOutputWorkflowExecutor)
1630 .with_workflow_registry(workflow_registry_fixture())
1631 .with_security_config(RuntimeSecurityConfig::development());
1632 let bad_output = bad_output_runtime.execute_workflow(valid_workflow_request());
1633 assert_eq!(
1634 bad_output.evidence.result.failure_reason,
1635 Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1636 );
1637
1638 let direct_success = runtime.traverse_workflow(
1639 &valid_workflow_request(),
1640 &resolved_workflow(workflow_definition_fixture(
1641 Some(EventReference {
1642 event_id: "content.comments.validated".to_string(),
1643 version: "1.0.0".to_string(),
1644 }),
1645 Some(WorkflowEdge {
1646 edge_id: "direct".to_string(),
1647 from: "create_draft".to_string(),
1648 to: "validate_comment".to_string(),
1649 trigger: WorkflowEdgeTrigger::Direct,
1650 event: None,
1651 predicate: None,
1652 }),
1653 )),
1654 );
1655 assert!(direct_success.is_ok());
1656
1657 let ambiguous_direct = runtime.traverse_workflow(
1658 &valid_workflow_request(),
1659 &resolved_workflow(WorkflowDefinition {
1660 edges: vec![
1661 WorkflowEdge {
1662 edge_id: "direct-1".to_string(),
1663 from: "create_draft".to_string(),
1664 to: "validate_comment".to_string(),
1665 trigger: WorkflowEdgeTrigger::Direct,
1666 event: None,
1667 predicate: None,
1668 },
1669 WorkflowEdge {
1670 edge_id: "direct-2".to_string(),
1671 from: "create_draft".to_string(),
1672 to: "persist_comment".to_string(),
1673 trigger: WorkflowEdgeTrigger::Direct,
1674 event: None,
1675 predicate: None,
1676 },
1677 ],
1678 ..workflow_definition_fixture(
1679 Some(EventReference {
1680 event_id: "content.comments.validated".to_string(),
1681 version: "1.0.0".to_string(),
1682 }),
1683 None,
1684 )
1685 }),
1686 );
1687 assert!(ambiguous_direct.is_err());
1688
1689 let ambiguous_event = runtime.traverse_workflow(
1690 &valid_workflow_request(),
1691 &resolved_workflow(WorkflowDefinition {
1692 edges: vec![
1693 WorkflowEdge {
1694 edge_id: "draft_to_validate".to_string(),
1695 from: "create_draft".to_string(),
1696 to: "validate_comment".to_string(),
1697 trigger: WorkflowEdgeTrigger::Event,
1698 event: Some(EventReference {
1699 event_id: "content.comments.draft-created".to_string(),
1700 version: "1.0.0".to_string(),
1701 }),
1702 predicate: None,
1703 },
1704 WorkflowEdge {
1705 edge_id: "draft_to_persist".to_string(),
1706 from: "create_draft".to_string(),
1707 to: "persist_comment".to_string(),
1708 trigger: WorkflowEdgeTrigger::Event,
1709 event: Some(EventReference {
1710 event_id: "content.comments.draft-created".to_string(),
1711 version: "1.0.0".to_string(),
1712 }),
1713 predicate: None,
1714 },
1715 ],
1716 ..workflow_definition_fixture(
1717 Some(EventReference {
1718 event_id: "content.comments.validated".to_string(),
1719 version: "1.0.0".to_string(),
1720 }),
1721 None,
1722 )
1723 }),
1724 );
1725 assert!(ambiguous_event.is_err());
1726
1727 let terminal_miss = runtime.traverse_workflow(
1728 &valid_workflow_request(),
1729 &resolved_workflow(WorkflowDefinition {
1730 edges: Vec::new(),
1731 terminal_nodes: vec!["persist_comment".to_string()],
1732 ..workflow_definition_fixture(
1733 Some(EventReference {
1734 event_id: "content.comments.validated".to_string(),
1735 version: "1.0.0".to_string(),
1736 }),
1737 None,
1738 )
1739 }),
1740 );
1741 assert!(terminal_miss.is_err());
1742
1743 let invalid_final_output = runtime.traverse_workflow(
1744 &valid_workflow_request(),
1745 &resolved_workflow(WorkflowDefinition {
1746 outputs: SchemaContainer {
1747 schema: json!({
1748 "type": "object",
1749 "properties": { "missing": { "type": "string" } },
1750 "required": ["missing"],
1751 "additionalProperties": true
1752 }),
1753 },
1754 ..workflow_definition_fixture(
1755 Some(EventReference {
1756 event_id: "content.comments.validated".to_string(),
1757 version: "1.0.0".to_string(),
1758 }),
1759 None,
1760 )
1761 }),
1762 );
1763 assert!(invalid_final_output.is_err());
1764
1765 let selection = SelectionRecord {
1766 status: crate::SelectionStatus::Selected,
1767 selected_capability_id: Some("content.comments.publish-comment".to_string()),
1768 selected_capability_version: Some("1.0.0".to_string()),
1769 failure_reason: None,
1770 remaining_candidates: Vec::new(),
1771 };
1772 let mut selected = runtime
1773 .registry
1774 .find_exact(
1775 LookupScope::PublicOnly,
1776 "content.comments.create-comment-draft",
1777 "1.0.0",
1778 )
1779 .unwrap_or_else(|| unreachable!("fixture capability missing"));
1780 selected.record.implementation_kind = ImplementationKind::Workflow;
1781 let (attempt, mut emitter) = super::super::begin_attempt(
1782 RuntimeRequest {
1783 kind: "runtime_request".to_string(),
1784 schema_version: "1.0.0".to_string(),
1785 request_id: "workflow-capability".to_string(),
1786 intent: RuntimeIntent {
1787 capability_id: Some("content.comments.publish-comment".to_string()),
1788 capability_version: Some("1.0.0".to_string()),
1789 version_range: None,
1790 intent_key: None,
1791 },
1792 input: json!({"comment_text": "hello"}),
1793 lookup: RuntimeLookup {
1794 scope: RuntimeLookupScope::PublicOnly,
1795 allow_ambiguity: false,
1796 },
1797 context: RuntimeContext {
1798 requested_target: crate::PlacementTarget::Local,
1799 correlation_id: None,
1800 caller: None,
1801 traceparent: None,
1802 tracestate: None,
1803 metadata: None,
1804 identity: None,
1805 },
1806 governing_spec: "006-runtime-request-execution".to_string(),
1807 },
1808 crate::RuntimeObservabilityConfig::default(),
1809 );
1810 emitter.push(
1811 crate::RuntimeState::Discovering,
1812 crate::RuntimeTransitionReasonCode::RequestStarted,
1813 json!({"lookup_scope": RuntimeLookupScope::PublicOnly}),
1814 );
1815 emitter.push(
1816 crate::RuntimeState::EvaluatingConstraints,
1817 crate::RuntimeTransitionReasonCode::CandidatesCollected,
1818 json!({"candidate_count": 1}),
1819 );
1820 emitter.push(
1821 crate::RuntimeState::Selecting,
1822 crate::RuntimeTransitionReasonCode::ConstraintsEvaluated,
1823 json!({"eligible_candidates": 1, "rejected_candidates": 0}),
1824 );
1825 let started_execution = crate::start_selected_execution(
1826 &mut emitter,
1827 &selected,
1828 crate::resolve_placement(crate::PlacementTarget::Local)
1829 .unwrap_or_else(|_| unreachable!("local placement should resolve")),
1830 None,
1831 );
1832 let outcome = runtime.execute_workflow_capability(
1833 crate::ExecutionContext {
1834 attempt,
1835 emitter,
1836 candidate_collection: CandidateCollectionRecord {
1837 lookup_scope: RuntimeLookupScope::PublicOnly,
1838 candidates: Vec::new(),
1839 rejected_candidates: Vec::new(),
1840 },
1841 selection,
1842 },
1843 &selected,
1844 started_execution,
1845 );
1846 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
1847
1848 let mut selected = runtime
1849 .registry
1850 .find_exact(
1851 LookupScope::PublicOnly,
1852 "content.comments.create-comment-draft",
1853 "1.0.0",
1854 )
1855 .unwrap_or_else(|| unreachable!("fixture capability missing"));
1856 selected.record.scope = RegistryScope::Private;
1857 selected.record.implementation_kind = ImplementationKind::Workflow;
1858 selected.artifact.workflow_ref = Some(traverse_registry::WorkflowReference {
1859 workflow_id: "content.comments.publish-comment".to_string(),
1860 workflow_version: "1.0.0".to_string(),
1861 });
1862 let (attempt, mut emitter) = super::super::begin_attempt(
1863 RuntimeRequest {
1864 request_id: "workflow-private".to_string(),
1865 ..valid_runtime_request()
1866 },
1867 crate::RuntimeObservabilityConfig::default(),
1868 );
1869 emitter.push(
1870 crate::RuntimeState::Discovering,
1871 crate::RuntimeTransitionReasonCode::RequestStarted,
1872 json!({"lookup_scope": RuntimeLookupScope::PreferPrivate}),
1873 );
1874 emitter.push(
1875 crate::RuntimeState::EvaluatingConstraints,
1876 crate::RuntimeTransitionReasonCode::CandidatesCollected,
1877 json!({"candidate_count": 1}),
1878 );
1879 emitter.push(
1880 crate::RuntimeState::Selecting,
1881 crate::RuntimeTransitionReasonCode::ConstraintsEvaluated,
1882 json!({"eligible_candidates": 1, "rejected_candidates": 0}),
1883 );
1884 let started_execution = crate::start_selected_execution(
1885 &mut emitter,
1886 &selected,
1887 crate::resolve_placement(crate::PlacementTarget::Local)
1888 .unwrap_or_else(|_| unreachable!("local placement should resolve")),
1889 None,
1890 );
1891 let failing_runtime = Runtime::new(capability_registry_fixture(), FailingWorkflowExecutor)
1892 .with_workflow_registry(workflow_registry_fixture())
1893 .with_security_config(RuntimeSecurityConfig::development());
1894 let outcome = failing_runtime.execute_workflow_capability(
1895 crate::ExecutionContext {
1896 attempt,
1897 emitter,
1898 candidate_collection: CandidateCollectionRecord {
1899 lookup_scope: RuntimeLookupScope::PreferPrivate,
1900 candidates: Vec::new(),
1901 rejected_candidates: Vec::new(),
1902 },
1903 selection: SelectionRecord {
1904 status: crate::SelectionStatus::Selected,
1905 selected_capability_id: Some("content.comments.publish-comment".to_string()),
1906 selected_capability_version: Some("1.0.0".to_string()),
1907 failure_reason: None,
1908 remaining_candidates: Vec::new(),
1909 },
1910 },
1911 &selected,
1912 started_execution,
1913 );
1914 assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
1915
1916 let mut unknown = runtime
1917 .registry
1918 .find_exact(
1919 LookupScope::PublicOnly,
1920 "content.comments.create-comment-draft",
1921 "1.0.0",
1922 )
1923 .unwrap_or_else(|| unreachable!("fixture capability missing"));
1924 unknown.record.id = "unknown".to_string();
1925 let _ = WorkflowExecutor.execute(&unknown, &json!({}));
1926 let _ = MissingEventWorkflowExecutor.execute(&unknown, &json!({}));
1927 let _ = BadOutputWorkflowExecutor.execute(&unknown, &json!({}));
1928 unknown.record.id = "content.comments.persist-comment".to_string();
1929 let _ = MissingEventWorkflowExecutor.execute(&unknown, &json!({}));
1930 }
1931
1932 #[test]
1933 fn pipeline_workflow_merges_namespaced_step_outputs_deterministically() {
1934 let registry = pipeline_capability_registry();
1935 let mut workflows = WorkflowRegistry::new();
1936 register_workflow_ok(&mut workflows, ®istry, pipeline_workflow_registration());
1937 let runtime = Runtime::new(registry, PipelineExecutor)
1938 .with_workflow_registry(workflows)
1939 .with_security_config(RuntimeSecurityConfig::development());
1940
1941 let first = runtime.execute_workflow(pipeline_workflow_request());
1942 let second = runtime.execute_workflow(pipeline_workflow_request());
1943
1944 assert_eq!(first.result.status, WorkflowTraversalStatus::Completed);
1945 assert_eq!(
1946 first.result.output,
1947 Some(json!({
1948 "validate": {"valid": true, "issues": []},
1949 "process": {
1950 "title": "Hello world",
1951 "tags": ["hello", "world"],
1952 "noteType": "fleeting",
1953 "suggestedNextAction": "archive",
1954 "status": "complete"
1955 },
1956 "summarize": {"summary": "Hello world (fleeting)", "wordCount": 3}
1957 }))
1958 );
1959 assert_eq!(first.result, second.result);
1960 assert_eq!(first.evidence.visited_nodes, second.evidence.visited_nodes);
1961
1962 let steps = &first.evidence.visited_nodes;
1963 assert_eq!(steps.len(), 3);
1964 assert_eq!(steps[0].step_index, 0);
1965 assert_eq!(steps[0].capability_id, "content.comments.pipeline-validate");
1966 assert_eq!(steps[1].step_index, 1);
1967 assert_eq!(steps[1].capability_id, "content.comments.pipeline-process");
1968 assert_eq!(steps[2].step_index, 2);
1969 assert_eq!(
1970 steps[2].capability_id,
1971 "content.comments.pipeline-summarize"
1972 );
1973 assert!(
1974 steps
1975 .iter()
1976 .all(|step| step.status == WorkflowTraversalStepStatus::Completed)
1977 );
1978 }
1979
1980 #[test]
1981 fn pipeline_workflow_stops_on_failed_step_with_failed_step_id_in_trace() {
1982 let registry = pipeline_capability_registry();
1983 let mut workflows = WorkflowRegistry::new();
1984 register_workflow_ok(&mut workflows, ®istry, pipeline_workflow_registration());
1985 let runtime = Runtime::new(registry, FailingPipelineExecutor)
1986 .with_workflow_registry(workflows)
1987 .with_security_config(RuntimeSecurityConfig::development());
1988
1989 let outcome = runtime.execute_workflow(pipeline_workflow_request());
1990
1991 assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
1992 assert_eq!(
1993 outcome.evidence.result.failure_reason,
1994 Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1995 );
1996 let steps = &outcome.evidence.visited_nodes;
1997 assert_eq!(steps.len(), 2);
1998 assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Completed);
1999 assert_eq!(steps[1].node_id, "process_note");
2000 assert_eq!(steps[1].status, WorkflowTraversalStepStatus::Failed);
2001 }
2002
2003 #[test]
2004 fn workflow_step_rejects_unsigned_artifact_under_default_security_config() {
2005 let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2009 .with_workflow_registry(workflow_registry_fixture());
2010
2011 let outcome = runtime.execute_workflow(valid_workflow_request());
2012
2013 assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2014 assert_eq!(
2015 outcome.evidence.result.failure_reason,
2016 Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2017 );
2018 let error = outcome.result.error;
2019 assert_eq!(
2020 error.as_ref().map(|error| error.code),
2021 Some(RuntimeErrorCode::ContractViolation)
2022 );
2023 assert_eq!(
2024 error
2025 .as_ref()
2026 .and_then(|error| error.details.get("code"))
2027 .and_then(Value::as_str),
2028 Some("missing_signature")
2029 );
2030 assert_eq!(
2031 error
2032 .as_ref()
2033 .and_then(|error| error.details.get("node_id"))
2034 .and_then(Value::as_str),
2035 Some("create_draft")
2036 );
2037 let steps = &outcome.evidence.visited_nodes;
2038 assert_eq!(steps.len(), 1);
2039 assert_eq!(steps[0].node_id, "create_draft");
2040 assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Failed);
2041 assert!(outcome.result.warnings.is_empty());
2042 }
2043
2044 #[test]
2045 fn workflow_step_unsigned_artifact_warns_and_executes_in_development_mode() {
2046 let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2047 .with_workflow_registry(workflow_registry_fixture())
2048 .with_security_config(RuntimeSecurityConfig::development());
2049
2050 let outcome = runtime.execute_workflow(valid_workflow_request());
2051
2052 assert_eq!(outcome.result.status, WorkflowTraversalStatus::Completed);
2053 assert_eq!(outcome.result.warnings.len(), 3);
2054 assert!(
2055 outcome
2056 .result
2057 .warnings
2058 .iter()
2059 .all(|warning| warning.code == "unsigned_local_dev_artifact")
2060 );
2061 }
2062
2063 #[test]
2064 fn workflow_step_fails_when_signed_artifact_bytes_cannot_be_loaded() {
2065 let runtime = Runtime::new(
2066 signed_missing_binary_capability_registry_fixture(),
2067 WorkflowExecutor,
2068 )
2069 .with_workflow_registry(workflow_registry_fixture());
2070
2071 let outcome = runtime.execute_workflow(valid_workflow_request());
2072
2073 assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2074 assert_eq!(
2075 outcome.evidence.result.failure_reason,
2076 Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2077 );
2078 let error = outcome.result.error;
2079 assert_eq!(
2080 error.as_ref().map(|error| error.code),
2081 Some(RuntimeErrorCode::ArtifactMissing)
2082 );
2083 assert_eq!(
2084 error
2085 .as_ref()
2086 .and_then(|error| error.details.get("code"))
2087 .and_then(Value::as_str),
2088 Some("artifact_load_failed")
2089 );
2090 let steps = &outcome.evidence.visited_nodes;
2091 assert_eq!(steps.len(), 1);
2092 assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Failed);
2093 }
2094
2095 fn pipeline_capability_registry() -> CapabilityRegistry {
2096 let mut registry = CapabilityRegistry::new();
2097 for id in [
2098 "content.comments.pipeline-validate",
2099 "content.comments.pipeline-process",
2100 "content.comments.pipeline-summarize",
2101 ] {
2102 register_capability_ok(
2103 &mut registry,
2104 CapabilityRegistration {
2105 scope: RegistryScope::Public,
2106 contract: capability_contract(
2107 id,
2108 Vec::new(),
2109 json!({"type": "object", "additionalProperties": true}),
2110 json!({"type": "object", "additionalProperties": true}),
2111 ),
2112 contract_path: format!("registry/{id}.json"),
2113 artifact: CapabilityArtifactRecord {
2114 artifact_ref: format!("artifact-{id}"),
2115 implementation_kind: ImplementationKind::Executable,
2116 source: SourceReference {
2117 kind: SourceKind::Git,
2118 location: format!("https://example.com/{id}.git"),
2119 },
2120 binary: Some(BinaryReference {
2121 format: BinaryFormat::Wasm,
2122 location: format!("{id}.wasm"),
2123 signature: None,
2124 }),
2125 workflow_ref: None,
2126 digests: ArtifactDigests {
2127 source_digest: "source".to_string(),
2128 binary_digest: Some("binary".to_string()),
2129 },
2130 provenance: RegistryProvenance {
2131 source: "fixtures".to_string(),
2132 author: "Enrico".to_string(),
2133 created_at: "2026-03-27T00:00:00Z".to_string(),
2134 },
2135 },
2136 registered_at: "2026-03-27T00:00:00Z".to_string(),
2137 tags: vec!["pipeline".to_string()],
2138 composability: ComposabilityMetadata {
2139 kind: CompositionKind::Atomic,
2140 patterns: vec![CompositionPattern::Sequential],
2141 provides: vec!["pipeline-step".to_string()],
2142 requires: Vec::new(),
2143 },
2144 governing_spec: "005-capability-registry".to_string(),
2145 validator_version: "validator".to_string(),
2146 },
2147 );
2148 }
2149 registry
2150 }
2151
2152 #[allow(clippy::too_many_lines)]
2153 fn pipeline_workflow_registration() -> WorkflowRegistration {
2154 WorkflowRegistration {
2155 scope: RegistryScope::Public,
2156 definition: WorkflowDefinition {
2157 kind: "workflow_definition".to_string(),
2158 schema_version: "1.0.0".to_string(),
2159 id: "content.comments.pipeline".to_string(),
2160 name: "pipeline".to_string(),
2161 version: "1.0.0".to_string(),
2162 lifecycle: Lifecycle::Active,
2163 owner: Owner {
2164 team: "traverse-core".to_string(),
2165 contact: "test@example.com".to_string(),
2166 },
2167 summary: "Deterministic three-step pipeline fixture.".to_string(),
2168 inputs: SchemaContainer {
2169 schema: json!({
2170 "type": "object",
2171 "required": ["note"],
2172 "properties": {"note": {"type": "string"}},
2173 "additionalProperties": false
2174 }),
2175 },
2176 outputs: SchemaContainer {
2177 schema: json!({
2178 "type": "object",
2179 "required": ["validate", "process", "summarize"],
2180 "properties": {
2181 "validate": {"type": "object"},
2182 "process": {"type": "object"},
2183 "summarize": {"type": "object"}
2184 },
2185 "additionalProperties": false
2186 }),
2187 },
2188 nodes: vec![
2189 WorkflowNode {
2190 node_id: "validate_note".to_string(),
2191 capability_id: "content.comments.pipeline-validate".to_string(),
2192 capability_version: "1.0.0".to_string(),
2193 input: WorkflowNodeInput {
2194 from_workflow_input: vec!["note".to_string()],
2195 },
2196 output: WorkflowNodeOutput {
2197 to_workflow_state: Vec::new(),
2198 publish_to_state_as: Some("validate".to_string()),
2199 },
2200 },
2201 WorkflowNode {
2202 node_id: "process_note".to_string(),
2203 capability_id: "content.comments.pipeline-process".to_string(),
2204 capability_version: "1.0.0".to_string(),
2205 input: WorkflowNodeInput {
2206 from_workflow_input: vec!["note".to_string()],
2207 },
2208 output: WorkflowNodeOutput {
2209 to_workflow_state: vec![
2210 "title".to_string(),
2211 "tags".to_string(),
2212 "noteType".to_string(),
2213 "suggestedNextAction".to_string(),
2214 "status".to_string(),
2215 ],
2216 publish_to_state_as: Some("process".to_string()),
2217 },
2218 },
2219 WorkflowNode {
2220 node_id: "summarize_note".to_string(),
2221 capability_id: "content.comments.pipeline-summarize".to_string(),
2222 capability_version: "1.0.0".to_string(),
2223 input: WorkflowNodeInput {
2224 from_workflow_input: vec![
2225 "title".to_string(),
2226 "tags".to_string(),
2227 "noteType".to_string(),
2228 "suggestedNextAction".to_string(),
2229 "status".to_string(),
2230 ],
2231 },
2232 output: WorkflowNodeOutput {
2233 to_workflow_state: Vec::new(),
2234 publish_to_state_as: Some("summarize".to_string()),
2235 },
2236 },
2237 ],
2238 edges: vec![
2239 WorkflowEdge {
2240 edge_id: "validate_to_process".to_string(),
2241 from: "validate_note".to_string(),
2242 to: "process_note".to_string(),
2243 trigger: WorkflowEdgeTrigger::Direct,
2244 event: None,
2245 predicate: None,
2246 },
2247 WorkflowEdge {
2248 edge_id: "process_to_summarize".to_string(),
2249 from: "process_note".to_string(),
2250 to: "summarize_note".to_string(),
2251 trigger: WorkflowEdgeTrigger::Direct,
2252 event: None,
2253 predicate: None,
2254 },
2255 ],
2256 start_node: "validate_note".to_string(),
2257 terminal_nodes: vec!["summarize_note".to_string()],
2258 output_projection: vec![
2259 "validate".to_string(),
2260 "process".to_string(),
2261 "summarize".to_string(),
2262 ],
2263 tags: vec!["pipeline".to_string()],
2264 governing_spec: "007-workflow-registry-traversal".to_string(),
2265 },
2266 workflow_path: "workflows/content.comments.pipeline/workflow.json".to_string(),
2267 registered_at: "2026-07-08T00:00:00Z".to_string(),
2268 validator_version: "validator".to_string(),
2269 }
2270 }
2271
2272 fn pipeline_workflow_request() -> WorkflowExecutionRequest {
2273 WorkflowExecutionRequest {
2274 kind: "workflow_execution_request".to_string(),
2275 schema_version: "1.0.0".to_string(),
2276 request_id: "pipeline-request".to_string(),
2277 workflow_id: "content.comments.pipeline".to_string(),
2278 workflow_version: "1.0.0".to_string(),
2279 scope: WorkflowLookupScope::PublicOnly,
2280 input: json!({"note": "Hello world"}),
2281 governing_spec: "007-workflow-registry-traversal".to_string(),
2282 }
2283 }
2284
2285 struct PipelineExecutor;
2286
2287 impl LocalExecutor for PipelineExecutor {
2288 fn execute(
2289 &self,
2290 capability: &ResolvedCapability,
2291 _input: &Value,
2292 ) -> Result<Value, LocalExecutionFailure> {
2293 let output = match capability.record.id.as_str() {
2294 "content.comments.pipeline-validate" => json!({"valid": true, "issues": []}),
2295 "content.comments.pipeline-process" => json!({
2296 "title": "Hello world",
2297 "tags": ["hello", "world"],
2298 "noteType": "fleeting",
2299 "suggestedNextAction": "archive",
2300 "status": "complete"
2301 }),
2302 _ => json!({"summary": "Hello world (fleeting)", "wordCount": 3}),
2303 };
2304 Ok(output)
2305 }
2306 }
2307
2308 struct FailingPipelineExecutor;
2309
2310 impl LocalExecutor for FailingPipelineExecutor {
2311 fn execute(
2312 &self,
2313 capability: &ResolvedCapability,
2314 _input: &Value,
2315 ) -> Result<Value, LocalExecutionFailure> {
2316 match capability.record.id.as_str() {
2317 "content.comments.pipeline-validate" => Ok(json!({"valid": true, "issues": []})),
2318 other => Err(LocalExecutionFailure {
2319 code: LocalExecutionFailureCode::ExecutionFailed,
2320 message: format!("step failed: {other}"),
2321 }),
2322 }
2323 }
2324 }
2325
2326 fn capability_registry_fixture() -> CapabilityRegistry {
2327 build_capability_registry(false, None)
2328 }
2329
2330 fn signed_missing_binary_capability_registry_fixture() -> CapabilityRegistry {
2331 build_capability_registry(
2332 false,
2333 Some(ArtifactSignature {
2334 scheme: ArtifactSignatureScheme::Ed25519,
2335 public_key_hex: Some("00".repeat(32)),
2336 signature_hex: Some("00".repeat(64)),
2337 sigstore_bundle_ref: None,
2338 }),
2339 )
2340 }
2341
2342 #[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
2343 fn build_capability_registry(
2344 strict_inputs: bool,
2345 signature: Option<ArtifactSignature>,
2346 ) -> CapabilityRegistry {
2347 let mut registry = CapabilityRegistry::new();
2348 for (id, emits, output, required_key) in [
2349 (
2350 "content.comments.create-comment-draft",
2351 vec![EventReference {
2352 event_id: "content.comments.draft-created".to_string(),
2353 version: "1.0.0".to_string(),
2354 }],
2355 json!({
2356 "type": "object",
2357 "properties": {
2358 "draft_id": { "type": "string" },
2359 "emitted_events": { "type": "array" }
2360 },
2361 "required": ["draft_id"],
2362 "additionalProperties": true
2363 }),
2364 "comment_text",
2365 ),
2366 (
2367 "content.comments.validate-comment",
2368 vec![EventReference {
2369 event_id: "content.comments.validated".to_string(),
2370 version: "1.0.0".to_string(),
2371 }],
2372 json!({
2373 "type": "object",
2374 "properties": {
2375 "draft_id": { "type": "string" },
2376 "emitted_events": { "type": "array" }
2377 },
2378 "required": ["draft_id"],
2379 "additionalProperties": true
2380 }),
2381 "draft_id",
2382 ),
2383 (
2384 "content.comments.persist-comment",
2385 vec![],
2386 json!({
2387 "type": "object",
2388 "properties": { "comment_id": { "type": "string" } },
2389 "required": ["comment_id"],
2390 "additionalProperties": true
2391 }),
2392 "draft_id",
2393 ),
2394 ] {
2395 register_capability_ok(
2396 &mut registry,
2397 CapabilityRegistration {
2398 scope: RegistryScope::Public,
2399 contract: capability_contract(
2400 id,
2401 emits,
2402 json!({
2403 "type": "object",
2404 "properties": {
2405 "comment_text": { "type": "string" },
2406 "draft_id": { "type": "string" }
2407 },
2408 "required": if strict_inputs {
2409 vec![required_key]
2410 } else {
2411 Vec::<&str>::new()
2412 },
2413 "additionalProperties": true
2414 }),
2415 output,
2416 ),
2417 contract_path: format!("registry/{id}.json"),
2418 artifact: CapabilityArtifactRecord {
2419 artifact_ref: format!("artifact-{id}"),
2420 implementation_kind: ImplementationKind::Executable,
2421 source: SourceReference {
2422 kind: SourceKind::Git,
2423 location: format!("https://example.com/{id}.git"),
2424 },
2425 binary: Some(BinaryReference {
2426 format: BinaryFormat::Wasm,
2427 location: format!("{id}.wasm"),
2428 signature: signature.clone(),
2429 }),
2430 workflow_ref: None,
2431 digests: ArtifactDigests {
2432 source_digest: "source".to_string(),
2433 binary_digest: Some("binary".to_string()),
2434 },
2435 provenance: RegistryProvenance {
2436 source: "fixtures".to_string(),
2437 author: "Enrico".to_string(),
2438 created_at: "2026-03-27T00:00:00Z".to_string(),
2439 },
2440 },
2441 registered_at: "2026-03-27T00:00:00Z".to_string(),
2442 tags: vec!["comments".to_string()],
2443 composability: ComposabilityMetadata {
2444 kind: CompositionKind::Atomic,
2445 patterns: vec![CompositionPattern::Sequential],
2446 provides: vec!["comment".to_string()],
2447 requires: Vec::new(),
2448 },
2449 governing_spec: "005-capability-registry".to_string(),
2450 validator_version: "validator".to_string(),
2451 },
2452 );
2453 }
2454 registry
2455 }
2456
2457 fn strict_input_capability_registry_fixture() -> CapabilityRegistry {
2458 build_capability_registry(true, None)
2459 }
2460
2461 fn workflow_registry_fixture() -> WorkflowRegistry {
2462 let registry = capability_registry_fixture();
2463 let mut workflows = WorkflowRegistry::new();
2464 register_workflow_ok(
2465 &mut workflows,
2466 ®istry,
2467 WorkflowRegistration {
2468 scope: RegistryScope::Public,
2469 definition: workflow_definition_fixture(
2470 Some(EventReference {
2471 event_id: "content.comments.validated".to_string(),
2472 version: "1.0.0".to_string(),
2473 }),
2474 None,
2475 ),
2476 workflow_path: "workflows/publish-comment.json".to_string(),
2477 registered_at: "2026-03-27T00:00:00Z".to_string(),
2478 validator_version: "workflow-validator".to_string(),
2479 },
2480 );
2481 workflows
2482 }
2483
2484 fn workflow_definition_fixture(
2485 second_event: Option<EventReference>,
2486 direct_edge: Option<WorkflowEdge>,
2487 ) -> WorkflowDefinition {
2488 let mut edges = vec![
2489 WorkflowEdge {
2490 edge_id: "draft_to_validate".to_string(),
2491 from: "create_draft".to_string(),
2492 to: "validate_comment".to_string(),
2493 trigger: WorkflowEdgeTrigger::Event,
2494 event: Some(EventReference {
2495 event_id: "content.comments.draft-created".to_string(),
2496 version: "1.0.0".to_string(),
2497 }),
2498 predicate: None,
2499 },
2500 WorkflowEdge {
2501 edge_id: "validate_to_persist".to_string(),
2502 from: "validate_comment".to_string(),
2503 to: "persist_comment".to_string(),
2504 trigger: WorkflowEdgeTrigger::Event,
2505 event: second_event,
2506 predicate: None,
2507 },
2508 ];
2509 if let Some(edge) = direct_edge {
2510 edges.push(edge);
2511 }
2512 WorkflowDefinition {
2513 kind: "workflow_definition".to_string(),
2514 schema_version: "1.0.0".to_string(),
2515 id: "content.comments.publish-comment".to_string(),
2516 name: "publish-comment".to_string(),
2517 version: "1.0.0".to_string(),
2518 lifecycle: Lifecycle::Active,
2519 owner: Owner {
2520 team: "comments".to_string(),
2521 contact: "comments@example.com".to_string(),
2522 },
2523 summary: "Publish a comment deterministically.".to_string(),
2524 inputs: SchemaContainer {
2525 schema: json!({
2526 "type": "object",
2527 "properties": { "comment_text": { "type": "string" } },
2528 "required": ["comment_text"],
2529 "additionalProperties": true
2530 }),
2531 },
2532 outputs: SchemaContainer {
2533 schema: json!({
2534 "type": "object",
2535 "properties": { "comment_id": { "type": "string" } },
2536 "required": ["comment_id"],
2537 "additionalProperties": true
2538 }),
2539 },
2540 nodes: vec![
2541 WorkflowNode {
2542 node_id: "create_draft".to_string(),
2543 capability_id: "content.comments.create-comment-draft".to_string(),
2544 capability_version: "1.0.0".to_string(),
2545 input: WorkflowNodeInput {
2546 from_workflow_input: vec!["comment_text".to_string()],
2547 },
2548 output: WorkflowNodeOutput {
2549 to_workflow_state: vec!["draft_id".to_string()],
2550 publish_to_state_as: None,
2551 },
2552 },
2553 WorkflowNode {
2554 node_id: "validate_comment".to_string(),
2555 capability_id: "content.comments.validate-comment".to_string(),
2556 capability_version: "1.0.0".to_string(),
2557 input: WorkflowNodeInput {
2558 from_workflow_input: vec!["draft_id".to_string()],
2559 },
2560 output: WorkflowNodeOutput {
2561 to_workflow_state: vec!["draft_id".to_string()],
2562 publish_to_state_as: None,
2563 },
2564 },
2565 WorkflowNode {
2566 node_id: "persist_comment".to_string(),
2567 capability_id: "content.comments.persist-comment".to_string(),
2568 capability_version: "1.0.0".to_string(),
2569 input: WorkflowNodeInput {
2570 from_workflow_input: vec!["draft_id".to_string()],
2571 },
2572 output: WorkflowNodeOutput {
2573 to_workflow_state: vec!["comment_id".to_string()],
2574 publish_to_state_as: None,
2575 },
2576 },
2577 ],
2578 edges,
2579 start_node: "create_draft".to_string(),
2580 terminal_nodes: vec!["persist_comment".to_string()],
2581 output_projection: Vec::new(),
2582 tags: vec!["comments".to_string()],
2583 governing_spec: "007-workflow-registry-traversal".to_string(),
2584 }
2585 }
2586
2587 fn capability_contract(
2588 id: &str,
2589 emits: Vec<EventReference>,
2590 inputs: Value,
2591 outputs: Value,
2592 ) -> CapabilityContract {
2593 CapabilityContract {
2594 kind: "capability_contract".to_string(),
2595 schema_version: "1.0.0".to_string(),
2596 id: id.to_string(),
2597 namespace: "content.comments".to_string(),
2598 name: id.rsplit('.').next().unwrap_or("capability").to_string(),
2599 version: "1.0.0".to_string(),
2600 lifecycle: Lifecycle::Active,
2601 owner: Owner {
2602 team: "comments".to_string(),
2603 contact: "comments@example.com".to_string(),
2604 },
2605 summary: "workflow fixture capability".to_string(),
2606 description: "workflow fixture capability used in runtime tests".to_string(),
2607 inputs: SchemaContainer { schema: inputs },
2608 outputs: SchemaContainer { schema: outputs },
2609 preconditions: vec![Condition {
2610 id: "precondition".to_string(),
2611 description: "must be valid".to_string(),
2612 }],
2613 postconditions: vec![Condition {
2614 id: "postcondition".to_string(),
2615 description: "must produce output".to_string(),
2616 }],
2617 side_effects: vec![SideEffect {
2618 kind: SideEffectKind::MemoryOnly,
2619 description: "memory only".to_string(),
2620 }],
2621 emits,
2622 consumes: Vec::new(),
2623 permissions: vec![IdReference {
2624 id: "permission".to_string(),
2625 }],
2626 execution: Execution {
2627 binary_format: ContractBinaryFormat::Wasm,
2628 entrypoint: Entrypoint {
2629 kind: EntrypointKind::WasiCommand,
2630 command: "run".to_string(),
2631 },
2632 preferred_targets: vec![ExecutionTarget::Local],
2633 constraints: ExecutionConstraints {
2634 host_api_access: HostApiAccess::None,
2635 network_access: NetworkAccess::Forbidden,
2636 filesystem_access: FilesystemAccess::None,
2637 },
2638 },
2639 policies: Vec::new(),
2640 dependencies: Vec::new(),
2641 provenance: Provenance {
2642 source: ProvenanceSource::Greenfield,
2643 author: "Enrico".to_string(),
2644 created_at: "2026-03-27T00:00:00Z".to_string(),
2645 spec_ref: Some("007-workflow-registry-traversal".to_string()),
2646 adr_refs: Vec::new(),
2647 exception_refs: Vec::new(),
2648 },
2649 evidence: vec![ValidationEvidence {
2650 evidence_id: "evidence".to_string(),
2651 evidence_type: EvidenceType::ContractValidation,
2652 status: EvidenceStatus::Passed,
2653 }],
2654 service_type: ServiceType::Stateless,
2655 permitted_targets: vec![
2656 ExecutionTarget::Local,
2657 ExecutionTarget::Cloud,
2658 ExecutionTarget::Edge,
2659 ExecutionTarget::Device,
2660 ],
2661 event_trigger: None,
2662 connector_requirements: Vec::new(),
2663 state_schema: None,
2664 }
2665 }
2666
2667 fn valid_workflow_request() -> WorkflowExecutionRequest {
2668 WorkflowExecutionRequest {
2669 kind: "workflow_execution_request".to_string(),
2670 schema_version: "1.0.0".to_string(),
2671 request_id: "workflow-request".to_string(),
2672 workflow_id: "content.comments.publish-comment".to_string(),
2673 workflow_version: "1.0.0".to_string(),
2674 scope: WorkflowLookupScope::PublicOnly,
2675 input: json!({"comment_text": "hello"}),
2676 governing_spec: "007-workflow-registry-traversal".to_string(),
2677 }
2678 }
2679
2680 fn valid_runtime_request() -> RuntimeRequest {
2681 RuntimeRequest {
2682 kind: "runtime_request".to_string(),
2683 schema_version: "1.0.0".to_string(),
2684 request_id: "runtime-request".to_string(),
2685 intent: RuntimeIntent {
2686 capability_id: Some("content.comments.publish-comment".to_string()),
2687 capability_version: Some("1.0.0".to_string()),
2688 version_range: None,
2689 intent_key: None,
2690 },
2691 input: json!({"comment_text": "hello"}),
2692 lookup: RuntimeLookup {
2693 scope: RuntimeLookupScope::PublicOnly,
2694 allow_ambiguity: false,
2695 },
2696 context: RuntimeContext {
2697 requested_target: crate::PlacementTarget::Local,
2698 correlation_id: None,
2699 caller: None,
2700 traceparent: None,
2701 tracestate: None,
2702 metadata: None,
2703 identity: None,
2704 },
2705 governing_spec: "006-runtime-request-execution".to_string(),
2706 }
2707 }
2708
2709 struct WorkflowExecutor;
2710
2711 impl LocalExecutor for WorkflowExecutor {
2712 fn execute(
2713 &self,
2714 capability: &ResolvedCapability,
2715 _input: &Value,
2716 ) -> Result<Value, LocalExecutionFailure> {
2717 let output = match capability.record.id.as_str() {
2718 "content.comments.create-comment-draft" => json!({
2719 "draft_id": "draft-1",
2720 "emitted_events": [
2721 {"event_id": "content.comments.draft-created", "version": "1.0.0"}
2722 ]
2723 }),
2724 "content.comments.validate-comment" => json!({
2725 "draft_id": "draft-1",
2726 "emitted_events": [
2727 {"event_id": "content.comments.validated", "version": "1.0.0"}
2728 ]
2729 }),
2730 "content.comments.persist-comment" => json!({
2731 "comment_id": "comment-1"
2732 }),
2733 _ => json!({}),
2734 };
2735 Ok(output)
2736 }
2737 }
2738
2739 struct FailingWorkflowExecutor;
2740
2741 impl LocalExecutor for FailingWorkflowExecutor {
2742 fn execute(
2743 &self,
2744 _capability: &ResolvedCapability,
2745 _input: &Value,
2746 ) -> Result<Value, LocalExecutionFailure> {
2747 Err(LocalExecutionFailure {
2748 code: LocalExecutionFailureCode::ExecutionFailed,
2749 message: "boom".to_string(),
2750 })
2751 }
2752 }
2753
2754 struct MissingEventWorkflowExecutor;
2755
2756 struct BadOutputWorkflowExecutor;
2757
2758 impl LocalExecutor for MissingEventWorkflowExecutor {
2759 fn execute(
2760 &self,
2761 capability: &ResolvedCapability,
2762 _input: &Value,
2763 ) -> Result<Value, LocalExecutionFailure> {
2764 let output = match capability.record.id.as_str() {
2765 "content.comments.create-comment-draft" => json!({
2766 "draft_id": "draft-1",
2767 "emitted_events": [
2768 {"event_id": "content.comments.draft-created", "version": "1.0.0"}
2769 ]
2770 }),
2771 "content.comments.validate-comment" => json!({
2772 "draft_id": "draft-1"
2773 }),
2774 "content.comments.persist-comment" => json!({
2775 "comment_id": "comment-1"
2776 }),
2777 _ => json!({}),
2778 };
2779 Ok(output)
2780 }
2781 }
2782
2783 impl LocalExecutor for BadOutputWorkflowExecutor {
2784 fn execute(
2785 &self,
2786 capability: &ResolvedCapability,
2787 _input: &Value,
2788 ) -> Result<Value, LocalExecutionFailure> {
2789 let output = match capability.record.id.as_str() {
2790 "content.comments.create-comment-draft" => json!({
2791 "emitted_events": [
2792 {"event_id": "content.comments.draft-created", "version": "1.0.0"}
2793 ]
2794 }),
2795 _ => json!({}),
2796 };
2797 Ok(output)
2798 }
2799 }
2800
2801 fn register_capability_ok(registry: &mut CapabilityRegistry, request: CapabilityRegistration) {
2802 match registry.register(request) {
2803 Ok(_) => {}
2804 Err(error) => unreachable!("{error:?}"),
2805 }
2806 }
2807
2808 fn register_workflow_ok(
2809 registry: &mut WorkflowRegistry,
2810 capabilities: &CapabilityRegistry,
2811 request: WorkflowRegistration,
2812 ) {
2813 match registry.register(capabilities, request) {
2814 Ok(_) => {}
2815 Err(error) => unreachable!("{error:?}"),
2816 }
2817 }
2818
2819 #[test]
2820 fn helper_guards_cover_unreachable_branches() {
2821 let capability_panic = std::panic::catch_unwind(|| {
2822 register_capability_ok(
2823 &mut CapabilityRegistry::new(),
2824 CapabilityRegistration {
2825 scope: RegistryScope::Public,
2826 contract: capability_contract("bad", Vec::new(), json!({}), json!({})),
2827 contract_path: String::new(),
2828 artifact: workflow_artifact_record("bad", "1.0.0", "artifact"),
2829 registered_at: String::new(),
2830 tags: Vec::new(),
2831 composability: ComposabilityMetadata {
2832 kind: CompositionKind::Atomic,
2833 patterns: Vec::new(),
2834 provides: Vec::new(),
2835 requires: Vec::new(),
2836 },
2837 governing_spec: "005-capability-registry".to_string(),
2838 validator_version: "validator".to_string(),
2839 },
2840 );
2841 });
2842 assert!(capability_panic.is_err());
2843
2844 let workflow_panic = std::panic::catch_unwind(|| {
2845 register_workflow_ok(
2846 &mut WorkflowRegistry::new(),
2847 &CapabilityRegistry::new(),
2848 WorkflowRegistration {
2849 scope: RegistryScope::Public,
2850 definition: workflow_definition_fixture(
2851 None,
2852 Some(WorkflowEdge {
2853 edge_id: "direct".to_string(),
2854 from: "create_draft".to_string(),
2855 to: "validate_comment".to_string(),
2856 trigger: WorkflowEdgeTrigger::Direct,
2857 event: None,
2858 predicate: None,
2859 }),
2860 ),
2861 workflow_path: String::new(),
2862 registered_at: String::new(),
2863 validator_version: "validator".to_string(),
2864 },
2865 );
2866 });
2867 assert!(workflow_panic.is_err());
2868 }
2869
2870 fn resolved_workflow(definition: WorkflowDefinition) -> ResolvedWorkflow {
2871 ResolvedWorkflow {
2872 record: WorkflowRegistryRecord {
2873 scope: RegistryScope::Public,
2874 id: definition.id.clone(),
2875 version: definition.version.clone(),
2876 lifecycle: definition.lifecycle.clone(),
2877 owner: definition.owner.clone(),
2878 workflow_path: "workflows/manual.json".to_string(),
2879 workflow_digest: "digest".to_string(),
2880 registered_at: "2026-03-27T00:00:00Z".to_string(),
2881 governing_spec: "007-workflow-registry-traversal".to_string(),
2882 validator_version: "validator".to_string(),
2883 evidence: traverse_registry::WorkflowRegistrationEvidence {
2884 evidence_id: "evidence".to_string(),
2885 workflow_id: definition.id.clone(),
2886 workflow_version: definition.version.clone(),
2887 scope: RegistryScope::Public,
2888 governing_spec: "007-workflow-registry-traversal".to_string(),
2889 validator_version: "validator".to_string(),
2890 produced_at: "2026-03-27T00:00:00Z".to_string(),
2891 result: traverse_registry::WorkflowRegistrationResult::Passed,
2892 },
2893 },
2894 index_entry: traverse_registry::WorkflowDiscoveryIndexEntry {
2895 scope: RegistryScope::Public,
2896 id: definition.id.clone(),
2897 version: definition.version.clone(),
2898 lifecycle: definition.lifecycle.clone(),
2899 owner: definition.owner.clone(),
2900 summary: definition.summary.clone(),
2901 tags: definition.tags.clone(),
2902 participating_capabilities: definition
2903 .nodes
2904 .iter()
2905 .map(|node| node.capability_id.clone())
2906 .collect(),
2907 events_used: Vec::new(),
2908 start_node: definition.start_node.clone(),
2909 terminal_nodes: definition.terminal_nodes.clone(),
2910 registered_at: "2026-03-27T00:00:00Z".to_string(),
2911 },
2912 definition,
2913 }
2914 }
2915}