Skip to main content

traverse_runtime/executor/
mod.rs

1//! Capability executor abstraction for Traverse.
2//!
3//! Governed by spec `025-wasm-executor-adapter`.
4//!
5//! Two concrete implementations:
6//! - [`NativeExecutor`] — executes capabilities implemented as native Rust closures.
7//! - `WasmExecutor` — executes capabilities compiled to `wasm32-wasi` binaries
8//!   via Wasmtime when the `wasmtime-executor` feature is enabled.
9//! - `ThreadPoolExecutor` — dispatches native capability execution onto a bounded
10//!   worker pool when the `native-executors` feature is enabled.
11pub mod native;
12#[cfg(feature = "native-executors")]
13pub mod thread_pool;
14#[cfg(feature = "wasmtime-executor")]
15pub mod wasm;
16
17pub use native::NativeExecutor;
18#[cfg(feature = "native-executors")]
19pub use thread_pool::{ConfigError, ThreadPoolExecutor, ThreadPoolExecutorConfig};
20#[cfg(feature = "wasmtime-executor")]
21pub use wasm::{
22    ActivatedConnector, ConnectorInvokeRequest, ConnectorInvokeResponse, HostAbiImport,
23    HostAbiValidation, MediatedConnector, MediatedConnectorContext, SUPPORTED_HOST_ABI_VERSION,
24    WasmBinaryCacheStats, WasmExecutionLimits, WasmExecutor, WasmModuleCacheConfig,
25    WasmModuleCacheStats, supported_host_abi_versions, verify_wasm_host_abi_bytes,
26};
27
28use crate::events::types::TraverseEvent;
29use serde_json::Value;
30use traverse_contracts::{EventReference, ServiceType};
31
32/// Immutable, non-secret outcome evidence for one mediated connector call.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ConnectorInvocationEvidence {
35    pub connector_id: String,
36    pub resolved_version: Option<String>,
37    pub result_class: String,
38    pub failure_class: Option<String>,
39}
40
41/// The artifact type recorded in a capability registration, used to route to the correct executor.
42#[derive(Debug, Clone, PartialEq, Eq, Hash)]
43pub enum ArtifactType {
44    /// Native Rust implementation — executed via [`NativeExecutor`].
45    Native,
46    /// WASM binary — executed via [`WasmExecutor`].
47    Wasm,
48}
49
50/// A resolved capability ready for execution.
51#[derive(Debug, Clone)]
52pub struct ExecutorCapability {
53    /// Unique capability identifier.
54    pub capability_id: String,
55    /// How the binary is packaged.
56    pub artifact_type: ArtifactType,
57    /// File-system path to the `.wasm` binary (only relevant for `ArtifactType::Wasm`).
58    pub wasm_binary_path: Option<String>,
59    /// Expected SHA-256 hex digest of the WASM binary (only relevant for `ArtifactType::Wasm`).
60    pub wasm_checksum: Option<String>,
61    /// Traverse Host ABI version declared by the module manifest.
62    pub host_abi_version: Option<String>,
63    /// Event types this capability's contract declares under `emits`, used to
64    /// validate `traverse_host::emit_event` calls synchronously at call time
65    /// (spec 098-capability-event-host-abi FR-002).
66    pub emits: Vec<EventReference>,
67    /// The capability contract's `service_type`; only `Subscribable` may call
68    /// `traverse_host::emit_event` (spec 098-capability-event-host-abi FR-003).
69    pub service_type: ServiceType,
70}
71
72/// Output of a [`CapabilityExecutor::execute`] call.
73#[derive(Debug, Clone, PartialEq)]
74pub struct ExecutorOutput {
75    /// The capability's JSON output value.
76    pub value: Value,
77    /// Events accepted via `traverse_host::emit_event` during this execution,
78    /// already validated against the capability's contract (spec
79    /// 098-capability-event-host-abi FR-002/FR-003). Always empty for
80    /// non-WASM executors, since the host ABI is WASM-only.
81    pub emitted_events: Vec<TraverseEvent>,
82    /// Immutable, non-secret connector authorization and outcome evidence.
83    pub connector_invocation_evidence: Vec<ConnectorInvocationEvidence>,
84}
85
86/// Error returned by a [`CapabilityExecutor`].
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum ExecutorError {
89    /// The WASM binary could not be loaded from the given path.
90    BinaryLoadFailed(String),
91    /// The SHA-256 checksum of the loaded binary did not match the expected value.
92    ChecksumMismatch { expected: String, actual: String },
93    /// The Wasmtime engine or linker could not be configured.
94    RuntimeSetupFailed(String),
95    /// The WASM artifact is malformed and cannot be parsed as a module.
96    MalformedWasmArtifact { error_code: String, detail: String },
97    /// The WASM artifact declares an unsupported Traverse Host ABI version.
98    UnsupportedAbiVersion {
99        error_code: String,
100        requested: String,
101        supported: String,
102    },
103    /// The WASM artifact imports a host function outside the declared ABI whitelist.
104    UnauthorizedHostImport {
105        error_code: String,
106        abi_version: String,
107        module: String,
108        name: String,
109    },
110    /// The WASM module trapped or returned a non-zero exit code.
111    ExecutionFailed(String),
112    /// WASM execution exhausted its configured CPU budget.
113    Timeout(String),
114    /// WASM execution exceeded its configured memory, table, or instance budget.
115    ResourceExhausted(String),
116    /// The executor produced output that could not be parsed as JSON.
117    OutputDeserializationFailed(String),
118    /// The executor type does not support the requested capability.
119    UnsupportedArtifactType,
120}
121
122impl std::fmt::Display for ExecutorError {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            Self::BinaryLoadFailed(msg) => write!(f, "binary load failed: {msg}"),
126            Self::ChecksumMismatch { expected, actual } => {
127                write!(f, "checksum mismatch: expected {expected}, got {actual}")
128            }
129            Self::RuntimeSetupFailed(msg) => write!(f, "runtime setup failed: {msg}"),
130            Self::MalformedWasmArtifact { error_code, detail } => {
131                write!(f, "{error_code}: {detail}")
132            }
133            Self::UnsupportedAbiVersion {
134                error_code,
135                requested,
136                supported,
137            } => write!(
138                f,
139                "{error_code}: requested Traverse Host ABI {requested}, supported {supported}"
140            ),
141            Self::UnauthorizedHostImport {
142                error_code,
143                abi_version,
144                module,
145                name,
146            } => write!(
147                f,
148                "{error_code}: ABI {abi_version} does not allow import {module}::{name}"
149            ),
150            Self::ExecutionFailed(msg) => write!(f, "execution failed: {msg}"),
151            Self::Timeout(msg) => write!(f, "execution timed out: {msg}"),
152            Self::ResourceExhausted(msg) => write!(f, "resource exhausted: {msg}"),
153            Self::OutputDeserializationFailed(msg) => {
154                write!(f, "output deserialization failed: {msg}")
155            }
156            Self::UnsupportedArtifactType => {
157                write!(f, "unsupported artifact type for this executor")
158            }
159        }
160    }
161}
162
163impl std::error::Error for ExecutorError {}
164
165/// Trait implemented by all capability executors.
166///
167/// Executors are stateless; all context is passed per call.
168pub trait CapabilityExecutor: Send + Sync {
169    /// Execute `capability` with `input`, returning the output or an error.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`ExecutorError`] when execution cannot be completed.
174    fn execute(
175        &self,
176        capability: &ExecutorCapability,
177        input: &Value,
178    ) -> Result<ExecutorOutput, ExecutorError>;
179}