* 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]>
* 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]>
* 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: 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.
Add support for OpenAI, Anthropic, Ollama, and OpenAI-compatible
endpoints alongside the existing NEAR AI backend. Users can now
bring their own API keys via environment variables (LLM_BACKEND,
OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) while NEAR AI remains
the default.
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: resolve runtime panic in Linux keychain integration
- Convert Linux keychain functions from sync (rt.block_on) to async
- Remove nested runtime panic when called from async context
- Make keychain API consistent across platforms (macOS, Linux, fallback)
- Propagate async through config loading and CLI commands
Fixes panic on Linux during 'ironclaw onboard' at Step 2 (Security).
* fix: await async Config::from_env in test_heartbeat example
* Orchestrating jobs and running them in sandboxes
* Fix heartbeat: dynamic max_tokens, empty content guard, notification fallback
- Query /v1/models API for context_length and set max_tokens to half
(floor 4096) instead of hardcoded 1024; reasoning models like GLM-4.7
need much larger budgets
- Guard against empty LLM content (reasoning models can burn all tokens
on chain-of-thought and return content: null)
- Simplify notification routing: try configured channel first, fall back
to broadcast_all so heartbeat alerts always reach someone
- Add ModelMetadata struct and model_metadata() to LlmProvider trait
- Refactor NearAiChatProvider::list_models into shared fetch_models()
- Add standalone test_heartbeat example for isolated debugging
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add job detail view with drill-down from jobs list
Click a job row to see full details across four sub-tabs:
Overview (metadata grid, description, state transitions timeline),
Actions (expandable tool call cards with input/output JSON),
Thinking (conversation messages styled by role), and
Files (embedded workspace tree browser).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Strip model-internal XML tags from LLM responses, fix Telegram parse_mode 400
Some models (GLM-4.7, etc.) emit <tool_call>tool_list</tool_call> in the
content field instead of using the OpenAI tool_calls array. This XML leaks
through to channels as text, and Telegram's Markdown parser chokes on the
underscores, returning 400 "can't parse entities".
Two fixes:
- Generalize clean_response() to strip <tool_call>, <function_call>,
<tool_calls>, and pipe-delimited variants (<|tool_call|>) alongside
the existing <thinking> tag stripping
- Add Telegram send_message helper with parse_mode fallback: try Markdown
first, retry as plain text on "can't parse entities" 400 errors
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add SystemCommand submission type for thread-state-independent commands
System commands (/help, /model, /version, /tools, /ping, /debug) now
bypass thread-state checks and safety validation via a dedicated
Submission::SystemCommand variant. Previously these flowed through
process_user_input() which blocked them during Processing/AwaitingApproval
/Completed states.
- Add /model [name] for runtime model switching with provider validation
- Add active_model_name()/set_model() to LlmProvider trait with RwLock
hot-swap in both NEAR AI providers
- Rewrite /help with aligned columns grouped by category
- Expand REPL tab-completion from 10 to 23 slash commands
- Remove REPL-local /help interception (now handled by agent)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add per-tool execution timeouts, auto-create sandbox project dirs, serve built files
The sandbox e2e pipeline (agent -> container -> built website -> browsable URL)
was broken by three gaps: hardcoded 60s timeouts killed sandbox jobs that need
minutes, no auto-created project directory meant container output vanished, and
no HTTP route to browse the built files.
- Add `execution_timeout()` to the `Tool` trait (default 60s), replace all four
hardcoded `Duration::from_secs(60)` call sites (agent_loop, worker, scheduler,
worker/runtime) with the per-tool value
- Override to 660s in `RunInSandboxTool` (10 min polling + 60s buffer)
- Auto-create `~/.ironclaw/projects/{uuid}/` when no `project_dir` is specified,
so every sandbox job gets a persistent bind mount
- Include `project_dir` and `browse_url` in sandbox tool output JSON
- Add `/projects/{id}` and `/projects/{id}/{path}` static file serving routes
to the web gateway with path traversal protection and MIME type detection
- Add `mime_guess` dependency for content-type detection
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Apply cargo fmt to wizard.rs after merge
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Persist sandbox jobs in DB, fix web UI, unify job model
Sandbox container jobs were invisible to the web UI because they lived
only in ContainerJobManager's in-memory HashMap while the API queried
ContextManager. This persists them to the agent_jobs table and fixes
all six front-end bugs (empty job list, broken back button, empty
actions/thinking tabs, wrong files tab, stuck status, no persistence).
Key changes:
- V4 migration adds project_dir and user_id columns to agent_jobs
- Embedded migrations via refinery (no external CLI needed)
- SandboxJobRecord CRUD in Store with fire-and-forget DB writes
- Unified job_id: sandbox tool generates UUID, passes to ContainerJobManager
- Web API queries DB for sandbox jobs, merges with ContextManager direct jobs
- New endpoints: restart, project file list/read with path traversal protection
- Front-end: rebuild DOM on back navigation, sandbox-aware tabs, job cards in
chat stream, source badges, restart button for failed/interrupted jobs
- Gateway defaults to enabled, prints Web UI URL on startup
- Stale jobs marked "interrupted" on restart for visibility and restartability
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Secure in-chat auth: tokens never touch the LLM or chat history
Remove the token parameter from tool_auth so the LLM cannot pass raw
API keys. Add dedicated REST (POST /api/chat/auth-token) and WebSocket
(auth_token) endpoints that route tokens directly to ext_mgr.auth(),
completely bypassing the message pipeline, turns, history, and compaction.
Web UI shows an auth card (password input + OAuth button) when the agent
enters auth mode, submitted via the dedicated endpoint. CLI auth mode
interception is unchanged (already secure).
New StatusUpdate::AuthRequired/AuthCompleted variants propagate through
all channels (SSE, WebSocket, REPL, WASM).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add Claude Code mode for sandbox jobs
Run Claude Code CLI inside Docker containers as an alternative to the
standard worker mode. The bridge spawns `claude -p` with stream-json
output, posts events to the orchestrator, and supports follow-up
prompts via `--resume`.
Key additions:
- `claude-bridge` CLI subcommand and ClaudeBridgeRuntime
- JobMode enum (Worker vs ClaudeCode) with per-mode container config
- Orchestrator endpoints for Claude events and prompt polling
- SSE event variants for real-time Claude Code streaming to frontend
- Claude Code sub-tab in web UI with terminal-style output and input bar
- Database migration for job_mode column and claude_code_events table
- ClaudeCodeConfig with env var support (CLAUDE_CODE_ENABLED, etc.)
- Mode parameter on run_in_sandbox tool schema
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Skip create_job tool when sandbox is enabled to prevent duplicate jobs
When sandbox mode is on, the LLM would call create_job (creating a
pending "direct" entry) then run_in_sandbox (creating a second "sandbox"
entry), producing two jobs in the list for a single user request.
Now register_job_tools() skips create_job when sandbox is enabled since
run_in_sandbox already creates tracked jobs. Also improved the
run_in_sandbox description to guide the LLM to use it directly and to
mention wait=false for async execution.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Web gateway UI quality-of-life improvements
Phase 1: Send button disabled state to prevent double-sends, copy button
on code blocks, confirm() guards on destructive actions, SSE-driven job
list auto-refresh, log filters re-applied on tab switch, jobEvents memory
leak fix (cap at 500, cleanup after 60s).
Phase 2: Toast notification system replacing chat-based system messages,
memory search highlighting with centered snippets, keyboard shortcuts
(Ctrl+1-5 tabs, Ctrl+K focus, Ctrl+N new thread, Escape close/blur),
activity tab toolbar with event type filter and auto-scroll toggle.
Phase 3: Thread sidebar with load/switch/create, thread_id passed with
messages, collapsible to hamburger. Memory inline editing with textarea,
Save/Cancel, POST to /api/memory/write.
Phase 4: Gateway status popover on hover (polls every 30s), extension
install form (name/URL/kind), markdown rendering in memory viewer for
.md files, mobile responsive layout at 768px breakpoint.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add routines system, remove non-sandbox job mode from web UI
Routines: scheduled & reactive job system with cron and event triggers,
lightweight (single LLM call) and full-job execution modes, guardrails
(cooldown, max concurrent, dedup), and LLM-facing tools for CRUD.
Web UI: remove ContextManager-backed "direct" job mode entirely. Jobs
are now exclusively sandbox-backed (DB + container). Simplify job detail
response, drop dead types (ActionInfo, MessageInfo, MessageToolCallInfo),
fix Browse Files CSS loading (trailing-slash redirect), fix Activity tab
event rendering.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Re-enable chat input on agent completion, auto-auth on tool_activate, recover tool calls from content XML
Three fixes:
1. Chat input stays disabled after agent finishes: the "Done" status
SSE event now calls enableChatInput() as a safety net when the
response event is empty or lost. Same for auth_completed and
cancelAuth().
2. tool_activate never triggers auth: when activation fails due to
missing authentication, it now auto-initiates the auth flow
(same pattern as the web API handler). detect_auth_awaiting()
also matches tool_activate results now.
3. Models like GLM-4.7 emit tool calls as XML tags in content
(<tool_call>tool_list</tool_call>) instead of using the structured
tool_calls array. recover_tool_calls_from_content() extracts and
validates these before falling back to plain text.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add routines web UI tab, update docs for sandbox-jobs branch
Add full routines management to the web gateway (list, detail, trigger,
toggle, delete) with 7 new API endpoints, response types, and frontend
(HTML, JS, CSS). Update FEATURE_PARITY.md (~23 rows), CLAUDE.md (new
subsystems, config, TODOs), and README.md (architecture diagram,
features, components, fix onboard command).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Bind Telegram bot to owner account during setup
Without owner binding, anyone who discovers the bot can send it messages.
The setup wizard now prompts the user to message their bot, captures their
Telegram user ID via getUpdates, and persists it as telegram_owner_id in
settings. On startup, the owner_id is injected into the WASM channel config
so the existing owner restriction logic drops messages from non-owners.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Move settings from disk to PostgreSQL database
Settings previously lived in three JSON files on disk (settings.json,
mcp-servers.json, session.json). This made them inaccessible from the
web UI and caused redundant disk reads (Settings::load() called 8+
times during startup).
Now all settings live in a `settings` table (user_id + key -> JSONB)
with only 4 bootstrap fields remaining on disk (database_url, pool
size, secrets key source, onboard_completed) since they're needed
before the DB connection exists.
- Add V8 migration for settings table
- Add BootstrapConfig (thin disk file) and Settings DB round-trip
- Add Store CRUD methods for settings (get/set/delete/list/bulk)
- Refactor Config to load from DB (env > DB > default cascade)
- Add SessionManager DB persistence for session tokens
- Add DB-backed MCP server config load/save functions
- Add 6 settings web API endpoints (list/get/set/delete/export/import)
- Add one-time disk-to-DB migration on first boot
- Make CLI config commands async with DB access (disk fallback)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Seed workspace on boot, fix gateway duplicate logs and URL auto-auth
- Add Workspace::seed_if_empty() to create core identity files (README,
MEMORY, IDENTITY, SOUL, AGENTS, USER, HEARTBEAT) when missing, called
on every boot without overwriting existing user edits
- Remove duplicate gateway log lines from web/mod.rs (main.rs has the
useful clickable ?token= URL)
- Auto-authenticate from ?token= URL parameter in the web UI and strip
the token from the address bar after successful auth
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Harden sandbox security (path traversal + orchestrator auth)
Two vulnerabilities fixed:
1. project_dir path traversal: The create_job tool let the LLM specify
arbitrary host paths for Docker bind mounts. Removed project_dir from
the tool schema entirely, and added canonicalization + prefix validation
at both resolve_project_dir() and the job_manager bind mount point.
2. Orchestrator API auth bypass: worker_auth_middleware was defined but
never applied. Each handler manually called validate_token(), so any
new endpoint that forgot would be publicly accessible. Applied the
middleware as route_layer on all /worker/ routes, removed manual auth
from all 7 handlers. Bind to 127.0.0.1 on macOS/Windows (Linux keeps
0.0.0.0 since containers reach host via docker bridge, not loopback).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Rework gateway chat with pinned assistant, pagination, and NEAR AI response chaining
Implements the 4-phase plan for overhauling the web gateway chat:
- Phase 1: Pinned "Assistant" thread at top of sidebar, regular threads below
- Phase 2: Cursor-based history pagination with infinite scroll
- Phase 3: NEAR AI previous_response_id chaining (delta-only messages),
with fallback to full history on chain errors, and DB persistence of
chain state across restarts
- Phase 4: SSE thread isolation (events filtered by thread_id)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Add per-request HTTP timeout to WASM host, redact credentials in errors
Three fixes for WASM channel reliability:
1. Per-request timeout: Add optional timeout-ms parameter to http-request
in both channel and tool WIT interfaces. Telegram long-poll now specifies
35s (outliving the 30s server-side hold), while regular API calls use
the 30s default. Fixes the triple-30s timeout race that caused polling
failures.
2. Credential redaction: reqwest::Error includes the full URL (with injected
bot tokens) in its Display output. Scrub credential values from error
messages before logging or returning to WASM.
3. Webhook route registration: Remove tunnel URL gate so webhook routes are
always available when webhook channels exist, not only when TUNNEL_URL
is configured.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: Fix clippy warnings in WASM tools and channels
- slack channel: allow dead_code on signing_secret_name (forward compat field)
- gmail tool: use div_ceil() instead of manual (n+2)/3
- google-calendar tool: extract CreateEventParams/UpdateEventParams structs
to fix too-many-arguments warnings
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fix approval flow
* fix: Rebuild bundled telegram.wasm with updated WIT interface
The bundled WASM binary must match the host's WIT definition.
Previous binary was compiled against the old 4-arg http-request;
this rebuild includes the new timeout-ms parameter.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: Load WASM channels from disk instead of bundling in binary
Remove include_bytes! embedding of telegram.wasm. Channels are now
loaded from their build output directories (channels-src/<name>/target/)
during onboarding, then from ~/.ironclaw/channels/ at runtime.
- bundled.rs: locate_channel_artifacts() finds WASM + capabilities from
build output; IRONCLAW_CHANNELS_SRC env var overrides the default path
- available_channel_names(): only lists channels with build artifacts
- bundled_channel_names(): lists all known channels (manifest)
- Setup wizard uses available_channel_names() to offer installable channels
- Add *.wasm to .gitignore, remove tracked telegram.wasm
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Persist gateway auth token, fix thread hydration race, polish auth screen
Three web gateway UX fixes:
1. Token persistence: Store auth token in sessionStorage so refreshing
the page doesn't force re-authentication. Hide the auth screen
immediately when a saved token exists to prevent flash.
2. Thread hydration: Remove the !msgs.is_empty() bail-out in
maybe_hydrate_thread so that even brand-new (empty) assistant threads
get hydrated with their correct DB UUID. Previously resolve_thread
would mint a fresh UUID, causing messages to land in the wrong
conversation and duplicate threads to appear.
3. Auth screen: Redesign as a centered card with brand, tagline, labeled
input, and hint text.
Also adds 34 new tests covering session/thread lifecycle, thread
resolution isolation (user, channel, external ID), hydration edge cases,
serialization round-trips, approval flows, and stale mapping recovery.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Use bindgen! for WASM tool wrapper, add dev tool loading
Three changes:
1. Rewrite src/tools/wasm/wrapper.rs to use wasmtime::component::bindgen!
instead of manual linker.root().func_wrap(). This fixes the
"component imports instance 'near:agent/host', but a matching
implementation was not found in the linker" error. All 6 host functions
(log, now-millis, workspace-read, http-request, secret-exists,
tool-invoke) are now properly registered under the near:agent/host
namespace. Also adds WASI support, credential injection, and leak
detection for HTTP requests made by WASM tools.
2. Add dev tool loading to src/tools/wasm/loader.rs. During startup, the
loader now also scans tools-src/*/target/wasm32-wasip2/release/ for
build artifacts that are newer than installed copies. This means during
development you just rebuild the WASM and restart the host; no manual
copy step needed. Set IRONCLAW_TOOLS_SRC to override the source dir.
3. Wire up load_dev_tools() in main.rs alongside the existing
load_from_dir() call.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Wire main startup and CLI to use DB-backed settings
main.rs now reloads Config from the database after connecting,
attaches the store to the session manager for dual-write tokens,
and loads MCP servers from DB instead of disk. ExtensionManager
and MCP CLI commands use DB when available with disk fallback.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Auth mode: when a tool requires an API key, the thread enters a special
mode where the next user message is routed directly to the credential
store, bypassing logs, turns, history, and compaction entirely. This
prevents tokens from leaking into debug output or persistent storage.
Fix MCP auth: auth_mcp now actually uses the token parameter (was
ignored as _token) and falls back to manual token entry when OAuth
and DCR are both unsupported.
Parallel loading: WASM tools, WASM channels, and MCP servers now load
concurrently at startup. Within each loader, individual items also
load in parallel (join_all for WASM, JoinSet for MCP servers).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace the dual-server architecture (HttpChannel + WasmChannelServer both
competing for port 8080) with a single WebhookServer that composes route
fragments from all sources. Channels define routes but never spawn servers.
- Add WebhookServer struct that collects Router fragments and binds one listener
- Extract routes() from HttpChannel, remove server-spawning from start/shutdown
- Delete WasmChannelServer (keep WasmChannelRouter and route builder)
- Rewire main.rs to compose all webhook routes into one server
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Consolidate channel sources into channels-src/ by moving whatsapp from
channels/. Add on_status stubs to Slack and WhatsApp so their WASM
binaries export the function added in the latest WIT. Fix Slack's
emit_message call to pass by reference (API changed). Guard WASM webhook
server startup to skip when the HTTP channel already occupies port 8080.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Serde alias on `onboard_completed` preserves existing settings.json files
that still have the old `setup_completed` key.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Drop the full Ratatui TUI in favor of a lighter REPL channel built on
rustyline (line editing, history, tab-completion) and termimad (inline
markdown rendering). Removes ratatui and crossterm event-stream deps,
adds rustyline and termimad. Simplifies main.rs startup to use the REPL
directly instead of the alternate-screen TUI.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Introduces a unified extension abstraction over MCP servers and WASM tools
with six agent-callable tools (tool_search, tool_install, tool_auth,
tool_activate, tool_list, tool_remove) so users can add capabilities
conversationally without CLI commands.
Includes built-in registry of 11 MCP servers, online discovery via URL
probing and GitHub search, OAuth 2.1 flows for MCP servers, and manual
token auth for WASM tools.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Enables connecting to official MCP servers (like Notion) instead of
building custom WASM tools. Uses OAuth 2.1 with PKCE and supports
Dynamic Client Registration for zero-config authentication.
Key features:
- OAuth 2.1 flow with PKCE for secure browser-based auth
- Dynamic Client Registration (DCR) for servers without pre-configured clients
- Automatic token refresh on 401 responses
- Session management with Mcp-Session-Id headers
- SSE streaming response handling
New CLI commands:
- `mcp add <name> <url>` - Add an MCP server
- `mcp remove <name>` - Remove an MCP server
- `mcp list` - List configured servers
- `mcp auth <name>` - Authenticate with a server
- `mcp test <name>` - Test connection
Also removes the Notion WASM tool example since it's superseded by the
Notion MCP server which provides 13 official tools.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Renamed project from "near-agent" to "ironclaw" throughout the codebase.
Updated documentation to emphasize the core philosophy:
- Your data stays yours (local, encrypted, no telemetry)
- Self-expanding capabilities (build tools on the fly)
- Defense in depth (WASM sandbox, prompt injection defense)
- Always on user's side
Key changes:
- Package name: near-agent -> ironclaw
- Config paths: ~/.near-agent/ -> ~/.ironclaw/
- Database name in docs: near_agent -> ironclaw
- CLI binary: near-agent -> ironclaw
- Log filters: RUST_LOG=near_agent -> RUST_LOG=ironclaw
- All user-facing strings (welcome messages, help text, etc.)
Preserved for compatibility:
- HKDF salt "near-agent-secrets-v1" (changing would break existing secrets)
- WIT interface names (near::agent::*)
- NEAR AI provider config (NEARAI_* env vars)
Co-Authored-By: Claude Opus 4.5 <[email protected]>
The WASM channel no longer needs its own polling_enabled/tunnel_url settings.
Instead, the host injects tunnel_url and webhook_secret into the channel config
at runtime before start() is called.
Changes:
- Add update_config() method to WasmChannel for runtime config injection
- Simplify TelegramConfig to only have bot_username, respond_to_all_group_messages
- Host injects tunnel_url (from Settings) and webhook_secret (from secrets store)
- Channel checks if tunnel_url is present to determine webhook vs polling mode
- Add delete_webhook() for clean transition to polling mode when no tunnel
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Remove the brittle natural language pattern matching from the router
and add job management tools to the normal tool registry instead.
- Add job tools: create_job, list_jobs, job_status, cancel_job
- Router now only handles explicit /commands
- Natural language goes through agentic loop with all tools
- LLM naturally picks appropriate tools based on user intent
- Share ContextManager between job tools and Agent
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Enable instant message delivery for Telegram via webhooks instead of polling.
Key changes:
- Add tunnel URL configuration for local development (ngrok, cloudflare)
- Auto-register webhook with Telegram API on startup using setWebhook
- Implement webhook secret validation via X-Telegram-Bot-Api-Secret-Token header
- Add credential injection for bot token via URL placeholder substitution
- Fix metadata preservation in respond() to route replies correctly
- Fix serde flatten with Option<T> issue in capabilities schema parsing
The credential injection pattern replaces {TELEGRAM_BOT_TOKEN} placeholders
in URLs with the actual token from the secrets store, keeping credentials
out of WASM module memory until the HTTP request is made.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Introduces `near-agent setup` command that guides users through:
- NEAR AI authentication (reuses existing OAuth flow)
- Model selection (fetches from API or shows defaults)
- Channel configuration (HTTP webhook, Telegram)
Features:
- First-run detection: auto-runs wizard if no session exists
- Respects existing settings: shows current model with keep/change option
- Saves channel secrets to ~/.near-agent/secrets/ with 0600 permissions
- Validates Telegram bot tokens via API before saving
Also fixes default NEARAI_BASE_URL to use cloud-api.near.ai (api.near.ai
returns 410 Gone).
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add NearAiChatProvider using /v1/chat/completions endpoint with API key auth
- Add NEARAI_API_KEY and NEARAI_API_MODE config options
- Auto-detect API mode from presence of API key
- Keep existing Responses API (NearAiProvider) for session-based auth
- Fix response parsing to accept input_text/output_text/text content types
- Expand REPL with /help, /debug toggle, colored output
- Better tool status display (dots vs verbose based on debug mode)
Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit addresses three critical issues from code review:
1. Tool approval enforcement: Tools declaring requires_approval() (shell,
http, file write/patch, build_software) now gate execution. Adds
PendingApproval struct, session-scoped auto-approved tools set, and
approval flow with yes/no/always commands.
2. Tool definition refresh: Tool definitions now refresh each iteration
in both chat and job loops, so newly built tools become visible
immediately within the same session.
3. Worker tool call handling: Changed respond() to respond_with_tools()
when select_tools returns empty, properly executing tool calls instead
of formatting them as text.
Also includes prior work from the plan:
- Wire embeddings provider (OpenAI + NEAR AI) to workspace
- Load workspace system prompt (identity files) into LLM context
- Route heartbeat notifications through channel manager
- Enable auto-context compaction when threshold exceeded
- Refactor to config structs (AgentDeps, WorkerDeps, LlmCallRecord)
- Fix clippy warnings (saturating_sub, too_many_arguments)
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add HeartbeatConfig for proactive periodic execution with channel notifications
- Add use_planning option to Worker for ActionPlan generation before tool execution
- Implement tool failure tracking in database (V3 migration)
- Add auto-repair via Builder for broken WASM tools in self_repair.rs
- Record tool failures in Worker for self-repair tracking
- Update .env.example with new configuration options
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Introduces a new CLI module with subcommands for managing WASM tools:
- `tool install`: Build and install tools from source or .wasm files
- `tool list`: List installed tools with optional verbose output
- `tool remove`: Remove installed tools
- `tool info`: Show detailed tool information including capabilities
The install command supports building from Cargo source directories using
cargo-component, or installing pre-compiled .wasm files directly. It
auto-detects capabilities JSON sidecar files and validates them before
installation.
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Remove SimpleCliChannel (only TuiChannel remains)
- Add ctrl_d_pending flag to AppState for two-press quit behavior
- Implement Ctrl+D twice to quit (first press shows hint, second quits)
- Add TuiLogWriter with MakeWriter impl for tracing integration
- Create TuiChannel event channel upfront in new() so log_writer() works
- Configure tracing to send logs to TUI status line
- Any key press clears the Ctrl+D pending state
Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Store event_tx in Arc<Mutex<>> so respond() can send to TUI
- Fix run_event_loop to take owned receiver
- Switch main.rs from SimpleCliChannel to TuiChannel
- Add proper terminal cleanup on TUI error
Co-Authored-By: Claude Opus 4.5 <[email protected]>