mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 16:19:21 +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]>
143 lines
5.8 KiB
Rust
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,
|
|
};
|