Repository navigation
Conversation
…3329172268 chore: promote staging to staging-promote/89203225-23327092672 (2026-03-20 04:32 UTC)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
* feat: complete multi-tenant isolation — per-user budgets, model selection, heartbeat cycling Finishes the remaining isolation work from phases 2–4 of nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix: address PR review — cost model attribution, heartbeat concurrency, pruning Fixes from review comments on nearai#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) <noreply@anthropic.com> * fix: /status ownership, model persistence scoping, heartbeat robustness Addresses second round of PR review on nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * feat: add TenantCtx for compile-time tenant isolation Implements zmanian's architectural proposal from nearai#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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(llm): filter XML tool-call recovery by context * fix: address review comments on PR nearai#1641
Chore: Sync Main/Staging
…3610882140 chore: promote staging to staging-promote/ed4d9293-23582522167 (2026-03-26 18:18 UTC)
…override (nearai#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) <noreply@anthropic.com> * style: cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * style: collapse nested if per clippy::collapsible_if Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…3618091675 chore: promote staging to staging-promote/ed4d9293-23582522167 (2026-03-26 21:08 UTC)
…3582522167 chore: promote staging to staging-promote/4c043bf0-23573031775 (2026-03-26 07:26 UTC)
…3573031775 chore: promote staging to main (2026-03-26 01:33 UTC)
… HTTP (nearai#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 nearai#1436 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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 nearai#1436. - Add doc warnings on both methods explaining the footgun. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: ilblackdragon@gmail.com <ilblackdragon@gmail.com>
…ai#1108) Co-authored-by: ilblackdragon@gmail.com <ilblackdragon@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…3624475316 chore: promote staging to staging-promote/1d577782-23620402154 (2026-03-27 00:14 UTC)
…3620402154 chore: promote staging to main (2026-03-26 22:07 UTC)
Co-authored-by: ironclaw-ci[bot] <266877842+ironclaw-ci[bot]@users.noreply.github.com>
… (nearai#1694) PR nearai#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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* fix(responses-api): address all review feedback on PR nearai#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) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…earai#1679) * fix(llm): prevent UTF-8 panic in line_bounds() (fixes nearai#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 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * 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 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> --------- Co-authored-by: willamhou <willamhou@ceresman.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
* 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 nearai#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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sanitize tool error results before llm injection Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: wrap preflight tool rejection errors for llm safety Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * style: apply rustfmt to error-path regressions Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: preserve wrapped tool errors in history replay * fix: address review findings on PR nearai#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) <noreply@anthropic.com> * fix: satisfy clippy on builder tool safety helper --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…3637233594 chore: promote staging to staging-promote/7234700c-23635804857 (2026-03-27 08:13 UTC)
…i-tenant isolation (nearai#1626) * feat: complete multi-tenant isolation — per-user budgets, model selection, heartbeat cycling Finishes the remaining isolation work from phases 2–4 of nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix: address PR review — cost model attribution, heartbeat concurrency, pruning Fixes from review comments on nearai#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) <noreply@anthropic.com> * fix: /status ownership, model persistence scoping, heartbeat robustness Addresses second round of PR review on nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * feat(db): add UserStore trait with users, api_tokens, invitations tables Foundation for DB-backed user management (nearai#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) <noreply@anthropic.com> * feat(web): DB-backed auth, user/token/invitation API handlers Adds the web gateway layer for DB-backed user management (nearai#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) <noreply@anthropic.com> * feat: startup env-var user migration + UserStore integration tests Completes the DB-backed user management feature (nearai#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) <noreply@anthropic.com> * 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) (nearai#3) - libSQL get_invitation_by_hash aligned with postgres: filters status='pending' AND expires_at > now (nearai#4) - UUID parse: returns DatabaseError::Serialization instead of unwrap_or_default (nearai#7) - PostgreSQL SELECT * replaced with explicit column lists (nearai#8) - Sort order aligned (both backends use DESC) (nearai#6) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * feat: add TenantCtx for compile-time tenant isolation Implements zmanian's architectural proposal from nearai#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) <noreply@anthropic.com> * fix: address PR nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * chore: update Cargo.lock for rustls + webpki-roots Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * debug: add tracing to users_create_handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix: harden multi-tenant isolation — review fixes from nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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 df40b22) to restore the original checksum. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * fix: immediate auth cache invalidation on security-critical actions (zmanian review nearai#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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up extension credentials on uninstall * Address PR review feedback * Cover channel webhook secrets on uninstall * Harden tool secret cleanup detection
nearai#2366) * fix(security): harden approval thread safety (TOCTOU + error handling) Consolidates two security fixes for the approval processing flow in thread_ops.rs: **TOCTOU race (nearai#1486):** Hold session lock for the entire take-verify sequence in process_approval() so pending approval cannot be lost if a concurrent operation modifies the thread between take and restore. Previously, the lock was dropped after take_pending_approval() and re-acquired for request_id verification, creating a window where the approval could be permanently lost. **Silent error fallback (nearai#1487):** Replace 10 silent `if let Some(thread)` patterns with explicit `match` arms. Critical paths (state transitions, deferred approval setup) return errors when threads disappear. Non-critical paths (tool result recording, auth mode, rejection) log debug messages but continue. Regression tests: - test_approval_request_id_mismatch_restores_pending - test_approval_on_missing_thread_should_error Supersedes nearai#1591 (branch had no merge base with current staging). Closes nearai#1486, Closes nearai#1487 https://claude.ai/code/session_01X86EZxqXEFiU9VetyhPKjM * fix(security): prevent orphaned SSE events for dead threads Address review feedback: - handle_auth_intercept: return early when thread is gone instead of emitting auth-required SSE to a dead thread - process_auth_token: skip emit_auth_required_status when thread disappeared (both Ok retry and Err retry paths) Clients will no longer see auth prompts that can never resolve when the underlying thread has been deleted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Robert Yan <46699230+think-in-universe@users.noreply.github.com>
* docs(setup): warn about telegram open mode split identity * Update src/setup/channels.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * fix(setup): use idiomatic telegram mode check --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…earai#1852) (nearai#2315) * fix(ux): actionable error messages and improved CLI help for issue nearai#1852 AuthFailed errors now include provider-specific guidance (which env var to set, relevant URLs, and how to run `ironclaw onboard --step provider`). CLI help text improved across top-level, onboard, models, config, and doctor commands to help new users discover the setup wizard and provider configuration commands. `models set-provider` now warns when an API key is missing after switching providers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(llm): snapshot coverage for rendered AuthFailed messages per provider Addresses PR nearai#2315 review note: auth error text is now policy-bearing product guidance and warrants explicit coverage so future edits are deliberate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cli): use secrets-aware optional_env for API key check Addresses PR nearai#2315 review: API key warning now checks the secrets store overlay via optional_env() instead of raw std::env::var(), preventing false "API key required" warnings for users who stored keys via `ironclaw secrets`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…2222) * fix: more strict check for registry to avoid false positives By default ironclaw gets installed in ~/.cargo/bin/ironclaw, if the user happens to try to compile anything (e.g. their own tool) then the ~/.cargo/registry folder gets created, which makes ironclaw think that it found an (empty) registry, and thus stops being able to do list or install tools from its internal registry * fix: comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Guillermo Alejandro Gallardo Diez <gagdiez@iR2.local> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…top-level LlmConfig (nearai#1749) * refactor(llm): promote decorator chain settings from NearAiConfig to top-level LlmConfig * review: add env var override/fallback tests and update module spec --------- Co-authored-by: Firat Sertgoz <f@nuff.tech> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…#1201) Store a reqwest::Client in CustomTunnel and reuse it across health check calls instead of creating a new client on every invocation. This avoids repeated TLS/connection-pool setup overhead. CustomTunnel::new() now returns Result<Self> so the client builder error is propagated rather than silently falling back. Co-authored-by: willamhou <willamhou@ceresman.com>
…i#2460) ThreadConfig::default() had max_consecutive_errors: None, which serializes to null. The Python orchestrator's config.get(..., 5) returns None (not 5) when the key is present with a null value, so the guard `consecutive_action_errors >= max_consecutive_errors + 2` crashed with TypeError on the first action error. Fix both sides: default to Some(5) in Rust so the happy path sends a real int, and treat None as "no limit" in Python so callers can explicitly disable the guard without blowing up arithmetic.
…fer (nearai#2406) (nearai#2441) * fix(web): prevent browser crash from timer leaks, DOM growth, SSE buffer (nearai#2406) Extended sessions with heavy bot interactions caused Chrome's "Pages Unresponsive" dialog due to accumulated browser resources that were never cleaned up. Fixes: - Add cleanupConnectionState() to clear leaked setInterval/setTimeout timers on SSE reconnect, tab visibility change, and page unload - Cap DOM at 200 message nodes via pruneOldMessages() with streaming- aware pruning (skips data-streaming elements, called at turn boundaries and after loadHistory) - Cap jobEvents Map at 50 entries with LRU eviction (excludes current job from eviction scan) - Increase SSE broadcast buffer from 256 to 1024 (configurable via SSE_BROADCAST_BUFFER env var, with zero-guard to prevent panic) Closes nearai#2406 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): address PR nearai#2433 review — move SSE buffer to GatewayConfig, fix E2E timer test Move SSE_BROADCAST_BUFFER env var from direct std::env::var() in sse.rs to GatewayConfig in config/channels.rs, following the convention that all gateway env vars flow through structured config. Add MAX_BROADCAST_BUFFER (65,536) clamp to prevent OOM from misconfiguration. Fix E2E timer leak test to install setInterval monkey-patch via page.add_init_script() before navigation so initialization timers are tracked. Add test_dom_resource_limits.py to E2E CLAUDE.md scenario table. Add unit test for buffer config parsing, zero-rejection, and clamp. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): address review — clean gatewayStatusInterval, prune user msgs, use constants - Add gatewayStatusInterval to cleanupConnectionState() so it is cleared on reconnect/tab-hide/unload; add guard in startGatewayStatusPolling() to prevent double-start; restart polling on tab visibility restore - Call pruneOldMessages() after addMessage('user', ...) in sendMessage() so DOM stays bounded even during rapid user input - Replace hardcoded broadcast_buffer: 1024 with DEFAULT_BROADCAST_BUFFER in all test construction sites (5 occurrences across 4 files) - Document in from_sender() doc comment why broadcast_buffer is absent - Tighten E2E timer leak assertion from baseline+1 to baseline [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): address PR nearai#2441 review — prune/timer/assert/doc fixes - Remove pruneOldMessages() from loadHistory() pagination path to avoid immediately evicting just-prepended older messages - Move MAX_DOM_MESSAGES constant to top-level constants block - Add _loadThreadsTimer to cleanupConnectionState() for consistency - Add assert!(broadcast_buffer > 0) to SseManager constructor with panic doc (tokio broadcast channel requires capacity > 0) - Use Set-based interval tracking in E2E test to prevent counter underflow from double-clear - Update CLAUDE.md broadcast buffer docs (256 → 1024, SSE_BROADCAST_BUFFER) [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): remove assert! from SseManager to pass no-panics CI check Replace assert!(broadcast_buffer > 0) with a doc comment noting the precondition. GatewayConfig already rejects 0 at the config layer. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): address ilblackdragon review — correctness, e2e tests, docs (nearai#2406) Correctness: - pruneOldMessages: clean up orphaned leading time-separators after pruning - jobEvents LRU: replace O(n) scan with O(1) Map insertion-order eviction - Document degenerate all-streaming under-prune case Playwright e2e tests: - Tab hide/restore: no duplicate gateway status polling intervals - DOM cap + streaming: 260 elements prune to ≤200, streaming preserved, no orphan separators - jobEvents bounded: 60 jobs stay capped at ≤50 via LRU eviction - Fix assertion selector to match pruneOldMessages superset, tighten lower bound Rust: - Unit test: SseManager buffer size parameter actually controls lag behavior - Document MAX_BROADCAST_BUFFER memory impact (65K×100×200B ≈ 1.3 GB) - Move "capacity baked into tx" comment from from_sender to rebuild_state Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(web): protect currentJobId from LRU eviction in jobEvents map (nearai#2441) The O(1) LRU eviction skips the job that just received an event (moved to end via delete+set), but did not protect the job the user is actively viewing in the detail panel (currentJobId). If the user views a quiet job while 50+ other jobs fire events, the viewed job's events would be evicted and the activity tab would appear empty. Add a currentJobId guard to the eviction loop and a Playwright e2e test that verifies the actively-viewed job survives LRU pressure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(e2e): add real-flow Playwright tests for DOM resource limits (nearai#2406) Add 4 E2E tests that exercise pruning and timer cleanup through actual UI interactions (mock LLM round-trips, real SSE reconnects) instead of page.evaluate() injection. Also fix the existing timer leak test which failed due to execution context destruction from add_init_script. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: serrrfirat <f@nuff.tech>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ext injection (nearai#2167) * fix(responses-api): thread creation, GET by ID, streaming delta, context injection - Allow new threads from Responses API (metadata.source check) - Session manager adopts external UUID as internal thread ID - Skip duplicate delta when StreamChunks already delivered - x_context field for structured data injection (IronClaw extension) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address zmanian + serrrfirat review feedback 1. Replace spoofable metadata check with channel-based check: `message.channel == "gateway"` instead of `metadata["source"] == "responses_api"`. Channel names are server-set and unforgeable by WASM channels. 2. Reuse placeholder item ID in streaming worker: capture ID from acc.output[idx] instead of calling make_item_id() again. Fixes added→done ID mismatch that breaks client correlation. 3. Enforce 10 KB size limit on x_context to prevent context window exhaustion and DB bloat. Returns 400 if exceeded. 4. Log warning on UUID collision in create_thread_with_id. 5. Guard ext_uuid adoption: re-check under read lock that UUID isn't already mapped to another ThreadKey before adopting. 6. Update x_context doc: clarify alias collision risk, max size. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove accidentally committed integration test files * fix: address henrypark133 review — warn→debug, comment, unwrap 1. tracing::warn! → tracing::debug! in create_thread_with_id (warn corrupts TUI per project convention) 2. Comment fix: "Re-check under write lock" → "Check under read lock" (matches actual code which uses read().await) 3. Replace .unwrap_or_default() with .map().unwrap_or(0) to avoid no-unwrap policy violation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the /v1/responses endpoints (create, get) including streaming SSE events, structured context (x_context), and multi-turn conversation support via previous_response_id. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add google turorial * feat: update zh google tutorial * feat: update firewall rules * feat: update zh files
* Fix WASM channel owner_id fallback * ci: ignore rand advisory * ci: satisfy cargo-deny path dependency versions * fix(telegram): handle null/string owner_id and propagate to WASM config The bundled Telegram capabilities.json ships `"owner_id": null`. The previous code only called `Value::as_i64()`, which returns `None` for `Null`, so the fallback silently produced no owner — the fix never actually worked for Telegram. Changes: - Handle `Null`, `String`, and `Number` variants in `owner_actor_id_for_channel()` so the real production payload works. - Propagate the *resolved* owner_id into the WASM runtime config map regardless of whether it came from runtime config or capabilities fallback (previously only the runtime-config path injected it). - Add `tracing::debug!` for non-scalar owner_id values to aid debugging. - Add tests: null config, missing capabilities file, empty string, non-scalar value, and caller-level register_channel tests that verify config injection and null-owner-id handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: drop overlapping Cargo.toml and deny.toml changes per review Revert cosmetic Cargo.toml attribute reorder and deny.toml comment shortening that overlap with nearai#2370 already on staging, avoiding potential merge conflicts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: log debug warning for non-integer numeric owner_id in capabilities When as_i64() returns None for a numeric owner_id (e.g., 1.0), emit a debug log to aid debugging instead of silently returning None. Adds a regression test for the float case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: retrigger checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Zaki <zaki@iqlusion.io>
* fix(ci): exclude test files from PR size classification Test code shouldn't inflate PR size labels — a 1-line fix with 500 lines of tests was getting classified as XL instead of XS. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: retrigger with skip-regression-check label Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Zaki <zaki@iqlusion.io>
…rai#2056) (nearai#2401) The `rewrite_telegram_api_url_for_testing()`, `rewrite_http_url_for_testing()`, and their supporting constants/helpers were gated behind `#[cfg(any(test, debug_assertions))]`, which means they shipped in all debug builds — including development/staging deployments. An attacker who could set `IRONCLAW_TEST_TELEGRAM_API_BASE_URL` or `IRONCLAW_TEST_HTTP_REWRITE_MAP` environment variables on such a deployment could redirect Telegram API traffic (and other HTTP traffic) to an arbitrary host. Changes: - Narrow all test URL rewrite constants, functions, and helpers from `#[cfg(any(test, debug_assertions))]` to `#[cfg(test)]` - Add missing `#[cfg(test)]` to `TELEGRAM_TEST_API_BASE_ENV` (was ungated) - Wrap the call site in `http_request()` with `#[cfg(test)]`/`#[cfg(not(test))]` blocks so production builds use `logical_url` directly - Remove the now-unnecessary `#[cfg(not(...))]` stub functions that returned None Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(sandbox): try Docker socket before CLI binary check The sandbox detection checked `which docker` first and returned NotInstalled if the CLI binary was absent — even when the Docker daemon was reachable via a bind-mounted socket. This broke container-in-container deployments (e.g., Nomad shards with /var/run/docker.sock mounted) where bollard can talk to the daemon but no CLI is installed in the slim image. Reorder check_docker() to try connect_docker() (bollard socket ping) first. If the daemon responds, return Available immediately. The CLI check is now only used as a fallback for error-message quality when the socket connection fails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(sandbox): skip slow daemon ping when Docker is clearly absent check_docker() called connect_docker() before checking whether Docker was even present, causing a 120s bollard timeout on hosts with an unreachable DOCKER_HOST and no Docker installation. Add a fast-path that checks for the docker binary, DOCKER_HOST env var, and socket files on disk before attempting the daemon ping. This preserves DinD support (bind-mounted socket, no CLI binary) while avoiding the latency regression for non-Docker hosts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(sandbox): add regression tests for check_docker fast-path Extract should_skip_daemon_ping() predicate from check_docker() and add unit tests covering all combinations: skip when no binary, no DOCKER_HOST, and no socket (the bug scenario); no skip when any of the three signals is present (DinD socket, DOCKER_HOST, CLI binary). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(db): add per-user CachedSettingsStore decorator SettingsStore methods hit the database on every call. The v2 engine path (effect_adapter) and the dispatcher's per-turn tool permission loading both called get_all_settings() without caching, adding unnecessary DB round-trips on every agentic loop iteration. Add a write-through CachedSettingsStore decorator that caches get_all_settings() results per user_id. Write operations (set_setting, delete_setting, set_all_settings) delegate to the inner store then invalidate that user's cache entry. The write lock is held across DB loads to prevent stale-data races from concurrent invalidations. Wire the cache into TenantScope via a new settings_store field on AgentDeps, so all settings reads in the agent loop go through the cache. Remove the per-turn cached_tool_permissions Mutex hack from ChatDelegate that was working around the missing cache layer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback - Store Arc<HashMap> in cache instead of bare HashMap to avoid cloning the full settings map on every cache hit. get_setting/has_settings now only clone the single requested value or check emptiness through the Arc. - Route get_setting_with_admin_fallback() through self.settings() instead of self.inner so both the per-user and admin lookups go through the cache. - Update settings section comment to accurately describe which methods delegate through settings(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address all PR review feedback - Use `crate::db::` imports instead of `super::` (convention fix) - Add `wrap()` factory fn to CachedSettingsStore, simplify app.rs construction - Store `Arc<HashMap>` in cache to avoid full map clones on hits - Expose `invalidate_user()` and `flush()` public methods - Wire `flush()` into SIGHUP handler via concrete `settings_cache` on AppComponents - Wire `settings_store` into GatewayState and route all settings handlers through it so web UI writes invalidate the cache (critical fix) - Route `get_setting_with_admin_fallback()` through `self.settings()` - Add error-path test (FailingStore mock, cache not poisoned on error) - Add concurrent-access test (8 concurrent readers, inner store hit once) - Add TenantScope caller-level test (read/write through cache wiring) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: reuse resolve_settings_store() in settings_tools_set_handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address all review feedback on CachedSettingsStore - Add TTL (300s) and max-entries cap (1000) to bound cache staleness and memory growth. Entries expire after TTL; cache clears when cap exceeded. - Route admin tool_policy GET/PUT through resolve_settings_store() so writes invalidate the __admin__ cache entry. - Route settings_export_handler and settings_tools_list_handler through resolve_settings_store() (were bypassing cache on reads). - Wire invalidate_user() into users_delete_handler and users_suspend_handler so deleted/suspended users' settings are evicted. - Replace GatewayState.settings_store (trait object) with settings_cache (concrete CachedSettingsStore) — single field for both trait dispatch and cache management, no desync risk. - Add settings_override to ExtensionManager with with_settings_store() builder. All settings reads/writes in ExtensionManager now route through the cached store when available. - Make ExtensionManager::settings_store() pub(crate); update AuthManager to call it instead of database(), closing the auth descriptor cache bypass. - Remove unused wrap() method; merge redundant invalidate/invalidate_user. - Add tracing::debug on SIGHUP cache flush. - Expand module docs with design assumptions, known bypass paths, TTL and eviction semantics. - Add tests: expired_entry_triggers_reload, fresh_entry_does_not_reload, max_entries_cap_triggers_eviction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: collapse nested if into filter to satisfy clippy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use gateway owner_id for relay OAuth nonce storage The relay OAuth nonce was stored under the authenticated user's ID (a DB user UUID) but the callback handler looked it up under state.owner_id (the gateway owner, typically "default"). This mismatch caused the nonce lookup to silently fail, returning "Invalid or expired authorization" on every Slack OAuth callback. Use self.user_id (which holds config.owner_id) in auth_channel_relay for nonce storage so both sides use the same scope. Also adds tracing to the callback handler's get_decrypted error path to make future auth failures diagnosable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add test for nonce user scope mismatch Verifies that a nonce stored under a DB user UUID (different from the gateway owner_id) is not found by the callback handler, reproducing the bug that caused "Invalid or expired authorization" on hosted instances. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review comments - Also delete legacy caller-scoped nonce on upgrade (Copilot) - Include redacted state param in tracing log (Gemini) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): add Playwright persistence happy-path test Add `test_message_persists_across_page_reload` which validates the full persistence round-trip: send a message via the chat UI, reload the page (clearing all client-side state), switch back to the thread, and verify both user message and assistant response are restored from the database. Cross-checks via the history API that exactly one user turn exists with a completed response. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(e2e): address PR review comments - Replace fixed `wait_for_timeout(2000)` with polling via history API until the turn reaches `Completed` state (avoids CI flakiness) - Use `SEL["auth_screen"]` instead of hardcoded `"#auth-screen"` selector (follows project convention from helpers.py) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gagdiez
pushed a commit
that referenced
this pull request
Apr 17, 2026
* fix(channels): wire up pairing approval, polling restart, and onboarding state
The Telegram channel setup flow via the gateway was broken end-to-end.
Four interconnected bugs prevented pairing/ownership from completing:
1. pairing_approve_handler only wrote to channel_identities DB — the
running WasmChannel's owner_actor_id was never updated, so the
owner was never recognized and broadcast metadata was never stored.
2. refresh_active_channel() re-ran on_start() but never called
ensure_polling(), leaving polling in a stale state on repeated
tool_activate calls and causing Telegram 409 conflicts.
3. activate_wasm_channel() had a TOCTOU race on active_channel_names
that allowed duplicate polling loops, and hot_add() didn't await
old polling task termination.
4. onboarding_state was always None in extension API responses and
PairingRequired SSE was never emitted, so the frontend could
never render the pairing card.
Changes:
- approve_pairing (DB trait + both backends) now returns external_id
- WasmChannel.owner_actor_id wrapped in RwLock with set_owner_actor_id()
- ExtensionManager.complete_pairing_approval() orchestrates: persist
owner_id → update running channel → restart polling
- pairing_approve_handler calls complete_pairing_approval and emits
PairingCompleted SSE (scoped to approving user)
- refresh_active_channel() calls ensure_polling() and syncs owner
- Per-channel activation mutex prevents TOCTOU race
- hot_add() drops write lock before awaiting shutdown
- Extension list handlers populate onboarding_state when Pairing
- derive_onboarding() helper in handlers/extensions.rs
- Regression tests for derive_onboarding and resolve_message_scope
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(bridge): eliminate dual card + text emission for gate-paused flows
When the v2 engine hits a gate-paused state (approval needed, auth
required), the web gateway was sending BOTH an interactive card (via
send_status → SSE) AND a redundant text message (via AppEvent::Response).
Users saw a duplicate prompt.
Root cause: v2 bridge functions returned Ok(Some(text)) for gate-paused
outcomes, which mapped via from_legacy to HandleOutcome::Respond — sending
both the card and the text. The v1 path correctly used HandleOutcome::Pending.
Fix:
- Gate-paused paths in router.rs now return Ok(None) instead of text
- New bridge_to_outcome() checks has_any_pending_gate() after each v2
bridge call — if a gate exists, returns Pending (suppresses text + Done)
- New from_bridge() maps None → NoResponse (not Shutdown) for v2 paths
- Removed pending_gate_prompt_message() — the function that generated
the duplicate text
- notify_pending_gate() no longer emits GateRequired SSE directly
(redundant with send_pending_gate_status per-channel routing)
- Updated 3 tests to assert None return + StatusUpdate delivery
Each channel renders the approval/auth card natively via send_status:
web → SSE card, TUI → widget, relay → buttons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review comments
- bridge_to_outcome: only return Pending when handler returned None
(preserves legitimate text responses for ambiguous gate messages)
- process_emitted_messages: clone owner_actor_id out of read lock
before awaiting resolve_message_scope_with_pairing
- Normalize channel_name to lowercase in complete_pairing_approval
and pairing_approve_handler for consistent webhook/store lookups
- cargo fmt
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address self-review — BridgeOutcome enum, ExternalId newtype, pairing extraction
- Replace Option<String> bridge handler returns with typed BridgeOutcome
enum (Respond/NoResponse/Pending), eliminating post-hoc has_any_pending_gate
query and the None→NoResponse mapping that swallowed v2 shutdown signals
- Add ExternalId newtype for approve_pairing return (was bare String)
- Fix noop PairingStore::approve to return NotFound instead of Ok("")
- Extract pairing approval orchestration to src/pairing/approval.rs
- Clone RwLock<owner_actor_id> before awaiting in respond()
- Downgrade warn! to debug! in pairing handlers (TUI logging rule)
- Gate TELEGRAM_TEST_API_BASE_ENV const behind cfg(test/debug_assertions)
- Remove hardcoded Telegram auth instructions; use capabilities prompt
- Fix unused mut receiver in test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): remove dead Telegram verification flow, consolidate to generic pairing
The Telegram-specific verification challenge (/start CODE deep link flow)
blocked the generic pairing flow from ever running — configure() returned
early with activated:false when the challenge was pending, so the channel
never started polling and users couldn't generate pairing codes.
Removed ~1200 lines:
- TelegramBindingResult, TelegramBindingData, TelegramOwnerBindingState,
TelegramVerificationMeta, PendingTelegramVerificationChallenge types
- configure_telegram_binding, resolve_telegram_binding,
issue_telegram_verification_challenge, notify_telegram_owner_verified
and all Telegram API response types (getUpdates polling loop, etc.)
- ConfigureResult.verification field + VerificationChallenge re-export
- All verification-related test fixtures and 6 test functions
- Dead RecordingChannel test helper, unused set_channel_owner_id method
- Gated send_telegram_text_message + helpers behind cfg(test)
Replaced with:
- validate_telegram_token() — lightweight getMe call for token validation
+ bot_username extraction (persisted for mention detection)
- All channels now follow: credentials → validate → activate → pairing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): broadcast PairingRequired SSE after activation in pairing mode
After a channel activates with no owner binding, broadcast a per-user
PairingRequired SSE event so the web UI shows the pairing card without
requiring a manual refresh. Also populate pairing_required, onboarding_state,
and onboarding fields on ConfigureResult so callers know the channel
needs pairing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): don't persist auth instructions as turn response
When a tool triggers an auth gate (awaiting_token), the dispatcher
already sends an AuthRequired card and puts the thread in auth mode.
The thread_ops handler was then calling complete_turn(&instructions)
which overwrote auth mode back to Idle AND persisted the auth prompt
("Enter your Telegram Bot API token...") as the turn response — rendering
a redundant text bubble alongside the auth card.
Fix: skip complete_turn and persist_assistant_response for AuthPending.
The turn is paused (not complete), and the auth card is the only
user-facing signal. Tool calls are still persisted for history.
Also removes the now-unused `instructions` field from
AgenticLoopResult::AuthPending — the instructions were already sent
via the AuthRequired status event before AuthPending is returned.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): resume agent turn after auth + pairing completion
After the web UI submits a token via /api/chat/auth-token or approves
pairing via /api/pairing/{channel}/approve, the agent's turn was stuck
at Pending forever — these HTTP handlers configured the extension
directly but never signaled the agent loop to resume.
Fix: inject a follow-up message through msg_tx (the agent's message
channel) after successful auth/pairing. This uses the same pattern as
the OAuth callback handler — the LLM picks up the injected message,
sees the activation/pairing result, and produces a natural response.
The response goes through the full agent pipeline (hooks, safety,
history persistence, Done event).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): also resume agent turn on auth cancel
When the user dismisses the auth card, the frontend calls
/api/chat/auth-cancel which clears auth mode. But the original agent
turn was still paused at Pending with no Done event. The UI stayed
stuck at "Processing..." forever.
Fix: inject a cancellation message through msg_tx so the LLM can
acknowledge the cancellation and the turn completes naturally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(channels): pass thread_id in pairing approve for proper routing
The injected follow-up message after pairing approval had no thread_id,
causing the gateway to fail with "missing a routing target." The
response from the LLM was produced but couldn't be delivered.
Fix: add optional thread_id to PairingApproveRequest. The frontend
passes currentThreadId so the agent responds in the same conversation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): add Playwright tests for channel pairing flow
Covers:
- Auth-token/cancel handlers don't 500
- Pairing approve accepts optional thread_id field
- Backward compatibility: approve without thread_id works
- PairingRequired SSE shows pairing card
- PairingCompleted SSE dismisses pairing card
- Frontend sends currentThreadId in pairing approve request body
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(agent): transition thread to Idle on AuthPending
The AuthPending handler was not calling complete_turn() (to avoid
persisting redundant auth instructions as the response), but this
also skipped the ThreadState::Processing → Idle transition. The
thread stayed stuck in Processing forever, so the follow-up message
injected through msg_tx after auth/pairing was silently rejected.
Fix: explicitly set thread.state = Idle in both AuthPending arms
without calling complete_turn().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): remove dead verification challenge branch from telegram e2e
The Telegram verification challenge flow was removed — channels now
go straight to activation and use the generic pairing flow. The
conditional verification retry in setup_telegram() was dead code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review comments
- Gate TELEGRAM_TEST_API_BASE_ENV and telegram_api_base_url() behind
cfg(any(test, debug_assertions)) to prevent production env var override
(serrrfirat HIGH — ship blocker)
- Sanitize validate_telegram_token() error messages to avoid leaking bot
tokens via reqwest Display (Copilot)
- Log failed msg_tx sends instead of silently dropping (ilblackdragon)
- Forward thread_id in PairingCompleted SSE event (Copilot)
- Fix stale doc comment on persist_numeric_owner_id (Copilot)
- Hoist duplicate parse::<i64>() in propagate_approval (ilblackdragon)
- Delete dead _removed_telegram_verification_test (ilblackdragon)
- Fix always-passing E2E thread_id assertion (Copilot)
- Add V24 migration checksum to checksums.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: update PairingStore::approve doc for noop mode
The doc said "silently succeeds" but the implementation returns
NotFound when no database is configured.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sanitize extension names in agent prompts + live owner_actor_id in spawned tasks
Two hardening fixes from PR review deferrals:
1. Extension names from HTTP request bodies were interpolated directly into
format strings that become IncomingMessage content fed to the agent loop.
Add sanitize_extension_name() that strips non-alphanumeric chars and apply
it at the two prompt injection points in chat_auth_token_handler and
chat_auth_cancel_handler.
2. start_polling() and start_websocket_runtime() captured owner_actor_id as
an owned Option<String> at spawn time. After pairing approval, WebSocket
channels kept using the stale pre-approval value. Change to pass
Arc<RwLock<Option<String>>> so spawned tasks read the current owner on
each tick/event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review findings — TurnOutcome refactor, security hardening, WS parity
Structural changes:
- Replace Thread::complete_turn/fail_turn/interrupt with single
conclude_turn(TurnOutcome) that makes it impossible to forget the
turn state. Fixes AuthPending arms leaving Turn stuck at Processing.
- Add TurnOutcome::CompletedSilently for auth-card-only turns.
Security:
- Sanitize channel name in pairing_approve_handler (missed injection site)
- Fix bot token leak in validate_telegram_token — log safe fields
(is_timeout, is_connect, status) instead of reqwest error display
which includes the URL containing the token
- Consume stale fallback auth gate before replaying message to prevent
duplicate agentic runs on repeated OAuth callbacks
- Sanitize channel_name in derive_onboarding user-visible strings
- Add #[must_use] to BridgeOutcome enum
WS/REST parity:
- Add thread_id to WsClientMessage::AuthToken and AuthCancel
- WS AuthToken handler now injects follow-up message via msg_tx
(matching REST chat_auth_token_handler behavior)
- WS AuthCancel handler now clears engine pending auth and injects
cancellation message (matching REST chat_auth_cancel_handler)
Cleanup:
- Deduplicate build_runtime_config_updates (manager.rs imports from
approval.rs instead of maintaining its own copy)
- Downgrade info! to debug! for auto-generated secret log
- Downgrade warn! to debug! for OAuth fallback diagnostic
- Upgrade debug! to warn! for on_start failure in propagate_approval
- Rename misleading e2e test to match what it actually tests
- Add mixed-character truncation test for sanitize_extension_name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): add critical coverage for auth flow security and msg_tx injection
New e2e tests:
- test_auth_cancel_injects_follow_up_message_via_sse: verifies the msg_tx
injection path actually delivers messages end-to-end (SSE response event
appears after auth-cancel)
- test_sanitize_extension_name_in_auth_cancel: verifies injection characters
in extension_name are stripped before reaching the agent loop
- test_pairing_approve_sanitizes_channel_name: verifies channel path param
is sanitized in pairing approve handler
- test_ws_auth_token_accepts_thread_id: verifies WS auth_token messages
accept the new thread_id field
- test_ws_auth_cancel_accepts_thread_id: verifies WS auth_cancel messages
accept thread_id and connection stays alive
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: always inject follow-up message after auth token submission
When result.activated was false, the chat_auth_token_handler skipped
the msg_tx injection. This left the paused turn (Pending with Done
suppressed) permanently stuck — the UI showed "Running tool_install..."
forever.
Now both REST and WS handlers always:
1. Clear auth mode
2. Broadcast AuthCompleted (with success=true/false)
3. Inject a follow-up message via msg_tx
The message content varies based on activation status so the LLM
can respond appropriately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: revert hot_add to clone-then-shutdown to preserve message_tx receiver
The previous fix (drop write lock before shutdown) removed the channel
from the map before calling shutdown(). This dropped the last strong
Arc reference in the channel manager, killing the forwarding task's
receiver. The router holds its own Arc to the inner WasmChannel, so
propagate_approval's ensure_polling() could still send via message_tx
— but the receiver was dead, causing "channel closed" errors.
Revert to the staging pattern: read-lock to clone the Arc, drop the
lock, shutdown the clone, then write-lock to insert the replacement.
The old entry stays in the map (keeping the forwarding task alive)
until the insert atomically replaces it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: log bot_username set_setting failure instead of silently dropping
Copilot review: the set_setting result for bot_username was silently
dropped with `let _ =`. Now logs at debug level if the DB write fails,
giving visibility into mention detection degradation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: repair message_tx when Channel::start() fails at boot
When a WASM channel is loaded at boot without credentials (fresh DB),
on_start fails (e.g., Telegram deleteWebhook returns 404 with unresolved
{TELEGRAM_BOT_TOKEN}). Previously, message_tx was set BEFORE on_start,
so the sender survived but the receiver (rx) was dropped on error return.
Later, refresh_active_channel restarted polling which cloned the orphaned
sender — every send failed with "channel closed".
Fixes:
- Move message_tx creation AFTER on_start succeeds in Channel::start()
- Add WasmChannel::ensure_message_channel() that creates (tx, rx) if
message_tx is None or closed, returning the stream for forwarding
- refresh_active_channel calls ensure_message_channel() after on_start
succeeds and wires up a forwarding task if needed
Also:
- Revert hot_add to match staging exactly (no behavior change needed)
- Remove temporary debug logging (message_tx state before dispatch)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address remaining review comments — stale doc, websockets import
- Update AuthPending doc to reflect TurnOutcome::CompletedSilently
(was "turn NOT completed", now accurately describes conclude_turn)
- Move `import websockets` inside try block so ImportError is caught
by the except handler when the package isn't installed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review comments — propagate on_start error, dedupe helpers, tighten tests
- propagate_approval: propagate on_start() error as ActivationFailed
instead of swallowing it (zmanian review #1)
- router.rs: move test-only HashMap import into mod tests (zmanian nearai#2)
- chat.rs: remove duplicate clear_auth_mode (Copilot review #1)
- e2e: strengthen auth-token assertion to check status 200 + success
field, remove overlapping test_auth_cancel_returns_success (Copilot nearai#2/nearai#3)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address serrrfirat review — TOCTOU race, missing v2 auth clear, warn log
- ensure_message_channel: single write lock for atomic check-and-create
(fixes TOCTOU race where concurrent callers could orphan a forwarding task)
- chat_auth_token_handler: add missing clear_engine_pending_auth() call
(REST/WS parity — WS and REST cancel already had it, REST token did not)
- pairing_approve_handler: debug! → warn! for complete_pairing_approval
failure (operationally significant — channel won't route until restart)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(web,extensions): address review — sanitize agent messages, fix approve propagation, skip double Telegram getMe (nearai#2432)
- Sanitize result.message before interpolation into synthetic agent input
to prevent prompt injection via crafted validation errors (server.rs + ws.rs)
- Surface complete_pairing_approval() failure to frontend with success=false
SSE event and ActionResponse::fail instead of silently succeeding
- Return ActionResponse::ok when auth_url is present even if activated=false
so OAuth flows can progress through the frontend popup
- Skip generic validation_endpoint check for Telegram (validate_telegram_token
already calls getMe and extracts bot_username — avoids double API round-trip)
- Sanitize generic validation_endpoint error messages to avoid leaking
sensitive URL paths (e.g. bot tokens) via reqwest::Error Display
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Unify gateway onboarding and pairing flows
* Fix gateway message metadata scoping
* Clean up web gateway warnings
* Fix auth and onboarding regression fallout
* Fix gate resolution and pairing rollback trust boundaries
* Guard legacy agent loop from v2 submissions
* Fix PR review follow-ups for onboarding flow
* Fix CI clippy failure in pairing tests
* Fix onboarding review follow-ups
* Fix clippy warning in skills catalog
* Tighten pairing flow e2e assertions
* Fix onboarding auth review follow-ups
* Fix auth routing and tui clippy lint
* Fix pairing gate handoff in onboarding flow
* Fix clippy guard in mission event scan
* Fix merged clippy regressions
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
serrrfirat
pushed a commit
that referenced
this pull request
Apr 19, 2026
* feat(agent): queue and merge messages during active turns
Replace the hard rejection ("Turn in progress") when messages arrive
during an active turn with a bounded queue (max 10) that auto-drains
after the turn completes.
Queued messages are merged with newlines into a single turn so the LLM
receives full context from rapid consecutive inputs instead of producing
fragmented responses from partial context.
Key changes:
- Thread.pending_messages (VecDeque) with queue_message/drain_pending_messages
- Drain loop in agent_loop.rs merges all queued messages per iteration
- interrupt() and /clear both clear the pending queue
- MAX_PENDING_MESSAGES constant with cap enforced inside queue_message()
- Drain loop continues on soft errors, stops on NeedApproval/Interrupted
- Drain loop logs respond() failures instead of silently swallowing them
Fixes nearai#259 — debounces rapid inbound messages during processing
Fixes nearai#826 — drain loop is bounded by MAX_PENDING_MESSAGES cap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review — drain loop busy-loop guard and stale state re-check
- Add Ok(SubmissionResult::Ok) to drain loop break conditions to prevent
a tight busy-loop if process_user_input returns a queued-ack (e.g. from
a corrupted/hydrated session stuck in Processing state)
- Re-check thread.state under the mutable lock in the Processing arm to
guard against the turn completing between the snapshot read and the
queue operation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clear attachments on drain-loop queued message processing
Queued messages are text-only (queued as strings during Processing
state). The drain loop was reusing the original IncomingMessage
reference which carried the first message's attachments, causing
augment_with_attachments to incorrectly re-apply them to unrelated
queued text. Clone the message with cleared attachments for drain-loop
turns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review round 2 — stale state fallthrough and thread-not-found guard
- Processing arm: when re-checked state is no longer Processing, fall
through to normal processing instead of dropping user input
- Processing arm: return error when thread not found instead of false
"queued" ack
- Document intermediate drain-loop responses as best-effort for one-shot
channels (HttpChannel)
- Add regression tests for both edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback for message queue drain loop
[skip-regression-check] — test modifications present but hook has
SIGPIPE/pipefail false negative when awk exits early on match
- Replace wildcard match in drain loop with explicit `while let
Ok(Response)` guard — stops on Error variant too, preventing
confusing interleaved output after soft errors (review issue #1)
- Reject queueing messages with attachments during Processing state
instead of silently dropping them (review issue nearai#2)
- Document response routing limitation: all drain-loop responses
route via original message identity (review issue nearai#3)
- Document why SubmissionResult::Ok is correct for queued ack and
how it interacts with drain loop break condition (review issue nearai#4)
- Rewrite two dead regression tests to assert actual behavior:
thread-gone returns error, state-changed does not queue (review nearai#5)
- Document MAX_PENDING_MESSAGES=10 as acceptable for personal
assistant use case (review issue nearai#6)
- Fix misleading one-shot channel comment — HttpChannel consumes
sender on first call, subsequent calls are dropped (review issue nearai#8)
- Simplify drain loop intermediate response since while-let guard
guarantees Response variant
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add missing extension_manager field in webhook EngineContext
The fire_webhook method's EngineContext initializer was missing the
extension_manager field added in staging, causing CI compilation failure.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: gate TestRig::session_manager() behind libsql feature flag
The field is #[cfg(feature = "libsql")] so the accessor must match.
All callers are already inside #[cfg(feature = "libsql")] blocks.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: re-queue drained messages on drain loop failure
If process_user_input fails after drain_pending_messages() removed
all queued content, that user input was permanently lost. Now the
merged content is re-queued at the front of pending_messages on any
non-Response result so it will be processed on the next successful
turn.
Adds Thread::requeue_drained() helper and unit test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove unreachable!() from drain loop, add lock-drop comments
- Extract content binding in `while let` pattern instead of using a
separate match with unreachable!() — satisfies the no-panic-in-
production convention (zmanian review item #1)
- Add comment clarifying session lock is dropped at Processing arm
boundary before fall-through (zmanian review item nearai#5)
- Document bounded cap overshoot on requeue_drained (review item nearai#2)
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): validate queued messages and touch updated_at on queue ops
- Run safety validation, policy checks, and secret scanning on
messages before queueing during Processing state. Previously,
content with leaked secrets could be stored in pending_messages
and serialized without hitting the inbound scanner.
- Touch updated_at in queue_message(), drain_pending_messages(),
and requeue_drained() so thread timestamps reflect queue activity.
[skip-regression-check] — safety validation requires full Agent;
updated_at is a data-level fix on existing tested methods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
serrrfirat
added a commit
that referenced
this pull request
Apr 19, 2026
* feat(setup): prompt for local profile on first run * refactor: encapsulate DB config backup/restore into Settings helpers Extract backup_database_config() and restore_database_config() on Settings to replace inline field-by-field save/restore in the setup wizard. Cleaner interface, consistent with project encapsulation standards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(setup): address henrypark133 + zmanian review — reorder flow, remove backup/restore (nearai#2389) - Remove `DatabaseConfigBackup` struct and backup/restore methods; reorder quick-mode flow so profile selection runs before `auto_setup_database()`, letting the existing clone→try_load→merge_from pattern preserve wizard-chosen DB settings naturally (henrypark133). - Add comment explaining the cfg-gated `loaded` variable shadowing in `try_load_existing_settings` (zmanian #1). - Change catch-all `_ =>` to explicit `1 => ... _ => unreachable!()` in profile match arm (zmanian nearai#3). - Add caller-level test verifying profile application preserves DB config through the merge_from cycle (henrypark133 testing feedback). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
serrrfirat
pushed a commit
that referenced
this pull request
Apr 19, 2026
* feat(engine-v2): mount-backend abstraction for per-project sandbox (Phase 1) Adds the engine-side `MountBackend` trait + minimal `WorkspaceMounts` registry and a host-side bridge interceptor that routes sandbox-eligible tool calls (`file_read`, `file_write`, `list_dir`, `apply_patch`, `shell`) through a backend when their path argument starts with `/project/`. Default behavior is unchanged: until `EffectBridgeAdapter::set_workspace_mounts(Some(...))` is called (Phase 6), the interception path is dormant. This is the first phase of the per-project sandbox plan (`docs/plans/2026-04-10-engine-v2-sandbox.md`) and a deliberately small subset of the unified Workspace VFS proposed in nearai#1894 — just enough abstraction so the sandbox can be a `MountBackend` rather than a special case in the bridge. When nearai#1894's full mount table lands, the sandbox backend slots in unchanged. Engine crate (`crates/ironclaw_engine/src/workspace/`): - `mount.rs` — `MountBackend` trait, `MountError` (NotFound / InvalidPath / PermissionDenied / Io / Tool / Backend / Unsupported), `DirEntry`, `EntryKind`, `ShellOutput` - `filesystem.rs` — `FilesystemBackend`: passthrough host-fs implementation with two-layer path validation (lexical reject of absolute / `..`, then symlink-escape canonicalization). `read`/`write`/`list` fully implemented; `patch`/`shell` return `Unsupported` so the bridge falls through to the host tool until Phase 5 - `registry.rs` — `WorkspaceMounts` per-project registry with lazy `ProjectMountFactory`, longest-prefix-match resolution, cached and invalidatable Bridge (`src/bridge/sandbox/`): - `intercept.rs` — `maybe_intercept` and `SANDBOX_TOOL_NAMES`. Returns `Handled(json)` on a successful backend dispatch, `FellThrough` for non-sandbox tools, host paths, missing path params, or `Unsupported` backend ops - `effect_adapter.rs` — `workspace_mounts` field + `set_workspace_mounts` setter; interception block in `execute_action_internal` right before `execute_tool_with_safety`, gated on the optional mount table Tests (31 new): - 17 engine workspace unit tests covering trait error mapping, path safety (lexical + symlink), longest-prefix routing, and lazy factory caching - 9 bridge sandbox unit tests including `intercept_actually_dispatches_into_backend` (counting backend) which proves the interceptor reaches the backend - 5 integration tests in `tests/engine_v2_sandbox_integration.rs` driving `EffectBridgeAdapter::execute_action()` end-to-end per the "Test Through the Caller" rule (`.claude/rules/testing.md`), including a host-path-falls-through test that asserts the sandbox tempdir was not touched, and a `..`-escape test that verifies no `/etc/passwd` content leaks even after safety-layer redaction Drive-by: feature-gate two pre-existing dead-code helpers in `crates/ironclaw_skills/src/parser.rs` on `#[cfg(feature = "registry")]` to match their only call site, fixing a pre-existing clippy warning that blocked the workspace's `-D warnings` policy when `ironclaw_skills` is built with `default-features = false` (as the engine crate does). Verification: - `cargo fmt --check` clean - `cargo clippy --all --benches --tests --examples --all-features` zero warnings - 31 / 31 new tests passing; no existing tests broken Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(engine-v2): per-project sandbox — Phases 2–7 + live Docker e2e test Completes the per-project sandbox plan (docs/plans/2026-04-10-engine-v2-sandbox.md Phases 2–7), building on Phase 1's mount-backend abstraction (nearai#2211). Phase 2 — Project workspace folder: - `Project.workspace_path: Option<PathBuf>` field + `with_workspace_path()` - Host-side `project_workspace_path()`, `ensure_project_workspace_dir()` (creates `~/.ironclaw/projects/<id>/` mode 0700, idempotent) - `FilesystemMountFactory` taking a `ProjectPathResolver` closure (decoupled from `Store`); wired into `EffectBridgeAdapter` via `set_workspace_mounts()` Phase 3 — Standalone daemon binary: - `src/bin/sandbox_daemon.rs` — NDJSON over stdin/stdout, health/shutdown/execute_tool - Constructs ReadFileTool/WriteFileTool/ListDirTool/ApplyPatchTool/ShellTool with `base_dir=/project` (override via `IRONCLAW_SANDBOX_BASE_DIR`) Phase 4 — Dockerfile.sandbox: - Multi-stage build: rust-slim builder (+ python3 for pyo3) compiles sandbox_daemon; debian-slim runtime with tini PID 1, common build tools, `/project` mount target Phase 5 — ProjectSandboxManager + ContainerizedFilesystemBackend: - protocol.rs: Request/Response/RpcError matching daemon wire format - transport.rs: `SandboxTransport` trait (seam for testing without Docker) - containerized_backend.rs: `ContainerizedFilesystemBackend` impls `MountBackend`, translates relative→`/project/<rel>`, maps tool-error→MountError - docker_transport.rs: real bollard exec session, serialized Mutex, lazy reconnect - lifecycle.rs: deterministic `ironclaw-sandbox-<pid>` naming, ensure_running/stop/remove - manager.rs: `ProjectSandboxManager` per-project transport cache Phase 6 — Router gating on ENGINE_V2_SANDBOX: - `engine_v2_sandbox_enabled()` helper (truthy: 1/true/yes/on) - Router selects `ContainerizedMountFactory` when enabled + Docker reachable; falls back to `FilesystemMountFactory` with warning otherwise Live e2e bugs caught and fixed: - Shell without explicit `workdir` defaulted to host (not sandbox); fixed by defaulting to `/project/` in `extract_path_param` - `ContainerizedFilesystemBackend::shell` parsed `stdout`/`stderr` but host ShellTool returns merged `output` field; fixed with fallback key lookup - SANDBOX_TOOL_NAMES only had v2 names (`file_read`/`file_write`) but host registry uses v1 names (`read_file`/`write_file`); added both aliases Tests (62 sandbox-related, all green): - 27 bridge sandbox unit tests (intercept, workspace_path, factory, protocol, lifecycle, containerized_backend with ScriptedTransport mock) - 7 containerized-backend tests (including 2 regression tests for the shell bugs) - 5 engine v2 sandbox integration tests (EffectBridgeAdapter end-to-end) - 5 daemon binary smoke tests (real subprocess + NDJSON I/O) - 17 engine workspace unit tests - 1 live Docker e2e test: agent clones nearai/ironclaw into sandbox, renames to megaclaw via sed, verifies with grep — 70s, $0.09, recorded trace committed Verification: - `cargo fmt --check` clean - `cargo clippy --all --benches --tests --examples --all-features` zero warnings - All 62 sandbox tests passing; no existing tests broken Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace .expect() with Result in DockerTransport::ensure_session CI's no-panics checker flagged the .expect("just inserted") in production code. Replace with .ok_or_else() returning MountError::Backend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: multi-tenant project paths + unify sandbox env var with v1 Two issues addressed: 1. Project workspace paths now namespace by user_id: `~/.ironclaw/projects/<user_id>/<project_id>/` instead of `~/.ironclaw/projects/<project_id>/`. Prevents filesystem collisions in multi-tenant deployments where two users could theoretically have the same project UUID. 2. Sandbox enablement now reads `SANDBOX_ENABLED` (same env var as v1 sandbox) in addition to `ENGINE_V2_SANDBOX`. Either being truthy enables the per-project sandbox. This means a single flag governs sandbox behavior regardless of engine version, while the v2-specific override remains available for transitional setups. Tests: 30 bridge sandbox unit tests passing (added multi-tenant path tests + env var combination tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — TOCTOU race, shell env passthrough, canonicalize guard Three issues flagged by the code review bot on nearai#2211: 1. TOCTOU race in WorkspaceMounts::resolve (HIGH): Added double-checked locking — re-check the cache after acquiring the write lock so two threads racing on the same project's first access don't both call factory.build(). The second thread finds the insert from the first. 2. Shell intercept ignores env parameter (MEDIUM): The shell arm in maybe_intercept was passing HashMap::new() instead of forwarding the tool call's env map. Fixed to parse parameters["env"] and pass it through to backend.shell(). 3. Canonicalization fails when root doesn't exist (MEDIUM): When self.root hasn't been created yet (first write to a new project), canonicalize_under_root would walk up to a real ancestor and the starts_with check against the non-existent root would always fail. Now skips canonicalization entirely when root doesn't exist — lexical safety is already guaranteed by safe_join. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review round 2 — apply_patch schema, content validation, dir perms, docs - Fix apply_patch schema mismatch: MountBackend::patch now takes (old_string, new_string, replace_all) matching ApplyPatchTool's actual contract. Previously sent {patch: diff} which would fail with invalid_params in the containerized daemon. - Validate file_write content param: return error instead of silently writing empty string when content is missing. - Log stderr frames from sandbox daemon at debug! instead of silently discarding them in docker_transport StreamReader. - Tighten permissions on intermediate directories created by ensure_project_workspace_dir (projects/, <user_id>/) to 0o700, not just the leaf. - Fix stale module doc in sandbox/mod.rs (referenced "Phase 5 will add" but all phases shipped). - Fix doc path mismatch: workspace path is <user_id>/<project_id>/, not <project_id>/ (workspace_path.rs, CLAUDE.md, design plan). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review round 3 — symlink safety, visibility, debug logging - Close TOCTOU window in canonicalize_under_root: re-canonicalize and verify containment when the reassembled path exists on disk - Fix list_dir_recursive: use symlink_metadata (lstat) so symlinks are detected instead of followed; validate directories against root before recursive traversal - Tighten is_mountable_path to /project/, /memory/, /home/ prefixes instead of any absolute path (defense-in-depth) - Narrow sandbox module visibility to pub(crate) and remove unused pub use re-exports - Remove concrete types (FilesystemBackend, DirEntry, EntryKind, ShellOutput) from engine crate top-level re-exports; access via ironclaw_engine::workspace:: module path - Add debug! tracing to sandbox intercept routing decisions - Add read_file/write_file v1 aliases to daemon SUPPORTED_TOOLS health response - Remove developer-local path from sandbox mod.rs doc comment - Merge staging to fix CI (user_timezone field on ThreadExecutionContext) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review round 4 — safety validation, network isolation, binary writes - Add pre-intercept safety param validation so sandbox-dispatched calls go through the same checks as host-dispatched calls (#1) - Set network_mode: "none" on sandbox containers to prevent outbound network access (nearai#3) - Reject binary content in containerized write instead of silently corrupting via from_utf8_lossy (nearai#5) - Cap list_dir depth to 10 to prevent unbounded traversal (nearai#8) - Change container creation log from info! to debug! to avoid breaking REPL/TUI output (nearai#10) - Make is_truthy case-insensitive so SANDBOX_ENABLED=True works (nearai#11) - Return error instead of unwrap_or_default for missing container ID (nearai#12) - Propagate set_permissions errors instead of silently ignoring (nearai#13) - Return error for missing daemon output key instead of defaulting to empty object (nearai#14) - Add env mutex guard in sandbox_live_e2e test (nearai#15) - Fix rustfmt formatting for let-chain in canonicalize_under_root Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review round 5 — path traversal, error types, tests Security fixes: - Sanitize user_id in workspace path to prevent directory traversal via malicious user IDs containing `..` or `/` - Add Component::ParentDir check in ContainerizedFilesystemBackend::container_path matching the defense-in-depth approach of FilesystemBackend::safe_join Correctness: - Use MountError::Tool instead of MountError::InvalidPath for missing tool parameters (content, old_string, new_string) — fixes confusing LLM-visible error messages - Fix clippy sort_by_key suggestion in registry.rs Cleanup: - Remove spurious Notify import and dead _notify_link function New tests: - ContainerizedFilesystemBackend path traversal rejection (read + write) - container_path unit tests for safe and unsafe paths - Adversarial user_id test in workspace_path - Daemon-side path traversal test in sandbox_daemon_smoke Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review round 6 — param normalization, error types, edge cases - Normalize sandbox params via prepare_tool_params() before validation, matching the host execution path (fixes inconsistent validation) - Return ToolError::InvalidParameters instead of EngineError::Effect for sandbox param validation failures (consistent error surface) - ensure_dir checks path.is_dir() not path.exists() (rejects files) - Empty user_id returns "_anonymous" sentinel instead of empty hex string that would drop the tenant namespace via PathBuf::join("") - Restore ENGINE_V2_SANDBOX env var after sandbox live E2E test - Tighten is_mountable_path to /project/ only (no mounts for /memory/ or /home/ yet) - Add v1 tool name aliases (read_file, write_file) to SUPPORTED_TOOLS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: unify sandbox env var — remove ENGINE_V2_SANDBOX, use SANDBOX_ENABLED only Single env var controls sandboxing for both engine versions. The transitional ENGINE_V2_SANDBOX override is removed from code, tests, docs, and Dockerfile. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: double-checked locking in transport_for, explicit stdin close in smoke test - ProjectSandboxManager::transport_for no longer holds the mutex across the Docker ensure_running await. Uses double-checked locking so concurrent projects initialize in parallel. - sandbox_daemon_smoke: explicitly take() stdin before wait_with_output so EOF is sent even without a shutdown request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review — network mode, error types, race, protocol dedup - Change sandbox container network_mode from "none" to default bridge so git clone / cargo build / pip install work inside the container - Fix binary content rejection to use MountError::Tool instead of MountError::InvalidPath (semantic mismatch) - Fix list depth: use actual depth value instead of depth.max(1) - Fix orphan container race in transport_for by holding lock across container creation instead of double-checked locking - Deduplicate protocol types: daemon now imports from shared bridge::sandbox::protocol instead of defining its own copies - Make bridge::sandbox pub (narrow exposure: only protocol and workspace_path sub-modules are pub) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update plan doc — sandbox uses bridge networking, not network_mode=none Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gagdiez
pushed a commit
that referenced
this pull request
Apr 20, 2026
…easoning-augmented recall (nearai#2336) * feat(memory): configurable insights interval, session summary hook, reasoning-augmented recall Three memory enrichment features: 1. Configurable conversation insights interval via MISSION_INSIGHTS_INTERVAL env var (default: 5, min: 1) with MissionsConfig + MissionSettings wiring 2. SessionSummaryHook that writes LLM-generated conversation summaries to workspace daily logs on session end (fail-open, 30s timeout) 3. Optional reasoning parameter on memory_search that synthesizes raw chunks via cheap LLM before returning, controlled by SEARCH_REASONING_ENABLED Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(memory): address PR nearai#2336 review feedback and CI failures Critical fixes: - Use DB-first config system for MissionsConfig instead of raw std::env::var in router.rs (issue #1) - SessionSummaryHook now uses thread_ids from HookEvent::SessionEnd to summarize the correct conversation instead of guessing via recency; falls back to most-recent for backward compatibility (nearai#2) - Add per-user rate limiter (10/min, 60/hr) and 15s timeout on reasoning LLM calls in MemorySearchTool to prevent unbounded usage (nearai#3) Test coverage: - Caller-level tests for reasoning-augmented recall (LLM wiring, disabled config, and failure fallback paths) (nearai#4) - SessionSummaryHook LLM failure path test confirming fail-open behavior (nearai#5) - reasoning_enabled config field tests (default, env, DB override) (nearai#6) - MissionSettings and SearchSettings round-trip assertions in comprehensive_db_map_round_trip (nearai#11) Convention fixes: - Remove double env-var parsing in MissionsConfig::resolve (nearai#7) - Use ChatMessage::system()/user() constructors in SessionSummaryHook (nearai#8) - Add TODO comments for inline prompt strings (nearai#9) - Add timeout on reasoning LLM call (nearai#10) CI fixes: - Remove 4 stale wasmtime advisory entries from deny.toml - Add RUSTSEC-2026-0097 (rand 0.8.5) to advisory ignore list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(memory): address henrypark133 + ilblackdragon review — safety, concurrency, prompts (nearai#2336) - Move inline prompt templates to prompts/*.md per project convention (session_summary.md, memory_reasoning_synthesis.md) — resolves TODOs - Add Arc<Semaphore> to SessionSummaryHook to cap concurrent LLM calls on mass session expiry (follows OutboundWebhookHook pattern) - Sanitize LLM-generated summaries via ironclaw_safety::Sanitizer before writing to workspace (mitigates stored prompt injection vector) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(memory): CI compile fix + reasoning sanitizer parity + harden test - Add live_state / live_state_started_at fields to ConversationSummary literals in three session-summary test sites; staging added these fields after the branch was created and clippy/test builds were failing on missing-field errors. - Replace silent unwrap_or_default on MissionsConfig::resolve in bridge::router::init_engine with an explicit warn-and-default match, so a misconfigured MISSION_INSIGHTS_INTERVAL surfaces in logs instead of being absorbed into the default. - Run the reasoning-synthesis output through ironclaw_safety::Sanitizer before persisting it to the tool result, matching the parity already applied in SessionSummaryHook. Memory chunks fed into synthesis can carry attacker-controlled text and the synthesis flows back into future LLM contexts via memory_search results. - Strengthen reasoning_enabled_fires_llm_and_returns_synthesis: add a preflight assertion that FTS returns the seeded doc, then unconditionally assert the LLM was called once and that synthesis matches the mocked response. Removes the prior `if llm.calls() > 0` guard that made the synthesis assertions vacuous when search returned empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hot-reload the live LLM provider chain when web settings change.
This keeps the currently running daemon on the new backend/model without requiring a Docker-only restart path. The implementation uses a swappable provider wrapper plus a reload handle wired through the gateway settings handlers.
Validation:
Note: this PR is intentionally based on the current feature branch head so it stays isolated from the GWS extension work already in flight.