Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions crates/traverse-cli/src/http_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7289,7 +7289,9 @@ mod tests {
RegistryProvenance, RegistryScope, SourceKind, SourceReference, WorkflowEdge, WorkflowNode,
WorkflowNodeInput, WorkflowNodeOutput,
};
use traverse_runtime::{LocalExecutionFailure, LocalExecutionFailureCode};
use traverse_runtime::{
LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutionOutput,
};

#[test]
fn serve_error_display_keeps_bind_and_accept_context() {
Expand Down Expand Up @@ -7323,11 +7325,17 @@ mod tests {
&self,
_capability: &ResolvedCapability,
_input: &Value,
) -> Result<Value, LocalExecutionFailure> {
self.result.clone().map_err(|msg| LocalExecutionFailure {
code: LocalExecutionFailureCode::ExecutionFailed,
message: msg,
})
) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
self.result
.clone()
.map(|value| LocalExecutionOutput {
value,
emitted_events: Vec::new(),
})
.map_err(|msg| LocalExecutionFailure {
code: LocalExecutionFailureCode::ExecutionFailed,
message: msg,
})
}
}

Expand Down
16 changes: 10 additions & 6 deletions crates/traverse-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ use traverse_registry::{
};
use traverse_runtime::executor::{SUPPORTED_HOST_ABI_VERSION, verify_wasm_host_abi_bytes};
use traverse_runtime::{
ArtifactRouter, LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutor, Runtime,
RuntimeExecutionOutcome, RuntimeRequest, RuntimeResultStatus, RuntimeTrace,
parse_runtime_request,
ArtifactRouter, LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutionOutput,
LocalExecutor, Runtime, RuntimeExecutionOutcome, RuntimeRequest, RuntimeResultStatus,
RuntimeTrace, parse_runtime_request,
};

#[derive(Debug)]
Expand Down Expand Up @@ -4838,8 +4838,8 @@ impl LocalExecutor for ExpeditionExampleExecutor {
&self,
capability: &traverse_registry::ResolvedCapability,
input: &Value,
) -> Result<Value, LocalExecutionFailure> {
match capability.contract.id.as_str() {
) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
let value = match capability.contract.id.as_str() {
"expedition.planning.capture-expedition-objective" => {
execute_capture_expedition_objective(input)
}
Expand All @@ -4860,7 +4860,11 @@ impl LocalExecutor for ExpeditionExampleExecutor {
other => Err(executor_failure(&format!(
"unsupported expedition example capability: {other}"
))),
}
}?;
Ok(LocalExecutionOutput {
value,
emitted_events: Vec::new(),
})
}
}

Expand Down
11 changes: 7 additions & 4 deletions crates/traverse-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,8 @@ mod tests {
WorkflowRegistration,
};
use traverse_runtime::{
LocalExecutionFailure, RuntimeContext, RuntimeIntent, RuntimeLookup, RuntimeLookupScope,
RuntimeResultStatus,
LocalExecutionFailure, LocalExecutionOutput, RuntimeContext, RuntimeIntent, RuntimeLookup,
RuntimeLookupScope, RuntimeResultStatus,
};

#[test]
Expand Down Expand Up @@ -1318,8 +1318,11 @@ mod tests {
&self,
_capability: &ResolvedCapability,
_input: &Value,
) -> Result<Value, LocalExecutionFailure> {
Ok(json!({"draft_id": "draft-001"}))
) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
Ok(LocalExecutionOutput {
value: json!({"draft_id": "draft-001"}),
emitted_events: Vec::new(),
})
}
}

Expand Down
11 changes: 8 additions & 3 deletions crates/traverse-mcp/src/stdio_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,8 +1073,9 @@ impl LocalExecutor for ExpeditionExampleExecutor {
&self,
capability: &traverse_registry::ResolvedCapability,
input: &Value,
) -> Result<Value, traverse_runtime::LocalExecutionFailure> {
match capability.contract.id.as_str() {
) -> Result<traverse_runtime::LocalExecutionOutput, traverse_runtime::LocalExecutionFailure>
{
let value = match capability.contract.id.as_str() {
"expedition.planning.capture-expedition-objective" => {
execute_capture_expedition_objective(input)
}
Expand All @@ -1091,7 +1092,11 @@ impl LocalExecutor for ExpeditionExampleExecutor {
other => Err(executor_failure(&format!(
"unsupported expedition capability for stdio execution: {other}"
))),
}
}?;
Ok(traverse_runtime::LocalExecutionOutput {
value,
emitted_events: Vec::new(),
})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::path::Path;
use traverse_registry::{
DiscoveryQuery, LookupScope, ResolvedCapability, WorkspaceAppStateErrorCode,
};
use traverse_runtime::{LocalExecutionFailure, LocalExecutor, Runtime};
use traverse_runtime::{LocalExecutionFailure, LocalExecutionOutput, LocalExecutor, Runtime};

#[derive(Debug)]
struct ConformanceExecutor;
Expand All @@ -14,8 +14,11 @@ impl LocalExecutor for ConformanceExecutor {
&self,
_capability: &ResolvedCapability,
_input: &Value,
) -> Result<Value, LocalExecutionFailure> {
Ok(json!({"status": "not_executed"}))
) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
Ok(LocalExecutionOutput {
value: json!({"status": "not_executed"}),
emitted_events: Vec::new(),
})
}
}

Expand Down
105 changes: 92 additions & 13 deletions crates/traverse-runtime/src/artifact_router.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use crate::executor::ExecutorError;
#[cfg(feature = "wasmtime-executor")]
use crate::executor::{ArtifactType, CapabilityExecutor, ExecutorCapability, WasmExecutor};
use crate::{LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutor};
use crate::{
LocalExecutionFailure, LocalExecutionFailureCode, LocalExecutionOutput, LocalExecutor,
};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use traverse_registry::ResolvedCapability;

type NativeHandler = dyn Fn(&Value) -> Result<Value, LocalExecutionFailure> + Send + Sync;
type NativeHandler =
dyn Fn(&Value) -> Result<LocalExecutionOutput, LocalExecutionFailure> + Send + Sync;

/// Production local-execution boundary for registered artifacts.
///
Expand Down Expand Up @@ -48,7 +51,10 @@ impl ArtifactRouter {
/// Registers one host-provided native handler for an exact capability id.
pub fn register_native_handler<F>(&mut self, capability_id: impl Into<String>, handler: F)
where
F: Fn(&Value) -> Result<Value, LocalExecutionFailure> + Send + Sync + 'static,
F: Fn(&Value) -> Result<LocalExecutionOutput, LocalExecutionFailure>
+ Send
+ Sync
+ 'static,
{
self.native_handlers
.insert(capability_id.into(), Arc::new(handler));
Expand All @@ -60,7 +66,7 @@ impl LocalExecutor for ArtifactRouter {
&self,
capability: &ResolvedCapability,
input: &Value,
) -> Result<Value, LocalExecutionFailure> {
) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
if let Some(binary) = &capability.artifact.binary {
#[cfg(feature = "wasmtime-executor")]
{
Expand All @@ -80,16 +86,20 @@ impl LocalExecutor for ArtifactRouter {
service_type: capability.contract.service_type.clone(),
};
// Events emitted via `traverse_host::emit_event` during this
// call are intentionally discarded here: `ArtifactRouter`
// bridges directly into `LocalExecutor`, bypassing
// `PlacementRouter` Step 5's `EventBroker` publish (a
// pre-existing gap for workflow-internal node execution,
// unrelated to this ABI — see spec 098's Capability
// Boundary).
// call are returned as real `LocalExecutionOutput.emitted_events`
// (spec 101-local-executor-event-emission FR-003) — already
// ABI-validated by `WasmExecutor`. `ArtifactRouter` itself
// does not publish them (FR-004): it is used both directly
// by `workflows.rs` and, via `BoundLocalExecutor`, by
// `PlacementRouter` Step 5, so publishing here would
// double-publish on the live `Runtime::execute()` path.
return self
.wasm
.execute(&executor_capability, input)
.map(|output| output.value)
.map(|output| LocalExecutionOutput {
value: output.value,
emitted_events: output.emitted_events,
})
.map_err(|error| map_executor_error(&error));
}
#[cfg(not(feature = "wasmtime-executor"))]
Expand Down Expand Up @@ -212,11 +222,17 @@ mod tests {
assert_eq!(failure.code, LocalExecutionFailureCode::ConstraintViolated);

router.register_native_handler("hello.world.say-hello", |_| {
Ok(serde_json::json!({"ok": true}))
Ok(LocalExecutionOutput {
value: serde_json::json!({"ok": true}),
emitted_events: Vec::new(),
})
});
assert_eq!(
router.execute(&capability, &serde_json::json!({})),
Ok(serde_json::json!({"ok": true}))
Ok(LocalExecutionOutput {
value: serde_json::json!({"ok": true}),
emitted_events: Vec::new(),
})
);
}

Expand All @@ -236,6 +252,69 @@ mod tests {
assert_eq!(failure.message, "registered artifact execution failed");
}

#[cfg(feature = "wasmtime-executor")]
#[test]
fn wasm_execution_success_returns_real_value_and_emitted_events() {
use sha2::{Digest, Sha256};
use std::fmt::Write as _;

// Spec 101-local-executor-event-emission FR-003: on a successful
// WASM execution, `ArtifactRouter` must return the executor's real
// `value`/`emitted_events`, not discard or reshape them.
let wat_src = r#"
(module
(import "wasi_snapshot_preview1" "fd_write"
(func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory (export "memory") 1)
(data (i32.const 8) "{}")
(func $_start (export "_start")
(i32.store (i32.const 0) (i32.const 8))
(i32.store (i32.const 4) (i32.const 2))
(drop (call $fd_write (i32.const 1) (i32.const 0) (i32.const 1) (i32.const 4)))
)
)
"#;
let wasm_bytes = wat::parse_str(wat_src).expect("WAT source should parse");

let mut hasher = Sha256::new();
hasher.update(&wasm_bytes);
let checksum = hasher
.finalize()
.iter()
.fold(String::new(), |mut acc, byte| {
let _ = write!(acc, "{byte:02x}");
acc
});

let tmp = format!(
"/tmp/traverse-artifact-router-test-{}.wasm",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos())
);
std::fs::write(&tmp, &wasm_bytes).expect("temp wasm module should write");

let mut capability = resolved_capability(Some(BinaryReference {
format: BinaryFormat::Wasm,
location: tmp.clone(),
signature: None,
}));
capability.artifact.digests.binary_digest = Some(format!("sha256:{checksum}"));

let result = ArtifactRouter::new()
.expect("router should initialize")
.execute(&capability, &serde_json::json!({}));
std::fs::remove_file(&tmp).ok();

assert_eq!(
result,
Ok(LocalExecutionOutput {
value: serde_json::json!({}),
emitted_events: Vec::new(),
})
);
}

#[test]
fn executor_errors_map_to_stable_local_failure_codes() {
let errors = [
Expand Down
Loading
Loading