Skip to main content

traverse_runtime/
artifact_router.rs

1use crate::executor::ExecutorError;
2#[cfg(feature = "wasmtime-executor")]
3use crate::executor::{ArtifactType, CapabilityExecutor, ExecutorCapability, WasmExecutor};
4use crate::{LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutor};
5use serde_json::Value;
6use std::collections::BTreeMap;
7use std::sync::Arc;
8use traverse_registry::ResolvedCapability;
9
10type NativeHandler = dyn Fn(&Value) -> Result<Value, LocalExecutionFailure> + Send + Sync;
11
12/// Production local-execution boundary for registered artifacts.
13///
14/// WASM executes only from the resolved registered artifact. Native execution
15/// is limited to explicitly registered host handlers and never loads a binary
16/// or command from artifact metadata.
17#[derive(Clone)]
18pub struct ArtifactRouter {
19    #[cfg(feature = "wasmtime-executor")]
20    wasm: Arc<WasmExecutor>,
21    native_handlers: BTreeMap<String, Arc<NativeHandler>>,
22}
23
24impl ArtifactRouter {
25    /// Creates a router using the default bounded Wasmtime configuration.
26    ///
27    /// # Errors
28    ///
29    /// Returns an execution failure when the Wasmtime runtime cannot initialize.
30    pub fn new() -> Result<Self, LocalExecutionFailure> {
31        #[cfg(feature = "wasmtime-executor")]
32        {
33            WasmExecutor::new()
34                .map(|wasm| Self {
35                    wasm: Arc::new(wasm),
36                    native_handlers: BTreeMap::new(),
37                })
38                .map_err(|error| map_executor_error(&error))
39        }
40        #[cfg(not(feature = "wasmtime-executor"))]
41        {
42            Ok(Self {
43                native_handlers: BTreeMap::new(),
44            })
45        }
46    }
47
48    /// Registers one host-provided native handler for an exact capability id.
49    pub fn register_native_handler<F>(&mut self, capability_id: impl Into<String>, handler: F)
50    where
51        F: Fn(&Value) -> Result<Value, LocalExecutionFailure> + Send + Sync + 'static,
52    {
53        self.native_handlers
54            .insert(capability_id.into(), Arc::new(handler));
55    }
56}
57
58impl LocalExecutor for ArtifactRouter {
59    fn execute(
60        &self,
61        capability: &ResolvedCapability,
62        input: &Value,
63    ) -> Result<Value, LocalExecutionFailure> {
64        if let Some(binary) = &capability.artifact.binary {
65            #[cfg(feature = "wasmtime-executor")]
66            {
67                let executor_capability = ExecutorCapability {
68                    capability_id: capability.contract.id.clone(),
69                    artifact_type: ArtifactType::Wasm,
70                    wasm_binary_path: Some(binary.location.clone()),
71                    wasm_checksum: capability
72                        .artifact
73                        .digests
74                        .binary_digest
75                        .as_deref()
76                        .and_then(|digest| digest.strip_prefix("sha256:"))
77                        .map(str::to_string),
78                    host_abi_version: None,
79                };
80                return self
81                    .wasm
82                    .execute(&executor_capability, input)
83                    .map_err(|error| map_executor_error(&error));
84            }
85            #[cfg(not(feature = "wasmtime-executor"))]
86            {
87                let _ = binary;
88                return Err(constraint_failure(
89                    "WASM execution is unavailable in this runtime build",
90                ));
91            }
92        }
93        self.native_handlers
94            .get(&capability.contract.id)
95            .ok_or_else(|| constraint_failure("native capability has no explicit host handler"))?(
96            input,
97        )
98    }
99}
100
101fn map_executor_error(error: &ExecutorError) -> LocalExecutionFailure {
102    let code = match error {
103        ExecutorError::Timeout(_) => LocalExecutionFailureCode::Timeout,
104        ExecutorError::ResourceExhausted(_) => LocalExecutionFailureCode::ResourceExhausted,
105        ExecutorError::ChecksumMismatch { .. }
106        | ExecutorError::MalformedWasmArtifact { .. }
107        | ExecutorError::UnsupportedAbiVersion { .. }
108        | ExecutorError::UnauthorizedHostImport { .. }
109        | ExecutorError::BinaryLoadFailed(_)
110        | ExecutorError::RuntimeSetupFailed(_) => LocalExecutionFailureCode::ConstraintViolated,
111        ExecutorError::OutputDeserializationFailed(_) => LocalExecutionFailureCode::InvalidInput,
112        ExecutorError::ExecutionFailed(_) | ExecutorError::UnsupportedArtifactType => {
113            LocalExecutionFailureCode::ExecutionFailed
114        }
115    };
116    LocalExecutionFailure {
117        code,
118        message: "registered artifact execution failed".to_string(),
119    }
120}
121
122fn constraint_failure(message: &str) -> LocalExecutionFailure {
123    LocalExecutionFailure {
124        code: LocalExecutionFailureCode::ConstraintViolated,
125        message: message.to_string(),
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    #![allow(clippy::expect_used)]
132
133    use super::*;
134    use traverse_registry::{
135        ArtifactDigests, BinaryFormat, BinaryReference, CapabilityArtifactRecord,
136        CapabilityRegistration, CapabilityRegistry, ComposabilityMetadata, CompositionKind,
137        CompositionPattern, ImplementationKind, LookupScope, RegistryProvenance, RegistryScope,
138        SourceKind, SourceReference,
139    };
140
141    fn resolved_capability(binary: Option<BinaryReference>) -> ResolvedCapability {
142        let contract = serde_json::from_str(include_str!(
143            "../../../contracts/examples/hello-world/capabilities/say-hello/contract.json"
144        ))
145        .expect("checked-in capability contract should parse");
146        let mut registry = CapabilityRegistry::new();
147        registry
148            .register(CapabilityRegistration {
149                scope: RegistryScope::Public,
150                contract,
151                contract_path:
152                    "contracts/examples/hello-world/capabilities/say-hello/contract.json"
153                        .to_string(),
154                artifact: CapabilityArtifactRecord {
155                    artifact_ref: "artifact:hello.world.say-hello:1.0.0".to_string(),
156                    implementation_kind: ImplementationKind::Executable,
157                    source: SourceReference {
158                        kind: SourceKind::Local,
159                        location: "examples".to_string(),
160                    },
161                    binary: Some(binary.unwrap_or(BinaryReference {
162                        format: BinaryFormat::Wasm,
163                        location: "registered-test-module.wasm".to_string(),
164                        signature: None,
165                    })),
166                    workflow_ref: None,
167                    digests: ArtifactDigests {
168                        source_digest: "source-digest".to_string(),
169                        binary_digest: Some("sha256:checksum".to_string()),
170                    },
171                    provenance: RegistryProvenance {
172                        source: "test".to_string(),
173                        author: "test".to_string(),
174                        created_at: "2026-07-13T00:00:00Z".to_string(),
175                    },
176                },
177                registered_at: "2026-07-13T00:00:00Z".to_string(),
178                tags: Vec::new(),
179                composability: ComposabilityMetadata {
180                    kind: CompositionKind::Atomic,
181                    patterns: vec![CompositionPattern::Sequential],
182                    provides: Vec::new(),
183                    requires: Vec::new(),
184                },
185                governing_spec: "064-production-artifact-execution".to_string(),
186                validator_version: "test".to_string(),
187            })
188            .expect("test capability should register");
189        registry
190            .find_exact(LookupScope::PublicOnly, "hello.world.say-hello", "1.0.0")
191            .expect("registered capability should resolve")
192    }
193
194    #[test]
195    fn native_execution_requires_an_explicit_handler() {
196        let mut capability = resolved_capability(None);
197        capability.artifact.binary = None;
198        let mut router = ArtifactRouter::new().expect("router should initialize");
199        let failure = router
200            .execute(&capability, &serde_json::json!({}))
201            .expect_err("unregistered native handler should fail closed");
202        assert_eq!(failure.code, LocalExecutionFailureCode::ConstraintViolated);
203
204        router.register_native_handler("hello.world.say-hello", |_| {
205            Ok(serde_json::json!({"ok": true}))
206        });
207        assert_eq!(
208            router.execute(&capability, &serde_json::json!({})),
209            Ok(serde_json::json!({"ok": true}))
210        );
211    }
212
213    #[cfg(feature = "wasmtime-executor")]
214    #[test]
215    fn wasm_artifacts_are_executed_only_from_registered_binary_metadata() {
216        let capability = resolved_capability(Some(BinaryReference {
217            format: BinaryFormat::Wasm,
218            location: "missing-test-module.wasm".to_string(),
219            signature: None,
220        }));
221        let failure = ArtifactRouter::new()
222            .expect("router should initialize")
223            .execute(&capability, &serde_json::json!({}))
224            .expect_err("missing registered binary should fail");
225        assert_eq!(failure.code, LocalExecutionFailureCode::ConstraintViolated);
226        assert_eq!(failure.message, "registered artifact execution failed");
227    }
228
229    #[test]
230    fn executor_errors_map_to_stable_local_failure_codes() {
231        let errors = [
232            (
233                ExecutorError::Timeout("x".to_string()),
234                LocalExecutionFailureCode::Timeout,
235            ),
236            (
237                ExecutorError::ResourceExhausted("x".to_string()),
238                LocalExecutionFailureCode::ResourceExhausted,
239            ),
240            (
241                ExecutorError::ChecksumMismatch {
242                    expected: "a".to_string(),
243                    actual: "b".to_string(),
244                },
245                LocalExecutionFailureCode::ConstraintViolated,
246            ),
247            (
248                ExecutorError::MalformedWasmArtifact {
249                    error_code: "x".to_string(),
250                    detail: "x".to_string(),
251                },
252                LocalExecutionFailureCode::ConstraintViolated,
253            ),
254            (
255                ExecutorError::UnsupportedAbiVersion {
256                    error_code: "x".to_string(),
257                    requested: "x".to_string(),
258                    supported: "x".to_string(),
259                },
260                LocalExecutionFailureCode::ConstraintViolated,
261            ),
262            (
263                ExecutorError::UnauthorizedHostImport {
264                    error_code: "x".to_string(),
265                    abi_version: "x".to_string(),
266                    module: "x".to_string(),
267                    name: "x".to_string(),
268                },
269                LocalExecutionFailureCode::ConstraintViolated,
270            ),
271            (
272                ExecutorError::BinaryLoadFailed("x".to_string()),
273                LocalExecutionFailureCode::ConstraintViolated,
274            ),
275            (
276                ExecutorError::RuntimeSetupFailed("x".to_string()),
277                LocalExecutionFailureCode::ConstraintViolated,
278            ),
279            (
280                ExecutorError::OutputDeserializationFailed("x".to_string()),
281                LocalExecutionFailureCode::InvalidInput,
282            ),
283            (
284                ExecutorError::ExecutionFailed("x".to_string()),
285                LocalExecutionFailureCode::ExecutionFailed,
286            ),
287            (
288                ExecutorError::UnsupportedArtifactType,
289                LocalExecutionFailureCode::ExecutionFailed,
290            ),
291        ];
292        for (error, expected) in errors {
293            let failure = map_executor_error(&error);
294            assert_eq!(failure.code, expected);
295            assert_eq!(failure.message, "registered artifact execution failed");
296        }
297    }
298}