1use crate::executor::ExecutorError;
2#[cfg(feature = "wasmtime-executor")]
3use crate::executor::{ArtifactType, CapabilityExecutor, ExecutorCapability, WasmExecutor};
4use crate::{
5 LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutionOutput, LocalExecutor,
6};
7use serde_json::Value;
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use traverse_registry::ResolvedCapability;
11
12type NativeHandler =
13 dyn Fn(&Value) -> Result<LocalExecutionOutput, LocalExecutionFailure> + Send + Sync;
14
15#[derive(Clone)]
21pub struct ArtifactRouter {
22 #[cfg(feature = "wasmtime-executor")]
23 wasm: Arc<WasmExecutor>,
24 native_handlers: BTreeMap<String, Arc<NativeHandler>>,
25}
26
27impl ArtifactRouter {
28 pub fn new() -> Result<Self, LocalExecutionFailure> {
34 #[cfg(feature = "wasmtime-executor")]
35 {
36 WasmExecutor::new()
37 .map(|wasm| Self {
38 wasm: Arc::new(wasm),
39 native_handlers: BTreeMap::new(),
40 })
41 .map_err(|error| map_executor_error(&error))
42 }
43 #[cfg(not(feature = "wasmtime-executor"))]
44 {
45 Ok(Self {
46 native_handlers: BTreeMap::new(),
47 })
48 }
49 }
50
51 pub fn register_native_handler<F>(&mut self, capability_id: impl Into<String>, handler: F)
53 where
54 F: Fn(&Value) -> Result<LocalExecutionOutput, LocalExecutionFailure>
55 + Send
56 + Sync
57 + 'static,
58 {
59 self.native_handlers
60 .insert(capability_id.into(), Arc::new(handler));
61 }
62}
63
64impl LocalExecutor for ArtifactRouter {
65 fn execute(
66 &self,
67 capability: &ResolvedCapability,
68 input: &Value,
69 ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
70 if let Some(binary) = &capability.artifact.binary {
71 #[cfg(feature = "wasmtime-executor")]
72 {
73 let executor_capability = ExecutorCapability {
74 capability_id: capability.contract.id.clone(),
75 artifact_type: ArtifactType::Wasm,
76 wasm_binary_path: Some(binary.location.clone()),
77 wasm_checksum: capability
78 .artifact
79 .digests
80 .binary_digest
81 .as_deref()
82 .and_then(|digest| digest.strip_prefix("sha256:"))
83 .map(str::to_string),
84 host_abi_version: None,
85 emits: capability.contract.emits.clone(),
86 service_type: capability.contract.service_type.clone(),
87 };
88 return self
97 .wasm
98 .execute(&executor_capability, input)
99 .map(|output| LocalExecutionOutput {
100 value: output.value,
101 emitted_events: output.emitted_events,
102 })
103 .map_err(|error| map_executor_error(&error));
104 }
105 #[cfg(not(feature = "wasmtime-executor"))]
106 {
107 let _ = binary;
108 return Err(constraint_failure(
109 "WASM execution is unavailable in this runtime build",
110 ));
111 }
112 }
113 self.native_handlers
114 .get(&capability.contract.id)
115 .ok_or_else(|| constraint_failure("native capability has no explicit host handler"))?(
116 input,
117 )
118 }
119}
120
121fn map_executor_error(error: &ExecutorError) -> LocalExecutionFailure {
122 let code = match error {
123 ExecutorError::Timeout(_) => LocalExecutionFailureCode::Timeout,
124 ExecutorError::ResourceExhausted(_) => LocalExecutionFailureCode::ResourceExhausted,
125 ExecutorError::ChecksumMismatch { .. }
126 | ExecutorError::MalformedWasmArtifact { .. }
127 | ExecutorError::UnsupportedAbiVersion { .. }
128 | ExecutorError::UnauthorizedHostImport { .. }
129 | ExecutorError::BinaryLoadFailed(_)
130 | ExecutorError::RuntimeSetupFailed(_) => LocalExecutionFailureCode::ConstraintViolated,
131 ExecutorError::OutputDeserializationFailed(_) => LocalExecutionFailureCode::InvalidInput,
132 ExecutorError::ExecutionFailed(_) | ExecutorError::UnsupportedArtifactType => {
133 LocalExecutionFailureCode::ExecutionFailed
134 }
135 };
136 LocalExecutionFailure {
137 code,
138 message: "registered artifact execution failed".to_string(),
139 }
140}
141
142fn constraint_failure(message: &str) -> LocalExecutionFailure {
143 LocalExecutionFailure {
144 code: LocalExecutionFailureCode::ConstraintViolated,
145 message: message.to_string(),
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 #![allow(clippy::expect_used)]
152
153 use super::*;
154 use traverse_registry::{
155 ArtifactDigests, BinaryFormat, BinaryReference, CapabilityArtifactRecord,
156 CapabilityRegistration, CapabilityRegistry, ComposabilityMetadata, CompositionKind,
157 CompositionPattern, ImplementationKind, LookupScope, RegistryProvenance, RegistryScope,
158 SourceKind, SourceReference,
159 };
160
161 fn resolved_capability(binary: Option<BinaryReference>) -> ResolvedCapability {
162 let contract = serde_json::from_str(include_str!(
163 "../../../contracts/examples/hello-world/capabilities/say-hello/contract.json"
164 ))
165 .expect("checked-in capability contract should parse");
166 let mut registry = CapabilityRegistry::new();
167 registry
168 .register(CapabilityRegistration {
169 scope: RegistryScope::Public,
170 contract,
171 contract_path:
172 "contracts/examples/hello-world/capabilities/say-hello/contract.json"
173 .to_string(),
174 artifact: CapabilityArtifactRecord {
175 artifact_ref: "artifact:hello.world.say-hello:1.0.0".to_string(),
176 implementation_kind: ImplementationKind::Executable,
177 source: SourceReference {
178 kind: SourceKind::Local,
179 location: "examples".to_string(),
180 },
181 binary: Some(binary.unwrap_or(BinaryReference {
182 format: BinaryFormat::Wasm,
183 location: "registered-test-module.wasm".to_string(),
184 signature: None,
185 })),
186 workflow_ref: None,
187 digests: ArtifactDigests {
188 source_digest: "source-digest".to_string(),
189 binary_digest: Some("sha256:checksum".to_string()),
190 },
191 provenance: RegistryProvenance {
192 source: "test".to_string(),
193 author: "test".to_string(),
194 created_at: "2026-07-13T00:00:00Z".to_string(),
195 },
196 },
197 registered_at: "2026-07-13T00:00:00Z".to_string(),
198 tags: Vec::new(),
199 composability: ComposabilityMetadata {
200 kind: CompositionKind::Atomic,
201 patterns: vec![CompositionPattern::Sequential],
202 provides: Vec::new(),
203 requires: Vec::new(),
204 },
205 governing_spec: "064-production-artifact-execution".to_string(),
206 validator_version: "test".to_string(),
207 })
208 .expect("test capability should register");
209 registry
210 .find_exact(LookupScope::PublicOnly, "hello.world.say-hello", "1.0.0")
211 .expect("registered capability should resolve")
212 }
213
214 #[test]
215 fn native_execution_requires_an_explicit_handler() {
216 let mut capability = resolved_capability(None);
217 capability.artifact.binary = None;
218 let mut router = ArtifactRouter::new().expect("router should initialize");
219 let failure = router
220 .execute(&capability, &serde_json::json!({}))
221 .expect_err("unregistered native handler should fail closed");
222 assert_eq!(failure.code, LocalExecutionFailureCode::ConstraintViolated);
223
224 router.register_native_handler("hello.world.say-hello", |_| {
225 Ok(LocalExecutionOutput {
226 value: serde_json::json!({"ok": true}),
227 emitted_events: Vec::new(),
228 })
229 });
230 assert_eq!(
231 router.execute(&capability, &serde_json::json!({})),
232 Ok(LocalExecutionOutput {
233 value: serde_json::json!({"ok": true}),
234 emitted_events: Vec::new(),
235 })
236 );
237 }
238
239 #[cfg(feature = "wasmtime-executor")]
240 #[test]
241 fn wasm_artifacts_are_executed_only_from_registered_binary_metadata() {
242 let capability = resolved_capability(Some(BinaryReference {
243 format: BinaryFormat::Wasm,
244 location: "missing-test-module.wasm".to_string(),
245 signature: None,
246 }));
247 let failure = ArtifactRouter::new()
248 .expect("router should initialize")
249 .execute(&capability, &serde_json::json!({}))
250 .expect_err("missing registered binary should fail");
251 assert_eq!(failure.code, LocalExecutionFailureCode::ConstraintViolated);
252 assert_eq!(failure.message, "registered artifact execution failed");
253 }
254
255 #[cfg(feature = "wasmtime-executor")]
256 #[test]
257 fn wasm_execution_success_returns_real_value_and_emitted_events() {
258 use sha2::{Digest, Sha256};
259 use std::fmt::Write as _;
260
261 let wat_src = r#"
265 (module
266 (import "wasi_snapshot_preview1" "fd_write"
267 (func $fd_write (param i32 i32 i32 i32) (result i32)))
268 (memory (export "memory") 1)
269 (data (i32.const 8) "{}")
270 (func $_start (export "_start")
271 (i32.store (i32.const 0) (i32.const 8))
272 (i32.store (i32.const 4) (i32.const 2))
273 (drop (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1) (i32.const 4)))
274 )
275 )
276 "#;
277 let wasm_bytes = wat::parse_str(wat_src).expect("WAT source should parse");
278
279 let mut hasher = Sha256::new();
280 hasher.update(&wasm_bytes);
281 let checksum = hasher
282 .finalize()
283 .iter()
284 .fold(String::new(), |mut acc, byte| {
285 let _ = write!(acc, "{byte:02x}");
286 acc
287 });
288
289 let tmp = format!(
290 "/tmp/traverse-artifact-router-test-{}.wasm",
291 std::time::SystemTime::now()
292 .duration_since(std::time::UNIX_EPOCH)
293 .map_or(0, |d| d.as_nanos())
294 );
295 std::fs::write(&tmp, &wasm_bytes).expect("temp wasm module should write");
296
297 let mut capability = resolved_capability(Some(BinaryReference {
298 format: BinaryFormat::Wasm,
299 location: tmp.clone(),
300 signature: None,
301 }));
302 capability.artifact.digests.binary_digest = Some(format!("sha256:{checksum}"));
303
304 let result = ArtifactRouter::new()
305 .expect("router should initialize")
306 .execute(&capability, &serde_json::json!({}));
307 std::fs::remove_file(&tmp).ok();
308
309 assert_eq!(
310 result,
311 Ok(LocalExecutionOutput {
312 value: serde_json::json!({}),
313 emitted_events: Vec::new(),
314 })
315 );
316 }
317
318 #[test]
319 fn executor_errors_map_to_stable_local_failure_codes() {
320 let errors = [
321 (
322 ExecutorError::Timeout("x".to_string()),
323 LocalExecutionFailureCode::Timeout,
324 ),
325 (
326 ExecutorError::ResourceExhausted("x".to_string()),
327 LocalExecutionFailureCode::ResourceExhausted,
328 ),
329 (
330 ExecutorError::ChecksumMismatch {
331 expected: "a".to_string(),
332 actual: "b".to_string(),
333 },
334 LocalExecutionFailureCode::ConstraintViolated,
335 ),
336 (
337 ExecutorError::MalformedWasmArtifact {
338 error_code: "x".to_string(),
339 detail: "x".to_string(),
340 },
341 LocalExecutionFailureCode::ConstraintViolated,
342 ),
343 (
344 ExecutorError::UnsupportedAbiVersion {
345 error_code: "x".to_string(),
346 requested: "x".to_string(),
347 supported: "x".to_string(),
348 },
349 LocalExecutionFailureCode::ConstraintViolated,
350 ),
351 (
352 ExecutorError::UnauthorizedHostImport {
353 error_code: "x".to_string(),
354 abi_version: "x".to_string(),
355 module: "x".to_string(),
356 name: "x".to_string(),
357 },
358 LocalExecutionFailureCode::ConstraintViolated,
359 ),
360 (
361 ExecutorError::BinaryLoadFailed("x".to_string()),
362 LocalExecutionFailureCode::ConstraintViolated,
363 ),
364 (
365 ExecutorError::RuntimeSetupFailed("x".to_string()),
366 LocalExecutionFailureCode::ConstraintViolated,
367 ),
368 (
369 ExecutorError::OutputDeserializationFailed("x".to_string()),
370 LocalExecutionFailureCode::InvalidInput,
371 ),
372 (
373 ExecutorError::ExecutionFailed("x".to_string()),
374 LocalExecutionFailureCode::ExecutionFailed,
375 ),
376 (
377 ExecutorError::UnsupportedArtifactType,
378 LocalExecutionFailureCode::ExecutionFailed,
379 ),
380 ];
381 for (error, expected) in errors {
382 let failure = map_executor_error(&error);
383 assert_eq!(failure.code, expected);
384 assert_eq!(failure.message, "registered artifact execution failed");
385 }
386 }
387}