* feat: add bundled and declarative hook bundle loading
* fix: load plugin hooks only for active extensions
* fix: avoid duplicate plugin hook registration
* security: harden outbound webhook hooks
* fix: pin webhook DNS resolutions for outbound hooks
* fix: block IPv4-mapped local webhook targets
* style: format webhook hardening changes for CI
* fix: pass HookRegistry to ExtensionManager in AppBuilder
After merging main (which extracted AppBuilder from main.rs in #198),
the ExtensionManager::new() call in app.rs was missing the `hooks`
parameter that PR #176 added. This moves HookRegistry creation before
init_extensions() and threads it through, matching the existing pattern
in main.rs.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* docs(security): add network security reference for all listeners
Catalogs every network-facing surface (web gateway, webhook server,
orchestrator API, OAuth callback, sandbox proxy) with auth mechanisms,
bind addresses, egress controls, known findings, and a review checklist
for PRs that touch network-facing code.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(security): address three network security findings
- Use constant-time comparison (ct_eq) for webhook secret validation,
matching the pattern in web gateway and orchestrator auth
- Add X-Content-Type-Options and X-Frame-Options security headers to
the web gateway via SetResponseHeaderLayer
- Warn at startup when HTTP webhook server binds to 0.0.0.0
- Update NETWORK_SECURITY.md to mark findings 1, 4, 5 as resolved
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(security): address PR #201 review findings
- Reorder web gateway layers so security headers (X-Content-Type-Options,
X-Frame-Options) are outermost and apply to all responses including
DefaultBodyLimit 413 rejections
- Move 0.0.0.0 warning to final bind address resolution so it fires for
WASM-only webhook servers that fall back to the default address
- Add webhook handler auth tests: correct secret -> 200, wrong secret
-> 401, missing secret -> 401
- Rewrite NETWORK_SECURITY.md: replace brittle line-number references
with function/struct name anchors, add threat model section, document
graceful shutdown per listener, fill content gaps (health endpoint
responses, content-type validation, CSRF analysis, WS auth flow, MCP
trust boundary, orchestrator rate limiting), change findings F-4/F-5
from "Resolved" to "Mitigated" with caveats
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt and clippy warnings from main merge
Fix formatting in llm/mod.rs and llm/rig_adapter.rs introduced by
PR #132, and collapse nested if in rig_adapter.rs per clippy.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: support per-request model override for /v1/chat/completions
- add optional model override to completion request types\n- forward request model through gateway, worker, and orchestrator proxy paths\n- use request model in NEAR AI providers with fallback to active model\n- replace model-mismatch integration test with override propagation checks\n- update FEATURE_PARITY.md note for OpenAI-compatible API behavior\n\nRefs #49
* Wire gateway OpenAI-compatible routes to active LLM provider
* Validate OpenAI model name length before streaming
* Address PR103 review feedback on model override and validation
* Report effective model in OpenAI-compatible responses
* Use async mutexes in OpenAI compatibility integration tests
* fix tests for per-request model field in response cache
* fix formatting and clippy lint after main merge
* Fix model override reporting and cache correctness
---------
Co-authored-by: Illia Polosukhin <[email protected]>
* fix(rig): prevent responses API panic on missing tool call IDs
* style: format rig adapter
* test(rig): add coverage for empty/whitespace tool call IDs
Add tests for assistant tool calls with empty and whitespace-only IDs,
and an end-to-end test documenting the seed mismatch limitation when
both assistant call and tool result are missing IDs.
* Apply suggestions from code review
Co-authored-by: Copilot <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Copilot <[email protected]>
* fix: prevent division-by-zero panic in ValueEstimator::is_profitable
Guard against Decimal division by zero when price is zero.
rust_decimal::Decimal panics on division by zero (unlike f64 which
returns infinity), so we short-circuit before the division.
When price is zero, a job is only profitable if the estimated cost
is negative (i.e., we get paid to do it).
Add test covering zero-price scenarios including the negative cost
edge case.
* style: fix pre-existing rustfmt and clippy issues in llm module
Fix formatting and lint issues that cause CI Code Style check to fail:
- src/llm/mod.rs: fix method chain indentation
- src/llm/rig_adapter.rs: collapse multi-line single-expression statements,
fix collapsible_if clippy warning
* Fix Telegram control commands being stripped
The `clean_message_text()` function was returning an empty string for
bare slash commands like `/interrupt`, `/stop`, `/help`, etc. This
caused the commands to be replaced with "[User started the bot]" placeholder
which broke command parsing in the agent.
Changes:
- Line 1079: Return the command unchanged instead of empty string
- Line 1042: Only replace with placeholder for `/start` specifically
- Add test coverage for control commands
This fixes the issue where `/interrupt` doesn't work when bot is stuck
waiting for approval.
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
* Add workspace declaration to Telegram package
Fixes workspace conflict when building WASM component standalone.
* Fix content_to_emit logic for bare control commands
Addresses code review feedback: keep clean_message_text() returning
empty for bare commands (its job is to extract user text, not pass
commands through). Instead, fix the caller to distinguish:
- /start (no args) → welcome placeholder
- Other bare /commands → pass raw command to Submission::parse()
- Commands with args → pass cleaned args
- Empty/whitespace → skip
Add comprehensive test_content_to_emit_logic() covering all edge cases
including /start, control commands, args, plain text, and empty input.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: ubuntu <ubuntu@tyo-dev>
Co-authored-by: Claude Sonnet 4.5 <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* fix: add missing type key to http tool body schema
The body property in HttpTool::parameters_schema() was missing the
required \"type\" key, causing OpenAI to reject all tool calls with:
Invalid schema for function 'http'
Fixes#131
* fix: add missing type key to json tool data schema
Same class of bug as http tool body — the data property in
JsonTool::parameters_schema() was missing the required "type" key,
causing OpenAI to reject all tool calls.
Fixes#131
* fix: use Chat Completions API to avoid rig-core Responses API panic
The default openai::Client routes through rig-core's Responses API,
which panics at "The tool call ID should exist!" because ironclaw
doesn't thread call_id through its ToolCall type. Switch to
openai::CompletionsClient which uses the Chat Completions API and works
correctly with the existing code.
* fix: normalize tool schemas for OpenAI strict mode compliance
GPT-5/5.2 enforce strict function calling by default. Add
normalize_schema_strict() that recursively transforms tool parameter
schemas at the provider boundary:
- Forces additionalProperties: false on all objects
- Makes required list ALL property keys
- Converts optional fields to nullable types
- Handles nested objects, array items, and combinators
Original schemas remain unchanged for other providers.
Closes#131
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Scanned the repo and past two weeks of commits to reconcile the feature
matrix with reality. Upgraded implemented features from ❌ to ✅ (skills,
memory CLI, embeddings batching, session permissions, OpenRouter, Ollama).
Marked partial implementations as 🚧 (agent event broadcast, payload
guard, skill routing, env sanitization). Added new OpenClaw features from
Feb 2025 (Telegram/Discord/Slack-specific, new hooks, security items).
Added IronClaw-only entries (Tinfoil, OpenAI-compatible, GitHub WASM tool).
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add issue triage skill
Adds a /triage-issues skill that classifies open GitHub issues into bugs
and feature requests, ranks bugs by severity and features by opportunity,
and flags under-specified issues needing clarification.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback on issue triage skill
- Fix invalid `comments` field to `commentsCount` + add `reactionGroups`
- Correct severity/opportunity max scores from 17 to base 14 (boosted 16)
- Clarify boost is one-time (+2 if any condition matches)
- Add explicit `gh pr list` command for PR exclusion filtering
- Adjust severity/opportunity thresholds in report section
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: split large files and consolidate test stubs for contributor velocity
- Extract 7 Database sub-traits (ConversationStore, JobStore, SandboxStore,
RoutineStore, ToolFailureStore, SettingsStore, WorkspaceStore) with Database
as a supertrait combining them all
- Split libsql_backend.rs (2769 lines) into src/db/libsql/ directory with
one file per sub-trait implementation
- Split config.rs (1753 lines) into src/config/ directory with 16 domain files
- Consolidate 3 duplicate test LLM stubs into shared StubLlm in src/testing.rs
- Split server.rs handlers into src/channels/web/handlers/ directory
- Extract main.rs init phases into AppBuilder (src/app.rs)
- Add developer setup script (scripts/dev-setup.sh)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: move heartbeat test from examples/ to tests/
Convert standalone example binary into a proper #[ignore] integration
test, matching the convention of the other integration tests.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix rustfmt formatting for CI
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review comments from Copilot
- tunnel.rs: replace .ok().flatten() with ? to propagate env var errors
- secrets.rs: remove misleading "process-wide cache" comment
- database.rs: use uppercase "DATABASE_URL" in error key
- testing.rs: gate harness tests with #[cfg(feature = "libsql")]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add PR triage dashboard skill
Adds /triage-prs slash command that classifies all open PRs by module,
review state, scope, and architectural impact to produce a prioritized
triage dashboard for maintainers.
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>
* Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* fix: address review feedback on triage-prs skill
- Add body and updatedAt to PR query fields for superseded detection
- Use --label/--author flags directly instead of post-filtering
- Use date-based --search for merged PRs instead of --limit 20
- Simplify LLM module listing, add missing module categories
- Use updatedAt for staleness, clarify lines changed metric
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* fix(security): prevent path traversal bypass in WASM HTTP allowlist
The allowlist validator checked url_path.starts_with(prefix) on the
raw, unnormalized path. A WASM tool could request a URL like:
https://api.openai.com/v1/../admin
The starts_with("/v1/") check would pass, but the server would
resolve the ".." and serve /admin — effectively bypassing the
path prefix restriction.
This commit adds normalize_path() which resolves . and .. segments
before validation, closing the bypass. It also includes 6 new tests
covering traversal attacks and normalization correctness.
* deslop: remove redundant comments, consolidate tests
* chore(allowlist): trim nonessential traversal helper comment
* harden URL parsing for wasm allowlist and proxy paths
---------
Co-authored-by: Illia Polosukhin <[email protected]>
The benchmarks crate is an internal tool, not intended for crates.io.
Adding `publish = false` fixes the release-plz CI failure caused by
the path-only ironclaw dependency lacking a version specifier.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Remove unused fields, methods, and error variants. Allow dead_code on
public API types intended for future use. Drop needless Default spread.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: persist OpenAI-compatible provider and respect embeddings disable (#129)
Three interrelated bugs caused the agent to ignore user choices made
during onboarding when using an OpenAI-compatible LLM provider:
1. Session auth ran before DB config reload, so Config::from_env()
defaulted to NearAi and attempted Clerk auth before the real
backend was known. Moved session auth to after final config
resolution.
2. EmbeddingsConfig::resolve() force-enabled embeddings whenever
OPENAI_API_KEY was present, ignoring the user's explicit disable.
Changed to respect the stored setting as source of truth.
3. LLM_BACKEND was not saved to the bootstrap .env file, so
Config::from_env() always defaulted to NearAi before the DB
was connected. Now saves LLM_BACKEND, LLM_BASE_URL, and
OLLAMA_BASE_URL alongside the database bootstrap vars.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add SAFETY comments and sanitize .env value escaping
Address PR review feedback:
- Add SAFETY comments to all unsafe env var manipulation in config
tests (gemini-code-assist).
- Escape backslashes and double quotes in save_bootstrap_env() to
prevent env var injection via malicious URLs (gemini-code-assist).
- Add test verifying injection attempt is neutralized.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: incorporate PR #138 changes (chat completions, model sorting, tool schemas)
Includes all changes from bigguybobby's PR #138:
- Use Chat Completions API for OpenAI-compatible providers (avoids
Responses API assumptions like required tool call IDs)
- Fall back to settings.selected_model when LLM_MODEL env var is unset
- Update OpenAI model list (add gpt-5 family) with priority-based sorting
- Add is_openai_chat_model() filter with broader exclusion patterns
- Fix http tool: headers schema → array of {name,value}, body → string type,
parse_headers_param() accepts both legacy object and array formats
- Fix json tool: data schema → string type, parse_json_input() normalizer,
validate uses strict string-only check
- Add mutex-serialized config tests for env var manipulation
- Update NEAR AI config comment for accuracy
Co-Authored-By: Bobby (bigguybobby) <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Bobby (bigguybobby) <[email protected]>
* fix: remove .expect() calls in FailoverProvider::try_providers (#155)
Replace two .expect() calls with proper error propagation to comply
with the project no-panic convention. Both were logically unreachable
but would panic if invariants were broken by a future refactor.
Closes#155
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Apply suggestions from code review
Co-authored-by: Copilot <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Copilot <[email protected]>
ProviderCooldown used 0 as both the "not in cooldown" sentinel and a
valid timestamp from now_nanos(), so activate_cooldown(0) would silently
fail to activate. Store max(now_nanos, 1) to keep 0 reserved.
Closes#125
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add Tinfoil private inference provider
Add a dedicated Tinfoil LLM backend (`LLM_BACKEND=tinfoil`) for
Tinfoil's private inference service (https://tinfoil.sh).
The existing `openai_compatible` backend cannot be used with Tinfoil
because rig-core 0.30.0 defaults to the OpenAI Responses API
(`/v1/responses`), which Tinfoil does not support — it only implements
the Chat Completions API (`/v1/chat/completions`), returning 403
"shim: path not allowed" when hit on the responses endpoint.
Rather than changing `openai_compatible` to use Chat Completions (which
would break users expecting the Responses API), this adds a dedicated
provider that explicitly uses rig's `.completions_api()` client.
This also lays the groundwork for integrating Tinfoil's privacy wrapper
client (enclave attestation, TLS certificate pinning) once their Rust
SDK is available. The provider implementation can be swapped to use the
Tinfoil Rust client without changing the LlmProvider interface.
Configuration:
LLM_BACKEND=tinfoil
TINFOIL_API_KEY=tk_...
TINFOIL_MODEL=kimi-k2-5 # optional, default
* style: fix rustfmt formatting in Tinfoil provider
* style: remove unnecessary tin_foil alias for Tinfoil backend
* Update src/llm/mod.rs
Co-authored-by: Copilot <[email protected]>
* fix: add tinfoil field to LlmConfig test fixture
* style: fix rustfmt output in session manager
---------
Co-authored-by: firat.sertgoz <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
* fix: skills module audit cleanup — deduplicate loading, async gating, pre-compute scoring fields
Address 7 issues from the skills module audit (#157–#163):
- Extract shared `load_and_validate_skill` helper, eliminating ~90 lines
of duplication between `load_skill_md` and `load_skill_md_standalone`
- Wrap blocking gating subprocess calls (`which`/`where`) in
`tokio::task::spawn_blocking` to avoid blocking the async runtime
- Remove dead `SkillParseError::FileTooLarge` and `SkillSource::Registry`
- Replace `HashMap<String, ()>` with `HashSet<String>` in discovery
- Fix misleading doc comment and unnecessary `ref` clone pattern
- Use `CARGO_PKG_VERSION` for catalog HTTP user-agent instead of
hardcoded "0.1"
- Pre-compute lowercased keywords/tags at load time to avoid
per-message allocation in the scoring hot path
- Add tests for flat SKILL.md layout, mixed layouts, and lowercased
field population
Closes#157, closes#158, closes#159, closes#160, closes#161,
closes#162, closes#163
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #173 review feedback
- Distinguish cancel vs panic in spawn_blocking JoinError and include
error details in the gating failure message (Copilot review)
- Restore lowercased_keywords/lowercased_tags to `pub` for consistency
with other LoadedSkill fields (Copilot review)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: shell env scrubbing and command injection detection
Add two security hardening layers to the shell tool:
1. Environment scrubbing (CWE-200): When executing commands directly
(no sandbox), clear the process environment and only forward safe
variables (PATH, HOME, LANG, CARGO_HOME, etc.). API keys, session
tokens, and credentials are no longer inherited by child processes.
2. Command injection detection: Catch obfuscation and exfiltration
patterns that bypass existing blocked/dangerous command checks:
- Null bytes (bypass string matching)
- Base64/hex/xxd decode piped to shell
- DNS exfiltration via command substitution
- Netcat with data piping
- curl/wget posting file contents
- String reversal piped to shell
Includes 14 new tests covering all injection patterns, false negative
verification for legitimate dev workflows, and env scrubbing validation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address codex review findings
- Add Windows env vars to SAFE_ENV_VARS (SystemRoot, ComSpec, PATHEXT,
etc.) so env scrubbing doesn't break direct execution on Windows.
- Add has_command_token() helper for word-boundary-aware command
matching. Prevents false positives where substrings match: "sync"
no longer triggers "nc" detection, "ghost"/"--host" no longer
triggers "host" detection, "digital" no longer triggers "dig".
- Use has_command_token() in DNS exfil and netcat checks.
- Add regression tests for all identified false positive scenarios.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback
- Fix contains_shell_pipe word boundary: "| shell", "| shift", "| show"
no longer false-positive against "| sh". Uses has_pipe_to() helper
that validates the char after the shell name.
- Add "dash" to shell interpreter list.
- Add PWD to SAFE_ENV_VARS (many tools and scripts depend on it).
- Add curl -d@file (no space) pattern to injection detection.
- Use has_command_token for "od " to avoid matching "method", "period".
- Switch env-mutating tests to #[tokio::test(flavor = "current_thread")]
to prevent data races (tokio defaults to multi-threaded runtime).
- Add regression tests for all fixed false-positive scenarios.
- Add more legitimate pipe-heavy commands to false-negative test.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add PR review tools, job monitor, and channel injection for E2E sandbox workflows
Adds JobEventsTool and JobPromptTool so the main agent can read container
event logs and send follow-up prompts to running Claude Code sessions.
A background JobMonitor forwards container assistant messages into the
agent loop via a new inject channel on ChannelManager.
CreateJobTool now accepts a project_dir parameter for mounting existing
cloned repos into containers, and spawns the monitor automatically for
async jobs.
Also: Dockerfile bumped to Rust 1.88 (rig-core needs let chains),
GITHUB_TOKEN forwarded into containers for gh CLI auth, and truncate()
fixed for multi-byte char boundary panics.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR #57 review comments (IDOR, Dockerfile, truncate, logging)
- Add ownership checks to JobEventsTool and JobPromptTool via ContextManager
to prevent users from accessing other users' jobs (IDOR)
- Combine Dockerfile gh CLI install into single apt-get layer
- Handle truncate() edge case when max falls inside first multi-byte char
- Log actual count of registered job management tools
- Document fire-and-forget job monitor lifecycle
- Add tests for ownership rejection and schema validation
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Replace hardcoded GITHUB_TOKEN with on-demand credential delivery
Containers now fetch credentials via authenticated GET /worker/{id}/credentials
endpoint instead of receiving them baked into env vars at creation time. Secrets
are decrypted from SecretsStore on demand, scoped per-job via CredentialGrant,
and revoked automatically when the job completes.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address sandbox audit findings (CONNECT tunnel, readonly_rootfs, type consolidation)
- Implement real CONNECT tunnel with bidirectional TCP piping via hyper upgrade
- Fix readonly_rootfs to apply for both ReadOnly and WorkspaceWrite policies
- Consolidate duplicate CredentialMapping/CredentialLocation into secrets::types
- Share reqwest::Client across proxy requests instead of per-request allocation
- Store Docker connection and reuse across executions
- Remove .unwrap() from proxy response builders with safe fallbacks
- Add output truncation to direct (non-container) execution (64KB limit)
- Delete dead src/tools/sandbox.rs (ToolSandbox never used)
- Fix connect_docker error message to list all attempted socket paths
- Update proxy credential injection to handle all CredentialLocation variants
- Use glob-based host_patterns matching for credential lookup in proxy policy
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address PR #57 review comments (IDOR, Dockerfile, truncate, logging)
- Dockerfile: install curl+ca-certificates before fetching GitHub CLI GPG key
- JobEventsTool/JobPromptTool: reject missing context (prevents IDOR bypass)
- parse_credentials: validate env var names against denylist and pattern
- resolve_project_dir: require explicit paths to exist before validation
- Credential serving: lower log level from info to debug
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address orchestrator audit findings (constant-time auth, error handling, tests)
- auth: constant-time token comparison via subtle::ConstantTimeEq
- auth: replace hand-rolled hex_encode with std::fmt::Write fold
- api: report_status now updates ContainerHandle (was a no-op)
- api: log complete_job errors instead of silently discarding
- job_manager: log Docker cleanup errors in stop_job/complete_job
- job_manager: extract validate_bind_mount_path with proper error on
missing home_dir and mandatory base dir creation before canonicalize
- job_manager: cache Docker connection across operations
- error: remove dead OrchestratorError::AuthFailed and ContainerTimeout
- Add 13 new tests (prompt queue, credentials, events, status, paths)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use floor_char_boundary in sandbox manager truncate to prevent multi-byte panics
String::truncate() panics when the index falls mid-way through a
multi-byte UTF-8 character. Use the same floor_char_boundary utility
already used in worker/runtime.rs and tools/builtin/shell.rs.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: default base_url to private.near.ai for Responses API mode
Session tokens only authenticate against private.near.ai, not
cloud-api.near.ai. The default base_url now matches the api_mode:
- Responses (session token): https://private.near.ai
- ChatCompletions (API key): https://cloud-api.near.ai
This broke when the multi-provider merge introduced cloud-api.near.ai
as the unconditional default.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use private.near.ai as default base URL for all API modes
private.near.ai now supports both Responses and ChatCompletions
endpoints, so there is no reason to route through cloud-api.near.ai.
This also fixes session token auth which only works against
private.near.ai.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: harden libSQL concurrency, fix Claude Code Docker auth and permissions
Three fixes for the sandbox/Claude Code pipeline:
1. SQLite "database is locked": set WAL journal mode in migrations and
PRAGMA busy_timeout=5000 on every connection across LibSqlBackend,
LibSqlSecretsStore, and LibSqlWasmToolStore (~83 async call sites).
2. Claude Code container auth: extract OAuth token from macOS Keychain
(or Linux ~/.claude/.credentials.json) at startup and inject via
CLAUDE_CODE_OAUTH_TOKEN env var. Removes the broken bind-mount
approach that failed on uid mismatch.
3. Claude Code tool permissions: wire CLAUDE_CODE_ALLOWED_TOOLS env var
through to the worker binary (was hardcoded to empty vec), and expand
defaults to include all standard tools (Read, Write, Edit, Glob, Grep,
NotebookEdit, Bash, Task, WebFetch, WebSearch).
Also adds --verbose flag to claude CLI (required with stream-json + -p),
failover provider model switching, and nearai models endpoint fix.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: stream event parsing, job ID prefix resolution, session renewal in list_models
Three fixes for the Docker/gateway pipeline:
1. Claude Code stream event parsing (claude_bridge.rs): Rewrite
ClaudeStreamEvent to match actual NDJSON format where content blocks
are nested under message.content[], not at the top level. Add handler
for "user" events (tool_result blocks) and emit result text as a
"message" event so reviews appear in gateway activity view.
2. Job ID prefix resolution (job.rs): Add resolve_job_id() that accepts
short hex prefixes (like git short SHAs) in addition to full UUIDs.
The LLM sees truncated IDs in job monitor messages like "[Job f2854dd8]"
and can now use them directly with job_status/cancel/events/prompt tools.
3. Session renewal in list_models (nearai.rs): list_models() now retries
with OAuth renewal on 401, matching send_request()'s existing behavior.
Previously it returned SessionExpired immediately, causing the setup
wizard to fall back to defaults instead of prompting re-authentication.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: /model command now lists available models
Previously /model with no args only showed the current model name.
Now it fetches and displays all available models from the provider,
marking the active one, so users can see what's available before
switching with /model <name>.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #57 review findings (set_var UB, tunnel timeout, restart creds)
- Replace unsafe `std::env::set_var` in worker runtime and Claude bridge
with `Command::envs()` injection via a new `extra_env` field on
`JobContext`, avoiding undefined behavior in the multi-threaded tokio
runtime.
- Add 30-minute timeout to CONNECT tunnel `copy_bidirectional` in the
sandbox proxy to prevent stuck connections from leaking spawned tasks.
- Persist credential grants (as JSON in the description column) on
`SandboxJobRecord` so `jobs_restart_handler` can restore them instead
of passing `vec![]`, which caused restarted containers to lose access
to their original secrets.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address second round of PR #57 review comments
- Normalize host_patterns to lowercase in proxy policy matching
- Push LIMIT into SQL for list_job_events (Database trait + both backends)
- Remove unused was_explicit binding in job tool
- Return 500 instead of 200 in make_response fallback path
- Update copy_auth_from_mount docstring for env-var default
- Use entry.file_type() instead of is_dir() to avoid following symlinks
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address third round of PR #57 review comments
- Restore glob patterns in default_claude_code_allowed_tools (Bash -> Bash(*))
- Add tracing::warn for credential grant serialize/deserialize failures
- Wrap extra_env in Arc<HashMap> to avoid deep cloning per tool call
- Document unsupported credential locations (AuthorizationBasic, UrlPath)
- Document TOCTOU window in validate_bind_mount_path
- Expand doc comments on JobEventsTool and JobPromptTool
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address fourth round of PR #57 review comments
- Document CONNECT tunnel task lifecycle (timeout is the cleanup mechanism)
- Remove secret names from error-level credential logs to prevent leaking
- Expand DANGEROUS_ENV_VARS denylist with language runtime hijack vectors
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address fifth round of PR #57 review comments
- Promote job monitor startup log to info level for observability
- Require minimum 4-char prefix in resolve_job_id to limit enumeration
- Cap credential grants at 20 per job to bound column storage
- Clamp job events limit to 1..1000 to prevent memory abuse
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add missing closing brace for SkillsConfig impl block
The merge resolution dropped the closing `}` for `impl SkillsConfig`,
causing a compilation error in CI.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add secure prompt-based skills system (Phase 1 MVP)
Implement a skills system that extends the agent with prompt-level
instructions from local directories. Skills declare activation criteria,
tool permissions, and trust tiers that determine authority attenuation.
Core security model: the minimum trust level of any active skill
determines a tool ceiling -- tools above the ceiling are removed from
the LLM's tool list entirely at the API level, preventing prompt-based
manipulation.
New modules:
- skills/mod.rs: Core types (SkillTrust, SkillManifest, LoadedSkill)
- skills/scanner.rs: Content scanner for manipulation detection
- skills/registry.rs: Filesystem discovery and manifest parsing
- skills/selector.rs: Deterministic two-phase prefilter (no LLM)
- skills/attenuation.rs: Trust-based tool filtering
Integration:
- Agent loop selects skills per-turn and applies tool attenuation
- Reasoning engine injects skill context with structural isolation
- Config supports SKILLS_ENABLED, SKILLS_DIR, SKILLS_MAX_ACTIVE,
SKILLS_MAX_CONTEXT_TOKENS environment variables
- Disabled by default (SKILLS_ENABLED=false)
41 new tests covering all modules.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address all adversarial review findings for skills system
Security fixes:
- Escape skill name/version in XML attributes to prevent trust spoofing
- Escape prompt content to prevent </skill> tag breakout
- Require integrity hash for Verified/Community tier skills
- Validate skill names against [a-zA-Z0-9][a-zA-Z0-9._-]{0,63}
- Add 64 KiB file size limit on prompt.md
Bug fixes:
- Use actual SkillsConfig from AgentDeps instead of SkillsConfig::default()
- Add skills_config field to AgentDeps, wired through from main.rs
Performance:
- Pre-compile regex patterns at load time (cached on LoadedSkill)
- Selector uses pre-compiled patterns instead of recompiling per message
- Switch all std::fs to tokio::fs for non-blocking async I/O
Hardening:
- Cap keyword score at 30 points to prevent keyword stuffing attacks
- Enforce max 20 keywords and 5 patterns per skill
- Normalize line endings (CRLF/CR to LF) before hashing
- Also includes cargo fmt formatting fixes for adjacent code
Tests: 54 skills tests pass (up from 41), zero new clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address medium/low severity findings from adversarial review
Fixes all 18 medium/low severity findings identified by the security review:
- mod.rs: Add MAX_TAGS_PER_SKILL cap (10) in enforce_limits(); use
RegexBuilder with 64 KiB size_limit to prevent ReDoS; replace
case-enumerated escape_skill_content with regex matching all case
variants plus whitespace/null byte injection between </ and skill;
document allowed_patterns as unenforced until Phase 2; document
Marketplace URL validation as Phase 3 concern
- registry.rs: Add MAX_MANIFEST_FILE_SIZE (16 KiB) check before reading;
add symlink detection via symlink_metadata to reject symlinks in
discover_local; add MAX_DISCOVERED_SKILLS (100) cap; validate
prompt_hash format (sha256: + 64 hex chars); warn on name collision
before overwriting; accept SkillSource parameter in load_skill instead
of always using Local; add InvalidHashFormat, ManifestTooLarge,
SymlinkDetected error variants
- selector.rs: Add MAX_TAG_SCORE (15) cap parallel to keyword cap; warn
when declared max_context_tokens diverges >2x from actual prompt size
- scanner.rs: Add mixed-script homoglyph detection (Cyrillic, Greek,
Armenian unicode ranges); document token-boundary bypass and semantic
paraphrasing as known limitations
- attenuation.rs: Document READ_ONLY_TOOLS maintenance requirements
- agent_loop.rs: Surface scan warnings via structured tracing; add
structured audit events for skill activation and tool attenuation
61 tests pass, 0 new clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address 12 findings from second adversarial security review
HIGH:
- Escape opening <skill tags in prompt content (prevents fake skill block injection)
- Scan manifest metadata fields (description, author, tags, reasons) not just prompt
- Block trust downgrade on name collision (existing Local can't be replaced by Community)
MEDIUM:
- Eliminate TOCTOU gap: read files then check size instead of metadata-then-read
- Reject file-level symlinks in load_skill (prompt.md, skill.toml)
- Truncate and filter manifest.skill.tags (prevent unlimited tag scoring)
- Cap regex pattern score at 40 (prevent 5x20=100 dominating keyword+tag)
- Add doc comment about skill_list tool exposing metadata (sanitization required)
- Move Community disclaimer inside <skill> tags (not outside structural boundary)
- Filter keywords/tags shorter than 3 chars (prevent broad matching)
LOW:
- Enforce minimum token_cost of 1 (max_context_tokens=0 can't bypass budget)
- Remove redundant try_exists checks in discover_local (let load_skill handle errors)
70 skills tests passing.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add HTTP endpoint scoping for skills (Phase 1)
Skills that declare an [http] section in skill.toml now have their HTTP
requests constrained to declared endpoints at runtime. This addresses
the gap where allowed_patterns was parsed but never enforced -- once the
http tool was visible via attenuation, the LLM could reach any URL.
Enforcement reuses EndpointPattern/AllowlistValidator from the WASM
capability system. Semantics: if no active skill declares [http], all
requests pass through (backward compat). If any skill declares [http],
URLs must match at least one skill's allowlist (union). Community skills'
[http] declarations are silently ignored (defense in depth).
Shell commands using curl/wget are also validated against scopes.
Scanner gains detection for known exfiltration domains (webhook.site,
ngrok.io, etc.), overly broad wildcards, and credential/host mismatches.
Closes#38
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: Apply cargo fmt to http_scoping.rs
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: Apply cargo fmt across codebase
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add parameter-level permission enforcement for skills (Phase 2)
Activates enforcement of `allowed_patterns` in skill.toml permissions.
Previously these patterns were parsed but not enforced -- a Verified skill
declaring `permissions.shell` with `allowed_patterns = [{command = "cargo *"}]`
could still run any shell command. Now the enforcer validates tool parameters
against declared glob patterns before execution.
Key changes:
- New `enforcer.rs` module with `SkillPermissionEnforcer`, `glob_to_regex()`,
and `validate_tool_call()` with union semantics across active skills
- Typed pattern enums (`ShellPattern`, `FilePathPattern`, `MemoryTargetPattern`)
replace the previous `Vec<serde_json::Value>` in `ToolPermissionDeclaration`
- Scanner gains `scan_permission_patterns()` detecting dangerous patterns
(rm, sudo, curl, bare wildcards, command chaining, sensitive paths, identity files)
- Registry blocks non-Local skills with critical permission pattern warnings
- Agent loop threads enforcer into `execute_chat_tool` alongside HTTP scoping
Trust interaction: Community patterns ignored, Verified enforced, Local without
patterns unrestricted, Local with patterns enforced as guidance. Union semantics
across skills -- tool call allowed if ANY skill's patterns permit it.
34 new tests. All 818 library tests pass.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add worker permission enforcement and LLM behavioral analysis (Phase 3+4)
Phase 3 - Worker-side permission enforcement:
- Add SerializedToolPermission/SerializedPattern DTOs for HTTP boundary crossing
- Extend JobDescription, ContainerHandle, and orchestrator API to carry permissions
- CreateJobTool snapshots and forwards skill permissions to spawned workers
- Worker runtime builds SkillPermissionEnforcer and checks before tool execution
- Load-time token budget enforcement rejects prompts exceeding 2x declared budget
- Deduplicate enforcer construction: from_active_skills() delegates to from_serialized()
Phase 4 - LLM behavioral analysis:
- BehavioralAnalyzer with cached, LLM-based semantic content analysis
- Structured output parsing (FINDING|CATEGORY|SEVERITY|DESCRIPTION or CLEAN)
- Content-hash caching with bounded size (MAX_CACHE_ENTRIES=256)
- Graceful degradation when LLM unavailable
- Integrated into load_skill() for non-Local skills; critical findings block loading
Review fixes:
- Real cache tests with CountingLlm mock (test_cache_hit, test_cache_miss, test_cache_bounded)
- UTF-8-safe truncate() in worker runtime
- Few-shot examples in behavioral analysis prompt
- Documented max_context_tokens=0 opt-out and create_job() permission gap
848 tests passing, no new clippy warnings.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Address review feedback from serrrfirat on skills-phase2
- Fix truncate_cmd UTF-8 panic: use char-boundary-aware slicing
- Remove redundant effective_tools branching in reasoning.rs
- Document cache eviction as known limitation (arbitrary, not LRU)
- Add safety comment on SkillTrust enum ordering (security-critical)
- Simplify active_skills selection (prefilter_skills handles empty input)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining skills review feedback
* refactor: replace skills system with OpenClaw SKILL.md format + 2-state trust
Replace the 5-gate, 3-tier trust hierarchy (scanner, behavioral analyzer,
parameter-level enforcer, HTTP endpoint scoping) with a simplified 3-layer
security model: gating -> attenuation -> Docker confinement.
Key changes:
- SKILL.md format (YAML frontmatter + markdown prompt) replaces skill.toml + prompt.md
- 2-state trust (Installed/Trusted) replaces 3-tier (Community/Verified/Local)
- New parser.rs for SKILL.md parsing with serde_yaml
- New gating.rs for requirements checking (bins/env/config)
- Simplified registry with 2-location discovery (workspace + user dirs)
- Removed scanner, behavioral_analyzer, enforcer, http_scoping (~4,100 lines)
- Removed skill_permissions propagation through job/orchestrator/worker pipeline
- Added serde_yaml dependency for YAML frontmatter parsing
Net: -5,298 lines, 59 skills tests pass, 907 total tests pass.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add in-app skill management tools and ClawHub catalog integration
Add 4 chat-callable tools (skill_list, skill_search, skill_install,
skill_remove) plus matching web gateway endpoints for managing skills
at runtime. The catalog fetches from ClawHub's public registry API
at runtime rather than bundling entries at compile time.
Key changes:
- SkillRegistry gains mutation methods (install_skill, remove_skill,
reload, find_by_name) with Arc<RwLock> for concurrent access
- New catalog module queries ClawHub /api/v1/search with in-memory
caching (5-min TTL, configurable via CLAWHUB_REGISTRY env var)
- skill_list and skill_search added to READ_ONLY_TOOLS for safe use
under Installed trust ceiling
- Web gateway gets /api/skills, /api/skills/search, /api/skills/install,
and /api/skills/{name} DELETE endpoints
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #51 review feedback from ilblackdragon
Security:
- Add SSRF protection to fetch_skill_content: require HTTPS, reject
private/loopback/link-local IPs and internal hostnames, disable
redirects. Gateway install handler now reuses the same validation.
- URL-encode slug in skill_download_url to prevent query injection.
- Require X-Confirm-Action header on gateway skill install/remove
endpoints (equivalent to chat tool requires_approval gate).
Correctness:
- Eliminate all block_in_place/block_on usage in skill tools and
gateway handlers. Split install into prepare_install_to_disk (static
async, no lock) + commit_install (sync, brief write lock). Same
pattern for remove: validate_remove + delete_skill_files + commit_remove.
- Write normalized content to disk in install_skill (was writing
original un-normalized content, causing hash mismatch on re-read).
- Fix token estimation from 0.75 to 0.25 tokens/byte (~4 chars per
token) in registry.rs, selector.rs, and standalone loader.
Dependencies:
- Replace deprecated serde_yaml 0.9 with serde_yml 0.0.12.
- Remove unused toml dependency.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add benchmarking harness for agent evaluation
Introduces ironclaw-bench, a Rust-native benchmarking crate that drives the
real agent loop headlessly. Supports standard benchmarks (GAIA, Tau-bench,
SWE-bench Pro) and custom JSONL task sets with parallel execution, resume
support, and incremental JSONL output.
Key components:
- BenchChannel: headless Channel impl with auto-approval and response capture
- InstrumentedLlm: LlmProvider wrapper recording per-call token/cost metrics
- BenchRunner: task orchestration with parallel execution and JSONL resume
- Scoring utilities: exact match, contains, regex (all with normalization)
- CLI: run, results, compare, list subcommands via clap
- Four suite adapters: custom, gaia, tau_bench, swe_bench
Also fixes a pre-existing missing SseEvent::ToolResult match arm in the web
gateway and adds FinishReason to the LLM module's public re-exports.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: Add spot benchmark suite for end-to-end agent verification
Adds a "spot" suite with 13 scenarios across 4 categories (smoke,
tool use, multi-tool chaining, robustness) using multi-criterion
assertions instead of simple text matching. Also adds an `error`
field to TaskSubmission so suites can hard-fail on agent errors.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address audit findings in benchmarks crate
- Fix O(n²) scoring loop by indexing tasks in a HashMap (was re-parsing JSONL per result)
- Add UTF-8-safe truncation to prevent panic on multi-byte chars in channel capture
- Wire setup_task/teardown_task into both sequential and parallel runner paths
- Convert BenchRunner.suite from Box to Arc for parallel task setup/teardown
- Add tracing::warn for placeholder scores in custom, swe_bench, tau_bench adapters
- Add spot suite to CLI help text
- Add doc comment clarifying tools_used HashSet behavior in SpotAssertions
- Reorder match arms in create_suite to match KNOWN_SUITES alphabetical order
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: rewrite tasks.jsonl with scored results after scoring
The JSONL file was only written during execution (pre-scoring), so the
`results` command showed "pending" scores even after scoring completed.
Now the runner rewrites the JSONL with final scored results, keeping
task-level and aggregate data consistent.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: prefix benchmark runs with model name and commit hash
Run logs and results table now show the base model and short git commit
hash, making it easy to correlate results with code versions. The commit
hash is also persisted in run.json for historical tracking.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add 8 memory benchmark scenarios to spot suite
Tests save-and-recall workflows using file tools:
- daily tasks, reminders, meeting notes, append logs
- detail extraction, todo priorities, multi-file ops
- context updates (write-read-rewrite-verify)
Total spot scenarios: 13 -> 21
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: fmt channel.rs and gitignore bench-results
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address critical and high findings from PR review
- Fix race condition: parallel mode now writes JSONL after all tasks
complete instead of concurrent unsynchronized appends
- Fix UTF-8 panic: use .chars().take(25) instead of byte slicing on
task_id which could panic on multi-byte characters
- Remove dead code: max_iterations (parsed but never used),
tool_whitelist() (declared but never called), MatrixEntry.tools
(declared but never applied)
- Eliminate double load_tasks(): cache task list on first load and
reuse the index for scoring instead of re-reading from disk
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: relax smoke-greeting assertion to not demand parrot greeting
The LLM often introduces itself without echoing "hello" back. Use a
regex that accepts any reasonable self-introduction (hello, hi, hey,
assistant, agent, help) instead of demanding a specific word.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: 100% spot baseline (GPT-5.2 @ 2c43b83, 21/21 pass)
Relax two brittle assertions:
- smoke-greeting: use regex for any reasonable self-intro instead of
demanding the model parrot "hello"
- memory-update-context: drop response_not_contains PST since the
model correctly says "not PST" which triggers the literal check
- memory-multifile: lower min_tool_calls from 4 to 3, the model can
batch two writes in one LLM turn
Baseline results committed to benchmarks/baselines/ for regression
tracking. Local runs stay in bench-results/ (gitignored).
Results: 100.0% pass, 1.000 avg, $0.31 cost, 111s wall time
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address remaining PR review comments
- Replace .expect("semaphore closed") with proper error handling
- Derive PartialEq on BenchScore for cleaner test assertions
- Use ToPrimitive::to_f64() instead of string roundtrip in estimated_cost()
- Validate SWE-bench inputs: task_id (path traversal), repo (owner/repo format),
base_commit (valid git ref) with 5 new tests
- Skip "pending" (unscored) entries during resume so they get re-executed
- Use run.json mtime for find_latest_run (falls back to tasks.jsonl, then dir)
- Move additional_tools() outside parallel loop to share Arc<[Tool]> across tasks
- Add doc comments documenting known limitations (single-turn, resources, conversation)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: reject absolute paths in SWE-bench and validate matrix config
- is_safe_path_component now rejects paths starting with '/'
- BenchConfig::from_file validates matrix is non-empty
- Added tests for both validations
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: fail tasks on setup_task error and compute git hash once
- setup_task failure now records an error TaskResult instead of
continuing to run the task (both sequential and parallel paths)
- git_short_hash() computed once per run instead of twice
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* refactor: break up agent_loop.rs into four focused modules
Split the monolithic 2835-line agent_loop.rs into:
- agent_loop.rs (722L): Agent struct, event loop, message dispatch
- dispatcher.rs (635L): Agentic tool loop, tool execution, auth detection
- commands.rs (484L): System commands, job handlers, heartbeat, summarize
- thread_ops.rs (1059L): Thread lifecycle, approval, undo/redo, persistence
Each module gets its own impl Agent block. Agent fields changed to
pub(super) so sibling modules in the agent package can access them.
All 16 existing tests pass in their new locations.
Inspired by ZeroClaw's agent module split (agent.rs, loop_.rs,
dispatcher.rs, prompt.rs, memory_loader.rs).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add cost caps and guardrails for autonomous agent spending
Daily budget (MAX_COST_PER_DAY_CENTS) and hourly action rate
(MAX_ACTIONS_PER_HOUR) limits prevent runaway agents from burning
through API credits, especially in daemon/heartbeat modes.
- CostGuard with pre-flight check and post-call recording
- Sliding window for hourly rate, midnight-UTC daily reset
- 80% threshold warning, atomic fast-path for exceeded budget
- Wired into dispatcher loop (check before LLM call, record after)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add circuit breaker on LLM providers
Wraps LlmProvider with a Closed/Open/HalfOpen state machine that
trips after consecutive transient failures, preventing request storms
against a degraded backend. Automatically probes for recovery.
- CircuitBreakerProvider implements LlmProvider (drop-in wrapper)
- Transient error classification (server, rate-limit, network, auth infra)
- Client errors (wrong model, context overflow) don't trip the breaker
- Configurable via CIRCUIT_BREAKER_THRESHOLD and CIRCUIT_BREAKER_RECOVERY_SECS
- Composes with existing FailoverProvider (circuit breaker wraps failover)
- 12 tests covering full state machine and error classification
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add tunnel abstraction for remote access
Trait-based tunnel system with lifecycle management (start/stop/health)
for exposing the agent to the internet through external tunnel binaries.
Five providers:
- Cloudflare Tunnel (cloudflared, Zero Trust token auth)
- Tailscale (serve for tailnet, funnel for public)
- ngrok (with optional custom domain)
- Custom (arbitrary command with {host}/{port} placeholders)
- None (local-only, no external exposure)
Config via TUNNEL_PROVIDER + provider-specific env vars. Extends
existing TunnelConfig with optional managed provider alongside the
static TUNNEL_URL path. Factory, shared process management, and
37 tests covering all providers and edge cases.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add OS service management (launchd/systemd)
Adds `ironclaw service {install,start,stop,status,uninstall}` for
running the agent as a background daemon. macOS uses launchd plists
under ~/Library/LaunchAgents, Linux uses systemd user units.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add observability trait system with noop, log, and multi backends
Introduces an Observer trait for recording agent lifecycle events and
metrics, with pluggable backends. The noop backend compiles to zero
overhead, log backend uses tracing, and multi fans out to multiple
observers. Configured via OBSERVABILITY_BACKEND env var.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add in-memory LLM response cache with TTL and LRU eviction
CachedProvider wraps any LlmProvider and caches complete() responses
keyed by SHA-256(model + messages). Tool-calling requests are never
cached since they trigger side effects. Configurable via
RESPONSE_CACHE_ENABLED, RESPONSE_CACHE_TTL_SECS, and
RESPONSE_CACHE_MAX_ENTRIES env vars.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add memory hygiene with cadence-gated daily log cleanup
Adds workspace::hygiene module that automatically deletes daily log
documents older than a configurable retention period (default 30 days).
Runs on a 12-hour cadence tracked via a local state file to avoid
redundant passes. Best-effort design: failures are logged, never fatal.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add doctor diagnostics command for active health probing
Probes external dependencies (Docker, cloudflared, ngrok, tailscale),
validates NEAR AI session, checks database connectivity, and verifies
workspace directory. Complements the passive `status` command.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: add structured TOML config file support
Adds ~/.ironclaw/config.toml as a configuration layer between env vars
and database settings. Priority: env var > TOML file > DB > defaults.
- `ironclaw config init` generates a commented config.toml from current settings
- `ironclaw --config path/to/config.toml` loads a custom config file
- Settings.merge_from() only overlays non-default values from the TOML file
- `ironclaw config path` now shows TOML file status
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address codex review findings
- apply_toml_overlay now returns Result and errors on explicit missing
or invalid config paths (was log-only, violating the documented
contract that explicit paths are fatal)
- custom tunnel url_pattern is now used to filter extracted URLs, not
just as a gate for scanning stdout
- systemd ExecStart path is now quoted to handle spaces in paths
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback
- Cache key now includes max_tokens, temperature, and stop_sequences
so different request parameters produce distinct keys
- to_cents() uses .trunc() + parse::<u64> instead of f64 intermediary,
avoiding precision loss for large values
- Tailscale public URL no longer includes local port (serve/funnel
expose on standard HTTPS port 443)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* feat: wire up tunnel lifecycle and fix audit findings
Connect the tunnel module to the rest of the application so that
setting TUNNEL_PROVIDER actually starts a managed tunnel at boot and
stops it on shutdown. Previously create_tunnel() was never called
outside tests.
Changes:
- Expand TunnelSettings with provider credential fields (settings.rs)
- TunnelConfig::resolve() falls back to DB settings when env vars unset
- Start tunnel at boot, stop on shutdown, show URL in boot screen
- Setup wizard collects provider-specific credentials (ngrok, cloudflare,
tailscale, custom, static URL)
- Fix public_url() returning None under lock contention (SharedUrl)
- Fix local_host parameter ignored by cloudflare/ngrok/tailscale
- Fix tailscale silent fallback to "localhost" on bad JSON
- Fix ngrok globally mutating config via add-authtoken (use env var)
- Add 10s timeout to tailscale status --json
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review comments
- Document split_whitespace limitation in CustomTunnel doc comment
- Remove unnecessary quotes from systemd ExecStart directive
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR review feedback (round 3)
- doctor: missing libSQL DB on fresh install is Pass, not Fail
- service: quote ExecStart path for systemd space handling
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: correct cost guard doc comment (LLM calls, not LLM/tool)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* 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]>