Commit Graph
45 Commits
Author SHA1 Message Date
09e1c97a27 fix(approval): make "always" auto-approve work for credentialed HTTP requests (#1257)
The HTTP tool returned `ApprovalRequirement::Always` for requests with
credentials, but `Always` is hardcoded to ignore the session auto-approve
set. This meant users who clicked "always" were re-prompted on every
subsequent HTTP call — the UI offered "always" but the backend ignored it.

Two fixes:
1. HTTP credentialed requests now return `UnlessAutoApproved` instead of
   `Always`, so the session auto-approve set is respected.
2. `StatusUpdate::ApprovalNeeded` now carries `allow_always: bool`. All
   channel UIs (Telegram, Slack, Signal, REPL, Web) conditionally hide
   the "always" option when a tool truly requires per-invocation approval
   (`ApprovalRequirement::Always`, e.g. destructive shell commands).

Also boxes `PendingApproval` in `AgenticLoopResult::NeedApproval` to fix
a pre-existing clippy `large_enum_variant` warning.

Regression tests included (test_credentialed_requests_respect_auto_approve,
test_allow_always_matches_approval_requirement) but CI heuristic cannot
detect them in cross-fork PR diffs.

[skip-regression-check]

Co-authored-by: Tyler <[email protected]>
2026-03-19 11:45:32 -07:00
3dcccc1e64 feat(self-repair): wire stuck_threshold, store, and builder (#712)
* feat(self-repair): wire stuck_threshold, store, and builder (#647)

Wire the previously dead-code fields in DefaultSelfRepair:

- stuck_threshold: detect_stuck_jobs() now filters by duration, only
  reporting jobs stuck longer than the configured threshold
- with_store(): wired in agent_loop.rs from AgentDeps.store for
  tool failure tracking via Database trait
- with_builder(): wired from register_builder_tool() return value
  through AppComponents and AgentDeps for automatic tool rebuilding
- tools: passed alongside builder for hot-reload logging

Remove all #[allow(dead_code)] annotations. Add regression tests for
threshold-based filtering (both above and below threshold).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add missing `builder` field to AgentDeps in gateway workflow harness

After rebase onto staging, AgentDeps gained a `builder` field for
self-repair tool rebuilding. The gateway workflow test harness was
missing this field, causing CI compilation failure.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* ci: retrigger CI

* fix: force CI refresh after path_routing_tests dedup

* test: add E2E test for stuck job repair and tool rebuild cycle

Tests the full self-repair flow requested in review:
1. Job transitions Pending -> InProgress -> Stuck
2. detect_stuck_jobs() finds it (zero threshold)
3. repair_stuck_job() recovers it back to InProgress
4. A broken tool is repaired via MockBuilder
5. Verify builder was invoked and repair succeeded

Uses a MockBuilder (impl SoftwareBuilder) that returns successful
BuildResult without requiring an LLM or filesystem. Uses libsql
test database for the store (increment_repair_attempts, mark_tool_repaired).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(self-repair): measure stuck_duration from Stuck transition, not started_at

- Use ctx.transitions to find the most recent Stuck transition timestamp
  instead of ctx.started_at (which reflects job start, not stuck time)
- Fix StuckJob.last_activity to use stuck transition timestamp
- Remove misleading "hot-reloaded into registry" log
- Remove stray "// ci fix" comment in memory.rs
- Add regression test: backdated started_at must not inflate stuck_duration

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* ci: re-trigger CI with latest changes

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add type annotation to Ok(()) in test to resolve E0282

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-18 20:51:21 -07:00
Nick PismenkovandClaude Haiku 4.5 b50eddfe0a Merge branch 'main' into fix/resolve-conflicts
Resolved merge conflicts in 5 files:

1. src/agent/job_monitor.rs - Used is_internal flag approach (HEAD) for safe internal message marking. Removed metadata-based approach which could be spoofed by external channels.

2. src/agent/agent_loop.rs - Used is_internal check (HEAD) for routing internal messages, consistent with security model where is_internal field cannot be spoofed.

3. src/agent/dispatcher.rs - Included notify_metadata in job context (main), needed for job routing through JobMonitorRoute.

4. src/setup/wizard.rs - Added build_nearai_model_fetch_config() function (main) for model selection during setup.

5. src/tools/builtin/job.rs - Used both comments from HEAD (clarifying notify_channel and notify_user logic) while removing metadata field from JobMonitorRoute (consistent with job_monitor.rs).

All conflicts resolved with security-first approach: use is_internal boolean field for internal message marking (cannot be spoofed), while passing routing metadata through context.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
2026-03-16 14:47:07 -07:00
Henry ParkandGitHub 878a67cdb6 Refactor owner scope across channels and fix default routing fallback (#1151)
* refactor: add explicit owner scope across channels

* fix: tighten routine owner target routing

* fix: address owner scope review feedback

* Fix owner-scope onboarding and event trigger isolation

* Tighten routing fallback and wizard owner validation

* fix: address owner-scope follow-up review

* fix: tighten owner-scope follow-up details

* fix: import Channel trait in telegram test

* fix: normalize http webhook sender ids

* fix: address remaining owner-scope review issues

* fix: reconcile config rebase fallout

* fix: reconcile extension manager rebase drift

* fix: address current copilot review regressions

* fix: restore clippy matrix after rebase
2026-03-16 13:31:03 -07:00
Henry ParkandGitHub 4890e73a34 Merge pull request #1132 from nearai/staging-promote/e805ec61-23059634819
chore: promote staging to main (2026-03-13 16:09 UTC)
2026-03-16 09:22:58 -07:00
bde0b77a86 fix(security): prevent metadata spoofing of internal job monitor flag (#1195)
The `__internal_job_monitor` metadata key that bypassed the entire
agent pipeline (hooks, safety checks, LLM processing) was spoofable
by external channels — WASM channel plugins could inject arbitrary
metadata including this key, causing attacker-controlled content to be
forwarded directly as assistant responses.

Replace the metadata-based check with a dedicated `is_internal` field
on `IncomingMessage` that can only be set via `into_internal()` by
trusted in-process code. Both the field and setter are `pub(crate)` to
prevent external crates from spoofing the flag. Also remove
`notify_metadata` forwarding (the monitor only needs channel/user/thread
routing) and the unused `__job_monitor_job_id` metadata key.

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-15 21:33:04 +00:00
pikaxingeandGitHub c4e098d4e3 Fix subagent monitor events being treated as user input (#1173)
* Fix subagent monitor routing to avoid LLM re-entry

* Update yanked uds_windows dependency in lockfile
2026-03-15 06:00:19 +00:00
757d24bd90 feat(web): add follow-up suggestion chips and ghost text (#1156)
* feat(web): add follow-up suggestion chips and ghost text to chat UI

The LLM now always generates 1-3 follow-up command suggestions via
<suggestions> tags in its response. These are extracted server-side,
broadcast as SSE events, and rendered as clickable chips above the
chat input. The first suggestion also appears as ghost text in the
input field (Tab to accept). Includes debug logging for LLM responses
in the agentic loop and removes noisy NEAR AI status logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: resolve deferred review items from PR #1156 [skip-regression-check]

- Remove literal backslashes from raw string prompt (reasoning.rs)
- Make WASM channels skip Suggestions status (no-op instead of empty callback)
- Add !e.shiftKey guard to Tab-to-accept ghost text handler
- Cap extracted suggestions at 3 and trim whitespace-only entries
- Extract suggestions in approval-resume path (prevents tag leaking)
- Remove stale .has-ghost class during showSuggestionChips reset

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-14 18:57:24 +00:00
c94ecf19db feat: add Slack approval buttons for tool execution in DMs (#796)
* feat: add channel-relay integration for Slack via external relay service

- Add RelayChannel and RelayClient for connecting to channel-relay SSE streams
- Add RelayConfig with env-based configuration (CHANNEL_RELAY_URL, CHANNEL_RELAY_API_KEY)
- Add channel-relay extension lifecycle: install, OAuth auth, activate with hot-add
- Add proxy message sending through channel-relay for Slack chat.postMessage
- Add extension registry entry for Slack relay with OAuth auth hint
- Add relay integration test with mock SSE server
- Wire relay channel into app startup with reconnect on stored credentials
- Add AuthRequired extension error variant for cleaner auth flow detection

[skip-regression-check]

* chore: apply cargo fmt

* fix: remove remaining Telegram test references in relay channel

* fix: address PR #790 review feedback — parser handle leak, CSRF, circuit breaker

- Fix parser handle leak on reconnect by sharing Arc<RwLock> instead of
  creating a local copy in start() (shutdown now aborts the correct task)
- Add CSRF state nonce to OAuth flow: generate in auth_channel_relay,
  validate in slack_relay_oauth_callback_handler, one-time use
- Remove dead proxy_slack method, update integration test to use
  proxy_provider
- Add reconnect circuit breaker (max_consecutive_failures, default 50)
- Fix stale docs (Telegram refs), extract event_types constants

* fix: double backoff in reconnect loop and UTF-8 chunk-boundary corruption

- Remove second sleep+backoff in list_connections error branch to prevent
  O(4^n) backoff growth (was sleeping and doubling twice per iteration)
- Buffer raw bytes in SSE parser instead of per-chunk String::from_utf8_lossy
  to prevent U+FFFD corruption when multi-byte chars span chunk boundaries

* feat: add Slack approval buttons for tool execution in DMs

Send Block Kit Approve/Deny buttons via relay when a tool requires
approval in a DM context. Auto-deny approval-requiring tools in
shared channels to prevent prompt injection and stuck threads.

* fix: address PR #796 review — use PreflightOutcome::Rejected, add tests

- Auto-deny in non-DM relay channels now uses PreflightOutcome::Rejected
  instead of manually pushing to reason_ctx.messages, so the post-flight
  handler properly records the error in the turn
- Add regression tests for relay auto-deny decision logic
- Remove test_clean.db artifact

* feat: restore Block Kit approval buttons in send_status

The send_status implementation was accidentally dropped during the
staging merge. Restores Approve/Deny Block Kit buttons for DM tool
approval, with required sender_id validation, payload size docs,
and 4 regression tests. Also removes test_clean.db.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: apply rustfmt formatting to dispatcher test code

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 11:38:53 -07:00
1f5b582c5f fix: agent logging (#888)
* fix: optimize agent logging to reduce DataDog bill

* fix: log permanent repair failures as ERROR not WARN

RepairResult::Failed is permanent failure requiring attention (ERROR level)
not a temporary/retryable condition (WARN level).

[skip-regression-check]

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* security: remove user message content from trace logs

Never log user message content at any log level (includes TRACE).
Log only safe metadata: content length, message ID, image count.

This prevents accidental exposure of sensitive user data in logs
even at the most verbose logging level.

[skip-regression-check]

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* security: move LLM response body logging to TRACE level

Response bodies can contain user-generated content, tool outputs, and
leaked secrets. Moving to TRACE (not enabled in production) prevents
exposure in DEBUG logs. Status log remains at DEBUG.

[skip-regression-check]

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* refactor: simplify URL sanitization using url::Url API

Use set_query, set_fragment, set_username, set_password methods
instead of manual string reconstruction. Cleaner, handles edge cases,
eliminates port branching complexity.

[skip-regression-check]

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* test: add comprehensive unit tests for sanitize_url_for_logging

Add 9 test cases covering:
- URL with query parameters
- URL with credentials (user:pass@host)
- URL with fragment
- URL with port
- URL with all components combined
- Malformed URL fallback behavior
- Short strings (pass-through)
- Non-URL-like strings
- Path preservation

Tests verify that sanitization correctly removes sensitive components
while preserving safe components like host, port, and path.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>

* fix: libsql per-migration logs should be DEBUG, not TRACE

Individual migration logs are now visible with standard debug logging
(RUST_LOG=ironclaw=debug), improving debuggability when troubleshooting
migration issues. Summary log remains at INFO level.

Fixes behavioral change that made it harder to identify which specific
migration ran or failed without enabling full TRACE logging.

[skip-regression-check]

---------

Co-authored-by: Claude Haiku 4.5 <[email protected]>
2026-03-10 13:55:06 -07:00
88f4894a18 merge: resolve conflicts for PR #800 and #822 into staging (#881)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)

GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.

Co-authored-by: Claude Sonnet 4.6 <[email protected]>

* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)

- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
  already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs

Co-authored-by: Claude Opus 4.6 <[email protected]>

* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)

- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)

Co-authored-by: Claude Opus 4.6 <[email protected]>

* feat: persist user_id in save_job and expose job_id on routine runs (#709)

* feat: persist worker events to DB and fix activity tab rendering

In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.

Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: wire RoutineEngine into gateway for direct manual trigger firing

Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.

Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: remove stale gateway_state argument from Agent::new test call sites

The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review — restore sandbox source filter, remove blank lines

- Revert removal of `source = 'sandbox'` filter in all SandboxStore
  queries (8 sites across PG and libSQL). Sandbox-specific APIs should
  stay scoped to sandbox jobs; unified job listing for the Jobs tab
  should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
  formatting CI failure.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address review — regenerate Cargo.lock, add user_id regression test

- Regenerate Cargo.lock from main's lockfile to eliminate dependency
  version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
  and get_job in the libSQL backend.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: remove trailing blank line in libsql jobs.rs

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add Postgres-side regression test for user_id persistence in save_job

Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>

* refactor: unify three agentic loops into single AgenticLoop engine (#654)

Replace three independent copy-pasted agentic loops (dispatcher, worker,
container runtime) with a single shared engine in `agentic_loop.rs` that
all consumers customize via the `LoopDelegate` trait.

Phase 1 — Shared engine (`src/agent/agentic_loop.rs`, 205 lines):
  - `run_agentic_loop()` owns the core LLM → tool exec → repeat cycle
  - `LoopDelegate` trait (Send + Sync, &dyn dispatch) with 6 hook points
  - Tool intent nudge logic consolidated (was duplicated in 3 files)
  - Iteration limit + force-text behavior preserved

Phase 2 — Three delegate implementations:
  - `ChatDelegate` (dispatcher.rs): 3-phase approval flow, hooks, cost
    guard, context compaction, skill attenuation, interruption
  - `JobDelegate` (worker/job.rs): planning pre-loop phase, parallel
    JoinSet exec, mark_completed/stuck/failed, SSE streaming, self-repair
  - `ContainerDelegate` (worker/container.rs): sequential tool exec,
    HTTP-proxied LLM, container-safe tools, credential injection

Phase 3 — File moves and cleanup:
  - Delete `src/agent/worker.rs` — job logic moved to `src/worker/job.rs`
  - Rename `src/worker/runtime.rs` → `src/worker/container.rs`
  - Re-export `Worker`/`WorkerDeps` from `crate::worker` in `agent/mod.rs`
  - Update `scheduler.rs` imports to new worker location

Shared helpers (`src/tools/execute.rs`):
  - `execute_tool_with_safety()` replaces 4 copies of validate → timeout
    → execute → serialize
  - `process_tool_result()` replaces 3 copies of sanitize → wrap →
    ChatMessage (also used by thread_ops.rs approval resume paths)

Net result: -2,408 lines, zero duplicated loop logic, single code path
for tool intent nudge and completion detection.

Closes #654

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address review feedback from Copilot

1. scheduler.rs: Replace `unwrap_or` fallback with proper error
   propagation when parsing tool output JSON — surfaces bugs instead
   of silently changing the output type.

2. worker/job.rs: Drop MutexGuard before the cancellation `.await` in
   `check_signals()` to avoid holding a lock across an async I/O call
   (prevents `await_holding_lock` lint).

3. worker/job.rs: Restore consecutive rate-limit counter
   (MAX_CONSECUTIVE_RATE_LIMITS = 10) so sustained rate limiting marks
   the job stuck with "Persistent rate limiting" instead of silently
   burning through max_iterations.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: incorporate staging changes — token budget tracking + mark_failed

Merge staging's changes into the refactored JobDelegate:
- Add token budget tracking in call_llm (update_context/add_tokens)
- mark_stuck → mark_failed for iteration cap and rate-limit exhaustion
  (aligns with staging's #788 fix)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address zmanian's PR review — eliminate type erasure, clean up

Address all 6 review points from zmanian on PR #800:

1. Replace LoopOutcome::Custom(Box<dyn Any>) with typed
   LoopOutcome::NeedApproval(Box<PendingApproval>) — eliminates
   type erasure and downcast, resolves clippy large_enum_variant.

2. Remove dead max_tool_iterations field from ChatDelegate struct.

3. Add on_tool_intent_nudge() hook to LoopDelegate trait with
   implementations in Job and Container delegates for observability.

4. Fix SSE events in job worker to emit raw sanitized content
   instead of XML-wrapped <tool_output> tags.

5. Remove 4 duplicate completion tests from job.rs that were
   already covered by the shared util module.

6. Avoid logging full tool results — use result_size_bytes in
   debug logs (execute.rs, job.rs).

Also updates path references in CLAUDE.md, COVERAGE_PLAN.md,
and add-sse-event.md command.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(doctor): expand diagnostics from 7 to 16 health checks

* test: add unit tests for agentic_loop and execute shared modules

Add 16 tests covering the two new critical shared modules:

agentic_loop.rs (10 tests):
- Text response exits loop immediately
- Tool call → text response continuation
- LoopSignal::Stop exits before LLM call
- LoopSignal::InjectMessage adds user message to context
- Max iterations terminates with LoopOutcome::MaxIterations
- Tool intent nudge fires twice then caps
- before_llm_call early exit bypasses LLM
- truncate_for_preview: short string, long string, multibyte safety

execute.rs (6 tests):
- execute_tool_with_safety success path
- Missing tool returns ToolError::NotFound
- Tool execution failure propagates
- Per-tool timeout enforcement (50ms)
- process_tool_result XML wrapping on success
- process_tool_result error formatting

All 2,777 unit tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address code review — 9 issues across agentic loop, job worker, container

CRITICAL fixes:
- Rate-limit exhaustion now returns Err(LlmError::RateLimited) instead of
  Ok(Text("")), stopping the loop immediately with no ghost iteration.
  Below-threshold retries still use Text("") with an explicit empty-string
  guard in handle_text_response to skip injection.
- check_signals drains the entire message channel before returning,
  prioritizing Stop over UserMessage. Previously returned early on first
  UserMessage, silently dropping any queued Stop or additional messages.
- check_signals now detects all non-progressing job states (Cancelled,
  Failed, Stuck, Completed, Submitted, Accepted) instead of only
  Cancelled and Failed.

HIGH fixes:
- Error path in process_tool_result_job applies truncate_for_preview to
  bound error strings in SSE/DB events (was unbounded).
- Document Send+Sync lifetime constraint on LoopDelegate trait.
- Test mock before_llm_call refactored from double-lock to single lock
  acquisition, eliminating deadlock risk on refactor.

MEDIUM fixes:
- CompletionReport includes actual iteration count via shared
  Arc<Mutex<u32>> tracker (was hardcoded 0).
- process_tool_result_job return type changed from Result<bool> to
  Result<()> — the bool was always false (dead API).
- Deduplicate truncate in container.rs; now uses truncate_for_preview
  from agentic_loop.

Verified: 0 clippy warnings, 2781 tests pass, cargo fmt clean.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Umesh Kumar Singh <[email protected]>
Co-authored-by: reidliu41 <[email protected]>
2026-03-10 11:19:23 -07:00
60881d6888 feat(agent): add context size logging before LLM prompt (#810)
* fix(ci): secrets can't be used in step if conditions [skip-regression-check] (#787)

GitHub Actions step-level `if:` doesn't have access to `secrets` context.
Replace `if: secrets.X != ''` with `continue-on-error: true` and let
the Set token step handle the fallback.

Co-authored-by: Claude Sonnet 4.6 <[email protected]>

* fix(ci): clean up staging pipeline — remove hacks, skip redundant checks [skip-regression-check] (#794)

- Remove continue-on-error from staging-ci.yml app token steps (secrets are configured)
- Skip test.yml and code_style.yml on PRs targeting staging (staging-ci.yml
  already runs tests before promoting, promotion PR gets full CI on main)
- Allow ironclaw-ci[bot] in Claude Code review for bot-created promotion PRs

Co-authored-by: Claude Opus 4.6 <[email protected]>

* fix(ci): run fmt + clippy on staging PRs, skip Windows clippy [skip-regression-check] (#802)

- Remove branches:[main] filter from code_style.yml so it runs on all PRs
- Gate clippy-windows with `if: github.base_ref == 'main'` (skip on staging PRs)
- Update rollup job to allow skipped clippy-windows
- Simplify claude-review.yml to only trigger on labeled event (avoids duplicate runs)

Co-authored-by: Claude Opus 4.6 <[email protected]>

* feat: persist user_id in save_job and expose job_id on routine runs (#709)

* feat: persist worker events to DB and fix activity tab rendering

In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.

Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: wire RoutineEngine into gateway for direct manual trigger firing

Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.

Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: remove stale gateway_state argument from Agent::new test call sites

The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review — restore sandbox source filter, remove blank lines

- Revert removal of `source = 'sandbox'` filter in all SandboxStore
  queries (8 sites across PG and libSQL). Sandbox-specific APIs should
  stay scoped to sandbox jobs; unified job listing for the Jobs tab
  should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
  formatting CI failure.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address review — regenerate Cargo.lock, add user_id regression test

- Regenerate Cargo.lock from main's lockfile to eliminate dependency
  version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
  and get_job in the libSQL backend.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: remove trailing blank line in libsql jobs.rs

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add Postgres-side regression test for user_id persistence in save_job

Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>

* feat(agent): add context size logging before LLM prompt

---------

Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
2026-03-10 11:07:29 -07:00
83950d11a4 fix: job token budget, iteration cap → Failed, web cancel stops worker (#788)
* feat: persist user_id in save_job and expose job_id on routine runs (#709)

* feat: persist worker events to DB and fix activity tab rendering

In-process Worker (used by Scheduler::dispatch_job) now persists events
via save_job_event at key execution points: plan creation, LLM
responses, tool_use, tool_result, and job completion/failure/stuck.
Event data shapes match the container worker format so the gateway
activity tab renders them correctly.

Frontend: tool_result errors now show a red X icon with danger styling
instead of a silent empty output. The result event falls back to the
error field when message is absent.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: wire RoutineEngine into gateway for direct manual trigger firing

Replace the message-channel hack in routines_trigger_handler with a
direct call to RoutineEngine::fire_manual(), ensuring FullJob routines
dispatch correctly when triggered from the web UI. Inject the engine
into GatewayState from Agent::run after construction.

Also persists user_id in save_job for both PG and libSQL backends,
removes the source='sandbox' filter so all jobs are visible, and
exposes job_id on RoutineRunInfo for the frontend job link.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: remove stale gateway_state argument from Agent::new test call sites

The gateway_state parameter was removed from Agent::new during rebase
(replaced by post-construction set_routine_engine_slot), but three test
call sites still passed the extra None argument.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review — restore sandbox source filter, remove blank lines

- Revert removal of `source = 'sandbox'` filter in all SandboxStore
  queries (8 sites across PG and libSQL). Sandbox-specific APIs should
  stay scoped to sandbox jobs; unified job listing for the Jobs tab
  should use a separate query path.
- Remove extra blank lines in agent_loop.rs and worker.rs that caused
  formatting CI failure.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address review — regenerate Cargo.lock, add user_id regression test

- Regenerate Cargo.lock from main's lockfile to eliminate dependency
  version downgrades (anyhow, syn, etc.) that were churn from rebase.
- Add regression test verifying user_id round-trips through save_job
  and get_job in the libSQL backend.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: remove trailing blank line in libsql jobs.rs

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add Postgres-side regression test for user_id persistence in save_job

Mirrors the existing libSQL test (test_save_job_persists_user_id) for the
Postgres backend. Gated behind #[cfg(feature = "postgres")] + #[ignore]
since it requires a running PostgreSQL instance (integration tier).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>

* fix: add job token budget, change iteration cap to Failed, fix web cancel (#698)

Jobs could enter infinite retry loops because: (1) no token budget was
enforced, (2) iteration cap marked jobs as Stuck (allowing self-repair to
restart them), and (3) the web UI cancel button only updated the DB without
stopping the running worker.

- Add `max_tokens_per_job` config (settings.json + AGENT_MAX_TOKENS_PER_JOB
  env var, default 0 = unlimited) with per-job metadata override
- Track token usage after respond_with_tools() and fail the job on budget
  exceeded
- Change iteration cap and persistent rate limiting from mark_stuck to
  mark_failed, preventing self-repair restart loops
- Fix web cancel handler to call scheduler.stop() which updates in-memory
  state AND aborts the worker task, falling back to DB-only update

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review — always persist cancel to DB, simplify token check

- Cancel handler now always persists Cancelled to DB regardless of whether
  scheduler.stop() ran, fixing the edge case where stop() returns Ok(())
  for jobs not in the scheduler map
- Collapse nested ifs per clippy (let-chains)
- Add NOTE comment about select_tools() not exposing TokenUsage

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: rustfmt formatting in wizard.rs (pre-existing)

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-10 02:19:56 +00:00
Nick PismenkovandGitHub 7d8576a464 fix: destructive actions from ambiguous user prompts (#782)
* fix: destructive actions from ambiguous user prompts

* review fixes

* review fixes
2026-03-09 18:03:39 -07:00
ReidandGitHub 290d925c7f fix: preserve tool-call history across thread hydration (#568) (#670)
Prevent model re-attempts and data inconsistencies when rebuilding
  conversation context from persisted tool-call records.

  - Remove raw tool parameters from persisted tool_calls JSON to prevent
    unredacted sensitive data from being stored in the database. The LLM
    context rebuild only needs call_id + name + result.
  - Make record_tool_error/record_tool_result mutually exclusive in all
    three execution paths (dispatcher, approval, deferred). Previously
    error cases called both methods, violating the TurnToolCall invariant
    and sending contradictory outcomes to the LLM.
  - Unify call_id format to turn{N}_{i} between live sessions and
    persisted hydration to eliminate ID mismatch in the LLM context.
  - Auto-close </tool_output> XML tags after truncate_preview truncation
    to prevent malformed tool output reaching the LLM.

  [skip-regression-check]
2026-03-09 08:36:07 -07:00
553c306c52 feat: full image support across all channels (#725)
* feat: full image support across all channels

End-to-end image handling: upload, generation, analysis, editing, and
rendering across web gateway, HTTP webhook, WASM (Telegram/Slack), and
REPL channels. Builds on the attachment infrastructure from #596 and
draws inspiration from PR #641's image pipeline approach — credit to
that PR's author for the sentinel JSON pattern and base64-in-JSON
upload design.

Key changes:
- Image upload in web UI (file picker, paste, preview strip)
- Image generation tool (FLUX/DALL-E via /v1/images/generations)
- Image edit tool (multipart /v1/images/edits with fallback)
- Image analysis tool (vision model for workspace images)
- Model detection utilities (image_models.rs, vision_models.rs)
- Sentinel JSON detection in dispatcher for generated image rendering
- StatusUpdate::ImageGenerated → SSE/WS/REPL/WASM broadcast
- HTTP webhook attachment support (base64, 5MB/file, 10MB total)
- WASM channel image download (Telegram via file API, Slack via host HTTP)
- Tool registration wiring in app.rs

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR #725 review comments (16 issues)

- SecretString for API keys in all image tools (image_gen, image_edit, image_analyze)
- Binary image read via tokio::fs::read instead of DB-backed workspace.read()
- Replace Arc<Workspace> with Option<PathBuf> base_dir (workspace has no filesystem API)
- ApprovalRequirement::UnlessAutoApproved for cost-sensitive image tools
- Scope sentinel detection to image_generate/image_edit tool names only
- Skip ToolResult preview broadcast for image sentinels (avoids multi-MB base64 in SSE)
- Extract shared media_type_from_path() to builtin/mod.rs
- Rename fallback_chat_edit → fallback_generate with tracing::warn
- Increase gateway body limit from 1MB to 10MB for image uploads
- Increase webhook body limit to 15MB (base64 overhead)
- Log warning on invalid base64 in images_to_attachments
- Client-side image size limits (5MB/file, 5 images max) in app.js
- aria-label on attach button for accessibility
- Update body_too_large test for new 10MB limit

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add Slack file size check before download (PR review item #15)

Skip downloading files larger than 20 MB in the Slack WASM channel to
avoid excessive memory use and slow downloads in the WASM runtime.
Logs a warning when a file is skipped. Also bumps channel versions
for Slack and Telegram (prior branch changes).

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(security): add path validation and approval requirement to image tools

Add sandbox path validation via validate_path() to both ImageAnalyzeTool
and ImageEditTool to prevent path traversal attacks that could exfiltrate
arbitrary files through external vision/edit APIs. Also fix
ImageAnalyzeTool::requires_approval to return UnlessAutoApproved,
consistent with ImageEditTool and ImageGenerateTool.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: post-download size guards and empty data_url sentinel check

- Slack: add post-download size check on actual bytes when metadata
  size_bytes is absent, preventing bypass of the 20MB limit
- Telegram: add 20MB download size limit (matching Slack) enforced
  in download_telegram_file() after receiving response bytes
- Dispatcher: skip broadcasting ImageGenerated SSE event when
  data_url is empty from unwrap_or_default(), log warning instead

Closes correctness issues #3, #4, #5 from PR #725 review.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use mime_guess for media type detection, add alt attrs and media_type validation

- Replace hardcoded media type mapping with mime_guess crate (already in deps)
- Add alt attributes to img elements in web UI for accessibility
- Validate media_type starts with "image/" in images_to_attachments()
- Update bmp test assertion to match mime_guess behavior

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Zaki <[email protected]>
2026-03-09 03:41:27 +00:00
272d31797e chore: remove dead code (#648) (#703)
* chore: remove dead code (LlmEvaluator, chunk_by_paragraphs, bundled channel installer, Reasoning::safety)

Delete unused code flagged in #648:
- evaluation/success.rs: delete LlmEvaluator struct/impl, remove #[allow(dead_code)] from RuleBasedEvaluator methods
- workspace/chunker.rs: delete chunk_by_paragraphs() and its tests (zero production callers)
- extensions/manager.rs: delete install_bundled_channel_from_artifacts() (hot-activation never shipped)
- llm/reasoning.rs: remove unused safety field from Reasoning struct; cascade removal through ContextCompactor, HeartbeatRunner, LlmSoftwareBuilder, and all callers

Closes #648

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: move RuleBasedEvaluator into test module to fix dead_code warning

RuleBasedEvaluator has no production callers -- it was only used in
tests of itself. Moving it into #[cfg(test)] eliminates the clippy
dead_code error that broke CI.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-08 08:26:04 +00:00
df3635d6be feat(timezone): add timezone-aware session context (#671)
* feat(timezone): add timezone-aware session context (#661)

All timestamps were UTC-only, causing daily logs to split at UTC midnight,
cron schedules to fire in UTC, and no quiet hours for heartbeat. This adds
timezone as a per-session property flowing from the client.

Key changes:
- New `src/timezone.rs` module with resolution chain, parsing, and detection
- `IncomingMessage` carries optional timezone from client
- `JobContext.user_timezone` flows timezone to tools
- `next_cron_fire()` accepts timezone for schedule evaluation
- `Trigger::Cron` stores optional timezone (backward-compatible)
- Workspace gains `_tz` variants for daily logs and system prompt
- Heartbeat supports quiet hours (`HEARTBEAT_QUIET_START/END`)
- Web frontend sends `Intl.DateTimeFormat().resolvedOptions().timeZone`
- REPL auto-detects system timezone
- `DEFAULT_TIMEZONE` env var / settings for server-wide default

Storage stays UTC. Conversion happens at display boundaries.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): address review feedback on timezone-aware sessions

- Validate quiet hours values (0-23) in HeartbeatConfig::resolve()
- Fall back to settings values when env vars are unset for quiet hours
- Validate IANA timezone strings in routine_create/update with parse_timezone
- Add timezone field to routine_create tool schema
- Allow standalone timezone update on cron routines without changing schedule
- Return path from append_daily_log_tz to avoid TOCTOU race at midnight
- Delegate append_daily_log to append_daily_log_tz(entry, UTC) to avoid drift
- Preserve timezone through approval flow via PendingApproval.user_timezone
- Improve test_today_in_tz to not depend on hardcoded year
- Add 3 regression tests for quiet hours config validation

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix formatting in routine.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): address second round of review feedback

- Remove .claude/scheduled_tasks.lock from repo and add to .gitignore
- Store resolved timezone (not raw message.timezone) in PendingApproval
- Carry forward user_timezone through chained approvals in thread_ops
- Wire quiet_hours_start/end from config to HeartbeatRunner
- Support X-Timezone header as fallback in chat_send_handler

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): include user's local time in time tool response

The time tool's "now" operation now returns local_iso and timezone
fields based on ctx.user_timezone, so the LLM can report time in
the user's timezone instead of always UTC.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix formatting in time.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(timezone): address Copilot review round 3 — validation, deterministic tests, schema fixes

- Validate DEFAULT_TIMEZONE and HEARTBEAT_TIMEZONE at config load time
- Add timezone field to HeartbeatSettings and config::HeartbeatConfig
- Wire heartbeat timezone from config through agent_loop to HeartbeatRunner
- Add timezone to routine_update tool schema (was accepted but not advertised)
- Error on schedule/timezone update for non-cron routines
- Validate timezone in Trigger::from_db (coerce invalid to None with warning)
- Validate timezone in approval path (thread_ops.rs) before overwriting
- Time tool always includes timezone/local_iso fields (fallback to UTC)
- Make quiet hours tests deterministic using current UTC hour
- Add regression tests for config validation

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-08 08:01:56 +00:00
d144484b06 feat: WASM channel attachments with LLM pipeline integration (#596)
* feat: add inbound attachment support to WASM channel system

Add attachment record to WIT interface and implement inbound media
parsing across all four channel implementations (Telegram, Slack,
WhatsApp, Discord). Attachments flow from WASM channels through
EmittedMessage to IncomingMessage with validation (size limits,
MIME allowlist, count caps) at the host boundary.

- Add `attachment` record to `emitted-message` in wit/channel.wit
- Add `IncomingAttachment` struct to channel.rs and re-export
- Add host-side validation (20MB total, 10 max, MIME allowlist)
- Telegram: parse photo, document, audio, video, voice, sticker
- Slack: parse file attachments with url_private
- WhatsApp: parse image, audio, video, document with captions
- Discord: backward-compatible empty attachments
- Update FEATURE_PARITY.md section 7
- Add fixture-based tests per channel and host integration tests

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: integrate outbound attachment support and reconcile WIT types (#409)

Reconcile PR #409's outbound attachment work with our inbound attachment
support into a unified design:

WIT type split:
- `inbound-attachment` in channel-host: metadata-only (id, mime_type,
  filename, size_bytes, source_url, storage_key, extracted_text)
- `attachment` in channel: raw bytes (filename, mime_type, data) on
  agent-response for outbound sending

Outbound features (from PR #409):
- `on-broadcast` WIT export for proactive messages without prior inbound
- Telegram: multipart sendPhoto/sendDocument with auto photo→document
  fallback for files >10MB
- wrapper.rs: `call_on_broadcast`, `read_attachments` from disk,
  attachment params threaded through `call_on_respond`
- HTTP tool: `save_to` param for binary downloads to /tmp/ (50MB limit,
  path traversal protection, SSRF-safe redirect following)
- Message tool: allow /tmp/ paths for attachments alongside base_dir
- Credential env var fallback in inject_channel_credentials

Channel updates:
- All 4 channels implement on_broadcast (Telegram full, others stub)
- Telegram: polling_enabled config, adjusted poll timeout
- Inbound attachment types renamed to InboundAttachment in all channels

Tests: 1965 passing (9 new), 0 clippy warnings

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add audio transcription pipeline and extensible WIT attachment design

Add host-side transcription middleware (OpenAI Whisper) that detects audio
attachments with inline data on incoming messages and transcribes them
automatically. Refactor WIT inbound-attachment to use extras-json and a
store-attachment-data host function instead of typed fields, so future
attachment properties (dimensions, codec, etc.) don't require WIT changes
that invalidate all channel plugins.

- Add src/transcription/ module: TranscriptionProvider trait,
  TranscriptionMiddleware, AudioFormat enum, OpenAI Whisper provider
- Add src/config/transcription.rs: TRANSCRIPTION_ENABLED/MODEL/BASE_URL
- Wire middleware into agent message loop via AgentDeps
- WIT: replace data + duration-secs with extras-json + store-attachment-data
- Host: parse extras-json for well-known keys, merge stored binary data
- Telegram: download voice files via store-attachment-data, add duration
  to extras-json, add /file/bot to HTTP allowlist, voice-only placeholder
- Add reqwest multipart feature for Whisper API uploads
- 5 regression tests for transcription middleware

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: wire attachment processing into LLM pipeline with multimodal image support

Attachments on incoming messages are now augmented into user text via XML tags
before entering the turn system, and images with data are passed as multimodal
content parts (base64 data URIs) to LLM providers. This enables audio transcripts,
document text, and image content to reach the LLM without changes to ChatMessage
serialization or provider interfaces.

- Add src/agent/attachments.rs with augment_with_attachments() and 9 unit tests
- Add ContentPart/ImageUrl types to llm::provider with OpenAI-compatible serde
- Carry image_content_parts transiently on Turn (skipped in serialization)
- Update nearai_chat and rig_adapter to serialize multimodal content
- Add 3 e2e tests verifying attachments flow through the full agent loop

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: CI failures — formatting, version bumps, and Telegram voice test

- Fix cargo fmt formatting in attachments.rs, nearai_chat.rs, rig_adapter.rs,
  e2e_attachments.rs
- Bump channel registry versions 0.1.0 → 0.2.0 (discord, slack, telegram,
  whatsapp) to satisfy version-bump CI check
- Fix Telegram test_extract_attachments_voice: add missing required `duration`
  field to voice fixture JSON

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: bump WIT channel version to 0.3.0, fix Telegram voice test, add pre-commit hook

- Bump wit/channel.wit package version 0.2.0 → 0.3.0 (interface changed with
  store-attachment-data)
- Update WIT_CHANNEL_VERSION constant and registry wit_version fields to match
- Fix Telegram test_extract_attachments_voice: gate voice download behind
  #[cfg(target_arch = "wasm32")] so host functions aren't called in native tests,
  update assertions for generated filename and extras_json duration
- Add @0.3.0 linker stubs in wit_compat.rs
- Add .githooks/pre-commit hook that runs scripts/check-version-bumps.sh when
  WIT or extension sources are staged
- Symlink commit-msg regression hook into .githooks/

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: extract voice download from extract_attachments into handle_message

Move download_voice_file + store_attachment_data calls out of
extract_attachments into a separate download_and_store_voice function
called from handle_message. This keeps extract_attachments as a pure
data-mapping function with no host calls, making it fully testable
in native unit tests without #[cfg(target_arch)] gates.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review comments — security, correctness, and code quality

Security fixes:
- Add path validation to read_attachments (restrict to /tmp/) preventing
  arbitrary file reads from compromised tools
- Escape XML special characters in attachment filenames, MIME types, and
  extracted text to prevent prompt injection via tag spoofing
- Percent-encode file_id in Telegram getFile URL to prevent query injection
- Clone SecretString directly instead of expose_secret().to_string()

Correctness fixes:
- Fix store_attachment_data overwrite accounting: subtract old entry size
  before adding new to prevent inflated totals and false rejections
- Use max(reported, stored_size) for attachment size accounting to prevent
  WASM channels from under-reporting size_bytes to bypass limits
- Add application/octet-stream to MIME allowlist (channels default unknown
  types to this)

Code quality:
- Extract send_response helper in Telegram, deduplicating on_respond and
  on_broadcast
- Rename misleading Discord test to test_parse_slash_command_interaction
- Fix .githooks/commit-msg to use relative symlink (portable across machines)

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add tool_upgrade command + fix TOCTOU in save_to path validation

Add `tool_upgrade` — a new extension management tool that automatically
detects and reinstalls WASM extensions with outdated WIT versions.
Preserves authentication secrets during upgrade. Supports upgrading a
single extension by name or all installed WASM tools/channels at once.

Fix TOCTOU in `validate_save_to_path`: validate the path *before*
creating parent directories, so traversal paths like `/tmp/../../etc/`
cannot cause filesystem mutations outside /tmp before being rejected.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: unify WIT package version to 0.3.0 across tool.wit and all capabilities

tool.wit and channel.wit share the `near:agent` package namespace, so they
must declare the same version. Bumps tool.wit from 0.2.0 to 0.3.0 and
updates all capabilities files and registry entries to match.

Fixes `cargo component build` failure: "package identifier near:[email protected]
does not match previous package name of near:[email protected]"

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: move WIT file comments after package declaration

WIT treats `//` comments before `package` as doc comments. When both
tool.wit and channel.wit had header comments, the parser rejected them
as "doc comments on multiple 'package' items". Move comments after the
package declaration in both files.

Also bumps tool registry versions to 0.2.0 to match the WIT 0.3.0 bump.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: display extension versions in gateway Extensions tab

Add version field to InstalledExtension and RegistryEntry types, pipe
through the web API (ExtensionInfo, RegistryEntryInfo), and render as
a badge in the gateway UI for both installed and available extensions.

For installed WASM extensions, version is read from the capabilities
file with a fallback to the registry entry when the local file has no
version (old installations). Bump all extension Cargo.toml and registry
JSON versions from 0.1.0 to 0.2.0 to keep them in sync.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add document text extraction middleware for PDF, Office, and text files

Extract text from document attachments (PDF, DOCX, PPTX, XLSX, RTF, plain text,
code files) so the LLM can reason about uploaded documents. Uses pdf-extract for
PDFs, zip+XML parsing for Office XML formats, and UTF-8 decode for text files.
Wired into the agent loop after transcription middleware.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: download document files in Telegram channel for text extraction

The DocumentExtractionMiddleware needs file bytes in the attachment `data`
field, but only voice files were being downloaded. Document attachments
(PDFs, DOCX, etc.) had empty `data` and a source_url with a credential
placeholder that only works inside the WASM host's http_request.

Add `download_and_store_documents()` that downloads non-voice, non-image,
non-audio attachments via the existing two-step getFile→download flow and
stores bytes via `store_attachment_data` for host-side extraction.

Also rename `download_voice_file` → `download_telegram_file` since it's
generic for any file_id.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: allow Office MIME types and increase file download limit for Telegram

Two issues preventing document extraction from Telegram:

1. PPTX/DOCX/XLSX MIME types (application/vnd.*) were dropped by the
   WASM host attachment allowlist — add application/vnd., application/msword,
   and application/rtf prefixes.

2. Telegram file downloads over 10 MB failed with "Response body too large" —
   set max_response_bytes to 20 MB in Telegram capabilities.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: report document extraction errors back to user instead of silently skipping

- Bump max_response_bytes to 50 MB for Telegram file downloads
- When document extraction fails (too large, download error, parse error),
  set extracted_text to a user-friendly error message instead of leaving it
  None. This ensures the LLM tells the user what went wrong.
- On Telegram download failure, set extracted_text with the error so the
  user sees feedback even when the file never reaches the extraction middleware.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: store extracted document text in workspace memory for search/recall

After document extraction succeeds, write the extracted text to workspace
memory at `documents/{date}/{filename}`. This enables:
- Full-text and semantic search over past uploaded documents
- Cross-conversation recall ("what did that PDF say?")
- Automatic chunking and embedding via the workspace pipeline

Documents are stored with metadata header (uploader, channel, date, MIME type).
Error messages (extraction failures) are not stored — only successful extractions.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: CI failures — formatting, unused assignment warning

- Run cargo fmt on document_extraction and agent_loop modules
- Suppress unused_assignments warning on trace_llm_ref (used only
  behind #[cfg(feature = "libsql")])

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review comments — security, correctness, and code quality

Security fixes:
- Remove SSRF-prone download() from DocumentExtractionMiddleware (#13)
- Sanitize filenames in workspace path to prevent directory traversal (#11)
- Pre-check file size before reading in WASM wrapper to prevent OOM (#2)
- Percent-encode file_id in Telegram source URLs (#7)

Correctness fixes:
- Clear image_content_parts on turn end to prevent memory leak (#1)
- Find first *successful* transcription instead of first overall (#3)
- Enforce data.len() size limit in document extraction (#10)
- Use UTF-8 safe truncation with char_indices() (#12)

Robustness & code quality:
- Add 120s timeout to OpenAI Whisper HTTP client (#5)
- Trim trailing slash from Whisper base_url (#6)
- Allow ~/.ironclaw/ paths in WASM wrapper (#8)
- Return error from on_broadcast in Slack/Discord/WhatsApp (#9)
- Fix doc comment in HTTP tool (#4)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: formatting — cargo fmt

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address latest PR review — doc comments, error messages, version bumps

- Fix DocumentExtractionMiddleware doc comment (no longer downloads from source_url)
- Fix error message: "no inline data" instead of "no download URL"
- Log error + fallback instead of silent unwrap_or_default on Whisper HTTP client
- Bump all capabilities.json versions from 0.1.0 to 0.2.0 to match Cargo.toml

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: remove unsupported profile: minimal from CI workflows [skip-regression-check]

dtolnay/rust-toolchain@stable does not accept the 'profile' input
(it was a parameter for the deprecated actions-rs/toolchain action).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: merge with latest main — resolve compilation errors and PR review nits

- Add version: None to RegistryEntry/InstalledExtension test constructors
- Fix MessageContent type mismatches in nearai_chat tests (String → MessageContent::Text)
- Fix .contains() calls on MessageContent — use .as_text().unwrap()
- Remove redundant trace_llm_ref = None assignment in test_rig
- Check data size before clone in document extraction to avoid unnecessary allocation

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-07 18:01:40 +00:00
30790439ee perf: build system prompt once per turn, skip tools on force-text (#583)
* perf: build system prompt once per turn, skip tools on force-text, fix nudge role (#565)

Three fixes to agentic loop prompt handling:

1. Build system prompt once per turn instead of every tool iteration.
   `build_system_prompt_with_tools` is now pub; callers pass the result
   via `ReasoningContext::system_prompt` to avoid rebuilding ~1,500 tokens
   per iteration.

2. Skip `## Available Tools` section when `force_text = true`. The
   dispatcher passes a no-tools prompt variant on the final iteration,
   saving ~460 tokens and removing misleading instructions.

3. Change nudge message from `Role::System` to `Role::User`. A second
   system message mid-conversation is unsupported by most providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: revert nudge role change to keep ChatMessage::system

Copilot review correctly identified that using Role::User for the nudge
breaks compact_messages_for_retry, which uses rposition for Role::User
to find the last real user message. Role::Assistant would cause
back-to-back assistant messages. Since no production issues were reported
with the original system role, revert to ChatMessage::system.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address PR review — omit tool guidance when tools empty, rename shadowed var

- Conditionalize "Call tools…" guidelines and "## Tool Call Style" section
  in the system prompt so they are only included when tools are non-empty.
  Previously the force-text (no-tools) prompt still contained misleading
  tool-calling instructions. (Copilot review comment)

- Rename `system_prompt` → `cached_prompt` in dispatcher to avoid shadowing
  the earlier workspace identity `system_prompt` variable. (Copilot review)

- Add regression tests: `test_system_prompt_with_tools_contains_tool_guidance`
  and extended assertions in `test_system_prompt_without_tools_omits_tools_section`.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: [email protected] <[email protected]>
2026-03-07 09:15:00 +00:00
424a0366a9 feat: enable Anthropic prompt caching via automatic cache_control injection (#660)
* feat(llm): add Anthropic prompt caching and cache token tracking

- Inject cache_control via additional_params for Claude models in rig_adapter
- Add cache_read_input_tokens and cache_creation_input_tokens to
  CompletionResponse and ToolCompletionResponse
- Extract cached_input_tokens from rig-core unified Usage
- Add is_anthropic_model() detection helper with provider prefix support
- Log prompt cache hits at debug level (consistent with response_cache)
- Add 7 unit tests for cache injection and model detection
- Update all mock providers and test fixtures with new fields

* feat(cost): apply 90% cache discount to prompt-cached tokens in CostGuard

- Add cache_read_input_tokens to TokenUsage so cache counts flow from
  CompletionResponse through the reasoning layer to the dispatcher
- Update CostGuard::record_llm_call() to accept cache_read_input_tokens:
  cached tokens are billed at 10% of the normal input rate
- Thread cache_read_input_tokens from dispatcher into CostGuard
- Add test_cache_discount_reduces_cost verifying exact savings match
  90% of input cost for fully-cached requests
- Update all existing test callers with zero-cache parameter

* refactor(cache): scope cache_control to Anthropic backend and validate model support

- Replace model-name-based is_anthropic_model() with explicit
  enable_prompt_cache flag on RigAdapter, set only for the direct
  Anthropic backend via with_prompt_cache(true)
- Add supports_prompt_cache() to validate model names per Anthropic
  docs: only Claude 3+ models support caching; claude-2 and
  claude-instant are excluded to prevent 400 errors
- Warn when caching is enabled but model does not support it
- Replace is_anthropic_model tests with flag-based and model
  validation tests

* fix(cache): validate model at construction and propagate cache metrics through proxy

- Move supports_prompt_cache() check into with_prompt_cache() so
  unsupported models are detected once at construction, not per request
- Add cache_read_input_tokens and cache_creation_input_tokens to
  ProxyCompletionResponse and ProxyToolCompletionResponse with
  serde(default) for backward compatibility
- Pass cache metrics through orchestrator proxy instead of zeroing
- Use claude-opus-4-6 in cache discount test to match Anthropic
  semantics

* feat(llm): add configurable cache retention with write surcharge

- Add CacheRetention enum (none/short/long) to AnthropicDirectConfig
- Parse ANTHROPIC_CACHE_RETENTION env var (default: short)
- Inject TTL-aware cache_control (short=5m ephemeral, long=1h)
- Extract cache_creation_input_tokens from raw Anthropic response
- Add cache_write_multiplier() to LlmProvider trait (1.25x short, 2.0x long)
- Pipe dynamic write multiplier through dispatcher to CostGuard
- Add TokenUsage.cache_creation_input_tokens field
- Add tests for Long TTL injection, 5m and 1h write surcharges
- Document ANTHROPIC_CACHE_RETENTION in .env.example

* docs: fix stale cache_retention field comment

* fix: resolve CI failures after upstream merge

- Add missing cost_per_token arg to cache test callsites
- Apply cargo fmt to long lines in tests and tracing macros

* fix: address Copilot review feedback

- Use saturating_add for cache token sum to prevent u32 overflow
- Tighten supports_prompt_cache to explicitly match claude-3+/claude-4+
  and named families (claude-sonnet/claude-opus/claude-haiku)

* fix: adapt prompt caching to registry architecture and add missing cache fields

- Resolve merge conflicts: adapt CacheRetention and cache injection to
  the declarative provider registry (RegistryProviderConfig replaces
  AnthropicDirectConfig)
- Parse ANTHROPIC_CACHE_RETENTION env var in create_anthropic_from_registry()
- Use Anthropic automatic caching via top-level cache_control in
  additional_params (rig-core #[serde(flatten)] places it at request root)
- Add cache_read/creation_input_tokens fields to all mock LlmProviders
  added on main after PR #291 branched (response_cache, dispatcher,
  provider_chaos, trace_llm)
- Suppress clippy::too_many_arguments on record_llm_call and
  build_rig_request
- Add regression tests for cache injection (short/long/none) and
  cache_write_multiplier values

Co-Authored-By: Canvinus <[email protected]>

* fix: delegate cache_write_multiplier through provider wrappers and make cache_read_discount configurable

The 6 decorator providers (Retry, CircuitBreaker, Failover, SmartRouting,
CachedProvider, RecordingLlm) did not delegate cache_write_multiplier()
to their inner provider, causing it to always return 1.0 instead of the
actual 1.25x/2.0x from RigAdapter. This fix adds delegation for both
cache_write_multiplier() and the new cache_read_discount() method.

Also makes the cache read discount per-provider instead of hardcoding
Anthropic's 90% discount (÷10). OpenAI uses 50% (÷2), so the discount
is now returned by each provider via the LlmProvider trait.

Addresses review feedback on PR #660.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add CacheRetention FromStr/Display unit tests

Tests cover primary values, aliases (off/disabled/5m/ephemeral/1h),
case-insensitivity, invalid input error, and Display round-trip.

Addresses Copilot review feedback on PR #660.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Andrey <[email protected]>
Co-authored-by: Andrey Gruzdev <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-07 09:10:05 +00:00
8fbb782090 fix(llm): nudge LLM when it expresses tool intent without calling tools (#653)
* fix(llm): nudge LLM when it expresses tool intent without calling tools

Non-Anthropic models (especially GLM-5 via NEAR AI) frequently output
text like "Let me search for X" without including tool_calls, creating
a frustrating loop where the user waits but nothing happens.

Add llm_signals_tool_intent() detection that matches intent phrases
("let me search", "I'll fetch") while excluding conversational phrases
("let me explain", "let me know") and content inside code blocks.
When detected, inject a nudge message telling the model to actually
call the tool. Cap at 2 consecutive nudges to avoid infinite loops.

Applied to all three agentic loops: dispatcher (interactive chat),
agent/worker (background jobs), and worker/runtime (sandbox containers).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(nudge): address PR #653 review comments

1. Update doc comment to match implementation (code blocks only, not quotes)
2. Use match_indices() instead of find() to check all prefix occurrences
3. Add !available_tools.is_empty() guard in dispatcher nudge check
4. Reset consecutive_tool_intent_nudges on non-intent text responses
5. Add regression test for shadowed prefix detection

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(nudge): address second round of PR #653 review comments

1. Strip double-quoted strings in tool-intent detection to avoid false
   positives on quoted prose like `"Let me search the database"`.
2. Only reset consecutive_tool_intent_nudges when text does NOT signal
   intent — preserves the 2-nudge cap when intent is detected but cap
   is already reached.
3. Fix assertion message in nudge_cap test to report correct call index.
4. Add regression test for quoted strings outside code blocks.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-07 08:05:55 +00:00
4ac78a5b1f fix: reliable network tests and improved tool error messages (#626)
* fix(wasm): use per-engine cache dirs on Windows to avoid file lock error (#448)

On Windows, multiple wasmtime Engine instances sharing the default
compilation cache directory hit OS error 33 (ERROR_LOCK_VIOLATION)
because Windows holds exclusive file locks on memory-mapped cache
files. This is especially triggered when the Telegram channel WASM
module is loaded at startup and then hot-activated via the Extensions
UI.

Fix by giving each engine its own cache subdirectory on Windows
(~/.cache/ironclaw/wasmtime-tools/ and wasmtime-channels/). On
Unix the shared default cache continues to work as before.

Also adds Windows CI jobs (cargo check + clippy across all feature
flag combinations) to catch Windows-specific issues going forward.

Closes #448

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: silence Windows clippy warnings for platform-gated code

Gate PathBuf import behind #[cfg(unix)] in container.rs (only used
in Unix socket path), suppress unused_mut on conflicts Vec in
channels.rs (mutations are platform-gated), and add cfg gates on
keychain constants and hex_to_bytes that are only used on macOS/Linux.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: escape directory path in TOML cache config to prevent injection

Use double-quoted TOML strings with backslash and double-quote
escaping for the cache directory path, preventing breakage or
injection when paths contain special characters (e.g. single
quotes on Unix, backslashes on Windows).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: resolve cargo fmt formatting errors

Fix import ordering in container.rs and line wrapping in runtime.rs
to pass the CI formatting check.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(ci): restore Path import for all platforms, keep PathBuf unix-only

Path is used in non-cfg-gated functions (lines 148, 244) so it must
be available on all platforms. Only PathBuf is unix-specific.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use RFC 5737 TEST-NET-1 IPs for reliable network failure tests

Replace localhost/loopback addresses with 192.0.2.1 (TEST-NET-1) in
network failure tests so they work consistently behind HTTP proxies.
Tighten the catalog.rs error assertion to avoid matching any string
containing "error".

Closes #444 (takeover from hobostay)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: include tool name in error messages sent to LLM

Format tool errors as "Tool '<name>' failed: <reason>" instead of the
bare "Error: <reason>" so the LLM can identify which tool failed and
reason about alternatives. Does not short-circuit the agent loop --
errors still flow back to the LLM for reasoning.

Closes #487 (takeover from lustsazeus-lab, PR #530)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: resolve cargo fmt formatting in dispatcher

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-07 05:54:12 +00:00
470de5bd2d feat: merge http/web_fetch tools, add tool output stash for large responses (#578)
* feat: merge http/web_fetch tools, add tool output stash for large responses

Merge `web_fetch` into `http` tool with smart approval: plain GETs (no
headers, no body) run without approval and follow redirects with SSRF
re-validation per hop; all other requests require approval as before.

Add `tool_output_stash` on JobContext so full tool outputs are preserved
before safety-layer truncation. The `json` tool gains a
`source_tool_call_id` parameter to reference stashed outputs, enabling
reliable parsing of large API responses that exceed the 100KB context
limit.

Other improvements:
- Descriptive User-Agent header using CARGO_PKG_VERSION
- Truncation now keeps partial data + hint about source_tool_call_id
- System prompt reinforces tool_calls over narration
- json tool query/stringify handle pre-parsed (non-string) data

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* chore: delete dead web_fetch.rs (merged into http tool)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix rustfmt formatting

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: rename shadowed data binding for clarity in json tool

Address PR review: rename owned `data` to `data_value` before
re-binding as `let data = &data_value` to make ownership explicit.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(ci): mark network-dependent trace tests as #[ignore]

The weather_sf and baseball_stats tests hit live external APIs (wttr.in,
ESPN) which are unreliable in CI. Mark them #[ignore] so they don't
block the pipeline. Run locally with `--ignored` to include them.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: replay recorded HTTP exchanges in trace tests instead of hitting live APIs

Wire ReplayingHttpInterceptor into TestRig when the trace fixture
contains http_exchanges. This replays recorded responses instead of
making live network calls, making tests deterministic and CI-stable.

Add captured HTTP responses to weather_sf.json (wttr.in) and
baseball_stats.json (ESPN API) fixtures.

Revert #[ignore] on both tests — they now run offline.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: recover inline bracket-format tool calls from LLM text responses

When flatten_tool_messages converts tool calls to text like
`[Called tool `http` with arguments: {...}]` for NEAR AI compatibility,
the LLM sometimes echoes this format back in its text responses instead
of using proper tool_calls. Add recovery for this bracket format in
recover_tool_calls_from_content and strip it in clean_response so
users don't see raw tool call syntax.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-06 00:49:10 +00:00
b4b19738a8 Trajectory benchmarks and e2e trace test rig (#553)
* refactor: extract shared assertion helpers to support/assertions.rs

Move 5 assertion helpers from e2e_spot_checks.rs to a shared module.
Add assert_all_tools_succeeded and assert_tool_succeeded for eliminating
false positives in E2E tests.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add tool output capture via tool_results() accessor

Extract (name, preview) from ToolResult status events in TestChannel
and TestRig, enabling content assertions on tool outputs.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: correct tool parameters in 3 broken trace fixtures

- tool_time.json: add missing "operation": "now" for time tool
- robust_correct_tool.json: same fix
- memory_full_cycle.json: change "path" to "target" for memory_write

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add tool success and output assertions to eliminate false positives

Every E2E test that exercises tools now calls assert_all_tools_succeeded.
Added tool output content assertions where tool results are predictable
(time year, read_file content, memory_read content).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: capture per-tool timing from ToolStarted/ToolCompleted events

Record Instant on ToolStarted and compute elapsed duration on
ToolCompleted, wiring real timing data into collect_metrics() instead
of hardcoded zeros.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: add RAII CleanupGuard for temp file/dir cleanup in tests

Replace manual cleanup_test_dir() calls and inline remove_file() with
Drop-based CleanupGuard that ensures cleanup even if a test panics.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add Drop impl and graceful shutdown for TestRig

Wrap agent_handle in Option so Drop can abort leaked tasks. Signal
the channel shutdown before aborting for future cooperative shutdown.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: replace agent startup sleep with oneshot ready signal

Use a oneshot channel fired in Channel::start() instead of a fixed
100ms sleep, eliminating the race condition on slow systems.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: replace fragile string-matching iteration limit with count-based detection

Use tool completion count vs max_tool_iterations instead of scanning
status messages for "iteration"/"limit" substrings.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use assert_all_tools_succeeded for memory_full_cycle test

Remove incorrect comment about memory_tree failing with empty path
(it actually succeeds). Omit empty path from fixture and use the
standard assert_all_tools_succeeded instead of per-tool assertions.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: promote benchmark metrics types to library code

Move TraceMetrics, ScenarioResult, RunResult, MetricDelta, and
compare_runs() from tests/support/metrics.rs to src/benchmark/metrics.rs.
Existing tests use re-export for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add Scenario and Criterion types for agent benchmarking

Scenario defines a task with input, success criteria, and resource
limits. Criterion is an enum of programmatic checks (tool_used,
response_contains, etc.) evaluated without LLM judgment.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add initial benchmark scenario suite (12 scenarios across 5 categories)

Scenarios cover tool_selection, tool_chaining, error_recovery,
efficiency, and memory_operations. All loaded from JSON with
deserialization validation test.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add benchmark runner with BenchChannel and InstrumentedLlm

BenchChannel is a minimal Channel implementation for benchmarks.
InstrumentedLlm wraps any LlmProvider to capture per-call metrics.
Runner creates a fresh agent per scenario, evaluates success criteria,
and produces RunResult with timing, token, and cost metrics.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add baseline management, reports, and benchmark entry point

- baseline.rs: load/save/promote benchmark results
- report.rs: format comparison reports with regression detection
- benchmark_runner.rs: integration test with real LLM (feature-gated)
- Add benchmark feature flag to Cargo.toml

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: apply cargo fmt to benchmark module

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add multi-turn scenario types with setup, judge, ResponseNotContains

Add BenchScenario, Turn, TurnAssertions, JudgeConfig, ScenarioSetup,
WorkspaceSetup, SeedDocument types for multi-turn benchmark scenarios.
Add ResponseNotContains criterion variant. Add TurnAssertions::to_criteria()
converter for backward compat with existing evaluation engine.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add JSON scenario loader with recursive discovery and tag filter

Add load_bench_scenarios() for the new BenchScenario format with recursive
directory traversal and tag-based filtering. Create 4 initial trajectory
scenarios across tool-selection, multi-turn, and efficiency categories.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): multi-turn runner with workspace seeding and per-turn metrics

Add run_bench_scenario() that loops over BenchScenario turns, seeds workspace
documents, collects per-turn metrics (tokens, tool calls, wall time), and
evaluates per-turn assertions. Add TurnMetrics to metrics.rs and
clear_for_next_turn() to BenchChannel.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add LLM-as-judge scoring with prompt formatting and score parsing

Create judge.rs with format_judge_prompt, parse_judge_score, and judge_turn.
Wire into run_bench_scenario for turns with judge config -- scores below
min_score fail the turn.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add CLI subcommand (ironclaw benchmark)

Add BenchmarkCommand with --tags, --scenario, --no-judge, --timeout,
--update-baseline flags. Wire into Command enum and main.rs dispatch.
Feature-gated behind benchmark flag.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): per-scenario JSON output with full trajectory

Add save_scenario_results() that writes per-scenario JSON files alongside
the run summary. Each scenario gets its own file with turn_metrics trajectory.
Update CLI to use new output format.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add ToolRegistry::retain_only and wire tool filtering in scenarios

Add a retain_only() method to ToolRegistry that filters tools down to a
given allowlist. Wire this into run_bench_scenario() so that when a
scenario specifies a tools list in its setup, only those tools are
available during the benchmark run. Includes two tests for the new
method: one verifying filtering works and one verifying empty input
is a no-op.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): wire identity overrides into workspace before agent start

Add seed_identity() helper that writes identity files (IDENTITY.md,
USER.md, etc.) into the workspace before the agent starts, so that
workspace.system_prompt() picks them up. Wire it into
run_bench_scenario() after workspace seeding. Include a test that
verifies identity files are written and readable.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add --parallel and --max-cost CLI flags

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(benchmark): use feature-conditional snapshot names for CLI help tests

Prevents snapshot conflicts between default (no benchmark) and
all-features (with benchmark) builds by using separate snapshot names
per feature set.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): parallel execution with JoinSet and budget cap enforcement

Replace sequential loop in run_all_bench() with parallel execution using
JoinSet + semaphore when config.parallel > 1. Add budget cap enforcement
that skips remaining scenarios when max_total_cost_usd is exceeded.
Track skipped count in RunResult.skipped_scenarios and display it in
format_report().

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add tool restriction and identity override test scenarios

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* chore: fix formatting for Phase 3

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add SkillRegistry::retain_only and wire skill filtering in scenarios

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(benchmark): add --json flag for machine-readable output

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* ci: add GitHub Actions benchmark workflow (manual trigger)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor(benchmark): remove in-tree benchmark harness, keep retain_only utilities

Move benchmark-specific code out of ironclaw in preparation for the
nearai/benchmarks trajectory adapter. This removes:

- src/benchmark/ (runner, scenarios, metrics, judge, report, etc.)
- src/cli/benchmark.rs and the Benchmark CLI subcommand
- benchmarks/ data directory (scenarios + trajectories)
- .github/workflows/benchmark.yml
- The "benchmark" Cargo feature flag

What remains:
- ToolRegistry::retain_only() and SkillRegistry::retain_only()
- Test support types (TraceMetrics, InstrumentedLlm) inlined into
  tests/support/ instead of re-exporting from the deleted module

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* docs: add README for LLM trace fixture format

Documents the trajectory JSON format, response types, request hints,
directory structure, and how to write new traces.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat(test): unify trace format around turns, add multi-turn support

Introduce TraceTurn type that groups user_input with LLM response steps,
making traces self-contained conversation trajectories. Add run_trace()
to TestRig for automatic multi-turn replay. Backward-compatible: flat
"steps" JSON is deserialized as a single turn transparently.

Includes all trace fixtures (spot, coverage, advanced), plan docs, and
new e2e tests for steering, error recovery, long chains, memory, and
prompt injection resilience.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(test): fix CI failures after merging main

- Fix tool_json fixture: use "data" parameter (not "input") to match
  JsonTool schema
- Fix status_events test: remove assertion for "time" tool that isn't
  in the fixture (only "echo" calls are used)
- Allow dead_code in test support metrics/instrumented_llm modules
  (utilities for future benchmark tests)

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Working on recording traces and testing them

* feat(test): add declarative expects to trace fixtures, split infra tests

Add TraceExpects struct with 9 optional assertion fields (response_contains,
tools_used, all_tools_succeeded, etc.) that can be declared in fixture JSON
instead of hand-written Rust. Add verify_expects() and run_recorded_trace()
so recorded trace tests become one-liners.

Split trace infra tests (deserialization, backward compat) into
tests/trace_format.rs which doesn't require the libsql feature gate.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor(test): add expects to all trace fixtures, simplify e2e tests

Add declarative expects blocks to all 19 trace fixture JSONs across
spot/, coverage/, advanced/, and root directories. Update all 8 e2e
test files to use verify_trace_expects() / run_and_verify_trace(),
replacing ~270 lines of hand-written assertions with fixture-driven
verification.

Tests that check things beyond expects (file content on disk, metrics,
event ordering) keep those extra assertions alongside the declarative
ones.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(test): adapt tests to AppBuilder refactor, fix formatting

Update test files to work with refactored TestRigBuilder that uses
AppBuilder::build_all() (removing with_tools/with_workspace methods).
Update telegram_check fixture to use tool_list instead of echo.
Fix cargo fmt issues in src/llm/mod.rs and src/llm/recording.rs.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor(test): deduplicate support unit tests into single binary

Support modules (assertions, cleanup, test_channel, test_rig, trace_llm)
had #[cfg(test)] mod tests blocks that were compiled and run 12 times —
once per e2e test binary that declares `mod support;`. Extracted all 29
support unit tests into a dedicated `tests/support_unit_tests.rs` so they
run exactly once.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix trailing newlines in support files

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor(test): unify trace types and fix recorded multi-turn replay

Import shared types (TraceStep, TraceResponse, TraceToolCall, RequestHint,
ExpectedToolResult, MemorySnapshotEntry, HttpExchange*) from
ironclaw::llm::recording instead of redefining them in trace_llm.rs.

Fix the flat-steps deserializer to split at UserInput boundaries into
multiple turns, instead of filtering them out and wrapping everything
into a single turn. This enables recorded multi-turn traces to be
replayed as proper multi-turn conversations via run_trace().

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(test): fix CI failures - unused imports and missing struct fields

- Add #[allow(unused_imports)] on pub use re-exports in trace_llm.rs
  (types are re-exported for downstream test files, not used locally)
- Add `..` to ToolCompleted pattern in test_channel.rs to match new
  `error` and `parameters` fields

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(test): fix CI failures after merging main

- Add missing `error` and `parameters` fields to ToolCompleted
  constructors in support_unit_tests.rs
- Add `..` to ToolCompleted pattern match in support_unit_tests.rs
- Add #[allow(dead_code)] to CleanupGuard, LlmTrace impl, and
  TraceLlm impl (only used behind #[cfg(feature = "libsql")])

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Adding coverage running script

* fix(test): address review feedback on E2E test infrastructure

- Increase wait_for_responses polling to exponential backoff (50ms-500ms)
  and raise default timeout from 15s to 30s to reduce CI flakiness (#1)
- Strengthen prompt_injection_resilience test with positive safety layer
  assertion via has_safety_warnings(), enable injection_check (#2)
- Add assert_tool_order() helper and tools_order field in TraceExpects
  for verifying tool execution ordering in multi-step traces (#3)
- Document TraceLlm sequential-call assumption for concurrency (#6)
- Clean up CleanupGuard with PathKind enum instead of shotgun
  remove_file + remove_dir_all on every path (#8)
- Fix coverage.sh: default to --lib only, fix multi-filter syntax,
  add COV_ALL_TARGETS option
- Add coverage/ to .gitignore
- Remove planning docs from PR

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review - use HashSet in retain_only, improve skill test

- Use HashSet for O(N+M) lookup in SkillRegistry::retain_only and
  ToolRegistry::retain_only instead of linear scan
- Strengthen test_retain_only_empty_is_noop in SkillRegistry to
  pre-populate with a skill before asserting the no-op behavior

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(test): revert incorrect safety layer assertion in injection test

The safety layer sanitizes tool output, not user input. The injection
test sends a malicious user message with no tools called, so the safety
layer never fires. Reverted to the original test which correctly
validates the LLM refuses via trace expects. Also fixed case-sensitive
request hint ("ignore" -> "Ignore") to suppress noisy warning.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: clean stale profdata before coverage run

Adds `cargo llvm-cov clean` before each run to prevent
"mismatched data" warnings from stale instrumentation profiles.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: fix formatting in retain_only test

[skip-regression-check]

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]>
2026-03-05 09:13:09 +00:00
902492bcdb feat(web): show error details for failed tool calls (#490)
* feat(web): show error details and input params for failed tool calls

Failed tool calls in the gateway UI previously showed only a red X icon
with an empty expandable body. This change:

- Adds optional `error` and `parameters` fields to `ToolCompleted` SSE
  events so the browser receives failure details in real-time
- Auto-expands failed tool cards to make errors immediately visible
- Adds `StatusUpdate::tool_completed()` constructor that centralizes
  the 5 duplicated construction sites and applies `redact_params()` to
  prevent sensitive values (e.g. secret_save's "value" param) from
  leaking through SSE broadcasts
- Adds `sensitive_params()` trait method to `Tool` for declaring which
  parameters must be redacted before logging, hooks, and UI display
- Adds `redact_params()` utility and wires it through hooks, approvals,
  ActionRecord storage, and debug logs in dispatcher/worker
- Adds `SecretListTool` and `SecretDeleteTool` for LLM-driven secret
  management (values never returned, only names/metadata)
- Fixes auth flow: setup-only extensions show configure modal instead
  of OAuth card; auth_completed SSE dismisses both UI paths
- CI: release workflow creates PR instead of pushing directly to main
- Registry: MissingChecksum error enables source fallback for
  bootstrapping when checksums haven't been populated yet

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: apply cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: keep original params in PendingApproval for execution, redact only for display

Address two PR review comments:

1. execute_chat_tool_standalone now redacts sensitive params before logging,
   matching the pattern already used in worker.rs.

2. PendingApproval previously stored redacted parameters, which meant
   approved tool calls received "[REDACTED]" instead of the actual values.
   Add a display_parameters field for UI/logs and keep parameters as the
   original values used for execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address PR review comments

- worker.rs: redact sensitive params before BeforeToolCall hook, matching
  dispatcher.rs — hooks in the autonomous job path now receive redacted
  params instead of raw values
- registry.rs: fix docstring for register_secrets_tools (list, delete,
  not save/list/delete — no SecretSaveTool is registered)
- app.js: fix double toast/loadExtensions in submitConfigureModal —
  for non-OAuth success the auth_completed SSE already handles both,
  so skip them in the HTTP response handler to avoid duplicates

[skip-regression-check]

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-04 15:38:26 -08:00
89600e2b5c fix(agent): strip leaked [Called tool ...] text from responses (#497)
* fix(agent): strip leaked [Called tool ...] text from agent responses

When the NEAR AI provider flattens tool_call messages to plain text,
markers like [Called tool ...] and [Tool ... returned: ...] can leak
into the user-visible response if the LLM echoes them back. This adds
a sanitization step in the agentic loop's text response path that
strips these internal markers before returning. If stripping leaves
the response empty, a generic fallback message is returned instead.

Closes #487

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: use fold instead of collect+join to avoid heap allocation

Address review feedback: replace Vec collect + join with fold to build
the filtered string directly, avoiding an intermediate heap allocation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Pierre LE GUEN <[email protected]>
2026-03-04 18:16:26 +00:00
f18fb5173b feat(web): fix jobs UI parity for non-sandbox mode (#491)
* feat(web): fix jobs UI parity for non-sandbox mode

The web gateway Jobs UI was built primarily for sandbox (Docker) jobs.
When running without sandbox (common for NEAR AI hosted envs), multiple
features were broken. This change fixes all of them:

- Agent jobs now broadcast live SSE events to the web UI (Activity tab)
- Agent job restart via scheduler.dispatch_job (not chat message)
- Follow-up prompts for agent jobs via WorkerMessage injection
- Capability flags (can_restart, can_prompt, job_kind) in job detail API
- Rate-limit retry with cap (10 consecutive) and Retry-After header parsing
- Plan interruption on user message (breaks out of plan, re-evaluates)
- Correct SSE status field in mark_completed/mark_failed/mark_stuck
- SseManager preserved across rebuild_state calls

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* style: fix formatting in db/mod.rs and nearai_chat.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-03 22:23:30 +08:00
a24fd3e8a3 Add automated QA: schema validator, CI matrix, Docker build, and P1 test coverage (#353)
* Add automated QA: tool schema validator, feature-flag CI matrix, Docker build

P0 items from the automated QA plan (#352):

- Add validate_tool_schema() that checks OpenAI strict-mode rules
  (type: object, required keys in properties, nested object/array
  recursion) with 10 unit tests and 6 integration tests covering
  all core built-in tools

- CI test matrix now runs with --all-features, default features, and
  --no-default-features --features libsql to catch dead code behind
  wrong cfg gates

- CI clippy now runs the same 3-feature matrix with --all flags

- Docker build job added to catch missing files in Dockerfile

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add P1 automated QA tests and fix LeakDetector prefix shadowing bug

P1 test coverage: config round-trip (settings + bootstrap), shell tool
arg handling, safety adversarial tests (sanitizer, leak detector,
allowlist), turn persistence (conversations, metadata, pagination, jobs),
and a clippy fix for libsql-only builds.

Fixed a real bug where AhoCorasick non-overlapping prefix iteration
caused shorter prefixes (e.g. "sk-") to shadow longer ones
(e.g. "sk-ant-api"), preventing Anthropic API key and SSH private key
detection.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add P2 automated QA tests: chaos, lifecycle, collision, and recovery

Cover all P2 items from the automated QA plan:
- Circuit breaker chaos tests (hanging provider, rapid cycles, mixed errors)
- Failover chaos tests (hanging failover, all-fail, tools path, single provider)
- Value estimator boundary tests (negative cost, zero price, zero earnings)
- Context length recovery test (ContextLengthExceeded -> compact -> retry)
- WASM channel lifecycle tests (write/commit/read round-trip, namespace isolation)
- Extension registry collision tests (same-name different-kind coexistence)
- Extension filesystem collision tests (separate dirs, detect_kind priority)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add P3 concurrent stress tests for ContextManager and SessionManager

Tests verify thread safety of double-checked locking, TOCTOU
prevention, and RwLock-based concurrent access patterns under load.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add dispatcher loop guard and self-repair stuck job tests

Dispatcher: test force_text mechanism prevents infinite tool call loops,
verify iteration bound arithmetic guarantees termination for all configs.

Self-repair: test stuck job detection, recovery within attempt limits,
manual escalation when limit exceeded, graceful degradation without
store/builder dependencies.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add E2E testing infrastructure design doc

Python + Playwright framework with mock LLM server for deterministic
browser-level testing of the web gateway. Covers connection/auth,
chat round-trip with SSE streaming, and skills lifecycle scenarios.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Add E2E testing infrastructure implementation plan

10-task plan covering: scaffolding, mock LLM server, helpers,
conftest fixtures, connection/chat/skills test scenarios,
CI workflow, README, and integration run.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* scaffold: E2E test project with pyproject.toml

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: E2E helpers with DOM selectors and port discovery

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: mock OpenAI-compat LLM server for E2E tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: E2E conftest with session fixtures for mock LLM and ironclaw

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: E2E scenario 1 -- connection and tab navigation tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: E2E scenario 2 -- chat message round-trip tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: E2E scenario 3 -- skills search, install, remove tests

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* ci: add weekly E2E test workflow with Playwright

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* docs: E2E test README with setup and usage instructions

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: E2E test integration fixes from first run

- Use temp file DB instead of :memory: (libSQL :memory: doesn't persist
  tables across execute_batch)
- Fix installed skills selector: #skills-list not #installed-skills
- Add pytest-timeout to dependencies
- Improve skills install/remove test with wait_for instead of fixed sleeps

8 passed, 1 skipped (skills install depends on ClawHub availability)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add OpenAI strict-mode schema validator for all built-in tools (QA 1.1)

Add src/tools/schema_validator.rs with validate_strict_schema() that checks
tool parameter schemas against OpenAI function calling strict-mode rules:
type object at top level, required keys in properties, enum type consistency,
array items definitions, nested object recursion, and additionalProperties.

17 tests validate all 34+ built-in tool schemas across 5 test groups:
- 9 simple tools (echo, time, json, http, shell, file read/write/list/patch)
- 4 job tools (create, list, status, cancel)
- 4 skill tools (list, search, install, remove)
- 13 inline schemas for extension, routine, and complex job tools
- 4 memory tool schemas

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add E2E scenarios for SSE reconnect, HTML injection, and tool approval (QA 3.3/5/6)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: E2E test reliability for HTML injection and SSE reconnect

- HTML injection: test sanitization directly via JS injection instead of
  depending on full LLM round-trip (avoids intermittent 404 from mock)
- SSE reconnect: increase wait times for DB persistence and relax
  assertion to check total message count after history reload

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt formatting

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add WASM and MCP tool schema validation tests (QA 1.1)

Extends the schema validator with representative WASM tool schemas
(weather, HTTP client, batch processor, status), MCP tool schemas
(default, file read, SQL query, strict mode), and defect detection
tests for common external schema issues (missing type, typo in
required, array without items, enum type mismatch).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add auth middleware and compaction module tests

Auth middleware (8 new tests): valid/invalid bearer tokens, query param
fallback, case sensitivity, empty tokens, whitespace handling.

Compaction module (16 new tests): truncation strategy, summarize strategy
with mock LLM, workspace fallback, format_turns helper, sequential
compactions, coherence after compaction, token decrease verification.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add config round-trip integration tests (QA 1.2)

Test the full bootstrap .env lifecycle: write via the same format
as save_bootstrap_env/upsert_bootstrap_var, read back via dotenvy,
and assert values match. Covers LLM backend selection, embedding
disable flag, onboard completion flag, session token keys, multi-key
preservation across upsert, and special characters (spaces, equals,
quotes, backslashes, hashes).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add value estimator boundary tests and dispatcher loop guard (QA 4.3/4.4)

Value estimator (14 new tests): zero/negative prices, large values,
negative cost, exact margin boundaries, custom margin configuration.

Dispatcher loop guard (2 new tests): verifies the dispatch loop terminates
when all tool calls fail (regression guard for PR #252 infinite loop)
and when max iterations are reached.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* test: add failover edge cases and provider chaos tests (QA 2.6/4.1)

Failover edge cases (4 new tests): cooldown at zero nanos, half-open
failure reopens circuit, all providers fail gracefully (no panic),
single failing provider with cooldown.

Provider chaos tests (15 new tests): flakey provider with retries,
hanging provider with timeout, garbage provider, circuit breaker
trip/recover, failover chain cascading, non-transient error stops
chain, full stack integration (retry + failover + circuit breaker).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review feedback on QA tests

- Fix Bearer auth case-sensitivity per RFC 6750 (auth.rs)
- Refactor bootstrap.rs to expose path-parameterized variants so
  config_round_trip tests call real code instead of reimplementations
- Remove deprecated event_loop fixture, use dynamic ports, minimal env,
  session-scoped browser, and wire HEADED=1 in E2E conftest
- Add cross-referencing doc comments between schema validators
- Simplify array validation logic in tool.rs
- Bump e2e.yml checkout@v4 to @v6

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* style: cargo fmt and fix clippy warning in signal.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: improve E2E fixture error reporting and prevent stdin blocking

- Add --no-onboard flag to prevent wizard from blocking in CI
- Pipe /dev/null to stdin to prevent any stdin reads from hanging
- Add RUST_BACKTRACE=1 for crash diagnostics
- On server startup timeout, dump stderr to pytest output so CI
  logs show why the server failed to start

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: set session-scoped event loop for E2E async fixtures

pytest-asyncio 1.3.0 defaults asyncio_default_fixture_loop_scope to
None (function scope), causing session-scoped async fixtures to be
re-evaluated per test function with independent event loops. Each test
then independently attempts to start the ironclaw server, times out
at 120s, and wastes ~24 minutes of CI before the job is cancelled.

Setting asyncio_default_fixture_loop_scope = "session" ensures all
session-scoped async fixtures share a single event loop, so the server
starts once and is reused across all tests.

Also adds -x flag to pytest in CI to stop on first failure instead of
running all 19 tests when the fixture is broken.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: set test loop scope to session to match fixture loop scope

With asyncio_default_fixture_loop_scope=session but
asyncio_default_test_loop_scope=function (the default), tests run on
a per-function event loop while fixtures produce objects (Playwright
pages, browser contexts) on the session event loop. This event loop
mismatch causes the test to hang indefinitely awaiting Playwright
operations that are bound to the wrong loop.

Setting both scopes to "session" ensures a single event loop is shared
across all fixtures and tests, eliminating the deadlock.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* ci: add roll-up jobs to match branch protection required checks

Branch protection expects "Code Style (fmt + clippy)" and "Run Tests"
status checks, but only individual job names were reported. Add
roll-up jobs that aggregate results and report the expected names.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-27 09:09:45 +04:00
ibhagwanandGitHub bf35b59222 feat(signal) attachment upload + message tool (#375)
* feat(channels/signal): add attachment upload support

- Add attachments field to OutgoingResponse for carrying file paths
- Add with_attachments() builder method to OutgoingResponse
- Update build_rpc_params() to include attachments array in JSON-RPC
- Update respond() and broadcast() to handle attachments:
  - Text + attachments: sends text first, then each attachment
  - Attachments only: sends each attachment with path as message
  - Text only: original behavior (no change)
- Add tests for build_rpc_params with attachments
- Add tests for OutgoingResponse attachment builder

This enables the Signal channel to send files via signal-cli daemon's
JSON-RPC send method, matching the nullclaw implementation.

Risk: Low - uses existing JSON-RPC infrastructure
Tests: 85 signal tests pass, 1543 lib tests pass

* feat(tools): add message tool for cross-channel messaging

Add a new 'message' tool that allows the agent to send messages to
any connected channel (signal, telegram, slack, etc.) with optional
file attachments.

Features:
- Send messages to specific channel + target combinations
- Support for attachments (file paths)
- E.164 validation delegated to channel (signal expects +number,
  telegram accepts username/chat_id, slack uses #channels)
- Helpful error messages showing available channels on failure

Tool schema:
- content: message text (required)
- channel: target channel name (optional, defaults to current channel)
- target: recipient (E.164, group ID, chat ID) (optional, defaults to
  current user/group chat)
- attachments: optional file paths to send

This complements the recently added attachment upload support for the
Signal channel by giving the agent a proper way to specify attachments
when sending messages.

Tests: 4 new tests for message tool schema
Risk: Low - new tool with no breaking changes
Tests: All 1547 lib tests pass, clippy clean

* feat(llm): add conversation context to system prompt for Signal

Add conversation_context HashMap to Reasoning struct to pass channel-specific
metadata (sender phone, sender UUID, group ID) to the LLM. This helps the
agent know who/group it's talking to, preventing it from hallucinating
phone numbers or sending to wrong recipients.

Changes:
- Add conversation_context field and with_conversation_data() builder method
- Add build_conversation_section() to include current conversation info in system prompt
- Update dispatcher to extract Signal metadata (sender, sender_uuid, group) and pass to Reasoning
- Add signal_sender_uuid to Signal channel metadata for privacy mode users

* feat(tools): add secure attachment path validation with sandbox enforcement

Implement robust path validation for message tool attachments to prevent
directory traversal attacks and unauthorized file access. Attachments are
now sandboxed to ~/.ironclaw/ by default.

Key changes:
- Create shared path_utils module with validate_path() and is_path_safe_basic()
- Extract normalize_lexical() from file.rs for reuse
- MessageTool now enforces sandbox at ~/.ironclaw/ for all attachments
- Path validation includes: traversal detection, canonicalization, symlink resolution
- Error messages reveal the allowed sandbox directory for user clarity

Security improvements:
- Blocks path traversal attacks (../, URL-encoded, null bytes)
- Canonicalizes paths to resolve symlinks before validation
- Walks up to nearest existing ancestor for non-existent paths
- Prevents escape from sandbox directory

Backward compatibility:
- File tools continue to work with their configured base_dir
- Message tool defaults to ~/.ironclaw/ sandbox
- Tests updated to create files within sandbox

Tests added:
- path_utils module tests (9 tests for validation logic)
- message tool attachment validation tests
- All 1571 existing tests pass

* fix(channels/signal): use robust path validation with full security coverage

Signal channel's validate_attachment_paths() now uses path_utils::validate_path()
for consistent, secure path validation.

Fixes:
- Replaced weak path.contains('..') check with robust validate_path()
- validate_path() now includes is_path_safe_basic() as first-pass filter to
  block null bytes and URL-encoded traversal sequences (%2e%2e%2f)
- Error message now shows allowed sandbox directory (~/.ironclaw/)

Security coverage:
- Path traversal: ../, foo/../bar, ../../etc/passwd ✓
- URL-encoded traversal: %2e%2e%2fetc/passwd ✓
- Null byte injection: file\0.txt ✓
- Paths outside sandbox: /tmp/evil.txt ✓
- Symlink escape attempts (via canonicalization) ✓

Tests added:
- validate_attachment_paths_rejects_path_outside_sandbox
- validate_attachment_paths_rejects_url_encoded_traversal
- validate_attachment_paths_rejects_null_byte
- Fixed broken assertion in rejects_double_dot test

* fix(llm): add Signal channel to build_channel_section to include message tool hint

The catch-all '_' arm was returning early before the message_tool_hint
section was constructed, which meant Signal users never got the
'## Proactive Messaging' section with examples for:
- Using attachments parameter
- Targeting different users/groups
- Cross-channel messaging

Now Signal will include the full message_tool_hint section with usage examples.

* fix(tools): use async locks in register_message_tools to prevent silent failures

The method was using register_sync which calls try_write() on self.tools.
If the lock was held, try_write() would return Err and silently skip
adding the tool to the registry, while self.message_tool already held
a reference. This creates an inconsistent state.

Fix: use async write locks directly instead of register_sync to ensure
the tool is always registered or the method fails explicitly.

* refactor(dispatcher): use Channel trait for conversation context

Replace hardcoded 'if message.channel == signal' block with generic
conversation_context() method on the Channel trait. This allows any
channel to provide context (sender, group, etc.) without hardcoding
channel names.

Changes:
- Add conversation_context() method to Channel trait (default: empty)
- Implement for SignalChannel: extracts sender, sender_uuid, group
- Add get_channel() to ChannelManager (returns Arc<dyn Channel>)
- Change ChannelManager storage from Box to Arc for shared access
- Update dispatcher to use new trait method
- Add tests for conversation_context extraction

Other channels (Telegram, Slack, Discord) can now implement this
method to provide conversation context without code changes in dispatcher.

* fix(tests): split message_tool_with_attachments into sandbox and channel tests

The original test was passing for the wrong reason - it expected an error
because the channel doesn't exist, but actually failed earlier during sandbox
validation because /tmp paths are outside ~/.ironclaw/.

Split into two tests:
- message_tool_with_attachments_outside_sandbox: verifies sandbox rejection
  with explicit error message check
- message_tool_with_attachments_inside_sandbox_no_channel: uses files within
  sandbox (like message_tool_passes_attachment_to_broadcast does) and verifies
  the channel-related error message

* security(message tool): add rate limiting, approval requirements, and audit logging

The message tool can send to ANY connected channel/target making it a significant
abuse vector if the LLM is compromised or prompt-injected. This commit adds:

1. Rate limiting: 10 messages/minute, 100/hour per user
2. Approval requirement: Always requires approval for cross-channel messages
   (when channel differs from the default conversation channel)
3. Audit logging: Every successful message send is logged with channel,
   target, and attachment count

The approval logic:
- If channel param is provided and differs from default -> Always require approval
- If no default channel is set and explicit channel provided -> Always require approval
- Otherwise (using default channel) -> UnlessAutoApproved

* fix(message tool): return explicit error for malformed attachments array

Previously, malformed attachments like {"attachments": [123, true]} would be
silently ignored via .ok().unwrap_or_default(), leaving users confused
when attachments weren't sent.

Now returns explicit error: "Invalid attachments format: ..."

* fix(message tool): verify attachment files exist before sending

Previously, non-existent paths would pass sandbox validation and surface
as confusing Signal RPC errors. Now returns clear "Attachment file not found" error.

* fix(test): create sandbox directory if it doesn't exist for CI

The test validate_attachment_paths_accepts_normal_paths uses
tempfile::tempdir_in() which requires the parent directory to exist.
In CI, ~/.ironclaw doesn't exist, causing test failure.
2026-02-26 18:06:21 +04:00
443b120272 feat(web): inline tool activity cards with auto-collapsing (#376)
* feat(web): inline tool activity cards with auto-collapsing

Add Claude/Codex-style inline tool activity cards to the web UI that
show tool execution progress directly in the chat conversation.

While processing:
- Animated thinking dots with message text (e.g. "Calling LLM...")
- Individual tool cards with live spinner and elapsed timer
- Cards show tool name, duration, and expandable output preview

After response arrives:
- Activity group auto-collapses to "Used N tools (Xs)"
- Click summary to expand and see individual tool cards
- Click card header to see tool output in monospace

Also includes:
- "Calling LLM..." thinking status from dispatcher (all channels)
- 5-minute max timer guard to prevent leaks on dropped SSE
- Handles parallel tools, same tool twice, failures, thread switching

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix(web): use frozen duration for completed tools in activity summary

The collapsed activity summary was showing inflated total duration
because finalizeActivityGroup() recalculated elapsed time from
Date.now() for already-completed tools. Now each tool card stores
its final duration at completion time and the summary uses that
frozen value instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-02-25 16:01:35 -08:00
4e2dd76ae5 Fix skills system: enable by default, fix registry and install (#300)
* feat: add Docker detection module with platform guidance

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add Docker sandbox step to setup wizard

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: show Docker status in boot screen

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: check Docker availability at startup

When SANDBOX_ENABLED=true, proactively detect whether Docker is
installed and running before creating the ContainerJobManager.
If Docker is unavailable, log a warning with platform-specific
guidance and disable the sandbox for the session.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: enable sandbox by default, improve wizard explanation, document detection limits

- SandboxConfig defaults to enabled=true (startup check disables
  gracefully if Docker is unavailable)
- Wizard step explains why Docker matters: isolation for LLM-generated
  code vs running directly on the host
- Document detection confidence per platform in detect.rs module docs:
  high on macOS/Linux, medium on Windows (named pipe edge cases)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: cargo fmt + update test_builder_defaults for enabled-by-default

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: deduplicate wizard Docker status handling per review

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: fix skills system - enable by default, fix registry connectivity and install

- Enable skills system by default (SKILLS_ENABLED no longer required)
- Bypass Vercel TLS fingerprint blocking by pointing DEFAULT_REGISTRY_URL
  directly at the Convex backend (wry-manatee-359.convex.site)
- Handle ZIP archives from ClawHub download API - the registry returns
  ZIP files containing SKILL.md, not raw text. Uses flate2 (existing dep)
  to extract SKILL.md from the archive.
- Surface catalog search errors in the UI with a yellow warning banner
  instead of silently returning empty results
- Handle both {"results":[...]} envelope and bare [...] array JSON formats
  from the search API
- Add ClawHub links and metadata to search result cards (clickable skill
  names linking to clawhub.ai, relevance score, "updated X ago" recency)
- Fix 3 pre-existing clippy warnings in tests/html_to_markdown.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address security review feedback on ZIP extraction and SSRF

- Cap download size to 10 MB before reading response body
- Guard against ZIP bombs: cap uncompressed_size at 1 MB, wrap
  DeflateDecoder with .take() read limit
- Use checked_add for ZIP header offset arithmetic to prevent overflow
- Remove .unwrap() on try_into() -- use direct array construction
- Handle IPv4-mapped IPv6 addresses (::ffff:192.168.x.x) in SSRF checks
- Don't leak internal registry URLs in user-facing catalog_error messages
- Fix non-ASCII panic in catalog response debug logging (use .get() instead
  of byte slicing)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add /skills command and enrich search results with ClawHub metadata

- Parse /skills and /skills search <query> as SystemCommands in submission.rs
- Add skill_catalog to AgentDeps and wire it through main.rs
- Handle "skills" command in commands.rs: list installed skills and search ClawHub
- Add /skills and /skills search <q> entries to /help output
- Add SkillDetail, SkillStats, SkillOwner structs to catalog.rs
- Add fetch_skill_detail() calling GET /api/v1/skills/{slug} on Convex backend
- Add enrich_search_results() to fetch stars/downloads/owner for top 5 results in parallel
- Fix SkillDetailResponse wrapper struct to match actual API shape: {"skill":{...},"owner":{...}}
- Surface stars, downloads, owner in web UI skill search cards (app.js)
- Surface enriched data in skills web handler and skill_search tool output

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: cargo fmt after merge conflict resolution

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: separate installed_skills dir for correct trust on restart, remove duplicate handlers

Trust level bug: skills installed from ClawHub were written to user_dir
(~/.ironclaw/skills/) which is discovered as Trusted on restart. Now installs
go to ~/.ironclaw/installed_skills/ which is discovered as Installed, matching
the documented skill directory layout.

Changes:
- SkillsConfig: add installed_dir field (SKILLS_INSTALLED_DIR env var,
  default ~/.ironclaw/installed_skills/)
- SkillRegistry: add with_installed_dir() builder, installed_dir()/
  install_target_dir() accessors, and discover installed_dir with
  SkillTrust::Installed in discover_all()
- All install paths (web handler, skill tool) use install_target_dir()
  instead of user_dir() so new installs land in the correct directory
- 3 new registry tests: test_installed_dir_uses_installed_trust,
  test_install_target_dir_prefers_installed_dir,
  test_user_dir_stays_trusted_with_installed_dir

Duplicate handler cleanup: handlers/skills.rs was the canonical implementation
but the handlers module was never compiled (not declared in web/mod.rs), so
server.rs had its own duplicate inline definitions that the router used.
Wire up the handlers module, delete the 260-line duplicate in server.rs, and
have server.rs import skills handlers from handlers::skills. Fix pre-existing
compile error in handlers/extensions.rs (missing needs_setup field). Add
#[allow(dead_code)] on not-yet-migrated handler modules to suppress warnings.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: probe more Docker socket paths on macOS

Docker Desktop 4.13+ (stabilised in 4.18) no longer creates the
/var/run/docker.sock symlink by default. The API socket lives at
~/.docker/run/docker.sock, which bollard's connect_with_local_defaults()
does not try.

Add a fallback probe list covering the common macOS container runtimes:
- ~/.docker/run/docker.sock   — Docker Desktop 4.13+
- ~/.colima/default/docker.sock — Colima
- ~/.rd/docker.sock             — Rancher Desktop

Remove the bogus ~/.docker/desktop/docker.sock path that was added
previously; it is not an API socket on any known Docker installation.

Fixes the false-negative "Docker is installed but not running" warning
reported by Illia on macOS with Docker Desktop 4.18+.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* Harden Docker detection for rootless Linux and Windows fallback

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-23 10:04:02 -08:00
6f21cfa680 fix: auto-compact and retry on ContextLengthExceeded (#315)
* fix: auto-compact and retry on ContextLengthExceeded in agentic loop

When the LLM returns a context-length-exceeded error mid-turn, the
dispatcher now automatically compacts the conversation history and
retries once instead of propagating the raw error to the user.

The compaction keeps all system messages (system prompt, skill context),
the last user message, and all subsequent messages (current turn's tool
calls and results), dropping older conversation history. A note is
inserted to inform the LLM that earlier context was dropped.

If the retry also fails, the original error is returned.

Fixes nearai/ironclaw#260

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Address Gemini/Copilot review feedback

- Fix system message duplication: only collect system messages before the
  last User message to avoid duplicating nudges in the tail slice (Gemini + Copilot)
- Only add compaction note when earlier history is actually dropped (Copilot)
- Propagate actual retry error instead of masking with original (Copilot)
- Fix else branch to preserve system messages when no User messages exist
- Add test for nudge-after-user deduplication

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-23 00:29:31 +00:00
ea57447649 feat: hot-activate WASM channels, channel-first prompts, unified artifact resolution (#297)
* refactor: unify WASM artifact resolution into registry/artifacts.rs

Consolidate duplicated WASM find/build/install logic from 5+ files into
a single src/registry/artifacts.rs module. This fixes two bugs:
- registry/installer.rs now respects CARGO_TARGET_DIR (was hardcoded)
- channels/wasm/bundled.rs now searches all WASM triples (was wasip2 only)

Also includes: extension manager hot-activation for WASM channels,
extension guidance in LLM prompts, channel manager hot-add support,
webhook router channel lookup, and minor cleanups.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: send approval prompts as messages on WASM channels (Telegram, Slack)

WASM channels mapped ApprovalNeeded status to a typing indicator,
so users on Telegram never saw tool approval prompts — the agent
got stuck in AwaitingApproval and all subsequent messages failed
with "Waiting for approval".

- Intercept ApprovalNeeded in WasmChannel::handle_status_update and
  send the prompt as an actual message via call_on_respond, showing
  tool name, description, parameters, and yes/no/always instructions
- Guard against empty LLM responses after clean_response() strips
  reasoning_content think-tags (defense-in-depth for reasoning models)
- Add reasoning_content fallback to NearAiChatProvider::complete()
  for consistency with complete_with_tools()
- Add debug logging when empty responses are suppressed
- Improve error logging for channel respond() failures
- Register WASM channel webhook routes before credential checks so
  platforms don't deactivate webhook URLs with 404s

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR #297 review comments

- ChannelManager::add: use async write().await instead of try_write()
- resolve_target_dir: resolve relative CARGO_TARGET_DIR against crate_dir
- install_wasm_files: log warning on capabilities copy failure
- refresh_active_channel: load capabilities file for webhook secret name
- activate_wasm_channel: validate name against path traversal
- Fix cargo fmt formatting in nearai_chat.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: wire up channel runtime for hot-activation and address PR review round 2

- Wire up set_channel_runtime() in main.rs so hot-activation actually works
  (with_channel_runtime was never called — hot-activation was dead code)
- Change ExtensionManager channel runtime fields to RwLock<Option<...>>
  interior mutability so set_channel_runtime(&self) works after Arc wrapping
- Fix artifact tests to use resolve_target_dir() instead of hardcoding
  "target/" (breaks when CARGO_TARGET_DIR is set)
- Fix bundled.rs build hint: cargo component build (not cargo build --target)
- Fix wasm_artifact_path doc: binary_name should not include .wasm extension

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use char-aware truncation to prevent UTF-8 panic in approval prompt

&s[..77] panics on multi-byte UTF-8 (CJK, emoji). Use s.chars().take(77)
for safe truncation at character boundaries.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-22 08:09:56 +00:00
c3ce26278a refactor: simplify config resolution and consolidate main.rs init (#287)
* refactor: simplify config resolution and consolidate main.rs init into AppBuilder

- Add parse_bool_env() and parse_string_env() helpers to eliminate repetitive
  5-line optional_env/parse/map_err/unwrap_or boilerplate across 12 config files
- Add EmbeddingsConfig::create_provider() to centralize embeddings construction
  (fixes hardcoded 1536 dimensions and missing Ollama provider in app.rs)
- Extract init_cli_tracing(), setup_wasm_channels(), start_tunnel(),
  run_memory_command(), run_worker(), run_claude_bridge() from main.rs
- Replace ~600 lines of inline init in main.rs with AppBuilder::build_all()
- Expose catalog_entries from AppComponents for gateway registry entries
- Net reduction: ~738 lines across 15 files

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: propagate dev_loaded_tool_names from AppBuilder and add parse_option_env helper

Address PR review feedback:

- Capture dev_loaded_tool_names from WASM loading in init_extensions()
  and expose via AppComponents so bootstrap_hooks receives the actual
  dev tool names instead of an empty slice (fixes silent hook skip)
- Add parse_option_env<T>() helper for Option<T> config fields,
  simplifying max_cost_per_day_cents and max_actions_per_hour in agent.rs

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: fetch real NEAR AI pricing and unify cost calculation path

CostGuard was independently looking up pricing via costs::model_cost(),
falling back to GPT-4o default rates when NEAR AI model names didn't
match the static table — causing ~3x cost overestimates in logs.

- Add pricing map to NearAiChatProvider that fetches real rates from
  /v1/model/list at startup (background, non-blocking)
- Update cost_per_token() to check fetched pricing first, then static
  table, then default
- Add cost_per_token parameter to CostGuard::record_llm_call() so the
  dispatcher passes provider-sourced rates directly

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* chore: update default NEAR AI model to GLM-latest

Replace fireworks llama4-maverick-instruct-basic with zai-org/GLM-latest
as the default model in config and setup wizard.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: align wizard default model name with config

Change "zai/GLM-latest" to "zai-org/GLM-latest" in wizard.rs to match
the default in config/llm.rs.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-22 02:54:31 +00:00
48b5323ec9 feat: group chat privacy, channel-aware prompts, and safety hardening (#285)
Prevent personal memory (MEMORY.md) from leaking into group chat contexts
by adding system_prompt_for_context(is_group_chat) to the workspace. Add
channel-specific formatting hints (Discord, Telegram, Slack, WhatsApp),
runtime metadata injection, group chat behavioral guidance with NO_REPLY
silent token, safety rules in the system prompt, tool call style guidance,
wrap_external_content() for untrusted data, and improved workspace seed
files with richer identity/soul/agent templates and heartbeat checklist.

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-21 06:28:33 +00:00
2cdd1acb1e refactor: consolidate tool approval into single param-aware method (#274)
* refactor: consolidate tool approval into single param-aware method

Replace the two confusing approval methods (requires_approval() and
requires_approval_for()) with a single requires_approval(&self, params)
returning a 3-variant ApprovalRequirement enum (Never, UnlessAutoApproved,
Always). This enables param-aware approval decisions: HTTP calls without
auth headers now skip approval entirely, while authenticated requests
always require it. Shell tool merges its destructive-command detection
into the same method.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add credential injection to built-in HTTP tool

Wire the WASM credential injection system into the built-in HTTP tool
so credentials are auto-injected at the boundary (zero-exposure model).

- Add SharedCredentialRegistry: thread-safe, append-only registry of
  credential mappings populated by WASM tools at registration time
- Add credential_detect module with broad auth detection for headers
  (12 exact + 5 substring matches), header values (7 auth scheme
  prefixes), and URL query params (17 exact + 5 substring matches)
- HttpTool now accepts optional credential registry + secrets store,
  auto-injects matching credentials in execute(), and uses broader
  auth detection in requires_approval()
- ToolRegistry passes credential registry to HttpTool at startup and
  populates it when WASM tools register
- Remove old hardcoded AUTH_HEADER_NAMES / has_auth_headers in favor
  of the new params_contain_manual_credentials()

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR #274 review comments (query param injection, lock poisoning, visibility)

- Fix injected query params not being sent on outbound HTTP requests by
  also calling .query() on the RequestBuilder alongside parsed_url mutation
- Recover from poisoned RwLock in SharedCredentialRegistry instead of
  silently ignoring failures, with tracing::warn for visibility
- Narrow inject_credential and host_matches_pattern to pub(crate) to
  avoid committing to them as stable public API

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-21 01:28:23 +00:00
e8dcb52fda feat: configurable tool iterations, auto-approve, and policy fix (#251)
* feat: direct agentic loop for SWE-bench benchmarks

Replace the full Agent-based runner with a purpose-built agentic loop
that directly calls the LLM with tools. The old path routed through
SafetyLayer (which blocked SWE-bench prompts), dispatcher (capped at
10 iterations), approval flow (wasted iterations), and 20+ irrelevant
builtin tools (diluted the model's focus).

New architecture:
- AgenticLoop: LLM call -> tool execution -> repeat (up to 30 iters)
- Per-task tool scoping via BenchSuite::task_tools() with working dirs
- Suite-provided system prompts via BenchSuite::system_prompt()
- No safety layer, no approval flow, no sessions/threads overhead
- Configurable max_iterations in BenchConfig and TOML

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: apply --model CLI override to LLM provider

The --model flag was updating matrix entry labels but not the actual
LLM provider, so requests were still sent using the model from .env.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: configurable tool iterations and auto-approve for benchmarks

Add max_tool_iterations and auto_approve_tools settings to AgentConfig,
replacing the hardcoded MAX_TOOL_ITERATIONS constant. Fix shell_injection
policy rule to not block markdown backtick code snippets.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address benchmarks crate audit findings

High:
- Fix truncate_output UTF-8 panic on multi-byte char boundaries
- Fix parallel results durability (write JSONL per-task, not after all)

Medium:
- Fix --sample to use random shuffle instead of first-N
- Delegate all LlmProvider methods in InstrumentedLlm
- Fix LLM-as-judge to return fail instead of misleading 0.5
- Remove unnecessary shallow clone (always gets unshallowed)
- Replace .unwrap() with .expect() in LazyLock regex init

Low:
- Remove dead code: unused error variants, trait methods, struct fields
- Remove BenchSuite::name() (redundant with id())
- Remove TaskSubmission::conversation, ConversationTurn, TurnRole
- Remove unused methods from BenchChannel, results, config
- Clean up ChannelCapture conversation tracking

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: add SWE-bench dataset and Docker scoring infrastructure

Add the SWE-bench Lite dataset (300 tasks) and Docker files for
isolated test execution and scoring of SWE-bench patches.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* chore: remove benchmarks (extracted to separate repo)

Benchmarks crate has been extracted to its own repository.
Remove the workspace member and all benchmarks/ files.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: add missing AgentConfig fields in test initializer

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-21 00:21:13 +00:00
448383cfb0 refactor: remove Responses API, consolidate to Chat Completions (#272)
* fix: strip reasoning from LLM responses and persist assistant messages reliably

- Filter out `type: "reasoning"` output items from NEAR AI Responses API
  parsing so chain-of-thought never reaches the UI (nearai.rs)
- Rewrite clean_response with regex-based tag stripping that is
  code-aware (preserves tags inside fenced blocks and inline backticks),
  supports 9+ tag names (think, thought, reasoning, reflection, etc.),
  handles <final> extraction, pipe-delimited tags, and case/whitespace
  tolerance (reasoning.rs)
- Add Reasoning::complete() helper so all non-agentic LLM call sites
  (summarize, suggest, heartbeat, compaction) get automatic response
  cleaning; thread SafetyLayer through to those callers
- Change persist_turn from fire-and-forget tokio::spawn to awaited async
  so both user and assistant messages are written before returning,
  preventing data loss on shutdown/restart
- Pass input_count through seed_response_chain so response chaining
  delta calculation is accurate after thread hydration on restart
- Make NearAiResponse.usage optional and preserve response_id in alt
  response path for chaining continuity
- Persist session token to DB during onboarding wizard so runtime
  loads it without legacy-key fallback; suppress spurious warning on
  fresh installs
- Fix dev tool double-registration when builder already registers them
- Load dotenv/ironclaw env for doctor and status subcommands
- Reduce startup log noise (demote info→debug for skills, remove
  redundant info lines)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Nudge to not loop over tools continuesly

* refactor: remove Responses API, consolidate NEAR AI to Chat Completions only

The Responses API provider (nearai.rs, 1278 lines) added significant complexity
(response chaining state machine, delta message calculation, previous_response_id
persistence) for marginal benefit. This consolidates to the Chat Completions API
only, upgrading NearAiChatProvider with dual auth (session token + API key) and
401 retry for session token renewal.

- Delete src/llm/nearai.rs (Responses API provider)
- Upgrade nearai_chat.rs with SessionManager, dual auth, flexible list_models
- Remove response_id from CompletionResponse and ToolCompletionResponse
- Remove seed_response_chain/get_response_chain_id from LlmProvider trait
- Remove response chain persistence from agent (thread_ops, session)
- Remove NearAiApiMode enum and NEARAI_API_MODE config
- Clean up all wrapper providers (retry, circuit_breaker, failover, cache)
- Update documentation (CLAUDE.md, .env.example)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* feat: runtime log level control via gateway UI and URL parameter

Add server-side log level switching using tracing_subscriber::reload::Layer
so the EnvFilter can be swapped at runtime without restarting. Expose via
GET/PUT /api/logs/level endpoints, a "Server: LEVEL" dropdown in the logs
toolbar, and a ?log_level=debug URL parameter for one-click activation.

Also applies cargo fmt to pre-existing files (llm/, tests/).

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-20 20:43:32 +00:00
8a4f3b6f88 fix: remove auto-proceed fake user message injection from agent loop (#255)
The agentic loop injected fake user messages ("Please proceed and use
the available tools to complete this task.") when the LLM responded
with text instead of tool calls. This caused hallucinated conversations
during casual chat, 3x wasted LLM calls, and trust issues.

Remove the `resume_after_tool` parameter and `tools_executed` tracking
entirely. Text responses now return immediately, trusting the LLM to
decide when tools are needed (consistent with ZeroClaw and OpenClaw).

Closes #145

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-20 08:09:52 +00:00
bfe393eb38 fix: parallelize tool call execution via JoinSet (#219) (#252)
* fix: parallelize tool call execution via JoinSet (#219)

When the LLM returns multiple tool_calls in a single response, they were
executed sequentially. This change makes both the worker and dispatcher
paths concurrent using tokio::task::JoinSet, so N independent tool calls
complete in ~max(latency) instead of sum(latency).

Worker path: migrate execute_tools_parallel from join_all to JoinSet and
route the respond_with_tools branch through the same parallel path.

Dispatcher path: restructure the while-idx loop into three phases —
preflight (sequential approval/hook checks), parallel execution via
JoinSet, and sequential post-flight processing (session recording,
auth detection, sanitization).

Also fixes a pre-existing infinite loop bug where hook rejection used
`continue` inside a `while idx` loop, skipping `idx += 1` and retrying
the same rejected tool forever.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review — ordered results, deferred auth, dedup standalone fn

- Fix auth early return skipping unrecorded tool results: defer auth
  response until after all results in the batch are recorded in session
  history and context_messages (both dispatcher and thread_ops paths)
- Fix tool results appearing out of order: collect Phase 1 hook
  rejections indexed by original position, merge with Phase 2 execution
  results, and emit all in Phase 3 in original tool_calls order
- Deduplicate execute_chat_tool: Agent method now delegates to the
  standalone function instead of duplicating 90 lines of logic
- Fix benchmark compilation: add missing session_manager arg to Agent::new

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: rustfmt alignment for CI compatibility

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address second round of PR review comments

- Distinguish JoinError panic vs cancellation in log messages and error
  reasons across all 3 files (dispatcher, thread_ops, worker)
- Simplify deferred_auth from Option<(String, String)> to Option<String>
  since only the instructions string is used
- Add single-tool short-circuit in worker execute_tools_parallel to
  avoid JoinSet overhead for the common single-tool case

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-20 06:07:30 +00:00
3f135bdde9 fix: persist turns after approval and add agent-level tests (#250)
* fix: persist turns after approval and add agent-level tests

Port relevant changes from PR #112 that were not carried over to #237:

- Add persist_turn calls in process_approval for the response, error,
  and auth-required paths. Previously, turns completed after tool
  approval were never persisted to DB — if the process crashed after
  approval the entire turn (user message + assistant response) was lost.

- Add agent-level unit tests: StaticLlmProvider mock, make_test_agent
  helper, tests for auto-approval logic, destructive shell command
  detection, and PendingApproval backward-compatible deserialization
  (without deferred_tool_calls field).

- Remove unused _thread_state binding in process_approval.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address 14 audit findings in src/agent/

Audit of the agent module found 2 High, 7 Medium, 3 Low, and 2 Nit
severity issues. This commit fixes all of them:

High:
- Remove 4 `.expect()` calls in session.rs (entry API, match, direct
  indexing, if-let) to eliminate panic paths in production
- Add typed RoutineError enum replacing Result<_, String> across
  routine.rs, routine_engine.rs, and callers in history/store.rs and
  db/libsql/mod.rs

Medium:
- Sanitize routine names in path construction to prevent directory
  traversal (routine_engine.rs)
- Log warnings for 5 silently-swallowed errors in scheduler.rs,
  compaction.rs, and worker.rs
- Extract shared handle_auth_intercept helper to deduplicate auth
  interception in thread_ops.rs
- Add session count warning threshold in session_manager.rs
- Make FullJob stub degradation visible via warn-level log and
  prepended warning in output

Low:
- Restrict dead code visibility with #[cfg(test)] on 19 unused items
  in submission.rs, task.rs, and undo.rs
- Narrow pub to pub(crate) on self_repair.rs builder methods
- Remove TaskStatus from mod.rs re-exports (test-only type)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review comments

- Reorder persist_turn before persist_response_chain so the
  conversation row exists before the metadata UPDATE runs
- Add persist_response_chain call to handle_auth_intercept so
  auth-required paths preserve the response chain
- Harden sanitize_routine_name to use allowlist (alphanumeric,
  dash, underscore) instead of denylist replacements
- Fix stale active_thread ID in get_or_create_thread: fall back
  to create_thread() when the stored ID is missing from the map
- Persist turn on approval rejection so user messages survive
  crashes after a tool is rejected

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-20 02:28:15 +00:00
097a26ace6 fix: harden openai-compatible provider, approval replay, and embeddings defaults (#237)
* fix: harden openai-compatible tool flow and local defaults

* fix: close approval replay gaps and harden openai-compatible flow

* fix: address review feedback and code improvements (takeover #112)

- Make ChatCompletionResponse.id Optional<String> to handle providers
  that omit or null the field
- Propagate HTTP client builder errors instead of silently dropping
  timeout configuration (openai_compatible_chat, nearai_chat)
- Add EMBEDDING_DIMENSION env var with smart per-model defaults instead
  of hardcoding 768/1536 everywhere
- Remove duplicated dimension inference logic from main.rs

Co-Authored-By: panosAthDBX <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: harden src/llm/ module from crate audit findings

- Replace 9x .expect() on RwLock with graceful poison recovery
  (nearai.rs: 7, nearai_chat.rs: 2) — eliminates production panics
- Propagate HTTP client builder errors in nearai.rs instead of
  silently dropping timeout config (NearAiProvider::new now returns Result)
- Make nearai_chat ChatCompletionResponse.id Optional<String>
  (mirrors openai_compatible_chat.rs fix for providers that omit id)
- Make nearai_chat usage fields optional with defensive parse_usage()
  helper (was required u32 fields that crash on null/missing)
- Truncate error responses to 512 chars in nearai_chat.rs error
  messages to prevent log bloat and potential data leakage
- Delegate 4 missing LlmProvider methods in FailoverProvider
  (model_metadata, seed_response_chain, get_response_chain_id,
  calculate_cost) to last-used provider instead of trait defaults

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor(llm): add RetryProvider, remove openai_compatible_chat, harden decorators

- Add composable RetryProvider decorator wrapping any LlmProvider with
  exponential backoff + jitter, respecting RateLimited retry_after hints
- Remove openai_compatible_chat.rs — replaced by rig adapter + RetryProvider
- Remove internal retry loop from nearai.rs (was causing double-retry
  with external RetryProvider, up to 16 attempts instead of 4)
- Remove internal retry loop from nearai_chat.rs (same issue)
- Wire RetryProvider into main.rs composition chain: each provider gets
  its own retry wrapper before failover
- Move normalize_tool_name to rig_adapter.rs for all rig-based providers
- Reconcile is_retryable() vs is_transient() error classification:
  ModelNotAvailable no longer retryable, Json no longer transient
- Fix unchecked Duration subtraction panic in circuit_breaker.rs
- Make failover.rs use shared is_retryable() from retry.rs
- Remove stale #[allow(dead_code)] on NearAiResponse::id (field is used)

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review feedback — error handling, dimension validation, libSQL warning

- Replace response.text().await.unwrap_or_default() with proper error
  propagation in nearai.rs and nearai_chat.rs (4 call sites). Failures
  now return LlmError::RequestFailed with context instead of silently
  proceeding with an empty string.
- Add embedding dimension validation in OllamaEmbeddings::embed_batch():
  returns EmbeddingError if Ollama returns embeddings with a dimension
  that doesn't match the configured value.
- Add runtime warning when libSQL backend is used with non-1536 embedding
  dimension, since the libSQL schema uses F32_BLOB(1536) and cannot store
  different-dimension vectors.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* Apply suggestions from code review

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: panosAthDbx <[email protected]>
Co-authored-by: panosAthDBX <[email protected]>
Co-authored-by: panosAthDBX <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Copilot <[email protected]>
2026-02-19 23:05:04 +00:00
bac2d75713 feat: Secure prompt-based skills system (Phases 1-4) (#51)
* 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]>
2026-02-18 00:28:38 +00:00
a158eee1b0 feat: 10 infrastructure improvements from zeroclaw (#126)
* 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]>
2026-02-17 19:35:56 +00:00