mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 08:17:53 +00:00
* feat(wasm): lazy schema injection on WASM tool errors When a WASM tool returns an error (ToolReturnedError), call the module's description() and schema() WIT exports and append them as a hint in the error message. This lets the LLM retry with correct parameters without us including large schemas in every request's tools array. - Change ToolReturnedError from tuple to struct variant with hint field - Add build_tool_hint() that calls WASM description()/schema() exports - Cap description at 500 chars, schema at 3000 chars to limit context - Hint flows automatically through Display → ToolError → ChatMessage Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: use floor_char_boundary for UTF-8 safe truncation in tool hints Use existing crate::util::floor_char_boundary() to avoid panicking when truncation lands mid-multibyte character. Addresses review feedback on PR #638. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
230 lines
7.0 KiB
Rust
230 lines
7.0 KiB
Rust
//! WASM sandbox error types.
|
|
|
|
use std::fmt;
|
|
|
|
use thiserror::Error;
|
|
|
|
/// Errors that can occur during WASM tool execution.
|
|
#[derive(Debug, Error)]
|
|
pub enum WasmError {
|
|
/// Failed to create the Wasmtime engine.
|
|
#[error("Engine creation failed: {0}")]
|
|
EngineCreationFailed(String),
|
|
|
|
/// Failed to compile WASM bytes into a component.
|
|
#[error("Compilation failed: {0}")]
|
|
CompilationFailed(String),
|
|
|
|
/// WASM validation failed (malformed or invalid component).
|
|
#[error("Validation failed: {0}")]
|
|
ValidationFailed(String),
|
|
|
|
/// Failed to instantiate the component.
|
|
#[error("Instantiation failed: {0}")]
|
|
InstantiationFailed(String),
|
|
|
|
/// Component execution trapped (e.g., unreachable, memory access violation).
|
|
#[error("Execution trapped: {0}")]
|
|
Trapped(String),
|
|
|
|
/// Component panicked during execution.
|
|
#[error("Execution panicked: {0}")]
|
|
ExecutionPanicked(String),
|
|
|
|
/// Fuel limit exhausted during execution.
|
|
#[error("Fuel exhausted: execution exceeded {limit} fuel units")]
|
|
FuelExhausted {
|
|
/// The fuel limit that was exceeded.
|
|
limit: u64,
|
|
},
|
|
|
|
/// Memory limit exceeded during execution.
|
|
#[error("Memory limit exceeded: {used} bytes used, {limit} bytes allowed")]
|
|
MemoryExceeded {
|
|
/// Bytes used when limit was hit.
|
|
used: u64,
|
|
/// Maximum allowed bytes.
|
|
limit: u64,
|
|
},
|
|
|
|
/// Required export not found in component.
|
|
#[error("Missing export: {0}")]
|
|
MissingExport(String),
|
|
|
|
/// IO error (e.g., reading WASM file).
|
|
#[error("IO error: {0}")]
|
|
IoError(String),
|
|
|
|
/// Configuration error.
|
|
#[error("Configuration error: {0}")]
|
|
ConfigError(String),
|
|
|
|
/// Host function error.
|
|
#[error("Host error: {0}")]
|
|
HostError(String),
|
|
|
|
/// Execution timed out.
|
|
#[error("Execution timed out after {0:?}")]
|
|
Timeout(std::time::Duration),
|
|
|
|
/// Component returned an error response.
|
|
/// When `hint` is non-empty it carries the tool's description and parameter
|
|
/// schema so the LLM can retry with correct arguments.
|
|
#[error("Tool error: {message}{}", if hint.is_empty() { String::new() } else { format!("\n\nTool usage hint:\n{hint}") })]
|
|
ToolReturnedError {
|
|
/// The error message from the WASM tool.
|
|
message: String,
|
|
/// Optional description + schema hint (empty when unavailable).
|
|
hint: String,
|
|
},
|
|
|
|
/// Invalid JSON in tool response.
|
|
#[error("Invalid response JSON: {0}")]
|
|
InvalidResponseJson(String),
|
|
|
|
/// Path traversal attempt blocked.
|
|
#[error("Path traversal blocked: {0}")]
|
|
PathTraversalBlocked(String),
|
|
}
|
|
|
|
impl From<std::io::Error> for WasmError {
|
|
fn from(e: std::io::Error) -> Self {
|
|
WasmError::IoError(e.to_string())
|
|
}
|
|
}
|
|
|
|
impl From<WasmError> for crate::tools::ToolError {
|
|
fn from(e: WasmError) -> Self {
|
|
crate::tools::ToolError::Sandbox(e.to_string())
|
|
}
|
|
}
|
|
|
|
/// Details about a trap that occurred during execution.
|
|
#[derive(Debug, Clone)]
|
|
pub struct TrapInfo {
|
|
/// Human-readable trap message.
|
|
pub message: String,
|
|
/// Trap code if available.
|
|
pub code: Option<TrapCode>,
|
|
}
|
|
|
|
impl fmt::Display for TrapInfo {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match &self.code {
|
|
Some(code) => write!(f, "{}: {}", code, self.message),
|
|
None => write!(f, "{}", self.message),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Known trap codes from Wasmtime.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum TrapCode {
|
|
/// Out of bounds memory access.
|
|
MemoryOutOfBounds,
|
|
/// Out of bounds table access.
|
|
TableOutOfBounds,
|
|
/// Indirect call type mismatch.
|
|
IndirectCallToNull,
|
|
/// Signature mismatch on indirect call.
|
|
BadSignature,
|
|
/// Integer overflow.
|
|
IntegerOverflow,
|
|
/// Integer division by zero.
|
|
IntegerDivisionByZero,
|
|
/// Invalid conversion to integer.
|
|
BadConversionToInteger,
|
|
/// Unreachable instruction executed.
|
|
UnreachableCodeReached,
|
|
/// Call stack exhausted.
|
|
StackOverflow,
|
|
/// Out of fuel.
|
|
OutOfFuel,
|
|
/// Unknown trap code.
|
|
Unknown,
|
|
}
|
|
|
|
impl fmt::Display for TrapCode {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let s = match self {
|
|
TrapCode::MemoryOutOfBounds => "memory out of bounds",
|
|
TrapCode::TableOutOfBounds => "table out of bounds",
|
|
TrapCode::IndirectCallToNull => "indirect call to null",
|
|
TrapCode::BadSignature => "bad signature",
|
|
TrapCode::IntegerOverflow => "integer overflow",
|
|
TrapCode::IntegerDivisionByZero => "integer division by zero",
|
|
TrapCode::BadConversionToInteger => "bad conversion to integer",
|
|
TrapCode::UnreachableCodeReached => "unreachable code reached",
|
|
TrapCode::StackOverflow => "stack overflow",
|
|
TrapCode::OutOfFuel => "out of fuel",
|
|
TrapCode::Unknown => "unknown trap",
|
|
};
|
|
write!(f, "{}", s)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::tools::wasm::error::{TrapCode, TrapInfo, WasmError};
|
|
|
|
#[test]
|
|
fn test_error_display() {
|
|
let err = WasmError::FuelExhausted { limit: 1_000_000 };
|
|
assert!(err.to_string().contains("1000000"));
|
|
|
|
let err = WasmError::MemoryExceeded {
|
|
used: 20_000_000,
|
|
limit: 10_000_000,
|
|
};
|
|
assert!(err.to_string().contains("20000000"));
|
|
assert!(err.to_string().contains("10000000"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_trap_info_display() {
|
|
let info = TrapInfo {
|
|
message: "access at offset 0x1000".to_string(),
|
|
code: Some(TrapCode::MemoryOutOfBounds),
|
|
};
|
|
let s = info.to_string();
|
|
assert!(s.contains("memory out of bounds"));
|
|
assert!(s.contains("access at offset"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_conversion_to_tool_error() {
|
|
let wasm_err = WasmError::Trapped("test trap".to_string());
|
|
let tool_err: crate::tools::ToolError = wasm_err.into();
|
|
match tool_err {
|
|
crate::tools::ToolError::Sandbox(msg) => {
|
|
assert!(msg.contains("test trap"));
|
|
}
|
|
_ => panic!("Expected Sandbox variant"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_returned_error_without_hint() {
|
|
let err = WasmError::ToolReturnedError {
|
|
message: "unknown action: foobar".to_string(),
|
|
hint: String::new(),
|
|
};
|
|
let display = err.to_string();
|
|
assert!(display.contains("unknown action: foobar"));
|
|
assert!(!display.contains("Tool usage hint"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_tool_returned_error_with_hint() {
|
|
let err = WasmError::ToolReturnedError {
|
|
message: "unknown action: foobar".to_string(),
|
|
hint: "Description: Gmail tool\nParameters schema: {\"type\":\"object\"}".to_string(),
|
|
};
|
|
let display = err.to_string();
|
|
assert!(display.contains("unknown action: foobar"));
|
|
assert!(display.contains("Tool usage hint"));
|
|
assert!(display.contains("Gmail tool"));
|
|
assert!(display.contains("Parameters schema"));
|
|
}
|
|
}
|