* feat(llm): hot-reload provider chain from settings (#1350) Adds SwappableLlmProvider and LlmReloadHandle so changes to the active LLM backend/model via the settings API take effect without restarting the daemon. The settings handlers trigger a chain rebuild from the latest Config::from_db_with_toml whenever an LLM-relevant key is written, and atomically swap the inner provider under the running wrappers. Addresses review feedback on the original PR #2059 (superseded): - single RwLock<ProviderSnapshot> for atomic metadata updates (no torn reads across model_name / cost / cache multipliers) - interned &'static str for model_name() to cap Box::leak at the set of distinct names a process ever sees, not one leak per swap - single critical section around swap+snapshot refresh to kill the race between concurrent reloads - tokio::sync::Mutex on LlmReloadHandle to serialize reloads and avoid overlapping OAuth refreshes / HTTP probes - warn!, not silent Ok, when reload wiring is missing from the gateway state - integration coverage per .claude/rules/testing.md: a test that drives settings_set_handler end-to-end and asserts the same Arc<dyn LlmProvider> reports the new active_model_name after swap Co-authored-by: Nigel Coleman <coleman.nige@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(llm): gate hot-reload on scope + admin; re-hydrate secrets Addresses review findings on #2673: - Scope gate: reload only fires when the written scope actually feeds the global provider chain (admin scope or gateway owner scope). A member writing their own `selected_model` lands in their user row but no longer triggers a chain rebuild that would read back from a different scope — fixing both the "write ignored by reload" bug and the DoS vector where any authed user could force expensive rebuilds. - Admin-only provider selection: `llm_backend` and `bedrock_{region, cross_region, profile}` join the existing admin-only LLM key list, matching the product directive "admins choose the provider, members pick the model within it". `selected_model` stays non-admin so every user can change their own model. - Secret re-hydration on reload: `reload_llm_after_settings_change` now calls `re_resolve_llm_with_secrets` after the bare `from_db_with_toml` read, so a new OPENAI_API_KEY / NEARAI_SESSION_TOKEN added alongside a backend switch is visible to the rebuilt chain. - Style cleanup: drop dead `llm_model` allowlist entry; drop unused `Clone` on `ProviderSnapshot`; document `reload_lock`'s purpose; explicit comment that `active_config.enabled_channels` is not refreshed (channel enablement is orthogonal to LLM config). New regression tests (5149 → 5154 passing): - `llm_reload_handle_preserves_old_chain_on_build_failure` — a failed reload leaves the primary wrapper pointing at the old chain. - `settings_set_handler_rejects_member_writing_llm_backend` — member writing `llm_backend` gets 403 (admin-only). - `settings_set_handler_member_selected_model_skips_reload` — member can set their own model, and it does NOT trigger a global reload. - `settings_set_handler_owner_scope_triggers_reload` — owner writing their own scope (no `scope=admin`) still reloads the chain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(llm): decouple reload from HTTP status; atomic set_model; cap interner Addresses PR #2673 review comments from Copilot and gemini-code-assist: - **Reload failure no longer 500s the setting write** (Copilot): `reload_llm_after_settings_change` is now infallible — it logs at `error!` when the chain rebuild fails but the handler still returns 204. Returning 500 after a successful `set_setting` misrepresented the outcome (DB committed, chain stale) and drove client retries that re-ran the same failing reload. - **set_model race with swap closed** (gemini): the write lock is now held across the inner `set_model` call and the snapshot refresh, so a concurrent `swap()` can't clobber the just-updated inner with a snapshot of the older one. - **Interner leak capped** (gemini): `intern_model_name` now refuses names longer than 256 bytes and caps distinct entries at 1024, past either limit returning a static `<model-name-overflow>` sentinel and logging at `warn!`. Protects against adversarial `set_model` input. New regression tests (5154 passing): - `settings_set_handler_returns_success_when_reload_fails` — admin switches backend to a value with no credentials; handler returns 204, DB has the new value, old chain still serving. - `set_model_and_swap_are_mutually_atomic` — concurrent set_model + swap stress; final wrapper is readable and consistent. - `intern_into_rejects_oversized_input` — oversized name never leaks, returns sentinel without touching the map. - `intern_into_caps_distinct_entries` — past the cap, sentinel; already-interned names still resolve. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(llm): load reload config from owner scope; rollback on build failure Addresses PR #2673 review comments from @serrrfirat and Copilot. - **Reload scope fix** (serrrfirat, Copilot): `reload_llm_after_settings_change` now reads with `state.owner_id` instead of the just-written `effective_user_id`. `Config::from_db_with_toml` skips the admin-merge step when `user_id == __admin__`, so reloading at admin scope was dropping owner-scope overlays that startup normally applies. Using `state.owner_id` matches `AppBuilder::init_config` and keeps the layering consistent. - **Rollback on reload failure** (serrrfirat): handlers now snapshot the affected keys before the DB write and restore them if the chain rebuild returns `ConfigLoadFailed` or `BuildFailed`. The handler then returns 422 with the rolled-back state. This closes the split-brain window where a bad `llm_backend=openai` write could leave the DB saying "openai" while the runtime kept serving "nearai". `set_setting`, `delete_setting`, and `set_all_settings` all participate. - **`ReloadOutcome` enum** replaces the previous infallible return, so callers can distinguish transient "nothing wired" (skip) from actual "chain rebuild failed" (roll back) outcomes. Regression tests (5184 passing): - `reload_rebuilds_from_owner_scope_not_effective_scope` — pre-seeds an owner-scope `selected_model` overlay and has admin write a benign key under `scope=admin`. Assertion: after reload, the wrapper reports the owner's overlay, not admin's default. This fails pre-fix because reading at `__admin__` scope silently skipped the admin merge. - `settings_set_handler_rolls_back_on_reload_failure` — pokes a poisoned `bedrock_cross_region` sibling into admin scope, triggers a handler write, asserts 422 and that the DB is back to its pre-request state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(llm): fail-loud on snapshot read errors; surface 422 reason in body Addresses Copilot review comments on PR #2673. - **Snapshot read errors** (c5, c6): `settings_{set,delete,import}_handler` used to `.unwrap_or(None)` when reading the previous value for rollback, which would silently treat a DB read failure as "no prior value" and turn a later rollback into `delete_setting` on a key whose prior value we couldn't actually read — a silent data-loss path. The handlers now map snapshot read errors to 500 and abort before persisting. The import handler does the same inside its per-key snapshot loop. - **422 body carries the reason** (c7): the `ReloadOutcome::BuildFailed` and `ConfigLoadFailed` reason strings are now included in the 422 response body. Handler error type changed from `Result<StatusCode, StatusCode>` to `Result<StatusCode, (StatusCode, String)>` (axum's `IntoResponse` for tuples). The web UI's `apiFetch` can surface the reason to the operator instead of a bare "Unprocessable Entity". Auth/validation paths keep empty-body semantics via the `no_body` helper. Test updates: - Existing handler tests: `.0` on the error tuple where they previously compared bare `StatusCode`. - Extended `settings_set_handler_rolls_back_on_reload_failure` to assert the 422 body includes the failure reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nigel Coleman <coleman.nige@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IronClaw
Your secure personal AI assistant, always on your side
English | 简体中文 | Русский | 日本語 | 한국어
Philosophy • Features • Installation • Configuration • Security • Architecture
Philosophy
IronClaw is built on a simple principle: your AI assistant should work for you, not against you.
In a world where AI systems are increasingly opaque about data handling and aligned with corporate interests, IronClaw takes a different approach:
- Your data stays yours - All information is stored locally, encrypted, and never leaves your control
- Transparency by design - Open source, auditable, no hidden telemetry or data harvesting
- Self-expanding capabilities - Build new tools on the fly without waiting for vendor updates
- Defense in depth - Multiple security layers protect against prompt injection and data exfiltration
IronClaw is the AI assistant you can actually trust with your personal and professional life.
Features
Security First
- WASM Sandbox - Untrusted tools run in isolated WebAssembly containers with capability-based permissions
- Credential Protection - Secrets are never exposed to tools; injected at the host boundary with leak detection
- Prompt Injection Defense - Pattern detection, content sanitization, and policy enforcement
- Endpoint Allowlisting - HTTP requests only to explicitly approved hosts and paths
Always Available
- Multi-channel - REPL, HTTP webhooks, WASM channels (Telegram, Slack), and web gateway
- Docker Sandbox - Isolated container execution with per-job tokens and orchestrator/worker pattern
- Web Gateway - Browser UI with real-time SSE/WebSocket streaming
- Routines - Cron schedules, event triggers, webhook handlers for background automation
- Heartbeat System - Proactive background execution for monitoring and maintenance tasks
- Parallel Jobs - Handle multiple requests concurrently with isolated contexts
- Self-repair - Automatic detection and recovery of stuck operations
Self-Expanding
- Dynamic Tool Building - Describe what you need, and IronClaw builds it as a WASM tool
- MCP Protocol - Connect to Model Context Protocol servers for additional capabilities
- Plugin Architecture - Drop in new WASM tools and channels without restarting
Persistent Memory
- Hybrid Search - Full-text + vector search using Reciprocal Rank Fusion
- Workspace Filesystem - Flexible path-based storage for notes, logs, and context
- Identity Files - Maintain consistent personality and preferences across sessions
Installation
Prerequisites
- Rust 1.85+
- PostgreSQL 15+ with pgvector extension
- NEAR AI account (authentication handled via setup wizard)
Download or Build
Visit Releases page to see the latest updates.
Install via Windows Installer (Windows)
Download the Windows Installer and run it.
Install via powershell script (Windows)
irm https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-installer.ps1 | iex
Install via shell script (macOS, Linux, Windows/WSL)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/nearai/ironclaw/releases/latest/download/ironclaw-installer.sh | sh
Install via Homebrew (macOS/Linux)
brew install ironclaw
Compile the source code (Cargo on Windows, Linux, macOS)
Install it with cargo, just make sure you have Rust installed on your computer.
# Clone the repository
git clone https://github.com/nearai/ironclaw.git
cd ironclaw
# Build
cargo build --release
# Run tests
cargo test
For full release (after modifying channel sources), run ./scripts/build-all.sh to rebuild channels first.
Database Setup
# Create database
createdb ironclaw
# Enable pgvector
psql ironclaw -c "CREATE EXTENSION IF NOT EXISTS vector;"
Configuration
Run the setup wizard to configure IronClaw:
ironclaw onboard
The wizard handles database connection, NEAR AI authentication (via browser OAuth),
and secrets encryption (using your system keychain). Settings are persisted in the
connected database; bootstrap variables (e.g. DATABASE_URL, LLM_BACKEND) are
written to ~/.ironclaw/.env so they are available before the database connects.
Alternative LLM Providers
IronClaw defaults to NEAR AI but supports many LLM providers out of the box. Built-in providers include Anthropic, OpenAI, GitHub Copilot, Google Gemini, MiniMax, Mistral, and Ollama (local). OpenAI-compatible services like OpenRouter (300+ models), Together AI, Fireworks AI, and self-hosted servers (vLLM, LiteLLM) are also supported.
Select your provider in the wizard, or set environment variables directly:
# Example: MiniMax (built-in, 204K context)
LLM_BACKEND=minimax
MINIMAX_API_KEY=...
# Example: OpenAI-compatible endpoint
LLM_BACKEND=openai_compatible
LLM_BASE_URL=https://openrouter.ai/api/v1
LLM_API_KEY=sk-or-...
LLM_MODEL=anthropic/claude-sonnet-4
See docs/capabilities/llm-providers.md for a full provider guide.
Security
IronClaw implements defense in depth to protect your data and prevent misuse.
WASM Sandbox
All untrusted tools run in isolated WebAssembly containers:
- Capability-based permissions - Explicit opt-in for HTTP, secrets, tool invocation
- Endpoint allowlisting - HTTP requests only to approved hosts/paths
- Credential injection - Secrets injected at host boundary, never exposed to WASM code
- Leak detection - Scans requests and responses for secret exfiltration attempts
- Rate limiting - Per-tool request limits to prevent abuse
- Resource limits - Memory, CPU, and execution time constraints
WASM ──► Allowlist ──► Leak Scan ──► Credential ──► Execute ──► Leak Scan ──► WASM
Validator (request) Injector Request (response)
Prompt Injection Defense
External content passes through multiple security layers:
- Pattern-based detection of injection attempts
- Content sanitization and escaping
- Policy rules with severity levels (Block/Warn/Review/Sanitize)
- Tool output wrapping for safe LLM context injection
Data Protection
- All data stored locally in your PostgreSQL database
- Secrets encrypted with AES-256-GCM
- No telemetry, analytics, or data sharing
- Full audit log of all tool executions
Architecture
┌────────────────────────────────────────────────────────────────┐
│ Channels │
│ ┌──────┐ ┌──────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ REPL │ │ HTTP │ │WASM Channels│ │ Web Gateway │ │
│ └──┬───┘ └──┬───┘ └──────┬──────┘ │ (SSE + WS) │ │
│ │ │ │ └──────┬──────┘ │
│ └─────────┴──────────────┴────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ Agent Loop │ Intent routing │
│ └────┬──────────┬───┘ │
│ │ │ │
│ ┌──────────▼────┐ ┌──▼───────────────┐ │
│ │ Scheduler │ │ Routines Engine │ │
│ │(parallel jobs)│ │(cron, event, wh) │ │
│ └──────┬────────┘ └────────┬─────────┘ │
│ │ │ │
│ ┌─────────────┼────────────────────┘ │
│ │ │ │
│ ┌───▼─────┐ ┌────▼────────────────┐ │
│ │ Local │ │ Orchestrator │ │
│ │Workers │ │ ┌───────────────┐ │ │
│ │(in-proc)│ │ │ Docker Sandbox│ │ │
│ └───┬─────┘ │ │ Containers │ │ │
│ │ │ │ ┌───────────┐ │ │ │
│ │ │ │ │Worker / CC│ │ │ │
│ │ │ │ └───────────┘ │ │ │
│ │ │ └───────────────┘ │ │
│ │ └─────────┬───────────┘ │
│ └──────────────────┤ │
│ │ │
│ ┌───────────▼──────────┐ │
│ │ Tool Registry │ │
│ │ Built-in, MCP, WASM │ │
│ └──────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
Core Components
| Component | Purpose |
|---|---|
| Agent Loop | Main message handling and job coordination |
| Router | Classifies user intent (command, query, task) |
| Scheduler | Manages parallel job execution with priorities |
| Worker | Executes jobs with LLM reasoning and tool calls |
| Orchestrator | Container lifecycle, LLM proxying, per-job auth |
| Web Gateway | Browser UI with chat, memory, jobs, logs, extensions, routines |
| Routines Engine | Scheduled (cron) and reactive (event, webhook) background tasks |
| Workspace | Persistent memory with hybrid search |
| Safety Layer | Prompt injection defense and content sanitization |
Usage
# First-time setup (configures database, auth, etc.)
ironclaw onboard
# Start interactive REPL
cargo run
# With debug logging
RUST_LOG=ironclaw=debug cargo run
Development
# Format code
cargo fmt
# Lint
cargo clippy --all --benches --tests --examples --all-features
# Run tests
createdb ironclaw_test
cargo test
# Run specific test
cargo test test_name
- Channels: See docs/channels/overview.mdx for setup of Telegram, Discord, and other channels.
- Changing channel sources: Run
./channels-src/telegram/build.shbeforecargo buildso the updated WASM is bundled.
OpenClaw Heritage
IronClaw is a Rust reimplementation inspired by OpenClaw. See FEATURE_PARITY.md for the complete tracking matrix.
Key differences:
- Rust vs TypeScript - Native performance, memory safety, single binary
- WASM sandbox vs Docker - Lightweight, capability-based security
- PostgreSQL vs SQLite - Production-ready persistence
- Security-first design - Multiple defense layers, credential protection
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
