The extract_tool_description and extract_tool_schema stubs in runtime.rs
returned permissive fallbacks ("WASM sandboxed tool" and
additionalProperties:true) for every WASM tool, defeating parameter
validation and preventing the LLM from using tools correctly.
Add optional `description` and `parameters` fields to CapabilitiesFile so
tool authors can declare proper metadata in their sidecar JSON. The
WasmToolLoader now extracts these fields and passes them through to the
tool registry as overrides. Tools without a capabilities.json or without
these fields get a tracing::warn and fall back to the old stubs.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(wasm): use per-engine cache dirs on Windows to avoid file lock error (#448)
On Windows, multiple wasmtime Engine instances sharing the default
compilation cache directory hit OS error 33 (ERROR_LOCK_VIOLATION)
because Windows holds exclusive file locks on memory-mapped cache
files. This is especially triggered when the Telegram channel WASM
module is loaded at startup and then hot-activated via the Extensions
UI.
Fix by giving each engine its own cache subdirectory on Windows
(~/.cache/ironclaw/wasmtime-tools/ and wasmtime-channels/). On
Unix the shared default cache continues to work as before.
Also adds Windows CI jobs (cargo check + clippy across all feature
flag combinations) to catch Windows-specific issues going forward.
Closes#448
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: silence Windows clippy warnings for platform-gated code
Gate PathBuf import behind #[cfg(unix)] in container.rs (only used
in Unix socket path), suppress unused_mut on conflicts Vec in
channels.rs (mutations are platform-gated), and add cfg gates on
keychain constants and hex_to_bytes that are only used on macOS/Linux.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: escape directory path in TOML cache config to prevent injection
Use double-quoted TOML strings with backslash and double-quote
escaping for the cache directory path, preventing breakage or
injection when paths contain special characters (e.g. single
quotes on Unix, backslashes on Windows).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve cargo fmt formatting errors
Fix import ordering in container.rs and line wrapping in runtime.rs
to pass the CI formatting check.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(ci): restore Path import for all platforms, keep PathBuf unix-only
Path is used in non-cfg-gated functions (lines 148, 244) so it must
be available on all platforms. Only PathBuf is unix-specific.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: init WASM runtime eagerly regardless of tools directory existence
The WASM tool runtime was only created at startup when both
`wasm.enabled` and `wasm.tools_dir.exists()` were true. This meant
that if the tools directory didn't exist yet (e.g. fresh deploy with
`--no-onboard`), the runtime was set to None and passed to the
ExtensionManager. Extensions installed later via the web UI would
then fail with "WASM runtime not available" because the runtime
could not be retroactively created.
The Wasmtime engine initialization has no dependency on the tools
directory — it only configures the compiler and starts an epoch
ticker thread. The directory is only needed later when loading
.wasm modules. Remove the directory check so the runtime is
available for post-startup extension activation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add regression tests for WASM runtime eager init
- runtime.rs: test_runtime_creation_without_tools_dir confirms the
Wasmtime engine initialises without a tools directory on disk
- manager.rs: test_activate_wasm_tool_with_runtime_passes_runtime_check
verifies activation gets past the runtime check when a runtime is
provided (fails on missing file, not missing runtime)
- manager.rs: test_activate_wasm_tool_without_runtime_fails_with_runtime_error
verifies the original error when no runtime is available
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: use idiomatic Result-to-Option conversion for WASM runtime init
Address PR review feedback: replace match block with
.map(Arc::new).map_err(|e| warn!(...)).ok() chain.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix formatting in extension manager tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Three high-impact changes eliminate most startup latency:
1. Enable wasmtime persistent compilation cache — call
cache_config_load_default() so compiled native code is serialized to
disk (~/.cache/wasmtime). Subsequent startups deserialize instead of
recompiling, dropping the WASM phase from ~13s to <1s.
2. Cache compiled Component in PreparedModule — store the compiled
wasmtime::component::Component directly instead of raw bytes.
Eliminates ~2.6s recompilation on every first tool/channel execution.
3. Move blocking housekeeping to background tasks — embedding backfill
(~1.3s of failing HTTP calls) and stale job cleanup are fire-and-forget
work that no longer blocks the critical startup path.
Also: deduplicate Workspace creation in main.rs (two identical instances
reduced to one), and replace leftover println! in session validation with
tracing calls.
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: comprehensive security hardening across all layers
Critical:
- Replace --dangerously-skip-permissions with explicit tool allowlist
via settings.json (Claude Code bridge)
- Constant-time token comparison (subtle crate) in web auth and
orchestrator auth to prevent timing attacks
High:
- Revoke tokens and clean up handles on container creation failure
- Drop SETUID/SETGID capabilities from containers (keep only CHOWN)
- Disable redirect following in HTTP tool and WASM wrapper (SSRF)
- Reject URL userinfo (@) in WASM allowlist parser (host confusion)
- Fix binary body bypassing leak detection (from_utf8 -> from_utf8_lossy)
- Protect identity files from LLM overwrites (prompt injection defense)
- Prevent tool shadowing: built-in tools cannot be replaced dynamically
- User-scoped job APIs: list/detail/cancel/restart/prompt/events/files
- CORS restricted to localhost origins, WebSocket origin validation
- Sandbox shell fail-closed: no silent fallback to unsandboxed execution
- Scrub secrets from log broadcaster before SSE broadcast
- XSS sanitization on rendered markdown in web UI
- WASM epoch ticker thread so timeout deadlines actually fire
Medium:
- Cap state transition history at 200 entries
- SSE/WebSocket connection limit (100 max)
- Request body size limit (1MB)
- Response body size limit enforcement in WASM HTTP
- UTF-8 safe string truncation (routine engine, shell tool)
- Fix PolicyAction::Sanitize to actually run the sanitizer
- TOCTOU fix in scheduler and context manager (hold write lock)
- Project file serving moved behind auth
- Path traversal guard on project_id
- Session file permissions set to 0600 on unix
- AtomicUsize for routine running_count (panic-safe)
- Completion detection hardened against false positives and tool injection
- Tool output no longer drives job completion (only LLM response)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address security review findings across all layers
- Fix path traversal sandbox bypass via lexical normalization (file.rs)
- Fix SSRF via DNS rebinding with pre-request hostname resolution (http.rs)
- Add token budget enforcement on LLM calls (reasoning.rs, state.rs)
- Fix cross-user chat history leak with ownership verification (store.rs, server.rs)
- Add sliding-window rate limiter on gateway chat endpoint (server.rs)
- Harden extension install: HTTPS-only, 50MB cap, WASM magic validation (manager.rs)
- Add destructive command blocklist that overrides shell auto-approval (shell.rs)
- Add 5MB response body size cap to HTTP tool (http.rs)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: deduplicate shared helpers and remove dead code
Extract floor_char_boundary and llm_signals_completion into src/util.rs,
unifying diverging phrase lists from agent/worker.rs and worker/runtime.rs.
Remove dead RespondResult::usage(), duplicate PROTECTED_IDENTITY_FILES
constant, double LeakDetector scanning in WebLogLayer, and invalid
0.0.0.0 origin from WebSocket allow list.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review findings and CI test failures
- Fix record_failed_approve: .truncate(true) wiped the attempts file
before reading, so failed pairing attempts never accumulated and
rate limiting never triggered.
- Guard wizard WASM test: skip gracefully when channel build artifacts
are absent (CI doesn't compile wasm32-wasip2 targets).
- Fix DNS rebinding check: use port 0 instead of hardcoded 443, since
the port is irrelevant for hostname resolution.
- Remove hardcoded CORS port 3001: the dynamic addr.port() entries
already cover the actual server port.
- Require WebSocket Origin header: reject connections that omit it
entirely, since browsers always send Origin for WS upgrades and a
missing header indicates a non-browser client bypassing the check.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address second round of PR review findings
- store.rs: reintroduce file locking around read-modify-write in
record_failed_approve (concurrent callers could clobber each other).
- sse.rs: replace load+check+fetch_add with atomic fetch_update in both
subscribe_raw() and subscribe() to prevent overshooting max_connections.
- ws.rs: decrement WS tracker before early return when subscribe_raw()
returns None (connection limit reached), fixing a counter leak.
- server.rs: parse WS Origin host exactly instead of prefix matching,
preventing bypass via crafted origins like http://localhost.evil.com.
- workspace_integration.rs: skip tests gracefully when Postgres is
unreachable instead of panicking (fixes 10 CI failures).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add Origin header to WS integration tests
The Origin header requirement added in a3b0190 broke the WS gateway
integration tests. Test clients now send Origin: http://127.0.0.1:{port}
to match the server's localhost validation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>