mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
3c4095c7a1446ade51c288070fe0ce0930b272f0
542
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3c4095c7a1 | mesh and xray | ||
|
|
7e2ad874bd | Merge branch 'nearai:staging' into staging | ||
|
|
93c27fa053 |
feat: autonomous AI mesh network with post-quantum crypto
Add peer-to-peer mesh cluster module (feature-gated behind `cluster`): - UDP beacon discovery (port 9900) for zero-config LAN auto-discovery - ML-KEM-768 (Kyber) post-quantum key exchange + AES-256-GCM encrypted WebSocket overlay mesh - SWIM gossip protocol for membership and failure detection - Intelligent task routing: scores nodes by load, VRAM, model match, hop distance - Remote task execution via subprocess with streaming results - REST API endpoints: /api/mesh/status, /api/mesh/nodes - ed25519 signed beacons, persistent keypairs (~/.optimclaw/mesh_keys.json) - Lazy tool loading (OPTIMCLAW_LAZY_TOOLS) to reduce system prompt size Enable with: CLUSTER_ENABLED=1 optimclaw run Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
a8e83210ff |
feat(discord): add gateway channel flow in wasm (#944)
* feat(discord): restore gateway channel flow in wasm * chore(discord): bump channel version to 0.2.1 * fix(discord): address review feedback on gateway channel PR - Add #[serde(default)] to DiscordMessageMetadata for backward compat with old Option<String> serialized metadata - Restore mention polling alongside Gateway (on_poll processes gateway events first, then runs poll_for_mentions if configured) - Update on_respond to handle source_message_id with message_reference for mention-poll reply threading - Implement Gateway presence status: dnd before pairing, online after - Implement Gateway resume (OP 6) with session_id tracking, falling back to fresh identify on Invalid Session (OP 9) - Extract WebsocketSessionState and spawn_websocket_poll to reduce nesting in start_websocket_runtime - Simplify should_apply_dm_pairing tautology - Remove completed plan docs - Fix clippy items_after_test_module in extensions handler Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(discord): address review findings in gateway channel PR - Fix gateway presence always showing "online" by filtering empty owner_id strings from workspace store reads - Fix interaction followup using POST instead of PATCH to /messages/@original, which left deferred "thinking" state unresolved - Restore mention-poll pagination (up to 5 pages of 100 messages) - Remove dead ed25519-dalek and hex dependencies from WASM crate - Remove unused _channel_id parameter from remember_processed_id - Clean up redundant let binding in send_pairing_reply Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(discord): address second-round review findings - Log warning when gateway event queue JSON fails to deserialize instead of silently returning empty (zmanian review item 1) - Defer presence update from OP 10 Hello to after OP 0 READY, per Discord gateway protocol which requires READY before non-Identify commands (zmanian review item 2) - Add 0-25% random jitter to websocket reconnect backoff per Discord's reconnection recommendations (zmanian suggestion) - Extract WebsocketPollContext struct to replace 19-parameter spawn_websocket_poll function (zmanian suggestion) - Document intent bitmask 4609 = GUILDS + GUILD_MESSAGES + DIRECT_MESSAGES in capabilities JSON (zmanian suggestion) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: zhyaoyu <[email protected]> Co-authored-by: [email protected] <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
6d9dbbb3b9 |
fork: rename IronClaw → OptimClaw
Full rename of all identifiers, filenames, and references: ironclaw → optimclaw IronClaw → OptimClaw IRONCLAW → OPTIMCLAW ironclaw_common → optimclaw_common ironclaw_safety → optimclaw_safety Upstream: nearai/ironclaw |
||
|
|
8a320ae9db |
fix(routines): complete full_job execution reliability overhaul (#1650)
* fix(routines): persist full LLM transcript and remove sandbox gate for full_job Routine execution output was invisible — routine_fire returned a one-liner, routine_history had no actual output, and the conversation thread contained only a summary. Full-job routines also hard-failed without Docker. Three fixes: 1. **Full transcript persistence**: execute_lightweight now persists every message (prompt, LLM responses, tool calls with params, tool results) to the routine's conversation thread as it executes, not just a summary after the fact. 2. **Routine output visibility**: routine_history includes conversation_id and recent_output messages. routine_fire tells the user to check routine_history. Web detail page has a "View Execution Thread" button that navigates to the chat tab. ROUTINE_OK stores "No issues found" instead of None. Full-job summary pulls actual job output instead of generic "Job X finished". 3. **Remove SandboxReadiness gate**: full_job routines dispatch through the scheduler like regular /job commands — no Docker required. The SandboxReadiness enum is removed entirely. 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(worker): treat AutonomousUnavailable tool errors as recoverable The job worker crashed the entire job when a tool was denied for autonomous execution (e.g. secret_list). The error was already recorded in reason_ctx for the LLM to see, but process_tool_result_job returned Err which propagated through the agentic loop and terminated the job. Now all tool errors (including AutonomousUnavailable) return Ok, letting the LLM see the denial and try a different approach. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(llm): sanitize tool names for OpenAI Codex Responses API The Codex API requires tool names to match `^[a-zA-Z0-9_-]+$` but MCP/extension tools can have dots in their names (e.g. `mcp.server.tool`). This caused HTTP 400 errors when the job worker sent tool calls back to the LLM. Sanitize tool names in both `convert_tool_definition` and `convert_message` (function_call items) by replacing invalid characters with underscores. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(routines): inject execution context into full_job description [skip-regression-check] When a full_job routine dispatches a job, the LLM had no context that it was already executing inside a routine. It wasted iterations on infrastructure (discovering tools, creating routines, setting up auth) instead of doing the actual work. Prepend a clear directive to the job description telling the LLM that tools and the routine are already configured, and to execute the task directly. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(mcp): auto-refresh expired OAuth tokens on access [skip-regression-check] When IronClaw restarts, MCP servers fail with "Secret has expired" because get_access_token() checks token expiry locally and returns an error before any HTTP request is made — so the existing 401-retry refresh logic never triggers. Now get_access_token() catches SecretError::Expired and automatically calls refresh_access_token() using the stored refresh token. If the refresh succeeds, the new token is returned transparently. If it fails, the error message includes both the expiry and the refresh failure. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(mcp): align refresh token naming and set expiry on stored tokens Two bugs prevented MCP OAuth token auto-refresh on restart: 1. Naming mismatch: the hosted OAuth flow stored the refresh token as `{token_secret_name}_refresh_token` (e.g. `mcp_notion_access_token_refresh_token`) but `McpServerConfig::refresh_token_secret_name()` returned `mcp_notion_refresh_token`. The refresh token was there but unfindable. 2. Missing expiry: `store_tokens` in auth.rs never called `with_expiry()` even though `AccessToken::expires_in` was available. Combined with the fix from the previous commit (auto-refresh on Expired), tokens stored via the MCP auth flow will now also trigger refresh correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(web): show activity and transitions for agent jobs in job detail [skip-regression-check] The job events endpoint only checked sandbox jobs for ownership, returning 404 for agent jobs dispatched from routines. The detail handler also returned empty transitions for agent jobs. - events handler: fall back to agent job ownership check - detail handler: populate transitions from job's state history Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(routines): expose max_iterations for full_job routines (default 25) The max_iterations parameter was hardcoded to 10 and not configurable via routine_create or routine_update, causing complex tasks to hit the iteration cap. - Add max_iterations to full_job execution schema (1-200, default 25) - Thread it through parse → build → RoutineAction - Support updating via routine_update - Raise default from 10 to 25 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(routines): break self-dialogue loop after full_job plan execution After plan execution, the completion-check Q&A ("Is the job complete?" / "No, not complete...") was left in the message context, causing the agentic loop to repeat the same analysis instead of calling tools. Replace the stale dialogue with an action-oriented continuation prompt that instructs the LLM to use tools for remaining work. Also strip <suggestions> tags from all job output since they're only meaningful for interactive chat sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(repl): prevent test hang in single-message mode In single-message mode, start() stored a clone of the mpsc sender in self.msg_tx for approval injection. After the thread sent /quit and exited, the stored clone kept the stream alive, so stream.next() blocked forever in the test assertion that the stream ends. Skip storing the sender in single-message mode since interactive approval is not needed. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(jobs): treat text responses as final answer in agentic loop When the LLM produces a non-empty text response with no tool intent (already filtered by the nudge mechanism), it is the job's final answer. Previously, handle_text_response only exited the loop if the text matched rigid completion phrases like "job is complete". Natural summaries like "Weekly review completed and saved to Notion" were added to context and the loop continued, causing the LLM to restate the same summary until max_iterations was hit. Now any non-empty text response marks the job complete and stops the loop, matching the chat dispatcher behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * perf(tests): reduce skills catalog network failure test from 10s to 1s The test_search_returns_error_on_network_failure test connects to an unreachable RFC 5737 TEST-NET IP and waited for the full 10s production REQUEST_TIMEOUT. Add with_url_and_timeout test helper and use a 1s timeout instead. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tools): accept 'message' as alias for 'content' in message tool LLMs frequently call the message tool with {"message": "..."} instead of {"content": "..."}. Fall back to the 'message' key when 'content' is missing to avoid InvalidParameters errors during autonomous job execution. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tools): attach thread_id for gateway broadcast in message tool When the message tool broadcasts to all channels (channel=null), it sent an OutgoingResponse without a thread_id. The gateway silently dropped these messages (returned Ok but never sent the SSE event), so they appeared in repl but not in the web UI. The thread_id was only populated when channel was explicitly "gateway". Now it is always populated from notify_thread_id metadata, so broadcast_all delivers to the gateway correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(gateway): return error instead of silently dropping messages Gateway broadcast() and respond() previously returned Ok(()) when thread_id was missing, silently swallowing the message. Callers (message tool, agent loop) believed delivery succeeded when it didn't. Now returns ChannelError::MissingRoutingTarget so callers can detect and report the failure. Four regression tests verify the contract: respond/broadcast with and without thread_id. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: resolve rebase conflicts with staging Restore sandbox_readiness field removed by pre-rebase commits (staging still uses it). Update repl test to match staging's single-message behavior (no longer sends /quit). Add missing reasoning field to ToolCall in codex test. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tools): log error when routine conversation lookup fails The routine_history tool silently swallowed errors from get_or_create_routine_conversation, returning empty output without any diagnostic logging. Add tracing::warn so failures are visible in logs. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR #1650 review comments - E2E test: accept submitted/accepted as success states in job assertion - TimeTool: remove operation from required schema (defaults to "now") - jobs handler: log DB errors server-side, return generic message to client - routines handler: use read-only find_routine_conversation on GET - codex provider: reverse-map sanitized tool names so MCP tools resolve Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address zmanian review feedback on PR #1650 - MCP refresh token: fall back to legacy secret name (mcp_{name}_refresh_token) so existing users don't need to re-authenticate after the naming fix - Job worker: replace fragile messages.pop() with truncate-to-saved-count to avoid maintenance hazard if message flow changes - Document cost implications of max_iterations 10->25 default bump - Revert Cargo.toml dist profile change (thin LTO comment, codegen-units=16) as it's unrelated to this PR Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: resolve rebase conflicts and address new Copilot comments - Fix no_silent_drop tests for updated GatewayConfig (user_id moved to GatewayChannel::new second arg, user_tokens removed) - Fix handle_text_response param name (_reason_ctx -> reason_ctx) - Fix missing has_text_response field in test JobDelegate - Propagate row.get errors in find_routine_conversation instead of unwrap_or_default - Only fall back to legacy refresh token name on NotFound/Expired, propagate real errors (DB, decryption) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
fd41bdf4be |
fix(worker): treat empty LLM response after text output as completion (#1677)
* fix(worker): treat empty LLM response after text output as completion When a job's LLM produces a substantive text response (e.g., formatted results from a routine) and the next LLM call returns empty or errors, the worker now treats this as successful completion instead of continuing the loop until failure. Previously, empty responses always triggered TextAction::Continue, causing the loop to re-call the LLM. The LLM had nothing more to say, so the provider returned "Response contained no message or tool call (empty)". This made routine jobs that successfully produced results report as "failed". The fix adds a `has_text_response` flag to JobDelegate: - After any non-empty text response: flag is set - Empty text after flag is set: treated as completion - LLM errors (select_tools/respond_with_tools) after flag: treated as completion instead of propagating - Empty text before any output: still retries (rate-limit backoff) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(worker): restrict error swallowing to EmptyResponse variant only - Add LlmError::EmptyResponse variant for when LLM returns no content - Update nearai_chat and github_copilot providers to emit EmptyResponse instead of InvalidResponse for empty/no-choice responses - try_complete_on_error now only swallows EmptyResponse (not AuthFailed, ContextLengthExceeded, Http, Io, etc.) - Extract is_completion_eligible_error as testable pure function - Log mark_completed errors at warn level instead of silently dropping - Add EmptyResponse to retry and circuit breaker transient classifications - Rewrite test to exercise real classification logic against all variants Addresses review feedback from zmanian and gemini-code-assist. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(worker): extract mark_completed_or_warn helper to DRY completion logic Extract shared mark-completed + warn-on-failure pattern into a single helper method used by both try_complete_on_error and handle_text_response. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: j-bloggs <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
de5a1c7b0d |
fix(worker): replace script -qfc with pty-process for injection-safe PTY (#1678)
- Add pty-process crate (MIT, tokio async support) for PTY allocation - Spawn claude CLI with pty-process::Command::arg() chaining instead of building a shell string for script -qfc - Eliminates all shell injection surfaces: prompt, model, session_id are passed via execve, never interpreted by a shell - Keep stderr on separate pipe to prevent NDJSON parse breakage (pty-process attaches PTY to all fds by default) - Gate PTY behind #[cfg(unix)] with direct-spawn fallback for Windows CI - Read stdout from PTY master (implements tokio::io::AsyncRead) - Add regression tests: arg vector construction + PTY allocation Addresses review feedback from zmanian and gemini-code-assist. Co-authored-by: j-bloggs <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
9bb19a98f7 | fix(web): redact database error details from API responses (#1711) | ||
|
|
0b33ca9926 |
fix(oauth): tighten legacy state validation and fallback handling (#1701)
* fix(oauth): tighten legacy state validation and fallback handling * style: fix formatting * refactor: separate validation checks for clearer error messages |
||
|
|
9ba10eac35 |
fix(db): add tracing warn for naive timestamp fallback and improve parse_timestamp tests (#1700)
* fix(db): add tracing warn for naive timestamp fallback and improve parse_timestamp tests * style: fix formatting |
||
|
|
27e8d6f8dd | fix(wasm): use typed WASM schema as advertised schema when available (#1699) | ||
|
|
f49f368355 |
Clean up extension credentials on uninstall (#1718)
* Clean up extension credentials on uninstall * Address PR review feedback * Cover channel webhook secrets on uninstall * Harden tool secret cleanup detection |
||
|
|
8f8cb7f7b1 |
feat: DB-backed user management, admin secrets provisioning, and multi-tenant isolation (#1626)
* feat: complete multi-tenant isolation — per-user budgets, model selection, heartbeat cycling Finishes the remaining isolation work from phases 2–4 of #59: Phase 2 (DB scoping): Fix /status and /list commands to use _for_user DB variants instead of global queries that leaked cross-user job data. Phase 3 (Runtime isolation): Per-user workspace in routine engine's spawn_fire so lightweight routines run in the correct user context. Per-user daily cost tracking in CostGuard with configurable budget via MAX_COST_PER_USER_PER_DAY_CENTS. Multi-user heartbeat that cycles through all users with routines, auto-detected from GATEWAY_USER_TOKENS. Phase 4 (Provider/tools): Per-user model selection via preferred_model setting — looked up from SettingsStore on first iteration, threaded through ReasoningContext.model_override to CompletionRequest. Works with providers that support per-request model overrides (NearAI). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: use selected_model setting key to match /model command persistence The dispatcher was reading "preferred_model" but the /model command (merged from staging) persists to "selected_model". Since set_setting is already per-user scoped, using the same key makes /model work as the per-user model override in multi-tenant mode. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: heartbeat hygiene, /model multi-tenant guard, RigAdapter model override Three follow-up fixes for multi-tenant isolation: 1. Multi-user heartbeat now runs memory hygiene per user before each heartbeat check, matching single-user heartbeat behavior. 2. /model command in multi-tenant mode only persists to per-user settings (selected_model) without calling set_model() on the shared LlmProvider. The per-request model_override in the dispatcher reads from the same setting. Added multi_tenant flag to AgentConfig (auto-detected from GATEWAY_USER_TOKENS). 3. RigAdapter now supports per-request model overrides by injecting the model name into rig-core's additional_params. OpenAI/Anthropic/Ollama API servers use last-key-wins for duplicate JSON keys, so the override takes effect via serde's flatten serialization order. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review — cost model attribution, heartbeat concurrency, pruning Fixes from review comments on #1614: - Cost tracking now uses the override model name (not active_model_name) when a per-user model override is active, for accurate attribution. - Multi-user heartbeat runs per-user checks concurrently via JoinSet instead of sequentially, preventing one slow user from blocking others. - Per-user failure counts tracked independently; users exceeding max_failures are skipped (matching single-user semantics). - per_user_daily_cost HashMap pruned on day rollover to prevent unbounded growth in long-lived deployments. - Doc comment fixed: says "routines" not "active routines". Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: /status ownership, model persistence scoping, heartbeat robustness Addresses second round of PR review on #1614: - /status <job_id> DB path now validates job.user_id == requesting user before returning data (was missing ownership check, security fix). - persist_selected_model takes user_id param instead of owner_id, and skips .env/TOML writes in multi-tenant mode (these are shared global files). handle_system_command now receives user_id from caller. - JoinSet collection handles Err(JoinError) explicitly instead of silently dropping panicked tasks. - Notification forwarder extracts owner_id from response metadata in multi-tenant mode for per-user routing instead of broadcasting to the agent owner. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: cost pricing, fire_manual workspace, heartbeat concurrency cap Round 3 review fixes: - Cost tracking passes None for cost_per_token when model override is active, letting CostGuard look up pricing by model name instead of using the default provider's rates (serrrfirat). - fire_manual() now uses per-user workspace, matching spawn_fire() pattern (serrrfirat). - Removed MULTI_TENANT env var — multi-tenant mode is auto-detected solely from GATEWAY_USER_TOKENS presence (serrrfirat + Copilot). - Multi-user heartbeat capped at 8 concurrent tasks to avoid flooding the LLM provider (serrrfirat + Copilot). - Fixed inject_model_override doc comment accuracy (Copilot). - Added comment explaining multi-tenant notification routing priority (Copilot). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: user-scoped webhook endpoint for multi-tenant isolation Adds POST /api/webhooks/u/{user_id}/{path} — a user-scoped webhook endpoint that filters the routine lookup by user_id, preventing cross-user webhook triggering when paths collide. The existing /api/webhooks/{path} endpoint remains unchanged for backward compatibility in single-user deployments. Changes: - get_webhook_routine_by_path gains user_id: Option<&str> param - Both postgres and libsql implementations add AND user_id = ? filter when user_id is provided - New webhook_trigger_user_scoped_handler extracts (user_id, path) from URL and passes to shared fire_webhook_inner logic - Route registered on public router (webhooks are called by external services that can't send bearer tokens) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(db): add UserStore trait with users, api_tokens, invitations tables Foundation for DB-backed user management (#1605): - UserRecord, ApiTokenRecord, InvitationRecord types in db/mod.rs - UserStore sub-trait (17 methods) added to Database supertrait - PostgreSQL migration V14__users.sql (users, api_tokens, invitations) - libSQL schema + incremental migration V14 - Full implementations for both PgBackend (via Store delegation) and LibSqlBackend (direct SQL in libsql/users.rs) - authenticate_token JOINs api_tokens+users with active/non-revoked checks; has_any_users for bootstrap detection Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(web): DB-backed auth, user/token/invitation API handlers Adds the web gateway layer for DB-backed user management (#1605): Auth refactor: - CombinedAuthState wraps env-var tokens (MultiAuthState) + optional DbAuthenticator for DB-backed token lookup with LRU cache (60s TTL, 1024 max entries) - auth_middleware tries env-var tokens first, then DB fallback - From<MultiAuthState> impl for backward compatibility - main.rs wires with_db_auth when database is available API handlers (12 new endpoints): - /api/admin/users — CRUD: create, list, detail, update, suspend, activate - /api/tokens — create (returns plaintext once), list, revoke - /api/invitations — create, list, accept (creates user + first token) Token creation: 32 random bytes → hex plaintext, SHA-256 hash stored. Invitation accept: validates hash + pending + not expired, creates user record and first API token atomically. All test files updated for CombinedAuthState type change. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: startup env-var user migration + UserStore integration tests Completes the DB-backed user management feature (#1605): - Startup migration: when GATEWAY_USER_TOKENS is set and the users table is empty, inserts env-var users + hashed tokens into DB. Logs deprecation notice when DB already has users. - hash_token made pub for reuse in migration code. - 10 integration tests for UserStore (libsql file-backed): - has_any_users bootstrap detection - create/get/get_by_email/list/update user lifecycle - token create → authenticate → revoke → reject cycle - suspended user tokens rejected - wrong-user token revoke returns false - invitation create → accept → user created - record_login and record_token_usage timestamps - libSQL migration: removed FK constraints from V14 (incompatible with execute_batch inside transactions). Tables in both base SCHEMA and incremental migration for fresh and existing databases. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: remove GATEWAY_USER_TOKENS, fix review feedback GATEWAY_USER_TOKENS never went to production — replaced entirely by DB-backed user management via /api/admin/users and /api/tokens. Removed: - UserTokenConfig struct and GATEWAY_USER_TOKENS env var parsing - user_tokens field from GatewayConfig - GatewayChannel::new_multi_auth() constructor - Env-var user migration block in main.rs (~90 lines) - multi_tenant auto-detection from GATEWAY_USER_TOKENS (now runtime via db.has_any_users() in app.rs) Review fixes (zmanian): - User ID generation: UUID instead of display-name derivation (#1) - Invitation accept moved to public router (no auth needed) (#3) - libSQL get_invitation_by_hash aligned with postgres: filters status='pending' AND expires_at > now (#4) - UUID parse: returns DatabaseError::Serialization instead of unwrap_or_default (#7) - PostgreSQL SELECT * replaced with explicit column lists (#8) - Sort order aligned (both backends use DESC) (#6) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: add role-based access control (admin/member) Adds a `role` field (admin|member) to user management: Schema: - `role TEXT NOT NULL DEFAULT 'member'` added to users table in both PostgreSQL V14 migration and libSQL schema/incremental migration - UserRecord gains `role: String` field - UserIdentity gains `role: String` field, populated from DB in DbAuthenticator and defaulting to "admin" for single-user mode Access control: - AdminUser extractor: returns 403 Forbidden if role != "admin" - /api/admin/users/* handlers: require AdminUser (create, list, detail, update, suspend, activate) - POST /api/invitations: requires AdminUser (only admins can invite) - User creation accepts optional "role" param (defaults to "member") - Invitation acceptance creates users with "member" role Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(web): add Users admin tab to web UI Adds a Users tab to the web gateway UI for managing users, tokens, and roles without needing direct API calls. Features: - User list table with ID, name, email, role, status, created date - Create user form with display name, email, role selector - Suspend/activate actions per user - Create API token for any user (shows plaintext once with copy button) - Role badges (admin highlighted, member muted) - Non-admin users see "Admin access required" message - Keyboard shortcut: Cmd/Ctrl+5 switches to Users tab CSS: - Reuses routines-table styles for the user list - Badge, token-display, btn-small, btn-danger, btn-primary components Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: move Users to Settings subtab, bootstrap admin user on first run - Moved Users from top-level tab to Settings sidebar subtab (under Skills, before Theme toggle) - On first startup with empty users table, automatically creates an admin user from GATEWAY_USER_ID config with a corresponding API token from GATEWAY_AUTH_TOKEN. This ensures the owner appears in the Users panel immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: user creation shows token, + Token works, no password save popup Three UI/UX fixes: 1. Create user now generates an initial API token and shows it in a copy-able banner instead of triggering the browser's password save dialog. Uses autocomplete="off" and type="text" for email field. 2. "+ Token" button works: exposed createTokenForUser/suspendUser/ activateUser on window for inline onclick handlers in dynamically generated table rows. Token creation uses showTokenBanner helper. 3. Admin token creation: POST /api/tokens now accepts optional "user_id" field when the requesting user is admin, allowing token creation for other users from the Users panel. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: use event delegation for user action buttons (CSP compliance) Inline onclick handlers are blocked by the Content-Security-Policy (script-src 'self' without 'unsafe-inline'). Switched to data-action attributes with a delegated click listener on the users table. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: add i18n for Users subtab, show login link on user creation - Added 'settings.users' i18n key for English and Chinese - Token banner now shows a full login link (domain/?token=xxx) with a Copy Link button, plus the raw token below - Login link works automatically via existing ?token= auto-auth Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: token hash mismatch — hash hex string, not raw bytes Critical auth bug: token creation hashed the raw 32 bytes (hasher.update(token_bytes)) but authentication hashed the hex-encoded string (hash_token(candidate) where candidate is the hex string the user sends). This meant newly created tokens could never authenticate. Fixed all 4 token creation sites (users, tokens, invitations create, invitations accept) to use hash_token(&plaintext_token) which hashes the hex string consistently with the auth lookup path. Removed now-unused sha2::Digest imports from handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: remove invitation system The invitation flow is redundant — admin create user already generates a token and shows a login link. Invitations add complexity without value until email integration exists. Removed: - InvitationRecord struct and 4 UserStore trait methods - invitations table from V14 migration (postgres + both libsql schemas) - PostgreSQL Store methods (create/get/accept/list invitations) - libSQL UserStore invitation methods + row_to_invitation helper - invitations.rs handler file (212 lines) - /api/invitations routes (create, list, accept) - test_invitation_lifecycle test Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: user deletion, self-service profile, per-user job limits, usage API Four multi-tenancy improvements: 1. User deletion cascade (DELETE /api/admin/users/{id}): Deletes user and all data across 11 user-scoped tables (settings, secrets, routines, memory, jobs, conversations, etc.). Admin only. 2. Self-service profile (GET/PATCH /api/profile): Users can read and update their own display_name and metadata without admin privileges. 3. Per-user job concurrency (MAX_JOBS_PER_USER env var): Scheduler checks active_jobs_for(user_id) before dispatch. Prevents one user from exhausting all job slots. 4. Usage reporting (GET /api/admin/usage?user_id=X&period=day|week|month): Aggregates LLM costs from llm_calls via agent_jobs.user_id. Returns per-user, per-model breakdown of calls, tokens, and cost. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: add TenantCtx for compile-time tenant isolation Implements zmanian's architectural proposal from #1614 review: two-tier scoped database access (TenantScope/AdminScope) so handler code cannot accidentally bypass tenant scoping. TenantScope (default): wraps user_id + Arc<dyn Database>, auto-binds user_id on every operation. ID-based lookups return None for cross- tenant resources. No escape hatch — forgetting to scope is a compile error. AdminScope (explicit opt-in): cross-tenant access for system-level components (heartbeat, routine engine, self-repair, scheduler, worker). TenantCtx bundles TenantScope + workspace + cost guard + per-user rate limiting. Constructed once per request in handle_message, threaded through all command handlers and ChatDelegate. Key changes: - New src/tenant.rs (~920 lines): TenantScope, AdminScope, TenantCtx, TenantRateState, TenantRateRegistry - All command handlers: user_id: &str → ctx: &TenantCtx - ChatDelegate: cost check/record/settings via self.tenant - System components: store field changed to AdminScope - Config: TENANT_MAX_LLM_CONCURRENT, TENANT_MAX_JOBS_CONCURRENT env vars - Fixes bug: /status <job_id> cross-tenant leak (now auto-filtered) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR #1626 review feedback — bounded LRU cache, admin auth, FK cleanup - Replace HashMap with lru::LruCache in DbAuthenticator so the token cache is hard-bounded at 1024 entries (evicts LRU, not just expired) - Gate admin user endpoints (list/detail/update/suspend/activate) with AdminUser extractor so members get 403 instead of full access - Add api_tokens to libSQL delete_user cleanup list to prevent orphaned tokens (libSQL has no FK cascade) - Add regression tests for all three fixes Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: update CA certificates in runtime Docker image Ensures the root certificate bundle is current so TLS handshakes to services like Supabase succeed on Railway. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: resolve CI failures — formatting, no-panics check - Run cargo fmt on test code - Replace .expect() with const NonZeroUsize in DbAuthenticator - Add // safety: comments for test-only code in multi_tenant.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: switch PostgreSQL TLS from rustls to native-tls rustls with rustls-native-certs fails TLS handshake on Railway's slim container (empty or stale root cert store). native-tls delegates to OpenSSL on Linux which handles system certs more reliably. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * Adding user management api * feat: admin secrets provisioning API + API documentation - Add PUT/GET/DELETE /api/admin/users/{id}/secrets/{name} endpoints for application backends to provision per-user secrets (AES-256-GCM encrypted) - Add secrets_store field to GatewayState with builder wiring - Create docs/USER_MANAGEMENT_API.md with full API spec covering users, secrets, tokens, profile, and usage endpoints - Update web gateway CLAUDE.md route table Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: add CatchPanicLayer to capture handler panics Without this, panics in async handlers silently drop the connection and the edge proxy returns a generic 503. Now panics are caught, logged, and returned as 500 with the panic message. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address second-round review — transactional delete, overflow, error logging - C1: Wrap PostgreSQL delete_user() in a transaction so partial cleanup can't leave users in a half-deleted state - M2: Add job_events to delete cleanup (both backends) — FK to agent_jobs without CASCADE would cause FK violation - H1/M4: Cap expires_in_days to 36500 before i64 cast (tokens + secrets) - H2: Validate target user exists before creating admin token to prevent orphan tokens on libSQL - H3: Log DB errors in DbAuthenticator::authenticate() instead of silently swallowing them as 401 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: revert to rustls with webpki-roots fallback for PostgreSQL TLS native-tls/OpenSSL caused silent crashes (segfaults in C code) during DB writes on Railway containers. Switch back to rustls but add webpki-roots as a fallback when system certs are missing, which was the original TLS handshake failure on slim container images. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: update Cargo.lock for rustls + webpki-roots Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * debug: add /api/debug/db-write endpoint to diagnose user insert failure Temporary diagnostic endpoint that tests DB INSERT to users table with full error logging. No auth required. Will be removed after debugging. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * perf: use cargo-chef in Dockerfile for dependency caching Splits the build into planner/deps/builder stages. Dependencies are only recompiled when Cargo.toml or Cargo.lock change. Source-only changes skip straight to the final build stage. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * debug: add tracing to users_create_handler Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: guard created_by FK in user creation handler The auth identity user_id (from owner_id scope) may not match any user row in the DB, causing a FK violation on the created_by column. Check that the referenced user exists before setting created_by. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: collapse GATEWAY_USER_ID into IRONCLAW_OWNER_ID Remove the separate GATEWAY_USER_ID config. The gateway now uses IRONCLAW_OWNER_ID (config.owner_id) directly for auth identity, bootstrap user creation, and workspace scoping. Previously, with_owner_scope() rebinds the auth identity to owner_id while keeping default_sender_id as the gateway user_id. This caused a FK constraint violation when creating users because the auth identity ("default") didn't match any user in the DB ("nearai"). Changes: - Remove GATEWAY_USER_ID env var and gateway_user_id from settings - Remove user_id field from GatewayConfig - Add owner_id parameter to GatewayChannel::new() - Remove with_owner_scope() method - Remove default_sender_id from GatewayState - Remove sender override logic in chat/approval handlers - Remove debug endpoint and tracing from prior debugging - Update all tests and E2E fixtures Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: hide Users tab for non-admins, remove auth hint text - Fetch /api/profile after login and hide the Users settings tab when the user's role is not admin - Remove the "Enter the GATEWAY_AUTH_TOKEN" hint from the login page since tokens are now managed via the admin panel, not .env files Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review feedback (auth 503, token expiry, CORS PATCH) - DB auth errors now return 503 instead of 401 so outages are distinguishable from invalid tokens (serrrfirat H3) - Cap expires_in_days to 36500 before i64 cast to prevent negative duration from u64 overflow (serrrfirat H1) - Add PATCH to CORS allowed methods for profile/user update endpoints (Copilot) - Stop leaking panic details in CatchPanicLayer response body Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: harden multi-tenant isolation — review fixes from #1614 - Add conversation ownership checks in TenantScope: add_conversation_message, touch_conversation, list_conversation_messages (+ paginated), update_conversation_metadata_field, get_conversation_metadata now return NotFound for conversations not owned by the tenant (cross-tenant data leak) - Fix multi-user heartbeat: clear notify_user_id per runner so notifications persist to the correct user, not the shared config target - Move hygiene tasks into bounded JoinSet instead of unbounded tokio::spawn - Revert send_notification to private visibility (only used within module) - Use effective_model_name() for cost attribution in dispatcher so providers that ignore per-request model overrides report the actual model used - Fix inject_model_override doc comment; add 3 unit tests - Fix heartbeat doc comment ("routines" not "active routines") Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: add Jobs, Cost, Last Active columns to admin Users table Add UserSummaryStats struct and user_summary_stats() batch query to the UserStore trait (both PostgreSQL and libSQL backends). The admin users list endpoint now fetches per-user aggregates (job count, total LLM spend, most recent activity) in a single query and includes them inline in the response. The frontend Users table displays three new columns. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review comments and CI formatting failures CI fixes: - cargo fmt fixes in cli/mod.rs and db/tls.rs Security/correctness (from Copilot + serrrfirat + pranavraja99 reviews): - Token create: reject expires_in_days > 36500 with 400 instead of silent clamp - Token create: return 404 when admin targets non-existent user - User create: map duplicate email constraint violations to 409 Conflict - User create: remove unnecessary DB roundtrip for created_by (use AdminUser directly) - DB auth: log warn on DB lookup failures instead of silently swallowing errors - libSQL: add FK constraints on users.created_by and api_tokens.user_id Config fixes: - agent.multi_tenant: resolve from AGENT_MULTI_TENANT env var instead of hardcoding false - heartbeat.multi_tenant: fix doc comment to match actual env-var-based behavior UI fix: - showTokenBanner: pass correct title ("Token created!" vs "User created!") Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address remaining review comments (round 2) - Secrets handlers: normalize name to lowercase before store operations, validate target user_id exists (returns 404 if not found) - libSQL: propagate cost parsing errors instead of unwrap_or_default() in both user_usage_stats and user_summary_stats - users_list_handler: propagate user_summary_stats DB errors (was silently swallowed with unwrap_or_default) - loadUsers: distinguish 401/403 (admin required) from other errors - Docs: fix users.id type (TEXT not UUID), remove "invitation flow" from V14 migration comment Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: i18n for Users tab, atomic user+token creation, transactional delete_user i18n: - Add 31 translation keys for all Users tab strings (en + zh-CN) - Wire data-i18n attributes on HTML elements (headings, buttons, inputs, table headers, empty state) - Replace all hard-coded strings in app.js with I18n.t() calls Atomic user+token creation: - Add create_user_with_token() to UserStore trait - PostgreSQL: wraps both INSERTs in conn.transaction() with auto-rollback - libSQL: wraps in explicit BEGIN/COMMIT with ROLLBACK on error - Handler uses single atomic call instead of two separate operations Transactional delete_user for libSQL: - Wrap multi-table DELETE cascade in BEGIN/COMMIT transaction - ROLLBACK on any error to prevent partial cleanup / inconsistent state - Matches the PostgreSQL implementation which already used transactions Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: revert V14 migration to match deployed checksum [skip-regression-check] Refinery checksums applied migrations — editing V14__users.sql after it was already applied causes deployment failures. Revert the cosmetic comment changes (added in df40b22f) to restore the original checksum. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: bootstrap onboarding flow for multi-tenant users The bootstrap greeting and workspace seeding only ran for the owner workspace at startup, so new users created via the admin API never received the welcome message or identity files (BOOTSTRAP.md, SOUL.md, AGENTS.md, USER.md, etc.). Three fixes: - tenant_ctx(): seed per-user workspace on first creation via seed_if_empty(), which writes identity files and sets bootstrap_pending when the workspace is truly fresh - handle_message(): check take_bootstrap_pending() on the tenant workspace (not the owner workspace) and persist the greeting to the user's own assistant conversation + broadcast via SSE - WorkspacePool: seed new per-user workspaces in the web gateway so memory tools also see identity files immediately The existing single-user bootstrap in Agent::run() is preserved for non-multi-tenant deployments. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address remaining PR review comments (round 3) - Docs: fix metadata description from "merge patch" to "full replacement" - Secrets: reject expires_in_days > 36500 with 400 (was silently clamped) - libSQL: CAST(SUM(cost) AS TEXT) in user_usage_stats and user_summary_stats to prevent SQLite numeric coercion from crashing get_text() — this was the root cause of the Copilot "SUM returns numeric type" comments - Add 3 regression tests: user_summary_stats (empty + with data) and user_usage_stats (multi-model aggregation) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: add role change support for users (admin/member toggle) - Add update_user_role() to UserStore trait + both backends (PostgreSQL and libSQL) - Extend PATCH /api/admin/users/{id} to accept optional "role" field with validation (must be "admin" or "member") - Add "Make Admin" / "Make Member" toggle button in Users table actions - Add i18n keys for role change (en + zh-CN) - Update API docs to document the role field on PATCH - Fix test helpers to use fmt_ts() for timestamps (was using SQLite datetime('now') which produces incompatible format for string comparison) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: show live LLM spend in Users table instead of only DB-recorded costs [skip-regression-check] Chat turns record LLM cost in CostGuard (in-memory) but don't create agent_jobs/llm_calls DB rows — those are only written for background jobs. The Users table was querying only from DB, so it showed $0.00 for users who only chatted. Now supplements DB stats with CostGuard.daily_spend_for_user() — the same source displayed in the status bar token counter. Shows whichever is larger (DB historical total vs live daily spend). Also falls back to last_login_at for "Last Active" when no DB job activity exists. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: persist chat LLM calls to DB and fix usage stats query Two root causes for zero usage stats: 1. ChatDelegate only recorded LLM costs to CostGuard (in-memory) — never to the llm_calls DB table. Added DB persistence via TenantScope.record_llm_call() after each chat LLM call, with job_id=NULL and conversation_id=thread_id. 2. user_summary_stats query only joined agent_jobs→llm_calls, missing chat calls (which have job_id=NULL). Redesigned query to start from llm_calls and resolve user_id via COALESCE(agent_jobs.user_id, conversations.user_id) — covers both job and chat LLM calls. Both PostgreSQL and libSQL queries updated. TenantScope gets record_llm_call() method. Tests updated for new query semantics. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review comments — input validation, cost semantics, panic safety [skip-regression-check] - Validate display_name: trim whitespace, reject empty strings (create + update) - Validate metadata: must be a JSON object, return 400 if not (admin + profile) - secrets_list_handler: verify target user_id exists before listing - Cost display: use DB total directly (chat calls now persist to DB), remove confusing max(db,live) CostGuard fallback - CatchPanicLayer: truncate panic payload to 200 chars in log to limit potential sensitive data exposure Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address Copilot round 5 — docs, secrets consistency, token name, provider field [skip-regression-check] - Docs: users.id note updated to "typically UUID v4 strings (bootstrap admin may use a custom ID)" - secrets_list_handler: return 503 when DB store is None (was falling through to list secrets without user validation) - tokens_create: trim + reject empty token name (matching display_name pattern) - LlmCallRecord.provider: use llm_backend ("nearai","openai") instead of model_name() which returns the model identifier - user_summary_stats zero-LLM users: acceptable — handler already falls back to 0 cost and last_login_at for missing entries Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: DB auth returns 503 on outage, scheduler counts only blocking jobs From serrrfirat review: - DB auth: return Err(()) on database errors so middleware returns 503 instead of silently returning Ok(None) → 401 (auth miss) - Scheduler: add parallel_blocking_count_for() that uses is_parallel_blocking() (Pending/InProgress/Stuck) instead of is_active() for per-user concurrency — Completed/Submitted jobs no longer count against MAX_JOBS_PER_USER From Copilot: - CLAUDE.md: fix secrets route paths from {id} to {user_id} - token_hash: use .as_slice() instead of .to_vec() to avoid heap allocation on every token auth/creation call Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: immediate auth cache invalidation on security-critical actions (zmanian review #6) Add DbAuthenticator::invalidate_user() that evicts all cached entries for a user. Called after: - Suspend user (immediate lockout, was 60s delay) - Activate user (immediate access restoration) - Role change (admin↔member takes effect immediately) - Token revocation (revoked token can't be reused from cache) The DbAuthenticator is shared (via Clone, which Arc-clones the cache) between the auth middleware and GatewayState, so handlers can evict entries from the same cache the middleware reads. Also from zmanian's review: - Items 1-5, 7-11 were already resolved in prior commits - Item 12 (String→enum for status/role) is deferred as a broader refactor Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: last-admin protection, usage stats for chat calls, UTF-8 safe panic truncation Last-admin protection: - Suspend, delete, and role-demotion of the last active admin now return 409 Conflict instead of succeeding and locking out the admin API - Helper is_last_admin() checks active admin count before destructive ops Usage stats: - user_usage_stats() now includes chat LLM calls (job_id=NULL) by joining via conversations.user_id, matching user_summary_stats() - Both PostgreSQL and libSQL queries updated Panic handler: - Use floor_char_boundary(200) instead of byte-index [..200] to prevent panic on multi-byte UTF-8 characters in panic messages Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: workspace seed race, bootstrap atomicity, email trim, secrets upsert response [skip-regression-check] - WorkspacePool: await seed_if_empty() synchronously after inserting into cache (drop lock first to avoid blocking), so callers see identity files immediately instead of racing a background task - Bootstrap admin: use create_user_with_token() for atomic user+token creation, matching the admin create endpoint - Email: trim whitespace, treat empty as None to prevent " " being stored and breaking uniqueness - Secrets PUT: report "updated" vs "created" based on prior existence - Last token_hash.to_vec() → .as_slice() in authenticate_token Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: disable unscoped webhook endpoint in multi-tenant mode [skip-regression-check] The original /api/webhooks/{path} endpoint looks up routines across all users. In multi-tenant mode, anyone who knows the webhook path + secret could trigger another user's routine. Now returns 410 Gone with a message pointing to the scoped endpoint /api/webhooks/u/{user_id}/{path}. Detection uses state.db_auth.is_some() — present only when DB-backed auth is enabled (multi-tenant). Single-user deployments are unaffected. From: standardtoaster review comment Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: webhook multi-tenant check, secrets error propagation, stale doc comment [skip-regression-check] - Webhook: use workspace_pool.is_some() instead of db_auth.is_some() for multi-tenant detection — db_auth is set for any DB deployment, workspace_pool is only set when has_any_users() was true at startup - Secrets: propagate exists() errors instead of unwrap_or(false) so backend outages surface as 500 rather than incorrect "created" status - Config: fix stale workspace_read_scopes comment referencing user_id Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
2f4eb08613 |
fix: sanitize tool error results before llm injection (#1639)
* fix: sanitize tool error results before llm injection Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]> * fix: wrap preflight tool rejection errors for llm safety Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]> * style: apply rustfmt to error-path regressions Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]> * fix: preserve wrapped tool errors in history replay * fix: address review findings on PR #1639 - Simplify legacy error handling in rebuild_chat_messages_from_db: remove redundant "Error: " prefix since legacy errors already contain descriptive text (e.g. "Tool 'http' failed: timeout"). Both wrapped (new) and plain (legacy) errors now pass through as-is. - Update existing test assertion to match simplified format. - Restore error-path doc line on process_tool_result. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: satisfy clippy on builder tool safety helper --------- Co-authored-by: Sisyphus <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
30db07c58e |
fix: require Feishu webhook authentication (#1638)
* fix: require Feishu webhook authentication * fix: handle Feishu v2 webhook token auth * fix: skip empty verification token write, consistent with app_id/app_secret Address zmanian review nit #4: only write verification_token to workspace when present, matching the if-let pattern used for app_id and app_secret. Functionally identical (the auth check filters empty strings), but consistent. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
7234700c78 |
fix(llm): prevent UTF-8 panic in line_bounds() (fixes #1669) (#1679)
* fix(llm): prevent UTF-8 panic in line_bounds() (fixes #1669) `line_bounds()` used `text[..pos]` slicing which panics when `pos` lands inside a multi-byte UTF-8 character. This happens when `end.saturating_sub(1)` in `is_recoverable_tool_call_segment()` steps back into a multi-byte char like emoji. Fix: clamp `pos` to `text.len()` and walk backward to the nearest char boundary before slicing. Add 5 regression tests covering mid-char positions, emoji boundaries, and out-of-bounds pos. Also fix pre-existing clippy `unnecessary_sort_by` warnings in web gateway handlers. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <[email protected]> Co-Authored-By: Happy <[email protected]> * test: assert expected values in line_bounds UTF-8 tests Address Gemini review: strengthen regression tests to verify correct return values (not just absence of panic) when pos lands mid-char. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <[email protected]> Co-Authored-By: Happy <[email protected]> --------- Co-authored-by: willamhou <[email protected]> Co-authored-by: Claude <[email protected]> Co-authored-by: Happy <[email protected]> |
||
|
|
9c5ba43ccd |
feat(gateway): add OpenAI Responses API endpoints (#1656)
* feat(gateway): add OpenAI Responses API endpoints
Add POST /v1/responses and GET /v1/responses/{id} to the web gateway,
implementing the OpenAI Responses API. Unlike the existing Chat
Completions proxy which passes through to the raw LLM, the Responses
API routes requests through the full agent loop — giving external
clients access to tools, memory, safety, and server-side conversation
state via a standard OpenAI-compatible interface.
Key design decisions:
- Response IDs encode thread UUIDs statelessly (resp_{uuid_simple})
- previous_response_id enables multi-turn conversations
- Streaming maps AppEvent variants to Responses API SSE events
- Tool approval returns response.failed (no interactive approval flow)
- GET endpoint reconstructs ResponseObject from conversation_messages
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(responses-api): address all review feedback on PR #1656
- Decouple response ID from thread ID: encode both a per-call
response_uuid and the thread_uuid so each POST produces a unique ID
- Reject unsupported fields (instructions, tools, tool_choice,
temperature, max_output_tokens, non-default model) with 400
- Add user_id to IncomingMessage metadata for user-scoped SSE events
- Add conversation_belongs_to_user() ownership check on GET endpoint
- Fix tool call parsing: handle both legacy array and object wrapper
format; use call_id/tool_call_id/id key fallback chain
- Correlate tool role messages to preceding FunctionCall call_id
- Stabilize created_at (capture once in accumulator, reuse everywhere)
- Surface error_message via new ResponseObject.error field
- Handle streaming tool failures (emit FunctionCallOutput on error)
- Remove dead Incomplete status variant
- Fix formatting (cargo fmt)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
|
||
|
|
45cd6682d3 |
fix: downgrade excessive debug logging in hot path (closes #1686) (#1694)
PR #1681 introduced 23 debug-level log statements across relay client, web server handlers, and extension manager functions. Many of these fire on every HTTP request or in loops (e.g. has_stored_team_id called per extension in list_installed). Downgrade them to trace level to reduce noise at the default debug log level while preserving warn/info logs for actionable diagnostics. Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
5b95d22218 |
Support direct hosted OAuth callbacks with proxy auth token (#1684)
* Support direct hosted OAuth callbacks with proxy auth token * Make OAuth env tests panic-safe * Preserve public OAuth field compatibility * Fix OAuth proxy token whitespace fallback |
||
|
|
dd0a0e10ab |
fix(routines): recover delete name after failed update fallback (#1108)
Co-authored-by: [email protected] <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
1d5777824c |
fix(mcp): handle 202 Accepted and wire session manager for Streamable HTTP (#1437)
* fix(mcp): handle 202 Accepted for Streamable HTTP notifications The MCP Streamable HTTP spec requires servers to respond with 202 Accepted (empty body) for JSON-RPC notifications like `notifications/initialized`. The HTTP transport tried to parse this empty body as JSON, which failed and broke the session handshake — subsequent requests like `tools/list` were rejected because the server considered the session uninitialized. Add an early return for 202 responses that produces an empty McpResponse without attempting body parsing. Fixes #1436 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(mcp): wire session manager into transport for non-OAuth HTTP clients The factory used McpClient::new_with_config().with_session_manager() which only set the session manager on the client, not on the HttpMcpTransport. The transport never captured Mcp-Session-Id from responses, so subsequent requests lacked the header and the server rejected them as uninitialized. Fix by constructing the HttpMcpTransport with the session manager before wrapping it in Arc, matching the pattern already used by new_authenticated(). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(mcp): deduplicate factory HTTP path, gate dead-code methods as test-only - Collapse the two identical non-OAuth HTTP branches in `create_client_from_config()` into one (early-return for the authenticated path, fall through for the common case). - Gate `McpClient::new_with_config()` and `McpClient::with_session_manager()` as `#[cfg(test)]` — the factory was their only production caller and no longer uses them. Both methods silently skip wiring the session manager into the transport, which was the root cause of #1436. - Add doc warnings on both methods explaining the footgun. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: [email protected] <[email protected]> |
||
|
|
adf4e25c8f |
fix(extensions): channel-relay auth dead-end, observability, and URL override (#1681)
* fix(extensions): channel-relay auth dead-end, add observability and relay URL override
Fix a bug where clicking Activate on the Slack relay extension produces
a dead-end "Authentication required" error with no OAuth URL. The root
cause: `auth_channel_relay()` used `is_relay_channel()` to check auth
status, but that function returns true as soon as the extension is
*installed* (in-memory set), before OAuth completes. This short-circuits
the OAuth flow so the authorization URL is never offered.
Changes:
1. **Bug fix** — `auth_channel_relay()` now uses `has_stored_team_id()`
which only checks the persistent settings store for an actual team_id.
The extension list `authenticated` field uses the same check so the UI
accurately reflects OAuth completion status.
2. **Observability** — Added debug/warn/info tracing to all channel-relay
code paths that were previously silent on failure:
- `activate_channel_relay`: team_id retrieval, relay config, signing
secret fetch, hot_add, cache operations
- `auth_channel_relay`: auth check, OAuth initiation, nonce storage
- `extensions_activate_handler`: request entry, auth fallback flow
- `slack_relay_oauth_callback_handler`: team_id persistence (was
silently ignored with `let _`)
- `RelayClient`: initiate_oauth, get_signing_secret, proxy_provider
all log URL, status, and errors
- `has_stored_team_id`: store read success/failure
3. **Per-extension relay URL override** — Users can now override the
CHANNEL_RELAY_URL via Settings > Extensions > Reconfigure. Stored
under `extensions.{name}.relay_url` in settings. Both auth and
activate read this override before falling back to the env default.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review feedback — clear relay_url override and improve log message
1. Allow clearing the relay_url override: when an optional setup field
with a setting_path is submitted empty, delete the stored setting so
the system reverts to the env/default value. Previously empty values
were silently skipped, making it impossible to undo an override from
the UI.
2. Improve the OAuth callback team_id persistence error log to be
self-contained without referencing implementation details.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: collapse nested if per clippy::collapsible_if
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix: address review feedback — security, scope consistency, and error handling
1. OAuth callback team_id persistence is now fatal: if set_setting fails,
the callback returns an error instead of proceeding to activate (which
would re-read from the store and fail anyway).
2. effective_relay_url uses owner scope (self.user_id) for reads, matching
configure() which writes under the same scope. Prevents multi-user
mismatch where an override saved via Reconfigure was invisible during
auth/activation.
3. has_stored_team_id uses owner scope for the same reason — the OAuth
callback stores team_id under state.owner_id (= self.user_id).
4. Security: effective_relay_url validates the override URL — only
http/https without embedded credentials (userinfo) is accepted. This
prevents API-key exfiltration if a user points relay_url at an
attacker-controlled host. Logs only host portion, not full URL.
5. Fixed effective_relay_url docstring to match behavior (returns Option,
callers handle the fallback).
6. get_setup_schema for ChannelRelay now logs a warning on settings store
errors instead of silently returning None.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
|
||
|
|
ed4d92932a | fix(agent): discard truncated tool calls when finish_reason == Length (#1631) (#1632) | ||
|
|
b3fbef5287 |
fix(llm): filter XML tool-call recovery by context (#1641)
* fix(llm): filter XML tool-call recovery by context * fix: address review comments on PR #1641 |
||
|
|
4c043bf057 |
feat: complete multi-tenant isolation — phases 2–4 (#1614)
* feat: complete multi-tenant isolation — per-user budgets, model selection, heartbeat cycling Finishes the remaining isolation work from phases 2–4 of #59: Phase 2 (DB scoping): Fix /status and /list commands to use _for_user DB variants instead of global queries that leaked cross-user job data. Phase 3 (Runtime isolation): Per-user workspace in routine engine's spawn_fire so lightweight routines run in the correct user context. Per-user daily cost tracking in CostGuard with configurable budget via MAX_COST_PER_USER_PER_DAY_CENTS. Multi-user heartbeat that cycles through all users with routines, auto-detected from GATEWAY_USER_TOKENS. Phase 4 (Provider/tools): Per-user model selection via preferred_model setting — looked up from SettingsStore on first iteration, threaded through ReasoningContext.model_override to CompletionRequest. Works with providers that support per-request model overrides (NearAI). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: use selected_model setting key to match /model command persistence The dispatcher was reading "preferred_model" but the /model command (merged from staging) persists to "selected_model". Since set_setting is already per-user scoped, using the same key makes /model work as the per-user model override in multi-tenant mode. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: heartbeat hygiene, /model multi-tenant guard, RigAdapter model override Three follow-up fixes for multi-tenant isolation: 1. Multi-user heartbeat now runs memory hygiene per user before each heartbeat check, matching single-user heartbeat behavior. 2. /model command in multi-tenant mode only persists to per-user settings (selected_model) without calling set_model() on the shared LlmProvider. The per-request model_override in the dispatcher reads from the same setting. Added multi_tenant flag to AgentConfig (auto-detected from GATEWAY_USER_TOKENS). 3. RigAdapter now supports per-request model overrides by injecting the model name into rig-core's additional_params. OpenAI/Anthropic/Ollama API servers use last-key-wins for duplicate JSON keys, so the override takes effect via serde's flatten serialization order. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review — cost model attribution, heartbeat concurrency, pruning Fixes from review comments on #1614: - Cost tracking now uses the override model name (not active_model_name) when a per-user model override is active, for accurate attribution. - Multi-user heartbeat runs per-user checks concurrently via JoinSet instead of sequentially, preventing one slow user from blocking others. - Per-user failure counts tracked independently; users exceeding max_failures are skipped (matching single-user semantics). - per_user_daily_cost HashMap pruned on day rollover to prevent unbounded growth in long-lived deployments. - Doc comment fixed: says "routines" not "active routines". Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: /status ownership, model persistence scoping, heartbeat robustness Addresses second round of PR review on #1614: - /status <job_id> DB path now validates job.user_id == requesting user before returning data (was missing ownership check, security fix). - persist_selected_model takes user_id param instead of owner_id, and skips .env/TOML writes in multi-tenant mode (these are shared global files). handle_system_command now receives user_id from caller. - JoinSet collection handles Err(JoinError) explicitly instead of silently dropping panicked tasks. - Notification forwarder extracts owner_id from response metadata in multi-tenant mode for per-user routing instead of broadcasting to the agent owner. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: cost pricing, fire_manual workspace, heartbeat concurrency cap Round 3 review fixes: - Cost tracking passes None for cost_per_token when model override is active, letting CostGuard look up pricing by model name instead of using the default provider's rates (serrrfirat). - fire_manual() now uses per-user workspace, matching spawn_fire() pattern (serrrfirat). - Removed MULTI_TENANT env var — multi-tenant mode is auto-detected solely from GATEWAY_USER_TOKENS presence (serrrfirat + Copilot). - Multi-user heartbeat capped at 8 concurrent tasks to avoid flooding the LLM provider (serrrfirat + Copilot). - Fixed inject_model_override doc comment accuracy (Copilot). - Added comment explaining multi-tenant notification routing priority (Copilot). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: user-scoped webhook endpoint for multi-tenant isolation Adds POST /api/webhooks/u/{user_id}/{path} — a user-scoped webhook endpoint that filters the routine lookup by user_id, preventing cross-user webhook triggering when paths collide. The existing /api/webhooks/{path} endpoint remains unchanged for backward compatibility in single-user deployments. Changes: - get_webhook_routine_by_path gains user_id: Option<&str> param - Both postgres and libsql implementations add AND user_id = ? filter when user_id is provided - New webhook_trigger_user_scoped_handler extracts (user_id, path) from URL and passes to shared fire_webhook_inner logic - Route registered on public router (webhooks are called by external services that can't send bearer tokens) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: add TenantCtx for compile-time tenant isolation Implements zmanian's architectural proposal from #1614 review: two-tier scoped database access (TenantScope/AdminScope) so handler code cannot accidentally bypass tenant scoping. TenantScope (default): wraps user_id + Arc<dyn Database>, auto-binds user_id on every operation. ID-based lookups return None for cross- tenant resources. No escape hatch — forgetting to scope is a compile error. AdminScope (explicit opt-in): cross-tenant access for system-level components (heartbeat, routine engine, self-repair, scheduler, worker). TenantCtx bundles TenantScope + workspace + cost guard + per-user rate limiting. Constructed once per request in handle_message, threaded through all command handlers and ChatDelegate. Key changes: - New src/tenant.rs (~920 lines): TenantScope, AdminScope, TenantCtx, TenantRateState, TenantRateRegistry - All command handlers: user_id: &str → ctx: &TenantCtx - ChatDelegate: cost check/record/settings via self.tenant - System components: store field changed to AdminScope - Config: TENANT_MAX_LLM_CONCURRENT, TENANT_MAX_JOBS_CONCURRENT env vars - Fixes bug: /status <job_id> cross-tenant leak (now auto-filtered) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
86d1143064 | Fix libsql prompt scope regressions (#1651) | ||
|
|
ab0ad948f3 |
Normalize cron schedules on routine create (#1648)
* Fix REPL single-message hang and cap CI test duration * Fix Clippy nested-if lint in REPL startup * Fix single-message approval flow * Handle empty single-message REPL exits * Wait for one-shot event routines before exit * Fix MCP lifecycle trace user scope * Normalize cron schedules on routine create |
||
|
|
0341fcc940 |
Fix REPL single-message hang and cap CI test duration (#1643)
* Fix REPL single-message hang and cap CI test duration * Fix Clippy nested-if lint in REPL startup * Fix single-message approval flow * Handle empty single-message REPL exits * Wait for one-shot event routines before exit |
||
|
|
41ed0a0f98 |
feat(agent): thread per-tool reasoning through provider, session, and all surfaces (#1513)
* feat(agent): thread per-tool reasoning from LLM through to REPL, HTTP, SSE, and DB Add end-to-end agent reasoning summaries so users can see *why* the agent chose specific tools, not just what it did. - Add `reasoning: Option<String>` to `ToolCall` (all providers) - Populate from LLM response content in `Reasoning::respond_with_tools` and `select_tools`, with per-tool override when providers supply it - Extend `Turn` with `narrative` and `TurnToolCall` with `rationale` + `tool_call_id` for identity-based result matching - Persist reasoning in DB via existing tool_calls JSON (no migration) - Add `StatusUpdate::ReasoningUpdate` and `SseEvent::ReasoningUpdate` + `SseEvent::JobReasoning` for real-time streaming - Emit reasoning events in both chat dispatcher and worker job path - Add `/reasoning [N|all]` command for inspecting turn reasoning - Surface `narrative` and `rationale` in HTTP `/api/chat/history` Based on the design from #361 and #456, reconstructed cleanly with Option<String> to minimize blast radius (vs mandatory String that broke compilation in #456). Closes #456 Co-Authored-By: panosAthDBX <[email protected]> Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review feedback from Gemini and Copilot - Fix `_ => Ok(None)` in agent_loop.rs to avoid accidental shutdown - Fix fallback in record_tool_result_for/record_tool_error_for to use first pending call instead of last_mut (parallel execution safety) - Include per-tool decisions in WASM channel reasoning messages - Apply truncate_at_tool_tags + clean_response to shared_reasoning in select_tools (parity with respond_with_tools) - Persist turn-level narrative to DB in tool_calls JSON wrapper - Parse both old (array) and new (object) tool_calls formats in build_turns_from_db_messages for backward compatibility - Populate reasoning from action.reasoning in execute_plan ToolCalls [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address second round of review comments + merge fixes - Add reasoning: None to new github_copilot.rs ToolCall sites (from staging merge) - Run cargo fmt on 4 files with formatting diffs - Truncate narrative to 1000 chars before DB persistence - Clone turn data and drop session lock in /reasoning command - Extract ToolDecisionDto::from_json_array shared helper (deduplicate worker/job.rs and orchestrator/api.rs) - Add unit tests for wrapped tool_calls JSON format with narrative [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address third round of review comments (Copilot + serrrfirat) - Reword ToolCall.reasoning docstring to reflect provider-supplied or fallback contract - Sanitize narrative through SafetyLayer before storage/emission - Clean per-tool reasoning via truncate_at_tool_tags + clean_response in select_tools (parity with shared reasoning) - Convert 4 approval-path recording sites in thread_ops.rs to identity-based record_tool_result_for/record_tool_error_for - Preserve tool_call_id and reasoning through restore_from_messages - Fix has_result/has_error to reject JSON null values - Truncate tool_call_id to 128 chars before DB persistence - Add 4 unit tests for record_tool_result_for/error_for edge cases Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address zmanian review — sanitize JobDelegate reasoning + warn on dropped results - Sanitize narrative and per-tool rationale through SafetyLayer in JobDelegate reasoning events (parity with ChatDelegate) - Add tracing::warn when record_tool_result_for/error_for drops a result because no matching or pending tool call exists - Add 3 unit tests for reasoning normalization (thinking tags, tool tags, empty-after-cleaning) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address 4 remaining unreplied review comments - Clean per-tool reasoning in respond_with_tools via truncate_at_tool_tags + clean_response (parity with select_tools) - Handle wrapped JSON format in rebuild_chat_messages_from_db so cold hydration works after persist_tool_calls format change - Update persist_tool_calls doc comment to describe new JSON shape - Sanitize per-tool rationale through SafetyLayer in ChatDelegate before emission and storage (parity with JobDelegate) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address zmanian review round 2 - Add tracing::debug on fallback-to-pending path in record_tool_result_for and record_tool_error_for (item 1) - Add comment explaining why /reasoning is special-cased in agent_loop.rs (item 4) - Items 2 (narrative persistence), 3 (rationale sanitization), and 5 (catch-all fix) were already addressed in prior commits Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: panosAthDBX <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
6daa2f155f |
fix: ensure LLM calls always end with user message (closes #763) (#1259)
* fix: ensure LLM calls always end with user message (closes #763) Claude 4.6 models (claude-sonnet-4-6, claude-opus-4-6) no longer support assistant message prefill — any LLM call where the conversation ends on an assistant message is rejected with HTTP 400 "This model does not support assistant message prefill". The same root cause also triggers NEAR AI's "No user query found in messages" 400 error for the routine engine path. Two fixes: 1. src/worker/container.rs — before_llm_call() After poll_and_inject_prompt(), if no user follow-up arrived and handle_text_response() left an assistant message at the end of the conversation, inject a sentinel "Continue." user message before the next LLM call. 2. src/agent/routine_engine.rs — execute_lightweight_with_tools() Before the force_text final completion call, ensure messages end with a user-role message. Tool result messages (Role::Tool) satisfy Anthropic but not NEAR AI; assistant messages satisfy neither. Also updates the worker system prompt to instruct the agent to include the phrase "The job is complete" in its final message, so the agentic loop can detect termination reliably. Tested with claude-sonnet-4-6 and claude-opus-4-6. Workaround: ANTHROPIC_MODEL=claude-sonnet-4-20250514 (still supports prefill). * fix: broaden sentinel guard to any non-user message (per review) Gemini suggested the Role::Assistant check in before_llm_call() is too specific. Changed to !Role::User to match the routine_engine.rs fix and cover tool results too. * fix: address zmanian review — JobDelegate sentinel, shared helper, NearAI complete() flattening - Extract ensure_ends_with_user_message() to src/util.rs with 4 unit tests (empty list, after assistant, after tool result, no-op when already user) - Add sentinel guard to JobDelegate::before_llm_call() in src/worker/job.rs so scheduler jobs (CreateJob / /job path) no longer hit Claude 4.6 / NEAR AI 400s - Replace inline guards in ContainerDelegate and routine_engine.rs with the shared helper — all 3 call sites now use one implementation - Fix complete() in nearai_chat.rs to apply flatten_tool_messages when flatten_tool_messages=true — previously only complete_with_tools() flattened, so force_text paths could still send role:"tool" messages to NEAR AI - Update stale comment in container.rs: "assistant message" → "non-user message" - Add flatten tests in nearai_chat.rs covering the complete() path Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * ci: fix fmt and tar advisory --------- Co-authored-by: Jacob Lasky <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Illia Polosukhin <[email protected]> Co-authored-by: firat.sertgoz <[email protected]> |
||
|
|
706c3a1b47 |
refactor: extract AppEvent to crates/ironclaw_common (#1615)
* refactor: extract AppEvent to crates/ironclaw_common SseEvent was defined in src/channels/web/types.rs but imported by 12+ modules across agent, orchestrator, worker, tools, and extensions — it had become the application-wide event protocol, not a web transport concern. Create crates/ironclaw_common as a shared workspace crate and move the enum there as AppEvent. Also move the truncate_preview utility which was similarly leaked from the web gateway into agent modules. - New crate: crates/ironclaw_common (AppEvent, truncate_preview) - Rename SseEvent → AppEvent, from_sse_event → from_app_event - web/types.rs re-exports AppEvent for internal gateway use - web/util.rs re-exports truncate_preview - Wire format unchanged (serde renames are on variants, not the enum) Aligned with the event bus direction on refactor/architectural-hardening where DomainEvent (≡ AppEvent) is wrapped in a SystemEvent envelope. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: add AppEvent::event_type() helper, deduplicate match blocks Address Gemini review: extract the variant→string match into a single method on AppEvent, replacing the duplicated 22-arm matches in sse.rs and types.rs. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: rename leftover sse vars/tests to match AppEvent rename Address Copilot review: rename sse_event vars to app_event in orchestrator/api.rs and ws.rs, rename test functions from test_ws_server_from_sse_* to test_ws_server_from_app_event_*, and update stale SSE comments. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: add Deserialize to AppEvent, round-trip test, fix stale comments Address zmanian review: - Add Deserialize derive to AppEvent so downstream consumers can deserialize incoming events - Add event_type_matches_serde_type_field test that round-trips every variant through serde and asserts event_type() matches the serialized "type" field — catches drift between serde renames and the manual match - Add round_trip_deserialize test for basic Serialize/Deserialize parity - Update remaining "SSE" references in comments across server.rs, manager.rs, ws_gateway_integration.rs, and worker/job.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
656151783c |
feat(cli): show credential auth status in tool info (#1572)
* feat(cli): show credential auth status in `tool info` `ironclaw tool info` now checks the secrets store and shows whether each required credential is configured or missing, consolidated into a single Auth section that deduplicates across http.credentials, auth, and setup.required_secrets. Secrets already shown in Auth are filtered from the Secrets section to avoid redundancy. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(cli): address review feedback on tool info auth status - Fix clippy collapsible-if by using `if let` + `&&` - Use HashMap<String, usize> for O(1) dedup instead of HashSet + linear scan - Add --user flag to `tool info` for checking non-default user credentials - Show "? unknown" on secrets store errors instead of silently reporting missing - Surface secrets store init failure via eprintln instead of silent .ok() - Sort auth entries by secret name for deterministic output Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(cli): only filter secrets when auth section renders, add regression test When the secrets store fails to initialize, the Auth section is not rendered. Previously, secret names were still filtered from the Secrets section, causing credential names to disappear entirely. Now secrets are only filtered when the Auth section will actually be displayed. Adds test verifying auth secret deduplication across auth, setup, and http.credentials sections, plus secrets store existence checks. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(cli): extract collect_auth_secrets helper, always render Auth section Address review feedback: - Extract dedup logic into `collect_auth_secrets()` so the test exercises the same code path as production (not a re-implementation) - Always render the Auth section when auth secrets exist, showing "? unknown" status when the secrets store is unavailable instead of hiding credential names entirely - Lazily init secrets store only when capabilities contain auth secrets, avoiding spurious warnings for tools with no auth - Add test for empty capabilities edge case Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style(cli): move HashMap/HashSet imports to top of file Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(cli): use correct tagged JSON format for credential location in test The CredentialLocationSchema uses serde tagged enum format ({"type": "bearer"}), not a bare string ("AuthorizationBearer"). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
82822d7b25 |
fix: restore owner-scoped gateway startup (#1625)
* fix: restore owner-scoped gateway startup * fix: split gateway owner and sender scope * fix: keep multi-user gateway sender identity * test: cover gateway sender scope regression * test: harden e2e startup teardown race * fix: align gateway owner scope across auth modes |
||
|
|
dcb2d89e3a |
Fix hosted OAuth refresh via proxy (#1602)
* Fix hosted OAuth refresh via proxy * Address OAuth refresh review feedback * Address new OAuth refresh review comments * Address additional OAuth refresh review feedback * Harden proxy exchange redirects |
||
|
|
f3da30a454 | perf(agent): optimize approval thread resolution (UUID parsing + lock contention) (#1592) | ||
|
|
5901451603 |
fix: remove stale stream_token gate from channel-relay activation (#1623)
* fix: remove stale stream_token gate from channel-relay activation The relay architecture now uses instance-scoped bearer auth + webhook callbacks, not streaming. The `relay:<name>:stream_token` secret was never written by the current OAuth flow, so activation always failed with AuthRequired. Replace stream_token with the team_id setting (already stored by the OAuth callback) as the persistent "auth completed" marker: - is_relay_channel(): check team_id setting instead of stream_token secret - activate_channel_relay(): gate on team_id emptiness, not stream_token - removal flow: delete team_id setting + oauth_state secret - configure(): return empty allowed-secrets set (relay is OAuth-only) - configure_token(): return AuthRequired (no manual token entry) - list(): surface activation_error for relay channels (was hardcoded None) - Clean up stale comments referencing stream_token / "stored token" - Update test to match OAuth-only model (no secrets to pass) Made-with: Cursor * fix: address CI and review feedback - Fix pre-existing tunnel/mod.rs test compilation (missing GatewayConfig fields: memory_layers, user_tokens, workspace_read_scopes) - Log warnings on failed team_id/oauth_state cleanup during removal instead of silently ignoring errors (gemini review) - Also delete legacy stream_token secret during removal for backward compatibility with pre-webhook installs (codex review) Made-with: Cursor |
||
|
|
d3d517fd67 |
fix(agent): case-insensitive channel match and user_id filter for event triggers (#1211)
* fix(agent): case-insensitive channel match and user_id filter for event triggers (#1051, #1076) Event-triggered routines had two bugs preventing them from firing: 1. Channel comparison was case-sensitive (e.g., "Telegram" != "telegram"), while emit_system_event already used eq_ignore_ascii_case. Fixed to match. 2. No user_id scoping — routines from any user were evaluated against every message. Added ownership check so routines only fire for their owner's messages. Also adds periodic event cache refresh (every ~60s) in the cron ticker so web/CLI mutations are picked up without requiring the tool path. Upgrades skip-reason logging from trace to debug for debuggability. Closes #1051 Refs #1076 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: correct refresh_every from 6 to 4 to match 15s default interval The default cron_check_interval_secs is 15s, not 10s. With refresh_every=6, the cache would refresh every 90s instead of the intended ~60s. Fix to 4 ticks (4 * 15s = 60s). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(agent): address #1211 review -- extract routine_matches_message, fix refresh interval Extract user/channel filter logic from check_event_triggers into a standalone pure function routine_matches_message(). Rewrite tests to call this function directly with controlled Routine and IncomingMessage values, so they exercise the real code path and would catch a revert. Add test_no_channel_filter_matches_any_channel for the None channel case. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * ci: re-trigger CI with latest changes Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: add missing IncomingMessage fields in test helper Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(agent): address review -- time-based refresh, trace-level user mismatch, scope guard (#1211) - Use tokio::time::Instant for cache refresh instead of tick counting - Downgrade user-mismatch log to trace to reduce noise - Add early return false for non-Event triggers in routine_matches_message - Fix doc comment to say 'user scope' instead of 'message sender' Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: run cargo fmt on agent_loop.rs https://claude.ai/code/session_01ABGWibdKVQ3b6pEKtxPPkM * fix(agent): resolve clippy warnings for unused binding and needless borrow Fix unused `content` variable in event trigger guard (use `content: _`) and remove redundant `&` on `message` which was already a reference. https://claude.ai/code/session_01PzBK21BbUAuZbrfLpoz4Xb * fix(test): update check_event_triggers call sites to new single-arg signature The staging merge brought e2e_routine_heartbeat tests that still used the old 3-argument check_event_triggers(user_id, channel, content) signature. Updated all 11 call sites to pass &IncomingMessage directly. [skip-regression-check] https://claude.ai/code/session_012GrkTDrtDFkpJos2hkgTcE * fix(agent): address review feedback on event trigger handling - Use post-hook content for event trigger matching so BeforeInbound hooks that rewrite input are respected - Set MissedTickBehavior::Skip on cron ticker to avoid burst catch-up after delays Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: cargo fmt https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7 --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: firat.sertgoz <[email protected]> |
||
|
|
01678be61d |
fix(routines): normalize status display across web and CLI (#1469)
* fix(routines): normalize status display across web and CLI surfaces (#1319) - Use Display (lowercase) instead of Debug (PascalCase) for RunStatus serialization in web handler - Update JavaScript status class mapping to match lowercase values from the API - Enrich CLI `routines list` to show running/attention states by querying last run status [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(routines): address review -- batch last-run query, consistent status, simplify ternary (#1319) - Parallelize last-run lookups with join_all to avoid N+1 sequential queries - Normalize status in /api/routines/{id}/runs handler to match lowercase convention - Remove redundant 'running' check in app.js runStatusClass logic Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(db): replace N+1 last-run-status queries with batch method The CLI routines list was firing a separate list_routine_runs query per routine to determine each one's last run status. For large routine sets this overwhelms the connection pool. Add batch_get_last_run_status to the Database trait with implementations for both PostgreSQL (DISTINCT ON + ORDER BY) and libSQL (correlated subquery + in-memory filter). Update the CLI to call the batch method once instead of N times. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: cargo fmt https://claude.ai/code/session_01Va9wwvATNWFAx35GG7Zek7 --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
fb3548956b |
fix(tunnel): managed tunnels target wrong port and die from SIGPIPE (#1093)
* fix(tunnel): target webhook server port instead of gateway port start_managed_tunnel() always used the gateway port (3000) for the tunnel target. Webhook routes live on the webhook server (HTTP_PORT, default 8080), not the gateway. The old code never read config.channels.http — no configuration could work around this. Extracts resolve_tunnel_target() with regression tests. * fix(tunnel): prevent SIGPIPE and fix default port fallback Two fixes for managed tunnel subprocess lifetime: 1. After extracting the public URL from stdout/stderr, the pipe reader was dropped (Rust ownership). The tunnel binary's next log write hit the closed pipe and got SIGPIPE — killing it silently. Fix: drain pipes in background tasks stored in TunnelProcess. Storing without reading isn't enough — the OS pipe buffer fills up and the process blocks instead. 2. When neither HTTP_PORT nor gateway is configured, the tunnel fell back to 127.0.0.1:3000. But the webhook server defaults to 0.0.0.0:8080 in this case. Now the tunnel matches that fallback. Affects ngrok (stdout), cloudflare (stderr), and custom (stdout). Tailscale uses a daemon and is not affected by SIGPIPE. * fix(tunnel): simplify drain loops and suppress CI false positives Simplify `while let Ok(Ok(Some(line)))` drain pattern to `while let Ok(Some(line))` — the extra Ok wrapper was unnecessary. Add `// safety: test-only` to assert_eq! lines in test module to suppress the "No panics in production code" CI check which greps the diff without understanding Rust's #[cfg(test)] module boundaries. --------- Co-authored-by: firat.sertgoz <[email protected]> |
||
|
|
5847479fd8 |
fix(agent): persist /model selection to .env, TOML, and DB (#1581)
* fix(agent): persist /model selection to .env, TOML, and DB The /model command only wrote selected_model to the DB and config.toml, but env vars from ~/.ironclaw/.env (e.g. NEARAI_MODEL) have the highest priority in LlmConfig::resolve_model(). The .env value was never updated, so it always shadowed the new model on restart. Now persist_selected_model updates all three persistence layers: 1. The backend-specific model env var in ~/.ironclaw/.env (only if the var already exists, to avoid injecting new vars) 2. The config.toml file (created if absent, since TOML > DB priority) 3. The DB settings table (for completeness) Also adds diagnostic logging when the DB store is unavailable. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(agent): address PR review — backend from deps, exact .env match Review feedback: - Use resolved llm_backend from AgentDeps instead of re-reading from disk/env (fixes DB-only backend detection, eliminates redundant I/O) - Match .env var with exact "KEY=" prefix and skip commented lines (prevents false matches on NEARAI_MODEL_VERSION etc.) - TOML is now loaded once (no double-read for backend + model update) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
3fdb187796 |
refactor(tools): auto-compact WASM tool schemas, add descriptions, improve credential prompts (#1525)
* fix(tools): add missing description, parameters, and improve credential prompts Silence three categories of startup warnings emitted by CapabilitiesFile::validate() and WasmToolLoader: 1. "description" field missing → add tool descriptions to all manifests 2. "parameters" field missing → add action-enum parameter schemas 3. Short credential prompts (<30 chars) → append source URLs Affects: github, gmail, google-calendar, google-docs, google-drive, google-sheets, google-slides, slack, telegram, llm-context, feishu. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(tools): auto-compact WASM tool schemas from module exports Replace the manual `parameters` field in capabilities JSON with automatic schema compaction. WasmToolSchemas::compact_schema() derives a compact advertised schema from the WASM module's schema() export by keeping only required and enum-constrained properties. The full schema remains available via tool_info(detail: "schema"). This eliminates: - The `parameters` field from CapabilitiesFile and all 11 sidecar JSONs - The "missing parameters" startup warning from the loader - Manual maintenance of duplicate schema data The `description` field in capabilities JSON is retained. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tests): remove cap_file.parameters reference in test_rig The parameters field was removed from CapabilitiesFile in the previous commit. Update test_rig.rs to match — schema is now auto-compacted from the WASM module export, no sidecar override needed. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tools): handle oneOf schemas in compact_schema, add tool name to warning Address PR review feedback: - compact_schema now collects properties from oneOf/anyOf/allOf variants, fixing GitHub-style schemas that have no top-level properties - Use HashSet for required lookup instead of Vec::contains - Add tool name to "Capabilities file not found" warning for consistency [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tools): merge oneOf const values into enum, cap property collection Address review feedback from @serrrfirat: 1. Merge const values across oneOf variants into a single enum array, so the LLM sees all valid actions (not just the first variant's const). 2. Cap property collection at 100 to bound allocations. 3. Also keep properties with const constraint (single-variant case). 4. Update doc comment to describe variant collection and design choices around variant-level required fields. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
b441ebec02 |
feat: multi-tenant auth with per-user workspace isolation (#1118)
* feat: multi-tenant auth with per-user scoping Multi-user authentication and authorization for IronClaw gateway: - Token-based auth mapping tokens to user IDs via GATEWAY_USER_TOKENS - Per-user SSE broadcast scoping - Per-user rate limiting with poisoned lock recovery - Handler auth and ownership checks for jobs, settings, routines - Extension secrets scoped per-user - Chat handlers use authenticated identity - Reverse proxy deployment documentation - Comprehensive integration tests for auth, SSE, rate limiting, and job isolation * fix: scope memory tools per-user in multi-tenant mode Memory tools (search, write, read, tree) held a single workspace created at startup with GATEWAY_USER_ID. In multi-tenant mode, all users' tool calls searched the default user's scope. Add WorkspaceResolver trait that resolves workspaces per-request using JobContext.user_id. In single-user mode, returns the startup workspace. In multi-tenant mode (GATEWAY_USER_TOKENS configured), creates and caches per-user workspaces on demand. Includes regression tests for workspace resolution and user isolation. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: comprehensive multi-tenant isolation audit Address all review findings from @serrrfirat plus 7 additional gaps found via full security audit: Reviewer findings (5): - WorkspacePool now applies search config, memory layers, embedding cache, identity read scopes, and global config scopes (was bare) - jobs_summary_handler uses per-user queries instead of global counters - jobs_prompt_handler restructured to not 404 agent jobs + ownership check - jobs_restart_handler agent branch now verifies user ownership - agent_job_summary_for_user added to Database trait + both backends Audit findings (7): - Delete dead handlers/memory.rs (stale copies with no auth) - Add AuthenticatedUser to logs_events, logs_level_get, logs_level_set - Add AuthenticatedUser to extensions_tools_handler, gateway_status_handler - Add auth + ownership checks to all 6 routines handlers - Add auth to all 4 skills handlers with audit logging on mutations - Scope extension setup SSE broadcast to user (broadcast_for_user) - Fix pre-existing test compilation errors in extensions/manager.rs 17 new multi-tenant isolation tests covering: - WorkspacePool config propagation and scope merging - Jobs handler per-user isolation (summary, restart, prompt, cancel) - Routines handler auth enforcement and cross-user rejection - Auth middleware enforcement on logs, skills, status endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: second-pass multi-tenant audit — scope SSE broadcasts, DB queries, dead handlers Second audit pass applying learned patterns across the codebase: - OAuth callback SSE broadcasts now use broadcast_for_user (lines 773, 912) - jobs_list_handler uses list_agent_jobs_for_user instead of fetching all users' jobs and filtering in Rust - list_agent_jobs_for_user added to Database trait + postgres + libsql - Dead handler files (extensions.rs, static_files.rs) hardened with AuthenticatedUser to prevent auth regression if migrated Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review findings — token hashing, broadcast scoping, error handling Security fixes: - Hash tokens with SHA-256 at construction time so authentication compares fixed-size 32-byte digests, eliminating length-oracle timing leaks - Scope auth SSE broadcasts per-user in chat_auth_token_handler — AuthRequired/AuthCompleted events were leaking across tenants - Propagate DB errors in restart handlers instead of silently swallowing via `if let Ok(Some(...))` pattern Code quality: - Log SSE serialization failures instead of silently producing empty strings via unwrap_or_default() - Remove dead `pub type AuthState = MultiAuthState` alias - Replace `.unwrap()` with `Arc::clone(db)` in app.rs multi-tenant workspace setup (db is guaranteed Some in context, but unwrap violates project convention) - Fix telegram setup test to inject UserIdentity into request extensions (handler now requires AuthenticatedUser) - Add safety comments on test-only expect/unwrap calls for CI - Apply cargo fmt to fix pre-existing formatting Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review findings — unify workspace pool, fix SSE regression, cache job owners - Unify WorkspacePool and PerUserWorkspaceResolver: WorkspacePool now implements WorkspaceResolver, eliminating duplicate per-user workspace construction logic. app.rs uses WorkspacePool directly. - Fix sse_tx: None scheduler regression: change scheduler/worker SSE broadcasting from broadcast::Sender<SseEvent> to Arc<SseManager>, restoring SSE event delivery for scheduled agent jobs. - Cache job owner in orchestrator: add job_owner_cache to OrchestratorState so job_event_handler avoids a DB round-trip on every event after the first per job. - Deduplicate ext_user_id computation in main.rs. - Remove unused _gateway_state variable. - Fix pre-existing test: first_token() returns None in multi-user mode by design; align test assertion. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: fix formatting in app.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: extract memory handlers back into handlers/memory.rs Move memory API handlers out of server.rs into their own module, consistent with how jobs, routines, and skills handlers are organized. The resolve_workspace() helper moves with them since it is only used by memory handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: [email protected] <[email protected]> |
||
|
|
fa51b9f52d |
fix: post-merge review sweep — 8 fixes across security, perf, and correctness (#1550)
* fix: post-merge review sweep — 8 fixes across security, perf, and correctness 1. Fix code fence detection in extract_suggestions() (issue #1180) - rfind("```") couldn't handle odd fence counts (unclosed blocks) - Now counts all fence positions and checks parity 2. Cache routine parameters_schema() with OnceLock (issue #1361) - routine_create_parameters_schema() and event_emit_parameters_schema() were regenerating JSON on every LLM call 3. Replace O(n) LRU eviction with lru crate (issue #1430) - Embedding cache now uses lru::LruCache for O(1) eviction - Removes manual HashMap + last_accessed tracking 4. Fix WASM router secret_validated semantics (issue #1281) - Now reflects whether any auth (secret/Ed25519/HMAC) was performed - Previously only checked if a secret was configured 5. Sanitize channel/user in routine prompt interpolation (issue #1364) - Defense-in-depth: strip newlines, replace backticks, truncate to 128 chars before injecting into LLM prompt 6. Remove duplicate 401 retry in github_copilot.rs (PR #1512 review) - Internal retry conflicted with outer RetryProvider causing nested retries; now invalidates token and lets RetryProvider handle retry 7. Fix token error classification in github_copilot.rs (PR #1512 review) - AccessDenied/Expired errors now map to AuthFailed (non-retryable) - Transient errors remain RequestFailed (retryable) 8. Fix parse_extra_headers() hardcoded env var name (PR #1512 review) - Error messages now report the actual env var being parsed instead of always saying LLM_EXTRA_HEADERS Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review comments and fix formatting - sanitize_prompt_field: single-pass with map() instead of collect+replace - embed(): re-check cache under lock before cloning (thundering herd) - embed_batch(): limit caching to cache capacity, skip overflow entries - router: thread did_authenticate bool instead of re-calling async methods - github_copilot 401: use generic error message, avoid leaking response body - cargo fmt: fix two formatting violations caught by CI Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * chore: trigger CI re-run with updated refs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
dea789cca9 |
Default new lightweight routines to tools-enabled (#1573)
* Default new lightweight routines to tools-enabled * Fix fmt and clippy on lightweight routine PR * Use grouped execution field in routine no-tools fixture * Align CLI routine defaults with tools-enabled lightweight mode |
||
|
|
485d1568c4 |
feat(cli): add ironclaw models subcommands (list/status/set/set-provider) (#1043)
* feat(cli): add ironclaw models subcommands (list/status/set/set-provider) Implements model management CLI (part of #83): - `models list [provider] [--verbose] [--json]` — list providers; fetches live model list from the provider API when a specific provider is given - `models status [--json]` — show active provider/model - `models set <model>` — set default model with validation - `models set-provider <id> [--model <name>]` — set provider with alias normalization - fix conflicts * fix(deps): update tar to 0.4.45 (RUSTSEC-2026-0067, RUSTSEC-2026-0068) --------- Co-authored-by: firat.sertgoz <[email protected]> |
||
|
|
d9358b0fa9 |
feat(workspace): multi-scope workspace reads (#1117)
* feat(workspace): multi-scope workspace reads Adds the ability for a workspace to read from multiple user scopes while keeping writes isolated to the primary scope. Configuration via WORKSPACE_READ_SCOPES env var (comma-separated user IDs). Includes identity file isolation (read_primary), multi-scope search, list, and read operations, WorkspaceConfig refactor, and comprehensive integration tests. * fix: address review feedback for multi-scope workspace reads - fix(memory): deduplicate timezone parsing for daily_log target parse_timezone was called twice when target was "daily_log" without a layer — once in path resolution, again in the fallback. Now computed once and reused. - fix(config): add character validation for WORKSPACE_READ_SCOPES and layer scopes — both enforce [a-zA-Z0-9_-] to prevent path traversal or injection via scope strings used as user_id in SQL queries. - fix(config): use chars().take(32) instead of byte-index slicing for scope length error messages (UTF-8 safety). - fix(error): remove unused WorkspaceError::NotFound variant Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: downgrade search log to debug, add comments on list iteration - Downgrade hybrid_search_multi tracing::info! to debug! — fires on every multi-scope search with the default backend, too noisy for info - Add comments explaining why list/list_all iterate per-scope instead of using _multi trait methods (identity path filtering needs scope attribution that merged results lose) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: [email protected] <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
4d7501a968 |
Fix owner-scoped message routing fallbacks (#1574)
* Fix owner-scoped message routing fallbacks * Address PR feedback on routing regressions * Address review notes on routing fallbacks |
||
|
|
7034e910c4 |
fix: generate Mistral-compatible 9-char alphanumeric tool call IDs (#1242)
* fix: generate Mistral-compatible 9-char alphanumeric tool call IDs
Mistral's API requires tool call IDs to match [a-zA-Z0-9]{9} exactly.
Previously, IDs like 'turn1_0', 'recovered_0', 'call_<uuid>', and
'generated_tool_call_N' were generated, which Mistral rejects with
HTTP 400.
Add generate_tool_call_id() that produces deterministic 9-char base-36
IDs from two seed values, and use it at all tool call ID generation
sites.
Fixes #1241
* Update src/llm/provider.rs
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
* fix: address review feedback on Mistral tool-call ID generation
- Remove .unwrap() in generate_tool_call_id (provider.rs) per zero-tolerance policy
- Remove .expect() in normalized_tool_call_id (rig_adapter.rs), use direct array indexing
- Replace magic constant 99 with named RECOVERED_TOOL_CALL_SEED in reasoning.rs
- Add tests for normalized_tool_call_id: passthrough, hashing, empty/whitespace, determinism
- Add comment explaining intentional use of turn_idx vs turn.turn_number in session.rs
- Fix duplicate `mod tests` block in provider.rs (pre-existing compile error)
- Update stale test assertions expecting old `generated_tool_call_` prefix format
[skip-regression-check]
* Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
---------
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
|
||
|
|
3e73dbe615 |
perf(tools): remove unconditional params clone in shared execution (fix #893) (#926)
* perf(tools): remove unconditional params clone in shared execution * Update src/tools/execute.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore(fmt): apply rustfmt in worker container tool execution * fix(tools): restore owned param call sites * fix(tools): pass normalized_params to tool.execute() instead of raw params The ownership refactor accidentally passed the un-coerced `params` to `tool.execute()` while validation ran against the coerced `normalized_params`. This meant tools received un-normalized input (e.g. stringified JSON arrays instead of actual arrays). Since `normalized_params` is owned and unused after the execute call, passing it directly achieves the original zero-clone goal without breaking parameter coercion. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(tools): update empty-tool-name test for owned params signature Adapts the test_execute_empty_tool_name_returns_not_found test (added on staging) to pass owned Value instead of &Value, matching the new execute_tool_with_safety signature. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]> Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |