* feat: add Brave Web Search WASM tool
Add a new WASM tool for searching the web via the Brave Search API.
Follows the same architecture as the GitHub WASM tool with zero-exposure
credential injection (X-Subscription-Token header).
Features:
- Full Brave Search API support (query, count, country, search_lang,
ui_lang, freshness)
- Input validation on all parameters
- Retry logic for 429/5xx transient errors
- RFC 3986 percent-encoding
- Registry manifest for Extensions tab discovery
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: avoid Vec allocation in is_valid_ui_lang
Use iterator-based destructuring instead of collecting into a Vec,
avoiding a heap allocation in the WASM sandbox.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
- Add scrollIntoView to keep arrow-key-selected item visible in dropdown
- Make Enter complete the first matching command when autocomplete is
visible, instead of requiring explicit arrow-key navigation first
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
The tool manifests pointed to channel bundle URLs (telegram-wasm32-wasip2.tar.gz,
slack-wasm32-wasip2.tar.gz) instead of the tool bundles (telegram-mtproto-...,
slack-tool-...). This caused install to fail because the archive contents
didn't match the expected .wasm filename.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
The compiler cannot infer the element type of `conflicts` on Windows
because all `push` calls are inside `#[cfg(unix)]` blocks which don't
compile on Windows.
Co-authored-by: Claude Sonnet 4.6 (1M context) <[email protected]>
* fix(channels): add host-based credential injection to WASM channel wrapper
The channel WASM wrapper was missing the host-based credential injection
that the tools wrapper implements. The `credentials` block in channel
capabilities files was dead code: Slack's `on_respond` sends requests
with no Authorization header, expecting the host to inject the bot token
based on `host_patterns`, but the host never did.
This caused Slack (and any channel relying on capabilities-declared
credentials) to fail all outbound API calls with `not_authed`.
Changes:
- Add `ResolvedHostCredential` struct mirroring the tools wrapper
- Add `host_credentials` field to `ChannelStoreData`
- Add `inject_host_credentials()` method on `ChannelStoreData`
- Update `redact_credentials()` to also scrub host-injected secret values
- Add `secrets_store` field to `WasmChannel` + `with_secrets_store()` builder
- Add `resolve_channel_host_credentials()` async helper that decrypts
capabilities-declared credentials before each WASM callback
- Update `create_store()` and all `call_on_*` / `execute_status` /
`execute_poll` call sites to pre-resolve and pass host credentials
- Fix leak scan ordering: scan runs on WASM-provided values BEFORE host
credential injection, preventing false-positive blocks on injected
Bearer tokens (e.g. xoxb- Slack tokens)
- Make `credential_injector` module pub(crate) so channels can reuse
`inject_credential` and `host_matches_pattern`
Fixes#389, root cause of #413
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(wasm): redact URL-encoded credentials, use url::Url, derive Clone
Address review feedback on PR #421:
1. Security: redact_credentials now scrubs URL-encoded forms of secrets
in addition to raw values, preventing exfiltration via encoded
representations in error strings from reqwest
2. Use url::Url::query_pairs_mut() for query parameter injection instead
of manual string manipulation, improving robustness with malformed URLs
3. Derive Clone on ResolvedHostCredential and simplify the per-tick
clone in the status repeater loop
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Sprite <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* refactor: rename WasmBuildable::repo_url to source_dir
The field receives a local directory path (e.g. "tools-src/gmail"), not a
URL. Rename to source_dir to accurately reflect its purpose.
Adds #[serde(alias = "repo_url")] for backwards compatibility with any
previously serialized data.
Closes#329
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: rename extract_url to extract_source
The function can return a local directory path, not just a URL.
Addresses review feedback on PR #445.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: pre-validate Cloudflare tunnel token by spawning cloudflared
After format validation passes, spawn `cloudflared tunnel run` briefly
with a dummy URL and watch stderr for up to 10s. If an error appears
before a connection URL, report it and offer "Save anyway?". This
catches bad tokens during setup instead of at runtime 30s later.
Closes#440
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: tighten cloudflared output matching in live validation
- Check for cfargotunnel.com/trycloudflare.com in success detection
- Use starts_with("err") instead of contains("err") to avoid false
positives on words like "stderr"
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: prevent Telegram 409 Conflict on webhook re-registration
Delete any existing webhook before calling setWebhook in on_start(),
matching the defensive cleanup that polling mode already does. As a
safety net, register_webhook() now retries once on 409 after calling
delete_webhook().
Closes#440
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: deduplicate 409 retry logic in register_webhook
Restructure the match block so the initial request and retry share
a single response-handling code path.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: persist channel activation state across restarts (#392)
Channels activated via the web UI were lost on restart because
active_channel_names was only in memory. Now persist activation state
to the settings store under "activated_channels" and auto-activate
persisted channels on startup.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: log warnings for channel activation load failures
Replace silent catch-all with explicit error logging when
database queries or deserialization fails for activated channels.
Addresses Gemini review feedback on PR #432.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* 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]>
All PostgreSQL connection sites hardcoded NoTls, preventing connections
to managed providers that require TLS (AWS RDS, Neon, Supabase, etc.).
- Add tokio-postgres-rustls with rustls + system root certificates
- Add SslMode enum (disable/prefer/require) via DATABASE_SSLMODE env var
- Replace NoTls at all 4 production call sites with TLS-aware pool creation
- Add SslMode::from_env() helper for lightweight CLI tools
- Log native cert loading errors and warn on empty root store
Default mode is Prefer (attempts TLS, matching most managed providers).
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: scan inbound messages for leaked secrets before LLM processing (#393)
Add scan_inbound_for_secrets() to SafetyLayer that reuses the existing
leak detector on user input. Wire it into thread_ops.rs after the policy
check so messages containing API keys or tokens are rejected early,
preventing the LLM from echoing them back and triggering outbound
leak-detection error loops.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: unify inbound secret scan warning messages
Both the detected-secret and error branches now show the same
actionable message guiding users to remove secrets and use the
config system instead.
Addresses Gemini review feedback on PR #433.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: use tailscale funnel --bg for proper tunnel setup (#394)
The old command `tailscale funnel http://127.0.0.1:3000` would hang
without establishing a tunnel. The correct invocation is
`tailscale funnel --bg <port>` which configures the tunnel as a
background daemon and exits.
Changes:
- Use `--bg` flag with just the port number
- Run as a one-shot command instead of spawning a child process
- Use `tailscale <cmd> off` to tear down (matches --bg semantics)
- health_check uses stored URL instead of non-existent child PID
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use local_host parameter and verify tailscale health
Pass full http://host:port URL to tailscale instead of ignoring
the local_host parameter. Health check now verifies tailscale is
actually running via 'tailscale status --json'.
Addresses Gemini review feedback on PR #430.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: add missing build.sh for Discord and WhatsApp channels (#406)
Both channels had full source code in channels-src/ but no build.sh,
so their WASM binaries were never compiled and they didn't appear in
the setup wizard's channel selection list.
Modeled after the existing channels-src/telegram/build.sh.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: guard wasm-tools availability in WASM build scripts
Add command existence check before invoking wasm-tools in discord
and whatsapp build scripts. Prints actionable error message if missing.
Addresses Gemini review feedback on PR #429.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
The Slack channel capabilities.json declares secret names in lowercase
(slack_bot_token) but the web UI stored them in UPPERCASE
(SLACK_BOT_TOKEN), causing credential injection to fail with
"not_authed".
Changes:
- CreateSecretParams::new() normalizes name to lowercase on creation
- All SecretsStore lookups (get, exists, delete, is_accessible) now
lowercase the name parameter before querying
- Applied to all three backends: PostgreSQL, libSQL, InMemory
- CredentialInjector::is_secret_allowed() uses case-insensitive
comparison
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: persist model name to .env so dotted names survive restart (#400)
The setup wizard saved selected_model to the DB but not to .env.
Since Config::from_env_with_toml() runs before the DB connects, the
model name was lost on restart -- backends fell back to hardcoded
defaults, truncating names like "llama3.2" to "llama3".
- Add LlmBackend::model_env_var() as single source of truth for the
backend-to-env-var mapping
- Write the model env var in write_bootstrap_env() using the new method
- Add selected_model fallback to all 6 backends (was missing from
OpenAI, Anthropic, Ollama, and Tinfoil)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: extract resolve_model() helper to reduce duplication
Address review feedback: the env → settings → default model resolution
pattern was repeated across all 6 backends. Centralise it in a single
LlmConfig::resolve_model() helper.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(setup): check cloudflared binary and validate tunnel token (#418)
The Cloudflare tunnel setup accepted tokens blindly without checking if
cloudflared was installed or if the token was valid. Now:
- Checks for cloudflared on PATH before accepting a token, with install
instructions if missing (user can continue anyway)
- Validates token format (base64-decoded JSON with account/tunnel fields)
with a warning if malformed (user can override)
- Replaces misleading "will start automatically at boot" with honest
instructions for starting the tunnel and installing as a service
- Reuses binary_exists() from skills::gating (promoted to pub(crate))
for cross-platform PATH lookup
Closes#418
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: reuse cloudflared_found instead of redundant binary_exists call
Address review feedback: the binary check result was already stored
in cloudflared_found from earlier in the function.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(setup): validate PostgreSQL version and pgvector before migrations
The setup wizard accepted any DATABASE_URL without checking the server
version or pgvector availability. Users who installed PostgreSQL 14
(or any version < 15) got opaque migration failures. Users without
pgvector installed hit CREATE EXTENSION errors at runtime.
After a successful connection, the wizard now:
1. Queries SHOW server_version and rejects versions below 15
2. Checks pg_available_extensions for the vector extension
Both checks provide actionable error messages with platform-specific
install guidance.
Closes#415Closes#416
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: extract version constant, fix hex escapes in pgvector message
- Extract MIN_PG_MAJOR_VERSION constant to avoid magic number
- Replace \x20 hex escapes with regular spaces in install guidance
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(setup): use detected PG version in pgvector install instructions
The pgvector install hints were hardcoded for PG 16. Since we already
parse major_version from SHOW server_version, use it dynamically so
users on PG 15 or 17 get correct package names.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: guard zsh compdef call to prevent error before compinit
The generated ironclaw.zsh completions file calls compdef without
checking if it exists. Users who source this file before compinit
runs in their .zshrc get "compdef: command not found" on every
terminal open.
Wrap the call with the standard (( $+functions[compdef] )) guard.
Closes#420
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(completions): apply compdef guard during zsh generation
Instead of hand-patching the generated ironclaw.zsh file (which is
fragile and lost on regeneration), patch the compdef call in the
generation code itself. The Zsh output is post-processed to wrap
`compdef _ironclaw ironclaw` with a `$+functions[compdef]` guard.
Regenerated ironclaw.zsh from the patched code to stay in sync.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(cli): add `tool setup` command + GitHub setup schema
- Add `ironclaw tool setup <name>` CLI command that reads
`setup.required_secrets` from a tool's capabilities file and
prompts the user for each secret, saving them to the encrypted
secrets store. Handles already-configured secrets (ask to replace),
optional secrets (skip on empty), and hidden input.
- Add `setup.required_secrets` to GitHub tool capabilities file
with `github_token` — the only WASM tool that was missing it
after PR #437 added setup schemas to all other tools.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(cli): extract init_secrets_store helper + add tool name validation
Address PR review feedback:
- Extract duplicated secrets store initialization (~50 lines) from
auth_tool and setup_tool into shared init_secrets_store() helper
- Add validate_tool_name() to reject path traversal in tool names
(applies to both auth_tool and setup_tool)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix(web): remove gateway restart button from channel activation failure cards
When a WASM channel (e.g. Telegram) fails to hot-activate after setup,
the extension card showed a "Restart" button that calls POST /api/gateway/restart.
This triggers a process exit and relies on an external supervisor to relaunch,
which doesn't work reliably when running inside Docker.
Remove the Restart button entirely from the failed-activation card for all
channels — Reconfigure is the correct recovery action (re-enter credentials).
Also fix two bugs found during review:
- setServerLogLevel/loadServerLogLevel called .json() on the already-parsed
object returned by apiFetch, causing a silent TypeError that prevented the
log level selector from updating
- buildBreadcrumb embedded paths in inline onclick JS strings using escapeHtml,
which doesn't escape single quotes; switched to data-path attribute pattern
to avoid JS string injection from paths containing quotes
And simplify: collapse the dead Telegram-specific branch in submitConfigureModal
toast messaging — all channels now show "Configured and activated X" on success.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(telegram): propagate token validation errors from on_start
Both webhook and polling mode in on_start() swallowed activation errors
from register_webhook/delete_webhook — using `if let Err(e)` to log
but then returning Ok regardless. This caused a bad bot token to show
as "configured and active" instead of failing activation.
Telegram returns {"ok": true} when deleteWebhook is called with no
existing webhook (idempotent), so any error (e.g. 401 Unauthorized)
genuinely means an invalid token.
The WASM is rebuilt automatically via build.rs on cargo build.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(telegram): validate bot token before storing, fix misleading toast
Add upfront GET /getMe validation in save_setup_secrets() before writing
the bot token to the secrets store. This catches bad tokens immediately
for both fresh installs and reconfigures — the reconfigure path
(refresh_active_channel) skips on_start entirely and would never catch
an invalid token without this check. URL-encode the token before
interpolating into the getMe URL path.
Also update the activation-failure toast from "Restart required to
activate" (misleading now that the Restart button is gone) to
"Use Reconfigure to re-enter credentials and activate".
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(telegram): collapse nested if, fix formatting (clippy + fmt)
Collapse `if name == "telegram" { if let Some(...) }` into a single
let-chain condition as suggested by clippy's collapsible_if lint.
Also apply rustfmt line-length fixes in the same block.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* feat: add web_fetch built-in tool and web-fetch skill
- New web_fetch Rust built-in tool (GET-only, auto-approved, structured
output: url/title/content/word_count) with HTML to Markdown via Readability
- Full SSRF protection: HTTPS-only, no private IPs, DNS rebinding defence,
outbound/inbound leak scanning, 5 MB cap, no redirect following
- Rate limited: 30 req/min, 500/hr (same as http tool)
- Protected tool name; registered in register_builtin_tools()
- validate_url made pub(crate) so web_fetch can reuse it from http.rs
- New skills/web-fetch/SKILL.md for agent guidance on web browsing
- Fixes unicode panic in extract_title: use to_ascii_lowercase not
to_lowercase to preserve byte offsets when indexing original string
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* chore: remove web-fetch skill (tool description is self-sufficient)
The web_fetch tool's schema description already tells the LLM when and
how to use it. A SKILL.md would only add redundant prompt context.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: include HTTP status in web_fetch output
The LLM had no way to distinguish a 404 error page from a 200 success.
Including status in the structured output (alongside url/title/content/
word_count) lets the agent report failures correctly and matches the
behaviour of the http tool which always returns status.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* feat(web_fetch): add Chrome UA and safe redirect following
- Set a Chrome-like User-Agent so sites that block the default reqwest
string return real content instead of bot-rejection pages.
- Add Accept: text/markdown, text/html header (mirrors OpenClaw).
- Follow up to 3 redirects manually instead of blocking all 3xx.
Every Location URL is run through validate_url() before the next
request is sent, so SSRF protection applies to every hop identically
to how it applies to the original URL.
- Resolve relative Location values against the current URL before
SSRF-validating them.
- Log each followed hop at DEBUG level.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(web_fetch): expose final_url after redirect following
When redirects are followed, the original `url` field no longer
reflects where the content actually came from. Add `final_url` so
the LLM can cite the canonical source correctly. Equals `url` when
no redirects occurred.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(web_fetch): address review comments and fix CI failures
- Store LeakDetector in WebFetchTool struct (init once in new(), not per execute() call)
- Use self.leak_detector for both outbound scan and redirect re-validation
- Simplify HTML/cfg blocks to reduce duplication (gemini-code-assist suggestion)
- Fix pub use ordering in mod.rs (cargo fmt)
- Add web_fetch to core_registration_covers_expected_tools snapshot test
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* feat(web): DB-backed Jobs tab, scheduler-dispatched local jobs, remove active-jobs-bar
- Remove active-jobs-bar UI element (HTML, CSS, JS polling)
- Move job handlers from server.rs to handlers/jobs.rs
- Remove user_id scoping (single-user gateway)
- Add list_agent_jobs() and agent_job_summary() to Database trait
(both postgres and libsql backends) for non-sandbox job visibility
- Wire SchedulerSlot into CreateJobTool so execute_local dispatches
via scheduler (persists to DB + spawns worker) instead of creating
phantom ContextManager-only jobs
- Update /status and /list slash commands to read from DB for
consistency with Jobs tab
- Fix worker mark_completed: skip if already terminal or stuck
- Add agent job cancel via DB update in both web handler and slash cmd
- Add Stuck → Completed guard with tracing in worker completion path
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: address PR review comments
- Log warning when get_context fails in worker completion path
- Extract duplicated status-counting logic into AgentJobSummary::add_count()
helper, used by both postgres and libsql backends
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Nick Pismenkov <[email protected]>
* feat(bootstrap): auto-detect libsql when ironclaw.db exists
If DATABASE_BACKEND is unset after loading all env files and
~/.ironclaw/ironclaw.db exists, default to libsql automatically.
Fixes the chicken-and-egg problem on cloud instances where no
DATABASE_URL is configured: users no longer need to prefix every
ironclaw command with DATABASE_BACKEND=libsql.
Priority order: explicit env var > .env > ~/.ironclaw/.env > auto-detect
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(bootstrap): move env loading to sync main() before tokio runtime
- Fix cargo fmt: wrap three long assert! lines in new tests
- Address set_var data race: load_ironclaw_env() is now called from a
synchronous fn main() wrapper before the Tokio runtime starts, making
the set_var call provably safe (no worker threads exist yet)
- Remove the redundant dotenvy::dotenv() + load_ironclaw_env() calls
from inside command handlers and agent startup (already done pre-tokio)
- Update SAFETY comment to reflect the actual invariant
Addresses Gemini code review comment and cargo fmt CI failure on PR #399.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* feat(web): slash command autocomplete, /status /list /cancel, fix input locking
Backend:
- Add JobStatus, JobList, JobCancel Submission variants to submission.rs
- Parse /status [id], /progress [id], /list, /cancel <id> as control commands
- Dispatch to existing handle_check_status/handle_list_jobs/handle_cancel_job
handlers via new process_job_status/process_job_list/process_job_cancel methods
- Add 4 parser tests (34 total, all passing)
Web UI:
- Add slash command autocomplete: type / in chat input to see all 18 commands
with descriptions; arrow-key navigation, Tab/Enter to select, Escape to close
- Remove chat input locking: drop textarea.disabled + sendBtn.disabled so users
can always type and send (including /interrupt while agent is processing)
- Remove quick-action toolbar buttons (↩↪⏸⊖🗑📋) added in previous session
- Remove dead #chat-status bar (min-height 28px black bar always visible when empty)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor: address PR review comments
- Remove Submission::JobList variant; parse /list directly as
JobStatus { job_id: None } (simpler, eliminates redundant enum
variant, match arm, is_control branch, and wrapper function)
- Cache autocomplete matches in _slashMatches to avoid re-filtering
SLASH_COMMANDS on every keydown while autocomplete is open
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Pierre LE GUEN <[email protected]>
* feat(routines): deliver notifications to all installed channels
Routine notifications were silently lost because the forwarder didn't
use NotifyConfig fields and WASM channels (Telegram, Slack) had
broadcast() as a no-op. This fixes three issues:
1. send_notification() now includes notify_user/notify_channel in
metadata so the forwarder can route to specific channels
2. The routine forwarder mirrors the heartbeat pattern: try targeted
channel first, fall back to broadcast_all
3. WasmChannel implements broadcast() using last-seen message metadata
(chat_id), with persistence to the settings table so it survives
restarts. Only writes to DB when the value actually changes.
Heartbeat notifications also benefit from the WASM broadcast fix.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(wasm): extract do_update_broadcast_metadata to eliminate duplication
The inline metadata-update block in `dispatch_emitted_messages` was
identical to the `update_broadcast_metadata` instance method. Extract
the shared logic into a private free function `do_update_broadcast_metadata`
that both call, so the persistence logic lives in one place.
Addresses Gemini code review comment on PR #398.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Routines created via Telegram (or any WASM channel) were invisible in the
web UI because the routines list endpoint filtered by GATEWAY_USER_ID,
which didn't match the Telegram user's ID stored on the routine.
Add list_all_routines() to the RoutineStore trait (both libSQL and
PostgreSQL backends) and use it in the web dashboard handlers so all
routines are visible regardless of which channel created them.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* test: add failing tests for Discord signature validation and capabilities alias (Red phase)
TDD Red phase for #148. Adds 19 tests across 4 categories:
- Category 1: CredentialLocationSchema header_name alias (2 failing)
- Category 2: Ed25519 signature verification (3 failing)
- Category 3: Router signature key management (2 failing)
- Category 5: Discord capabilities public_key setup (1 failing)
All 8 failures are expected — stubs return false/None by design.
Implementation will follow in Green phase.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add Discord Ed25519 signature verification and capabilities alias (#148)
Implement the Green phase for Discord channel security fixes:
- Add real Ed25519 signature verification in signature.rs using ed25519-dalek
- Add #[serde(alias = "header_name")] to CredentialLocationSchema::Header
for backward compatibility with external JSON files
- Add signature_keys storage to WasmChannelRouter (register/get/unregister)
- Add discord_public_key to discord.capabilities.json setup.required_secrets
- Add nested capabilities resolution to CapabilitiesFile for channel-level
JSON compatibility
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: address PR #372 review comments
- Fix invalid hex character in test fake_pub_key (router.rs)
- Simplify signature parsing with from_slice/try_from (signature.rs)
- Use idiomatic Option::or for nested capability merging (capabilities_schema.rs)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce signature verification, staleness check, key validation, recursive resolve
Address PR #372 review feedback:
- Wire verify_discord_signature() into webhook_handler with Ed25519
signature + timestamp staleness check (5s window via now_secs param)
- Validate Ed25519 keys in register_signature_key() (hex decode +
VerifyingKey::try_from) before storing, return Result<(), String>
- Recursively resolve nested capabilities in resolve_nested()
- Add 25 new tests: 8 staleness, 6 key validation, 7 webhook
integration (tower::oneshot), 4 resolve_nested edge cases
- Fix pre-existing clippy warning in signal.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: wire register_signature_key() into all channel loading paths
The Ed25519 signature key registration was implemented and tested but
never called from production code. All three channel loading paths
(setup_wasm_channels, activate_wasm_channel, refresh_active_channel)
now read the public key from the secrets store and register it with
the webhook router, enabling Discord signature verification.
Adds `signature_key_secret_name` field to WebhookSchema so channels
can declare which secret contains their Ed25519 public key.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* Add automated QA: tool schema validator, feature-flag CI matrix, Docker build
P0 items from the automated QA plan (#352):
- Add validate_tool_schema() that checks OpenAI strict-mode rules
(type: object, required keys in properties, nested object/array
recursion) with 10 unit tests and 6 integration tests covering
all core built-in tools
- CI test matrix now runs with --all-features, default features, and
--no-default-features --features libsql to catch dead code behind
wrong cfg gates
- CI clippy now runs the same 3-feature matrix with --all flags
- Docker build job added to catch missing files in Dockerfile
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add P1 automated QA tests and fix LeakDetector prefix shadowing bug
P1 test coverage: config round-trip (settings + bootstrap), shell tool
arg handling, safety adversarial tests (sanitizer, leak detector,
allowlist), turn persistence (conversations, metadata, pagination, jobs),
and a clippy fix for libsql-only builds.
Fixed a real bug where AhoCorasick non-overlapping prefix iteration
caused shorter prefixes (e.g. "sk-") to shadow longer ones
(e.g. "sk-ant-api"), preventing Anthropic API key and SSH private key
detection.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add P2 automated QA tests: chaos, lifecycle, collision, and recovery
Cover all P2 items from the automated QA plan:
- Circuit breaker chaos tests (hanging provider, rapid cycles, mixed errors)
- Failover chaos tests (hanging failover, all-fail, tools path, single provider)
- Value estimator boundary tests (negative cost, zero price, zero earnings)
- Context length recovery test (ContextLengthExceeded -> compact -> retry)
- WASM channel lifecycle tests (write/commit/read round-trip, namespace isolation)
- Extension registry collision tests (same-name different-kind coexistence)
- Extension filesystem collision tests (separate dirs, detect_kind priority)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add P3 concurrent stress tests for ContextManager and SessionManager
Tests verify thread safety of double-checked locking, TOCTOU
prevention, and RwLock-based concurrent access patterns under load.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add dispatcher loop guard and self-repair stuck job tests
Dispatcher: test force_text mechanism prevents infinite tool call loops,
verify iteration bound arithmetic guarantees termination for all configs.
Self-repair: test stuck job detection, recovery within attempt limits,
manual escalation when limit exceeded, graceful degradation without
store/builder dependencies.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add E2E testing infrastructure design doc
Python + Playwright framework with mock LLM server for deterministic
browser-level testing of the web gateway. Covers connection/auth,
chat round-trip with SSE streaming, and skills lifecycle scenarios.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add E2E testing infrastructure implementation plan
10-task plan covering: scaffolding, mock LLM server, helpers,
conftest fixtures, connection/chat/skills test scenarios,
CI workflow, README, and integration run.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* scaffold: E2E test project with pyproject.toml
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: E2E helpers with DOM selectors and port discovery
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: mock OpenAI-compat LLM server for E2E tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: E2E conftest with session fixtures for mock LLM and ironclaw
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: E2E scenario 1 -- connection and tab navigation tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: E2E scenario 2 -- chat message round-trip tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: E2E scenario 3 -- skills search, install, remove tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* ci: add weekly E2E test workflow with Playwright
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* docs: E2E test README with setup and usage instructions
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: E2E test integration fixes from first run
- Use temp file DB instead of :memory: (libSQL :memory: doesn't persist
tables across execute_batch)
- Fix installed skills selector: #skills-list not #installed-skills
- Add pytest-timeout to dependencies
- Improve skills install/remove test with wait_for instead of fixed sleeps
8 passed, 1 skipped (skills install depends on ClawHub availability)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add OpenAI strict-mode schema validator for all built-in tools (QA 1.1)
Add src/tools/schema_validator.rs with validate_strict_schema() that checks
tool parameter schemas against OpenAI function calling strict-mode rules:
type object at top level, required keys in properties, enum type consistency,
array items definitions, nested object recursion, and additionalProperties.
17 tests validate all 34+ built-in tool schemas across 5 test groups:
- 9 simple tools (echo, time, json, http, shell, file read/write/list/patch)
- 4 job tools (create, list, status, cancel)
- 4 skill tools (list, search, install, remove)
- 13 inline schemas for extension, routine, and complex job tools
- 4 memory tool schemas
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add E2E scenarios for SSE reconnect, HTML injection, and tool approval (QA 3.3/5/6)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: E2E test reliability for HTML injection and SSE reconnect
- HTML injection: test sanitization directly via JS injection instead of
depending on full LLM round-trip (avoids intermittent 404 from mock)
- SSE reconnect: increase wait times for DB persistence and relax
assertion to check total message count after history reload
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add WASM and MCP tool schema validation tests (QA 1.1)
Extends the schema validator with representative WASM tool schemas
(weather, HTTP client, batch processor, status), MCP tool schemas
(default, file read, SQL query, strict mode), and defect detection
tests for common external schema issues (missing type, typo in
required, array without items, enum type mismatch).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add auth middleware and compaction module tests
Auth middleware (8 new tests): valid/invalid bearer tokens, query param
fallback, case sensitivity, empty tokens, whitespace handling.
Compaction module (16 new tests): truncation strategy, summarize strategy
with mock LLM, workspace fallback, format_turns helper, sequential
compactions, coherence after compaction, token decrease verification.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add config round-trip integration tests (QA 1.2)
Test the full bootstrap .env lifecycle: write via the same format
as save_bootstrap_env/upsert_bootstrap_var, read back via dotenvy,
and assert values match. Covers LLM backend selection, embedding
disable flag, onboard completion flag, session token keys, multi-key
preservation across upsert, and special characters (spaces, equals,
quotes, backslashes, hashes).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add value estimator boundary tests and dispatcher loop guard (QA 4.3/4.4)
Value estimator (14 new tests): zero/negative prices, large values,
negative cost, exact margin boundaries, custom margin configuration.
Dispatcher loop guard (2 new tests): verifies the dispatch loop terminates
when all tool calls fail (regression guard for PR #252 infinite loop)
and when max iterations are reached.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add failover edge cases and provider chaos tests (QA 2.6/4.1)
Failover edge cases (4 new tests): cooldown at zero nanos, half-open
failure reopens circuit, all providers fail gracefully (no panic),
single failing provider with cooldown.
Provider chaos tests (15 new tests): flakey provider with retries,
hanging provider with timeout, garbage provider, circuit breaker
trip/recover, failover chain cascading, non-transient error stops
chain, full stack integration (retry + failover + circuit breaker).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback on QA tests
- Fix Bearer auth case-sensitivity per RFC 6750 (auth.rs)
- Refactor bootstrap.rs to expose path-parameterized variants so
config_round_trip tests call real code instead of reimplementations
- Remove deprecated event_loop fixture, use dynamic ports, minimal env,
session-scoped browser, and wire HEADED=1 in E2E conftest
- Add cross-referencing doc comments between schema validators
- Simplify array validation logic in tool.rs
- Bump e2e.yml checkout@v4 to @v6
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt and fix clippy warning in signal.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: improve E2E fixture error reporting and prevent stdin blocking
- Add --no-onboard flag to prevent wizard from blocking in CI
- Pipe /dev/null to stdin to prevent any stdin reads from hanging
- Add RUST_BACKTRACE=1 for crash diagnostics
- On server startup timeout, dump stderr to pytest output so CI
logs show why the server failed to start
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: set session-scoped event loop for E2E async fixtures
pytest-asyncio 1.3.0 defaults asyncio_default_fixture_loop_scope to
None (function scope), causing session-scoped async fixtures to be
re-evaluated per test function with independent event loops. Each test
then independently attempts to start the ironclaw server, times out
at 120s, and wastes ~24 minutes of CI before the job is cancelled.
Setting asyncio_default_fixture_loop_scope = "session" ensures all
session-scoped async fixtures share a single event loop, so the server
starts once and is reused across all tests.
Also adds -x flag to pytest in CI to stop on first failure instead of
running all 19 tests when the fixture is broken.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: set test loop scope to session to match fixture loop scope
With asyncio_default_fixture_loop_scope=session but
asyncio_default_test_loop_scope=function (the default), tests run on
a per-function event loop while fixtures produce objects (Playwright
pages, browser contexts) on the session event loop. This event loop
mismatch causes the test to hang indefinitely awaiting Playwright
operations that are bound to the wrong loop.
Setting both scopes to "session" ensures a single event loop is shared
across all fixtures and tests, eliminating the deadlock.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* ci: add roll-up jobs to match branch protection required checks
Branch protection expects "Code Style (fmt + clippy)" and "Run Tests"
status checks, but only individual job names were reported. Add
roll-up jobs that aggregate results and report the expected names.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Register boot-loaded WASM channel names with the extension manager via
set_active_channels() before set_channel_runtime() so the dedup guard
in activate_wasm_channel() is armed before the activation path becomes
available. This fixes 409 Conflict errors from the Telegram API caused
by two concurrent getUpdates polling loops.
Also fix pre-existing clippy warning in signal.rs test.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(channels/signal): add attachment upload support
- Add attachments field to OutgoingResponse for carrying file paths
- Add with_attachments() builder method to OutgoingResponse
- Update build_rpc_params() to include attachments array in JSON-RPC
- Update respond() and broadcast() to handle attachments:
- Text + attachments: sends text first, then each attachment
- Attachments only: sends each attachment with path as message
- Text only: original behavior (no change)
- Add tests for build_rpc_params with attachments
- Add tests for OutgoingResponse attachment builder
This enables the Signal channel to send files via signal-cli daemon's
JSON-RPC send method, matching the nullclaw implementation.
Risk: Low - uses existing JSON-RPC infrastructure
Tests: 85 signal tests pass, 1543 lib tests pass
* feat(tools): add message tool for cross-channel messaging
Add a new 'message' tool that allows the agent to send messages to
any connected channel (signal, telegram, slack, etc.) with optional
file attachments.
Features:
- Send messages to specific channel + target combinations
- Support for attachments (file paths)
- E.164 validation delegated to channel (signal expects +number,
telegram accepts username/chat_id, slack uses #channels)
- Helpful error messages showing available channels on failure
Tool schema:
- content: message text (required)
- channel: target channel name (optional, defaults to current channel)
- target: recipient (E.164, group ID, chat ID) (optional, defaults to
current user/group chat)
- attachments: optional file paths to send
This complements the recently added attachment upload support for the
Signal channel by giving the agent a proper way to specify attachments
when sending messages.
Tests: 4 new tests for message tool schema
Risk: Low - new tool with no breaking changes
Tests: All 1547 lib tests pass, clippy clean
* feat(llm): add conversation context to system prompt for Signal
Add conversation_context HashMap to Reasoning struct to pass channel-specific
metadata (sender phone, sender UUID, group ID) to the LLM. This helps the
agent know who/group it's talking to, preventing it from hallucinating
phone numbers or sending to wrong recipients.
Changes:
- Add conversation_context field and with_conversation_data() builder method
- Add build_conversation_section() to include current conversation info in system prompt
- Update dispatcher to extract Signal metadata (sender, sender_uuid, group) and pass to Reasoning
- Add signal_sender_uuid to Signal channel metadata for privacy mode users
* feat(tools): add secure attachment path validation with sandbox enforcement
Implement robust path validation for message tool attachments to prevent
directory traversal attacks and unauthorized file access. Attachments are
now sandboxed to ~/.ironclaw/ by default.
Key changes:
- Create shared path_utils module with validate_path() and is_path_safe_basic()
- Extract normalize_lexical() from file.rs for reuse
- MessageTool now enforces sandbox at ~/.ironclaw/ for all attachments
- Path validation includes: traversal detection, canonicalization, symlink resolution
- Error messages reveal the allowed sandbox directory for user clarity
Security improvements:
- Blocks path traversal attacks (../, URL-encoded, null bytes)
- Canonicalizes paths to resolve symlinks before validation
- Walks up to nearest existing ancestor for non-existent paths
- Prevents escape from sandbox directory
Backward compatibility:
- File tools continue to work with their configured base_dir
- Message tool defaults to ~/.ironclaw/ sandbox
- Tests updated to create files within sandbox
Tests added:
- path_utils module tests (9 tests for validation logic)
- message tool attachment validation tests
- All 1571 existing tests pass
* fix(channels/signal): use robust path validation with full security coverage
Signal channel's validate_attachment_paths() now uses path_utils::validate_path()
for consistent, secure path validation.
Fixes:
- Replaced weak path.contains('..') check with robust validate_path()
- validate_path() now includes is_path_safe_basic() as first-pass filter to
block null bytes and URL-encoded traversal sequences (%2e%2e%2f)
- Error message now shows allowed sandbox directory (~/.ironclaw/)
Security coverage:
- Path traversal: ../, foo/../bar, ../../etc/passwd ✓
- URL-encoded traversal: %2e%2e%2fetc/passwd ✓
- Null byte injection: file\0.txt ✓
- Paths outside sandbox: /tmp/evil.txt ✓
- Symlink escape attempts (via canonicalization) ✓
Tests added:
- validate_attachment_paths_rejects_path_outside_sandbox
- validate_attachment_paths_rejects_url_encoded_traversal
- validate_attachment_paths_rejects_null_byte
- Fixed broken assertion in rejects_double_dot test
* fix(llm): add Signal channel to build_channel_section to include message tool hint
The catch-all '_' arm was returning early before the message_tool_hint
section was constructed, which meant Signal users never got the
'## Proactive Messaging' section with examples for:
- Using attachments parameter
- Targeting different users/groups
- Cross-channel messaging
Now Signal will include the full message_tool_hint section with usage examples.
* fix(tools): use async locks in register_message_tools to prevent silent failures
The method was using register_sync which calls try_write() on self.tools.
If the lock was held, try_write() would return Err and silently skip
adding the tool to the registry, while self.message_tool already held
a reference. This creates an inconsistent state.
Fix: use async write locks directly instead of register_sync to ensure
the tool is always registered or the method fails explicitly.
* refactor(dispatcher): use Channel trait for conversation context
Replace hardcoded 'if message.channel == signal' block with generic
conversation_context() method on the Channel trait. This allows any
channel to provide context (sender, group, etc.) without hardcoding
channel names.
Changes:
- Add conversation_context() method to Channel trait (default: empty)
- Implement for SignalChannel: extracts sender, sender_uuid, group
- Add get_channel() to ChannelManager (returns Arc<dyn Channel>)
- Change ChannelManager storage from Box to Arc for shared access
- Update dispatcher to use new trait method
- Add tests for conversation_context extraction
Other channels (Telegram, Slack, Discord) can now implement this
method to provide conversation context without code changes in dispatcher.
* fix(tests): split message_tool_with_attachments into sandbox and channel tests
The original test was passing for the wrong reason - it expected an error
because the channel doesn't exist, but actually failed earlier during sandbox
validation because /tmp paths are outside ~/.ironclaw/.
Split into two tests:
- message_tool_with_attachments_outside_sandbox: verifies sandbox rejection
with explicit error message check
- message_tool_with_attachments_inside_sandbox_no_channel: uses files within
sandbox (like message_tool_passes_attachment_to_broadcast does) and verifies
the channel-related error message
* security(message tool): add rate limiting, approval requirements, and audit logging
The message tool can send to ANY connected channel/target making it a significant
abuse vector if the LLM is compromised or prompt-injected. This commit adds:
1. Rate limiting: 10 messages/minute, 100/hour per user
2. Approval requirement: Always requires approval for cross-channel messages
(when channel differs from the default conversation channel)
3. Audit logging: Every successful message send is logged with channel,
target, and attachment count
The approval logic:
- If channel param is provided and differs from default -> Always require approval
- If no default channel is set and explicit channel provided -> Always require approval
- Otherwise (using default channel) -> UnlessAutoApproved
* fix(message tool): return explicit error for malformed attachments array
Previously, malformed attachments like {"attachments": [123, true]} would be
silently ignored via .ok().unwrap_or_default(), leaving users confused
when attachments weren't sent.
Now returns explicit error: "Invalid attachments format: ..."
* fix(message tool): verify attachment files exist before sending
Previously, non-existent paths would pass sandbox validation and surface
as confusing Signal RPC errors. Now returns clear "Attachment file not found" error.
* fix(test): create sandbox directory if it doesn't exist for CI
The test validate_attachment_paths_accepts_normal_paths uses
tempfile::tempdir_in() which requires the parent directory to exist.
In CI, ~/.ironclaw doesn't exist, causing test failure.
* feat(web): improve WASM channel setup flow with stepper UI and auto-configure
Streamline the WASM channel setup experience in the web gateway:
- Auto-open configure modal after installing a WASM channel
- Add progress stepper (Installed → Configured → Active) on channel cards
- Replace generic Activate button with state-specific actions (Setup, Reconfigure, Restart)
- Show "Awaiting Pairing" status for Telegram until first user is paired
- Add SSE extension_status events for real-time status updates
- Add gateway restart endpoint (POST /api/gateway/restart) with idempotency guard
- Always mount webhook routes at startup so hot-added channels work without restart
- Add pairing request polling (10s interval) on extensions tab
- Track activation errors per channel with inline error display
Includes review fixes: activation_error priority over active status, stepper
failed state rendering, restart poll timeout, configure modal double-submit
guard, and SSE sender ordering constraint documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: address PR review comments
- Move PairingStore construction outside .map() loop
- Extract createReconfigureButton() helper to reduce duplication
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Audit all built-in MCP server URLs against live endpoints. Fix 5 broken
paths (Linear, Sentry, Cloudflare, Asana, Intercom), fix 1 broken host
(GitHub), and remove 2 entries (Google Drive, Google Calendar) whose
domain mcp.google.com does not exist and Google has no official remote
MCP servers for these products.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(web): inline tool activity cards with auto-collapsing
Add Claude/Codex-style inline tool activity cards to the web UI that
show tool execution progress directly in the chat conversation.
While processing:
- Animated thinking dots with message text (e.g. "Calling LLM...")
- Individual tool cards with live spinner and elapsed timer
- Cards show tool name, duration, and expandable output preview
After response arrives:
- Activity group auto-collapses to "Used N tools (Xs)"
- Click summary to expand and see individual tool cards
- Click card header to see tool output in monospace
Also includes:
- "Calling LLM..." thinking status from dispatcher (all channels)
- 5-minute max timer guard to prevent leaks on dropped SSE
- Handles parallel tools, same tool twice, failures, thread switching
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(web): use frozen duration for completed tools in activity summary
The collapsed activity summary was showing inflated total duration
because finalizeActivityGroup() recalculated elapsed time from
Date.now() for already-completed tools. Now each tool card stores
its final duration at completion time and the summary uses that
frozen value instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: resolve_thread adopts existing session threads by UUID
When chat_new_thread_handler creates a thread directly in the session,
it doesn't register a thread_map entry. On the first message,
resolve_thread would create a duplicate thread with a different UUID,
causing:
- Thread appears empty when switching back (loadHistory queries the
original UUID but turns live on the duplicate)
- Orphaned tabs in the thread list (both the original and duplicate
appear)
Fix: before creating a new thread, check if the external_thread_id is
itself a UUID that exists as a thread in the session. If so, adopt it
and register the mapping. A mapped_elsewhere guard preserves channel
scope isolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: double-checked locking in resolve_thread UUID adoption
Re-check mapped_elsewhere after acquiring the write lock to prevent
a TOCTOU race where another task could map the same UUID between
the read lock check and write lock insertion, breaking channel
isolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Reverse log display order so the most recent entries appear at the top,
removing the need to scroll to see latest activity.
Frontend: rename appendLogEntry to prependLogEntry, use prepend() for
DOM insertion, cap oldest entries from the bottom, and auto-scroll to
top. Backend: update recent_entries() doc comment to clarify the
oldest-first return order works correctly with the frontend's prepend.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix(signal): send approval prompts to users
The Signal channel was not handling StatusUpdate::ApprovalNeeded,
causing approval requests to be silently ignored and users to
never see approval prompts.
This adds proper handling of ApprovalNeeded status that sends
a formatted message to the user with:
- Tool name and description
- Parameters (formatted as JSON)
- Request ID for reference
- Instructions on how to approve/deny/always-approve
The message uses Signal's markdown-style formatting for better
readability on mobile devices.
* feat(signal): add missing StatusUpdate handlers
Add handling for all StatusUpdate variants in Signal channel,
bringing it on par with Telegram's implementation:
- ToolStarted: Shows spinner icon when tool execution begins
- ToolCompleted: Shows checkmark/X based on success/failure
- JobStarted: Shows sandbox job start with ID and URL
- AuthRequired: Shows auth prompt with instructions and URLs
- AuthCompleted: Shows auth success/failure with optional message
This ensures Signal status feedback users receive full during
tool execution, approvals, and authentication flows, matching
the experience of Telegram and other channels.
fix(signal): address clippy warnings and improve error handling
- Collapse nested if statements into let-chains
- Fix needless borrow on Status message
- Extract send_status_message helper to reduce duplication
- Add warning logs for failed message sends
* fix(signal): suppress 'Done' status messages to user
* feat(signal): debug mode parity with REPL
- Add debug_mode to SignalChannel toggled via /debug command
- Gate ToolResult, ToolStarted, ToolCompleted behind debug mode
- Add tests: debug_mode_disabled_by_default, debug_mode_toggle, debug_mode_persists_across_toggles
* feat: add OpenRouter preset to setup wizard
Add OpenRouter as a top-level provider option in the onboarding wizard
(Step 3). Selecting it pre-fills the base URL (https://openrouter.ai/api/v1)
and prompts for an API key, avoiding manual URL entry. Under the hood it
uses the existing openai_compatible backend.
Inlines the key collection flow (rather than delegating to
setup_api_key_provider) so success messages consistently say "OpenRouter"
instead of "openai_compatible", including the early-return env-key path.
Closes#178
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address serrrfirat review comments on OpenRouter wizard preset
- Re-run path now recognizes OpenRouter: display shows "OpenRouter"
and keep-current routes to setup_openrouter() when base URL contains
openrouter.ai
- Refactor setup_openrouter() to delegate to setup_api_key_provider()
with a display_name override, eliminating ~40 lines of duplication
- Update README: remove false claim about model fetching from
OpenRouter API, add footnote explaining shared secret/env var
between OpenRouter and OpenAI-compatible
- Fix pre-existing clippy warning in settings.rs (field_reassign_with_default)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
When installing the Telegram WASM channel via the web UI, a name collision
between registry/tools/telegram.json and registry/channels/telegram.json
caused the tool entry to win, installing to ~/.ironclaw/tools/ instead of
~/.ironclaw/channels/. This made activation fail with "WASM runtime not
available".
- Add `get_with_kind()` to ExtensionRegistry for kind-aware lookup
- Use `kind_hint` parameter in `install()` to resolve collisions
- Rename tool entries to avoid future collisions: telegram → telegram-mtproto,
slack → slack-tool
- Fix `_bundles.json` stale reference (tools/slack → tools/slack-tool)
- Fix `cache_discovered()` to deduplicate by (name, kind) consistently
- Add path traversal validation to install/activate/remove entry points
- Add tests for kind-aware lookup, discovery cache, and bundle resolution
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(channels): add native Signal channel via signal-cli HTTP daemon
Implement a native Rust Signal channel that connects to a running
signal-cli daemon's HTTP endpoint, enabling Signal messaging without
WASM overhead.
Architecture:
- SSE listener at /api/v1/events for receiving messages with automatic
reconnection and exponential backoff
- JSON-RPC client at /api/v1/rpc for sending messages and typing
indicators
- Reply target tracking via Arc<RwLock<HashMap>> to route responses
back to the correct DM or group conversation
Features:
- User allowlisting supporting E.164 phone numbers, bare UUIDs, and
uuid:-prefixed identifiers (matching OpenClaw's format)
- Group allowlisting with wildcard (*) support
- Configurable story and attachment-only message filtering
- Health check via signal-cli /api/v1/check
- Broadcast support to all tracked reply targets
Configuration via environment variables:
- SIGNAL_HTTP_URL, SIGNAL_ACCOUNT (required)
- SIGNAL_ALLOWED_USERS, SIGNAL_ALLOWED_GROUPS
- SIGNAL_IGNORE_ATTACHMENTS (default: false)
- SIGNAL_IGNORE_STORIES (default: true)
Includes unit tests covering allowlist logic, envelope parsing,
recipient targeting, SSE deserialization, and edge cases.
* refactor(signal): remove expect|unwrap calls
- Change SignalChannel::new to return Result<Self, ChannelError>
- Replace .expect() on reqwest client build with proper error handling
- Replace .expect() on NonZeroUsize with compile-time const using unsafe new_unchecked
- Propagate errors through test helpers to avoid unwraps in tests
* fix(signal): prevent OOM from chunked response without Content-Length
Use bytes_stream() to check response size during download rather than
buffering entire body first. This closes the OOM vector where a
malicious signal-cli daemon could send unbounded chunked data.
* fix(signal): align is_e164 minimum digits with setup wizard
Both now require 7-15 digits after '+', preventing environment
variable bypass of the stricter onboarding validation.
* refactor(signal): extract from_parts constructor
Extract SignalChannel::from_parts() used by both new() and
sse_listener() to ensure consistent object construction.
* chore: remove redundant unused var
* refactor(signal): rename allowed_users to allow_from and add dm_policy/group_policy
- Rename allowed_users -> allow_from for consistency with other channels
- Rename allowed_groups -> allow_from_groups
- Add dm_policy field: 'open', 'allowlist', or 'pairing' (default: 'pairing')
- Add group_policy field: 'allowlist', 'open', or 'disabled' (default: 'allowlist')
- Add group_allow_from field that inherits from allow_from if empty
- Implement dm_policy and group_policy logic in message processing
- Add environment variable resolution: SIGNAL_ALLOW_FROM, SIGNAL_ALLOW_FROM_GROUPS,
SIGNAL_DM_POLICY, SIGNAL_GROUP_POLICY, SIGNAL_GROUP_ALLOW_FROM
- Add setup wizard prompts for new policy options
- Note: full pairing flow (PairingStore integration) marked as pending for future PR
* feat(signal): implement DM pairing workflow for unapproved senders
- Add PairingStore integration to check approved senders
- Handle pairing requests for unknown senders with dm_policy=pairing
- Send pairing reply message with approval instructions
- Update FEATURE_PARITY.md to reflect DM pairing support
* chore(ci): fix clippy warnings
* fix: make onboarding installs prefer release artifacts with source fallback
* fix: harden extension fallback errors and surface setup warnings
* fix: validate registry artifacts and harden fallback errors
* fix: address review feedback on installer fallback
- Add upfront validate_manifest_install_inputs() in
install_with_source_fallback so bad manifests fail fast without
relying on inner methods to catch them
- Document ALLOWED_ARTIFACT_HOSTS as GitHub-only by design
- Document intentional url omission from DownloadFailed Display
- Add channel manifest validation tests (wrong prefix rejected,
correct prefix accepted)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: require SHA256 checksum for artifact downloads
Reject artifact installs when the manifest has sha256: null instead of
warning and proceeding. This prevents installing unverified pre-built
binaries during onboarding. The check runs before downloading to avoid
wasting bandwidth.
Since InvalidManifest blocks source fallback, manifests with URLs but
no checksums will hard-fail rather than silently falling back to source
build — forcing the manifest to be fixed.
The release CI already computes SHA256 for each bundle; the manifests
just need to be populated with the actual values.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce SHA256 checksums and auto-patch manifests in CI
- Fix cargo fmt on SHA256 check code
- Reorder release CI: build WASM extensions before binary so manifests
can be patched with computed SHA256 before build.rs embeds them
- Add "Patch manifests with WASM checksums" step in build-local-artifacts
that reads checksums.txt and updates registry JSON files before building
- Add update-registry-checksums job that commits patched manifests back
to main after release, keeping the repo in sync with released artifacts
This closes the integrity gap where all manifests had sha256: null and
artifact downloads were unverified. The binary now embeds correct SHA256
values and the installer hard-rejects null checksums.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Bowen Wang <[email protected]>