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]>
* 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]>
* fix: add job token budget, change iteration cap to Failed, fix web cancel (#698)
Jobs could enter infinite retry loops because: (1) no token budget was
enforced, (2) iteration cap marked jobs as Stuck (allowing self-repair to
restart them), and (3) the web UI cancel button only updated the DB without
stopping the running worker.
- Add `max_tokens_per_job` config (settings.json + AGENT_MAX_TOKENS_PER_JOB
env var, default 0 = unlimited) with per-job metadata override
- Track token usage after respond_with_tools() and fail the job on budget
exceeded
- Change iteration cap and persistent rate limiting from mark_stuck to
mark_failed, preventing self-repair restart loops
- Fix web cancel handler to call scheduler.stop() which updates in-memory
state AND aborts the worker task, falling back to DB-only update
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review — always persist cancel to DB, simplify token check
- Cancel handler now always persists Cancelled to DB regardless of whether
scheduler.stop() ran, fixing the edge case where stop() returns Ok(())
for jobs not in the scheduler map
- Collapse nested ifs per clippy (let-chains)
- Add NOTE comment about select_tools() not exposing TokenUsage
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: rustfmt formatting in wizard.rs (pre-existing)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* 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]>
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]>
When users authenticate via NEAR AI Cloud API key (option 4) during
onboarding, the key is stored as an env var but fetch_nearai_models()
was hardcoding api_key: None. This caused resolve_bearer_token() to
re-trigger the interactive auth prompt at step 4 (model selection).
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]>
* refactor: restructure CLAUDE.md into modular rules and add pr-shepherd command
Trim CLAUDE.md from 710 lines to 92 by moving detailed guidance into
path-scoped `.claude/rules/` files that load on demand. Add a new
`/pr-shepherd` command that consolidates the full PR lifecycle
(review, fix, quality gate, CI fix loop, merge) into one workflow.
Changes:
- CLAUDE.md: keep only essentials (build commands, code style, architecture,
module specs, config reference, debugging)
- .claude/rules/review-discipline.md: 15+ review rules, scoped to src/**/*.rs
- .claude/rules/database.md: dual-backend rules with SQL dialect translation
table, scoped to src/db/** and migrations/**
- .claude/rules/safety-and-sandbox.md: safety layer and sandbox rules, scoped
to src/safety/**, src/sandbox/**, src/secrets/**
- .claude/rules/testing.md: test tiers and patterns, scoped to src/** and tests/**
- .claude/rules/tools.md: tool architecture and implementation pattern, scoped
to src/tools/** and tools-src/**
- .claude/commands/pr-shepherd.md: 7-phase PR lifecycle command that subsumes
review-pr, respond-pr, ship, and manual CI fix loops
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review feedback on CLAUDE.md restructure
- Restore project structure tree in CLAUDE.md (zmanian blocking)
- Create .claude/rules/skills.md with trust model, SKILL.md format,
selection pipeline, and skill tools (zmanian blocking)
- Restore configuration section with key env vars (zmanian medium)
- Restore "Adding a New Channel" guide (zmanian medium)
- Add heartbeat mention to Workspace & Memory section (zmanian low)
- Fix pr-shepherd: replace `git add -A` with specific file staging (zmanian)
- Fix pr-shepherd: ask user for merge strategy instead of hardcoding --squash (zmanian)
- Fix pr-shepherd: replace `--watch` with polling + 10min timeout (zmanian)
- Fix testing.md: "skipped if DB is unreachable" not "expected to fail" (Copilot)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review comments on PR #750
- Narrow `crate::` import rule: `super::` is fine in tests and intra-module refs
- Fix capabilities file naming: `<name>.capabilities.json` sidecar, not bare `capabilities.json`
- Update mechanical verification checklist to match narrowed import rule
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: move Bedrock docs from CLAUDE.md to src/llm/CLAUDE.md
Bedrock provider details (auth, config, feature flag) belong in the
LLM module spec, not the top-level guide. Added file map entry,
provider table row, and dedicated section in src/llm/CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: move env var config block out of CLAUDE.md
Replace 20-line config block with one-liner pointing to .env.example
and src/llm/CLAUDE.md. Config details are only needed during deployment,
not everyday coding.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use gh pr checkout for fork-safe PR checkout in pr-shepherd
Replaces git fetch/checkout with gh pr checkout {number} which
handles both same-repo and fork-based PRs automatically.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address Copilot review round 5 on PR #750
- Add gh pr list and gh pr checkout to pr-shepherd allowed-tools
- Align crate:: import rule in pr-shepherd with updated CLAUDE.md guidance
- Fix vector type in database.md: BLOB (flexible dims), not F32_BLOB(1536)
- Update MCP limitation: stdio/HTTP/Unix transports exist, no streaming
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 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
* refactor: make src/llm/ self-contained for crate extraction
Move LlmError, LLM config types, and OAuth callback helpers into
src/llm/ so the module has zero `use crate::` imports outside of
crate::llm. This prepares the module for extraction into a standalone
workspace crate.
- Move LlmError enum from src/error.rs to src/llm/error.rs
- Move LlmConfig, NearAiConfig, RegistryProviderConfig, BedrockConfig,
CacheRetention, OAUTH_PLACEHOLDER from src/config/llm.rs to
src/llm/config.rs
- Move OAuth callback utilities (callback_url, bind_callback_listener,
wait_for_callback, landing_html, etc.) from src/cli/oauth_defaults.rs
to src/llm/oauth_helpers.rs
- Remove session.rs dependency on crate::bootstrap (inline default path)
- Add cache_retention field to RegistryProviderConfig, resolve from env
in config/llm.rs instead of reading env var in llm/mod.rs
- Add Check 6 to scripts/check-boundaries.sh enforcing LLM isolation
- All original locations re-export for backward compatibility
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #767 review — session path bug and boundary check
1. Fix SessionConfig::default() usage in setup wizard: the fallback at
wizard.rs:995 now constructs SessionConfig with the real
default_session_path() instead of a relative "session.json", which
would write auth tokens to the CWD instead of ~/.ironclaw/.
2. Widen check-boundaries.sh Check 6 to catch all `crate::` references
(not just `use crate::` imports). Pre-existing inline references
(16 occurrences) are reported as warnings; only new `use crate::`
imports are hard violations.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #767 review and audit findings in src/llm/
PR review fixes:
- Reject wildcard addresses (0.0.0.0, ::) in OAuth callback listener
to prevent session token exposure on all interfaces
- Fix boundary check comment-stripping that could hide real violations
(use sed to strip inline comments before matching)
Audit fixes:
- Fix UTF-8 byte-index slicing panic in recording.rs hint extraction
- Add effective_model_name() delegation to RetryProvider and
SmartRoutingProvider for consistency with other wrappers
- Add calculate_cost() delegation to CachedProvider and RecordingLlm
- Deduplicate retry loop logic in RetryProvider via generic helper
- Replace hardcoded /tmp path in recording tests with tempfile
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add background sandbox reaper for orphaned Docker containers
* add tests
* review fixes
* linter fix
* review fixes
* style: format test assertion in reaper
Apply rustfmt to improve code formatting consistency.
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* fix: revert assertion to single-line format for CI compatibility
The assertion should remain on a single line to match CI's
rustfmt expectations.
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
* fix: format assertion to multi-line for CI rustfmt
Use multi-line format for the assert macro to comply with
CI's rustfmt line length limit (100 chars).
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
---------
Co-authored-by: Claude Haiku 4.5 <[email protected]>
* 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]>
* fix(mcp): JSON-RPC spec compliance — flexible id, correct notification format
- McpRequest.id is now Option<u64> with skip_serializing_if, so
notifications omit the id field as required by JSON-RPC 2.0 spec.
Previously sent id: 0 which violates the spec.
- McpResponse.id uses flexible deserialization that accepts number,
string, or null — fixes interop with non-standard MCP servers that
return string ids or missing id fields on error responses.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fix review feedback: remove serde(default) from McpResponse.id, fix test assertions
- Remove #[serde(default)] from McpResponse.id so notifications (no id field)
don't incorrectly parse as responses — prevents DoS/spoofing via SSE
- Update test assertions to use Some(value) after id became Option<u64>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: update new transport files for Option<u64> id after rebase
Upstream #721 added stdio/unix/transport modules that use McpRequest.id
and McpResponse.id as u64. After our rebase (which changes id to
Option<u64>), these need .unwrap_or(0) for HashMap keys and Some()
wrapping in tests.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add regression tests for JSON-RPC spec compliance
Tests for notification serialization without id field,
flexible id deserialization (string, null, non-numeric).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Prevent model re-attempts and data inconsistencies when rebuilding
conversation context from persisted tool-call records.
- Remove raw tool parameters from persisted tool_calls JSON to prevent
unredacted sensitive data from being stored in the database. The LLM
context rebuild only needs call_id + name + result.
- Make record_tool_error/record_tool_result mutually exclusive in all
three execution paths (dispatcher, approval, deferred). Previously
error cases called both methods, violating the TurnToolCall invariant
and sending contradictory outcomes to the LLM.
- Unify call_id format to turn{N}_{i} between live sessions and
persisted hydration to eliminate ID mismatch in the LLM context.
- Auto-close </tool_output> XML tags after truncate_preview truncation
to prevent malformed tool output reaching the LLM.
[skip-regression-check]
* feat: add AWS Bedrock LLM provider via native Converse API
* fix: use JSON parsing for tool result error detection instead of brittle substring matching
* refactor: extract duplicated inference config builder into helper function
* fix: address review feedback — safe casts, input validation, and tests
- Safe u32→i32 cast for max_tokens using try_from with clamp
- Remove brittle string-based error detection fallback for tool results
- Validate BEDROCK_CROSS_REGION against allowed values (us/eu/apac/global)
- Validate message list is non-empty before Converse API call
- Log when using default us-east-1 region
- Update llm_backend doc comment to list all backends
- Add tests for build_inference_config and empty message handling
* fix: persist AWS_PROFILE for Bedrock named profile auth
The wizard collected the profile name but only printed a hint to set
it manually. Now it saves to settings and writes AWS_PROFILE to the
bootstrap .env, consistent with how BEDROCK_REGION and other Bedrock
settings are persisted.
* feat: gate AWS Bedrock behind optional `bedrock` feature flag
The AWS SDK dependencies (aws-config, aws-sdk-bedrockruntime,
aws-smithy-types) require cmake and a C compiler to build aws-lc-sys.
Gate them behind an opt-in `bedrock` feature flag so default builds
are unaffected.
Build with: cargo build --features bedrock
All config, settings, and wizard code stays unconditional (no AWS deps)
so users can configure Bedrock even without the feature compiled — they
get a clear error at startup directing them to rebuild.
* fix: address review feedback and adapt Bedrock provider to registry architecture (takeover #345)
- Resolve merge conflicts with main's registry-based provider system
- Add missing cache_creation_input_tokens/cache_read_input_tokens fields
- Add missing content_parts field in test ChatMessage
- Fix string literal type mismatches in wizard env_vars (.to_string())
- Remove non-functional bearer token auth (AWS_BEARER_TOKEN_BEDROCK) from
wizard and documentation per reviewer feedback from @zmanian and @serrrfirat
- Remove stale BEDROCK_ACCESS_KEY proxy entry from provider table
- Update Bedrock provider to use is_bedrock string check (LlmBackend enum removed)
- Add bedrock_profile fallback from settings in config resolution
[skip-regression-check]
Co-Authored-By: cgorski <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use main's Cargo.lock as base to preserve dependency versions
Regenerating Cargo.lock from scratch caused transitive dependency version
drift that broke the html_to_markdown fixture test in CI.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: bedrock config bugs — spurious warning, alias normalization, profile fallback
- Move is_bedrock check before unknown-backend warning to prevent
spurious "unknown backend" log for bedrock users
- Normalize backend aliases ("aws", "aws_bedrock") to "bedrock" so
the provider factory matches correctly
- Add settings.bedrock_profile fallback for AWS_PROFILE, consistent
with region and cross_region resolution
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address Copilot review feedback — bearer token cleanup, stop_sequences, model dedup
- Remove stale bearer token refs from setup README and CHANGELOG
- Remove dead bedrock_api_key secret injection mapping
- Pass stop_sequences through to Bedrock InferenceConfiguration
- Remove "API key" from wizard menu description (bearer token removed)
- Skip duplicate LLM_MODEL write for bedrock backend in wizard
- Fix cargo fmt formatting
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review feedback — async new(), remove LiteLLM entry, wizard fixes
- Remove dead LiteLLM-based bedrock entry from providers.json (native
Converse API intercepts before registry lookup)
- Make BedrockProvider::new() async to avoid block_in_place panic in
current_thread runtimes; propagate async to create_llm_provider,
build_provider_chain, and init_llm
- Document CMake build prerequisite in docs/LLM_PROVIDERS.md
- Clear bedrock_profile when user selects "default credentials" in wizard
- Fix selected_model clearing to match established pattern (conditional
on provider switch, not unconditional)
- Add regression tests for bedrock model preservation and profile clearing
Addresses review feedback from @zmanian on PR #713.
Streaming support tracked in #741.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining review comments — CLAUDE.md backends, wizard UX
- Add `bedrock` to CLAUDE.md inline backend list (#10)
- Skip full setup re-run when keeping existing Bedrock config (#11)
- Clear stale bedrock_profile on empty named-profile input (#12)
- Add regression test for empty profile clearing
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Chris Gorski <[email protected]>
Co-authored-by: cgorski <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Add README.zh-CN.md with full simplified Chinese translation of the
README, and add language switcher links to the original README.
Co-authored-by: smartchoice <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: CLI commands ignore runtime DATABASE_BACKEND when both features compiled
`tool auth` and `mcp` CLI subcommands used compile-time `#[cfg]` gates
to select the database backend for secrets storage. When the binary is
compiled with both `postgres` and `libsql` features, the `#[cfg(feature
= "postgres")]` block always wins regardless of the runtime
`DATABASE_BACKEND` setting. This causes `tool auth` and `mcp auth` to
fail with a connection error for users running the libsql backend.
Switch both functions to `match config.database.backend { ... }` with
inner `#[cfg]` guards on each arm, matching the pattern already used in
`main.rs` and `app.rs`.
Also adds a top-level `auth` section to the Telegram channel
capabilities file so `ironclaw tool auth telegram` works for channels
(previously only tools had this section).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: extract create_secrets_store factory into src/db, bump telegram version
- Move duplicated DB backend selection logic from cli/tool.rs and
cli/mcp.rs into a shared db::create_secrets_store() factory, following
the existing db::connect_from_config() pattern.
- Bump telegram channel version 0.2.0 → 0.2.1 to fix CI Version Bump Check.
- Add regression test for create_secrets_store with libsql backend.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review feedback — wizard.rs pattern, formatting, version bump
- Convert setup/wizard.rs secrets store creation from tri-branch #[cfg]
to runtime match on selected_backend (same pattern as CLI fix).
- Fix formatting (assert! line wrapping caught by CI).
- Bump telegram version to 0.2.2 (main already has 0.2.1).
- Merge latest main.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: fix regression test doc comment formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
[skip-regression-check]
* fix: address Copilot review — wizard default backend, error chain preservation
- Fix wizard.rs: default selected_backend to "libsql" in libsql-only
builds so create_libsql_secrets_store is not skipped.
- Preserve error chain: replace .map_err(|e| anyhow!("{}", e)) with ?
in cli/tool.rs and cli/mcp.rs since DatabaseError implements
std::error::Error.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Tiny Tim <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
Currently, teeApiBase() splits the hostname by '.' and incorrectly parses IP addresses like 127.0.0.1 or localhost into invalid URLs (e.g., http://api.0.0.1/), which causes the fetch API to throw a 'Failed to construct Request' TypeError and crashes the web UI.
This fix:
- Skips TEE checks if the hostname is an IP address or localhost.
- Wraps checkTeeStatus() and fetchTeeReport() with try...catch to gracefully handle any unforeseen fetch errors without bubbling up to the global scope.
Co-authored-by: lighterEB <[email protected]>
Add focused coverage for create/list/status/cancel job tools so validation errors, summary formatting, and cancellation behavior stay stable. This locks in the current user-facing responses for running and completed jobs without changing production code.
Made-with: Cursor
* feat: full image support across all channels
End-to-end image handling: upload, generation, analysis, editing, and
rendering across web gateway, HTTP webhook, WASM (Telegram/Slack), and
REPL channels. Builds on the attachment infrastructure from #596 and
draws inspiration from PR #641's image pipeline approach — credit to
that PR's author for the sentinel JSON pattern and base64-in-JSON
upload design.
Key changes:
- Image upload in web UI (file picker, paste, preview strip)
- Image generation tool (FLUX/DALL-E via /v1/images/generations)
- Image edit tool (multipart /v1/images/edits with fallback)
- Image analysis tool (vision model for workspace images)
- Model detection utilities (image_models.rs, vision_models.rs)
- Sentinel JSON detection in dispatcher for generated image rendering
- StatusUpdate::ImageGenerated → SSE/WS/REPL/WASM broadcast
- HTTP webhook attachment support (base64, 5MB/file, 10MB total)
- WASM channel image download (Telegram via file API, Slack via host HTTP)
- Tool registration wiring in app.rs
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #725 review comments (16 issues)
- SecretString for API keys in all image tools (image_gen, image_edit, image_analyze)
- Binary image read via tokio::fs::read instead of DB-backed workspace.read()
- Replace Arc<Workspace> with Option<PathBuf> base_dir (workspace has no filesystem API)
- ApprovalRequirement::UnlessAutoApproved for cost-sensitive image tools
- Scope sentinel detection to image_generate/image_edit tool names only
- Skip ToolResult preview broadcast for image sentinels (avoids multi-MB base64 in SSE)
- Extract shared media_type_from_path() to builtin/mod.rs
- Rename fallback_chat_edit → fallback_generate with tracing::warn
- Increase gateway body limit from 1MB to 10MB for image uploads
- Increase webhook body limit to 15MB (base64 overhead)
- Log warning on invalid base64 in images_to_attachments
- Client-side image size limits (5MB/file, 5 images max) in app.js
- aria-label on attach button for accessibility
- Update body_too_large test for new 10MB limit
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add Slack file size check before download (PR review item #15)
Skip downloading files larger than 20 MB in the Slack WASM channel to
avoid excessive memory use and slow downloads in the WASM runtime.
Logs a warning when a file is skipped. Also bumps channel versions
for Slack and Telegram (prior branch changes).
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(security): add path validation and approval requirement to image tools
Add sandbox path validation via validate_path() to both ImageAnalyzeTool
and ImageEditTool to prevent path traversal attacks that could exfiltrate
arbitrary files through external vision/edit APIs. Also fix
ImageAnalyzeTool::requires_approval to return UnlessAutoApproved,
consistent with ImageEditTool and ImageGenerateTool.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: post-download size guards and empty data_url sentinel check
- Slack: add post-download size check on actual bytes when metadata
size_bytes is absent, preventing bypass of the 20MB limit
- Telegram: add 20MB download size limit (matching Slack) enforced
in download_telegram_file() after receiving response bytes
- Dispatcher: skip broadcasting ImageGenerated SSE event when
data_url is empty from unwrap_or_default(), log warning instead
Closes correctness issues #3, #4, #5 from PR #725 review.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use mime_guess for media type detection, add alt attrs and media_type validation
- Replace hardcoded media type mapping with mime_guess crate (already in deps)
- Add alt attributes to img elements in web UI for accessibility
- Validate media_type starts with "image/" in images_to_attachments()
- Update bmp test assertion to match mime_guess behavior
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Zaki <[email protected]>
* feat(skills): exclude_keywords veto in skill activation scoring
Add exclude_keywords field to ActivationCriteria. If any exclude
keyword is present in the user message, the skill scores 0 regardless
of keyword or pattern matches — prevents cross-skill interference.
Behaviour: exclude_keywords is a hard veto. Even an exact skill name
match gets vetoed if an exclude keyword is also present. This is
intentional; partial exclusion (score reduction) would create
unpredictable interference behaviour.
Example use case: a writing skill with keywords ["write", "draft"]
and exclude_keywords ["route", "redirect"] will not activate on
messages like "don't route this to the writing agent".
Changes:
- ActivationCriteria: new exclude_keywords field (serde default)
- LoadedSkill: new lowercased_exclude_keywords (preprocessed at load)
- selector.rs: early-return 0 in score_skill() on veto match
- registry.rs: populate lowercased_exclude_keywords during loading
- Test helpers updated across mod.rs, selector.rs, attenuation.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fix review feedback: enforce limits on exclude_keywords, extract helper, use any()
- Add exclude_keywords to enforce_limits() with same min-length and cap
rules as keywords — prevents empty string always-match and unbounded lists
- Extract to_lowercase_vec() helper to deduplicate three identical blocks
- Use idiomatic any() iterator instead of for loop in score_skill veto check
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test(skills): add exclude_keywords veto tests
Adds 4 tests for the exclude_keywords veto behavior as requested in review:
1. test_exclude_keyword_vetos_match — skill scores 0 when exclude keyword present
2. test_exclude_keyword_absent_does_not_block — skill activates normally without it
3. test_exclude_keyword_veto_wins_over_positive_match — veto wins even with multiple keyword hits
4. test_exclude_keyword_case_insensitive — veto fires regardless of message case
Also adds make_skill_with_excludes() test helper to avoid repeating the
LoadedSkill construction boilerplate in each test.
Note on substring matching: exclude_keywords uses message_lower.contains(excl)
(substring match), consistent with the existing positive keyword scoring path.
This means "red" would veto "redirect". This is documented behaviour — if
word-boundary semantics are needed, that's a follow-up change.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
* style: run cargo fmt on selector.rs
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat(mcp): transport abstraction, stdio/UDS transports, and OAuth fixes
Extract McpTransport trait from HTTP-coupled McpClient, enabling pluggable
transport backends. Implements stdio and Unix domain socket transports for
local MCP server integration, fixes OAuth discovery per RFC 9728, and adds
SSRF protection.
Transport abstraction (Step 2):
- McpTransport trait with send(), shutdown(), supports_http_features()
- HttpMcpTransport extracted from McpClient with SSE parsing, session tracking
- Shared JSON-RPC framing helpers (write_jsonrpc_line, spawn_jsonrpc_reader)
- McpClient refactored to hold Arc<dyn McpTransport>
Stdio transport (#652, Step 4):
- StdioMcpTransport spawns child process, communicates via stdin/stdout
- McpProcessManager for lifecycle management with exponential backoff restart
- Background stderr drain task for debug logging
Unix domain socket transport (#134, Step 5):
- UnixMcpTransport connects to existing Unix sockets
- Reuses shared JSON-RPC framing from transport.rs
HTML error body sanitization (#263, Step 1):
- sanitize_error_body() detects HTML, strips control chars, truncates to 500
Custom headers (#639, Step 3):
- headers field on McpServerConfig, merged into every HTTP request
- --header CLI arg for `mcp add`
Config and CLI updates (Step 6):
- McpTransportConfig tagged enum (Http/Stdio/Unix) with serde support
- EffectiveTransport for zero-copy config dispatch
- CLI: --transport, --command, --arg, --env, --socket flags for `mcp add`
- `mcp list` shows transport type
OAuth fixes (#299, Step 8):
- Multi-strategy discovery (401-based, RFC 9728, direct)
- RFC 8707 resource parameter in auth and refresh flows
- SSRF protection with IPv4-mapped IPv6 bypass detection
- Well-known URI construction per RFC 8414
Closes#652, #134, #639, #263, #299
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(mcp): address audit findings from crate review
- Fix SSRF bypass: make validate_url_safe async with DNS resolution to
block hostnames that resolve to private/link-local IPs
- Fix UTF-8 truncation: use char-based truncation in sanitize_error_body
to avoid panicking on multi-byte characters
- Fix SSE parser: process only complete lines to handle chunks split
across boundaries, add 10MB buffer size limit
- Add debug_assert for transport type mismatch in new_with_config
- Propagate custom headers in new_with_transport constructor
- Deduplicate effective_transport() calls in CLI list command
- Gate test-only accessors with #[cfg(test)] to eliminate dead_code warnings
- Document JSON-RPC notification id:0 limitation in protocol.rs
- Document total backoff wait time (31s) in process.rs
- Add regression test for multi-byte UTF-8 truncation
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(mcp): address PR review findings from Copilot, Gemini, and zmanian
Moderate/High fixes:
- Plumb custom headers through new_authenticated constructor
- Restrict HTTP to localhost only in validate_url_safe (prevent
plaintext credential leaks over non-localhost HTTP)
- Add mcp_process_manager.shutdown_all() to app shutdown path to
prevent orphaning stdio child processes
- Validate discovered authorization_url before opening browser
(prevent malicious MCP server redirecting to phishing page)
Medium fixes:
- Upgrade debug_assert to assert in new_with_config (fires in release)
- Remove pending map entry on Ok(Err(_)) in stdio/unix send() to avoid
stale entries and unnecessary 30s waits
- Shut down old transport in try_restart() before spawning replacement
- Redact env var values in mcp list --verbose (may contain secrets)
- Drain pending requests on shutdown to wake waiters immediately
- Add IPv6 link-local, site-local, unique-local, and documentation
ranges to is_dangerous_ip SSRF protection
Low fixes:
- Truncate logged JSON parse error lines to 200 chars (prevent
sensitive data in logs)
- Remove misleading shutdown comment in unix_transport
- Use tempfile::tempdir() instead of hardcoded /tmp/ path in test
- Adopt main's improved sanitize_error_body (HTML tag stripping,
200-char truncation with char_indices)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(mcp): gate unix_transport with #[cfg(unix)] for Windows compat
- Add #[cfg(unix)] to unix_transport module declaration
- Add #[cfg(unix)]/#[cfg(not(unix))] branches in app.rs for Unix
socket MCP server setup
- Remove unused sanitize_error_body import in client.rs tests
[skip-regression-check]
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
5432:5432 → 127.0.0.1:5432:5432 — the default docker-compose.yml
exposed postgres on all interfaces, making it reachable from the
local network in any docker compose deployment.
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Zaki Manian <[email protected]>
When running as a launchd/systemd daemon, stdin is /dev/null.
rustyline reads EOF immediately and the REPL thread was sending
a /quit message, causing the agent to shut down right after
startup — making service mode non-functional on both macOS and Linux.
Fix: check std::io::stdin().is_terminal() before sending /quit on
EOF. In daemon mode (no TTY) the REPL thread exits silently, leaving
other channels (gateway, telegram, …) running as expected.
Fixes#723
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Zaki Manian <[email protected]>
Align TestRig with the production agent wiring so create_job exercises the real scheduler path instead of silently falling back to an unscheduled context-only job. Tighten the e2e assertion to lock in the in-progress scheduler behavior for future refactors.
Made-with: Cursor
Co-authored-by: Zaki Manian <[email protected]>
* fix(config): init_secrets no longer overwrites entire config
init_secrets() was calling Config::from_db_with_toml() to re-resolve
config after injecting credentials. This rebuilt the entire config from
env/DB/defaults, nuking all other config fields (agent, safety, tools,
etc.) even though only LlmConfig depends on injected credentials.
This caused 5 CI test failures: the test rig's carefully chosen config
values (max_tool_iterations, allow_local_tools, etc.) were silently
overwritten with production defaults after secret injection.
Fix: add Config::re_resolve_llm() that re-resolves only the LLM config
after credential injection, leaving all other config fields untouched.
Also fix TraceLlm::complete() to skip ToolCalls steps when called in
force_text mode (iteration limit).
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(test): update test to match TraceLlm::complete() skip-tool-calls behavior [skip-regression-check]
TraceLlm::complete() now skips ToolCalls steps (force_text mode) instead
of erroring. Update the test to verify it skips past a ToolCalls step and
returns the subsequent Text step.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Zaki <[email protected]>
Each provider setup function unconditionally cleared selected_model,
so re-running the wizard with "Keep current provider? Yes" would lose
the model name, forcing the user to re-select it every time.
Now only clears selected_model when the backend actually changes
(old model may be invalid for the new provider). When keeping the
same provider, the model is preserved and Step 4 shows the
"Keep current model" prompt.
Co-authored-by: Claude Opus 4.6 <[email protected]>
Add LLM_REQUEST_TIMEOUT_SECS env var (default: 120) to configure the
HTTP request timeout for LLM API calls. Primarily useful for local
models (Ollama, vLLM, LM Studio) that need more time for prompt
evaluation on consumer hardware.
The timeout is applied to the NearAI provider's HTTP client. Other
providers (Anthropic, OpenAI) use rig-core's default client.
- Add request_timeout_secs field to LlmConfig
- Thread timeout through create_llm_provider -> NearAiChatProvider
- Add NearAiChatProvider::new_with_timeout constructor
- Add .env.example documentation
- 2 regression tests for default and custom timeout values
Co-authored-by: Claude Opus 4.6 <[email protected]>
The "Environment variable" option in the setup wizard's security step
generated a master key but never initialized `secrets_crypto`, causing
subsequent API key saves to fail silently. Fix by:
1. Creating SecretsCrypto from the generated key (matching keychain path)
2. Storing the key hex in settings for write_bootstrap_env to persist
3. Auto-writing SECRETS_MASTER_KEY to ~/.ironclaw/.env
4. Using inject_single_var for thread-safe env overlay
5. Fixing misleading message (shell profiles don't work, only .env)
Co-authored-by: Claude Opus 4.6 <[email protected]>
* chore: remove dead code (LlmEvaluator, chunk_by_paragraphs, bundled channel installer, Reasoning::safety)
Delete unused code flagged in #648:
- evaluation/success.rs: delete LlmEvaluator struct/impl, remove #[allow(dead_code)] from RuleBasedEvaluator methods
- workspace/chunker.rs: delete chunk_by_paragraphs() and its tests (zero production callers)
- extensions/manager.rs: delete install_bundled_channel_from_artifacts() (hot-activation never shipped)
- llm/reasoning.rs: remove unused safety field from Reasoning struct; cascade removal through ContextCompactor, HeartbeatRunner, LlmSoftwareBuilder, and all callers
Closes#648
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: move RuleBasedEvaluator into test module to fix dead_code warning
RuleBasedEvaluator has no production callers -- it was only used in
tests of itself. Moving it into #[cfg(test)] eliminates the clippy
dead_code error that broke CI.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: persist /model selection across restarts
The /model command called set_model() on the LLM provider but never
saved the choice to settings, so the model reverted on restart. Now
persists to both the DB settings store and config.toml.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address CI clippy lint and use spawn_blocking for TOML I/O
- Use struct init syntax instead of field reassignment in test (clippy)
- Wrap sync filesystem operations in spawn_blocking to avoid blocking
the tokio executor
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address review feedback — handle JoinError, remove exists() guard
- Log warning if spawn_blocking task panics/is cancelled (JoinError)
- Remove toml_path.exists() guard; load_toml already returns Ok(None)
for missing files, so permission errors are no longer silently skipped
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(routines): resolve message tool channel/target from per-job metadata
When a routine's notify.channel is None, the message tool had no way to
resolve channel/target for full-job workers, causing "No target specified"
errors. The previous approach mutated shared global state via
set_message_tool_context(), which also raced with concurrent jobs.
Now the routine's notify config (channel + user) is carried in the job's
metadata JSON, and MessageTool::execute falls back to ctx.metadata when
neither explicit params nor conversation defaults are available. This
eliminates both the None-channel bug and the concurrent-job race.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: apply cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(message): broadcast to all channels when notify.channel is None
Address review feedback:
- Fix stale "see above" comment → "populated below"
- When notify.channel is None, use broadcast_all instead of erroring
with "No channel specified". This matches NotifyConfig semantics
where channel=None means "broadcast to all channels"
- Channel resolution is now Option<String>: param → default → metadata → None
- When None, MessageTool uses ChannelManager::broadcast_all(target, response)
and reports which channels succeeded/failed
- Add regression test for broadcast-all behavior
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use failed channels in error message, remove redundant comment
Address review feedback:
- Use `failed` vec in error message instead of re-querying channel_names
- Remove redundant orphaned comment block in routine_engine.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>