mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
b441ebec02bdedf650abbcc89c6321b477247504
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b441ebec02 |
feat: multi-tenant auth with per-user workspace isolation (#1118)
* feat: multi-tenant auth with per-user scoping Multi-user authentication and authorization for IronClaw gateway: - Token-based auth mapping tokens to user IDs via GATEWAY_USER_TOKENS - Per-user SSE broadcast scoping - Per-user rate limiting with poisoned lock recovery - Handler auth and ownership checks for jobs, settings, routines - Extension secrets scoped per-user - Chat handlers use authenticated identity - Reverse proxy deployment documentation - Comprehensive integration tests for auth, SSE, rate limiting, and job isolation * fix: scope memory tools per-user in multi-tenant mode Memory tools (search, write, read, tree) held a single workspace created at startup with GATEWAY_USER_ID. In multi-tenant mode, all users' tool calls searched the default user's scope. Add WorkspaceResolver trait that resolves workspaces per-request using JobContext.user_id. In single-user mode, returns the startup workspace. In multi-tenant mode (GATEWAY_USER_TOKENS configured), creates and caches per-user workspaces on demand. Includes regression tests for workspace resolution and user isolation. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: comprehensive multi-tenant isolation audit Address all review findings from @serrrfirat plus 7 additional gaps found via full security audit: Reviewer findings (5): - WorkspacePool now applies search config, memory layers, embedding cache, identity read scopes, and global config scopes (was bare) - jobs_summary_handler uses per-user queries instead of global counters - jobs_prompt_handler restructured to not 404 agent jobs + ownership check - jobs_restart_handler agent branch now verifies user ownership - agent_job_summary_for_user added to Database trait + both backends Audit findings (7): - Delete dead handlers/memory.rs (stale copies with no auth) - Add AuthenticatedUser to logs_events, logs_level_get, logs_level_set - Add AuthenticatedUser to extensions_tools_handler, gateway_status_handler - Add auth + ownership checks to all 6 routines handlers - Add auth to all 4 skills handlers with audit logging on mutations - Scope extension setup SSE broadcast to user (broadcast_for_user) - Fix pre-existing test compilation errors in extensions/manager.rs 17 new multi-tenant isolation tests covering: - WorkspacePool config propagation and scope merging - Jobs handler per-user isolation (summary, restart, prompt, cancel) - Routines handler auth enforcement and cross-user rejection - Auth middleware enforcement on logs, skills, status endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: second-pass multi-tenant audit — scope SSE broadcasts, DB queries, dead handlers Second audit pass applying learned patterns across the codebase: - OAuth callback SSE broadcasts now use broadcast_for_user (lines 773, 912) - jobs_list_handler uses list_agent_jobs_for_user instead of fetching all users' jobs and filtering in Rust - list_agent_jobs_for_user added to Database trait + postgres + libsql - Dead handler files (extensions.rs, static_files.rs) hardened with AuthenticatedUser to prevent auth regression if migrated Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review findings — token hashing, broadcast scoping, error handling Security fixes: - Hash tokens with SHA-256 at construction time so authentication compares fixed-size 32-byte digests, eliminating length-oracle timing leaks - Scope auth SSE broadcasts per-user in chat_auth_token_handler — AuthRequired/AuthCompleted events were leaking across tenants - Propagate DB errors in restart handlers instead of silently swallowing via `if let Ok(Some(...))` pattern Code quality: - Log SSE serialization failures instead of silently producing empty strings via unwrap_or_default() - Remove dead `pub type AuthState = MultiAuthState` alias - Replace `.unwrap()` with `Arc::clone(db)` in app.rs multi-tenant workspace setup (db is guaranteed Some in context, but unwrap violates project convention) - Fix telegram setup test to inject UserIdentity into request extensions (handler now requires AuthenticatedUser) - Add safety comments on test-only expect/unwrap calls for CI - Apply cargo fmt to fix pre-existing formatting Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address review findings — unify workspace pool, fix SSE regression, cache job owners - Unify WorkspacePool and PerUserWorkspaceResolver: WorkspacePool now implements WorkspaceResolver, eliminating duplicate per-user workspace construction logic. app.rs uses WorkspacePool directly. - Fix sse_tx: None scheduler regression: change scheduler/worker SSE broadcasting from broadcast::Sender<SseEvent> to Arc<SseManager>, restoring SSE event delivery for scheduled agent jobs. - Cache job owner in orchestrator: add job_owner_cache to OrchestratorState so job_event_handler avoids a DB round-trip on every event after the first per job. - Deduplicate ext_user_id computation in main.rs. - Remove unused _gateway_state variable. - Fix pre-existing test: first_token() returns None in multi-user mode by design; align test assertion. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * style: fix formatting in app.rs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor: extract memory handlers back into handlers/memory.rs Move memory API handlers out of server.rs into their own module, consistent with how jobs, routines, and skills handlers are organized. The resolve_workspace() helper moves with them since it is only used by memory handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> Co-authored-by: [email protected] <[email protected]> |
||
|
|
3aa36c8f55 |
fix(tests): eliminate env mutex poison cascade (#1558)
* fix(tests): eliminate env mutex poison cascade and fix test flakiness The shared ENV_MUTEX used by ~68 config tests would cascade a single test panic into failures across every module. Replace all .unwrap() / .expect() lock acquisitions with a poison-recovering lock_env() helper. Consolidate rogue module-local ENV_LOCK instances (workspace, orchestrator, bootstrap) onto the shared global mutex to prevent cross-module races. Also fixes: - gateway user_id fallback was hardcoded to "default" instead of owner_id - test_ironclaw_env_path used LazyLock which is order-dependent Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * test(helpers): add regression test for lock_env poison recovery Satisfies the regression-test-check CI gate by adding a test that intentionally poisons ENV_MUTEX and verifies lock_env() recovers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(ci): detect test changes inside #[cfg(test)] regions The regression test check relied on git diff -W to expand context to function boundaries, but git doesn't recognize Rust `mod tests {}` as a function boundary. Changes to imports, helpers, or lock calls inside test modules were invisible to the check. Add a line-level fallback: for each changed .rs file, find where #[cfg(test)] starts and check if any diff hunk targets a line at or after that boundary. This catches edits anywhere inside test modules regardless of git's language awareness. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review feedback - Clear ENV_MUTEX poison after regression test so it doesn't leave global state dirty for subsequent tests. - Fix CI regression-test-check to match #[cfg(test)] only when followed by `mod` (the test module pattern), avoiding false positives from standalone #[cfg(test)] items like statics or functions. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
9d538136b5 |
fix(oauth): reject malformed ic2.* states in decode_hosted_oauth_state (#1441) (#1454)
* fix(oauth): reject malformed ic2.* states instead of falling through to legacy handler (#1441) When decode_hosted_oauth_state() encountered a versioned state (ic2.*) that failed to fully parse (bad base64, invalid JSON, missing separator), it silently fell through to legacy handling which used the full malformed envelope as the flow_id. This never matched the raw nonce stored in pending_oauth_flows, breaking the OAuth callback. Restructure the versioned decode path so any ic2.* state must parse as a valid envelope or return Err — never fall through to legacy handling. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(oauth): address PR review — avoid alloc in strip_prefix, strengthen JSON parse test - Replace `strip_prefix(&format!(...))` with a `HOSTED_STATE_PREFIX_DOT` constant to avoid per-call allocation. - Fix "valid base64 but not JSON" test to compute the correct checksum so it actually exercises the JSON parse error path instead of stopping at the checksum check. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: add missing fallback_deliverable field in job_monitor tests The SseEvent::JobResult struct gained a fallback_deliverable field in the structured fallback deliverables feature, but the job_monitor test constructors were not updated. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(oauth): remove HOSTED_STATE_PREFIX_DOT to avoid drift with HOSTED_STATE_PREFIX concat! requires literals and cannot reference const items, so a separate _DOT constant would duplicate the prefix string. Revert to deriving the dotted prefix via format!() — both encode and decode now use the same single HOSTED_STATE_PREFIX constant, keeping them mechanically consistent. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
c4ab382522 |
Make hosted OAuth and MCP auth generic (#1375)
* Make hosted OAuth and MCP auth generic * Address PR feedback and lint issues * Suppress built-in Google secret in hosted proxy flows * Align hosted OAuth secret suppression with proxy config * Harden hosted OAuth callback helpers * Tighten hosted OAuth URL rewriting |
||
|
|
8a26cfae73 |
fix(mcp): open MCP OAuth in same browser as gateway (#951)
* fix(mcp): use gateway callback for MCP OAuth so auth opens in same browser When MCP OAuth is triggered from the web gateway, the auth URL was being opened via `open::that()` which launches the OS default browser instead of the browser already running the gateway UI. This changes the MCP OAuth flow to use the same gateway callback pattern as WASM extensions: in gateway mode, the auth URL is returned to the frontend via SSE and opened with `window.open()`, keeping the user in the same browser. Also adds RFC 8707 `resource` parameter support to the gateway token exchange path, scoping issued tokens to the correct MCP server. Closes #299 Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: cargo fmt Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(mcp): persist DCR client_id in gateway OAuth callback for token refresh The gateway callback handler stored access and refresh tokens but not the DCR client_id. When the token expired, refresh failed with "No client ID found" because get_client_id() could not find it in secrets. Adds client_id_secret_name to PendingOAuthFlow so the gateway callback handler persists the client_id alongside the tokens, matching the behavior of the CLI flow in authorize_mcp_server(). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(mcp): return AuthRequired on 401 so activate triggers OAuth flow activate_mcp() returned ActivationFailed for all errors including 401 auth responses, so the activate handler never triggered the OAuth flow. Now 401/auth errors return AuthRequired, which the handler detects and redirects to the OAuth flow — matching the WASM extension pattern. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(mcp): fix gateway OAuth flow, approval cards, and auto-activation - Add explicit gateway_mode flag on ExtensionManager (set at startup by web gateway) so MCP OAuth returns auth URLs to the frontend instead of calling open::that() on the server machine. - Auto-activate extensions after successful OAuth callback so the UI transitions from "Activate" to "Active" without a second click. - Send ApprovalNeeded status (not generic "Awaiting approval") from thread_ops.rs for all three NeedApproval paths so the web UI shows approval cards for deferred tool calls. - Remove duplicate ApprovalNeeded send from agent_loop.rs (thread_ops.rs is now the canonical sender). - Skip approval for tool_auth in gateway mode since it only returns a URL. - Revert fragile active-server detection heuristic from system prompt. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review findings - Use Release/Acquire ordering for gateway_mode AtomicBool instead of Relaxed to ensure visibility across threads. - Report activation failure as error in OAuth callback SSE event instead of silently falling back to the success message. - Fix EnvGuard::drop to remove env var when original was unset. - Replace hardcoded /tmp/ path with std::env::temp_dir() in test helper. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test(mcp): add E2E trace test for MCP extension lifecycle with mock server Add a full MCP extension lifecycle E2E test that exercises: - Turn 1: tool_search → tool_install → text (extension discovery and install) - Token injection + activate (simulating OAuth completion) - Turn 2: MCP tool calls (notion-search → notion-fetch → text) Includes a mock MCP server (tests/support/mock_mcp_server.rs) with OAuth discovery, DCR, token exchange, and JSON-RPC endpoints. The mock server validates Bearer auth and serves pre-configured tool responses. Also adds inject_registry_entry() to ExtensionManager for test use and exposes extension_manager from TestRig. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review findings (round 2) - Only fall back to manual token entry on AuthNotSupported, propagate real errors from auth_mcp_build_url() instead of masking them - Use mcp:-prefixed provider string in PendingOAuthFlow for consistency with CLI MCP auth token storage - Only persist client_id_secret_name for DCR flows (not pre-configured OAuth) - Fix gateway_callback_redirect_uri to use /oauth/callback path - Bypass exchange proxy when flow has RFC 8707 resource parameter - Remove client_id double-prefix in oauth callback handler - Remove weak tests that didn't exercise production logic - Add clarifying comments for exchange_oauth_code delegation Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: keep OAuth success independent of activation, fix wait_for_responses scoping - OAuth success is now reported accurately even when auto-activation fails (tokens are already stored, so auth succeeded) - E2E test waits for turn1_count + 1 responses to ensure turn-2 behavior is actually observed Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> |
||
|
|
14aadd3063 |
refactor: make src/llm/ self-contained for crate extraction (#767)
* refactor: make src/llm/ self-contained for crate extraction Move LlmError, LLM config types, and OAuth callback helpers into src/llm/ so the module has zero `use crate::` imports outside of crate::llm. This prepares the module for extraction into a standalone workspace crate. - Move LlmError enum from src/error.rs to src/llm/error.rs - Move LlmConfig, NearAiConfig, RegistryProviderConfig, BedrockConfig, CacheRetention, OAUTH_PLACEHOLDER from src/config/llm.rs to src/llm/config.rs - Move OAuth callback utilities (callback_url, bind_callback_listener, wait_for_callback, landing_html, etc.) from src/cli/oauth_defaults.rs to src/llm/oauth_helpers.rs - Remove session.rs dependency on crate::bootstrap (inline default path) - Add cache_retention field to RegistryProviderConfig, resolve from env in config/llm.rs instead of reading env var in llm/mod.rs - Add Check 6 to scripts/check-boundaries.sh enforcing LLM isolation - All original locations re-export for backward compatibility [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix formatting Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR #767 review — session path bug and boundary check 1. Fix SessionConfig::default() usage in setup wizard: the fallback at wizard.rs:995 now constructs SessionConfig with the real default_session_path() instead of a relative "session.json", which would write auth tokens to the CWD instead of ~/.ironclaw/. 2. Widen check-boundaries.sh Check 6 to catch all `crate::` references (not just `use crate::` imports). Pre-existing inline references (16 occurrences) are reported as warnings; only new `use crate::` imports are hard violations. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR #767 review and audit findings in src/llm/ PR review fixes: - Reject wildcard addresses (0.0.0.0, ::) in OAuth callback listener to prevent session token exposure on all interfaces - Fix boundary check comment-stripping that could hide real violations (use sed to strip inline comments before matching) Audit fixes: - Fix UTF-8 byte-index slicing panic in recording.rs hint extraction - Add effective_model_name() delegation to RetryProvider and SmartRoutingProvider for consistency with other wrappers - Add calculate_cost() delegation to CachedProvider and RecordingLlm - Deduplicate retry loop logic in RetryProvider via generic helper - Replace hardcoded /tmp path in recording tests with tempfile Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> |
||
|
|
6a2a6cd050 |
fix(security): use OsRng for all security-critical key and token generation (#519)
* fix(security): use OsRng for all security-critical key and token generation Replace rand::thread_rng() with rand::rngs::OsRng in all security-critical code paths that generate cryptographic key material, bearer tokens, PKCE verifiers, CSRF state parameters, and webhook secrets. thread_rng() uses a userspace CSPRNG (ChaCha) seeded from OS entropy, which is fine for non-security contexts but adds an unnecessary intermediate layer for key material where direct OS entropy (OsRng) is the correct choice. Files changed: - src/secrets/keychain.rs: master encryption key generation - src/secrets/crypto.rs: per-secret HKDF salt generation - src/orchestrator/auth.rs: per-job bearer token generation - src/channels/web/mod.rs: gateway auth token fallback - src/cli/oauth_defaults.rs: OAuth PKCE verifier and CSRF state - src/tools/mcp/auth.rs: MCP OAuth PKCE verifier - src/extensions/manager.rs: auto-generated extension secrets - src/setup/channels.rs: webhook secret generation Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(security): address PR review feedback for OsRng migration - Remove shadowing inner `use rand::rngs::OsRng` in `generate_salt()`; use module-level `aes_gcm::aead::OsRng` import instead (same type, avoids divergence risk if rand_core versions drift) - Fix missed callsites in `pairing/store.rs`: `random_code()` and `generate_unique_code()` now use `OsRng` for pairing auth codes - Add regression tests for `generate_salt()`: correct length, non-zero output, uniqueness across calls Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> |
||
|
|
704d63f16a |
feat(oauth): route callbacks through web gateway for hosted instances (#555)
* feat: route OAuth callbacks through web gateway for hosted instances On hosted instances (e.g., NEAR AI), OAuth callbacks can't reach the local TCP listener on port 9876. This adds a gateway-routed OAuth flow that works behind reverse proxies and load balancers. Backend changes: - Add /oauth/callback as a public route on the web gateway - PendingOAuthFlow registry shared between ExtensionManager and handler - Gateway mode auto-detected via IRONCLAW_OAUTH_CALLBACK_URL env var - Platform state format (instance:nonce) for nginx routing - Token exchange proxy support via IRONCLAW_OAUTH_EXCHANGE_URL - Local TCP listener mode preserved as backward-compatible fallback UX improvements: - Hide Configure button for tools with auto-resolved OAuth credentials (builtin defaults or platform-injected env vars) - Skip client_id/client_secret fields in setup schema when auto-resolved - Show Reconfigure only after successful authentication Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(oauth): harden gateway callback and refactor AuthResult - Add 60s timeout to exchange_via_proxy HTTP client (matching exchange_oauth_code) - Read GATEWAY_AUTH_TOKEN once at ExtensionManager construction instead of per-flow from env (prevents coupling and clarifies token provenance) - Extract oauth_error_page() helper to deduplicate error landing pages - Remove IRONCLAW_FORCE_GATEWAY_CALLBACK env var (auto-detection suffices) - Refactor AuthResult into typed AuthStatus enum with constructors, eliminating stringly-typed status and Option fields that were always None - Adapt all handlers (chat, extensions, ws) to new AuthResult/AuthStatus API - Use setup_url (not validation_endpoint) for awaiting_token responses [skip-regression-check] Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(oauth): address review feedback — empty token guard, test flakiness, doc typos - Fail early in exchange_via_proxy() when gateway_token is empty instead of sending an unauthenticated request to the exchange proxy - Fix test_oauth_callback_strips_instance_prefix to use an expired flow so it never attempts a real HTTP token exchange (prevents CI flakiness) - Fix doc comments: /auth/callback → /oauth/callback in PendingOAuthFlow and ExtensionManager pending_oauth_flows docs [skip-regression-check] Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: clarify strip_instance_prefix safety, wrapper credential fix, test assertion - Add comment to strip_instance_prefix noting nonces are base64url (no colons) - Expand wrapper.rs comment explaining the credential_user_id bug fix - Fix test_oauth_callback_strips_instance_prefix assertion: landing_html does not include provider_name on error pages [skip-regression-check] Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
18b59ae9a7 |
feat: add OAuth support for WASM tools in web gateway (#489)
* feat: add OAuth support for WASM tools in web gateway Extract reusable OAuth functions (build_oauth_url, exchange_oauth_code, store_oauth_tokens, validate_oauth_token) from CLI into shared oauth_defaults module, then wire them into the web gateway's ExtensionManager. Key changes: - Install auto-activates WASM tools (no separate Activate button) - Configure button triggers OAuth flow via save_setup_secrets - Scope merging: installing a second Google tool triggers re-auth with merged scopes from all tools sharing the same secret_name - Cancel-and-retry: aborting stale OAuth listeners prevents port conflicts - Post-auth validation: wrong account detected via validation_endpoint - Reconfigure always re-auths (deletes old token before starting fresh) - UI shows error toast on OAuth failure, refreshes extension list Flow: Install → Active → Configure (enter client_id/secret) → Save → OAuth popup → authorize → done. Second Google tool install auto-triggers scope expansion OAuth. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address PR review comments - Add custom headers support to ValidationEndpointSchema (fixes missing Notion-Version header regression) - Guard activate handler auth check with status == "awaiting_authorization" to prevent unexpected OAuth popups - Add window dimensions to OAuth popup in activateExtension() - Simplify UTF-8 truncation boundary check Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: address Copilot PR review comments (security, UX, bugs) - Add CSRF state parameter to OAuth flow (random state in auth URL, validated in callback) - Restore MCP server Activate button in web UI (was hidden for all non-channel extensions) - Abort JoinHandle in cleanup_expired_auths to prevent port 9876 conflicts - Fix Google-specific error message for non-Google OAuth providers - Add has_auth field to ExtensionInfo API response (fixes Configure button visibility) - Use oauth_defaults::callback_url() instead of hardcoded redirect_uri (both CLI and manager) - Update auth check comment to match actual behavior (scope expansion + first-time auth) - Add unit tests for build_oauth_url (basic, PKCE, extra params, state uniqueness) - Check all required setup secrets (client_id + client_secret) before starting OAuth Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
37c0158765 |
Fix: allow OAuth callback to work on remote servers (fixes #186) (#212)
* fix: allow OAuth callback to work on remote servers via OAUTH_CALLBACK_HOST Fixes #186. The OAuth callback URL was hardcoded to `http://127.0.0.1:9876` in two places (NEAR AI login and MCP server auth). On a remote server this URL is unreachable from the user's browser, making authentication impossible. Changes: - Add `callback_host()` to `oauth_defaults` that reads `OAUTH_CALLBACK_HOST` (default: `127.0.0.1`) - Update `bind_callback_listener()` to bind to `0.0.0.0` when a non-loopback host is configured, so the port is reachable from outside the machine - Update `session.rs` and `mcp/auth.rs` to use `callback_host()` instead of hardcoded `127.0.0.1` / `localhost` Usage on a remote server: export OAUTH_CALLBACK_HOST=<your-server-ip> ironclaw login * fix: address PR review comments for OAuth callback security * fix: address serrrfirat review comments on PR #212 --------- Co-authored-by: firat.sertgoz <[email protected]> |
||
|
|
5725a62c83 |
fix: onboarding errors reset flow and remote server auth (#185, #186) (#248)
* fix: incremental settings persistence and remote server auth (#185, #186) Persist settings after each wizard step so failures don't lose prior progress. Load existing settings on re-run to recover from partial onboarding. Add manual token paste option for remote/headless servers where browser OAuth is unreachable, and support IRONCLAW_OAUTH_CALLBACK_URL for custom callback URLs. Color prompt output (green/red/blue prefixes). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: replace session token paste with API key entry, address PR review Replace option 4 in NEAR AI auth menu from session token paste to NEAR AI Cloud API key entry (cloud.near.ai). Also address all PR review feedback: restrict .env file permissions to 0o600, mask API key input with secret_input, fix libsql loaded flag in try_load_existing_settings, add ENV_MUTEX to oauth_defaults tests, and add NEARAI_API_KEY to secrets injection. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: deduplicate keys in upsert_bootstrap_var When the .env file contains duplicate keys (e.g. from manual editing), only write the replacement once and skip subsequent duplicates. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: NEARAI_SESSION_TOKEN env var takes precedence over file-based tokens Hosting providers inject session tokens via env var and expect them to be used directly. Previously the env var was only picked up when no session file existed and was treated as a legacy migration. Now the env var always wins, without persisting to disk. Co-Authored-By: Claude Opus 4.6 <[email protected]> * docs: distinguish NEAR AI Chat and NEAR AI Cloud providers Split documentation into two clearly named modes: - NEAR AI Chat: Responses API at private.near.ai, session token auth - NEAR AI Cloud: Chat Completions API at cloud-api.near.ai, API key auth Update default base URLs so each mode points to its correct endpoint. Update .env.example, deploy/env.example, CLAUDE.md, setup spec, and code comments across config/llm.rs, nearai.rs, nearai_chat.rs, mod.rs. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: wizard recovery ordering — load DB before persist, fresh choices win Previously, persist_after_step() ran after Step 1 but before try_load_existing_settings(), bulk-upserting defaults that clobbered prior settings. Additionally, merge_from gave stale DB values precedence over fresh Step 1 choices. Fix: snapshot Step 1 settings, load DB, then re-apply the snapshot. This ensures prior progress (steps 2-7) is recovered while fresh Step 1 choices override stale DB values. Add two tests verifying wizard recovery merge ordering. Addresses PR review comments from Copilot on wizard.rs:150, wizard.rs:1607, and wizard.rs:1626. Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix rustfmt formatting in config/llm.rs Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: collapse nested if per clippy collapsible_if lint Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: use print_success for API key confirmation, fix menu spacing - Use print_success() for colored output consistency in api_key_login - Fix box-drawing alignment: options 1-2 had an extra trailing space Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> |
||
|
|
f0a0642e7d |
feat: multi-provider inference + libSQL onboarding selection (#92)
* feat: add interactive database backend selection during onboarding Previously the onboarding wizard silently defaulted to PostgreSQL because libsql wasn't in the default feature set. Now both backends ship by default and the wizard presents a selection prompt when both are available. DATABASE_BACKEND env var still bypasses the prompt for headless/CI use. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: resolve libSQL onboarding crash, keychain double-prompt, and setup audit findings Three bugs fixed: 1. libSQL onboarding crash ("Missing required setting 'database_url'"): DatabaseConfig::resolve() only checked DATABASE_BACKEND env var, falling back to Postgres default. Now reads settings.database_backend, plus settings.libsql_path and settings.libsql_url as fallbacks. 2. OS keychain prompts twice during startup: Config::from_env() and Config::from_db() both called get_master_key(). Now caches the key in SECRETS_MASTER_KEY env var after first read so from_db() skips keychain. 3. "Path not found: nearai.session" warning: from_db_map() tried to apply app-specific DB keys (nearai.session_token) to the Settings struct. Now skips keys that don't map to known Settings fields. Also fixed bootstrap migration key mismatch (nearai.session -> nearai.session_token). Setup module audit fixes (14 findings): - Replace unreachable!() with proper error in provider match - Extract setup_api_key_provider() to deduplicate setup_anthropic/setup_openai - Add SAFETY comments to all unsafe std::env::set_var blocks - Fix .unwrap() calls with proper error handling - Remove incorrect #[allow(dead_code)] on used TelegramUpdate::update_id - Log warnings instead of silently discarding HTTP errors in Telegram binding - Guard select_many against empty options, fix mask_api_key for non-ASCII - Update stale doc comment in mod.rs, rename misleading variable - Add 7 new tests (model fetcher fallbacks, channel discovery, secret gen) Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR review feedback (set_var safety, parse warnings, db_map efficiency) 1. Replace unsafe set_var keychain caching with OnceLock<String> in SecretsConfig::resolve(). Eliminates the env var write from main.rs entirely, using a process-wide OnceLock cache instead. 2. Log tracing::warn when database_backend or llm_backend settings fail to parse, instead of silently falling back to defaults. 3. Remove O(K*S) get() pre-check in from_db_map(). Instead, let set() run and match on "Path not found" errors to skip unknown keys, avoiding full Settings serialization per key. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address critical/high audit findings across WASM sub-crates - Telegram: remove .unwrap() panic on workspace_read (owner_id check) - WhatsApp: use configured api_version instead of hardcoded v18.0 - WhatsApp: log config parse errors before falling back to defaults - Slack: log serialization errors in emit_message and json_response - Google Docs: safe array access for batch update replies - Google Sheets: safe array access for add_sheet replies - Google Calendar: fix doc comment secret name mismatch - Gmail: avoid unnecessary String allocation in UNREAD check Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address second-round PR review feedback - Validate custom model ID is non-empty (loop until valid input) - Warn on unknown DATABASE_BACKEND env var before defaulting to Postgres - Force re-selection when llm_backend contains unknown provider value - Use ok_or_else for proper String error type in google-sheets Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: harden setup module error handling and secret safety - Introduce ChannelSetupError typed enum replacing raw String errors across all channel setup functions (setup_telegram, setup_http, setup_tunnel, setup_wasm_channel, validate_telegram_token) - Add From<ChannelSetupError> for SetupError to simplify call sites - Convert setup_telegram retry from recursion to loop (unbounded stack) - Stop printing HTTP webhook secret plaintext to terminal - Use secret_input() for Turso auth token (was visible input()) - Replace dirs::home_dir().unwrap_or_default() with proper error - Fix UTF-8 panic in model name truncation (byte-index to chars-based) - Log warning in secret_exists() instead of silently swallowing errors - Deduplicate generate_webhook_secret() to delegate to shared helper Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: replace unreachable!() with error return in setup wizard The provider match in step_inference_provider was guarded by is_known but used unreachable!() as the catch-all. If a new provider is added to the is_known check without a corresponding match arm, this would panic at runtime. Return a typed error instead. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: remove unsafe set_var, use thread-safe overlay for injected secrets Address PR #92 review comments: - Replace all 5 unsafe `std::env::set_var()` calls with safe alternatives - Add INJECTED_VARS OnceLock<HashMap> overlay in config.rs, checked by optional_env() before falling back to std::env::var() - Cache wizard API key in SetupWizard.llm_api_key field instead of env - Pass explicit key param to fetch_anthropic_models/fetch_openai_models - Persist env-provided API keys to secrets store during onboarding Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address remaining PR review comments (clippy, TODO, secrets backend ordering) - Fix empty line after doc comment (clippy: empty_line_after_doc_comments) - Collapse nested if in optional_env overlay check (clippy: collapsible_if) - Remove dangling TODO(#XX) placeholder issue ref in channels.rs - Fix init_secrets_context to respect selected database_backend when both postgres and libsql features are compiled, preventing wrong-backend secrets storage when DATABASE_URL is set but libsql was chosen Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address latest PR review comments (SecretString, empty env, docs, embeddings) - Change wizard llm_api_key from String to SecretString to prevent accidental logging of API keys - Fix inject_llm_keys_from_secrets skipping when env var is set but empty, matching optional_env's treatment of empty as unset - Fix inverted doc comment on INJECTED_VARS (env checked first, overlay is the fallback, not the other way around) - Update stale "env vars" comments in main.rs to reflect overlay pattern - Fix step_embeddings not seeing cached OpenAI key from wizard session Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: OAuth callback listener binds IPv4 first to match redirect URLs The listener was binding to [::1] (IPv6) first, but NEAR AI and other OAuth flows redirect to http://127.0.0.1:9876/... (IPv4 explicit). On macOS and most systems, [::1] and 127.0.0.1 are separate addresses, so the browser's connection to 127.0.0.1 was refused when the listener was on [::1]. Reversed the bind order: try 127.0.0.1 first, fall back to [::1] if IPv4 is unavailable. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: cache keychain key eagerly to avoid redundant macOS password dialogs Replace has_master_key() with get_master_key() in step_security() and immediately build SecretsCrypto from the result. This eliminates redundant keychain accesses later in init_secrets_context(), each of which triggers macOS system dialogs (keychain unlock + app authorization). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: persist DATABASE_BACKEND to ~/.ironclaw/.env for libSQL startup The wizard saved database_backend only to the database, but Config::from_env() needs it BEFORE connecting to any database (to decide which backend to use). Without it, the backend defaults to Postgres and then fails with "Missing required setting database_url". Now save all database bootstrap vars (DATABASE_BACKEND, DATABASE_URL, LIBSQL_PATH, LIBSQL_URL) to ~/.ironclaw/.env via save_bootstrap_env(). Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: status command shows libSQL backend and skips keychain probe The status command only checked DATABASE_URL (postgres), showing "not configured" for libSQL users. Now detects the DATABASE_BACKEND env var and reports libSQL path and Turso sync status. Also remove the keychain probe from status. get_generic_password() triggers macOS unlock+authorization dialogs which is terrible UX for a read-only diagnostic command. Co-Authored-By: Claude Opus 4.6 <[email protected]> * style: fix rustfmt formatting in bootstrap test Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> |
||
|
|
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]> |