* fix: undo() peeks without popping, breaking repeated undo and leaking redo stack
undo() used self.undo_stack.back() (peek) instead of pop_back(), so
repeated undo always returned the same checkpoint while pushing to
the redo stack unboundedly.
Additionally, redo() did not save the current state to the undo stack,
breaking the undo/redo cycle.
Changes:
- undo(): change back() to pop_back(), return owned Checkpoint
- redo(): accept current_turn/current_messages params, save current
state to undo stack before popping from redo stack
- Update process_undo/process_redo callers in agent_loop.rs
- Add tests for repeated undo, undo/redo cycling, stack size invariant
* fix: standardize lock ordering and extract push_undo helper
Address review feedback:
- Standardize lock order (Session before UndoManager) in process_undo
and process_redo to match process_user_input and prevent deadlocks
- Extract push_undo() helper to deduplicate push-and-trim logic shared
by checkpoint() and redo()
* docs: add move-semantics notes and stack invariant to UndoManager
Address review feedback requesting documentation about the ownership
semantics of undo/redo parameters and the stack size invariant.
---------
Co-authored-by: Yi LIU <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
* fix: check Content-Length before downloading HTTP response body
The HTTP tool previously downloaded the entire response body into memory
before checking the size limit, allowing a malicious server to cause OOM.
Now the Content-Length header is checked first to reject obviously
oversized responses, and the body is streamed with a hard size cap so
reading stops as soon as the limit is exceeded.
* fix: check chunk size before allocation and fix Content-Length parsing
Address review feedback:
- Check body.len() + chunk.len() before extend_from_slice to prevent
OOM from a single oversized chunk
- Use let-chain for Content-Length parsing instead of unwrap_or to
gracefully handle invalid headers
* docs: document MAX_RESPONSE_SIZE rationale and add tracing on rejection
Address review feedback: explain why 5 MB was chosen for the response
size limit and log a warning when Content-Length causes early rejection.
---------
Co-authored-by: Yi LIU <[email protected]>
Track per-provider failure state with lock-free atomics and temporarily
skip providers that have repeatedly failed with retryable errors. This
reduces latency when a provider is known to be down, instead of
wasting time on every request trying all providers sequentially.
- Add CooldownConfig (duration + threshold) and ProviderCooldown (atomics)
- Rewrite try_providers() to skip cooled-down providers, with a safety
net that always tries the oldest-cooled provider if all are down
- Add 2 env vars: LLM_FAILOVER_COOLDOWN_SECS, LLM_FAILOVER_THRESHOLD
- Add MultiCallMockProvider and 7 new test cases
- Mark "Cooldown management" as complete in FEATURE_PARITY.md
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add review and fix-issue project commands
Add 4 Claude Code project commands adapted from global skills,
tailored to IronClaw's build/test/lint workflow and conventions:
- review-pr: Paranoid architect PR review across 6 lenses
- review-crate: Deep Rust crate audit (vulnerabilities, bugs, unfinished work)
- respond-pr: Triage and address PR review comments
- fix-issue: End-to-end GitHub issue resolution with branch/plan/implement flow
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback on project commands
- Add headRefOid to gh pr view and resolve {owner}/{repo} in review-pr.md
so Step 6 line comments actually work (Gemini + Copilot)
- Add --paginate to gh api calls in respond-pr.md for large PRs (Gemini + Copilot)
- Use gh repo view --json defaultBranchRef instead of hardcoded main/master
fallback in fix-issue.md (Gemini)
- Narrow allowed-tools in all four commands to match repo convention of
specific subcommands (Bash(cargo fmt:*) style) instead of broad wildcards (Copilot)
- Clarify >20 files guidance in review-pr.md: read all, process in priority order (Copilot)
- Make cargo audit mandatory with install hint in review-crate.md (Gemini)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
These are local tool data directories (Sidecar) that should not be
tracked. Added both to .gitignore to prevent future accidents.
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: move per-invocation approval check into Tool trait (#94)
Move shell-specific destructive command detection out of agent_loop.rs
into a new `requires_approval_for(params)` method on the Tool trait.
ShellTool overrides it to check for destructive patterns (rm -rf, git
push --force, etc.) while the default delegates to `requires_approval()`.
This follows the project's tool architecture principle of keeping
tool-specific logic out of the main agent codebase, and enables other
tools to implement per-invocation gating without modifying the agent loop.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: requires_approval_for default should return false, not self.requires_approval()
The previous default broke auto-approval for all tools: since
requires_approval_for() delegated to requires_approval(), any
auto-approved tool would have its auto-approval immediately overridden
on every invocation. The correct semantic is:
- requires_approval(): "Does this tool use the approval system?"
- requires_approval_for(params): "Should this invocation override auto-approval?"
The default for the latter must be false (allow auto-approval).
ShellTool's fallback for safe commands is also changed to false.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add polished boot screen on CLI startup
Replace the minimal one-liner REPL banner with an ANSI-styled status
panel that summarizes the agent's runtime state after initialization:
model, database, tool count, enabled features, active channels, and
the gateway URL. The boot screen is shown only in interactive CLI mode
(skipped for single-message -m mode).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback on boot screen
- Stop logging gateway auth token in tracing::info! (security)
- Use info.agent_name instead of hardcoded "IronClaw" in header
- Display embeddings provider in features line: "embeddings (openai)"
- Add Display impl for DatabaseBackend, simplify main.rs match
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add lifecycle hooks system with 6 interception points
Implement extensible hook infrastructure for intercepting and transforming
agent operations at well-defined points in the lifecycle:
- BeforeInbound: intercept/modify/reject incoming user messages
- BeforeToolCall: intercept/modify/reject tool executions (chat + job)
- BeforeOutbound: intercept/modify/suppress outgoing responses
- TransformResponse: transform final response before completing a turn
- OnSessionStart: fire-and-forget notification on new session creation
- OnSessionEnd: fire-and-forget notification on session pruning
Hooks execute in priority order with modification chaining, reject
short-circuits, configurable failure modes (FailOpen/FailClosed),
and per-hook timeouts. Empty registry is zero-cost (all hooks pass
through immediately).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce hook fail-closed semantics
* Merge upstream/main into feat/hooks-system-clean
Resolve merge conflicts:
- FEATURE_PARITY.md: Keep both upstream cron/routines status and hooks status
- src/error.rs: Keep both Hook and Orchestrator/Worker error variants
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve CI test failures in pairing store and wizard
- Fix pairing store truncate bug: record_failed_approve used
.truncate(true) which wiped the file before reading, causing rate
limiting to never accumulate past 1 attempt. Changed to
.truncate(false) to preserve existing data.
- Fix wizard test: skip test_install_missing_bundled_channels when
telegram WASM artifact specifically isn't available, not just when
all channels are empty (whatsapp may exist without telegram).
- Add workspace exclude for subcrate directories to prevent cargo
from discovering them as workspace members during builds.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #18 review comments
- Remove duplicate maybe_hydrate_thread call (rebase artifact)
- Fix RwLock held across async hook execution in HookRegistry::run()
- Add tracing::warn for silent JSON parse failures in hook modifications
- Refactor execute_tool_inner to accept &WorkerDeps instead of 8 Arc params
- Use real user_id from JobContext instead of job_id UUID in BeforeToolCall hook
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: cargo fmt + remove tracked worktree breaking CI
- Apply rustfmt formatting (method chain line breaks, match arm style)
- Remove .claude/worktrees/ from git tracking (caused submodule error in CI)
- Add .claude/worktrees/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Firat Sertgoz <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Support direct API key auth and cheap model routing
Allow using IronClaw with any OpenAI-compatible API provider (e.g.
Anthropic Claude) via API key, without requiring NEAR AI session auth.
Changes:
- Skip session authentication in chat_completions mode (API key auth)
- Skip first-run onboard check when NEARAI_API_KEY is configured
- Add `cheap_model` config field (NEARAI_CHEAP_MODEL env var) for a
secondary lightweight model used for heartbeat, routing, evaluation
- Add `create_cheap_llm_provider()` factory in llm module
- Add `cheap_llm` to AgentDeps with fallback to main model
- Route heartbeat through cheap model to reduce costs
- Fix wizard compilation for new config field
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #20 review feedback
- Check API key presence (not api_mode) for auth skip (ilblackdragon)
- Add Settings::load() call in check_onboard_needed (ilblackdragon)
- Warn and ignore cheap_model for non-NearAi backends (ilblackdragon)
- Add unit tests for create_cheap_llm_provider (ilblackdragon)
- Minor formatting cleanup in cheap provider match arm
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Samuel Barbosa <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Any agent working on a module with a README.md spec must read it first,
keep code and spec in sync, and treat the spec as the tiebreaker when
they disagree.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Authoritative specification for the 7-step onboarding wizard. Documents
the full flow, settings persistence (two-layer architecture), platform
caveats (macOS keychain dialogs, URL passwords), secrets context, and
a modification checklist for future contributors.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add interactive database backend selection during onboarding
Previously the onboarding wizard silently defaulted to PostgreSQL because
libsql wasn't in the default feature set. Now both backends ship by default
and the wizard presents a selection prompt when both are available.
DATABASE_BACKEND env var still bypasses the prompt for headless/CI use.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve libSQL onboarding crash, keychain double-prompt, and setup audit findings
Three bugs fixed:
1. libSQL onboarding crash ("Missing required setting 'database_url'"):
DatabaseConfig::resolve() only checked DATABASE_BACKEND env var, falling
back to Postgres default. Now reads settings.database_backend, plus
settings.libsql_path and settings.libsql_url as fallbacks.
2. OS keychain prompts twice during startup: Config::from_env() and
Config::from_db() both called get_master_key(). Now caches the key in
SECRETS_MASTER_KEY env var after first read so from_db() skips keychain.
3. "Path not found: nearai.session" warning: from_db_map() tried to apply
app-specific DB keys (nearai.session_token) to the Settings struct.
Now skips keys that don't map to known Settings fields. Also fixed
bootstrap migration key mismatch (nearai.session -> nearai.session_token).
Setup module audit fixes (14 findings):
- Replace unreachable!() with proper error in provider match
- Extract setup_api_key_provider() to deduplicate setup_anthropic/setup_openai
- Add SAFETY comments to all unsafe std::env::set_var blocks
- Fix .unwrap() calls with proper error handling
- Remove incorrect #[allow(dead_code)] on used TelegramUpdate::update_id
- Log warnings instead of silently discarding HTTP errors in Telegram binding
- Guard select_many against empty options, fix mask_api_key for non-ASCII
- Update stale doc comment in mod.rs, rename misleading variable
- Add 7 new tests (model fetcher fallbacks, channel discovery, secret gen)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback (set_var safety, parse warnings, db_map efficiency)
1. Replace unsafe set_var keychain caching with OnceLock<String> in
SecretsConfig::resolve(). Eliminates the env var write from main.rs
entirely, using a process-wide OnceLock cache instead.
2. Log tracing::warn when database_backend or llm_backend settings
fail to parse, instead of silently falling back to defaults.
3. Remove O(K*S) get() pre-check in from_db_map(). Instead, let set()
run and match on "Path not found" errors to skip unknown keys,
avoiding full Settings serialization per key.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address critical/high audit findings across WASM sub-crates
- Telegram: remove .unwrap() panic on workspace_read (owner_id check)
- WhatsApp: use configured api_version instead of hardcoded v18.0
- WhatsApp: log config parse errors before falling back to defaults
- Slack: log serialization errors in emit_message and json_response
- Google Docs: safe array access for batch update replies
- Google Sheets: safe array access for add_sheet replies
- Google Calendar: fix doc comment secret name mismatch
- Gmail: avoid unnecessary String allocation in UNREAD check
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address second-round PR review feedback
- Validate custom model ID is non-empty (loop until valid input)
- Warn on unknown DATABASE_BACKEND env var before defaulting to Postgres
- Force re-selection when llm_backend contains unknown provider value
- Use ok_or_else for proper String error type in google-sheets
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: harden setup module error handling and secret safety
- Introduce ChannelSetupError typed enum replacing raw String errors
across all channel setup functions (setup_telegram, setup_http,
setup_tunnel, setup_wasm_channel, validate_telegram_token)
- Add From<ChannelSetupError> for SetupError to simplify call sites
- Convert setup_telegram retry from recursion to loop (unbounded stack)
- Stop printing HTTP webhook secret plaintext to terminal
- Use secret_input() for Turso auth token (was visible input())
- Replace dirs::home_dir().unwrap_or_default() with proper error
- Fix UTF-8 panic in model name truncation (byte-index to chars-based)
- Log warning in secret_exists() instead of silently swallowing errors
- Deduplicate generate_webhook_secret() to delegate to shared helper
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: replace unreachable!() with error return in setup wizard
The provider match in step_inference_provider was guarded by
is_known but used unreachable!() as the catch-all. If a new
provider is added to the is_known check without a corresponding
match arm, this would panic at runtime. Return a typed error
instead.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove unsafe set_var, use thread-safe overlay for injected secrets
Address PR #92 review comments:
- Replace all 5 unsafe `std::env::set_var()` calls with safe alternatives
- Add INJECTED_VARS OnceLock<HashMap> overlay in config.rs, checked by
optional_env() before falling back to std::env::var()
- Cache wizard API key in SetupWizard.llm_api_key field instead of env
- Pass explicit key param to fetch_anthropic_models/fetch_openai_models
- Persist env-provided API keys to secrets store during onboarding
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining PR review comments (clippy, TODO, secrets backend ordering)
- Fix empty line after doc comment (clippy: empty_line_after_doc_comments)
- Collapse nested if in optional_env overlay check (clippy: collapsible_if)
- Remove dangling TODO(#XX) placeholder issue ref in channels.rs
- Fix init_secrets_context to respect selected database_backend when both
postgres and libsql features are compiled, preventing wrong-backend
secrets storage when DATABASE_URL is set but libsql was chosen
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address latest PR review comments (SecretString, empty env, docs, embeddings)
- Change wizard llm_api_key from String to SecretString to prevent
accidental logging of API keys
- Fix inject_llm_keys_from_secrets skipping when env var is set but
empty, matching optional_env's treatment of empty as unset
- Fix inverted doc comment on INJECTED_VARS (env checked first, overlay
is the fallback, not the other way around)
- Update stale "env vars" comments in main.rs to reflect overlay pattern
- Fix step_embeddings not seeing cached OpenAI key from wizard session
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: OAuth callback listener binds IPv4 first to match redirect URLs
The listener was binding to [::1] (IPv6) first, but NEAR AI and other
OAuth flows redirect to http://127.0.0.1:9876/... (IPv4 explicit).
On macOS and most systems, [::1] and 127.0.0.1 are separate addresses,
so the browser's connection to 127.0.0.1 was refused when the listener
was on [::1]. Reversed the bind order: try 127.0.0.1 first, fall back
to [::1] if IPv4 is unavailable.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: cache keychain key eagerly to avoid redundant macOS password dialogs
Replace has_master_key() with get_master_key() in step_security() and
immediately build SecretsCrypto from the result. This eliminates redundant
keychain accesses later in init_secrets_context(), each of which triggers
macOS system dialogs (keychain unlock + app authorization).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: persist DATABASE_BACKEND to ~/.ironclaw/.env for libSQL startup
The wizard saved database_backend only to the database, but
Config::from_env() needs it BEFORE connecting to any database (to
decide which backend to use). Without it, the backend defaults to
Postgres and then fails with "Missing required setting database_url".
Now save all database bootstrap vars (DATABASE_BACKEND, DATABASE_URL,
LIBSQL_PATH, LIBSQL_URL) to ~/.ironclaw/.env via save_bootstrap_env().
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: status command shows libSQL backend and skips keychain probe
The status command only checked DATABASE_URL (postgres), showing
"not configured" for libSQL users. Now detects the DATABASE_BACKEND
env var and reports libSQL path and Turso sync status.
Also remove the keychain probe from status. get_generic_password()
triggers macOS unlock+authorization dialogs which is terrible UX
for a read-only diagnostic command.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt formatting in bootstrap test
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: deduplicate tool parameter extraction and remove dead stub tools
Delete 4 never-registered stub tools (marketplace, restaurant, ecommerce,
taskrabbit) removing ~625 lines of dead code. Add require_str/require_param
helpers to tool.rs and refactor ~30 call sites across 10 tool files from
4-6 line inline extractions to single-line calls. Consolidate worker HTTP
client with get_json/post_json helpers, reducing boilerplate in 4 methods.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: return JSON from orchestrator /complete endpoint
The report_complete handler returned bare StatusCode::OK (no body),
which broke the post_json helper that expects a JSON response.
Return {"status": "ok"} for consistency with other worker endpoints.
Addresses review feedback on PR #98.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
The worker (both agent/worker.rs and worker/runtime.rs) was passing the
literal string "tool_call_id" to ChatMessage::tool_result instead of
the actual tool call ID from the LLM response. This breaks
OpenAI-compatible providers that match tool results to their
corresponding calls by ID.
- Add tool_call_id field to ToolSelection struct
- Propagate ToolCall.id through select_tools() into ToolSelection
- Replace all hardcoded "tool_call_id" usages with selection.tool_call_id
- Generate unique IDs for plan-based synthetic selections
- Add test verifying tool_call_id is preserved
Co-authored-by: Yi LIU <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* feat: Move debug log truncation from agent loop to REPL channel
Full tool output now flows through StatusUpdate so the web gateway
gets untruncated content. The REPL channel truncates at display time
(200 chars for tool results, thinking, and status messages).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Flatten WASM tool schemas and fix host HTTP runtime contention
LLMs can't reliably follow oneOf + const discriminator patterns in JSON
Schema, causing tools like Google Calendar to receive malformed params
(e.g., {"operation":"list_events","data":{"calendarId":"primary"}} instead
of {"action":"list_events","calendar_id":"primary"}). Replace all 9 WASM
tool schemas with flat action enum + top-level properties. The serde
#[serde(tag = "action")] deserialization works identically.
Also fixes WASM host HTTP requests (channels and tools) stalling during
startup by replacing Handle::current().block_on() with a dedicated
single-threaded runtime per request, avoiding I/O driver contention.
Reduces verbose LLM debug logging (full request/response payloads) and
changes tower_http default from debug to warn.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Built-in OAuth credentials and combined Google scopes
Add infrastructure for shipping default OAuth credentials with the binary,
similar to how gcloud/rclone bake in their client_id. Credentials are set
at compile time via IRONCLAW_GOOGLE_CLIENT_ID / IRONCLAW_GOOGLE_CLIENT_SECRET
env vars, or can be hardcoded in src/cli/oauth_defaults.rs.
The fallback chain is: capabilities file > runtime env var > built-in defaults.
Also, when authing any Google tool, scopes from ALL installed Google tools
are now combined into a single OAuth request (they all share the same
google_oauth_token secret). One login covers Gmail, Calendar, Drive, etc.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Ship default Google OAuth credentials for zero-config auth
Google Desktop App credentials are not secret (per Google's own docs).
Hardcode them so `ironclaw tool auth <google-tool>` works out of the box
without requiring users to register their own OAuth app.
Credentials can still be overridden at compile time
(IRONCLAW_GOOGLE_CLIENT_ID) or runtime (GOOGLE_OAUTH_CLIENT_ID).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Consistent OAuth callback port and polished landing page
- Use fixed port 9876 instead of scanning 9876-9886 (one redirect URI
to register in provider OAuth apps, deterministic behavior)
- Replace broken unicode checkmark with SVG icons (charset was missing,
rendered as mojibake)
- Dark themed landing page with proper card layout for both success
and error states
- Add charset=utf-8 to Content-Type headers
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: Unify OAuth callback server across all auth flows
All three OAuth flows (WASM tool auth, MCP server auth, NEAR AI login)
now share the same code from cli::oauth_defaults:
- Fixed port 9876 (one redirect URI to register per provider)
- Shared landing page HTML (dark card with SVG icons, proper charset)
- Parameterized wait_for_callback(listener, path, param, display_name)
Removes ~120 lines of duplicated callback/HTML code.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Support for oauth token refresh
* refactor: Replace bootstrap.json with ~/.ironclaw/.env for DATABASE_URL
Kill the 4-field BootstrapConfig JSON file. Only DATABASE_URL actually
needs disk persistence (chicken-and-egg before DB connect). The other
three fields are now derived: pool_size defaults to 10 via env var,
secrets master key is auto-detected (env then keychain probe), and
onboard_completed is inferred from DATABASE_URL presence.
The new format is a standard .env file loaded via dotenvy early in
main, so DATABASE_URL is available as a regular env var everywhere.
Handles three upgrade paths:
- Clean start: wizard writes .env, reload after wizard completes
- Returning user: .env loaded at startup, business as usual
- Legacy upgrade: bootstrap.json auto-migrated to .env on first run
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR review findings
- Fix UTF-8 panic in truncate_for_preview (byte-slice on char boundary)
- Cap WASM guest timeout_ms at 5 minutes to prevent resource exhaustion
- Fix localhost detection in requires_auth() to avoid substring matches
(e.g. "notlocalhost.com" no longer matches)
- Fix query param injection to insert before URL fragment
- Fix extract_host_from_url for IPv6 bracket notation
- Remove misleading schema defaults: Slack limit, Slides insertion_index,
Docs index (per-action defaults documented in descriptions instead)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: Fix cargo fmt formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: IPv6 loopback support for OAuth listener and localhost detection
- bind_callback_listener: try [::1] first, fall back to 127.0.0.1,
so OAuth redirects work on systems where localhost resolves to ::1
- is_localhost_url: replace manual string parsing with url::Url for
correct handling of IPv6 brackets, ports, userinfo, etc.
- Add url crate as direct dependency (already a transitive dep)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR review feedback on runtime reuse, onboard check, and OAuth binding
- Remove session file check from check_onboard_needed(); DATABASE_URL is sufficient
- Detect AddrInUse on IPv6 bind and fail immediately instead of falling through to IPv4
- Reuse dedicated tokio runtime across HTTP calls in both tool and channel WASM wrappers
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: HTML-escape provider name in OAuth landing page, simplify Slack limit description
- Add html_escape() to prevent XSS in landing_html() where provider_name
was interpolated directly into HTML (defense-in-depth, source is trusted
but escaping costs nothing)
- Remove per-action default numbers from Slack limit field description to
avoid confusing LLMs with conflicting defaults
Addresses review feedback from zmanian on PR #42.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Save all bootstrap fields from wizard, fix config module comment
- Wizard now saves secrets_master_key_source and database_pool_size to
bootstrap.json (was only saving database_url and onboard_completed,
which broke secrets after fresh onboard since SecretsConfig::resolve
reads key source from bootstrap)
- Update config.rs module doc to reflect bootstrap.json priority chain
instead of the removed ~/.ironclaw/.env approach
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: Replace BootstrapConfig with .env-based bootstrap
DATABASE_URL is the only setting that needs disk persistence before
the database is available. Instead of a custom bootstrap.json with 4
fields, use a standard ~/.ironclaw/.env file loaded via dotenvy.
- Remove BootstrapConfig struct entirely
- Restore ironclaw_env_path(), load_ironclaw_env(), save_database_url()
- SecretsConfig::resolve() now auto-detects (env var then keychain probe)
instead of reading a saved source from bootstrap.json
- DatabaseConfig::resolve() reads DATABASE_URL from env only (dotenvy
loads ~/.ironclaw/.env into the environment early in startup)
- check_onboard_needed() is now sync (just checks env vars)
- Wizard save_and_summarize() works for both postgres and libsql backends
- One-time migration from bootstrap.json to .env preserved
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Ensure load_ironclaw_env() runs in all Config paths, fix .env priority
- Config::from_env() and Config::from_db() now call load_ironclaw_env()
internally (after dotenvy::dotenv()), so CLI commands like `memory`
and `config` correctly load DATABASE_URL from ~/.ironclaw/.env
- Fix load order: standard ./.env first (higher priority), then
~/.ironclaw/.env, matching the documented priority chain
- Collapse nested if/if-let into let-chains (clippy::collapsible_if)
in oauth_defaults.rs, tool.rs, and secrets/store.rs
- Fix rename_to_migrated to take &Path instead of &PathBuf
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR review comments (quoting, SSRF, error mapping)
- Quote DATABASE_URL in .env writes so `#` in passwords isn't treated
as a dotenv comment (e.g., `DATABASE_URL="postgres://..."`)
- Add SSRF defenses to refresh_oauth_token(): require HTTPS, reject
private/loopback IPs (with DNS resolution), disable redirects.
token_url comes from tool capabilities JSON, so a malicious tool
could otherwise exfiltrate refresh tokens.
- Fix IPv4 bind error mapping: only map AddrInUse to PortInUse,
use generic Io variant for other bind failures
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add multi-provider LLM failover
Add FailoverProvider that wraps multiple LlmProvider instances and
tries each in sequence on transient failures. Non-retryable errors
(auth, context length, model not available) propagate immediately.
- New `FailoverProvider` with generic `try_providers` helper
- `is_retryable()` classifies transient errors (request failed,
rate limited, invalid response, session renewal, HTTP, IO)
- Configurable via `NEARAI_FALLBACK_MODEL` env var
- Returns `Result` from constructor (no panics in production)
- Updates FEATURE_PARITY.md: failover chains ✅, cooldown ❌
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: track last-used provider for accurate cost/model reporting
After failover, model_name() and cost_per_token() now reflect the
provider that actually handled the request, not always the primary.
Also corrects is_retryable() docs to list ModelNotAvailable as retryable.
Addresses PR #28 review comments.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add retry with exponential backoff for LLM providers
Add retry logic with exponential backoff and jitter to both NearAiProvider
and NearAiChatProvider for transient errors (HTTP 429, 500, 502, 503, 504).
Extract shared retry helpers (is_retryable_status, retry_backoff_delay)
into src/llm/retry.rs so both providers reuse the same logic.
Configurable via NEARAI_MAX_RETRIES env var (default: 3).
* docs: clarify max_retries means N retries, not N total attempts
* warn when fallback model equals primary model
* fix: saturating_mul in backoff delay, dedupe to_lowercase allocation
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* docs: Add review discipline guidelines to CLAUDE.md
Codifies lessons learned from Illia's review fixes on the libSQL
backend PR -- patterns we missed that should be caught systematically
going forward.
- Ban .expect() alongside .unwrap() in production code
- Add mechanical grep checks before committing
- New "Review & Fix Discipline" section covering:
- Fix all instances of a pattern, not just the one flagged
- Propagate architectural changes to satellite types
- Schema translation must include indexes and seed data
- Feature flag testing with each feature in isolation
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* feat: add libSQL/Turso database backend with full feature parity
Introduce a Database trait abstraction (~60 async methods) enabling
compile-time backend selection between PostgreSQL and libSQL/Turso.
Convert all modules from concrete Store to Arc<dyn Database>, add
LibSqlSecretsStore and LibSqlWasmToolStore implementations, wire
libsql stores throughout CLI and main entry points, and make the
setup wizard backend-agnostic.
Key changes:
- src/db/: Database trait, PostgresDatabase adapter, LibSqlBackend
with native SQLite-dialect SQL, and idempotent migration system
- src/secrets/store.rs: LibSqlSecretsStore (all 8 trait methods)
- src/tools/wasm/storage.rs: LibSqlWasmToolStore (all 7 trait methods)
- src/main.rs, cli/tool.rs, cli/mcp.rs: backend-conditional wiring
- src/setup/channels.rs: SecretsContext uses Arc<dyn SecretsStore>
- Feature-gate postgres-only tests and examples
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: enable onboarding wizard for libSQL builds
Refactor the setup wizard to work with both postgres and libsql feature
flags. Previously the wizard was gated behind #[cfg(feature = "postgres")]
only, so libsql-only builds would print an error on `ironclaw onboard`.
- Add libsql fields to Settings (database_backend, libsql_path, libsql_url)
- Split wizard database/migration/secrets methods into feature-gated variants
- Add step_database_libsql() with local path and Turso remote replica prompts
- Update setup/mod.rs and main.rs feature gates to any(postgres, libsql)
- Extend check_onboard_needed() to detect libsql database presence
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback for libSQL backend
- P0: Switch libsql_backend to connection-per-operation pattern to fix
shared Connection concurrency issue across tokio tasks
- P0: Wrap secrets store INSERT+SELECT in transaction to fix TOCTOU race
- P0: Document encryption-at-rest limitations and json_patch divergence
- P1: Fix get_opt_text removing .filter(|s| !s.is_empty()) that conflated
empty strings with NULL
- P1: Replace datetime('now') with fmt_ts(&Utc::now()) for consistent
RFC 3339 timestamps across all queries
- P2: Use explicit _rowid column in FTS5 triggers and joins for stability
across VACUUM operations
- P2: Add tracing::warn when embedding provided but vector search disabled
in hybrid_search
- Extract shared connect_from_config() helper to deduplicate DB connection
logic across main.rs, cli/config.rs, and cli/mcp.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add missing JobContext fields and resolve fmt/clippy warnings
Add total_tokens_used and max_tokens fields to JobContext in
libsql_backend.rs, apply cargo fmt, and fix clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: review fixes for libSQL backend (shared connections, panics, indexes)
- Replace .expect() with proper error propagation in 3 call sites
- Share Arc<Database> between backend and stores instead of single Connection
- Add connect-per-operation pattern to LibSqlSecretsStore and LibSqlWasmToolStore
- Wrap store() INSERT + SELECT-back in a transaction
- Add ~22 missing indexes for parity with PostgreSQL schema
- Add 18 leak_detection_patterns seed rows matching PostgreSQL V2 migration
- Fix super:: import to use crate:: style
- Gate mask_password_in_url behind #[cfg(feature = "postgres")]
- Rewrite secrets store init with or_else chain for runtime backend selection
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Resolve clippy lints (collapsible_if, too_many_arguments)
Collapse nested if blocks into let_chains to satisfy clippy's
collapsible_if lint (CI uses -D warnings). Suppress too_many_arguments
on libsql_row_to_tool_at since refactoring the positional index
pattern would be a larger change.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* feat: Move debug log truncation from agent loop to REPL channel
Full tool output now flows through StatusUpdate so the web gateway
gets untruncated content. The REPL channel truncates at display time
(200 chars for tool results, thinking, and status messages).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: truncating fmt layer for terminal, full logs for web gateway
Instead of truncating debug output at each LLM call site (fragile),
use a custom MakeWriter on the fmt layer that caps each tracing event
at 500 bytes before flushing to stderr. The web gateway WebLogLayer
still receives full untruncated content for /api/logs/events SSE.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: UTF-8 safe truncation in truncate_for_preview, remove double truncation
- Use char_indices() instead of byte-based slicing to find the cut
point, preventing panics on multi-byte characters (emoji, CJK, etc.)
- Remove redundant truncation in REPL channel (agent loop already
truncates ToolResult previews to 200 chars)
- Add 9 unit tests covering edge cases: empty, exact length, multi-byte
UTF-8 (emoji, CJK), mixed scripts, newline collapsing, whitespace
Addresses PR #65 review comments.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* Bump MSRV to 1.92 and add GCP deployment files
rig-core 0.30 uses let_chains (stabilized post-1.87), which breaks
builds on Rust 1.85. Bump rust-version in Cargo.toml and both
Dockerfiles to 1.92 (verified working).
Add cloud deployment scaffolding:
- Dockerfile: multi-stage build for the main agent container
- deploy/cloud-sql-proxy.service: systemd unit for Cloud SQL Auth Proxy
- deploy/ironclaw.service: systemd unit for the IronClaw container
- deploy/setup.sh: VM bootstrap script (Docker, proxy, services)
- deploy/env.example: reference environment configuration
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Address review feedback: harden deploy scaffolding
- Add comment explaining GATEWAY_HOST=0.0.0.0 and when to use 127.0.0.1
- Document /opt/ironclaw ownership model (root-owned, Docker reads as root)
- Switch cloud-sql-proxy service from User=root to DynamicUser=yes
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Resolve clippy lints (Rust 1.93) and fix CI test workflow
- Fix 97 collapsible_if warnings using let-chains syntax (auto-fixed)
- Fix ptr_arg: change &PathBuf to &Path in pairing store functions
- Fix suspicious_open_options: add .truncate(false) to OpenOptions
- Fix too_many_arguments: add clippy allow on execute_status
- Fix unnecessary_unwrap: use if-let in repository.rs hybrid_search
- Gate unused EchoTool with #[cfg(test)]
- Add PairingStore argument to ChannelStoreData::new() test call sites
- Add skip guard for bundled channel test when WASM artifacts unavailable
- Split CI test workflow to exclude PostgreSQL-dependent integration tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address review feedback from ilblackdragon
- Add root check to setup.sh (exits with error if not root)
- Add warning comment to env.example about placeholder passwords
- Dockerfile.worker already uses rust:1.92 (no change needed)
- PR #41 overlap noted; will rebase after #41 merges
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve 47 collapsible_if clippy warnings
Collapse nested if statements across the codebase to satisfy
clippy::collapsible_if on Rust 1.93.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add OpenAI-compatible HTTP API (/v1/chat/completions, /v1/models)
* - Reject model mismatches: validate req.model against the active model
and return 404 model_not_found instead of silently ignoring it
- Add x-ironclaw-streaming: simulated response header so clients know
streaming is not true token-by-token delivery
- Use SSE event type "error" for mid-stream LLM failures so clients can
distinguish errors from content chunks
- Mark docker-compose credentials as dev-only
- Add integration tests for model mismatch, streaming header, and body
size limit (axum's default 2MB)
* fix: address Copilot review feedback on OpenAI-compat API
- Wire chat_rate_limiter into /v1/chat/completions handler
- Execute LLM before starting SSE stream so failures return proper HTTP
errors instead of SSE error events
- Validate tool-role messages require tool_call_id and name fields
- Surface list_models() errors in models_handler via map_llm_error
- Reject unknown roles with 400 instead of defaulting to User
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: firat.sertgoz <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: flatten tool messages for NEAR AI cloud-api compatibility
NEAR AI cloud-api does not support the OpenAI multi-turn tool-calling
protocol (role:"tool" messages cause HTTP 400). This adds a
flatten_tool_messages() pass in NearAiChatProvider that rewrites
assistant tool_call messages and tool result messages into plain
assistant/user text before sending to the API. The model still sees
the tool execution history, just in a text format it can process.
Also includes a minor fix to telegram channel send_pairing_reply
for updated WASM host function signature.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve CI failures in fmt, rate limiting, and test configuration
- Apply cargo fmt to nearai_chat.rs formatting violations
- Fix truncate(true) bug in record_failed_approve that cleared the
attempts file before reading, preventing rate limit from ever
triggering
- Skip bundled channel test when WASM build artifacts are unavailable
(CI lacks wasm32-wasip2 target)
- Split CI test workflow to exclude workspace_integration tests that
require PostgreSQL
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve clippy unnecessary_unwrap lint (Rust 1.93)
Replace is_some() + unwrap() pattern with if-let binding to satisfy
clippy::unnecessary_unwrap which is now deny-by-default.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* fix: comprehensive security hardening across all layers
Critical:
- Replace --dangerously-skip-permissions with explicit tool allowlist
via settings.json (Claude Code bridge)
- Constant-time token comparison (subtle crate) in web auth and
orchestrator auth to prevent timing attacks
High:
- Revoke tokens and clean up handles on container creation failure
- Drop SETUID/SETGID capabilities from containers (keep only CHOWN)
- Disable redirect following in HTTP tool and WASM wrapper (SSRF)
- Reject URL userinfo (@) in WASM allowlist parser (host confusion)
- Fix binary body bypassing leak detection (from_utf8 -> from_utf8_lossy)
- Protect identity files from LLM overwrites (prompt injection defense)
- Prevent tool shadowing: built-in tools cannot be replaced dynamically
- User-scoped job APIs: list/detail/cancel/restart/prompt/events/files
- CORS restricted to localhost origins, WebSocket origin validation
- Sandbox shell fail-closed: no silent fallback to unsandboxed execution
- Scrub secrets from log broadcaster before SSE broadcast
- XSS sanitization on rendered markdown in web UI
- WASM epoch ticker thread so timeout deadlines actually fire
Medium:
- Cap state transition history at 200 entries
- SSE/WebSocket connection limit (100 max)
- Request body size limit (1MB)
- Response body size limit enforcement in WASM HTTP
- UTF-8 safe string truncation (routine engine, shell tool)
- Fix PolicyAction::Sanitize to actually run the sanitizer
- TOCTOU fix in scheduler and context manager (hold write lock)
- Project file serving moved behind auth
- Path traversal guard on project_id
- Session file permissions set to 0600 on unix
- AtomicUsize for routine running_count (panic-safe)
- Completion detection hardened against false positives and tool injection
- Tool output no longer drives job completion (only LLM response)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address security review findings across all layers
- Fix path traversal sandbox bypass via lexical normalization (file.rs)
- Fix SSRF via DNS rebinding with pre-request hostname resolution (http.rs)
- Add token budget enforcement on LLM calls (reasoning.rs, state.rs)
- Fix cross-user chat history leak with ownership verification (store.rs, server.rs)
- Add sliding-window rate limiter on gateway chat endpoint (server.rs)
- Harden extension install: HTTPS-only, 50MB cap, WASM magic validation (manager.rs)
- Add destructive command blocklist that overrides shell auto-approval (shell.rs)
- Add 5MB response body size cap to HTTP tool (http.rs)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: deduplicate shared helpers and remove dead code
Extract floor_char_boundary and llm_signals_completion into src/util.rs,
unifying diverging phrase lists from agent/worker.rs and worker/runtime.rs.
Remove dead RespondResult::usage(), duplicate PROTECTED_IDENTITY_FILES
constant, double LeakDetector scanning in WebLogLayer, and invalid
0.0.0.0 origin from WebSocket allow list.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review findings and CI test failures
- Fix record_failed_approve: .truncate(true) wiped the attempts file
before reading, so failed pairing attempts never accumulated and
rate limiting never triggered.
- Guard wizard WASM test: skip gracefully when channel build artifacts
are absent (CI doesn't compile wasm32-wasip2 targets).
- Fix DNS rebinding check: use port 0 instead of hardcoded 443, since
the port is irrelevant for hostname resolution.
- Remove hardcoded CORS port 3001: the dynamic addr.port() entries
already cover the actual server port.
- Require WebSocket Origin header: reject connections that omit it
entirely, since browsers always send Origin for WS upgrades and a
missing header indicates a non-browser client bypassing the check.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address second round of PR review findings
- store.rs: reintroduce file locking around read-modify-write in
record_failed_approve (concurrent callers could clobber each other).
- sse.rs: replace load+check+fetch_add with atomic fetch_update in both
subscribe_raw() and subscribe() to prevent overshooting max_connections.
- ws.rs: decrement WS tracker before early return when subscribe_raw()
returns None (connection limit reached), fixing a counter leak.
- server.rs: parse WS Origin host exactly instead of prefix matching,
preventing bypass via crafted origins like http://localhost.evil.com.
- workspace_integration.rs: skip tests gracefully when Postgres is
unreachable instead of panicking (fixes 10 CI failures).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add Origin header to WS integration tests
The Origin header requirement added in a3b0190 broke the WS gateway
integration tests. Test clients now send Origin: http://127.0.0.1:{port}
to match the server's localhost validation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Implement DM pairing for channels
- Introduced a new pairing system to manage direct messages from unknown senders.
- Added `PairingStore` to handle pending requests and allowlist management.
- Implemented CLI commands for listing and approving pairing requests.
- Updated Telegram channel to utilize the new pairing logic, including workspace paths for storing pairing data.
- Enhanced WASM channel integration to support pairing functionality.
This feature enhances security by requiring approval for unknown senders before they can interact with the agent.
* Enhance Telegram channel support with media captioning and DM pairing features
- Added support for media captions in Telegram messages, allowing for richer content handling.
- Updated message processing to utilize either text or caption, improving message flexibility.
- Enhanced DM pairing functionality to include approval and listing capabilities for direct messages.
- Updated feature parity documentation to reflect new capabilities and improvements in Telegram integration.
* Update README and BUILDING_CHANNELS documentation for Telegram channel integration
- Enhanced README with instructions for building and running the Telegram channel, including a note on running `./scripts/build-all.sh` for full releases.
- Added detailed steps in BUILDING_CHANNELS.md for building and deploying the Telegram channel, emphasizing the need to run `./channels-src/telegram/build.sh` before building the main crate to ensure updated WASM is included.
- Updated CLI module to expose a new command for pairing with store functionality.
* Implement build script for Telegram channel WASM and enhance pairing error handling
- Added a new `build.rs` script to automate the compilation of the Telegram channel's WASM binary from source, ensuring reproducible builds and emphasizing supply chain security by preventing committed binaries.
- Updated `BUILDING_CHANNELS.md` to reflect the new build process and the importance of not committing compiled binaries.
- Enhanced error handling in the pairing approval process to include rate limiting for failed attempts, improving security and user feedback.
* Remove Telegram channel WASM binary file as part of the build process cleanup, ensuring no committed binaries are present in the repository.