Commit Graph
150 Commits
Author SHA1 Message Date
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
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
Xing JiandGitHub 3f6d2ab6c2 fix(skill): treat empty url param as absent when installing skills (#1128)
LLMs sometimes pass "" for optional parameters instead of omitting them.
Previously, passing url: "" to skill_install would match the explicit-URL
branch and attempt to fetch from an empty string, producing an invalid URL
error instead of falling back to the catalog lookup.

Fix by adding .filter(|s| !s.is_empty()) so an empty url is treated the
same as a missing field.

A unit test verifies the parameter filtering behaviour directly; the full
execute path (catalog lookup + install) requires a real catalog and database
and cannot be covered at the unit level.
2026-03-15 05:50:39 +00:00
NigeGitHubgemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
f059d50331 fix: preserve AuthError type in oauth_http_client cache (#1152)
* fix(mcp): cache oauth client init error as AuthError

* Update src/tools/mcp/auth.rs

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix(mcp): use AuthError::Http in oauth client cache and add regression test

* test(mcp): annotate test assert for no-panics CI matcher

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-15 05:49:42 +00:00
62d16e69ac fix(mcp): handle 400 auth errors, clear auth mode after OAuth, trim tokens (#1158)
* fix(mcp): handle 400 auth errors, clear auth mode after OAuth, trim tokens

Three bugs prevented MCP server authentication (e.g. GitHub MCP) from
working correctly:

1. **400 treated as auth-required**: GitHub's MCP endpoint returns 400
   "Authorization header is badly formatted" instead of 401 when auth
   is missing. Broadened auth detection in activate_mcp, send_request,
   and discover_via_401 to also match 400+authorization errors.

2. **Auth mode not cleared after OAuth callback**: The OAuth callback
   handler and setup submit handler did not call clear_auth_mode(),
   leaving pending_auth on the thread. The next user message was
   intercepted as a token instead of triggering an LLM turn.

3. **Token trimming**: Tokens with leading/trailing whitespace or
   newlines produced malformed Authorization headers. Now trimmed
   before storage (configure) and before use (build_request_headers).

Adds E2E tests with a mock MCP server (JSON-RPC + OAuth discovery +
DCR + token exchange) covering install -> activate -> OAuth callback ->
LLM turn lifecycle, plus a GitHub-style 400 error variant.

[skip-regression-check]

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

* fix(mcp): add TTL to PendingAuth and clear auth mode on all failure paths

Auth mode (pending_auth on a Thread) had no timeout and several code
paths that failed to clear it, causing user messages to be swallowed
indefinitely. This adds defense-in-depth:

- Add created_at + 5-minute TTL to PendingAuth; auto-clear on next
  message if expired (safety net for edge cases like user closing
  browser mid-OAuth)
- Clear auth mode on OAuth callback failure paths (unknown/consumed
  state, expired flow)
- Move clear_auth_mode before configure() match in setup_submit so
  it runs on failure too (addresses Copilot review feedback)

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

* fix(ci): exclude test hunks from unwrap/assert pre-commit check

The pre-commit safety script only excluded files in tests/ but not
#[cfg(test)] mod tests blocks inside src/ files. Use the git diff @@
hunk header context (which includes the enclosing function name) to
detect and skip test hunks.

Also removes unnecessary // safety: comments from test assertions.

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

* fix: restore formatting in test assertions

The replace_all edit that removed // safety: comments collapsed
newlines. Restore proper line breaks.

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

* fix: address Copilot review - tighten pre-commit filter, document TTL sync

- pre-commit-safety.sh: only exclude `mod tests` hunks (not `fn test_*`)
  to avoid hiding unwrap/assert in production functions like test_server()
- session.rs: extract AUTH_MODE_TTL_SECS constant and add doc comment
  linking to OAUTH_FLOW_EXPIRY to prevent silent drift

[skip-regression-check]

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

* fix(mcp): return error on expired auth input, clear auth on all OAuth paths

- When auth mode TTL expires and the user sends a message (possibly a
  pasted token), return an explicit "expired, please retry" response
  instead of forwarding the content to the LLM/history
- Add clear_auth_mode() to all early-return paths in oauth_callback_handler
  (provider error, missing state/code, no extension manager)

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-15 05:42:49 +00:00
716629809c fix: eliminate panic paths in production code (#1184)
* fix: eliminate panic paths in production code and document infallible operations

PolicyRule::new() now returns Result instead of panicking on invalid
caller-supplied regex. CreateJobTool returns ToolError when job_manager
is unconfigured instead of panicking. Remaining infallible unwrap/expect
calls (hardcoded regexes, compile-time constants, guarded accesses)
are annotated with SAFETY comments. Where possible, unwraps are replaced
with safer patterns: split_last(), if-let, match-destructure, and
reusing peek() values.

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

* fix: use inline lowercase safety comments to match CI pattern

The no-panics CI check greps for '// safety:' (lowercase, inline)
to suppress false positives. Switch from block SAFETY comments to
inline safety comments on the .unwrap() lines.

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

* test: add regression tests for panic-path fixes

- PolicyRule::new returns Err on invalid regex (not panic)
- CreateJobTool::execute_sandbox returns ToolError when job_manager is None

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

* fix: add inline // safety: comments on all infallible unwrap/expect lines

The CI no-panics check requires '// safety:' on the same line as
unwrap()/expect() to suppress false positives. Move safety annotations
from block comments to inline comments on every infallible production
unwrap/expect across all touched files.

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

* chore: trigger CI with skip-regression-check label

[skip-regression-check]

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

* refactor: remove redundant block-level SAFETY comments

Each unwrap/expect now carries its own inline // safety: annotation,
making the standalone block comments above them redundant.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-15 03:17:03 +00:00
Henry ParkandGitHub c79754df28 Fix schema-guided tool parameter coercion (#1143)
* Fix schema-guided tool parameter coercion

* Fix CI checks for coercion regression tests

* Finish panic-scan annotations

* Avoid redundant worker param preparation

* Keep panic-scan annotations rustfmt-stable

* Handle nullable WASM schema review feedback

* Address param coercion review notes
2026-03-14 16:27:18 -07:00
NigeandGitHub 8753c48233 perf(mcp): avoid reallocating SSE buffer on each chunk (#1153) 2026-03-14 15:47:48 -07:00
Nick PismenkovandGitHub 1770663279 fix: Google Sheets returns 403 PERMISSION_DENIED after completing OAuth (#1164)
* fix: Google Sheets returns 403 PERMISSION_DENIED after completing OAuth

* fix: linter

* fix: linter

* fix: ci

* fix

* fix

* fix

* fix
2026-03-14 12:01:47 -07:00
Henry ParkandGitHub 7d745d5479 tools: improve routine schema guidance (#1089) 2026-03-13 11:24:45 -07:00
f53c1bb10b fix(mcp): address 14 audit findings across MCP module (#1094)
* fix(mcp): address 14 audit findings across MCP module

- Replace panicking assert! in new_with_config with Result return (Critical)
- Fix initialize() race condition using tokio::sync::OnceCell (High)
- Fix localhost check bypass via proper URL parsing (High)
- Extract shared stream_transport_send() to deduplicate stdio/unix send logic
- Use atomic write (tmp+rename) for config file persistence
- Filter SSE responses by request_id to prevent wrong-response dispatch
- Share a single reqwest::Client for OAuth via fallible OnceLock
- Log notification send errors instead of silently discarding
- Fix unwrap_or(0) that could steal id=0 responses
- Store InitializeResult in OnceCell so callers can access server capabilities
- Add redirect logging in OAuth discovery
- Reuse is_localhost_url() in auth.rs
- Add McpToolWrapper unit tests and regression tests
- URL-encode PKCE challenge for consistency

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

* chore: retrigger CI with skip-regression-check label

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-13 17:37:51 +00:00
Xing JiandGitHub 275bcfb658 fix(time): treat empty timezone string as absent (#1127)
LLMs sometimes pass "" for optional parameters instead of omitting
them. Previously, passing timezone: "" or from_timezone: "" to the
time tool would trigger a parse error ("Unknown timezone ''") rather
than falling back to the context timezone or UTC.

Fix by adding .filter(|s| !s.is_empty()) after .as_str() in
resolve_timezone_for_output and optional_timezone, so empty strings
are treated the same as a missing field.

The same pattern exists in routine.rs (cron trigger timezone and
schedule fields), where "" produces "invalid IANA timezone: ''" or a
cron parse error. That will be addressed separately once routine.rs
has a test harness in place.

Regression tests added for the now and convert operations with
empty timezone strings.

Closes #1127
2026-03-13 16:40:03 +00:00
e805ec61aa fix: 5 critical/high-priority bugs (auth bypass, relay failures, unbounded recursion, context growth) (#1083)
* fix: address 5 critical and high-priority bugs from issue tracker

- #1033: reject webhook requests when secret is cleared at runtime via
  update_secret(None), preventing auth bypass through SIGHUP hot-swap
- #908: reset consecutive_failures counter on successful SSE stream
  reconnection in relay channel, so circuit breaker counts truly
  consecutive failures
- #975: add depth limit (16) to validate_tool_schema() to prevent
  stack overflow on deeply nested schemas
- #974: add depth limit (8) to resolve_nested() to prevent stack
  overflow on deeply nested capabilities wrappers
- #826: truncate oversized tool outputs (>8KB) in routine lightweight
  loop to prevent unbounded context growth across iterations

Each fix includes a regression test.

Closes #1033, #908, #975, #974, #826

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

* fix: 5 more high-priority bugs (routine cache, job signals, input limits)

- #1077: recompute next_fire_at when re-enabling cron routines via web
  toggle, mirroring CLI behavior so cron ticker picks them up
- #1076: refresh event trigger cache after web toggle/delete operations
  so event/system_event routines reflect changes immediately
- #892: remove Stuck from check_signals() stop-states in JobDelegate
  since Stuck is recoverable (Stuck -> InProgress via self-repair)
- #976: truncate oversized description strings in CapabilitiesFile to
  4KB to prevent memory abuse from malicious capabilities files
- #977: drop oversized parameters schema JSON (>64KB) in
  CapabilitiesFile to prevent unbounded memory growth

Each fix includes regression tests where applicable.

Closes #1077, #1076, #892, #976, #977

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

* fix: prevent ReDoS in event trigger regex patterns

- #825: use RegexBuilder with 64KB size limit when compiling
  user-supplied event trigger patterns, both at creation time
  (routine tool) and at cache refresh (routine engine)

Note: Rust's regex crate already guarantees O(n) matching, so the
size limit prevents excessive memory use during compilation rather
than catastrophic backtracking at match time.

Closes #825

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

* Harden HTTP SSRF IP filtering

* Apply rustfmt after staging merge

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-13 16:04:55 +00:00
15c5d3e2e2 fix(wasm): address #1086 review followups -- description hint and coercion safety (#1092)
Two fixes from the review of #1086 (tool_info schema discovery):

1. Replace fragile description string mutation (append_schema_hint_if_permissive /
   strip_schema_hint) with composition at display time. The raw description stays
   clean; the tool_info hint is composed in the Tool::schema() override only when
   the advertised schema is permissive. This also includes the tool name and
   `include_schema: true` in the hint for better LLM guidance.

2. Make effective_for_coercion use the load-time extracted schema from
   PreparedModule instead of re-calling the WASM schema() export on the
   already-running instance mid-execution. This avoids potential state
   contamination from calling schema() after linear memory is initialized
   for execution.

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 17:48:05 -07:00
Henry ParkandGitHub 8a60fa2d37 fix: add tool_info schema discovery for WASM tools (#1086)
* fix: add tool_info schema discovery for WASM tools

* refactor: simplify WASM schema and hint state

* refactor: store tool_info registry reference as Weak
2026-03-12 16:30:38 -07:00
NigeandGitHub c54f739354 fix: resolve bug_bash UX/logging issues (#1054 #1055 #1058) (#1072)
* fix(web,db): improve auth UX + reduce naive timestamp log noise

* fix(clippy): keep memory test modules at end of file
2026-03-12 15:27:50 -07:00
6f00490900 fix: relax approval requirements for low-risk tools (#922)
* fix: relax approval requirements for low-risk tools

Remove unnecessary UnlessAutoApproved friction from list_dir, image_gen,
image_analyze, image_edit, tool_install, tool_auth, tool_upgrade, and
build_tool — these operate on trusted inputs or are low-risk operations
so they now use the trait default (Never).

For the http tool, GET requests without credentials now return Never
instead of UnlessAutoApproved, while credential-bearing requests and
non-GET methods retain their existing approval levels.

[skip-regression-check]

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

* style: apply cargo fmt

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

* fix: address review feedback on approval changes

Rename test_requires_approval_returns_unless_auto_approved to
test_requires_approval_returns_never to match the asserted behavior.

In http requires_approval(), treat missing method as unknown (falls
through to UnlessAutoApproved) instead of defaulting to GET, since
the schema requires method. Updated comment to reflect this.

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

* fix: make http method optional, default to GET

Make method optional in schema (only url is required) and default to
GET in both execute() and requires_approval(). This aligns approval
logic with execution and reduces friction for simple GET requests.

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

* fix: restore UnlessAutoApproved for build_tool, tool_install, tool_upgrade

Address review feedback: these tools modify the system's trust boundary
(shell execution, WASM installation, version mutation) and should retain
approval gating. tool_auth kept as Never per owner decision.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 22:10:56 +00:00
NigeandGitHub 7a9cbb3b50 fix(routines): run cron checks immediately on ticker startup (#1066)
* fix(routines): run cron check immediately at ticker startup

* test/ci: add routine_engine test and fix style lint drift
2026-03-12 15:04:21 -07:00
panosAthDBXandGitHub d5828b271d feat(tools): add reusable sensitive JSON redaction helper (#457)
* feat(tools): add reusable sensitive JSON redaction helper

* fix(tools): harden sensitive-key tokenization and context matching
2026-03-12 14:54:44 -07:00
e1691a8d42 feat: configurable hybrid search fusion strategy (#234)
* feat: configurable hybrid search fusion strategy (#169)

Add WeightedScore fusion as an alternative to the default RRF algorithm.
Users can now tune search behavior via env vars (SEARCH_FUSION_STRATEGY,
SEARCH_FTS_WEIGHT, SEARCH_VECTOR_WEIGHT, SEARCH_RRF_K) or by passing
SearchConfig with the new fields. Default behavior (RRF, k=60) is
unchanged.

- Add FusionStrategy enum (Rrf/WeightedScore) to workspace::search
- Add weighted_score_fusion() and fuse_results() dispatcher
- Add config/search.rs with WorkspaceSearchConfig from env vars
- Wire search defaults through Workspace struct
- Update both postgres and libsql backends to use fuse_results()
- Add 7 new tests (4 fusion + 3 config)

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

* fix: swap default search weights to match issue #169 spec (0.7 vector / 0.3 FTS)

The issue spec says "0.7/0.3 (vector/keyword) for weighted mode" but
our defaults had fts_weight=0.7, vector_weight=0.3 (inverted). Also
fixes the misleading docstring on weighted_score_fusion that claimed
1/rank normalizes to [0,1].

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

* fix: validate weight inputs and update stale doc comments

- Reject NaN, infinite, and negative values for SEARCH_FTS_WEIGHT and
  SEARCH_VECTOR_WEIGHT with a clear ConfigError
- Fix module-level docs that incorrectly claimed WeightedScore
  "normalizes per-method scores to [0,1]"
- Update SearchResult.score doc from "Combined RRF score" to
  strategy-agnostic "Combined fusion score"

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

* fix: validate weight setters against NaN/inf/negative values

with_fts_weight() and with_vector_weight() now silently ignore
non-finite (NaN, ±inf) and negative values, matching the env var
validation already in place for SEARCH_FTS_WEIGHT / SEARCH_VECTOR_WEIGHT.

Values > 1.0 remain valid since weights are normalized internally.

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

* fix: use crate-wide ENV_MUTEX in search config tests

Replace the module-local `ENV_MUTEX` in `search.rs` with a shared
`crate::config::helpers::ENV_MUTEX` to prevent cross-module env races
when `cargo test` runs tests in parallel.

Addresses copilot review comment. Tracked in #245.

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

* fix: per-strategy weight defaults to match issue #169 spec

RRF mode now defaults to 0.5/0.5 (fts/vector) and WeightedScore
defaults to 0.3/0.7, matching the acceptance criteria in #169.
Previously both modes used 0.3/0.7 uniformly.

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

* fix: reject both weights=0 in weighted fusion mode

When both SEARCH_FTS_WEIGHT and SEARCH_VECTOR_WEIGHT are 0.0 under
WeightedScore strategy, all scores would be 0.0, producing arbitrary
ordering. RRF mode is unaffected since it ignores weights entirely.

Addresses Copilot review comment. The other comment (rrf_k=0 division
by zero) is a false positive — ranks are 1-based, so k=0 just gives
inverse-rank scoring with no infinity.

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

* fix: clarify weight doc comments and error key

- SearchConfig field docs: clarify that Default always uses 0.5,
  per-strategy defaults only apply via WorkspaceSearchConfig::resolve()
- WorkspaceSearchConfig field docs: same clarification
- Error key for both-weights-zero now references both env vars

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

* fix: remove broken intra-doc links to pub(crate) resolve()

WorkspaceSearchConfig::resolve is pub(crate), so linking to it from
public field docs triggers rustdoc private_intra_doc_links warnings.
Switch to plain-text references.

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

* fix: add document_path to weighted_score_fusion results

The weighted_score_fusion function was missing the document_path field
added in a recent main branch commit, causing a compile error after rebase.

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

* chore: trigger CI re-check after rebase

* fix: resolve pre-existing staging fmt and clippy issues

- Fix import ordering in cli/mod.rs (cargo fmt)
- Fix line wrapping in tools/mcp/auth.rs (cargo fmt)
- Move path_routing_tests before MemoryTreeTool to fix
  clippy::items_after_test_module

[skip-regression-check]

* fix: remove duplicate path_routing_tests module after rebase

[skip-regression-check]

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 14:49:00 -07:00
8ac24e775b style: fix formatting in cli/mod.rs and mcp/auth.rs (#1071)
* style: fix formatting in cli/mod.rs and mcp/auth.rs

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

* fix(cli): add missing use_tools and max_tool_rounds fields to routines create

The routines CLI create command was missing the new Lightweight fields
added after the cron->routines rename merged.

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

* fix(clippy): move path_routing_tests after production code in memory.rs

Fixes items_after_test_module lint by moving the test module to the
end of the file, after all production structs and impls.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 13:21:49 -07:00
shibenandGitHub 8df51c04ae feat: enhance HTTP tool parameter parsing (#911)
* feat: enhance HTTP tool parameter parsing

- Add support for stringified JSON arrays in headers parameter.
- Introduce timeout_secs parameter parsing to accept both numbers and string representations.
- Implement save_to parameter parsing to handle empty strings as None.
- Update HTTP request handling to incorporate timeout and save_to parameters.
- Add unit tests for new parsing functions to ensure correct behavior.

* feat(http): enhance HTTP tool with timeout and header parsing improvements

- Introduced default and maximum request timeout constants to manage resource usage.
- Refactored header parsing logic to separate functions for better readability and maintainability.
- Updated timeout handling to ensure it respects the maximum allowed value.
- Added unit tests to validate new header parsing functionality.

* refactor(http): replace hardcoded timeout with effective_timeout variable in HTTP tool error handling
2026-03-12 11:38:30 -07:00
ReidandGitHub 6bbf87ba3a feat(routines): enable tool access in lightweight routine execution (#257) (#730)
* Rebase onto staging

* fix(routines): prevent autonomy-escalation in lightweight routines

  - Add ROUTINE_TOOL_DENYLIST to block routine_create/update/delete/fire
    and restart from being callable by lightweight routines
  - Deduplicate sentinel logic by reusing handle_text_response() in the
    no-tools path
  - Filter tool definitions sent to LLM to only include callable tools,
    avoiding wasted tokens on tools that would be rejected
2026-03-12 11:38:27 -07:00
NigeandGitHub d420abfa6a fix(memory): reject absolute filesystem paths with corrective routing (#934)
* ci(staging): use default branch instead of hardcoded main

* fix(memory): route absolute paths to filesystem tools
2026-03-12 11:28:57 -07:00
nearfamiliarcowandGitHub 4faf81ab61 fix(mcp): include OAuth state parameter in authorization URLs (#1049)
Some MCP servers (e.g. Attio) require the `state` parameter in OAuth
authorization requests and reject requests without it:

  {"error":"invalid_request","error_description":"Invalid value provided for: state"}

While OAuth 2.1 makes `state` optional when PKCE is used, the MCP
specification does not forbid servers from requiring it. This caused a
hard failure when authenticating with any MCP server that enforces the
state parameter.

Generate a 128-bit cryptographically random state (via OsRng, base64url
encoded without padding) and inject it into extra_params before building
the authorization URL. This covers both pre-configured OAuth and Dynamic
Client Registration (DCR) code paths.

The callback listener intentionally does not validate the echoed state
because: (1) PKCE already binds the authorization code to the token
exchange, preventing code injection attacks, and (2) not all MCP servers
echo state back — strict validation would break those servers. Other
OAuth flows in the codebase (tool.rs, extensions/manager.rs) that
generate and validate state are unaffected.
2026-03-12 11:16:26 -07:00
8a26cfae73 fix(mcp): open MCP OAuth in same browser as gateway (#951)
* fix(mcp): use gateway callback for MCP OAuth so auth opens in same browser

When MCP OAuth is triggered from the web gateway, the auth URL was being
opened via `open::that()` which launches the OS default browser instead
of the browser already running the gateway UI. This changes the MCP OAuth
flow to use the same gateway callback pattern as WASM extensions: in
gateway mode, the auth URL is returned to the frontend via SSE and opened
with `window.open()`, keeping the user in the same browser.

Also adds RFC 8707 `resource` parameter support to the gateway token
exchange path, scoping issued tokens to the correct MCP server.

Closes #299

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

* style: cargo fmt

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

* fix(mcp): persist DCR client_id in gateway OAuth callback for token refresh

The gateway callback handler stored access and refresh tokens but not
the DCR client_id. When the token expired, refresh failed with "No
client ID found" because get_client_id() could not find it in secrets.

Adds client_id_secret_name to PendingOAuthFlow so the gateway callback
handler persists the client_id alongside the tokens, matching the
behavior of the CLI flow in authorize_mcp_server().

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

* fix(mcp): return AuthRequired on 401 so activate triggers OAuth flow

activate_mcp() returned ActivationFailed for all errors including 401
auth responses, so the activate handler never triggered the OAuth flow.
Now 401/auth errors return AuthRequired, which the handler detects and
redirects to the OAuth flow — matching the WASM extension pattern.

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

* fix(mcp): fix gateway OAuth flow, approval cards, and auto-activation

- Add explicit gateway_mode flag on ExtensionManager (set at startup by
  web gateway) so MCP OAuth returns auth URLs to the frontend instead of
  calling open::that() on the server machine.
- Auto-activate extensions after successful OAuth callback so the UI
  transitions from "Activate" to "Active" without a second click.
- Send ApprovalNeeded status (not generic "Awaiting approval") from
  thread_ops.rs for all three NeedApproval paths so the web UI shows
  approval cards for deferred tool calls.
- Remove duplicate ApprovalNeeded send from agent_loop.rs (thread_ops.rs
  is now the canonical sender).
- Skip approval for tool_auth in gateway mode since it only returns a URL.
- Revert fragile active-server detection heuristic from system prompt.

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

* fix: address PR review findings

- Use Release/Acquire ordering for gateway_mode AtomicBool instead of
  Relaxed to ensure visibility across threads.
- Report activation failure as error in OAuth callback SSE event instead
  of silently falling back to the success message.
- Fix EnvGuard::drop to remove env var when original was unset.
- Replace hardcoded /tmp/ path with std::env::temp_dir() in test helper.

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

* test(mcp): add E2E trace test for MCP extension lifecycle with mock server

Add a full MCP extension lifecycle E2E test that exercises:
- Turn 1: tool_search → tool_install → text (extension discovery and install)
- Token injection + activate (simulating OAuth completion)
- Turn 2: MCP tool calls (notion-search → notion-fetch → text)

Includes a mock MCP server (tests/support/mock_mcp_server.rs) with OAuth
discovery, DCR, token exchange, and JSON-RPC endpoints. The mock server
validates Bearer auth and serves pre-configured tool responses.

Also adds inject_registry_entry() to ExtensionManager for test use and
exposes extension_manager from TestRig.

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

* fix: address PR review findings (round 2)

- Only fall back to manual token entry on AuthNotSupported, propagate
  real errors from auth_mcp_build_url() instead of masking them
- Use mcp:-prefixed provider string in PendingOAuthFlow for consistency
  with CLI MCP auth token storage
- Only persist client_id_secret_name for DCR flows (not pre-configured OAuth)
- Fix gateway_callback_redirect_uri to use /oauth/callback path
- Bypass exchange proxy when flow has RFC 8707 resource parameter
- Remove client_id double-prefix in oauth callback handler
- Remove weak tests that didn't exercise production logic
- Add clarifying comments for exchange_oauth_code delegation

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

* fix: keep OAuth success independent of activation, fix wait_for_responses scoping

- OAuth success is now reported accurately even when auto-activation
  fails (tokens are already stored, so auth succeeded)
- E2E test waits for turn1_count + 1 responses to ensure turn-2
  behavior is actually observed

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 11:16:23 -07:00
Zaki ManianandGitHub f31cd13135 fix(mcp): attach session manager for non-OAuth HTTP clients (#793) (#986)
* fix(mcp): attach session manager for non-OAuth HTTP clients (#793)

* style(mcp): format factory regression test (#986)
2026-03-12 01:12:01 +00:00
ReidandGitHub a1b3911b27 fix(mcp): header safety validation and Authorization conflict bug from #704 (#752)
* fix(mcp): header safety validation and Authorization conflict bug from #704

* fix(mcp): enforce RFC 9110 header validation on all config load paths

  Replace hand-written CRLF checks with reqwest::header::HeaderName::from_bytes()
  and HeaderValue::from_str(), catching spaces, colons, null bytes, and all
  non-token characters that the previous validation missed.

  Add validation to load_mcp_servers_from() and load_mcp_servers_from_db() so
  corrupted configs from disk or DB are rejected at load time instead of silently
  flowing through to McpClient. Improve app.rs error handling to distinguish
  "no config" from "corrupted config" (including malformed JSON).

  Also fix build_request_headers() to check self.custom_headers directly instead
  of indirectly via server_config, and clarify the wire test comment about
  HeaderMap::insert replacement semantics.

* fix ci issue
2026-03-11 16:53:02 -07:00
ReidandGitHub c8cac0925d fix(mcp): stdio/unix transports skip initialize handshake (#890) (#935)
fixes #890

  - Always call initialize() before list_tools()/call_tool(), removing
    the session_manager.is_some() guard that caused stdio/unix clients
    to skip the MCP protocol handshake entirely
  - Add local AtomicBool flag for idempotent initialization when no
    session manager is present
  - Fire-and-forget JSON-RPC notifications (id=None) in stdio/unix
    transports instead of registering a pending response that would
    block for 30s waiting on a reply that never comes
  - Fix mcp test panic on stdio/unix servers by using
    create_client_from_config() instead of new_with_config() which
    asserts HTTP-only transport
2026-03-11 16:46:40 -07:00
94b448ffab fix(security): load WASM tool description and schema from capabilities.json (#520)
The extract_tool_description and extract_tool_schema stubs in runtime.rs
returned permissive fallbacks ("WASM sandboxed tool" and
additionalProperties:true) for every WASM tool, defeating parameter
validation and preventing the LLM from using tools correctly.

Add optional `description` and `parameters` fields to CapabilitiesFile so
tool authors can declare proper metadata in their sidecar JSON. The
WasmToolLoader now extracts these fields and passes them through to the
tool registry as overrides. Tools without a capabilities.json or without
these fields get a tracing::warn and fall back to the old stubs.

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-03-11 16:29:59 -07:00
bb06565770 fix(security): resolve DNS once and reuse for SSRF validation to prevent rebinding (#518)
* fix(security): resolve DNS once and reuse for SSRF validation to prevent rebinding

The previous SSRF protection resolved DNS in validate_url() to check IPs
against a blocklist, but then reqwest independently re-resolved DNS when
making the actual HTTP connection.  Between validation and connection, a
DNS rebinding attack could flip the record from a public IP (passes
validation) to a private IP like 169.254.169.254 (AWS metadata endpoint).

Fix: split URL validation into two phases:
- validate_url(): synchronous URL structure checks (scheme, localhost,
  IP literals) -- no DNS resolution
- validate_and_resolve_url(): async DNS resolution via
  tokio::net::lookup_host, validates all resolved IPs, returns
  SocketAddrs
- build_pinned_client(): constructs a per-request reqwest Client with
  resolve() pinning so reqwest connects to the pre-validated IPs without
  a second DNS lookup

Applied to both HttpTool and WebFetchTool.  WebFetchTool builds a fresh
pinned client per redirect hop, ensuring DNS rebinding cannot occur at
any point in a redirect chain.

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

* style: run cargo fmt

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

---------

Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-03-11 16:18:43 -07:00
19d9562b4f feat(extensions): unify auth and configure into single entrypoint (#677)
* feat(extensions): unify auth and configure into single entrypoint

Refactors the extension lifecycle to eliminate the divergence between
chat and gateway paths that caused Telegram setup via chat to fail
(missing webhook secret auto-generation, no token validation).

Key changes:
- Rename save_setup_secrets() → configure(): single entrypoint for
  providing secrets to any extension (WasmChannel, WasmTool, MCP).
  Validates, stores, auto-generates, and activates.
- Add configure_token(): convenience wrapper for single-token callers
  (chat auth card, WebSocket, agent auth mode).
- Refactor auth() to pure status check: remove token parameter,
  delete token-storing branches from auth_mcp/auth_wasm_tool,
  rename auth_wasm_channel → auth_wasm_channel_status.
- Add ConfigureResult/MissingSecret types for structured responses.
- Replace hardcoded Telegram token validation with generic
  validation_endpoint from capabilities.json.
- Update all callers (9 files) to use the new interface.

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

* fix: use ValidationFailed error variant instead of string matching

Replace brittle msg.contains("Invalid token") checks with a proper
ExtensionError::ValidationFailed variant. configure() now returns
this variant for token validation failures, and callers match on it
directly instead of parsing error message strings.

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

* fix: address review — SSRF protection, error typing, missing-secret selection, WS auth

1. SSRF: call validate_fetch_url() before validation_endpoint HTTP request
2. Transport errors map to ExtensionError::Other (not ValidationFailed)
3. configure_token() picks first *missing* secret, not first non-optional
4. WebSocket error path re-emits AuthRequired on ValidationFailed

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

* test: add regression tests for extension lifecycle refactoring

- test_configure_token_picks_first_missing_secret: verifies multi-secret
  channels can be configured one secret at a time (commit ce106f4)
- test_auth_is_read_only_for_wasm_channel: verifies auth() has no side
  effects and doesn't store secrets (commit 47f8eb6)
- test_validation_failed_is_distinct_error_variant: verifies the typed
  error variant can be pattern-matched (commit a318161)

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

* fix: address review comments — activation dispatch, dead code, caps consolidation

- Fix configure() fallthrough bug: dispatch activation by ExtensionKind
  instead of unconditionally calling activate_wasm_channel() for all
  non-WasmTool types (MCP servers and channel relays now use their
  correct activation methods)
- Remove dead MissingSecret struct and missing_secrets field (never
  populated, flagged by reviewer)
- Consolidate capabilities file parsing in configure(): parse once
  and reuse for allowed names, validation_endpoint, and auto-generation
- Fix auth() doc comment: note MCP OAuth side effects
- Fix stale save_setup_secrets reference in server.rs comment
- Add regression test for activation dispatch bug

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-11 16:01:41 -07:00
369741fc60 Add generic host-verified /webhook/tools/{tool} ingress (#757)
* Add generic host-verified webhook ingress for tools

* Stabilize trace E2E test rig and approval behavior

* Fix webhook security issues from review feedback

- Reject tools without webhook_capability() (was unauthenticated RCE)
- Remove secret-in-query-string fallback (leak via logs/referrers)
- Require approval for event_emit tool (escalation via routine triggers)
- Simplify header_value() (HeaderMap already case-insensitive)
- Redact internal errors from webhook HTTP responses
- Remove unused hmac_timestamp_tolerance_secs field
- Add regression test for tool without webhook capability

[skip-regression-check]

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

* Harden webhook ingress: require auth mechanism, body limit layer, health check

- Reject webhook capabilities that declare no auth mechanism (empty
  WebhookCapability would previously allow unauthenticated access)
- Add DefaultBodyLimit layer to reject oversized payloads before buffering
- Health check (GET) now verifies tool has webhook_capability(), not just
  existence
- Add regression tests for all three fixes

[skip-regression-check]

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

* Fix auto_approve_tools inconsistency between dispatcher and thread_ops

dispatcher.rs skips all approval checks (including Always) when
auto_approve_tools is true, but thread_ops.rs still required approval
for Always tools. This caused deferred tool calls to unexpectedly halt
in test rigs and auto-approve configurations.

Match dispatcher behavior: short-circuit all approval when
auto_approve_tools is enabled.

[skip-regression-check]

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-11 03:36:25 +00: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
76375f2eaa refactor: centralize test credential constants into testing::credentials (#829)
* refactor: centralize test credential constants into testing::credentials

Scattered test credential strings (API keys, OAuth tokens, crypto keys,
Telegram tokens, session tokens) across ~25 files made security auditing
harder and created unnecessary duplication. Centralize all test-only fake
credentials into a new `src/testing/credentials.rs` module with named
constants and a shared `test_secrets_store()` helper.

- Convert `src/testing.rs` to directory module (`src/testing/mod.rs`)
- Add `src/testing/credentials.rs` with ~30 named constants
- Replace hardcoded literals in 24 source files
- Deduplicate `test_store()` helper (was copy-pasted in 3 files)
- Leave leak_detector/shell/signature tests as-is (inline values
  aid readability for pattern detection tests)

[skip-regression-check]

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

* refactor: replace real Telegram bot token with obviously fake test stub

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

* Update src/testing/credentials.rs

Co-authored-by: Copilot <[email protected]>

* Update src/testing/credentials.rs

Co-authored-by: Copilot <[email protected]>

* refactor: address PR review feedback on test credentials

- Fix TEST_CRYPTO_KEY doc comment ("32-byte hex" → "32-character key string")
- Rename confusing "real"/"fake" Anthropic constant names and values
- Change TEST_STRIPE_KEY from "sk-live" to "sk_test_fake123" to avoid scanners
- Use test_secrets_store() helper in orchestrator and http tool tests
- Clarify config_round_trip.rs doc comment about integration test visibility

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Copilot <[email protected]>
2026-03-10 13:25:32 -07:00
24d4fbb8a7 Revert "Feat/docker shell edition" + fix fmt/clippy (#886)
* Revert "Feat/docker shell edition (#804)"

This reverts commit c566faf28f.

* style: fix formatting issues from revert

Run cargo fmt to fix formatting across 7 files after the revert of
the docker shell edition feature.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-10 11:57:50 -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
6e1ed939cc Add event-triggered routines and workflow skill templates (#756)
* Add event-triggered routines and workflow skill templates

* 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: address PR review feedback for event_emit security and quality

Security fixes:
- Require approval (UnlessAutoApproved) for event_emit, matching routine_fire
- Enable sanitization on event_emit payload (external JSON reaches LLM)
- Remove user_id parameter from event_emit to prevent IDOR — always use ctx.user_id

Correctness fixes:
- Rename source → event_source in event_emit for consistency with routine_create
- Use json_value_as_filter_string for filter parsing (handles numbers/booleans)
- Case-insensitive matching for event source and event_type
- Add debug logging for missing filter keys in payload
- Fix skill_install_routine_webhook_sim test missing .with_skills()
- Fix schema_validator test for event_emit payload properties

Code quality:
- Move EventEmitTool struct/impl after RoutineHistoryTool (fix split layout)
- Deduplicate routine_to_info into RoutineInfo::from_routine in types.rs
- Add test section headers in e2e_routine_heartbeat.rs
- Clarify event_emit description to specify system_event routines only

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]>

* fix: make routine_system_event_emit test create routine before emitting

- Add routine_create step to trace fixture so event_emit has a matching
  routine to fire
- Assert fired_routines > 0, not just key presence (Copilot review)
- Add .with_auto_approve_tools(true) since event_emit now requires approval

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

* fix: renumber test headers after system_event test insertion

Test 4 was duplicated (routine_cooldown and heartbeat_findings).
Renumber heartbeat_findings to Test 5 and heartbeat_empty_skip to Test 6.

[skip-regression-check]

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

* fix: merge staging and add missing RoutineEngine args in test

RoutineEngine::new on staging requires `tools` and `safety` params.
Update system_event_trigger_matches_and_filters test to pass them.

[skip-regression-check]

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

* fix: address new Copilot review comments

- Add .with_auto_approve_tools(true) to skill_install_routine_webhook_sim
  test so event_emit doesn't block on approval
- Fix module-level doc comment for event_emit to specify system_event trigger

[skip-regression-check]

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

* fix: deduplicate json_value_as_string helper

Remove private `json_value_as_string` from routine_engine.rs and use
the identical public `json_value_as_filter_string` from routine.rs,
eliminating divergence risk. (Copilot review)

[skip-regression-check]

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]>
2026-03-10 11:08:04 -07:00
e8f8ec06e3 fix(mcp): strip top-level null params before forwarding to MCP servers (#795)
* feat(llm): per-provider unsupported parameter filtering (#749, #728) (#809)

Add declarative `unsupported_params` field to provider definitions in
providers.json. Parameters listed are stripped from requests before
sending, preventing 400 errors from providers that reject them (e.g.
gpt-5 family and kimi-k2.5 rejecting custom temperature values).

- Add `unsupported_params` to ProviderDefinition and RegistryProviderConfig
- Propagate from registry through config resolution
- Generic strip helpers handle temperature, max_tokens, stop_sequences
- Apply filtering in RigAdapter and AnthropicOAuthProvider
- Mark openai and tinfoil providers as unsupporting temperature
- Update openai default model to gpt-5-mini

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

* fix(mcp): strip top-level null params before forwarding to MCP servers

LLMs frequently emit `"field": null` for optional parameters in tool
calls. Many MCP servers reject explicit nulls for fields that should
simply be absent — e.g. Notion returns 400 for `"sort": null` in a
search call, expecting the field to be omitted entirely.

Strip top-level null keys from the params object before calling
`call_tool()`. Only top-level keys are stripped; nested nulls are
preserved since they may be semantically meaningful.

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

---------

Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-10 11:08:01 -07:00
46c01cb841 fix: preserve text before tool-call XML in forced-text responses (#852)
* fix: preserve text before tool-call XML in forced-text responses (#789)

Local models (Qwen3, DeepSeek, GLM) emit <tool_call> XML even when no
tools are available (force_text mode). The existing strip_xml_tag()
discards everything from an unclosed opening tag onward, producing an
empty string that triggers the "I'm not sure how to respond" fallback.

Add truncate_at_tool_tags() — a code-region-aware pre-processing step
that truncates at the first tool-call XML tag BEFORE clean_response()
runs, preserving all useful text before the tag. Protect all 7
clean_response() call sites. Case-insensitive matching handles models
that emit <TOOL_CALL> or <Tool_Call> variants.

Secondary fix: add has_native_thinking() model detection to skip
<think>/<final> system prompt injection for models with built-in
reasoning (Qwen3, QwQ, DeepSeek-R1, GLM-Z1, etc.), preventing
thinking-only responses that clean to empty.

Wire with_model_name(active_model_name()) at all 9 production sites
that construct Reasoning, so the runtime model name (not static config)
drives system prompt generation.

126 new/updated tests covering truncation edge cases, code-block
awareness, Unicode, case-insensitivity, StubLlm integration for
complete/plan/evaluate_success/respond_with_tools paths, model
detection, and conditional system prompt generation.

Closes #789

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

* fix: address Copilot review — unclosed-only truncation, ASCII case folding

- truncate_at_tool_tags() now only truncates at UNCLOSED tool tags;
  properly closed tags (e.g. <tool_call>...</tool_call>) are left intact
  for clean_response() to strip normally, preserving any text after them
- Switch from to_lowercase() to to_ascii_lowercase() to prevent byte
  offset misalignment with non-ASCII characters whose lowercase form
  has different byte length (e.g. Kelvin sign U+212A)
- Add closing_tag_for() helper to derive closing tags from open patterns
- Fix doc comment: "fenced markdown code blocks or inline code spans"
  (not "indented", which find_code_regions() doesn't detect)
- Add regression tests: closed vs unclosed for each tag variant,
  Unicode + case-insensitive offset safety, and mixed closed/unclosed

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

* fix: minor review items — consistent ascii_lowercase, closing_tag_for tests

- Switch has_native_thinking() from to_lowercase() to to_ascii_lowercase()
  for consistency with truncate_at_tool_tags() approach
- Add unit tests for closing_tag_for(): standard tags, space-suffixed
  patterns, pipe-delimited tags, and exhaustive coverage of all
  TOOL_TAG_PATTERNS entries
- Add test for mixed closed+unclosed tags of different types

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-10 11:07:52 -07:00
66e834d9d7 fix(wasm): run leak scan before credential injection in tools wrapper (#791)
* fix(wasm): run leak scan before credential injection in tools wrapper

The tools WASM wrapper runs the LeakDetector on HTTP request headers
AFTER inject_host_credentials() has already substituted real secrets
(e.g., xoxb- Slack bot tokens). This causes the leak detector to
flag the tool's own legitimate outbound API calls as secret exfiltration.

Move the scan to run on raw_headers before any credential injection,
matching the fix already applied to the channels wrapper in #421.

Fixes the same class of bug as #421 (which only fixed channels/wasm/wrapper.rs).

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

* perf: inline leak scan to avoid Vec allocation on every HTTP request

Address review feedback: instead of cloning all header keys/values into
a Vec to pass to scan_http_request(), iterate over raw_headers directly
using scan_and_clean(). This also provides more specific error messages
(URL vs header vs body).

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

* style: fix cargo fmt formatting in leak scan loop

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-10 11:06:53 -07:00
3a2989d009 feat(setup): quick onboarding, preserve .env vars, clean up boot logging (#821)
* feat(setup): quick onboarding, preserve .env vars, clean up boot logging (#751, #674)

- Add upsert_bootstrap_vars() to preserve user-added .env vars on re-onboarding
- Add --quick mode: auto-defaults DB + security, asks only LLM provider (2 steps)
- Auto-triggered onboarding uses quick mode for near-instant first run
- Fix NEAR AI model fetch to use cloud-api.near.ai when API key is set
- Handle missing WASM tools/channels directories gracefully
- Downgrade all boot/shutdown tracing::info! to debug (boot screen shows user output)

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

* fix(setup): gate env_backend variable behind postgres feature flag [skip-regression-check]

Clippy lint fix — not a behavioral change, just moving a variable declaration
inside the cfg(feature = "postgres") block where it's used.

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

* fix(review): address PR review comments

- WASM loaders: use tokio::fs::metadata, only treat NotFound as empty,
  propagate other IO errors, handle TOCTOU in read_dir
- bootstrap: only ignore NotFound in read_to_string, propagate other errors
- wizard: restore print_info/print_success for migrations in interactive
  mode (gated by !config.quick), keep tracing::debug for diagnostics
- tests: use shared crate::config::helpers::ENV_MUTEX instead of separate
  NEARAI_ENV_MUTEX to prevent cross-test env var races
- README: fix quick mode description to mention model selection, clarify
  auto_setup_database may prompt when DATABASE_URL is set

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

* feat(setup): skip prompts for DATABASE_URL in quick mode [skip-regression-check]

auto_setup_database() now uses DATABASE_URL directly without calling
step_database_postgres() (which prompts for confirmation). Quick mode
should be fully non-interactive when env vars are already set.

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

* fix(cli): update --quick help text to mention model selection [skip-regression-check]

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-10 05:02:33 +00:00
94d101924e refactor: encapsulate leaked abstractions into owning modules (#778)
* refactor: encapsulate leaked abstractions from main.rs and app.rs into owning modules

Move module-specific initialization logic out of main.rs (1222→665 lines, -46%) and
app.rs (944→780 lines, -17%) into their respective owning modules as public factory
functions. This enforces separation of concerns so that adding a new DB backend, MCP
transport, or channel doesn't require editing main.rs/app.rs.

Key changes:
- Tracing init functions → src/tracing_fmt.rs
- DB connection factory (connect_with_handles + DatabaseHandles) → src/db/mod.rs
- Secrets store factory (create_secrets_store) → src/secrets/mod.rs
- MCP transport dispatch factory (create_client_from_config) → src/tools/mcp/factory.rs
- Orchestrator setup (setup_orchestrator + OrchestratorSetup) → src/orchestrator/mod.rs
- WASM channel setup (setup_wasm_channels) → src/channels/wasm/setup.rs
- Worker entry points (run_worker, run_claude_bridge) → src/worker/mod.rs
- Shared CLI secrets init (init_secrets_store) → src/cli/mod.rs
- Tunnel startup (start_managed_tunnel) → src/tunnel/mod.rs
- Onboard check (check_onboard_needed) → src/setup/mod.rs
- ExtensionManager unified MCP: uses create_client_from_config via McpProcessManager,
  enabling stdio/Unix transports for hot-activated MCP servers
- Deduplicated ~130 lines of secrets store init across cli/mcp.rs and cli/tool.rs
- CLAUDE.md updated with module-owned initialization guideline

[skip-regression-check]

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

* refactor: address review feedback — deduplicate db factory, extract channel helper

- connect_from_config() now delegates to connect_with_handles() to eliminate
  duplicated backend-matching logic (Copilot review feedback)
- Extract register_channel() helper from setup_wasm_channels() loop body
  to improve readability (Gemini review feedback)

[skip-regression-check]

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

* style: fix rustfmt line wrapping in setup_wasm_channels

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

* test: add integration test for module-owned initialization factories

Exercises the full factory chain end-to-end to verify nothing was lost
when initialization logic was moved from main.rs/app.rs into owning modules:

- connect_with_handles returns Database + populated backend handles
- connect_from_config delegates correctly (produces working Database)
- secrets::create_secrets_store builds working store from DatabaseHandles
- db::create_secrets_store standalone factory round-trips secrets
- Both secrets factories produce compatible stores (cross-read works)
- ExtensionManager constructs with McpProcessManager and is functional
- DatabaseHandles default is empty

All tests run without external services using libsql in-memory/tempfile.

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

* fix: wire cli/mcp.rs and cli/tool.rs to shared init_secrets_store()

Both files had inline implementations identical to cli::init_secrets_store().
Replace with delegation to complete the claimed deduplication.

[skip-regression-check]

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

* style: fix rustfmt line wrapping in integration test

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

* fix(review): remove unused Config import and deduplicate Error Handling section

- Remove `#[allow(unused_imports)]` and unused `use crate::config::Config`
  from cli/tool.rs (no longer needed after delegating to shared
  `cli::init_secrets_store()`)
- Remove duplicate Error Handling subsection from CLAUDE.md Key Patterns
  (all four bullets already exist in Code Style section and
  review-discipline.md)

Addresses Copilot review comments.

[skip-regression-check]

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

* fix(review): address remaining Copilot review comments

- secrets/mod.rs: clarify docstring that None is a normal no-db condition
- app.rs: add comment explaining the empty_handles fallback path
- orchestrator/mod.rs: combine duplicated sandbox condition into single block
- setup/mod.rs: document env var reads and thread-safety caveat

[skip-regression-check]

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Henry Park <[email protected]>
2026-03-10 04:39:51 +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
fcb152e408 feat(wasm): lazy schema injection on WASM tool errors (#638)
* feat(wasm): lazy schema injection on WASM tool errors

When a WASM tool returns an error (ToolReturnedError), call the module's
description() and schema() WIT exports and append them as a hint in the
error message. This lets the LLM retry with correct parameters without
us including large schemas in every request's tools array.

- Change ToolReturnedError from tuple to struct variant with hint field
- Add build_tool_hint() that calls WASM description()/schema() exports
- Cap description at 500 chars, schema at 3000 chars to limit context
- Hint flows automatically through Display → ToolError → ChatMessage

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

* fix: use floor_char_boundary for UTF-8 safe truncation in tool hints

Use existing crate::util::floor_char_boundary() to avoid panicking
when truncation lands mid-multibyte character. Addresses review
feedback on PR #638.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-09 11:27:46 -07:00
ab0a2e05de fix(mcp): JSON-RPC spec compliance — flexible id, correct notification format (#685)
* fix(mcp): JSON-RPC spec compliance — flexible id, correct notification format

- McpRequest.id is now Option<u64> with skip_serializing_if, so
  notifications omit the id field as required by JSON-RPC 2.0 spec.
  Previously sent id: 0 which violates the spec.

- McpResponse.id uses flexible deserialization that accepts number,
  string, or null — fixes interop with non-standard MCP servers that
  return string ids or missing id fields on error responses.

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

* Fix review feedback: remove serde(default) from McpResponse.id, fix test assertions

- Remove #[serde(default)] from McpResponse.id so notifications (no id field)
  don't incorrectly parse as responses — prevents DoS/spoofing via SSE
- Update test assertions to use Some(value) after id became Option<u64>

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

* fix: update new transport files for Option<u64> id after rebase

Upstream #721 added stdio/unix/transport modules that use McpRequest.id
and McpResponse.id as u64. After our rebase (which changes id to
Option<u64>), these need .unwrap_or(0) for HashMap keys and Some()
wrapping in tests.

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

* test: add regression tests for JSON-RPC spec compliance

Tests for notification serialization without id field,
flexible id deserialization (string, null, non-numeric).

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-09 08:37:42 -07:00
Protocol ZeroandGitHub 98e9a40762 test(job): cover job tool validation and state transitions (#681)
Add focused coverage for create/list/status/cancel job tools so validation errors, summary formatting, and cancellation behavior stay stable. This locks in the current user-facing responses for running and completed jobs without changing production code.

Made-with: Cursor
2026-03-09 03:49:58 +00: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
02f85a8ad5 feat(mcp): transport abstraction, stdio/UDS transports, and OAuth fixes (#721)
* feat(mcp): transport abstraction, stdio/UDS transports, and OAuth fixes

Extract McpTransport trait from HTTP-coupled McpClient, enabling pluggable
transport backends. Implements stdio and Unix domain socket transports for
local MCP server integration, fixes OAuth discovery per RFC 9728, and adds
SSRF protection.

Transport abstraction (Step 2):
- McpTransport trait with send(), shutdown(), supports_http_features()
- HttpMcpTransport extracted from McpClient with SSE parsing, session tracking
- Shared JSON-RPC framing helpers (write_jsonrpc_line, spawn_jsonrpc_reader)
- McpClient refactored to hold Arc<dyn McpTransport>

Stdio transport (#652, Step 4):
- StdioMcpTransport spawns child process, communicates via stdin/stdout
- McpProcessManager for lifecycle management with exponential backoff restart
- Background stderr drain task for debug logging

Unix domain socket transport (#134, Step 5):
- UnixMcpTransport connects to existing Unix sockets
- Reuses shared JSON-RPC framing from transport.rs

HTML error body sanitization (#263, Step 1):
- sanitize_error_body() detects HTML, strips control chars, truncates to 500

Custom headers (#639, Step 3):
- headers field on McpServerConfig, merged into every HTTP request
- --header CLI arg for `mcp add`

Config and CLI updates (Step 6):
- McpTransportConfig tagged enum (Http/Stdio/Unix) with serde support
- EffectiveTransport for zero-copy config dispatch
- CLI: --transport, --command, --arg, --env, --socket flags for `mcp add`
- `mcp list` shows transport type

OAuth fixes (#299, Step 8):
- Multi-strategy discovery (401-based, RFC 9728, direct)
- RFC 8707 resource parameter in auth and refresh flows
- SSRF protection with IPv4-mapped IPv6 bypass detection
- Well-known URI construction per RFC 8414

Closes #652, #134, #639, #263, #299

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

* fix(mcp): address audit findings from crate review

- Fix SSRF bypass: make validate_url_safe async with DNS resolution to
  block hostnames that resolve to private/link-local IPs
- Fix UTF-8 truncation: use char-based truncation in sanitize_error_body
  to avoid panicking on multi-byte characters
- Fix SSE parser: process only complete lines to handle chunks split
  across boundaries, add 10MB buffer size limit
- Add debug_assert for transport type mismatch in new_with_config
- Propagate custom headers in new_with_transport constructor
- Deduplicate effective_transport() calls in CLI list command
- Gate test-only accessors with #[cfg(test)] to eliminate dead_code warnings
- Document JSON-RPC notification id:0 limitation in protocol.rs
- Document total backoff wait time (31s) in process.rs
- Add regression test for multi-byte UTF-8 truncation

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

* fix(mcp): address PR review findings from Copilot, Gemini, and zmanian

Moderate/High fixes:
- Plumb custom headers through new_authenticated constructor
- Restrict HTTP to localhost only in validate_url_safe (prevent
  plaintext credential leaks over non-localhost HTTP)
- Add mcp_process_manager.shutdown_all() to app shutdown path to
  prevent orphaning stdio child processes
- Validate discovered authorization_url before opening browser
  (prevent malicious MCP server redirecting to phishing page)

Medium fixes:
- Upgrade debug_assert to assert in new_with_config (fires in release)
- Remove pending map entry on Ok(Err(_)) in stdio/unix send() to avoid
  stale entries and unnecessary 30s waits
- Shut down old transport in try_restart() before spawning replacement
- Redact env var values in mcp list --verbose (may contain secrets)
- Drain pending requests on shutdown to wake waiters immediately
- Add IPv6 link-local, site-local, unique-local, and documentation
  ranges to is_dangerous_ip SSRF protection

Low fixes:
- Truncate logged JSON parse error lines to 200 chars (prevent
  sensitive data in logs)
- Remove misleading shutdown comment in unix_transport
- Use tempfile::tempdir() instead of hardcoded /tmp/ path in test
- Adopt main's improved sanitize_error_body (HTML tag stripping,
  200-char truncation with char_indices)

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

* fix(mcp): gate unix_transport with #[cfg(unix)] for Windows compat

- Add #[cfg(unix)] to unix_transport module declaration
- Add #[cfg(unix)]/#[cfg(not(unix))] branches in app.rs for Unix
  socket MCP server setup
- Remove unused sanitize_error_body import in client.rs tests

[skip-regression-check]

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-09 02:47:42 +00:00
FrankandGitHub 9401ab0d58 fix: add timezone conversion support to time tool (#687) 2026-03-08 21:17:07 +00:00