* fix(ci): disambiguate WASM bundle filenames to prevent tool/channel collision
When a tool and channel share the same name (e.g. slack, telegram), the
CI build produced identical bundle filenames, causing the second to
overwrite the first. Both manifests then pointed to the wrong binary.
Prefix bundle filenames with the extension kind (tool-slack-... vs
channel-slack-...) and parse the prefix when patching manifests, so each
manifest receives the correct artifact URL and SHA256.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test(registry): add installer tests for tool/channel name disambiguation
Regression tests for the CI artifact collision fix (PR #964). Verifies:
- extract_tar_gz rejects archives with wrong wasm name (the collision bug)
- Tool bundle extracts slack-tool.wasm correctly
- Channel bundle extracts slack.wasm correctly
- Tool and channel manifests install to separate directories
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(ci): add kind validation and filter non-WASM checksum entries
- Validate .kind is "tool" or "channel" before using in build-wasm-extensions (hard error)
- Filter checksums.txt to *-wasm32-wasip2.tar.gz entries before parsing, avoiding noisy warnings from binary artifact entries in build-local-artifacts
- Add kind validation with warning+skip in both checksum-parsing loops
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt formatting in installer tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(extensions): unify auth and configure into single entrypoint
Refactors the extension lifecycle to eliminate the divergence between
chat and gateway paths that caused Telegram setup via chat to fail
(missing webhook secret auto-generation, no token validation).
Key changes:
- Rename save_setup_secrets() → configure(): single entrypoint for
providing secrets to any extension (WasmChannel, WasmTool, MCP).
Validates, stores, auto-generates, and activates.
- Add configure_token(): convenience wrapper for single-token callers
(chat auth card, WebSocket, agent auth mode).
- Refactor auth() to pure status check: remove token parameter,
delete token-storing branches from auth_mcp/auth_wasm_tool,
rename auth_wasm_channel → auth_wasm_channel_status.
- Add ConfigureResult/MissingSecret types for structured responses.
- Replace hardcoded Telegram token validation with generic
validation_endpoint from capabilities.json.
- Update all callers (9 files) to use the new interface.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: use ValidationFailed error variant instead of string matching
Replace brittle msg.contains("Invalid token") checks with a proper
ExtensionError::ValidationFailed variant. configure() now returns
this variant for token validation failures, and callers match on it
directly instead of parsing error message strings.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review — SSRF protection, error typing, missing-secret selection, WS auth
1. SSRF: call validate_fetch_url() before validation_endpoint HTTP request
2. Transport errors map to ExtensionError::Other (not ValidationFailed)
3. configure_token() picks first *missing* secret, not first non-optional
4. WebSocket error path re-emits AuthRequired on ValidationFailed
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* test: add regression tests for extension lifecycle refactoring
- test_configure_token_picks_first_missing_secret: verifies multi-secret
channels can be configured one secret at a time (commit ce106f4)
- test_auth_is_read_only_for_wasm_channel: verifies auth() has no side
effects and doesn't store secrets (commit 47f8eb6)
- test_validation_failed_is_distinct_error_variant: verifies the typed
error variant can be pattern-matched (commit a318161)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review comments — activation dispatch, dead code, caps consolidation
- Fix configure() fallthrough bug: dispatch activation by ExtensionKind
instead of unconditionally calling activate_wasm_channel() for all
non-WasmTool types (MCP servers and channel relays now use their
correct activation methods)
- Remove dead MissingSecret struct and missing_secrets field (never
populated, flagged by reviewer)
- Consolidate capabilities file parsing in configure(): parse once
and reuse for allowed names, validation_endpoint, and auto-generation
- Fix auth() doc comment: note MCP OAuth side effects
- Fix stale save_setup_secrets reference in server.rs comment
- Add regression test for activation dispatch bug
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
The Claude review step was failing ~40% of the time because:
- --allowedTools didn't include Read, Glob, Grep, Agent, causing 8-9
permission denials per run and preventing Claude from reading files
or spawning the subagents the prompt required
- Step 4 spawned N additional scoring agents per issue found, exhausting
the 50-turn budget before the PR comment could be posted
- Subagents could independently post PR comments, causing fragmented output
Fix: add missing tools to --allowedTools, merge per-issue scoring into
the review agents themselves, and add guardrails ensuring exactly one
consolidated comment is always posted.
Co-authored-by: Claude Opus 4.6 <[email protected]>
The telegram-tests, windows-build, wasm-wit-compat, and docker-build
jobs were skipped during staging CI because their `if` conditions only
matched `push` and `pull_request` events. When staging-ci.yml calls
test.yml via workflow_call, github.event_name is `schedule` (inherited
from the caller), which matched neither condition.
Invert the conditions to blocklist the one case we want to skip (PRs
targeting staging) instead of allowlisting specific events. This handles
schedule, workflow_dispatch, and any future trigger types.
Co-authored-by: Claude Opus 4.6 <[email protected]>
- Use fetch-depth: 0 in update-tag to ensure current_head SHA is available
even when staging receives new commits during the CI run
- Only merge promotion PRs targeting main; leave chained PRs open to
prevent delete_branch_on_merge from auto-closing downstream PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): use explicit features in WASM WIT compat test to avoid sqlite3 symbol conflicts
The `import` feature (added in #903) brings in `rusqlite[bundled]` which
conflicts with `libsql-ffi` — both bundle SQLite C code, causing duplicate
symbol linker errors. Use explicit features matching the test matrix instead
of `--all-features`.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: replace rusqlite with libsql in import module to fix sqlite3 symbol conflict
The `import` feature used `rusqlite[bundled]` which bundled its own SQLite
C code, conflicting with `libsql-ffi` (also bundles SQLite). This caused
duplicate `sqlite3_*` symbol linker errors when both features were enabled
via `--all-features`.
Replace `rusqlite` with `libsql` (already a dependency) in the import
reader. The `import` feature now implies `libsql`. This eliminates the
duplicate symbol conflict and allows `--all-features` to compile cleanly.
Also restores `--all-features` in the WASM WIT compat CI test (now safe)
and converts all import test helpers from rusqlite to libsql.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: apply cargo fmt formatting fixes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Keep staging versions for all registry JSON files (sha256: null) and
LLM module helpers. CHANGELOG.md and Cargo updates from main applied.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: address staging-ci-review issues (batch 1)
- #811: Fix unreachable error handling in worker — restructure .await?
to explicit match on nested Result so token budget errors are properly
logged and marked as failed
- #813: Combine metadata + token budget into single update_context()
call to prevent concurrent worker observing partial state
- #814: Persist max_tokens and total_tokens_used to both PostgreSQL and
libSQL backends — add V12 migration, update save_job/get_job
- #815: Cap user-supplied max_tokens at configured max_tokens_per_job
to prevent budget bypass via metadata injection
- #869: Release locks before async I/O in webhook handler (http.rs) and
SIGHUP handler (main.rs) to prevent blocking concurrent requests
Fixes: #811, #813, #814, #815, #869
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #883 review feedback
- Fix min(user_val, 0) bug: guard for unlimited config (max_tokens_per_job == 0)
- Remove duplicate columns from libSQL base SCHEMA (v12 migration is sole source)
- Use get_i64() helper for consistency in libsql/jobs.rs
- Add regression tests for scheduler token budget capping
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(registry): version-pinned WASM artifact URLs + ChecksumMismatch fallback (#439)
Root cause: all artifact URLs used releases/latest/download/, which is a
moving target. Every release rebuilds all WASM extensions non-deterministically,
so sha256 baked into an older binary diverges from the content at 'latest'.
ChecksumMismatch was also a hard block with no source-build fallback.
Three-layer fix:
1. src/registry/installer.rs — allow source-build fallback for ChecksumMismatch
on releases/latest URLs (moving-target artifact rotation, not tampering).
Version-pinned URLs (releases/download/vX.Y.Z/) remain a hard block.
Adds regression test (test_source_fallback_on_latest_url_mismatch) and
updates test_should_attempt_source_fallback_policy to cover both URL types.
2. .github/workflows/release.yml — three CI changes:
- build-wasm-extensions: version-detect, skip-if-unchanged, versioned filenames
(name-{version}-wasm32-wasip2.tar.gz). Skip rebuild when manifest already has
a non-null sha256 and the URL embeds the current version — stable checksums
until source actually changes.
- build-local-artifacts: patch manifests with version-pinned URL + sha256 (for
binary embedding via build.rs).
- update-registry-checksums: same URL patching for the main-branch PR.
All three sed patterns use '.*' (greedy) to correctly handle pre-release
version strings like 0.1.0-alpha.1.
3. registry/{tools,channels}/*.json — null out all 14 stale sha256 values.
Null sha256 -> MissingChecksum -> source-build fallback (works on all binaries).
Next release CI will populate version-pinned URLs + stable checksums.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* style: cargo fmt
* fix(ci): use JSON filename stem for WASM bundle names to fix manifest lookup
Manifests like registry/tools/slack.json have name='slack-tool', causing
the patching step to look for registry/tools/slack-tool.json (missing).
Introduce file_stem (JSON filename without .json) for the bundle filename
and checksums.txt entry, while keeping ext_name (manifest .name) for archive
contents — the installer extracts files by manifest.name so those must still
match. The patching step strips -{version}-wasm32-wasip2.tar.gz from the
filename stem and looks up registry/tools/slack.json correctly.
* fix(registry): tighten fallback URL check + deduplicate tests
Address PR review feedback:
1. Make should_attempt_source_fallback check repo-specific
(github.com/nearai/ironclaw/releases/latest/) instead of a
generic substring (/releases/latest/download/).
2. Remove duplicate ChecksumMismatch cases from
test_should_attempt_source_fallback_policy — that coverage
lives in the dedicated regression test
test_source_fallback_on_latest_url_mismatch.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* refactor: centralize test credential constants into testing::credentials
Scattered test credential strings (API keys, OAuth tokens, crypto keys,
Telegram tokens, session tokens) across ~25 files made security auditing
harder and created unnecessary duplication. Centralize all test-only fake
credentials into a new `src/testing/credentials.rs` module with named
constants and a shared `test_secrets_store()` helper.
- Convert `src/testing.rs` to directory module (`src/testing/mod.rs`)
- Add `src/testing/credentials.rs` with ~30 named constants
- Replace hardcoded literals in 24 source files
- Deduplicate `test_store()` helper (was copy-pasted in 3 files)
- Leave leak_detector/shell/signature tests as-is (inline values
aid readability for pattern detection tests)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: replace real Telegram bot token with obviously fake test stub
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* Update src/testing/credentials.rs
Co-authored-by: Copilot <[email protected]>
* Update src/testing/credentials.rs
Co-authored-by: Copilot <[email protected]>
* refactor: address PR review feedback on test credentials
- Fix TEST_CRYPTO_KEY doc comment ("32-byte hex" → "32-character key string")
- Rename confusing "real"/"fake" Anthropic constant names and values
- Change TEST_STRIPE_KEY from "sk-live" to "sk_test_fake123" to avoid scanners
- Use test_secrets_store() helper in orchestrator and http tool tests
- Clarify config_round_trip.rs doc comment about integration test visibility
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Copilot <[email protected]>
* Revert "Feat/docker shell edition (#804)"
This reverts commit c566faf28f.
* style: fix formatting issues from revert
Run cargo fmt to fix formatting across 7 files after the revert of
the docker shell edition feature.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: persist user_id in save_job and expose job_id on routine runs (#709)
* feat: persist worker events to DB and fix activity tab rendering
In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.
Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire RoutineEngine into gateway for direct manual trigger firing
Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.
Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove stale gateway_state argument from Agent::new test call sites
The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — restore sandbox source filter, remove blank lines
- Revert removal of `source = 'sandbox'` filter in all SandboxStore
queries (8 sites across PG and libSQL). Sandbox-specific APIs should
stay scoped to sandbox jobs; unified job listing for the Jobs tab
should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
formatting CI failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review — regenerate Cargo.lock, add user_id regression test
- Regenerate Cargo.lock from main's lockfile to eliminate dependency
version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
and get_job in the libSQL backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: remove trailing blank line in libsql jobs.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add Postgres-side regression test for user_id persistence in save_job
Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(llm): per-provider unsupported parameter filtering (#749, #728) (#809)
Add declarative `unsupported_params` field to provider definitions in
providers.json. Parameters listed are stripped from requests before
sending, preventing 400 errors from providers that reject them (e.g.
gpt-5 family and kimi-k2.5 rejecting custom temperature values).
- Add `unsupported_params` to ProviderDefinition and RegistryProviderConfig
- Propagate from registry through config resolution
- Generic strip helpers handle temperature, max_tokens, stop_sequences
- Apply filtering in RigAdapter and AnthropicOAuthProvider
- Mark openai and tinfoil providers as unsupporting temperature
- Update openai default model to gpt-5-mini
Co-authored-by: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Cherry-pick of #802: run fmt + clippy on staging PRs, skip Windows clippy,
simplify claude-review trigger to labeled-only.
Co-authored-by: Claude Opus 4.6 <[email protected]>
- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)
Co-authored-by: Claude Opus 4.6 <[email protected]>
Cherry-pick of #794: remove continue-on-error hack, skip redundant checks
on staging PRs, allow ironclaw-ci[bot] in Claude Code review.
Co-authored-by: Claude Opus 4.6 <[email protected]>
- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs
Co-authored-by: Claude Opus 4.6 <[email protected]>
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* feat(ci): chained promotion PRs with multi-agent Claude review [skip-regression-check]
Staging CI workflow with batched promotion PRs:
- Creates staging-promote/<sha> branches per batch
- Chains PRs onto previous promotion branch (incremental diffs)
- Claude Code reviews only the incremental changes per batch
- Blocked PRs stay open as records of findings
- staging-tested tag advances regardless of gate outcome
- Runs every 60 min on cron + manual dispatch
Multi-agent Claude review (Sonnet orchestrator + Haiku agents):
- 4 parallel Sonnet review agents (security, architecture, bugs, performance)
- Haiku agents for severity/confidence scoring
- [SEVERITY:CONFIDENCE] output format
- Severity/confidence matrix for issue creation and gate blocking:
CRITICAL: always create issue, block if confidence >=80
HIGH: create issue if confidence >=50
MEDIUM/LOW: create issue if confidence >=80
* feat(wasm): lazy schema injection on WASM tool errors
When a WASM tool returns an error (ToolReturnedError), call the module's
description() and schema() WIT exports and append them as a hint in the
error message. This lets the LLM retry with correct parameters without
us including large schemas in every request's tools array.
- Change ToolReturnedError from tuple to struct variant with hint field
- Add build_tool_hint() that calls WASM description()/schema() exports
- Cap description at 500 chars, schema at 3000 chars to limit context
- Hint flows automatically through Display → ToolError → ChatMessage
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: use floor_char_boundary for UTF-8 safe truncation in tool hints
Use existing crate::util::floor_char_boundary() to avoid panicking
when truncation lands mid-multibyte character. Addresses review
feedback on PR #638.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* perf: build system prompt once per turn, skip tools on force-text, fix nudge role (#565)
Three fixes to agentic loop prompt handling:
1. Build system prompt once per turn instead of every tool iteration.
`build_system_prompt_with_tools` is now pub; callers pass the result
via `ReasoningContext::system_prompt` to avoid rebuilding ~1,500 tokens
per iteration.
2. Skip `## Available Tools` section when `force_text = true`. The
dispatcher passes a no-tools prompt variant on the final iteration,
saving ~460 tokens and removing misleading instructions.
3. Change nudge message from `Role::System` to `Role::User`. A second
system message mid-conversation is unsupported by most providers.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: revert nudge role change to keep ChatMessage::system
Copilot review correctly identified that using Role::User for the nudge
breaks compact_messages_for_retry, which uses rposition for Role::User
to find the last real user message. Role::Assistant would cause
back-to-back assistant messages. Since no production issues were reported
with the original system role, revert to ChatMessage::system.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review — omit tool guidance when tools empty, rename shadowed var
- Conditionalize "Call tools…" guidelines and "## Tool Call Style" section
in the system prompt so they are only included when tools are non-empty.
Previously the force-text (no-tools) prompt still contained misleading
tool-calling instructions. (Copilot review comment)
- Rename `system_prompt` → `cached_prompt` in dispatcher to avoid shadowing
the earlier workspace identity `system_prompt` variable. (Copilot review)
- Add regression tests: `test_system_prompt_with_tools_contains_tool_guidance`
and extended assertions in `test_system_prompt_without_tools_omits_tools_section`.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: [email protected] <[email protected]>
Reverts the checksums added in fe4c3c5. The baked-in checksums cause
production failures when the host binary's WIT version doesn't match
the artifacts at /releases/latest/ — WASM tools (web-search) and
channels (telegram) fail with "matching implementation was not found
in the linker".
Setting sha256 back to null unblocks the runtime install path
(ExtensionManager doesn't validate checksums) and allows the next
release-plz run to publish matching host + artifact pairs.
[skip-regression-check]
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Three related fixes for reasoning model artifacts (GLM-4/5, DeepSeek R1, Qwen3):
1. reasoning_content no longer leaks into tool-call assistant messages
in nearai_chat — only used as fallback for final text responses.
2. plan() and evaluate_success() now apply clean_response() before JSON
parsing, preventing <think> tag prefixes from breaking plan/eval.
3. Unclosed <think> before <final> no longer discards the answer —
the strict discard path now extracts <final> content first.
8 regression tests added.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* feat(e2e): extensions tab tests, CI parallelization, and 3 bug fixes
## E2E test coverage
- Add tests/e2e/scenarios/test_extensions.py with 57 tests covering all
extensions tab flows: installed WASM tool/MCP/channel cards, configure
modal (open, fields, cancel, save, OAuth, error), auth card (token,
OAuth, submit, cancel, error, multi-extension coexistence), activate
flow, install/remove flows, WASM channel stepper states, and tab reload
behaviour. All network calls intercepted via page.route() — no real
binaries or external registries needed.
- Expand tests/e2e/helpers.py with 50+ new CSS selectors for the
extensions tab UI.
- Add tests/e2e/README.md documentation on the page.route() mocking
pattern, LIFO handler ordering, and page.evaluate() injection.
## CI parallelization
- Split .github/workflows/e2e.yml into a build job (compile once,
upload artifact) and a 3-way parallel test matrix (core / features /
extensions), matching the pattern in test.yml. Reduces wall-clock time
from ~15–20 min serial to ~10–12 min. Adds an e2e roll-up job for
branch protection.
## Bug fixes in app.js (found via test-driven code review)
- Fix null crash: renderExtensionCard() called ext.tools.length without
a null guard; add ext.tools && check (regression: test_ext_tools_null).
- Fix modal UX: submitConfigureModal() closed the overlay before checking
success, making failures unrecoverable without reopening; close only on
success, re-enable buttons and keep modal open on failure
(regression: test_configure_modal_stays_open_on_save_failure).
- Fix URL injection: all window.open() calls for server-supplied auth_url
now go through openOAuthUrl() which rejects non-HTTPS schemes
(regression: test_oauth_url_injection_blocked).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* refactor(e2e): prune extensions tests 57→46 by merging redundant setups
Merge 11 tests that shared identical fixture+navigation overhead:
- Group A: 3 empty-state tests → test_extensions_empty_tab_layout
- Group B: card_renders absorbs ext_tools_list_shown (same _WASM_TOOL fixture)
- Group B: auth_dot_unauthed + unauthed_shows_configure_btn → test_installed_wasm_tool_unauthed_state
- Group D: installed + configured states → test_wasm_channel_setup_states (identical UI)
- Group D: failed_state + stepper_failed_circle → test_wasm_channel_failed_renders
- Group G: 5 field badge tests → test_configure_modal_field_variants (4 fields, one pass)
- Group H: submit_success + enter_key_submits → test_auth_card_submit_success
Coverage preserved: all assertions kept, no unique behaviors removed.
Extensions CI job estimated to drop from ~7 min to ~5 min.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(e2e): fix configure_input selector scoping in merged field variants test
modal.locator(".configure-modal input[type='password']") scoped the absolute
selector inside .configure-modal, effectively searching for a nested
.configure-modal which never exists → count() == 0. Use page.locator()
instead, consistent with all other tests in the file.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(e2e): address PR review comments — replace fixed sleeps with deterministic waits
- Remove unnecessary wait_for_timeout(1000) from test_remove_cancelled_keeps_card
(window.confirm = () => false is synchronous; DOM is unchanged when click() returns)
- Replace wait_for_timeout(800) with wait_for_function() for window._lastOpenedUrl
checks in configure_modal_save_oauth and activate_with_auth_url_opens_popup
- Replace wait_for_timeout(300) with nth(1).wait_for(visible) in
test_auth_card_multiple_extensions_coexist
- Remove wait_for_timeout(800/300) in test_auth_card_submit_empty_noop and
test_auth_completed_sse_dismisses_card (both check synchronous JS side-effects)
- Add comment in test_oauth_url_injection_blocked explaining why timeout is kept
(negative assertion — cannot use wait_for_function for absence of event)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(e2e): address remaining PR review comments
- Remove unused `import pytest` from test_extensions.py
- Fix unawaited coroutine bug: convert lambda route handlers to async def
in test_extensions_tab_reloads_on_revisit and
test_auth_completed_sse_triggers_extensions_reload (lambda r: r.fulfill(...)
returns an unawaited coroutine; requests silently fell through to real server)
- Fix README.md example to use async def handler (same bug in docs)
- Harden openOAuthUrl() in app.js: use URL constructor instead of
.startsWith() so non-string server-supplied values (objects, null, etc.)
are safely rejected rather than throwing TypeError
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(e2e): address second round of PR review comments
- Add timeout-minutes to CI build job to prevent hung workflows
- Use parsed.href instead of raw url in openOAuthUrl for safety
- Remove unused MessageEvent variable in auth_completed test
- Replace wait_for_timeout(800) with expect_response in activate test
- Replace wait_for_timeout(300) with tab panel wait_for in reload test
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
coverage/ matched tests/fixtures/llm_traces/coverage/, causing
release-plz to detect committed+ignored files and abort on every push
to main. PR #561 has been stuck with only 1 changelog entry since v0.15.0.
Anchor the rule to the repo root with /coverage/ so it only ignores the
top-level coverage report directory generated by cargo llvm-cov, not
nested fixture directories.
[skip-regression-check]
Add Google Discovery Service URLs to all 6 Google WASM tool
descriptions so the LLM can fetch full API documentation on demand
using its built-in HTTP tool. Discovery API is public and requires
no authentication.
URLs added:
- Gmail: googleapis.com/discovery/v1/apis/gmail/v1/rest
- Calendar: calendar-json.googleapis.com/$discovery/rest?version=v3
- Drive: googleapis.com/discovery/v1/apis/drive/v3/rest
- Docs: googleapis.com/discovery/v1/apis/docs/v1/rest
- Sheets: googleapis.com/discovery/v1/apis/sheets/v4/rest
- Slides: googleapis.com/discovery/v1/apis/slides/v1/rest
[skip-regression-check]
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: sort tool_definitions() for deterministic LLM tool ordering
HashMap iteration order is non-deterministic, causing the LLM to receive
tools in different orders across calls. Sort alphabetically by name to
eliminate position bias in tool selection.
Closes#566
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: use sort_unstable_by for tool definitions ordering
Stable sort is unnecessary since tool names are unique. Unstable sort
avoids the overhead of preserving equal-element order.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: repair bad merge in registry.rs (missing closing brace and test attribute)
The merge of main into fix/sort-tool-definitions dropped the closing `}`
of test_tool_definitions_sorted_alphabetically and the `#[tokio::test]`
attribute on test_retain_only_filters_tools, causing an unclosed delimiter
parse error that failed all CI jobs.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat: route OAuth callbacks through web gateway for hosted instances
On hosted instances (e.g., NEAR AI), OAuth callbacks can't reach the
local TCP listener on port 9876. This adds a gateway-routed OAuth flow
that works behind reverse proxies and load balancers.
Backend changes:
- Add /oauth/callback as a public route on the web gateway
- PendingOAuthFlow registry shared between ExtensionManager and handler
- Gateway mode auto-detected via IRONCLAW_OAUTH_CALLBACK_URL env var
- Platform state format (instance:nonce) for nginx routing
- Token exchange proxy support via IRONCLAW_OAUTH_EXCHANGE_URL
- Local TCP listener mode preserved as backward-compatible fallback
UX improvements:
- Hide Configure button for tools with auto-resolved OAuth credentials
(builtin defaults or platform-injected env vars)
- Skip client_id/client_secret fields in setup schema when auto-resolved
- Show Reconfigure only after successful authentication
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(oauth): harden gateway callback and refactor AuthResult
- Add 60s timeout to exchange_via_proxy HTTP client (matching exchange_oauth_code)
- Read GATEWAY_AUTH_TOKEN once at ExtensionManager construction instead of
per-flow from env (prevents coupling and clarifies token provenance)
- Extract oauth_error_page() helper to deduplicate error landing pages
- Remove IRONCLAW_FORCE_GATEWAY_CALLBACK env var (auto-detection suffices)
- Refactor AuthResult into typed AuthStatus enum with constructors,
eliminating stringly-typed status and Option fields that were always None
- Adapt all handlers (chat, extensions, ws) to new AuthResult/AuthStatus API
- Use setup_url (not validation_endpoint) for awaiting_token responses
[skip-regression-check]
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix(oauth): address review feedback — empty token guard, test flakiness, doc typos
- Fail early in exchange_via_proxy() when gateway_token is empty instead
of sending an unauthenticated request to the exchange proxy
- Fix test_oauth_callback_strips_instance_prefix to use an expired flow
so it never attempts a real HTTP token exchange (prevents CI flakiness)
- Fix doc comments: /auth/callback → /oauth/callback in PendingOAuthFlow
and ExtensionManager pending_oauth_flows docs
[skip-regression-check]
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* fix: clarify strip_instance_prefix safety, wrapper credential fix, test assertion
- Add comment to strip_instance_prefix noting nonces are base64url (no colons)
- Expand wrapper.rs comment explaining the credential_user_id bug fix
- Fix test_oauth_callback_strips_instance_prefix assertion: landing_html
does not include provider_name on error pages
[skip-regression-check]
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(web): show error details and input params for failed tool calls
Failed tool calls in the gateway UI previously showed only a red X icon
with an empty expandable body. This change:
- Adds optional `error` and `parameters` fields to `ToolCompleted` SSE
events so the browser receives failure details in real-time
- Auto-expands failed tool cards to make errors immediately visible
- Adds `StatusUpdate::tool_completed()` constructor that centralizes
the 5 duplicated construction sites and applies `redact_params()` to
prevent sensitive values (e.g. secret_save's "value" param) from
leaking through SSE broadcasts
- Adds `sensitive_params()` trait method to `Tool` for declaring which
parameters must be redacted before logging, hooks, and UI display
- Adds `redact_params()` utility and wires it through hooks, approvals,
ActionRecord storage, and debug logs in dispatcher/worker
- Adds `SecretListTool` and `SecretDeleteTool` for LLM-driven secret
management (values never returned, only names/metadata)
- Fixes auth flow: setup-only extensions show configure modal instead
of OAuth card; auth_completed SSE dismisses both UI paths
- CI: release workflow creates PR instead of pushing directly to main
- Registry: MissingChecksum error enables source fallback for
bootstrapping when checksums haven't been populated yet
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: apply cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: keep original params in PendingApproval for execution, redact only for display
Address two PR review comments:
1. execute_chat_tool_standalone now redacts sensitive params before logging,
matching the pattern already used in worker.rs.
2. PendingApproval previously stored redacted parameters, which meant
approved tool calls received "[REDACTED]" instead of the actual values.
Add a display_parameters field for UI/logs and keep parameters as the
original values used for execution.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review comments
- worker.rs: redact sensitive params before BeforeToolCall hook, matching
dispatcher.rs — hooks in the autonomous job path now receive redacted
params instead of raw values
- registry.rs: fix docstring for register_secrets_tools (list, delete,
not save/list/delete — no SecretSaveTool is registered)
- app.js: fix double toast/loadExtensions in submitConfigureModal —
for non-OAuth success the auth_completed SSE already handles both,
so skip them in the HTTP response handler to avoid duplicates
[skip-regression-check]
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Swap the order of import_from_directory() and seed_if_empty() so that
custom workspace templates from WORKSPACE_IMPORT_DIR take priority
over generic seeds. Previously, seed_if_empty() ran first and created
all default files, causing import_from_directory() to skip everything
since the files already existed in the DB.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat: add OAuth support for WASM tools in web gateway
Extract reusable OAuth functions (build_oauth_url, exchange_oauth_code,
store_oauth_tokens, validate_oauth_token) from CLI into shared
oauth_defaults module, then wire them into the web gateway's
ExtensionManager.
Key changes:
- Install auto-activates WASM tools (no separate Activate button)
- Configure button triggers OAuth flow via save_setup_secrets
- Scope merging: installing a second Google tool triggers re-auth with
merged scopes from all tools sharing the same secret_name
- Cancel-and-retry: aborting stale OAuth listeners prevents port conflicts
- Post-auth validation: wrong account detected via validation_endpoint
- Reconfigure always re-auths (deletes old token before starting fresh)
- UI shows error toast on OAuth failure, refreshes extension list
Flow: Install → Active → Configure (enter client_id/secret) → Save →
OAuth popup → authorize → done. Second Google tool install auto-triggers
scope expansion OAuth.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review comments
- Add custom headers support to ValidationEndpointSchema (fixes
missing Notion-Version header regression)
- Guard activate handler auth check with status == "awaiting_authorization"
to prevent unexpected OAuth popups
- Add window dimensions to OAuth popup in activateExtension()
- Simplify UTF-8 truncation boundary check
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address Copilot PR review comments (security, UX, bugs)
- Add CSRF state parameter to OAuth flow (random state in auth URL, validated in callback)
- Restore MCP server Activate button in web UI (was hidden for all non-channel extensions)
- Abort JoinHandle in cleanup_expired_auths to prevent port 9876 conflicts
- Fix Google-specific error message for non-Google OAuth providers
- Add has_auth field to ExtensionInfo API response (fixes Configure button visibility)
- Use oauth_defaults::callback_url() instead of hardcoded redirect_uri (both CLI and manager)
- Update auth check comment to match actual behavior (scope expansion + first-time auth)
- Add unit tests for build_oauth_url (basic, PKCE, extra params, state uniqueness)
- Check all required setup secrets (client_id + client_secret) before starting OAuth
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(web): fix jobs UI parity for non-sandbox mode
The web gateway Jobs UI was built primarily for sandbox (Docker) jobs.
When running without sandbox (common for NEAR AI hosted envs), multiple
features were broken. This change fixes all of them:
- Agent jobs now broadcast live SSE events to the web UI (Activity tab)
- Agent job restart via scheduler.dispatch_job (not chat message)
- Follow-up prompts for agent jobs via WorkerMessage injection
- Capability flags (can_restart, can_prompt, job_kind) in job detail API
- Rate-limit retry with cap (10 consecutive) and Retry-After header parsing
- Plan interruption on user message (breaks out of plan, re-evaluates)
- Correct SSE status field in mark_completed/mark_failed/mark_stuck
- SseManager preserved across rebuild_state calls
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: fix formatting in db/mod.rs and nearai_chat.rs
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* refactor: remove restart infrastructure and generalize Telegram-specific code
Remove the gateway restart mechanism (hot-activation works, restart won't
fix activation failures) and generalize Telegram-specific hardcoded checks
so all WASM channels get equal treatment.
Part 1 - Remove restart infrastructure:
- Remove needs_restart from ActionResponse, restart_requested from GatewayState
- Remove gateway_restart_handler, /api/gateway/restart route, exit code 75
- Remove restart overlay JS/CSS (dead code - restartGateway() never called)
- Surface actual activation errors instead of suggesting restart
Part 2 - Generalize Telegram-specific code:
- Replace telegram_owner_id: Option<i64> with generic
wasm_channel_owner_ids: HashMap<String, i64> (backwards-compatible
via TELEGRAM_OWNER_ID env var)
- Pairing status check now applies to all active WASM channels
- All channels get 3-step stepper in web UI, remove "coming soon" note
- Remove dead setup_telegram() code (~700 lines) - Telegram's
capabilities.json declares required_secrets, so the generic
setup_wasm_channel() path handles it
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* test: add Settings::set() test for wasm_channel_owner_ids
Addresses review feedback: verify that setting per-channel owner IDs
via the dotted-path Settings::set() API works correctly with the new
HashMap<String, i64> type.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(web): refresh extension stepper after pairing approval
loadPairingRequests only refreshed the pairing section, not the
stepper status. Call loadExtensions() instead so the stepper updates
from "Awaiting Pairing" to "Active" immediately after approval.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat(workspace): add TOOLS.md, BOOTSTRAP.md, and disk-to-DB import
Add two new OpenClaw-compatible workspace markdown files:
- TOOLS.md: Environment-specific tool notes (SSH hosts, device names,
etc.) injected into the system prompt under "## Tool Notes". Seeded
as comment-only (like HEARTBEAT.md) so it's effectively empty until
the user adds real content. Not write-protected — the agent can
update it as it learns the environment.
- BOOTSTRAP.md: First-run onboarding ritual. Injected FIRST in the
system prompt when present. Guides the agent through introducing
itself, learning about the user, and updating workspace files.
Only seeded on truly fresh workspaces (no existing identity files)
to avoid triggering the ritual on existing deployments. Agent clears
it via `memory_write(target="bootstrap")` when done.
Add `Workspace::import_from_directory()` for disk-to-DB import:
- Scans a directory for *.md files and imports any that don't already
exist in the database (never overwrites user edits)
- Controlled by WORKSPACE_IMPORT_DIR env var, runs after seed_if_empty()
- Enables Docker images / deployment scripts to ship customized
workspace templates that override generic seeds
- Backwards compatible: no-op when env var is unset
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address PR review comments
- Use stable `path.extension() != Some(OsStr::new("md"))` instead of
unstable `is_none_or` (nightly-only)
- Use `tokio::join!` for concurrent DB reads in fresh-workspace check
- Skip unreadable directory entries instead of failing the entire import
- Skip unreadable files instead of failing the entire import
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
WASM tools and channels activated at runtime (via web UI or CLI) were
missing secrets store wiring, causing credential injection to silently
fail. Tools like web-search would get 401s from APIs even though the
user had configured their API key.
Four bugs fixed:
- activate_wasm_tool(): WasmToolLoader created without .with_secrets_store()
- register_wasm_from_storage(): hardcoded secrets_store: None
- WasmChannelLoader: no secrets_store field at all (added field + builder)
- activate_wasm_channel() and startup path: both missed wiring secrets
The startup path in app.rs was correct; all runtime paths now match it.
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
* 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]>
* 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]>
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(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]>
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]>
* fix: persist user message at turn start before agentic loop
Split persist_turn into persist_user_message + persist_assistant_response.
The user message is now written to DB immediately after thread.start_turn(),
before the agentic loop runs. This ensures the message survives process
crashes mid-response. The assistant response is persisted only on completion.
Updated all 6 call sites in thread_ops.rs (success, error, approval
success/error, rejection, and auth intercept paths).
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: document persist_assistant_response dependency on persist_user_message
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: apply cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: re-ensure conversation in persist_assistant_response
Add ensure_conversation call and user_id parameter to
persist_assistant_response so assistant replies are still persisted
even if persist_user_message failed transiently at turn start.
Addresses PR review feedback from @ilblackdragon.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* fix: block send until thread is selected
Prevents messages from ending up in orphan threads when user sends
while currentThreadId is null during page load.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: guard enableChatInput against null thread + add user feedback
Prevents SSE events from re-enabling input before a thread is selected.
Adds status message when user tries to send without a thread.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
When SSE auto-reconnects after a server restart, the chat now
re-syncs from the database so no messages are lost.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
The Bundled variant and its local-artifacts fallback are superseded by the
embedded registry catalog which provides WasmDownload entries with GitHub
release URLs. The in-chat extension manager now always downloads channel
WASM binaries from releases, simplifying the install path.
The setup wizard retains its own local install_bundled_channel path for
dev builds where build artifacts exist on disk.
Co-authored-by: Claude Opus 4.6 <[email protected]>