* feat(setup): add Anthropic OAuth and Codex OAuth onboarding flows
Add OAuth token authentication as an alternative to API keys during
onboarding for both Anthropic (via `claude login`) and OpenAI/Codex
(via `~/.codex/auth.json`).
Key changes:
- New `AnthropicOAuthProvider` using `Authorization: Bearer` header
(rig-core hardcodes `x-api-key` which rejects OAuth tokens)
- Wizard auth method selector: "Direct API Key" vs "OAuth Token"
for both Anthropic and OpenAI providers
- Codex token extraction from `$CODEX_HOME/auth.json` / `~/.codex/auth.json`
- Claude Code sandbox sub-step in Docker setup (checks for credentials)
- Secret injection mappings for `ANTHROPIC_OAUTH_TOKEN` and `CODEX_OAUTH_TOKEN`
- `CODEX_OAUTH_TOKEN` falls back to `OPENAI_API_KEY` (same Bearer auth)
Supersedes #143 which had a broken auth flow (OAuth token sent as
x-api-key → 401). Credit to @bigguybobby for the original approach.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: persist OAuth tokens in bootstrap .env and re-extract at startup
OAuth tokens stored only in the secrets DB were invisible to
Config::from_env() which runs before the DB connects (chicken-and-egg).
Two fixes:
1. write_bootstrap_env() now persists ANTHROPIC_OAUTH_TOKEN and
CODEX_OAUTH_TOKEN to ~/.ironclaw/.env (same pattern as NEARAI_API_KEY)
2. main.rs re-extracts a fresh token from the OS credential store
(macOS Keychain / ~/.claude/.credentials.json) before config resolution,
handling token expiry (8-12h) gracefully
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: persist all LLM credentials in bootstrap .env, not just NEAR AI
All providers had the same chicken-and-egg issue: API keys stored in the
secrets DB were invisible to Config::from_env() which runs before DB
connects. Only NEARAI_API_KEY was written to bootstrap .env.
Now write_bootstrap_env() persists all credential env vars:
NEARAI_API_KEY, ANTHROPIC_API_KEY, ANTHROPIC_OAUTH_TOKEN, OPENAI_API_KEY,
CODEX_OAUTH_TOKEN, LLM_API_KEY, TINFOIL_API_KEY.
Also: setup_api_key_provider() now sets the env var during the wizard
session so write_bootstrap_env() can pick it up.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address security review findings for OAuth onboarding
- Extract "oauth-placeholder" to named OAUTH_PLACEHOLDER constant shared
across config and wizard to prevent silent drift
- Document plaintext credential tradeoff in write_bootstrap_env (API keys
stored with 0o600 permissions, recommend full-disk encryption)
- Add blocking "Press Enter" wait in Anthropic OAuth retry flow so user
has time to run `claude login` in another terminal
- Add escape hatch from manual OAuth paste back to API key flow (empty
input switches to setup_api_key_provider)
- Fix Retry-After header: parse u64 seconds into Duration before passing
to LlmError::RateLimited
- Make config::llm module pub(crate) for constant visibility
- Use .bearer_auth() instead of manual format!("Bearer {}")
- Remove response body from debug log (may contain PII)
- Update Anthropic API version to 2024-10-22
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* security: remove plaintext credentials from bootstrap .env
Credentials (API keys, OAuth tokens) were being written in plaintext to
~/.ironclaw/.env to work around a chicken-and-egg problem: Config::from_env()
runs before the encrypted secrets DB is connected.
Instead of storing secrets on disk, LlmConfig::resolve() now defers
gracefully when credentials are missing — it returns None for the provider
config instead of hard-erroring with MissingRequired. After the DB connects,
AppBuilder::build_all() loads secrets from encrypted storage via
inject_llm_keys_from_secrets() and re-resolves the config.
For Anthropic OAuth tokens (which expire in 8-12h), the secret injection
step also tries the OS credential store (macOS Keychain / Linux
credentials.json) for a fresh token, overriding the potentially stale
copy in the DB.
Changes:
- LlmConfig::resolve(): OpenAI, Anthropic, OpenAI-compatible, and Tinfoil
all return None instead of MissingRequired when credentials are absent
- write_bootstrap_env(): no longer writes any credential env vars
- inject_llm_keys_from_secrets(): refreshes Anthropic OAuth from OS
credential store before overlay is finalized
- main.rs: removed OAuth re-extraction hack (no longer needed)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: load OS credential store tokens even without secrets DB
The OAuth token extraction from macOS Keychain / Linux credentials files
was only running inside inject_llm_keys_from_secrets(), which requires
the encrypted secrets DB. When no master key is configured, init_secrets()
returned early — skipping both DB secret loading AND OS credential store
extraction, leaving the Anthropic OAuth token unavailable.
Split into two paths:
- inject_llm_keys_from_secrets(): loads from encrypted DB + OS stores
- inject_os_credentials(): loads from OS stores only (no DB needed)
init_secrets() now calls inject_os_credentials() and re-resolves config
even in the no-master-key early-return path, so `claude login` tokens
are always available regardless of secrets DB state.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add anthropic-beta header required for OAuth authentication
Anthropic's api.anthropic.com requires the `anthropic-beta: oauth-2025-04-20`
header to accept OAuth Bearer tokens. Without it, the API returns 401
"OAuth authentication is currently not supported."
Also reverts API version to 2023-06-01 since the OAuth beta flag does
not support the 2024-10-22 version (returns 400 "not a valid version").
This was the same bug that caused PR #143's 401 errors — the beta header
was missing entirely.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: Anthropic and OpenAI model resolution respects selected_model
The Anthropic and OpenAI config resolution ignored settings.selected_model
entirely, only checking the provider-specific env var (ANTHROPIC_MODEL,
OPENAI_MODEL) and falling back to a hardcoded default. This meant the
model chosen during onboarding wizard was silently overridden.
Now follows the same pattern as NearAI and OpenAI-compatible:
env var > settings.selected_model > hardcoded default.
Also deduplicated the Anthropic config construction (two identical
branches for API key vs OAuth now share model/base_url resolution).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add provider resolution tests for all LLM backends
Covers deferred resolution (no credentials → None instead of error),
credential presence, model selection fallback chain, and OAuth token
routing for Anthropic, OpenAI, Tinfoil, Ollama, and NearAI.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: handle nested tokens.access_token format in Codex auth.json
Codex CLI stores OAuth tokens in a nested format under
tokens.access_token (ChatGPT OAuth flow), not at the top level.
Also adds ENV_MUTEX to Codex token tests for thread safety.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: remove Codex OAuth onboarding (incompatible with OpenAI API)
Codex CLI OAuth tokens use a different endpoint
(chatgpt.com/backend-api/codex) and the Responses API wire format,
not api.openai.com with Chat Completions. The tokens lack the
model.request scope needed for the platform API, so they can't be
used as drop-in OPENAI_API_KEY replacements.
Removes: extract_codex_oauth_token(), wizard Codex OAuth flow,
CODEX_OAUTH_TOKEN env var support, and related tests.
OpenAI onboarding now uses direct API key only.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix formatting for CI (cargo fmt)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address Gemini review feedback
- Use ? operator for ANTHROPIC_MODEL/BASE_URL env resolution instead of
.ok().flatten() to propagate ConfigErrors consistently
- Skip Tool messages without tool_call_id with a warning instead of
using unwrap_or_default() which would send empty string to Anthropic
- Extract credential check into closure to reduce duplication in
Claude Code sandbox setup
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor(review): address PR review feedback for OAuth onboarding
- Gate ANTHROPIC_OAUTH_TOKEN resolution to Anthropic provider only
(was needlessly checked for all registry providers)
- Add 3 regression tests for OAuth config resolution:
- oauth_token sets placeholder api_key
- real api_key takes priority over oauth
- non-Anthropic providers don't pick up oauth_token
- Validate OAuth token prefix (sk-ant-oat) in wizard to catch
accidentally pasted API keys
- Improve error body read handling in AnthropicOAuthProvider
(was silently swallowing read errors with unwrap_or_default)
- Remove extra blank line in write_bootstrap_env
- Remove stale blank line in RegistryProviderConfig doc comment
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #384 review comments
Blocker:
- Replace OnceLock<HashMap> with LazyLock<Mutex<HashMap>> for INJECTED_VARS
so both inject_os_credentials() and inject_llm_keys_from_secrets() merge
data instead of the second caller silently dropping its entries.
High:
- Add 401 retry with OS credential store re-extraction in
AnthropicOAuthProvider, recovering from expired OAuth tokens (~8-12h)
without manual intervention.
- Fix comment in app.rs: ~/.codex/auth.json → ~/.claude/.credentials.json.
Medium:
- Remove unsafe { std::env::set_var } from wizard; use thread-safe
inject_single_var() overlay instead (safe on multi-threaded Tokio).
- Add post-init validation in AppBuilder: fail early with clear error when
LLM_BACKEND is set but no credentials were resolved after secret injection.
- Add sk-ant-oat prefix validation in parse_oauth_access_token().
- Only route to AnthropicOAuthProvider when api_key is missing or equals
OAUTH_PLACEHOLDER (API key takes priority over OAuth token).
- Teach fetch_anthropic_models() to use Bearer auth when only OAuth token
is available (model listing no longer fails for OAuth-only users).
Low:
- Use optional_env() in wizard credential checks to read from injected
overlay, not just raw env vars.
[skip-regression-check]
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: cargo fmt
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: [email protected] <[email protected]>
* fix: init WASM runtime eagerly regardless of tools directory existence
The WASM tool runtime was only created at startup when both
`wasm.enabled` and `wasm.tools_dir.exists()` were true. This meant
that if the tools directory didn't exist yet (e.g. fresh deploy with
`--no-onboard`), the runtime was set to None and passed to the
ExtensionManager. Extensions installed later via the web UI would
then fail with "WASM runtime not available" because the runtime
could not be retroactively created.
The Wasmtime engine initialization has no dependency on the tools
directory — it only configures the compiler and starts an epoch
ticker thread. The directory is only needed later when loading
.wasm modules. Remove the directory check so the runtime is
available for post-startup extension activation.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* test: add regression tests for WASM runtime eager init
- runtime.rs: test_runtime_creation_without_tools_dir confirms the
Wasmtime engine initialises without a tools directory on disk
- manager.rs: test_activate_wasm_tool_with_runtime_passes_runtime_check
verifies activation gets past the runtime check when a runtime is
provided (fails on missing file, not missing runtime)
- manager.rs: test_activate_wasm_tool_without_runtime_fails_with_runtime_error
verifies the original error when no runtime is available
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: use idiomatic Result-to-Option conversion for WASM runtime init
Address PR review feedback: replace match block with
.map(Arc::new).map_err(|e| warn!(...)).ok() chain.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix formatting in extension manager tests
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: make onboarding installs prefer release artifacts with source fallback
* fix: harden extension fallback errors and surface setup warnings
* fix: validate registry artifacts and harden fallback errors
* fix: address review feedback on installer fallback
- Add upfront validate_manifest_install_inputs() in
install_with_source_fallback so bad manifests fail fast without
relying on inner methods to catch them
- Document ALLOWED_ARTIFACT_HOSTS as GitHub-only by design
- Document intentional url omission from DownloadFailed Display
- Add channel manifest validation tests (wrong prefix rejected,
correct prefix accepted)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: require SHA256 checksum for artifact downloads
Reject artifact installs when the manifest has sha256: null instead of
warning and proceeding. This prevents installing unverified pre-built
binaries during onboarding. The check runs before downloading to avoid
wasting bandwidth.
Since InvalidManifest blocks source fallback, manifests with URLs but
no checksums will hard-fail rather than silently falling back to source
build — forcing the manifest to be fixed.
The release CI already computes SHA256 for each bundle; the manifests
just need to be populated with the actual values.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce SHA256 checksums and auto-patch manifests in CI
- Fix cargo fmt on SHA256 check code
- Reorder release CI: build WASM extensions before binary so manifests
can be patched with computed SHA256 before build.rs embeds them
- Add "Patch manifests with WASM checksums" step in build-local-artifacts
that reads checksums.txt and updates registry JSON files before building
- Add update-registry-checksums job that commits patched manifests back
to main after release, keeping the repo in sync with released artifacts
This closes the integrity gap where all manifests had sha256: null and
artifact downloads were unverified. The binary now embeds correct SHA256
values and the installer hard-rejects null checksums.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
Co-authored-by: Bowen Wang <[email protected]>
* fix: make Telegram status prompts reliable
Approval and auth prompts could be missed when polling or reply-context sends failed, leaving users stuck in waiting states. This adds explicit status mapping and retries, keeps typing active through intermediate work while suppressing noisy tool telemetry, and adds regression tests plus CI coverage for the Telegram channel crate.
* fix: normalize terminal status handling
Terminal status strings from the agent loop can vary in casing and formatting, which could leak internal status lines to Telegram. This normalizes Done/Interrupted mapping and filters terminal status text consistently to keep chat UX clean while preserving actionable prompts.
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix: persist WASM channel workspace writes across callbacks
WASM channel callbacks (polling, webhooks, on_start) call
workspace_write() to persist state, but the host code never committed
these writes — take_pending_writes() was never called. Additionally,
no WorkspaceReader was injected into channel capabilities, so
workspace_read() always returned None.
This caused Telegram's polling offset to reset to 0 on every tick,
making getUpdates re-deliver already-processed messages and producing
2-4 duplicate LLM responses per user message.
Add ChannelWorkspaceStore (Arc-wrapped HashMap with std::sync::RwLock)
that persists across callback invocations within a channel's lifetime.
Inject it as the WorkspaceReader and commit pending writes after every
callback execution (on_start, on_poll, on_http_request, execute_poll).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* style: fix formatting
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
The agentic loop injected fake user messages ("Please proceed and use
the available tools to complete this task.") when the LLM responded
with text instead of tool calls. This caused hallucinated conversations
during casual chat, 3x wasted LLM calls, and trust issues.
Remove the `resume_after_tool` parameter and `tools_executed` tracking
entirely. Text responses now return immediately, trusting the LLM to
decide when tools are needed (consistent with ZeroClaw and OpenClaw).
Closes#145
Co-authored-by: Claude Opus 4.6 <[email protected]>
* ci: add automated PR labeling system
Add two independent workflows for PR auto-labeling:
- Scope labels via actions/labeler (path glob matching)
- Size, risk, and contributor tier via custom shell script
Includes idempotent label bootstrap script (create-labels.sh).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* ci: temporarily use pull_request trigger for testing
Switch to pull_request so workflows run from the PR branch.
Will revert to pull_request_target before merge.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(ci): use absolute path for search/issues API call
gh api requires a leading slash for REST endpoints.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix(ci): use gh pr list instead of search API for contributor count
The search/issues API returns 404 with the default GITHUB_TOKEN.
gh pr list --state merged works with standard permissions.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* ci: revert to pull_request_target for fork PR support
Restore pull_request_target trigger and base branch checkout
now that testing is complete.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: add bundled and declarative hook bundle loading
* fix: load plugin hooks only for active extensions
* fix: avoid duplicate plugin hook registration
* security: harden outbound webhook hooks
* fix: pin webhook DNS resolutions for outbound hooks
* fix: block IPv4-mapped local webhook targets
* style: format webhook hardening changes for CI
* fix: pass HookRegistry to ExtensionManager in AppBuilder
After merging main (which extracted AppBuilder from main.rs in #198),
the ExtensionManager::new() call in app.rs was missing the `hooks`
parameter that PR #176 added. This moves HookRegistry creation before
init_extensions() and threads it through, matching the existing pattern
in main.rs.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Illia Polosukhin <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
These are local tool data directories (Sidecar) that should not be
tracked. Added both to .gitignore to prevent future accidents.
Co-authored-by: Claude Opus 4.6 <[email protected]>
* feat: Add lifecycle hooks system with 6 interception points
Implement extensible hook infrastructure for intercepting and transforming
agent operations at well-defined points in the lifecycle:
- BeforeInbound: intercept/modify/reject incoming user messages
- BeforeToolCall: intercept/modify/reject tool executions (chat + job)
- BeforeOutbound: intercept/modify/suppress outgoing responses
- TransformResponse: transform final response before completing a turn
- OnSessionStart: fire-and-forget notification on new session creation
- OnSessionEnd: fire-and-forget notification on session pruning
Hooks execute in priority order with modification chaining, reject
short-circuits, configurable failure modes (FailOpen/FailClosed),
and per-hook timeouts. Empty registry is zero-cost (all hooks pass
through immediately).
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: enforce hook fail-closed semantics
* Merge upstream/main into feat/hooks-system-clean
Resolve merge conflicts:
- FEATURE_PARITY.md: Keep both upstream cron/routines status and hooks status
- src/error.rs: Keep both Hook and Orchestrator/Worker error variants
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: resolve CI test failures in pairing store and wizard
- Fix pairing store truncate bug: record_failed_approve used
.truncate(true) which wiped the file before reading, causing rate
limiting to never accumulate past 1 attempt. Changed to
.truncate(false) to preserve existing data.
- Fix wizard test: skip test_install_missing_bundled_channels when
telegram WASM artifact specifically isn't available, not just when
all channels are empty (whatsapp may exist without telegram).
- Add workspace exclude for subcrate directories to prevent cargo
from discovering them as workspace members during builds.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: address PR #18 review comments
- Remove duplicate maybe_hydrate_thread call (rebase artifact)
- Fix RwLock held across async hook execution in HookRegistry::run()
- Add tracing::warn for silent JSON parse failures in hook modifications
- Refactor execute_tool_inner to accept &WorkerDeps instead of 8 Arc params
- Use real user_id from JobContext instead of job_id UUID in BeforeToolCall hook
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: cargo fmt + remove tracked worktree breaking CI
- Apply rustfmt formatting (method chain line breaks, match arm style)
- Remove .claude/worktrees/ from git tracking (caused submodule error in CI)
- Add .claude/worktrees/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Firat Sertgoz <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
* Add WebSocket gateway and control plane endpoint
Adds bidirectional WebSocket transport to the web gateway alongside
the existing SSE stream. Clients can send messages, approvals, and
pings over a single persistent connection at /api/chat/ws.
- Enable axum `ws` feature for built-in WebSocket support
- Add WsClientMessage/WsServerMessage types with tagged JSON protocol
- Add subscribe_raw() to SseManager for non-SSE consumers
- Create ws.rs with connection handler (split sender/receiver tasks)
- Add WsConnectionTracker for active connection counting
- Add /api/gateway/status control plane endpoint (SSE + WS counts)
- 35 new tests covering message types, broadcast, and handler logic
https://claude.ai/code/session_01KEaLN6Xq2j5EeV3SGHQT6b
* Add e2e WebSocket gateway integration tests
- Add tokio-tungstenite dev-dependency for WebSocket client in tests
- Update start_server to return actual bound SocketAddr (enables port 0)
- Add 10 e2e tests covering full HTTP upgrade → WebSocket → message flow:
ping/pong, message routing to agent, broadcast event delivery,
connection tracking, invalid message handling, auth rejection,
gateway status endpoint, and multi-event sequencing
https://claude.ai/code/session_01KEaLN6Xq2j5EeV3SGHQT6b
---------
Co-authored-by: Claude <[email protected]>