Commit Graph
35 Commits
Author SHA1 Message Date
8f8cb7f7b1 feat: DB-backed user management, admin secrets provisioning, and multi-tenant isolation (#1626)
* feat: complete multi-tenant isolation — per-user budgets, model selection, heartbeat cycling

Finishes the remaining isolation work from phases 2–4 of #59:

Phase 2 (DB scoping): Fix /status and /list commands to use _for_user
DB variants instead of global queries that leaked cross-user job data.

Phase 3 (Runtime isolation): Per-user workspace in routine engine's
spawn_fire so lightweight routines run in the correct user context.
Per-user daily cost tracking in CostGuard with configurable budget via
MAX_COST_PER_USER_PER_DAY_CENTS. Multi-user heartbeat that cycles
through all users with routines, auto-detected from GATEWAY_USER_TOKENS.

Phase 4 (Provider/tools): Per-user model selection via preferred_model
setting — looked up from SettingsStore on first iteration, threaded
through ReasoningContext.model_override to CompletionRequest. Works
with providers that support per-request model overrides (NearAI).

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

* fix: use selected_model setting key to match /model command persistence

The dispatcher was reading "preferred_model" but the /model command
(merged from staging) persists to "selected_model". Since set_setting
is already per-user scoped, using the same key makes /model work as
the per-user model override in multi-tenant mode.

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

* fix: heartbeat hygiene, /model multi-tenant guard, RigAdapter model override

Three follow-up fixes for multi-tenant isolation:

1. Multi-user heartbeat now runs memory hygiene per user before each
   heartbeat check, matching single-user heartbeat behavior.

2. /model command in multi-tenant mode only persists to per-user
   settings (selected_model) without calling set_model() on the shared
   LlmProvider. The per-request model_override in the dispatcher reads
   from the same setting. Added multi_tenant flag to AgentConfig
   (auto-detected from GATEWAY_USER_TOKENS).

3. RigAdapter now supports per-request model overrides by injecting the
   model name into rig-core's additional_params. OpenAI/Anthropic/Ollama
   API servers use last-key-wins for duplicate JSON keys, so the override
   takes effect via serde's flatten serialization order.

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

* fix: address PR review — cost model attribution, heartbeat concurrency, pruning

Fixes from review comments on #1614:

- Cost tracking now uses the override model name (not active_model_name)
  when a per-user model override is active, for accurate attribution.
- Multi-user heartbeat runs per-user checks concurrently via JoinSet
  instead of sequentially, preventing one slow user from blocking others.
- Per-user failure counts tracked independently; users exceeding
  max_failures are skipped (matching single-user semantics).
- per_user_daily_cost HashMap pruned on day rollover to prevent
  unbounded growth in long-lived deployments.
- Doc comment fixed: says "routines" not "active routines".

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

* fix: /status ownership, model persistence scoping, heartbeat robustness

Addresses second round of PR review on #1614:

- /status <job_id> DB path now validates job.user_id == requesting user
  before returning data (was missing ownership check, security fix).

- persist_selected_model takes user_id param instead of owner_id, and
  skips .env/TOML writes in multi-tenant mode (these are shared global
  files). handle_system_command now receives user_id from caller.

- JoinSet collection handles Err(JoinError) explicitly instead of
  silently dropping panicked tasks.

- Notification forwarder extracts owner_id from response metadata in
  multi-tenant mode for per-user routing instead of broadcasting to
  the agent owner.

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

* fix: cost pricing, fire_manual workspace, heartbeat concurrency cap

Round 3 review fixes:

- Cost tracking passes None for cost_per_token when model override is
  active, letting CostGuard look up pricing by model name instead of
  using the default provider's rates (serrrfirat).

- fire_manual() now uses per-user workspace, matching spawn_fire()
  pattern (serrrfirat).

- Removed MULTI_TENANT env var — multi-tenant mode is auto-detected
  solely from GATEWAY_USER_TOKENS presence (serrrfirat + Copilot).

- Multi-user heartbeat capped at 8 concurrent tasks to avoid flooding
  the LLM provider (serrrfirat + Copilot).

- Fixed inject_model_override doc comment accuracy (Copilot).

- Added comment explaining multi-tenant notification routing priority
  (Copilot).

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

* feat: user-scoped webhook endpoint for multi-tenant isolation

Adds POST /api/webhooks/u/{user_id}/{path} — a user-scoped webhook
endpoint that filters the routine lookup by user_id, preventing
cross-user webhook triggering when paths collide.

The existing /api/webhooks/{path} endpoint remains unchanged for
backward compatibility in single-user deployments.

Changes:
- get_webhook_routine_by_path gains user_id: Option<&str> param
- Both postgres and libsql implementations add AND user_id = ? filter
  when user_id is provided
- New webhook_trigger_user_scoped_handler extracts (user_id, path)
  from URL and passes to shared fire_webhook_inner logic
- Route registered on public router (webhooks are called by external
  services that can't send bearer tokens)

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

* feat(db): add UserStore trait with users, api_tokens, invitations tables

Foundation for DB-backed user management (#1605):

- UserRecord, ApiTokenRecord, InvitationRecord types in db/mod.rs
- UserStore sub-trait (17 methods) added to Database supertrait
- PostgreSQL migration V14__users.sql (users, api_tokens, invitations)
- libSQL schema + incremental migration V14
- Full implementations for both PgBackend (via Store delegation) and
  LibSqlBackend (direct SQL in libsql/users.rs)
- authenticate_token JOINs api_tokens+users with active/non-revoked
  checks; has_any_users for bootstrap detection

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

* feat(web): DB-backed auth, user/token/invitation API handlers

Adds the web gateway layer for DB-backed user management (#1605):

Auth refactor:
- CombinedAuthState wraps env-var tokens (MultiAuthState) + optional
  DbAuthenticator for DB-backed token lookup with LRU cache (60s TTL,
  1024 max entries)
- auth_middleware tries env-var tokens first, then DB fallback
- From<MultiAuthState> impl for backward compatibility
- main.rs wires with_db_auth when database is available

API handlers (12 new endpoints):
- /api/admin/users — CRUD: create, list, detail, update, suspend, activate
- /api/tokens — create (returns plaintext once), list, revoke
- /api/invitations — create, list, accept (creates user + first token)

Token creation: 32 random bytes → hex plaintext, SHA-256 hash stored.
Invitation accept: validates hash + pending + not expired, creates
user record and first API token atomically.

All test files updated for CombinedAuthState type change.

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

* feat: startup env-var user migration + UserStore integration tests

Completes the DB-backed user management feature (#1605):

- Startup migration: when GATEWAY_USER_TOKENS is set and the users
  table is empty, inserts env-var users + hashed tokens into DB.
  Logs deprecation notice when DB already has users.
- hash_token made pub for reuse in migration code.
- 10 integration tests for UserStore (libsql file-backed):
  - has_any_users bootstrap detection
  - create/get/get_by_email/list/update user lifecycle
  - token create → authenticate → revoke → reject cycle
  - suspended user tokens rejected
  - wrong-user token revoke returns false
  - invitation create → accept → user created
  - record_login and record_token_usage timestamps
- libSQL migration: removed FK constraints from V14 (incompatible
  with execute_batch inside transactions). Tables in both base SCHEMA
  and incremental migration for fresh and existing databases.

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

* refactor: remove GATEWAY_USER_TOKENS, fix review feedback

GATEWAY_USER_TOKENS never went to production — replaced entirely by
DB-backed user management via /api/admin/users and /api/tokens.

Removed:
- UserTokenConfig struct and GATEWAY_USER_TOKENS env var parsing
- user_tokens field from GatewayConfig
- GatewayChannel::new_multi_auth() constructor
- Env-var user migration block in main.rs (~90 lines)
- multi_tenant auto-detection from GATEWAY_USER_TOKENS (now runtime
  via db.has_any_users() in app.rs)

Review fixes (zmanian):
- User ID generation: UUID instead of display-name derivation (#1)
- Invitation accept moved to public router (no auth needed) (#3)
- libSQL get_invitation_by_hash aligned with postgres: filters
  status='pending' AND expires_at > now (#4)
- UUID parse: returns DatabaseError::Serialization instead of
  unwrap_or_default (#7)
- PostgreSQL SELECT * replaced with explicit column lists (#8)
- Sort order aligned (both backends use DESC) (#6)

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

* feat: add role-based access control (admin/member)

Adds a `role` field (admin|member) to user management:

Schema:
- `role TEXT NOT NULL DEFAULT 'member'` added to users table in both
  PostgreSQL V14 migration and libSQL schema/incremental migration
- UserRecord gains `role: String` field
- UserIdentity gains `role: String` field, populated from DB in
  DbAuthenticator and defaulting to "admin" for single-user mode

Access control:
- AdminUser extractor: returns 403 Forbidden if role != "admin"
- /api/admin/users/* handlers: require AdminUser (create, list,
  detail, update, suspend, activate)
- POST /api/invitations: requires AdminUser (only admins can invite)
- User creation accepts optional "role" param (defaults to "member")
- Invitation acceptance creates users with "member" role

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

* feat(web): add Users admin tab to web UI

Adds a Users tab to the web gateway UI for managing users, tokens,
and roles without needing direct API calls.

Features:
- User list table with ID, name, email, role, status, created date
- Create user form with display name, email, role selector
- Suspend/activate actions per user
- Create API token for any user (shows plaintext once with copy button)
- Role badges (admin highlighted, member muted)
- Non-admin users see "Admin access required" message
- Keyboard shortcut: Cmd/Ctrl+5 switches to Users tab

CSS:
- Reuses routines-table styles for the user list
- Badge, token-display, btn-small, btn-danger, btn-primary components

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

* fix: move Users to Settings subtab, bootstrap admin user on first run

- Moved Users from top-level tab to Settings sidebar subtab (under
  Skills, before Theme toggle)
- On first startup with empty users table, automatically creates an
  admin user from GATEWAY_USER_ID config with a corresponding API
  token from GATEWAY_AUTH_TOKEN. This ensures the owner appears in
  the Users panel immediately.

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

* fix: user creation shows token, + Token works, no password save popup

Three UI/UX fixes:

1. Create user now generates an initial API token and shows it in a
   copy-able banner instead of triggering the browser's password save
   dialog. Uses autocomplete="off" and type="text" for email field.

2. "+ Token" button works: exposed createTokenForUser/suspendUser/
   activateUser on window for inline onclick handlers in dynamically
   generated table rows. Token creation uses showTokenBanner helper.

3. Admin token creation: POST /api/tokens now accepts optional
   "user_id" field when the requesting user is admin, allowing
   token creation for other users from the Users panel.

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

* fix: use event delegation for user action buttons (CSP compliance)

Inline onclick handlers are blocked by the Content-Security-Policy
(script-src 'self' without 'unsafe-inline'). Switched to data-action
attributes with a delegated click listener on the users table.

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

* fix: add i18n for Users subtab, show login link on user creation

- Added 'settings.users' i18n key for English and Chinese
- Token banner now shows a full login link (domain/?token=xxx)
  with a Copy Link button, plus the raw token below
- Login link works automatically via existing ?token= auto-auth

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

* fix: token hash mismatch — hash hex string, not raw bytes

Critical auth bug: token creation hashed the raw 32 bytes
(hasher.update(token_bytes)) but authentication hashed the hex-encoded
string (hash_token(candidate) where candidate is the hex string the
user sends). This meant newly created tokens could never authenticate.

Fixed all 4 token creation sites (users, tokens, invitations create,
invitations accept) to use hash_token(&plaintext_token) which hashes
the hex string consistently with the auth lookup path.

Removed now-unused sha2::Digest imports from handlers.

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

* refactor: remove invitation system

The invitation flow is redundant — admin create user already generates
a token and shows a login link. Invitations add complexity without
value until email integration exists.

Removed:
- InvitationRecord struct and 4 UserStore trait methods
- invitations table from V14 migration (postgres + both libsql schemas)
- PostgreSQL Store methods (create/get/accept/list invitations)
- libSQL UserStore invitation methods + row_to_invitation helper
- invitations.rs handler file (212 lines)
- /api/invitations routes (create, list, accept)
- test_invitation_lifecycle test

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

* feat: user deletion, self-service profile, per-user job limits, usage API

Four multi-tenancy improvements:

1. User deletion cascade (DELETE /api/admin/users/{id}):
   Deletes user and all data across 11 user-scoped tables (settings,
   secrets, routines, memory, jobs, conversations, etc.). Admin only.

2. Self-service profile (GET/PATCH /api/profile):
   Users can read and update their own display_name and metadata
   without admin privileges.

3. Per-user job concurrency (MAX_JOBS_PER_USER env var):
   Scheduler checks active_jobs_for(user_id) before dispatch.
   Prevents one user from exhausting all job slots.

4. Usage reporting (GET /api/admin/usage?user_id=X&period=day|week|month):
   Aggregates LLM costs from llm_calls via agent_jobs.user_id.
   Returns per-user, per-model breakdown of calls, tokens, and cost.

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

* feat: add TenantCtx for compile-time tenant isolation

Implements zmanian's architectural proposal from #1614 review:
two-tier scoped database access (TenantScope/AdminScope) so handler
code cannot accidentally bypass tenant scoping.

TenantScope (default): wraps user_id + Arc<dyn Database>, auto-binds
user_id on every operation. ID-based lookups return None for cross-
tenant resources. No escape hatch — forgetting to scope is a compile
error.

AdminScope (explicit opt-in): cross-tenant access for system-level
components (heartbeat, routine engine, self-repair, scheduler, worker).

TenantCtx bundles TenantScope + workspace + cost guard + per-user
rate limiting. Constructed once per request in handle_message, threaded
through all command handlers and ChatDelegate.

Key changes:
- New src/tenant.rs (~920 lines): TenantScope, AdminScope, TenantCtx,
  TenantRateState, TenantRateRegistry
- All command handlers: user_id: &str → ctx: &TenantCtx
- ChatDelegate: cost check/record/settings via self.tenant
- System components: store field changed to AdminScope
- Config: TENANT_MAX_LLM_CONCURRENT, TENANT_MAX_JOBS_CONCURRENT env vars
- Fixes bug: /status <job_id> cross-tenant leak (now auto-filtered)

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

* fix: address PR #1626 review feedback — bounded LRU cache, admin auth, FK cleanup

- Replace HashMap with lru::LruCache in DbAuthenticator so the token
  cache is hard-bounded at 1024 entries (evicts LRU, not just expired)
- Gate admin user endpoints (list/detail/update/suspend/activate) with
  AdminUser extractor so members get 403 instead of full access
- Add api_tokens to libSQL delete_user cleanup list to prevent orphaned
  tokens (libSQL has no FK cascade)
- Add regression tests for all three fixes

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

* fix: update CA certificates in runtime Docker image

Ensures the root certificate bundle is current so TLS handshakes
to services like Supabase succeed on Railway.

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

* fix: resolve CI failures — formatting, no-panics check

- Run cargo fmt on test code
- Replace .expect() with const NonZeroUsize in DbAuthenticator
- Add // safety: comments for test-only code in multi_tenant.rs

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

* fix: switch PostgreSQL TLS from rustls to native-tls

rustls with rustls-native-certs fails TLS handshake on Railway's
slim container (empty or stale root cert store). native-tls delegates
to OpenSSL on Linux which handles system certs more reliably.

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

* Adding user management api

* feat: admin secrets provisioning API + API documentation

- Add PUT/GET/DELETE /api/admin/users/{id}/secrets/{name} endpoints for
  application backends to provision per-user secrets (AES-256-GCM encrypted)
- Add secrets_store field to GatewayState with builder wiring
- Create docs/USER_MANAGEMENT_API.md with full API spec covering users,
  secrets, tokens, profile, and usage endpoints
- Update web gateway CLAUDE.md route table

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

* fix: add CatchPanicLayer to capture handler panics

Without this, panics in async handlers silently drop the connection
and the edge proxy returns a generic 503. Now panics are caught,
logged, and returned as 500 with the panic message.

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

* fix: address second-round review — transactional delete, overflow, error logging

- C1: Wrap PostgreSQL delete_user() in a transaction so partial cleanup
  can't leave users in a half-deleted state
- M2: Add job_events to delete cleanup (both backends) — FK to
  agent_jobs without CASCADE would cause FK violation
- H1/M4: Cap expires_in_days to 36500 before i64 cast (tokens + secrets)
- H2: Validate target user exists before creating admin token to prevent
  orphan tokens on libSQL
- H3: Log DB errors in DbAuthenticator::authenticate() instead of
  silently swallowing them as 401

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

* fix: revert to rustls with webpki-roots fallback for PostgreSQL TLS

native-tls/OpenSSL caused silent crashes (segfaults in C code) during
DB writes on Railway containers. Switch back to rustls but add
webpki-roots as a fallback when system certs are missing, which was
the original TLS handshake failure on slim container images.

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

* chore: update Cargo.lock for rustls + webpki-roots

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

* debug: add /api/debug/db-write endpoint to diagnose user insert failure

Temporary diagnostic endpoint that tests DB INSERT to users table
with full error logging. No auth required. Will be removed after
debugging.

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

* perf: use cargo-chef in Dockerfile for dependency caching

Splits the build into planner/deps/builder stages. Dependencies are
only recompiled when Cargo.toml or Cargo.lock change. Source-only
changes skip straight to the final build stage.

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

* debug: add tracing to users_create_handler

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

* fix: guard created_by FK in user creation handler

The auth identity user_id (from owner_id scope) may not match any
user row in the DB, causing a FK violation on the created_by column.
Check that the referenced user exists before setting created_by.

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

* refactor: collapse GATEWAY_USER_ID into IRONCLAW_OWNER_ID

Remove the separate GATEWAY_USER_ID config. The gateway now uses
IRONCLAW_OWNER_ID (config.owner_id) directly for auth identity,
bootstrap user creation, and workspace scoping.

Previously, with_owner_scope() rebinds the auth identity to owner_id
while keeping default_sender_id as the gateway user_id. This caused
a FK constraint violation when creating users because the auth
identity ("default") didn't match any user in the DB ("nearai").

Changes:
- Remove GATEWAY_USER_ID env var and gateway_user_id from settings
- Remove user_id field from GatewayConfig
- Add owner_id parameter to GatewayChannel::new()
- Remove with_owner_scope() method
- Remove default_sender_id from GatewayState
- Remove sender override logic in chat/approval handlers
- Remove debug endpoint and tracing from prior debugging
- Update all tests and E2E fixtures

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

* fix: hide Users tab for non-admins, remove auth hint text

- Fetch /api/profile after login and hide the Users settings tab
  when the user's role is not admin
- Remove the "Enter the GATEWAY_AUTH_TOKEN" hint from the login page
  since tokens are now managed via the admin panel, not .env files

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

* fix: address review feedback (auth 503, token expiry, CORS PATCH)

- DB auth errors now return 503 instead of 401 so outages are
  distinguishable from invalid tokens (serrrfirat H3)
- Cap expires_in_days to 36500 before i64 cast to prevent negative
  duration from u64 overflow (serrrfirat H1)
- Add PATCH to CORS allowed methods for profile/user update
  endpoints (Copilot)
- Stop leaking panic details in CatchPanicLayer response body

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

* fix: harden multi-tenant isolation — review fixes from #1614

- Add conversation ownership checks in TenantScope: add_conversation_message,
  touch_conversation, list_conversation_messages (+ paginated),
  update_conversation_metadata_field, get_conversation_metadata now return
  NotFound for conversations not owned by the tenant (cross-tenant data leak)
- Fix multi-user heartbeat: clear notify_user_id per runner so notifications
  persist to the correct user, not the shared config target
- Move hygiene tasks into bounded JoinSet instead of unbounded tokio::spawn
- Revert send_notification to private visibility (only used within module)
- Use effective_model_name() for cost attribution in dispatcher so providers
  that ignore per-request model overrides report the actual model used
- Fix inject_model_override doc comment; add 3 unit tests
- Fix heartbeat doc comment ("routines" not "active routines")

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

* feat: add Jobs, Cost, Last Active columns to admin Users table

Add UserSummaryStats struct and user_summary_stats() batch query to the
UserStore trait (both PostgreSQL and libSQL backends). The admin users
list endpoint now fetches per-user aggregates (job count, total LLM
spend, most recent activity) in a single query and includes them inline
in the response. The frontend Users table displays three new columns.

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

* fix: address review comments and CI formatting failures

CI fixes:
- cargo fmt fixes in cli/mod.rs and db/tls.rs

Security/correctness (from Copilot + serrrfirat + pranavraja99 reviews):
- Token create: reject expires_in_days > 36500 with 400 instead of silent clamp
- Token create: return 404 when admin targets non-existent user
- User create: map duplicate email constraint violations to 409 Conflict
- User create: remove unnecessary DB roundtrip for created_by (use AdminUser directly)
- DB auth: log warn on DB lookup failures instead of silently swallowing errors
- libSQL: add FK constraints on users.created_by and api_tokens.user_id

Config fixes:
- agent.multi_tenant: resolve from AGENT_MULTI_TENANT env var instead of hardcoding false
- heartbeat.multi_tenant: fix doc comment to match actual env-var-based behavior

UI fix:
- showTokenBanner: pass correct title ("Token created!" vs "User created!")

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

* fix: address remaining review comments (round 2)

- Secrets handlers: normalize name to lowercase before store operations,
  validate target user_id exists (returns 404 if not found)
- libSQL: propagate cost parsing errors instead of unwrap_or_default()
  in both user_usage_stats and user_summary_stats
- users_list_handler: propagate user_summary_stats DB errors (was
  silently swallowed with unwrap_or_default)
- loadUsers: distinguish 401/403 (admin required) from other errors
- Docs: fix users.id type (TEXT not UUID), remove "invitation flow"
  from V14 migration comment

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

* feat: i18n for Users tab, atomic user+token creation, transactional delete_user

i18n:
- Add 31 translation keys for all Users tab strings (en + zh-CN)
- Wire data-i18n attributes on HTML elements (headings, buttons, inputs,
  table headers, empty state)
- Replace all hard-coded strings in app.js with I18n.t() calls

Atomic user+token creation:
- Add create_user_with_token() to UserStore trait
- PostgreSQL: wraps both INSERTs in conn.transaction() with auto-rollback
- libSQL: wraps in explicit BEGIN/COMMIT with ROLLBACK on error
- Handler uses single atomic call instead of two separate operations

Transactional delete_user for libSQL:
- Wrap multi-table DELETE cascade in BEGIN/COMMIT transaction
- ROLLBACK on any error to prevent partial cleanup / inconsistent state
- Matches the PostgreSQL implementation which already used transactions

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

* fix: revert V14 migration to match deployed checksum [skip-regression-check]

Refinery checksums applied migrations — editing V14__users.sql after
it was already applied causes deployment failures. Revert the cosmetic
comment changes (added in df40b22f) to restore the original checksum.

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

* fix: bootstrap onboarding flow for multi-tenant users

The bootstrap greeting and workspace seeding only ran for the owner
workspace at startup, so new users created via the admin API never
received the welcome message or identity files (BOOTSTRAP.md, SOUL.md,
AGENTS.md, USER.md, etc.).

Three fixes:
- tenant_ctx(): seed per-user workspace on first creation via
  seed_if_empty(), which writes identity files and sets
  bootstrap_pending when the workspace is truly fresh
- handle_message(): check take_bootstrap_pending() on the tenant
  workspace (not the owner workspace) and persist the greeting to
  the user's own assistant conversation + broadcast via SSE
- WorkspacePool: seed new per-user workspaces in the web gateway
  so memory tools also see identity files immediately

The existing single-user bootstrap in Agent::run() is preserved for
non-multi-tenant deployments.

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

* fix: address remaining PR review comments (round 3)

- Docs: fix metadata description from "merge patch" to "full replacement"
- Secrets: reject expires_in_days > 36500 with 400 (was silently clamped)
- libSQL: CAST(SUM(cost) AS TEXT) in user_usage_stats and user_summary_stats
  to prevent SQLite numeric coercion from crashing get_text() — this was
  the root cause of the Copilot "SUM returns numeric type" comments
- Add 3 regression tests: user_summary_stats (empty + with data) and
  user_usage_stats (multi-model aggregation)

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

* feat: add role change support for users (admin/member toggle)

- Add update_user_role() to UserStore trait + both backends (PostgreSQL
  and libSQL)
- Extend PATCH /api/admin/users/{id} to accept optional "role" field
  with validation (must be "admin" or "member")
- Add "Make Admin" / "Make Member" toggle button in Users table actions
- Add i18n keys for role change (en + zh-CN)
- Update API docs to document the role field on PATCH
- Fix test helpers to use fmt_ts() for timestamps (was using SQLite
  datetime('now') which produces incompatible format for string comparison)

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

* fix: show live LLM spend in Users table instead of only DB-recorded costs [skip-regression-check]

Chat turns record LLM cost in CostGuard (in-memory) but don't create
agent_jobs/llm_calls DB rows — those are only written for background
jobs. The Users table was querying only from DB, so it showed $0.00
for users who only chatted.

Now supplements DB stats with CostGuard.daily_spend_for_user() —
the same source displayed in the status bar token counter. Shows
whichever is larger (DB historical total vs live daily spend).

Also falls back to last_login_at for "Last Active" when no DB job
activity exists.

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

* fix: persist chat LLM calls to DB and fix usage stats query

Two root causes for zero usage stats:

1. ChatDelegate only recorded LLM costs to CostGuard (in-memory) —
   never to the llm_calls DB table. Added DB persistence via
   TenantScope.record_llm_call() after each chat LLM call, with
   job_id=NULL and conversation_id=thread_id.

2. user_summary_stats query only joined agent_jobs→llm_calls, missing
   chat calls (which have job_id=NULL). Redesigned query to start from
   llm_calls and resolve user_id via COALESCE(agent_jobs.user_id,
   conversations.user_id) — covers both job and chat LLM calls.

Both PostgreSQL and libSQL queries updated. TenantScope gets
record_llm_call() method. Tests updated for new query semantics.

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

* fix: address review comments — input validation, cost semantics, panic safety [skip-regression-check]

- Validate display_name: trim whitespace, reject empty strings (create + update)
- Validate metadata: must be a JSON object, return 400 if not (admin + profile)
- secrets_list_handler: verify target user_id exists before listing
- Cost display: use DB total directly (chat calls now persist to DB),
  remove confusing max(db,live) CostGuard fallback
- CatchPanicLayer: truncate panic payload to 200 chars in log to limit
  potential sensitive data exposure

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

* fix: address Copilot round 5 — docs, secrets consistency, token name, provider field [skip-regression-check]

- Docs: users.id note updated to "typically UUID v4 strings (bootstrap
  admin may use a custom ID)"
- secrets_list_handler: return 503 when DB store is None (was falling
  through to list secrets without user validation)
- tokens_create: trim + reject empty token name (matching display_name
  pattern)
- LlmCallRecord.provider: use llm_backend ("nearai","openai") instead
  of model_name() which returns the model identifier
- user_summary_stats zero-LLM users: acceptable — handler already falls
  back to 0 cost and last_login_at for missing entries

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

* fix: DB auth returns 503 on outage, scheduler counts only blocking jobs

From serrrfirat review:
- DB auth: return Err(()) on database errors so middleware returns 503
  instead of silently returning Ok(None) → 401 (auth miss)
- Scheduler: add parallel_blocking_count_for() that uses
  is_parallel_blocking() (Pending/InProgress/Stuck) instead of
  is_active() for per-user concurrency — Completed/Submitted jobs
  no longer count against MAX_JOBS_PER_USER

From Copilot:
- CLAUDE.md: fix secrets route paths from {id} to {user_id}
- token_hash: use .as_slice() instead of .to_vec() to avoid
  heap allocation on every token auth/creation call

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

* fix: immediate auth cache invalidation on security-critical actions (zmanian review #6)

Add DbAuthenticator::invalidate_user() that evicts all cached entries
for a user. Called after:
- Suspend user (immediate lockout, was 60s delay)
- Activate user (immediate access restoration)
- Role change (admin↔member takes effect immediately)
- Token revocation (revoked token can't be reused from cache)

The DbAuthenticator is shared (via Clone, which Arc-clones the cache)
between the auth middleware and GatewayState, so handlers can evict
entries from the same cache the middleware reads.

Also from zmanian's review:
- Items 1-5, 7-11 were already resolved in prior commits
- Item 12 (String→enum for status/role) is deferred as a broader refactor

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

* fix: last-admin protection, usage stats for chat calls, UTF-8 safe panic truncation

Last-admin protection:
- Suspend, delete, and role-demotion of the last active admin now
  return 409 Conflict instead of succeeding and locking out the admin API
- Helper is_last_admin() checks active admin count before destructive ops

Usage stats:
- user_usage_stats() now includes chat LLM calls (job_id=NULL) by
  joining via conversations.user_id, matching user_summary_stats()
- Both PostgreSQL and libSQL queries updated

Panic handler:
- Use floor_char_boundary(200) instead of byte-index [..200] to
  prevent panic on multi-byte UTF-8 characters in panic messages

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

* fix: workspace seed race, bootstrap atomicity, email trim, secrets upsert response [skip-regression-check]

- WorkspacePool: await seed_if_empty() synchronously after inserting
  into cache (drop lock first to avoid blocking), so callers see
  identity files immediately instead of racing a background task
- Bootstrap admin: use create_user_with_token() for atomic user+token
  creation, matching the admin create endpoint
- Email: trim whitespace, treat empty as None to prevent " " being
  stored and breaking uniqueness
- Secrets PUT: report "updated" vs "created" based on prior existence
- Last token_hash.to_vec() → .as_slice() in authenticate_token

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

* fix: disable unscoped webhook endpoint in multi-tenant mode [skip-regression-check]

The original /api/webhooks/{path} endpoint looks up routines across all
users. In multi-tenant mode, anyone who knows the webhook path + secret
could trigger another user's routine. Now returns 410 Gone with a
message pointing to the scoped endpoint /api/webhooks/u/{user_id}/{path}.

Detection uses state.db_auth.is_some() — present only when DB-backed
auth is enabled (multi-tenant). Single-user deployments are unaffected.

From: standardtoaster review comment

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

* fix: webhook multi-tenant check, secrets error propagation, stale doc comment [skip-regression-check]

- Webhook: use workspace_pool.is_some() instead of db_auth.is_some()
  for multi-tenant detection — db_auth is set for any DB deployment,
  workspace_pool is only set when has_any_users() was true at startup
- Secrets: propagate exists() errors instead of unwrap_or(false) so
  backend outages surface as 500 rather than incorrect "created" status
- Config: fix stale workspace_read_scopes comment referencing user_id

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-28 00:19:17 -07:00
485d1568c4 feat(cli): add ironclaw models subcommands (list/status/set/set-provider) (#1043)
* feat(cli): add ironclaw models subcommands (list/status/set/set-provider)
  Implements  model management CLI (part of #83):
  - `models list [provider] [--verbose] [--json]` — list providers; fetches
    live model list from the provider API when a specific provider is given
  - `models status [--json]` — show active provider/model
  - `models set <model>` — set default model with validation
  - `models set-provider <id> [--model <name>]` — set provider with alias
    normalization
  - fix conflicts

* fix(deps): update tar to 0.4.45 (RUSTSEC-2026-0067, RUSTSEC-2026-0068)

---------

Co-authored-by: firat.sertgoz <[email protected]>
2026-03-23 12:36:41 +01:00
a09c023642 feat(ux): complete UX overhaul — design system, onboarding, web polish (#1277)
* feat(ux): complete UX overhaul — design system, boot screen, onboarding, web polish

Shared design system: CSS custom properties for spacing, typography,
transitions, and color tokens used across web UI and boot screen.

Boot screen: compact feature-tags line showing enabled subsystems
(db, tools, routines, heartbeat, skills, sandbox, embeddings) at a
glance. Downgrade startup info logs (libSQL, webhook, workspace seed)
to debug level since the boot screen now covers this.

Onboarding wizard: model picker with live API fetch, provider-aware
auth flow, improved error recovery and progress display.

Web UI: ARIA attributes, welcome card, streaming debounce,
connection status banner, skeleton loaders, send cooldown.

CLI: doctor command enhancements, status command cleanup,
REPL banner consolidation, shared fmt module.

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

* feat(ux): Apple-level design refinements — spring physics, glass morphism, chat polish

Merge staging theme support (dark/light/system toggle) and layer UX
polish on top: spring-physics motion, glass morphism depth, chat
experience improvements, and responsive mobile refinements.

Design system:
- Restore and extend design token system (spacing, typography, timing,
  easing) with legacy aliases for theme compatibility
- Add shadow tiers, accent glow, glass morphism, spring easing tokens
- Tokens defined in both dark (:root) and light ([data-theme="light"])

Micro-interactions (Phase 2):
- Spring-overshoot message entry animation (slideUp)
- Spring-scale button press on all interactive buttons
- Tab crossfade animation, tool card smooth accordion (max-height)
- Modal scale(0.95) + blur(8px) entry, toast spring slide
- Sidebar width crossfade, card hover lift

Visual depth (Phase 3):
- Tab bar glass morphism + surface highlight + sliding indicator
- Active tab accent background pill
- Assistant message accent left border, user message bubble tail
- Floating input area (rounded + shadow + margin)

Chat polish (Phase 4):
- Smooth streaming cursor (cursorPulse), message hover timestamps
- Time separators (Today/Yesterday/date)
- Textarea smooth auto-expand, send button glow

Settings & forms (Phase 5):
- iOS-style toggle switches for boolean settings
- Input focus glow, save feedback spring animation
- Welcome card with gradient background + proper spacing
- Sticky settings group headers with glass backdrop

Accessibility & mobile (Phase 6):
- Animated focus ring, prefers-reduced-motion global kill-switch
- Touch target audit (44px min), mobile bottom-sheet modals
- Mobile bottom tab bar, toast redesign (icon + border + countdown)
- Thread hover translateX, badge in_progress pulse

Bug fixes:
- Gateway/TEE popover z-index (tab-bar z-index: 200, popovers 500)
- Connection lost banner as fixed top bar instead of flex child
- Sidebar collapse keeps toggle + new thread buttons visible
- Downgrade noisy startup logs (db, webhook, vector) to debug
- Remove green dot pulse animation on connected status
- Deduplicate confirm-modal in HTML, add tab-indicator div

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

* feat(web): mobile layout improvements — sidebar toggle, settings drill-down, tab bar polish

- Fix mobile sidebar toggle: use expanded-mobile class instead of collapsed,
  add backdrop overlay, auto-close on thread select, outside-click dismiss
- Settings: replace cramped horizontal tabs with drill-down navigation
  (category list → detail view → back button)
- Bottom tab bar: add glass morphism, hide theme toggle, flip tab indicator
  to top edge
- Keep thread toggle button visible in collapsed 36px sidebar strip

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

* feat(repl): interactive approval selector and transient status lines

- Replace ASCII-art approval box with clean horizontal rule card
- Add inquire-based interactive selector for tool approvals (↑↓ + Enter)
- Selector runs directly from send_status via spawn_blocking, with
  stdin_locked flag to prevent readline from competing for stdin
- Transient thinking/tool-started lines: each replaces the previous,
  all erased before final output (no clutter left in scrollback)
- Esc in selector sends denial so agent never gets stuck

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

* fix: widen TurnCost token fields to u64 and remove unused variable

- Change input_tokens/output_tokens from u32 to u64 in StatusUpdate::TurnCost,
  SseEvent::TurnCost, and the thread_ops emit site to avoid truncation on
  large conversations
- Remove unused _routine_engine_for_loop binding in agent_loop.rs

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

* chore: reduce startup log noise — demote info to debug

Demote routine startup messages (builder, WASM tools, tunnel, WASM
channels) from info to debug so the default log output stays clean.

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

* fix(web): allow CDN scripts in CSP connect-src directive

Add cdn.jsdelivr.net and cdnjs.cloudflare.com to connect-src so the
browser can fetch marked.js and DOMPurify without CSP violations.

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

* style: fix cargo fmt in repl.rs

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

* fix(web): gate turn_cost SSE handler on current thread

Prevents cost badge from attaching to the wrong message when
switching threads or receiving events from background threads.

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

* ci: retrigger CI

* fix: add missing extension_manager to webhook EngineContext

The webhook trigger path added in #736 was missing the
extension_manager field introduced by #1453.

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

* chore: ignore RUSTSEC-2026-0049 rustls-webpki CRL advisory

Low impact — requires compromised CA to exploit. Tracked for
upstream rustls-webpki upgrade.

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

* fix(routines): use fields.join for cron normalization

Use split_whitespace fields instead of re-trimming the original string
to avoid preserving extra internal whitespace in cron expressions.

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

* feat(repl): Apple-style approval card — clean vertical flow

- Drop verbose tool description (the command IS the decision surface)
- Unified vertical pipe layout: ◆ header → │ params → │ selector
- Selector options show keyboard shortcuts inline: Approve (y)
- Compact help message, answered state uses └ to close the flow
- No horizontal rules, no blank-line padding — just breathing room

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

* refactor(repl): replace inquire with crossterm for approval selector

Drop the inquire dependency (which pulled in crossterm 0.25, duplicating
the existing 0.28). The 3-option approval selector is now built directly
with crossterm raw mode — same UX, zero new dependencies.

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

* chore(deps): upgrade crossterm 0.28 → 0.29, eliminate duplication

termimad (via crokey) uses crossterm 0.29. Upgrading our direct
dependency from 0.28 to 0.29 collapses to a single crossterm version
in the dependency tree. Also migrated termimad::crossterm:: references
to the direct crossterm import.

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

* fix: address review comments — box_top off-by-one, smart_truncate overflow, mobile theme toggle

- Fix box_top() fill calculation: was off-by-one, producing boxes 1 char
  too wide (fmt.rs)
- Fix smart_truncate(): account for "..." in the budget so output never
  exceeds max_chars (repl.rs)
- Move theme toggle to settings sidebar on mobile instead of display:none,
  so mobile users can still switch themes (style.css, index.html, app.js)

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

* style: cargo fmt repl.rs

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

* fix: address review — retry duplication, CSP connect-src, deny color

- Remove failed message before retry to prevent duplicate user messages
- Revert connect-src to 'self' — CDN hosts only need script-src
- Use red for Deny confirmation in REPL approval selector

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
2026-03-21 23:50:49 -07:00
89394ebd29 feat(cli): add ironclaw hooks list subcommand (#1023)
Part of #83

  Static discovery of lifecycle hooks from bundled (audit_log) and plugin
  (WASM *.capabilities.json sidecar) sources. Supports --verbose and
  --json output. Workspace hooks (DB-stored) noted but omitted without
  DB connection.

  [skip-regression-check]

Co-authored-by: [email protected] <[email protected]>
2026-03-21 21:08:13 -07:00
3da9810e87 feat(llm): Add OpenAI Codex (ChatGPT subscription) as LLM provider (#1461)
* feat(llm): add OpenAI Codex backend config and OAuth session manager

Add OpenAiCodex as a new LLM backend variant with config for auth
endpoint, API base URL, client ID, and session persistence path.

The session manager implements OpenAI's device code auth flow
(headless-friendly, no browser required on the server) with automatic
token refresh, following the same persistence pattern as the existing
NEAR AI session manager.

Closes #742

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

* feat(llm): add Responses API client and token-refreshing decorator

Native Responses API client for chatgpt.com/backend-api/codex/responses,
the endpoint that works with ChatGPT subscription tokens. Handles SSE
streaming, text completions, and tool call round-trips.

Token-refreshing decorator wraps the provider to pre-emptively refresh
OAuth tokens before API calls and retry once on auth failures. Reports
zero cost since billing is through subscription.

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

* feat(llm): wire OpenAI Codex into provider factory, CLI, and setup wizard

Connect the new provider to the LLM factory, add openai_codex to the
CLI --backend flag, and add it as an option in the onboarding wizard.

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

* fix(llm): address PR #744 review feedback (20 items)

Review fixes for the OpenAI Codex provider PR:

- Remove dead `generate_pkce()` code (device flow gets PKCE from server)
- Fix `refresh_tokens()` to use `.form()` instead of `.json()` per OAuth spec
- Inline codex dispatch into `build_provider_chain()` (single async function,
  no separate `assemble_provider_chain()` helper — matches main's pattern)
- Remove Clone from `OpenAiCodexSession`, restrict fields to `pub(crate)`
- Propagate HTTP client builder error instead of silent fallback
- Redact device code response body from debug log
- Change `set_model()` in TokenRefreshingProvider to delegate to inner
- Replace hardcoded `/tmp/` test path with `tempfile::tempdir()`
- Accept `request_timeout_secs` from config instead of hardcoded 300s
- Parse `Retry-After` header on 429 responses (matches nearai_chat.rs pattern)
- Reuse `normalize_schema_strict()` for Codex tool definitions
- Add warning log for dropped image attachments
- Add doc comments on `list_models()` and `include` field
- Add `OPENAI_CODEX_API_URL` to `.env.example`
- Fix codex error message in `create_llm_provider()` for clarity
- Revert unrelated `.worktrees` addition to `.gitignore`
- Update `src/llm/CLAUDE.md` with Codex provider docs

[skip-regression-check]

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

* fix: address review feedback and harden OpenAI Codex provider (takeover #744)

Security:
- Add SSRF validation (validate_base_url) on OPENAI_CODEX_AUTH_URL and
  OPENAI_CODEX_API_URL, matching the pattern used by all other base URL
  configs (regression test for #1103 included)

Correctness:
- Add missing cache_write_multiplier() and cache_read_discount() trait
  delegation in TokenRefreshingProvider
- Cap device-code polling backoff at 60s to prevent unbounded interval
  growth on repeated 429 responses
- Default expires_in to 3600s when server returns 0, preventing
  immediately-expired sessions
- Fix pre-existing SseEvent::JobResult missing fallback_deliverable field
  in job_monitor.rs tests

Cleanup:
- Extract duplicated make_test_jwt() and test_codex_config() into shared
  codex_test_helpers module

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

* fix: address PR review feedback on OpenAI Codex provider (#1461)

- Login command now resolves OPENAI_CODEX_* env overrides even when
  LLM_BACKEND isn't set to openai_codex (Copilot review)
- Setup wizard "Keep current provider?" for codex no longer re-triggers
  device code login — mirrors Bedrock's keep-and-return pattern (Copilot)
- Revert provider init log from info back to debug (Copilot)
- Add warning log when token expires_in=0, before defaulting to 3600s
  (Gemini review)

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

---------

Co-authored-by: Sanjeev Suresh <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-20 08:14:20 -07:00
86ae12747b feat: LRU embedding cache for workspace search (#1423)
* feat: LRU embedding cache for workspace search (#165)

Add CachedEmbeddingProvider that wraps any EmbeddingProvider with an
in-memory LRU cache keyed by SHA-256(model_name + text). This avoids
redundant HTTP calls when the same text is embedded multiple times
(common during reindexing and repeated searches).

- Cache uses HashMap + last_accessed tracking with manual LRU eviction
  (same pattern as llm::response_cache::CachedProvider)
- Lock is never held during HTTP calls to prevent blocking
- embed_batch() partitions into hits/misses and only fetches misses
- Default 10,000 entries (~58 MB for 1536-dim vectors)
- Configurable via EMBEDDING_CACHE_SIZE env var
- Workspace.with_embeddings() auto-wraps; with_embeddings_uncached()
  available for tests

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

* fix: address review comments on embedding cache

- Validate embed_batch return count matches expected miss count
- Replace unwrap_or_default() with proper error propagation
- Fix batch eviction: run final eviction pass after insert to enforce cap
- Fix test: use different-length inputs to verify ordering correctness
- Reject EMBEDDING_CACHE_SIZE=0 in config validation (minimum is 1)

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

* fix: replace .expect() with proper error handling in embed_batch

The all-cache-hits early-return path used .expect("all cache hits") which
violates the project convention of no .unwrap()/.expect() in production
code. Replaced with the same ok_or_else pattern used in the normal path.

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

* fix: clarify memory sizing docs and use saturating_add for eviction

- Update memory comments in embedding_cache.rs, config/embeddings.rs,
  and workspace/mod.rs to note the ~58 MB figure is payload-only
  (actual memory is higher due to HashMap/key/allocation overhead)
- Use saturating_add(1) instead of + 1 for eviction threshold to
  prevent overflow if max_entries is usize::MAX

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

* fix: address Copilot review on embedding cache

- Avoid double-clone per miss in embed_batch: move embedding into
  results, clone only for the cache entry
- Evict per-insert instead of after all inserts to keep peak memory
  bounded during large batches
- Clamp max_entries to at least 1 in constructor to prevent unexpected
  eviction behavior when set to 0 via the public API

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

* fix: reduce embedding_cache module visibility to private

Types are already re-exported via `pub use`, so the module itself
doesn't need to be public. Reduces unnecessary API surface.

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

* fix: address serrrfirat review feedback on embedding cache

- Add TODO comment for O(n) LRU eviction scalability
- Add thundering herd note at lock release in embed()
- Warn when cache max_entries exceeds 100k
- Use with_embeddings_uncached() in integration test
- Add tests: error_does_not_pollute_cache, embed_batch_empty_input
- Update README with cache-aware with_embeddings() docs

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

* fix: prevent u32 wrapping in FailThenSucceedMock failure counter

fetch_sub(1) wraps to u32::MAX when called past zero, silently
breaking the mock for 3+ calls. Switch to load-then-store to avoid
the wrapping bug in both embed() and embed_batch().

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

* fix: address Copilot and serrrfirat review findings on embedding cache

- Switch tokio::sync::Mutex to std::sync::Mutex (lock never held across
  .await — cheaper synchronous lock)
- Extract DEFAULT_EMBEDDING_CACHE_SIZE constant to avoid 10_000 duplication
  between EmbeddingCacheConfig and EmbeddingsConfig

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

* test: add all-misses batch test for embedding cache

Adds embed_batch_all_misses test covering the case where every text in a
batch is a cache miss — fulfilling the commitment from serrrfirat's review.

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

* chore: trigger CI re-check after rebase

* fix: use raw [u8;32] cache keys and pre-allocate HashMap capacity

Address Copilot review findings:
- cache_key() now returns [u8; 32] instead of hex String, avoiding a
  64-byte allocation per lookup
- HashMap::with_capacity(max_entries) avoids incremental reallocation
- Fix pre-existing staging compilation error in cli/routines.rs
  (missing max_tool_rounds/use_tools fields)

[skip-regression-check]

* fix: make cache accessors sync and update doc for [u8;32] keys

Address Copilot review:
- len(), is_empty(), clear() are now sync since they only take a
  std::sync::Mutex lock with no .await points
- Update cache_size doc comment to reflect [u8;32] keys instead of
  String keys

[skip-regression-check]

* fix: remove clone_on_copy for [u8; 32] cache keys

[skip-regression-check]

* ci: add safety comments to test code for no-panics check

The CI no-panics grep check cannot distinguish test code inside
src/ files from production code. Add // safety: test annotations
to .unwrap(), .expect(), and assert!() calls in #[cfg(test)] modules.

* fix: correct cache doc and demote hit/miss logs to trace

- Fix misleading "String keys" in memory comment (cache uses [u8; 32])
- Demote per-request hit/miss logs from debug to trace to reduce noise
  on hot paths (batch summary stays at trace too)

* docs: add missing Arc import in workspace README example

* perf: batch eviction in embed_batch to avoid O(n×m) cost

Replace per-insert evict_lru call with a single evict_k_oldest pass
that computes eviction count upfront and removes the k oldest entries
in one O(n) scan. Avoids O(n×m) HashMap iterations while holding the
mutex during batch inserts.

* fix: cap batch cache inserts at max_entries and use O(n) selection

- evict_k_oldest now uses select_nth_unstable_by_key for O(n) average
  partial selection instead of O(n log n) full sort
- embed_batch caps cached entries at max_entries when misses exceed
  capacity, preventing the cache from growing unbounded
- Added test: batch_exceeding_capacity_respects_max_entries

* fix: flatten test assert for fmt compatibility

Shorten assert message to fit single line so cargo fmt doesn't
split the safety annotation onto a separate line.

* fix: address review feedback and improve embedding cache (takeover #235)

- Fix merge conflict: add missing allow_always field in PendingApproval
- Thread EmbeddingCacheConfig through CLI memory commands so they respect
  EMBEDDING_CACHE_SIZE instead of silently using default (fixes #235 review)
- Cap HashMap pre-allocation at min(max_entries, 1024) to avoid upfront
  memory waste at large cache sizes
- Fix FailThenSucceedMock race: replace load+store with atomic fetch_update
- Remove noisy '// safety: test' comments (40+ lines of diff noise)
- Fix collapsed lines from comment removal
- Simplify redundant Ok(...collect()?) to just collect()

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

* fix(embedding-cache): skip eviction on concurrent duplicate insert

When the lock is released for the HTTP call, another caller may insert
the same key. Re-check under lock and just update the existing entry
without evicting, avoiding unnecessary cache churn under concurrency.

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

---------

Co-authored-by: ztsalexey <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: ztsalexey <[email protected]>
2026-03-19 13:37:55 -07:00
ReidandGitHub 67b2c08a7c feat(cli): add logs command for gateway log access (#1105)
- Add `ironclaw logs` to tail gateway.log with reverse-seek (O(output) memory, no full-file load)
  - Add `--follow` for live SSE streaming from /api/logs/events
  - Add `--level` to get/set runtime log level via /api/logs/level
  - Support --json, --plain, --local-time, --url, --token, --timeout flags
  - Respect --config for gateway address/token resolution (consistent with other CLI commands)
  - Fail explicitly when --config points to invalid file instead of silent fallback
  - Wire Logs variant into Command enum and main.rs dispatch
  - Add 9 unit tests (tail_file chunked read, colorize, timestamp conversion, JSON output)
  - Update FEATURE_PARITY.md: logs 🚧
2026-03-15 05:32:10 +00:00
8ac24e775b style: fix formatting in cli/mod.rs and mcp/auth.rs (#1071)
* style: fix formatting in cli/mod.rs and mcp/auth.rs

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 13:21:49 -07:00
bcda73c2e0 feat(cli): add cron subcommand for managing scheduled routines (#1017)
* feat(cli): add cron subcommand for managing scheduled routines
  Rebase onto staging branch and address collaborator review:
  - Fix .unwrap_or(None) → proper error propagation in set_enabled()
  - Add --yes/-y flag for non-interactive deletion with confirmation prompt
  - Add --json flag for machine-readable output in list and history
  - Preserve error context chain with {e:#} in run_cron_cli()

  Note: GATEWAY_USER_ID is trusted from the environment; future work may
  add authentication for multi-tenant deployments.

* fix(cli): reject invalid cron timezones

* refactor(cli): rename cron subcommand to routines

The system manages all routine types (cron, webhook, event, manual),
not just cron schedules. Rename the CLI subcommand to reflect this:
- `ironclaw cron` -> `ironclaw routines` (with `cron` as hidden alias)
- List shows all routines by default, add --trigger filter
- Remove cron-trigger-only validation
- Simplify require_routine helper (no trigger type check)

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

---------

Co-authored-by: reidliu41 <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-12 12:47:23 -07:00
ReidandGitHub 5d9d17bf71 feat(cli): add ironclaw channels list subcommand (#933) 2026-03-12 03:43:17 -07:00
ReidandGitHub 3fbe290901 feat(cli): add ironclaw skills list/search/info subcommands (#918) 2026-03-11 20:20:20 -07:00
26068db24b feat: Import OpenClaw memory, history and settings (#903)
* feat: Import OpenClaw memory, history and settings

* review fixes

* fix: address remaining code quality issues

1. Remove dead import_conversation() function - replaced by import_conversation_atomic()
2. Improve non-UTF-8 filename handling in list_agent_dbs() - log warning instead of silent 'unknown'
3. Remove emojis from CLI output per project style guide

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

---------

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

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

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

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

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

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

* fix(review): address PR review comments

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

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

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

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

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

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

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

---------

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

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

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

[skip-regression-check]

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

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

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

[skip-regression-check]

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

* style: fix rustfmt line wrapping in setup_wasm_channels

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

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

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

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

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

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

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

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

[skip-regression-check]

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

* style: fix rustfmt line wrapping in integration test

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

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

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

Addresses Copilot review comments.

[skip-regression-check]

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

* fix(review): address remaining Copilot review comments

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

[skip-regression-check]

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Henry Park <[email protected]>
2026-03-10 04:39:51 +00:00
02f85a8ad5 feat(mcp): transport abstraction, stdio/UDS transports, and OAuth fixes (#721)
* feat(mcp): transport abstraction, stdio/UDS transports, and OAuth fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[skip-regression-check]

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-09 02:47:42 +00:00
5c2ba44f12 feat(llm): declarative provider registry (#618)
* feat(llm): declarative provider registry, replace hardcoded provider configs

Replace the hardcoded LlmBackend enum and per-provider config structs with
a declarative JSON registry. Adding a new OpenAI-compatible provider now
requires zero Rust code changes -- just add an entry to providers.json.

- Add providers.json with 14 providers (openai, anthropic, ollama,
  openai_compatible, tinfoil, openrouter, groq, nvidia, venice, together,
  fireworks, deepseek, cerebras, sambanova)
- Add src/llm/registry.rs with ProviderProtocol, SetupHint,
  ProviderDefinition, and ProviderRegistry types
- Rewrite src/config/llm.rs: remove LlmBackend enum and 5 per-provider
  config structs, replace with generic RegistryProviderConfig
- Simplify src/llm/mod.rs: remove 5 create_*_provider functions, dispatch
  on ProviderProtocol (3 code paths for all providers)
- Dynamic setup wizard: menu built from registry.selectable(), generic
  credential collection dispatched by SetupHint kind
- Dynamic secret injection: inject_llm_keys_from_secrets() discovers
  secret-to-env mappings from registry instead of hardcoded list
- Users can extend with ~/.ironclaw/providers.json (no recompile)
- Subsumes open provider PRs: Groq #570, NVIDIA NIM #576, Venice.ai #451
  (Gemini #476 excluded -- not OpenAI-compatible)

[skip-regression-check]

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

* feat(llm): self-sufficient provider auth, onboard --provider-only, extract SessionConfig

- NearAiChatProvider handles its own session auth lazily in
  resolve_bearer_token() instead of requiring main.rs to pre-check.
  Triggers OAuth/API-key login on first request when no token exists.

- Add `ironclaw onboard --provider-only` to reconfigure just the LLM
  provider and model selection without re-running the full wizard.

- Extract auth_base_url and session_path from NearAiConfig into
  LlmConfig::session (SessionConfig). Callers now use
  config.llm.session directly instead of reaching into nearai fields.

[skip-regression-check]

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

* fix(llm): address PR review comments on provider registry

- Use registry.selectable() instead of registry.all() for secret
  injection to avoid duplicates from user provider overrides.

- Fix selectable() dedup bug: check setup hint on the final (overridden)
  definition, not the first occurrence. User overrides that add a setup
  hint are now included correctly.

- Only store openai_compatible_base_url for providers that actually use
  LLM_BASE_URL, preventing base URL pollution for groq/nvidia/etc.

- Normalize provider_id to canonical registry def.id instead of using
  the raw user-supplied alias string.

- Add comment explaining why .completions_api() is used over the
  default Responses API path.

[skip-regression-check]

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

* fix(docker): copy providers.json into build context

The declarative provider registry uses `include_str!("../../providers.json")`
at compile time, so the file must be present in the Docker builder stage.

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

* fix(llm): address second-round PR review comments (#618)

- Make --channels-only and --provider-only mutually exclusive via clap
  conflicts_with (Copilot: cli/mod.rs)
- Add 5s timeout to fetch_openai_compatible_models(), matching the other
  three model-fetch helpers (Copilot: wizard.rs)
- Apply models_filter from setup hints when listing models, so Groq's
  "chat" filter actually excludes non-chat models (Copilot: wizard.rs)
- Normalize LlmConfig.backend to the canonical provider ID instead of
  the raw user-supplied alias string (Copilot: llm.rs)
- Add models_filter() accessor to SetupHint with regression test

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

* fix(test): relax flaky parallel speedup timing threshold

The test_parallel_speedup test asserted <500ms but CI runners can be
slow enough to exceed that while still proving parallelism. Bumped to
800ms which still validates parallel execution (sequential would be
~600ms minimum) while tolerating CI jitter.

[skip-regression-check]

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

* fix(llm): handle api_key_login path in resolve_bearer_token, warn on missing keys

- resolve_bearer_token() now checks NEARAI_API_KEY env var after
  ensure_authenticated(), handling the case where the user entered an
  API key via the interactive login flow (which sets the env var but
  not a session token)
- Add tracing::warn when creating an OpenAI-compatible provider without
  an API key, making 401 errors easier to diagnose
- Add regression test for resolve_bearer_token auth paths

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

* style: fix formatting in nearai_chat test

[skip-regression-check]

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

* fix(llm): correct bearer token priority, handle setup-less providers (#618)

- resolve_bearer_token(): session token now takes priority over
  NEARAI_API_KEY env var, preventing unexpected auth mode switches.
  The env var fallback only triggers after ensure_authenticated() when
  no session token was stored (api_key_login path).
- run_provider_setup(): providers with setup: None no longer error,
  allowing env-var-only providers to be kept during re-onboarding.
- Split bearer token test into 3 focused tests: config api_key path,
  session token path, and session-beats-env-var precedence test.
- Add test for wizard handling of providers without setup hints.

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

* test(llm): comprehensive tests for provider registry, config, and auth

Add 13 new tests covering the critical paths in the provider system:

Bearer token auth priority (nearai_chat.rs):
- config api_key wins over session token and env var
- session token wins over env var (prevents mid-run auth mode switches)
- config api_key path works in isolation
- session token path works in isolation

Config resolution (config/llm.rs):
- backend alias normalization (open_ai → openai)
- unknown backend falls back to openai_compatible
- nearai aliases (nearai, near_ai, near) all resolve correctly
- base URL resolution priority (env > settings > registry default)

Registry dedup (registry.rs):
- user override adds setup hint → appears in selectable()
- user override removes setup hint → excluded from selectable()
- selectable() preserves insertion order during dedup
- all built-in ApiKey providers have api_key_env set

Wizard (wizard.rs):
- setup: None providers don't error during re-onboarding

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-07 02:18:57 +00:00
DevBrocoandGitHub a89c5f7348 Improve --help: add detailed about/examples/color, snapshot test (clo… (#371) 2026-02-27 17:45:30 +04:00
db2ba424ce Add --version flag with clap built-in support and test (#342)
Co-authored-by: firat.sertgoz <[email protected]>
2026-02-25 17:25:07 +04:00
7f68207f1e Feat/completion (#240)
* feat: add OpenRouter usage examples

* feat: add HTPS headers

* feat: add shell completion generation via clap_complete

* feat: add shell completion generation via clap_complete

* feat: add shell completion generation via clap_complete

* Refactor completion: use clap_complete::Shell directly, improve tests, remove tracing duplication, fix .env.example and Cargo.toml

* fix: rename init_cli_logging to init_cli_tracing (sync with main)

---------

Co-authored-by: BroccoliFin <[email protected]>
Co-authored-by: firat.sertgoz <[email protected]>
2026-02-22 19:08:43 +00:00
97a7637f30 feat: extension registry with metadata catalog and onboarding integration (#238)
* feat: add extension registry with metadata catalog, CLI, and onboarding integration

Adds a central registry that catalogs all 14 available extensions (10 tools,
4 channels) with their capabilities, auth requirements, and artifact references.
The onboarding wizard now shows installable channels from the registry and
offers tool installation as a new Step 7.

- registry/ folder with per-extension JSON manifests and bundle definitions
- src/registry/ module: manifest structs, catalog loader, installer
- `ironclaw registry list|info|install|install-defaults` CLI commands
- Setup wizard enhanced: channels from registry, new extensions step (8 steps)

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

* fix(setup): resolve workspace errors for tool crates and channels-only onboarding

Tool crates in tools-src/ and channels-src/ failed `cargo metadata` during
onboard install because Cargo resolved them as part of the root workspace.
Add `[workspace]` table to each standalone crate and extend the root
`workspace.exclude` list so they build independently.

Channels-only mode (`onboard --channels-only`) failed with "Secrets not
configured" and "No database connection" because it skipped database and
security setup. Add `reconnect_existing_db()` to establish the DB connection
and load saved settings before running channel configuration.

Also improve the tunnel "already configured" display to show full provider
details (domain, mode, command) instead of just the provider name.

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

* fix(registry): address PR review feedback on installer and catalog

- Use manifest.name (not crate_name) for installed filenames so
  discovery, auth, and CLI commands all agree on the stem (#1)
- Add AlreadyInstalled error variant instead of misleading
  ExtensionNotFound (#2)
- Add DownloadFailed error variant with URL context instead of
  stuffing URLs into PathBuf (#3)
- Validate HTTP status with error_for_status() before reading
  response bytes in artifact downloads (#4)
- Switch build_wasm_component to tokio::process::Command with
  status() so build output streams to the terminal (#6)
- Find WASM artifact by crate_name specifically instead of picking
  the first .wasm file in the release directory (#7)
- Add is_file() guard in catalog loader to skip directories (#8)
- Detect ambiguous bare-name lookups when both tools/<name> and
  channels/<name> exist, with get_strict() returning an error (#9)
- Fix wizard step_extensions to check tool.name for installed
  detection, consistent with the new naming (#11, #12)
- Fix redundant closures and map_or clippy warnings in changed files

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

* fix(setup): restore DB connection fields after settings reload

reconnect_postgres() and reconnect_libsql() called Settings::from_db_map()
which overwrote database_url / libsql_path / libsql_url set from env vars.
Also use get_strict() in cmd_info to surface ambiguous bare-name errors.

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

* style: fix clippy collapsible_if and print_literal warnings

Collapse nested if-let chains and inline string literals in format
macros to satisfy CI clippy lint checks (deny warnings).

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

* fix(registry): prefer artifacts for install-defaults and improve dir lookup

- InstallDefaults now defaults to downloading pre-built artifacts
  (matching `registry install` behavior), with --build flag for source builds.
- find_registry_dir() walks up 3 ancestor levels from the exe and adds
  a CARGO_MANIFEST_DIR fallback, matching load_registry_catalog() logic.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-20 01:17:44 +00:00
a158eee1b0 feat: 10 infrastructure improvements from zeroclaw (#126)
* refactor: break up agent_loop.rs into four focused modules

Split the monolithic 2835-line agent_loop.rs into:
- agent_loop.rs (722L): Agent struct, event loop, message dispatch
- dispatcher.rs (635L): Agentic tool loop, tool execution, auth detection
- commands.rs (484L): System commands, job handlers, heartbeat, summarize
- thread_ops.rs (1059L): Thread lifecycle, approval, undo/redo, persistence

Each module gets its own impl Agent block. Agent fields changed to
pub(super) so sibling modules in the agent package can access them.
All 16 existing tests pass in their new locations.

Inspired by ZeroClaw's agent module split (agent.rs, loop_.rs,
dispatcher.rs, prompt.rs, memory_loader.rs).

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

* feat: add cost caps and guardrails for autonomous agent spending

Daily budget (MAX_COST_PER_DAY_CENTS) and hourly action rate
(MAX_ACTIONS_PER_HOUR) limits prevent runaway agents from burning
through API credits, especially in daemon/heartbeat modes.

- CostGuard with pre-flight check and post-call recording
- Sliding window for hourly rate, midnight-UTC daily reset
- 80% threshold warning, atomic fast-path for exceeded budget
- Wired into dispatcher loop (check before LLM call, record after)

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

* feat: add circuit breaker on LLM providers

Wraps LlmProvider with a Closed/Open/HalfOpen state machine that
trips after consecutive transient failures, preventing request storms
against a degraded backend. Automatically probes for recovery.

- CircuitBreakerProvider implements LlmProvider (drop-in wrapper)
- Transient error classification (server, rate-limit, network, auth infra)
- Client errors (wrong model, context overflow) don't trip the breaker
- Configurable via CIRCUIT_BREAKER_THRESHOLD and CIRCUIT_BREAKER_RECOVERY_SECS
- Composes with existing FailoverProvider (circuit breaker wraps failover)
- 12 tests covering full state machine and error classification

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

* feat: add tunnel abstraction for remote access

Trait-based tunnel system with lifecycle management (start/stop/health)
for exposing the agent to the internet through external tunnel binaries.

Five providers:
- Cloudflare Tunnel (cloudflared, Zero Trust token auth)
- Tailscale (serve for tailnet, funnel for public)
- ngrok (with optional custom domain)
- Custom (arbitrary command with {host}/{port} placeholders)
- None (local-only, no external exposure)

Config via TUNNEL_PROVIDER + provider-specific env vars. Extends
existing TunnelConfig with optional managed provider alongside the
static TUNNEL_URL path. Factory, shared process management, and
37 tests covering all providers and edge cases.

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

* feat: add OS service management (launchd/systemd)

Adds `ironclaw service {install,start,stop,status,uninstall}` for
running the agent as a background daemon. macOS uses launchd plists
under ~/Library/LaunchAgents, Linux uses systemd user units.

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

* feat: add observability trait system with noop, log, and multi backends

Introduces an Observer trait for recording agent lifecycle events and
metrics, with pluggable backends. The noop backend compiles to zero
overhead, log backend uses tracing, and multi fans out to multiple
observers. Configured via OBSERVABILITY_BACKEND env var.

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

* feat: add in-memory LLM response cache with TTL and LRU eviction

CachedProvider wraps any LlmProvider and caches complete() responses
keyed by SHA-256(model + messages). Tool-calling requests are never
cached since they trigger side effects. Configurable via
RESPONSE_CACHE_ENABLED, RESPONSE_CACHE_TTL_SECS, and
RESPONSE_CACHE_MAX_ENTRIES env vars.

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

* feat: add memory hygiene with cadence-gated daily log cleanup

Adds workspace::hygiene module that automatically deletes daily log
documents older than a configurable retention period (default 30 days).
Runs on a 12-hour cadence tracked via a local state file to avoid
redundant passes. Best-effort design: failures are logged, never fatal.

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

* feat: add doctor diagnostics command for active health probing

Probes external dependencies (Docker, cloudflared, ngrok, tailscale),
validates NEAR AI session, checks database connectivity, and verifies
workspace directory. Complements the passive `status` command.

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

* feat: add structured TOML config file support

Adds ~/.ironclaw/config.toml as a configuration layer between env vars
and database settings. Priority: env var > TOML file > DB > defaults.

- `ironclaw config init` generates a commented config.toml from current settings
- `ironclaw --config path/to/config.toml` loads a custom config file
- Settings.merge_from() only overlays non-default values from the TOML file
- `ironclaw config path` now shows TOML file status

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

* fix: address codex review findings

- apply_toml_overlay now returns Result and errors on explicit missing
  or invalid config paths (was log-only, violating the documented
  contract that explicit paths are fatal)
- custom tunnel url_pattern is now used to filter extracted URLs, not
  just as a gate for scanning stdout
- systemd ExecStart path is now quoted to handle spaces in paths

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

* fix: address PR review feedback

- Cache key now includes max_tokens, temperature, and stop_sequences
  so different request parameters produce distinct keys
- to_cents() uses .trunc() + parse::<u64> instead of f64 intermediary,
  avoiding precision loss for large values
- Tailscale public URL no longer includes local port (serve/funnel
  expose on standard HTTPS port 443)

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

* feat: wire up tunnel lifecycle and fix audit findings

Connect the tunnel module to the rest of the application so that
setting TUNNEL_PROVIDER actually starts a managed tunnel at boot and
stops it on shutdown. Previously create_tunnel() was never called
outside tests.

Changes:
- Expand TunnelSettings with provider credential fields (settings.rs)
- TunnelConfig::resolve() falls back to DB settings when env vars unset
- Start tunnel at boot, stop on shutdown, show URL in boot screen
- Setup wizard collects provider-specific credentials (ngrok, cloudflare,
  tailscale, custom, static URL)
- Fix public_url() returning None under lock contention (SharedUrl)
- Fix local_host parameter ignored by cloudflare/ngrok/tailscale
- Fix tailscale silent fallback to "localhost" on bad JSON
- Fix ngrok globally mutating config via add-authtoken (use env var)
- Add 10s timeout to tailscale status --json

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

* fix: address PR review comments

- Document split_whitespace limitation in CustomTunnel doc comment
- Remove unnecessary quotes from systemd ExecStart directive

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

* fix: address PR review feedback (round 3)

- doctor: missing libSQL DB on fresh install is Pass, not Fail
- service: quote ExecStart path for systemd space handling

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

* fix: correct cost guard doc comment (LLM calls, not LLM/tool)

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-17 19:35:56 +00:00
a53b2c10b5 fix: Fix wasm tool schemas and runtime (#42)
* feat: Move debug log truncation from agent loop to REPL channel

Full tool output now flows through StatusUpdate so the web gateway
gets untruncated content. The REPL channel truncates at display time
(200 chars for tool results, thinking, and status messages).

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

* fix: Flatten WASM tool schemas and fix host HTTP runtime contention

LLMs can't reliably follow oneOf + const discriminator patterns in JSON
Schema, causing tools like Google Calendar to receive malformed params
(e.g., {"operation":"list_events","data":{"calendarId":"primary"}} instead
of {"action":"list_events","calendar_id":"primary"}). Replace all 9 WASM
tool schemas with flat action enum + top-level properties. The serde
#[serde(tag = "action")] deserialization works identically.

Also fixes WASM host HTTP requests (channels and tools) stalling during
startup by replacing Handle::current().block_on() with a dedicated
single-threaded runtime per request, avoiding I/O driver contention.

Reduces verbose LLM debug logging (full request/response payloads) and
changes tower_http default from debug to warn.

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

* feat: Built-in OAuth credentials and combined Google scopes

Add infrastructure for shipping default OAuth credentials with the binary,
similar to how gcloud/rclone bake in their client_id. Credentials are set
at compile time via IRONCLAW_GOOGLE_CLIENT_ID / IRONCLAW_GOOGLE_CLIENT_SECRET
env vars, or can be hardcoded in src/cli/oauth_defaults.rs.

The fallback chain is: capabilities file > runtime env var > built-in defaults.

Also, when authing any Google tool, scopes from ALL installed Google tools
are now combined into a single OAuth request (they all share the same
google_oauth_token secret). One login covers Gmail, Calendar, Drive, etc.

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

* feat: Ship default Google OAuth credentials for zero-config auth

Google Desktop App credentials are not secret (per Google's own docs).
Hardcode them so `ironclaw tool auth <google-tool>` works out of the box
without requiring users to register their own OAuth app.

Credentials can still be overridden at compile time
(IRONCLAW_GOOGLE_CLIENT_ID) or runtime (GOOGLE_OAUTH_CLIENT_ID).

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

* fix: Consistent OAuth callback port and polished landing page

- Use fixed port 9876 instead of scanning 9876-9886 (one redirect URI
  to register in provider OAuth apps, deterministic behavior)
- Replace broken unicode checkmark with SVG icons (charset was missing,
  rendered as mojibake)
- Dark themed landing page with proper card layout for both success
  and error states
- Add charset=utf-8 to Content-Type headers

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

* refactor: Unify OAuth callback server across all auth flows

All three OAuth flows (WASM tool auth, MCP server auth, NEAR AI login)
now share the same code from cli::oauth_defaults:

- Fixed port 9876 (one redirect URI to register per provider)
- Shared landing page HTML (dark card with SVG icons, proper charset)
- Parameterized wait_for_callback(listener, path, param, display_name)

Removes ~120 lines of duplicated callback/HTML code.

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

* Support for oauth token refresh

* refactor: Replace bootstrap.json with ~/.ironclaw/.env for DATABASE_URL

Kill the 4-field BootstrapConfig JSON file. Only DATABASE_URL actually
needs disk persistence (chicken-and-egg before DB connect). The other
three fields are now derived: pool_size defaults to 10 via env var,
secrets master key is auto-detected (env then keychain probe), and
onboard_completed is inferred from DATABASE_URL presence.

The new format is a standard .env file loaded via dotenvy early in
main, so DATABASE_URL is available as a regular env var everywhere.

Handles three upgrade paths:
- Clean start: wizard writes .env, reload after wizard completes
- Returning user: .env loaded at startup, business as usual
- Legacy upgrade: bootstrap.json auto-migrated to .env on first run

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

* fix: Address PR review findings

- Fix UTF-8 panic in truncate_for_preview (byte-slice on char boundary)
- Cap WASM guest timeout_ms at 5 minutes to prevent resource exhaustion
- Fix localhost detection in requires_auth() to avoid substring matches
  (e.g. "notlocalhost.com" no longer matches)
- Fix query param injection to insert before URL fragment
- Fix extract_host_from_url for IPv6 bracket notation
- Remove misleading schema defaults: Slack limit, Slides insertion_index,
  Docs index (per-action defaults documented in descriptions instead)

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

* style: Fix cargo fmt formatting

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

* fix: IPv6 loopback support for OAuth listener and localhost detection

- bind_callback_listener: try [::1] first, fall back to 127.0.0.1,
  so OAuth redirects work on systems where localhost resolves to ::1
- is_localhost_url: replace manual string parsing with url::Url for
  correct handling of IPv6 brackets, ports, userinfo, etc.
- Add url crate as direct dependency (already a transitive dep)

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

* fix: Address PR review feedback on runtime reuse, onboard check, and OAuth binding

- Remove session file check from check_onboard_needed(); DATABASE_URL is sufficient
- Detect AddrInUse on IPv6 bind and fail immediately instead of falling through to IPv4
- Reuse dedicated tokio runtime across HTTP calls in both tool and channel WASM wrappers

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

* fix: HTML-escape provider name in OAuth landing page, simplify Slack limit description

- Add html_escape() to prevent XSS in landing_html() where provider_name
  was interpolated directly into HTML (defense-in-depth, source is trusted
  but escaping costs nothing)
- Remove per-action default numbers from Slack limit field description to
  avoid confusing LLMs with conflicting defaults

Addresses review feedback from zmanian on PR #42.

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

* fix: Save all bootstrap fields from wizard, fix config module comment

- Wizard now saves secrets_master_key_source and database_pool_size to
  bootstrap.json (was only saving database_url and onboard_completed,
  which broke secrets after fresh onboard since SecretsConfig::resolve
  reads key source from bootstrap)
- Update config.rs module doc to reflect bootstrap.json priority chain
  instead of the removed ~/.ironclaw/.env approach

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

* refactor: Replace BootstrapConfig with .env-based bootstrap

DATABASE_URL is the only setting that needs disk persistence before
the database is available. Instead of a custom bootstrap.json with 4
fields, use a standard ~/.ironclaw/.env file loaded via dotenvy.

- Remove BootstrapConfig struct entirely
- Restore ironclaw_env_path(), load_ironclaw_env(), save_database_url()
- SecretsConfig::resolve() now auto-detects (env var then keychain probe)
  instead of reading a saved source from bootstrap.json
- DatabaseConfig::resolve() reads DATABASE_URL from env only (dotenvy
  loads ~/.ironclaw/.env into the environment early in startup)
- check_onboard_needed() is now sync (just checks env vars)
- Wizard save_and_summarize() works for both postgres and libsql backends
- One-time migration from bootstrap.json to .env preserved

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

* fix: Ensure load_ironclaw_env() runs in all Config paths, fix .env priority

- Config::from_env() and Config::from_db() now call load_ironclaw_env()
  internally (after dotenvy::dotenv()), so CLI commands like `memory`
  and `config` correctly load DATABASE_URL from ~/.ironclaw/.env
- Fix load order: standard ./.env first (higher priority), then
  ~/.ironclaw/.env, matching the documented priority chain
- Collapse nested if/if-let into let-chains (clippy::collapsible_if)
  in oauth_defaults.rs, tool.rs, and secrets/store.rs
- Fix rename_to_migrated to take &Path instead of &PathBuf

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

* fix: Address PR review comments (quoting, SSRF, error mapping)

- Quote DATABASE_URL in .env writes so `#` in passwords isn't treated
  as a dotenv comment (e.g., `DATABASE_URL="postgres://..."`)
- Add SSRF defenses to refresh_oauth_token(): require HTTPS, reject
  private/loopback IPs (with DNS resolution), disable redirects.
  token_url comes from tool capabilities JSON, so a malicious tool
  could otherwise exfiltrate refresh tokens.
- Fix IPv4 bind error mapping: only map AddrInUse to PortInUse,
  use generic Io variant for other bind failures

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-14 21:21:22 +00:00
e843c18141 feat: add libSQL/Turso embedded database backend (#47)
* feat: add libSQL/Turso database backend with full feature parity

Introduce a Database trait abstraction (~60 async methods) enabling
compile-time backend selection between PostgreSQL and libSQL/Turso.
Convert all modules from concrete Store to Arc<dyn Database>, add
LibSqlSecretsStore and LibSqlWasmToolStore implementations, wire
libsql stores throughout CLI and main entry points, and make the
setup wizard backend-agnostic.

Key changes:
- src/db/: Database trait, PostgresDatabase adapter, LibSqlBackend
  with native SQLite-dialect SQL, and idempotent migration system
- src/secrets/store.rs: LibSqlSecretsStore (all 8 trait methods)
- src/tools/wasm/storage.rs: LibSqlWasmToolStore (all 7 trait methods)
- src/main.rs, cli/tool.rs, cli/mcp.rs: backend-conditional wiring
- src/setup/channels.rs: SecretsContext uses Arc<dyn SecretsStore>
- Feature-gate postgres-only tests and examples

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

* feat: enable onboarding wizard for libSQL builds

Refactor the setup wizard to work with both postgres and libsql feature
flags. Previously the wizard was gated behind #[cfg(feature = "postgres")]
only, so libsql-only builds would print an error on `ironclaw onboard`.

- Add libsql fields to Settings (database_backend, libsql_path, libsql_url)
- Split wizard database/migration/secrets methods into feature-gated variants
- Add step_database_libsql() with local path and Turso remote replica prompts
- Update setup/mod.rs and main.rs feature gates to any(postgres, libsql)
- Extend check_onboard_needed() to detect libsql database presence

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

* fix: address PR review feedback for libSQL backend

- P0: Switch libsql_backend to connection-per-operation pattern to fix
  shared Connection concurrency issue across tokio tasks
- P0: Wrap secrets store INSERT+SELECT in transaction to fix TOCTOU race
- P0: Document encryption-at-rest limitations and json_patch divergence
- P1: Fix get_opt_text removing .filter(|s| !s.is_empty()) that conflated
  empty strings with NULL
- P1: Replace datetime('now') with fmt_ts(&Utc::now()) for consistent
  RFC 3339 timestamps across all queries
- P2: Use explicit _rowid column in FTS5 triggers and joins for stability
  across VACUUM operations
- P2: Add tracing::warn when embedding provided but vector search disabled
  in hybrid_search
- Extract shared connect_from_config() helper to deduplicate DB connection
  logic across main.rs, cli/config.rs, and cli/mcp.rs

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

* fix: add missing JobContext fields and resolve fmt/clippy warnings

Add total_tokens_used and max_tokens fields to JobContext in
libsql_backend.rs, apply cargo fmt, and fix clippy warnings.

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

* fix: review fixes for libSQL backend (shared connections, panics, indexes)

- Replace .expect() with proper error propagation in 3 call sites
- Share Arc<Database> between backend and stores instead of single Connection
- Add connect-per-operation pattern to LibSqlSecretsStore and LibSqlWasmToolStore
- Wrap store() INSERT + SELECT-back in a transaction
- Add ~22 missing indexes for parity with PostgreSQL schema
- Add 18 leak_detection_patterns seed rows matching PostgreSQL V2 migration
- Fix super:: import to use crate:: style
- Gate mask_password_in_url behind #[cfg(feature = "postgres")]
- Rewrite secrets store init with or_else chain for runtime backend selection

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

* fix: Resolve clippy lints (collapsible_if, too_many_arguments)

Collapse nested if blocks into let_chains to satisfy clippy's
collapsible_if lint (CI uses -D warnings). Suppress too_many_arguments
on libsql_row_to_tool_at since refactoring the positional index
pattern would be a larger change.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Illia Polosukhin <[email protected]>
2026-02-14 02:05:05 +00:00
Ilgın KanatandGitHub 115b7f38fe DM pairing + Telegram channel improvements (#17)
* feat: Implement DM pairing for channels

- Introduced a new pairing system to manage direct messages from unknown senders.
- Added `PairingStore` to handle pending requests and allowlist management.
- Implemented CLI commands for listing and approving pairing requests.
- Updated Telegram channel to utilize the new pairing logic, including workspace paths for storing pairing data.
- Enhanced WASM channel integration to support pairing functionality.

This feature enhances security by requiring approval for unknown senders before they can interact with the agent.

* Enhance Telegram channel support with media captioning and DM pairing features

- Added support for media captions in Telegram messages, allowing for richer content handling.
- Updated message processing to utilize either text or caption, improving message flexibility.
- Enhanced DM pairing functionality to include approval and listing capabilities for direct messages.
- Updated feature parity documentation to reflect new capabilities and improvements in Telegram integration.

* Update README and BUILDING_CHANNELS documentation for Telegram channel integration

- Enhanced README with instructions for building and running the Telegram channel, including a note on running `./scripts/build-all.sh` for full releases.
- Added detailed steps in BUILDING_CHANNELS.md for building and deploying the Telegram channel, emphasizing the need to run `./channels-src/telegram/build.sh` before building the main crate to ensure updated WASM is included.
- Updated CLI module to expose a new command for pairing with store functionality.

* Implement build script for Telegram channel WASM and enhance pairing error handling

- Added a new `build.rs` script to automate the compilation of the Telegram channel's WASM binary from source, ensuring reproducible builds and emphasizing supply chain security by preventing committed binaries.
- Updated `BUILDING_CHANNELS.md` to reflect the new build process and the importance of not committing compiled binaries.
- Enhanced error handling in the pairing approval process to include rate limiting for failed attempts, improving security and user feedback.

* Remove Telegram channel WASM binary file as part of the build process cleanup, ensuring no committed binaries are present in the repository.
2026-02-12 00:46:47 +00:00
ced83d5b4d feat: Sandbox jobs (#4)
* Orchestrating jobs and running them in sandboxes

* Fix heartbeat: dynamic max_tokens, empty content guard, notification fallback

- Query /v1/models API for context_length and set max_tokens to half
  (floor 4096) instead of hardcoded 1024; reasoning models like GLM-4.7
  need much larger budgets
- Guard against empty LLM content (reasoning models can burn all tokens
  on chain-of-thought and return content: null)
- Simplify notification routing: try configured channel first, fall back
  to broadcast_all so heartbeat alerts always reach someone
- Add ModelMetadata struct and model_metadata() to LlmProvider trait
- Refactor NearAiChatProvider::list_models into shared fetch_models()
- Add standalone test_heartbeat example for isolated debugging

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

* Add job detail view with drill-down from jobs list

Click a job row to see full details across four sub-tabs:
Overview (metadata grid, description, state transitions timeline),
Actions (expandable tool call cards with input/output JSON),
Thinking (conversation messages styled by role), and
Files (embedded workspace tree browser).

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

* Strip model-internal XML tags from LLM responses, fix Telegram parse_mode 400

Some models (GLM-4.7, etc.) emit <tool_call>tool_list</tool_call> in the
content field instead of using the OpenAI tool_calls array. This XML leaks
through to channels as text, and Telegram's Markdown parser chokes on the
underscores, returning 400 "can't parse entities".

Two fixes:
- Generalize clean_response() to strip <tool_call>, <function_call>,
  <tool_calls>, and pipe-delimited variants (<|tool_call|>) alongside
  the existing <thinking> tag stripping
- Add Telegram send_message helper with parse_mode fallback: try Markdown
  first, retry as plain text on "can't parse entities" 400 errors

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

* Add SystemCommand submission type for thread-state-independent commands

System commands (/help, /model, /version, /tools, /ping, /debug) now
bypass thread-state checks and safety validation via a dedicated
Submission::SystemCommand variant. Previously these flowed through
process_user_input() which blocked them during Processing/AwaitingApproval
/Completed states.

- Add /model [name] for runtime model switching with provider validation
- Add active_model_name()/set_model() to LlmProvider trait with RwLock
  hot-swap in both NEAR AI providers
- Rewrite /help with aligned columns grouped by category
- Expand REPL tab-completion from 10 to 23 slash commands
- Remove REPL-local /help interception (now handled by agent)

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

* Add per-tool execution timeouts, auto-create sandbox project dirs, serve built files

The sandbox e2e pipeline (agent -> container -> built website -> browsable URL)
was broken by three gaps: hardcoded 60s timeouts killed sandbox jobs that need
minutes, no auto-created project directory meant container output vanished, and
no HTTP route to browse the built files.

- Add `execution_timeout()` to the `Tool` trait (default 60s), replace all four
  hardcoded `Duration::from_secs(60)` call sites (agent_loop, worker, scheduler,
  worker/runtime) with the per-tool value
- Override to 660s in `RunInSandboxTool` (10 min polling + 60s buffer)
- Auto-create `~/.ironclaw/projects/{uuid}/` when no `project_dir` is specified,
  so every sandbox job gets a persistent bind mount
- Include `project_dir` and `browse_url` in sandbox tool output JSON
- Add `/projects/{id}` and `/projects/{id}/{path}` static file serving routes
  to the web gateway with path traversal protection and MIME type detection
- Add `mime_guess` dependency for content-type detection

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

* Apply cargo fmt to wizard.rs after merge

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

* Persist sandbox jobs in DB, fix web UI, unify job model

Sandbox container jobs were invisible to the web UI because they lived
only in ContainerJobManager's in-memory HashMap while the API queried
ContextManager. This persists them to the agent_jobs table and fixes
all six front-end bugs (empty job list, broken back button, empty
actions/thinking tabs, wrong files tab, stuck status, no persistence).

Key changes:
- V4 migration adds project_dir and user_id columns to agent_jobs
- Embedded migrations via refinery (no external CLI needed)
- SandboxJobRecord CRUD in Store with fire-and-forget DB writes
- Unified job_id: sandbox tool generates UUID, passes to ContainerJobManager
- Web API queries DB for sandbox jobs, merges with ContextManager direct jobs
- New endpoints: restart, project file list/read with path traversal protection
- Front-end: rebuild DOM on back navigation, sandbox-aware tabs, job cards in
  chat stream, source badges, restart button for failed/interrupted jobs
- Gateway defaults to enabled, prints Web UI URL on startup
- Stale jobs marked "interrupted" on restart for visibility and restartability

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

* Secure in-chat auth: tokens never touch the LLM or chat history

Remove the token parameter from tool_auth so the LLM cannot pass raw
API keys. Add dedicated REST (POST /api/chat/auth-token) and WebSocket
(auth_token) endpoints that route tokens directly to ext_mgr.auth(),
completely bypassing the message pipeline, turns, history, and compaction.

Web UI shows an auth card (password input + OAuth button) when the agent
enters auth mode, submitted via the dedicated endpoint. CLI auth mode
interception is unchanged (already secure).

New StatusUpdate::AuthRequired/AuthCompleted variants propagate through
all channels (SSE, WebSocket, REPL, WASM).

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

* feat: Add Claude Code mode for sandbox jobs

Run Claude Code CLI inside Docker containers as an alternative to the
standard worker mode. The bridge spawns `claude -p` with stream-json
output, posts events to the orchestrator, and supports follow-up
prompts via `--resume`.

Key additions:
- `claude-bridge` CLI subcommand and ClaudeBridgeRuntime
- JobMode enum (Worker vs ClaudeCode) with per-mode container config
- Orchestrator endpoints for Claude events and prompt polling
- SSE event variants for real-time Claude Code streaming to frontend
- Claude Code sub-tab in web UI with terminal-style output and input bar
- Database migration for job_mode column and claude_code_events table
- ClaudeCodeConfig with env var support (CLAUDE_CODE_ENABLED, etc.)
- Mode parameter on run_in_sandbox tool schema

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

* fix: Skip create_job tool when sandbox is enabled to prevent duplicate jobs

When sandbox mode is on, the LLM would call create_job (creating a
pending "direct" entry) then run_in_sandbox (creating a second "sandbox"
entry), producing two jobs in the list for a single user request.

Now register_job_tools() skips create_job when sandbox is enabled since
run_in_sandbox already creates tracked jobs. Also improved the
run_in_sandbox description to guide the LLM to use it directly and to
mention wait=false for async execution.

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

* feat: Web gateway UI quality-of-life improvements

Phase 1: Send button disabled state to prevent double-sends, copy button
on code blocks, confirm() guards on destructive actions, SSE-driven job
list auto-refresh, log filters re-applied on tab switch, jobEvents memory
leak fix (cap at 500, cleanup after 60s).

Phase 2: Toast notification system replacing chat-based system messages,
memory search highlighting with centered snippets, keyboard shortcuts
(Ctrl+1-5 tabs, Ctrl+K focus, Ctrl+N new thread, Escape close/blur),
activity tab toolbar with event type filter and auto-scroll toggle.

Phase 3: Thread sidebar with load/switch/create, thread_id passed with
messages, collapsible to hamburger. Memory inline editing with textarea,
Save/Cancel, POST to /api/memory/write.

Phase 4: Gateway status popover on hover (polls every 30s), extension
install form (name/URL/kind), markdown rendering in memory viewer for
.md files, mobile responsive layout at 768px breakpoint.

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

* feat: Add routines system, remove non-sandbox job mode from web UI

Routines: scheduled & reactive job system with cron and event triggers,
lightweight (single LLM call) and full-job execution modes, guardrails
(cooldown, max concurrent, dedup), and LLM-facing tools for CRUD.

Web UI: remove ContextManager-backed "direct" job mode entirely. Jobs
are now exclusively sandbox-backed (DB + container). Simplify job detail
response, drop dead types (ActionInfo, MessageInfo, MessageToolCallInfo),
fix Browse Files CSS loading (trailing-slash redirect), fix Activity tab
event rendering.

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

* fix: Re-enable chat input on agent completion, auto-auth on tool_activate, recover tool calls from content XML

Three fixes:

1. Chat input stays disabled after agent finishes: the "Done" status
   SSE event now calls enableChatInput() as a safety net when the
   response event is empty or lost. Same for auth_completed and
   cancelAuth().

2. tool_activate never triggers auth: when activation fails due to
   missing authentication, it now auto-initiates the auth flow
   (same pattern as the web API handler). detect_auth_awaiting()
   also matches tool_activate results now.

3. Models like GLM-4.7 emit tool calls as XML tags in content
   (<tool_call>tool_list</tool_call>) instead of using the structured
   tool_calls array. recover_tool_calls_from_content() extracts and
   validates these before falling back to plain text.

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

* feat: Add routines web UI tab, update docs for sandbox-jobs branch

Add full routines management to the web gateway (list, detail, trigger,
toggle, delete) with 7 new API endpoints, response types, and frontend
(HTML, JS, CSS). Update FEATURE_PARITY.md (~23 rows), CLAUDE.md (new
subsystems, config, TODOs), and README.md (architecture diagram,
features, components, fix onboard command).

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

* fix: Bind Telegram bot to owner account during setup

Without owner binding, anyone who discovers the bot can send it messages.
The setup wizard now prompts the user to message their bot, captures their
Telegram user ID via getUpdates, and persists it as telegram_owner_id in
settings. On startup, the owner_id is injected into the WASM channel config
so the existing owner restriction logic drops messages from non-owners.

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

* feat: Move settings from disk to PostgreSQL database

Settings previously lived in three JSON files on disk (settings.json,
mcp-servers.json, session.json). This made them inaccessible from the
web UI and caused redundant disk reads (Settings::load() called 8+
times during startup).

Now all settings live in a `settings` table (user_id + key -> JSONB)
with only 4 bootstrap fields remaining on disk (database_url, pool
size, secrets key source, onboard_completed) since they're needed
before the DB connection exists.

- Add V8 migration for settings table
- Add BootstrapConfig (thin disk file) and Settings DB round-trip
- Add Store CRUD methods for settings (get/set/delete/list/bulk)
- Refactor Config to load from DB (env > DB > default cascade)
- Add SessionManager DB persistence for session tokens
- Add DB-backed MCP server config load/save functions
- Add 6 settings web API endpoints (list/get/set/delete/export/import)
- Add one-time disk-to-DB migration on first boot
- Make CLI config commands async with DB access (disk fallback)

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

* feat: Seed workspace on boot, fix gateway duplicate logs and URL auto-auth

- Add Workspace::seed_if_empty() to create core identity files (README,
  MEMORY, IDENTITY, SOUL, AGENTS, USER, HEARTBEAT) when missing, called
  on every boot without overwriting existing user edits
- Remove duplicate gateway log lines from web/mod.rs (main.rs has the
  useful clickable ?token= URL)
- Auto-authenticate from ?token= URL parameter in the web UI and strip
  the token from the address bar after successful auth

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

* fix: Harden sandbox security (path traversal + orchestrator auth)

Two vulnerabilities fixed:

1. project_dir path traversal: The create_job tool let the LLM specify
   arbitrary host paths for Docker bind mounts. Removed project_dir from
   the tool schema entirely, and added canonicalization + prefix validation
   at both resolve_project_dir() and the job_manager bind mount point.

2. Orchestrator API auth bypass: worker_auth_middleware was defined but
   never applied. Each handler manually called validate_token(), so any
   new endpoint that forgot would be publicly accessible. Applied the
   middleware as route_layer on all /worker/ routes, removed manual auth
   from all 7 handlers. Bind to 127.0.0.1 on macOS/Windows (Linux keeps
   0.0.0.0 since containers reach host via docker bridge, not loopback).

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

* feat: Rework gateway chat with pinned assistant, pagination, and NEAR AI response chaining

Implements the 4-phase plan for overhauling the web gateway chat:

- Phase 1: Pinned "Assistant" thread at top of sidebar, regular threads below
- Phase 2: Cursor-based history pagination with infinite scroll
- Phase 3: NEAR AI previous_response_id chaining (delta-only messages),
  with fallback to full history on chain errors, and DB persistence of
  chain state across restarts
- Phase 4: SSE thread isolation (events filtered by thread_id)

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

* fix: Add per-request HTTP timeout to WASM host, redact credentials in errors

Three fixes for WASM channel reliability:

1. Per-request timeout: Add optional timeout-ms parameter to http-request
   in both channel and tool WIT interfaces. Telegram long-poll now specifies
   35s (outliving the 30s server-side hold), while regular API calls use
   the 30s default. Fixes the triple-30s timeout race that caused polling
   failures.

2. Credential redaction: reqwest::Error includes the full URL (with injected
   bot tokens) in its Display output. Scrub credential values from error
   messages before logging or returning to WASM.

3. Webhook route registration: Remove tunnel URL gate so webhook routes are
   always available when webhook channels exist, not only when TUNNEL_URL
   is configured.

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

* chore: Fix clippy warnings in WASM tools and channels

- slack channel: allow dead_code on signing_secret_name (forward compat field)
- gmail tool: use div_ceil() instead of manual (n+2)/3
- google-calendar tool: extract CreateEventParams/UpdateEventParams structs
  to fix too-many-arguments warnings

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

* Fix approval flow

* fix: Rebuild bundled telegram.wasm with updated WIT interface

The bundled WASM binary must match the host's WIT definition.
Previous binary was compiled against the old 4-arg http-request;
this rebuild includes the new timeout-ms parameter.

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

* refactor: Load WASM channels from disk instead of bundling in binary

Remove include_bytes! embedding of telegram.wasm. Channels are now
loaded from their build output directories (channels-src/<name>/target/)
during onboarding, then from ~/.ironclaw/channels/ at runtime.

- bundled.rs: locate_channel_artifacts() finds WASM + capabilities from
  build output; IRONCLAW_CHANNELS_SRC env var overrides the default path
- available_channel_names(): only lists channels with build artifacts
- bundled_channel_names(): lists all known channels (manifest)
- Setup wizard uses available_channel_names() to offer installable channels
- Add *.wasm to .gitignore, remove tracked telegram.wasm

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

* fix: Persist gateway auth token, fix thread hydration race, polish auth screen

Three web gateway UX fixes:

1. Token persistence: Store auth token in sessionStorage so refreshing
   the page doesn't force re-authentication. Hide the auth screen
   immediately when a saved token exists to prevent flash.

2. Thread hydration: Remove the !msgs.is_empty() bail-out in
   maybe_hydrate_thread so that even brand-new (empty) assistant threads
   get hydrated with their correct DB UUID. Previously resolve_thread
   would mint a fresh UUID, causing messages to land in the wrong
   conversation and duplicate threads to appear.

3. Auth screen: Redesign as a centered card with brand, tagline, labeled
   input, and hint text.

Also adds 34 new tests covering session/thread lifecycle, thread
resolution isolation (user, channel, external ID), hydration edge cases,
serialization round-trips, approval flows, and stale mapping recovery.

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

* fix: Use bindgen! for WASM tool wrapper, add dev tool loading

Three changes:

1. Rewrite src/tools/wasm/wrapper.rs to use wasmtime::component::bindgen!
   instead of manual linker.root().func_wrap(). This fixes the
   "component imports instance 'near:agent/host', but a matching
   implementation was not found in the linker" error. All 6 host functions
   (log, now-millis, workspace-read, http-request, secret-exists,
   tool-invoke) are now properly registered under the near:agent/host
   namespace. Also adds WASI support, credential injection, and leak
   detection for HTTP requests made by WASM tools.

2. Add dev tool loading to src/tools/wasm/loader.rs. During startup, the
   loader now also scans tools-src/*/target/wasm32-wasip2/release/ for
   build artifacts that are newer than installed copies. This means during
   development you just rebuild the WASM and restart the host; no manual
   copy step needed. Set IRONCLAW_TOOLS_SRC to override the source dir.

3. Wire up load_dev_tools() in main.rs alongside the existing
   load_from_dir() call.

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

* feat: Wire main startup and CLI to use DB-backed settings

main.rs now reloads Config from the database after connecting,
attaches the store to the session manager for dual-write tokens,
and loads MCP servers from DB instead of disk. ExtensionManager
and MCP CLI commands use DB when available with disk fallback.

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

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-11 08:31:25 +00:00
Illia Polosukhin 9b729795fb Merge remote-tracking branch 'origin/main' into ui
# Conflicts:
#	src/channels/mod.rs
#	src/main.rs
2026-02-06 13:22:26 -08:00
Illia PolosukhinandClaude Opus 4.6 8be390afab Rename setup CLI command to onboard for compatibility
Serde alias on `onboard_completed` preserves existing settings.json files
that still have the old `setup_completed` key.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-06 09:56:32 -08:00
Illia PolosukhinandClaude Opus 4.6 4d0fe7d37e Replace TUI (ratatui) with REPL (rustyline + termimad)
Drop the full Ratatui TUI in favor of a lighter REPL channel built on
rustyline (line editing, history, tab-completion) and termimad (inline
markdown rendering). Removes ratatui and crossterm event-stream deps,
adds rustyline and termimad. Simplifies main.rs startup to use the REPL
directly instead of the alternate-screen TUI.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-06 09:00:32 -08:00
Illia PolosukhinandClaude Opus 4.6 f3c85f57fc Add proactivity features: memory CLI, session pruning, self-repair notifications, slash commands, status diagnostics, context warnings
Closes the proactivity gap with six features:

- Memory CLI (`ironclaw memory search/read/write/tree/status`) for direct workspace access without starting the full agent
- Session pruning background task that evicts idle sessions (configurable TTL, default 7 days)
- Self-repair notifications broadcast recovery results through channel manager instead of silent logging
- `/heartbeat`, `/summarize`, `/suggest` slash commands for manual heartbeat trigger, thread summarization, and next-step suggestions
- `ironclaw status` diagnostics command checking DB, session, secrets, embeddings, WASM tools, channels, heartbeat, and MCP servers
- Context pressure warning that notifies users before auto-compaction fires

Co-Authored-By: Claude Opus 4.6 <[email protected]>
2026-02-05 19:28:13 -08:00
Illia PolosukhinandClaude Opus 4.5 974bc8d407 Add hosted MCP server support with OAuth 2.1 and token refresh
Enables connecting to official MCP servers (like Notion) instead of
building custom WASM tools. Uses OAuth 2.1 with PKCE and supports
Dynamic Client Registration for zero-config authentication.

Key features:
- OAuth 2.1 flow with PKCE for secure browser-based auth
- Dynamic Client Registration (DCR) for servers without pre-configured clients
- Automatic token refresh on 401 responses
- Session management with Mcp-Session-Id headers
- SSE streaming response handling

New CLI commands:
- `mcp add <name> <url>` - Add an MCP server
- `mcp remove <name>` - Remove an MCP server
- `mcp list` - List configured servers
- `mcp auth <name>` - Authenticate with a server
- `mcp test <name>` - Test connection

Also removes the Notion WASM tool example since it's superseded by the
Notion MCP server which provides 13 official tools.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-05 17:43:26 -08:00
Illia PolosukhinandClaude Opus 4.5 0ab9643843 Add interactive setup wizard and persistent settings
- Add 7-step setup wizard: database, security, auth, model, embeddings, channels, heartbeat
- Store settings in ~/.ironclaw/settings.json with env var > settings > default priority
- Add OS keychain integration for secrets master key (macOS/Linux)
- Add `ironclaw config` CLI subcommand (list/get/set/reset/path)
- Expand Settings struct with all configuration fields
- Enhanced setup detection to auto-trigger wizard when needed

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-05 09:50:52 -08:00
Illia PolosukhinandClaude Opus 4.5 598dd43b1c Rebrand to IronClaw with security-first mission
Renamed project from "near-agent" to "ironclaw" throughout the codebase.
Updated documentation to emphasize the core philosophy:
- Your data stays yours (local, encrypted, no telemetry)
- Self-expanding capabilities (build tools on the fly)
- Defense in depth (WASM sandbox, prompt injection defense)
- Always on user's side

Key changes:
- Package name: near-agent -> ironclaw
- Config paths: ~/.near-agent/ -> ~/.ironclaw/
- Database name in docs: near_agent -> ironclaw
- CLI binary: near-agent -> ironclaw
- Log filters: RUST_LOG=near_agent -> RUST_LOG=ironclaw
- All user-facing strings (welcome messages, help text, etc.)

Preserved for compatibility:
- HKDF salt "near-agent-secrets-v1" (changing would break existing secrets)
- WIT interface names (near::agent::*)
- NEAR AI provider config (NEARAI_* env vars)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-05 01:10:40 -08:00
Illia PolosukhinandClaude Opus 4.5 c7f0e8014d Add interactive setup wizard for first-run configuration
Introduces `near-agent setup` command that guides users through:
- NEAR AI authentication (reuses existing OAuth flow)
- Model selection (fetches from API or shows defaults)
- Channel configuration (HTTP webhook, Telegram)

Features:
- First-run detection: auto-runs wizard if no session exists
- Respects existing settings: shows current model with keep/change option
- Saves channel secrets to ~/.near-agent/secrets/ with 0600 permissions
- Validates Telegram bot tokens via API before saving

Also fixes default NEARAI_BASE_URL to use cloud-api.near.ai (api.near.ai
returns 410 Gone).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-04 09:42:41 -08:00
Illia Polosukhin 2c26ba8431 Support non interactive mode and model selection 2026-02-03 14:47:26 -08:00
Illia PolosukhinandClaude Opus 4.5 d1cb748914 Add CLI subcommands for WASM tool management
Introduces a new CLI module with subcommands for managing WASM tools:
- `tool install`: Build and install tools from source or .wasm files
- `tool list`: List installed tools with optional verbose output
- `tool remove`: Remove installed tools
- `tool info`: Show detailed tool information including capabilities

The install command supports building from Cargo source directories using
cargo-component, or installing pre-compiled .wasm files directly. It
auto-detects capabilities JSON sidecar files and validates them before
installation.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-03 00:05:12 -08:00