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#[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 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 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}