Commit Graph
28 Commits
Author SHA1 Message Date
c916069dd2 refactor(registry): move MCP servers from code to JSON manifests (#1144)
* refactor(registry): move MCP server entries from code to JSON manifests

Move 8 hardcoded MCP server RegistryEntry structs from
builtin_entries() into data-driven JSON files under
registry/mcp-servers/, matching the existing pattern used by
tools and channels. Exclude the GitHub MCP entry which conflicts
with the WASM GitHub tool's OAuth flow.

Extend ManifestKind with McpServer, make version/source optional
on ExtensionManifest (MCP servers don't need them), and add
url/auth fields for MCP-specific config. Update build.rs,
embedded catalog, catalog loader, installer, and CLI display
to handle the new kind and optional fields.

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

* fix(registry): address PR review — add missing slack-mcp, remove .expect(), fix fmt

- Add missing slack-mcp.json (was dropped during migration)
- Remove production .expect() in get_strict(), replace with .ok_or_else()
- Clean up unwrap_or_default() in key_for() to use .next() directly
- Log warning for MCP manifests missing url field instead of silent empty
- Run cargo fmt to fix formatting diffs

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

* ci: re-trigger CI with correct base branch (staging)

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

* fix(ci): improve no-panics check to properly exclude test modules

The grep-based filter only excluded lines literally containing
#[cfg(test)], #[test], or 'mod tests' — not lines *inside* test
modules. Use awk to track hunk context from diff @@ headers and
skip all added lines within test module hunks.

[skip-regression-check]

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

* refactor(registry): remove slack-mcp MCP entry (conflicts with WASM slack tool)

Remove slack-mcp.json alongside the already-excluded github MCP
entry — both conflict with existing WASM tools of the same name.

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

* fix(registry): address re-review — skip invalid MCP entries, fix install order

- to_registry_entry() now returns Option<RegistryEntry>; MCP manifests
  missing a url field are skipped with a warning instead of creating
  broken entries with empty URLs
- Move McpServer early-return before require_source() in install paths
  so the error message is clear ("cannot install MCP servers") rather
  than the misleading "missing source spec"
- Add test for MCP manifest with missing URL returning None

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:59:55 +00: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
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
Henrik RosenquistandGitHub fd574b2859 Expose the shared agent session manager via AppComponents (#532)
* Expose agent session manager via AppComponents

* Polish AppComponents session manager naming
2026-03-12 12:14:01 -07: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
b0214fef41 feat: add channel-relay integration for Slack (#790)
* 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]>

* 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

---------

Co-authored-by: Henry Park <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
2026-03-10 16:34:54 -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
d73e35cfb0 feat: add AWS Bedrock LLM provider via native Converse API (#713)
* feat: add AWS Bedrock LLM provider via native Converse API

* fix: use JSON parsing for tool result error detection instead of brittle substring matching

* refactor: extract duplicated inference config builder into helper function

* fix: address review feedback — safe casts, input validation, and tests

- Safe u32→i32 cast for max_tokens using try_from with clamp
- Remove brittle string-based error detection fallback for tool results
- Validate BEDROCK_CROSS_REGION against allowed values (us/eu/apac/global)
- Validate message list is non-empty before Converse API call
- Log when using default us-east-1 region
- Update llm_backend doc comment to list all backends
- Add tests for build_inference_config and empty message handling

* fix: persist AWS_PROFILE for Bedrock named profile auth

The wizard collected the profile name but only printed a hint to set
it manually. Now it saves to settings and writes AWS_PROFILE to the
bootstrap .env, consistent with how BEDROCK_REGION and other Bedrock
settings are persisted.

* feat: gate AWS Bedrock behind optional `bedrock` feature flag

The AWS SDK dependencies (aws-config, aws-sdk-bedrockruntime,
aws-smithy-types) require cmake and a C compiler to build aws-lc-sys.
Gate them behind an opt-in `bedrock` feature flag so default builds
are unaffected.

Build with: cargo build --features bedrock
All config, settings, and wizard code stays unconditional (no AWS deps)
so users can configure Bedrock even without the feature compiled — they
get a clear error at startup directing them to rebuild.

* fix: address review feedback and adapt Bedrock provider to registry architecture (takeover #345)

- Resolve merge conflicts with main's registry-based provider system
- Add missing cache_creation_input_tokens/cache_read_input_tokens fields
- Add missing content_parts field in test ChatMessage
- Fix string literal type mismatches in wizard env_vars (.to_string())
- Remove non-functional bearer token auth (AWS_BEARER_TOKEN_BEDROCK) from
  wizard and documentation per reviewer feedback from @zmanian and @serrrfirat
- Remove stale BEDROCK_ACCESS_KEY proxy entry from provider table
- Update Bedrock provider to use is_bedrock string check (LlmBackend enum removed)
- Add bedrock_profile fallback from settings in config resolution

[skip-regression-check]

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

* fix: use main's Cargo.lock as base to preserve dependency versions

Regenerating Cargo.lock from scratch caused transitive dependency version
drift that broke the html_to_markdown fixture test in CI.

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

* fix: bedrock config bugs — spurious warning, alias normalization, profile fallback

- Move is_bedrock check before unknown-backend warning to prevent
  spurious "unknown backend" log for bedrock users
- Normalize backend aliases ("aws", "aws_bedrock") to "bedrock" so
  the provider factory matches correctly
- Add settings.bedrock_profile fallback for AWS_PROFILE, consistent
  with region and cross_region resolution

[skip-regression-check]

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

* fix: address Copilot review feedback — bearer token cleanup, stop_sequences, model dedup

- Remove stale bearer token refs from setup README and CHANGELOG
- Remove dead bedrock_api_key secret injection mapping
- Pass stop_sequences through to Bedrock InferenceConfiguration
- Remove "API key" from wizard menu description (bearer token removed)
- Skip duplicate LLM_MODEL write for bedrock backend in wizard
- Fix cargo fmt formatting

[skip-regression-check]

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

* fix: address review feedback — async new(), remove LiteLLM entry, wizard fixes

- Remove dead LiteLLM-based bedrock entry from providers.json (native
  Converse API intercepts before registry lookup)
- Make BedrockProvider::new() async to avoid block_in_place panic in
  current_thread runtimes; propagate async to create_llm_provider,
  build_provider_chain, and init_llm
- Document CMake build prerequisite in docs/LLM_PROVIDERS.md
- Clear bedrock_profile when user selects "default credentials" in wizard
- Fix selected_model clearing to match established pattern (conditional
  on provider switch, not unconditional)
- Add regression tests for bedrock model preservation and profile clearing

Addresses review feedback from @zmanian on PR #713.
Streaming support tracked in #741.

[skip-regression-check]

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

* fix: address remaining review comments — CLAUDE.md backends, wizard UX

- Add `bedrock` to CLAUDE.md inline backend list (#10)
- Skip full setup re-run when keeping existing Bedrock config (#11)
- Clear stale bedrock_profile on empty named-profile input (#12)
- Add regression test for empty profile clearing

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

---------

Co-authored-by: Chris Gorski <[email protected]>
Co-authored-by: cgorski <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-09 07:10:25 +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
461d7712e8 fix(config): init_secrets no longer overwrites entire config (#726)
* fix(config): init_secrets no longer overwrites entire config

init_secrets() was calling Config::from_db_with_toml() to re-resolve
config after injecting credentials. This rebuilt the entire config from
env/DB/defaults, nuking all other config fields (agent, safety, tools,
etc.) even though only LlmConfig depends on injected credentials.

This caused 5 CI test failures: the test rig's carefully chosen config
values (max_tool_iterations, allow_local_tools, etc.) were silently
overwritten with production defaults after secret injection.

Fix: add Config::re_resolve_llm() that re-resolves only the LLM config
after credential injection, leaving all other config fields untouched.
Also fix TraceLlm::complete() to skip ToolCalls steps when called in
force_text mode (iteration limit).

[skip-regression-check]

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

* fix(test): update test to match TraceLlm::complete() skip-tool-calls behavior [skip-regression-check]

TraceLlm::complete() now skips ToolCalls steps (force_text mode) instead
of erroring. Update the test to verify it skips past a ToolCalls step and
returns the subsequent Text step.

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-08 13:32:42 -07: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
11c5e25422 feat(setup): Anthropic OAuth onboarding with setup-token support (#384)
* feat(setup): add Anthropic OAuth and Codex OAuth onboarding flows

Add OAuth token authentication as an alternative to API keys during
onboarding for both Anthropic (via `claude login`) and OpenAI/Codex
(via `~/.codex/auth.json`).

Key changes:
- New `AnthropicOAuthProvider` using `Authorization: Bearer` header
  (rig-core hardcodes `x-api-key` which rejects OAuth tokens)
- Wizard auth method selector: "Direct API Key" vs "OAuth Token"
  for both Anthropic and OpenAI providers
- Codex token extraction from `$CODEX_HOME/auth.json` / `~/.codex/auth.json`
- Claude Code sandbox sub-step in Docker setup (checks for credentials)
- Secret injection mappings for `ANTHROPIC_OAUTH_TOKEN` and `CODEX_OAUTH_TOKEN`
- `CODEX_OAUTH_TOKEN` falls back to `OPENAI_API_KEY` (same Bearer auth)

Supersedes #143 which had a broken auth flow (OAuth token sent as
x-api-key → 401). Credit to @bigguybobby for the original approach.

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

* fix: persist OAuth tokens in bootstrap .env and re-extract at startup

OAuth tokens stored only in the secrets DB were invisible to
Config::from_env() which runs before the DB connects (chicken-and-egg).

Two fixes:
1. write_bootstrap_env() now persists ANTHROPIC_OAUTH_TOKEN and
   CODEX_OAUTH_TOKEN to ~/.ironclaw/.env (same pattern as NEARAI_API_KEY)
2. main.rs re-extracts a fresh token from the OS credential store
   (macOS Keychain / ~/.claude/.credentials.json) before config resolution,
   handling token expiry (8-12h) gracefully

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

* fix: persist all LLM credentials in bootstrap .env, not just NEAR AI

All providers had the same chicken-and-egg issue: API keys stored in the
secrets DB were invisible to Config::from_env() which runs before DB
connects. Only NEARAI_API_KEY was written to bootstrap .env.

Now write_bootstrap_env() persists all credential env vars:
NEARAI_API_KEY, ANTHROPIC_API_KEY, ANTHROPIC_OAUTH_TOKEN, OPENAI_API_KEY,
CODEX_OAUTH_TOKEN, LLM_API_KEY, TINFOIL_API_KEY.

Also: setup_api_key_provider() now sets the env var during the wizard
session so write_bootstrap_env() can pick it up.

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

* fix: address security review findings for OAuth onboarding

- Extract "oauth-placeholder" to named OAUTH_PLACEHOLDER constant shared
  across config and wizard to prevent silent drift
- Document plaintext credential tradeoff in write_bootstrap_env (API keys
  stored with 0o600 permissions, recommend full-disk encryption)
- Add blocking "Press Enter" wait in Anthropic OAuth retry flow so user
  has time to run `claude login` in another terminal
- Add escape hatch from manual OAuth paste back to API key flow (empty
  input switches to setup_api_key_provider)
- Fix Retry-After header: parse u64 seconds into Duration before passing
  to LlmError::RateLimited
- Make config::llm module pub(crate) for constant visibility
- Use .bearer_auth() instead of manual format!("Bearer {}")
- Remove response body from debug log (may contain PII)
- Update Anthropic API version to 2024-10-22

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

* security: remove plaintext credentials from bootstrap .env

Credentials (API keys, OAuth tokens) were being written in plaintext to
~/.ironclaw/.env to work around a chicken-and-egg problem: Config::from_env()
runs before the encrypted secrets DB is connected.

Instead of storing secrets on disk, LlmConfig::resolve() now defers
gracefully when credentials are missing — it returns None for the provider
config instead of hard-erroring with MissingRequired. After the DB connects,
AppBuilder::build_all() loads secrets from encrypted storage via
inject_llm_keys_from_secrets() and re-resolves the config.

For Anthropic OAuth tokens (which expire in 8-12h), the secret injection
step also tries the OS credential store (macOS Keychain / Linux
credentials.json) for a fresh token, overriding the potentially stale
copy in the DB.

Changes:
- LlmConfig::resolve(): OpenAI, Anthropic, OpenAI-compatible, and Tinfoil
  all return None instead of MissingRequired when credentials are absent
- write_bootstrap_env(): no longer writes any credential env vars
- inject_llm_keys_from_secrets(): refreshes Anthropic OAuth from OS
  credential store before overlay is finalized
- main.rs: removed OAuth re-extraction hack (no longer needed)

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

* fix: load OS credential store tokens even without secrets DB

The OAuth token extraction from macOS Keychain / Linux credentials files
was only running inside inject_llm_keys_from_secrets(), which requires
the encrypted secrets DB. When no master key is configured, init_secrets()
returned early — skipping both DB secret loading AND OS credential store
extraction, leaving the Anthropic OAuth token unavailable.

Split into two paths:
- inject_llm_keys_from_secrets(): loads from encrypted DB + OS stores
- inject_os_credentials(): loads from OS stores only (no DB needed)

init_secrets() now calls inject_os_credentials() and re-resolves config
even in the no-master-key early-return path, so `claude login` tokens
are always available regardless of secrets DB state.

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

* fix: add anthropic-beta header required for OAuth authentication

Anthropic's api.anthropic.com requires the `anthropic-beta: oauth-2025-04-20`
header to accept OAuth Bearer tokens. Without it, the API returns 401
"OAuth authentication is currently not supported."

Also reverts API version to 2023-06-01 since the OAuth beta flag does
not support the 2024-10-22 version (returns 400 "not a valid version").

This was the same bug that caused PR #143's 401 errors — the beta header
was missing entirely.

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

* fix: Anthropic and OpenAI model resolution respects selected_model

The Anthropic and OpenAI config resolution ignored settings.selected_model
entirely, only checking the provider-specific env var (ANTHROPIC_MODEL,
OPENAI_MODEL) and falling back to a hardcoded default. This meant the
model chosen during onboarding wizard was silently overridden.

Now follows the same pattern as NearAI and OpenAI-compatible:
env var > settings.selected_model > hardcoded default.

Also deduplicated the Anthropic config construction (two identical
branches for API key vs OAuth now share model/base_url resolution).

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

* test: add provider resolution tests for all LLM backends

Covers deferred resolution (no credentials → None instead of error),
credential presence, model selection fallback chain, and OAuth token
routing for Anthropic, OpenAI, Tinfoil, Ollama, and NearAI.

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

* fix: handle nested tokens.access_token format in Codex auth.json

Codex CLI stores OAuth tokens in a nested format under
tokens.access_token (ChatGPT OAuth flow), not at the top level.
Also adds ENV_MUTEX to Codex token tests for thread safety.

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

* refactor: remove Codex OAuth onboarding (incompatible with OpenAI API)

Codex CLI OAuth tokens use a different endpoint
(chatgpt.com/backend-api/codex) and the Responses API wire format,
not api.openai.com with Chat Completions. The tokens lack the
model.request scope needed for the platform API, so they can't be
used as drop-in OPENAI_API_KEY replacements.

Removes: extract_codex_oauth_token(), wizard Codex OAuth flow,
CODEX_OAUTH_TOKEN env var support, and related tests.

OpenAI onboarding now uses direct API key only.

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

* style: fix formatting for CI (cargo fmt)

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

* fix: address Gemini review feedback

- Use ? operator for ANTHROPIC_MODEL/BASE_URL env resolution instead of
  .ok().flatten() to propagate ConfigErrors consistently
- Skip Tool messages without tool_call_id with a warning instead of
  using unwrap_or_default() which would send empty string to Anthropic
- Extract credential check into closure to reduce duplication in
  Claude Code sandbox setup

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

* refactor(review): address PR review feedback for OAuth onboarding

- Gate ANTHROPIC_OAUTH_TOKEN resolution to Anthropic provider only
  (was needlessly checked for all registry providers)
- Add 3 regression tests for OAuth config resolution:
  - oauth_token sets placeholder api_key
  - real api_key takes priority over oauth
  - non-Anthropic providers don't pick up oauth_token
- Validate OAuth token prefix (sk-ant-oat) in wizard to catch
  accidentally pasted API keys
- Improve error body read handling in AnthropicOAuthProvider
  (was silently swallowing read errors with unwrap_or_default)
- Remove extra blank line in write_bootstrap_env
- Remove stale blank line in RegistryProviderConfig doc comment

[skip-regression-check]

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

* fix: address PR #384 review comments

Blocker:
- Replace OnceLock<HashMap> with LazyLock<Mutex<HashMap>> for INJECTED_VARS
  so both inject_os_credentials() and inject_llm_keys_from_secrets() merge
  data instead of the second caller silently dropping its entries.

High:
- Add 401 retry with OS credential store re-extraction in
  AnthropicOAuthProvider, recovering from expired OAuth tokens (~8-12h)
  without manual intervention.
- Fix comment in app.rs: ~/.codex/auth.json → ~/.claude/.credentials.json.

Medium:
- Remove unsafe { std::env::set_var } from wizard; use thread-safe
  inject_single_var() overlay instead (safe on multi-threaded Tokio).
- Add post-init validation in AppBuilder: fail early with clear error when
  LLM_BACKEND is set but no credentials were resolved after secret injection.
- Add sk-ant-oat prefix validation in parse_oauth_access_token().
- Only route to AnthropicOAuthProvider when api_key is missing or equals
  OAUTH_PLACEHOLDER (API key takes priority over OAuth token).
- Teach fetch_anthropic_models() to use Bearer auth when only OAuth token
  is available (model listing no longer fails for OAuth-only users).

Low:
- Use optional_env() in wizard credential checks to read from injected
  overlay, not just raw env vars.

[skip-regression-check]

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: [email protected] <[email protected]>
2026-03-07 20:59:17 +00:00
ce5961b1ec fix(libsql): support flexible embedding dimensions (#534)
* fix(libsql): support flexible embedding dimensions (#494)

The libSQL schema hardcoded F32_BLOB(1536) for the embedding column,
preventing use of models with other dimensions (e.g. 768-dim
nomic-embed-text). This adds incremental migration support to the
libSQL backend and a V9 migration that rebuilds the memory_chunks
table with a plain BLOB column accepting any dimension.

- Add incremental migration infrastructure (INCREMENTAL_MIGRATIONS
  array + run_incremental() runner tracked via _migrations table)
- V9 migration rebuilds memory_chunks with BLOB column, drops the
  vector index (which requires fixed-dimension F32_BLOB)
- Update base schema for fresh installs (BLOB, no vector index)
- Vector search gracefully falls back to FTS-only when the index
  is absent (matches PostgreSQL behavior after its V9 migration)
- Remove now-incorrect "dimension is not 1536" warnings

Existing embeddings are preserved during migration. Users only need
to re-embed if they change their embedding model/dimension.

[skip-regression-check]

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

* fix: wrap incremental migrations in transaction for atomicity

Address PR review feedback: if the process crashes after executing
migration SQL but before recording it in _migrations, the migration
would be applied but not marked complete. Wrapping both operations
in a transaction ensures they succeed or fail together.

[skip-regression-check]

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

* chore: merge main and fix formatting drift

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-06 23:29:32 +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
944968bf76 fix(workspace): import custom templates before seeding defaults (#505)
Swap the order of import_from_directory() and seed_if_empty() so that
custom workspace templates from WORKSPACE_IMPORT_DIR take priority
over generic seeds. Previously, seed_if_empty() ran first and created
all default files, causing import_from_directory() to skip everything
since the files already existed in the DB.

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-03 11:14:02 -08:00
5f841554d5 feat(workspace): add TOOLS.md, BOOTSTRAP.md, and disk-to-DB import (#477)
* feat(workspace): add TOOLS.md, BOOTSTRAP.md, and disk-to-DB import

Add two new OpenClaw-compatible workspace markdown files:

- TOOLS.md: Environment-specific tool notes (SSH hosts, device names,
  etc.) injected into the system prompt under "## Tool Notes". Seeded
  as comment-only (like HEARTBEAT.md) so it's effectively empty until
  the user adds real content. Not write-protected — the agent can
  update it as it learns the environment.

- BOOTSTRAP.md: First-run onboarding ritual. Injected FIRST in the
  system prompt when present. Guides the agent through introducing
  itself, learning about the user, and updating workspace files.
  Only seeded on truly fresh workspaces (no existing identity files)
  to avoid triggering the ritual on existing deployments. Agent clears
  it via `memory_write(target="bootstrap")` when done.

Add `Workspace::import_from_directory()` for disk-to-DB import:

- Scans a directory for *.md files and imports any that don't already
  exist in the database (never overwrites user edits)
- Controlled by WORKSPACE_IMPORT_DIR env var, runs after seed_if_empty()
- Enables Docker images / deployment scripts to ship customized
  workspace templates that override generic seeds
- Backwards compatible: no-op when env var is unset

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

* fix: address PR review comments

- Use stable `path.extension() != Some(OsStr::new("md"))` instead of
  unstable `is_none_or` (nightly-only)
- Use `tokio::join!` for concurrent DB reads in fresh-workspace check
- Skip unreadable directory entries instead of failing the entire import
- Skip unreadable files instead of failing the entire import

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-02 19:00:21 -08:00
7b883a02c0 fix: init WASM runtime eagerly regardless of tools directory existence (#401)
* fix: init WASM runtime eagerly regardless of tools directory existence

The WASM tool runtime was only created at startup when both
`wasm.enabled` and `wasm.tools_dir.exists()` were true. This meant
that if the tools directory didn't exist yet (e.g. fresh deploy with
`--no-onboard`), the runtime was set to None and passed to the
ExtensionManager. Extensions installed later via the web UI would
then fail with "WASM runtime not available" because the runtime
could not be retroactively created.

The Wasmtime engine initialization has no dependency on the tools
directory — it only configures the compiler and starts an epoch
ticker thread. The directory is only needed later when loading
.wasm modules. Remove the directory check so the runtime is
available for post-startup extension activation.

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

* test: add regression tests for WASM runtime eager init

- runtime.rs: test_runtime_creation_without_tools_dir confirms the
  Wasmtime engine initialises without a tools directory on disk
- manager.rs: test_activate_wasm_tool_with_runtime_passes_runtime_check
  verifies activation gets past the runtime check when a runtime is
  provided (fails on missing file, not missing runtime)
- manager.rs: test_activate_wasm_tool_without_runtime_fails_with_runtime_error
  verifies the original error when no runtime is available

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

* refactor: use idiomatic Result-to-Option conversion for WASM runtime init

Address PR review feedback: replace match block with
.map(Arc::new).map_err(|e| warn!(...)).ok() chain.

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

* style: fix formatting in extension manager tests

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-01 08:51:05 +00: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
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
436066415b feat: embedded registry catalog and WASM bundle install pipeline (#283)
* feat: embedded registry catalog and WASM bundle install pipeline

Embed registry manifests at compile time so the extension catalog is
available without network access. Add tar.gz bundle support for WASM
extension downloads (tools and channels), a /api/extensions/registry
endpoint, CI job to build and publish WASM bundles on release, and
ephemeral in-memory secrets fallback so the extension manager works
even without a persistent secrets store.

Key changes:
- build.rs: collect registry/*.json into embedded_catalog.json at compile time
- src/registry/embedded.rs + catalog.rs: load embedded or on-disk catalog
- src/extensions/manager.rs: download_and_install_wasm handles tar.gz bundles,
  bare .wasm files, and separate capabilities downloads; wasm channel install
- src/channels/web/server.rs: /api/extensions/registry endpoint + no-cache headers
- src/app.rs: ephemeral InMemorySecretsStore fallback for extension manager
- registry/*.json: populate artifact download URLs for release bundles
- .github/workflows/release.yml: build-wasm-extensions CI job
- Simplified setup wizard and CLI registry commands

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

* fix: address PR review — archive hardening, decompression bomb guard, test fix

- Add 100 MB decompressed entry size cap to tar.gz extraction in both
  manager.rs and installer.rs to prevent decompression bombs
- Add archive.set_preserve_permissions(false) and set_unpack_xattrs(false)
  for defense-in-depth against malicious archives
- Fix test assertion logic in catalog.rs (|| → || with correct negation)
- Replace silent tar fallback in CI with explicit if/else for capabilities
- Add warning when installing without SHA256 verification

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

* fix: resolve clippy warning in settings.rs and enforce zero-warnings policy

Use struct initializer with ..Default::default() instead of field
reassignment. Update CLAUDE.md to codify zero clippy warnings policy —
all warnings must be fixed before committing, including pre-existing ones.

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

* fix: address PR review round 2 — build reliability, caps validation, naming

- build.rs: emit per-file rerun-if-changed for reliable content tracking;
  fix bundles fallback to match BundlesFile shape ({"bundles":{}})
- embedded.rs: parse catalog once via OnceLock instead of double-parsing
- manager.rs + installer.rs: add 1 MB size cap on capabilities_url downloads
  with proper error surfacing
- secrets/store.rs: rename misleading `pub mod testing` to `pub mod in_memory`
- server.rs: track installed extensions by (name, kind) tuple to avoid
  false positives across different extension kinds

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-21 05:43:28 +00:00
c038c7705b feat: add smart routing provider for cost-optimized model selection (#281)
* feat: add smart routing provider for cost-optimized model selection

Route simple tasks (greetings, status checks, short questions) to a cheap
model (e.g. Haiku) and complex tasks (code generation, analysis) to the
primary model, reducing agent costs without sacrificing quality.

Activates automatically when NEARAI_CHEAP_MODEL is set. Cascade mode
retries uncertain cheap-model responses with the primary model.

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

* style: apply cargo fmt formatting

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

* refactor: extract provider chain into shared build_provider_chain()

Consolidate the duplicated LLM provider chain construction from main.rs
and app.rs into a single build_provider_chain() function in llm/mod.rs.

This fixes the inconsistency where app.rs was missing retry wrapping
that main.rs had, and ensures both paths apply identical decorators:
retry → smart routing → failover → circuit breaker → cache.

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

* fix: address PR review — uncertainty detection and clippy lint

- Remove false-positive short response (<20 chars) uncertainty check
  that would escalate "Yes.", "42" etc. Now only empty responses and
  explicit uncertainty phrases trigger cascade escalation.
- Add #[allow(clippy::type_complexity)] to build_provider_chain() to
  fix CI clippy -D warnings failure.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-21 02:37:45 +00:00
98ee648fcb perf: speed up startup from ~15s to ~2s (#280)
Three high-impact changes eliminate most startup latency:

1. Enable wasmtime persistent compilation cache — call
   cache_config_load_default() so compiled native code is serialized to
   disk (~/.cache/wasmtime). Subsequent startups deserialize instead of
   recompiling, dropping the WASM phase from ~13s to <1s.

2. Cache compiled Component in PreparedModule — store the compiled
   wasmtime::component::Component directly instead of raw bytes.
   Eliminates ~2.6s recompilation on every first tool/channel execution.

3. Move blocking housekeeping to background tasks — embedding backfill
   (~1.3s of failing HTTP calls) and stale job cleanup are fire-and-forget
   work that no longer blocks the critical startup path.

Also: deduplicate Workspace creation in main.rs (two identical instances
reduced to one), and replace leftover println! in session validation with
tracing calls.

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-21 02:30:57 +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
e87d7bd066 feat: extend lifecycle hooks with declarative bundles (#176)
* feat: add bundled and declarative hook bundle loading

* fix: load plugin hooks only for active extensions

* fix: avoid duplicate plugin hook registration

* security: harden outbound webhook hooks

* fix: pin webhook DNS resolutions for outbound hooks

* fix: block IPv4-mapped local webhook targets

* style: format webhook hardening changes for CI

* fix: pass HookRegistry to ExtensionManager in AppBuilder

After merging main (which extracted AppBuilder from main.rs in #198),
the ExtensionManager::new() call in app.rs was missing the `hooks`
parameter that PR #176 added. This moves HookRegistry creation before
init_extensions() and threads it through, matching the existing pattern
in main.rs.

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

---------

Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-19 23:00:54 +00:00
ffb1cc9be8 refactor: architecture improvements for contributor velocity (#198)
* refactor: split large files and consolidate test stubs for contributor velocity

- Extract 7 Database sub-traits (ConversationStore, JobStore, SandboxStore,
  RoutineStore, ToolFailureStore, SettingsStore, WorkspaceStore) with Database
  as a supertrait combining them all
- Split libsql_backend.rs (2769 lines) into src/db/libsql/ directory with
  one file per sub-trait implementation
- Split config.rs (1753 lines) into src/config/ directory with 16 domain files
- Consolidate 3 duplicate test LLM stubs into shared StubLlm in src/testing.rs
- Split server.rs handlers into src/channels/web/handlers/ directory
- Extract main.rs init phases into AppBuilder (src/app.rs)
- Add developer setup script (scripts/dev-setup.sh)

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

* refactor: move heartbeat test from examples/ to tests/

Convert standalone example binary into a proper #[ignore] integration
test, matching the convention of the other integration tests.

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

* style: fix rustfmt formatting for CI

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

* fix: address PR review comments from Copilot

- tunnel.rs: replace .ok().flatten() with ? to propagate env var errors
- secrets.rs: remove misleading "process-wide cache" comment
- database.rs: use uppercase "DATABASE_URL" in error key
- testing.rs: gate harness tests with #[cfg(feature = "libsql")]

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

---------

Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-18 23:05:47 +00:00