Files
optimclaw/src/tools/wasm/mod.rs
T
04c5c3fe9f feat: WASM extension versioning with WIT compat checks (#592)
* 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]>
2026-03-06 04:38:07 +00:00

143 lines
5.8 KiB
Rust

//! WASM sandbox for untrusted tool execution.
//!
//! This module provides Wasmtime-based sandboxed execution for tools,
//! following patterns from NEAR blockchain and modern WASM best practices:
//!
//! - **Compile once, instantiate fresh**: Tools are validated and compiled
//! at registration time. Each execution creates a fresh instance.
//!
//! - **Fuel metering**: CPU usage is limited via Wasmtime's fuel system.
//!
//! - **Memory limits**: Memory growth is bounded via ResourceLimiter.
//!
//! - **Extended host API (V2)**: log, time, workspace, HTTP, tool invoke, secrets
//!
//! - **Capability-based security**: Features are opt-in via Capabilities.
//!
//! # Architecture (V2)
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────────────────┐
//! │ WASM Tool Execution │
//! │ │
//! │ WASM Tool ──▶ Host Function ──▶ Allowlist ──▶ Credential ──▶ Execute │
//! │ (untrusted) (boundary) Validator Injector Request │
//! │ │ │
//! │ ▼ │
//! │ ◀────── Leak Detector ◀────── Response │
//! │ (sanitized, no secrets) │
//! └─────────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! # Security Constraints
//!
//! | Threat | Mitigation |
//! |--------|------------|
//! | CPU exhaustion | Fuel metering |
//! | Memory exhaustion | ResourceLimiter, 10MB default |
//! | Infinite loops | Epoch interruption + tokio timeout |
//! | Filesystem access | No WASI FS, only host workspace_read |
//! | Network access | Allowlisted endpoints only |
//! | Credential exposure | Injection at host boundary only |
//! | Secret exfiltration | Leak detector scans all outputs |
//! | Log spam | Max 1000 entries, 4KB per message |
//! | Path traversal | Validate paths (no `..`, no `/` prefix) |
//! | Trap recovery | Discard instance, never reuse |
//! | Side channels | Fresh instance per execution |
//! | Rate abuse | Per-tool rate limiting |
//! | WASM tampering | BLAKE3 hash verification on load |
//! | Direct tool access | Tool aliasing (indirection layer) |
//!
//! # Example
//!
//! ```ignore
//! use ironclaw::tools::wasm::{WasmToolRuntime, WasmRuntimeConfig, WasmToolWrapper};
//! use ironclaw::tools::wasm::Capabilities;
//! use std::sync::Arc;
//!
//! // Create runtime
//! let runtime = Arc::new(WasmToolRuntime::new(WasmRuntimeConfig::default())?);
//!
//! // Prepare a tool from WASM bytes
//! let wasm_bytes = std::fs::read("my_tool.wasm")?;
//! let prepared = runtime.prepare("my_tool", &wasm_bytes, None).await?;
//!
//! // Create wrapper with HTTP capability
//! let capabilities = Capabilities::none()
//! .with_http(HttpCapability::new(vec![
//! EndpointPattern::host("api.openai.com").with_path_prefix("/v1/"),
//! ]));
//! let tool = WasmToolWrapper::new(runtime, prepared, capabilities);
//!
//! // Execute (implements Tool trait)
//! let output = tool.execute(serde_json::json!({"input": "test"}), &ctx).await?;
//! ```
/// Host WIT version for tool extensions.
///
/// Extensions declaring a `wit_version` in their capabilities file are checked
/// against this at load time: same major, not greater than host.
pub const WIT_TOOL_VERSION: &str = "0.2.0";
/// Host WIT version for channel extensions.
pub const WIT_CHANNEL_VERSION: &str = "0.2.0";
mod allowlist;
mod capabilities;
mod capabilities_schema;
pub(crate) mod credential_injector;
mod error;
mod host;
mod limits;
pub(crate) mod loader;
mod rate_limiter;
mod runtime;
pub(crate) mod storage;
mod wrapper;
// Core types
pub use error::{TrapCode, TrapInfo, WasmError};
pub use host::{HostState, LogEntry, LogLevel};
pub use limits::{
DEFAULT_FUEL_LIMIT, DEFAULT_MEMORY_LIMIT, DEFAULT_TIMEOUT, FuelConfig, ResourceLimits,
WasmResourceLimiter,
};
pub use runtime::{PreparedModule, WasmRuntimeConfig, WasmToolRuntime};
pub use wrapper::{OAuthRefreshConfig, WasmToolWrapper};
// Capabilities (V2)
pub use capabilities::{
Capabilities, EndpointPattern, HttpCapability, RateLimitConfig, SecretsCapability,
ToolInvokeCapability, WorkspaceCapability, WorkspaceReader,
};
// Security components (V2)
pub use allowlist::{AllowlistResult, AllowlistValidator, DenyReason};
pub(crate) use credential_injector::inject_credential;
pub use credential_injector::{
CredentialInjector, InjectedCredentials, InjectionError, SharedCredentialRegistry,
};
pub use rate_limiter::{LimitType, RateLimitError, RateLimitResult, RateLimiter};
// Storage (V2)
#[cfg(feature = "libsql")]
pub use storage::LibSqlWasmToolStore;
#[cfg(feature = "postgres")]
pub use storage::PostgresWasmToolStore;
pub use storage::{
StoreToolParams, StoredCapabilities, StoredWasmTool, StoredWasmToolWithBinary, ToolStatus,
TrustLevel, WasmStorageError, WasmToolStore, compute_binary_hash, verify_binary_integrity,
};
// Loader
pub use loader::{
DiscoveredTool, LoadResults, WasmLoadError, WasmToolLoader, discover_dev_tools, discover_tools,
load_dev_tools, resolve_wasm_target_dir, wasm_artifact_path,
};
// Capabilities schema (for parsing *.capabilities.json files)
pub use capabilities_schema::{
AuthCapabilitySchema, CapabilitiesFile, OAuthConfigSchema, RateLimitSchema,
ValidationEndpointSchema,
};