mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* feat: add WASM extension versioning with WIT compat checks and CI enforcement Phase 1 — WIT Versioning & Compatibility Checks: - Version WIT packages as `package near:[email protected];` - Add `semver` crate for version parsing and comparison - Add `WIT_TOOL_VERSION` / `WIT_CHANNEL_VERSION` host constants - Add `version` and `wit_version` fields to capabilities schemas - Add `wit_version` column to `wasm_tools` DB table (both backends) - Add load-time `check_wit_version_compat()` with semver rules - Add `IncompatibleWitVersion` error variants for tools and channels - Enhance instantiation errors with WIT version mismatch hints - Update all 14 capabilities JSON and 14 registry JSON files Phase 2 — Upgrade-in-Place & Channel DB Storage: - Change tool store to DELETE-before-INSERT (one version per extension) - Create `wasm_channels` table (PostgreSQL migration + libSQL schema) - Add `WasmChannelStore` trait with PostgreSQL and libSQL backends - Add `extension_info` tool showing version, WIT version, and status - Wire `ExtensionInfoTool` into tool registry (7 extension tools) Phase 3 — CI Version-Bump Enforcement: - Add `scripts/check-version-bumps.sh` checking WIT/tool/channel versions - Add `version-check` CI job (PR-only) to `.github/workflows/test.yml` - Support `[skip-version-check]` label/commit message bypass Includes 7 regression tests for WIT version compatibility checking and 2 integration tests for WIT version annotation verification. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review feedback for WASM extension versioning - Wrap PostgreSQL DELETE+INSERT in transactions for both tool and channel store() methods to prevent data loss on partial failure (Gemini, Copilot) - Rename StoredWasmChannelWithBinary.tool → .channel (copy-paste fix) - Remove unused WasmError::IncompatibleWitVersion variant (dead code) - Map channel loader WIT mismatch to IncompatibleWitVersion instead of generic Config error, simplify variant to single String message - Fix extension_info description to match actual returned fields - Add schema test for ExtensionInfoTool matching existing test pattern - Fix CI script to fail fast on git errors instead of silent bypass [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
153 lines
5.1 KiB
Plaintext
153 lines
5.1 KiB
Plaintext
// WASM Tool Sandbox Interface
|
|
//
|
|
// Defines the contract between sandboxed tools and the host runtime.
|
|
// Tools export the `tool` interface; the host provides the `host` interface.
|
|
//
|
|
// Security Model:
|
|
// - WASM tools are untrusted and run in a sandbox
|
|
// - All capabilities are opt-in (default: no access)
|
|
// - Secrets are NEVER exposed to WASM; credentials are injected at host boundary
|
|
// - All outputs are scanned for secret leakage before returning to WASM
|
|
|
|
package near:[email protected];
|
|
|
|
/// Host-provided capabilities for sandboxed tools.
|
|
///
|
|
/// These are the only ways a sandboxed tool can interact with the outside world.
|
|
/// The set is intentionally minimal to reduce attack surface.
|
|
interface host {
|
|
/// Log levels for structured logging.
|
|
enum log-level {
|
|
trace,
|
|
debug,
|
|
info,
|
|
warn,
|
|
error,
|
|
}
|
|
|
|
/// Emit a log message.
|
|
///
|
|
/// Messages are collected and emitted after execution completes.
|
|
/// Rate-limited to 1000 entries per execution, 4KB per message.
|
|
log: func(level: log-level, message: string);
|
|
|
|
/// Get the current timestamp in milliseconds since Unix epoch.
|
|
now-millis: func() -> u64;
|
|
|
|
/// Read a file from the workspace (if capability granted).
|
|
///
|
|
/// Path must be relative (no leading /) and cannot contain "..".
|
|
/// Returns None if the file doesn't exist or capability not granted.
|
|
workspace-read: func(path: string) -> option<string>;
|
|
|
|
// ==================== HTTP Capability ====================
|
|
|
|
/// Response from an HTTP request.
|
|
record http-response {
|
|
/// HTTP status code.
|
|
status: u16,
|
|
/// Response headers as JSON object string.
|
|
headers-json: string,
|
|
/// Response body bytes.
|
|
body: list<u8>,
|
|
}
|
|
|
|
/// Make an HTTP request (if capability granted).
|
|
///
|
|
/// Security:
|
|
/// - Only allowed endpoints (host/path patterns) can be accessed
|
|
/// - Credentials are injected by the host; WASM never sees them
|
|
/// - Response is scanned for leaked secrets before returning
|
|
/// - Rate-limited per tool
|
|
///
|
|
/// Returns Err with error message if:
|
|
/// - Endpoint not in allowlist
|
|
/// - Rate limit exceeded
|
|
/// - Request/response size limit exceeded
|
|
/// - Network error
|
|
/// - Timeout
|
|
/// - Secret leak detected in response
|
|
///
|
|
/// The optional timeout-ms parameter controls the HTTP client timeout
|
|
/// in milliseconds. Defaults to 30000 (30s) when not provided.
|
|
/// Capped at the callback timeout to prevent hangs.
|
|
http-request: func(
|
|
method: string,
|
|
url: string,
|
|
headers-json: string,
|
|
body: option<list<u8>>,
|
|
timeout-ms: option<u32>,
|
|
) -> result<http-response, string>;
|
|
|
|
// ==================== Tool Invocation Capability ====================
|
|
|
|
/// Invoke another tool by alias (if capability granted).
|
|
///
|
|
/// Security:
|
|
/// - WASM calls tools by alias, not real name (indirection layer)
|
|
/// - Only aliased tools can be invoked
|
|
/// - Rate-limited per tool
|
|
/// - Output is scanned for leaked secrets before returning
|
|
///
|
|
/// Returns the tool output as JSON string, or Err with error message.
|
|
tool-invoke: func(alias: string, params-json: string) -> result<string, string>;
|
|
|
|
// ==================== Secrets Capability ====================
|
|
|
|
/// Check if a secret exists (if capability granted).
|
|
///
|
|
/// Security:
|
|
/// - WASM can only check existence, NEVER read values
|
|
/// - Only allowed secret names can be checked
|
|
/// - Actual credentials are injected by host during HTTP requests
|
|
///
|
|
/// Returns true if the secret exists and is accessible to this tool.
|
|
secret-exists: func(name: string) -> bool;
|
|
}
|
|
|
|
/// Tool interface that sandboxed tools must implement.
|
|
interface tool {
|
|
/// Request payload for tool execution.
|
|
record request {
|
|
/// JSON-encoded parameters matching the tool's schema.
|
|
params: string,
|
|
/// Optional JSON-encoded job context for stateful operations.
|
|
context: option<string>,
|
|
}
|
|
|
|
/// Response from tool execution.
|
|
record response {
|
|
/// JSON-encoded output on success.
|
|
output: option<string>,
|
|
/// Error message on failure.
|
|
error: option<string>,
|
|
}
|
|
|
|
/// Execute the tool with the given request.
|
|
///
|
|
/// This is the main entry point. The tool should:
|
|
/// 1. Parse params as JSON according to its schema
|
|
/// 2. Perform the operation
|
|
/// 3. Return a response with either result or error set
|
|
execute: func(req: request) -> response;
|
|
|
|
/// Get the JSON Schema for this tool's parameters.
|
|
///
|
|
/// Must return a valid JSON Schema object describing the expected
|
|
/// structure of the `params` field in requests.
|
|
schema: func() -> string;
|
|
|
|
/// Get a human-readable description of what this tool does.
|
|
///
|
|
/// Used by the LLM to understand when to invoke the tool.
|
|
description: func() -> string;
|
|
}
|
|
|
|
/// World definition for sandboxed tools.
|
|
///
|
|
/// Tools import host capabilities and export the tool interface.
|
|
world sandboxed-tool {
|
|
import host;
|
|
export tool;
|
|
}
|