feat(reborn): serve webui at root path instead of v2 - #6152
Conversation
📝 WalkthroughWalkthroughThe Reborn WebUI moves from ChangesRoot-mounted WebUI
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request migrates the Reborn WebUI from the legacy /v2 path prefix to root-level browser routes (/), adding temporary redirects to preserve compatibility for existing deep links and query parameters. While the routing logic successfully handles standard paths, a security review identified a potential route bypass in the fail-closed namespace check: requests with multiple leading slashes (e.g., //api/...) could bypass the check and incorrectly serve the SPA shell. It is recommended to explicitly reject paths starting with double slashes to maintain a secure default posture.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
@claude review |
|
🚅 Deployed to the ironclaw-pr-6152 environment in ironclaw-ci-preview
|
Coverage ratchetReborn integration-tier coverageLine coverage (Reborn crates): 85.76% — 308021 / 359163 lines Per-crate breakdown (63 crates, lowest-covered first)
This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors. Exemptions (3 entry/entries excluded from the accounting above)
|
|
@claude review |
This comment was marked as resolved.
This comment was marked as resolved.
8994a6e to
52d9923
Compare
🔎 IronLoop Review StatusHead: Current reviewers:
Reviewer summaries
Recent activity
Available commands
Run metadataAdmission: webhook accepted the request and IronLoop persisted reviewer state before this projection. |
|
@claude review |
Code Review — PR #6152Reviewed by 4 parallel agents (Security & Safety, Architecture & Patterns, Bug Scan, Performance & Production). Summary below. Key FindingsNo blocking issues found. The PR is well-structured with comprehensive validation and test coverage. Architecture & Design
Validation & Error Handling
Security
Tests
Performance
Test Coverage — Regression Assurance
Changelog & Documentation
RecommendationReady to merge. All substantive requirements met. Minor gaps (XSS test, O(N²) sorting observation) are polish items, not blockers. |
There was a problem hiding this comment.
✅ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ✅ Approved | 0 | 0 | 0 | 29ff024a5f2d |
Head: 29ff024a5f2d2ee1d7b173bfb25128460ff6670f
Next: No reviewer action needed.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Reviewed the complete base-to-head diff. The root-mounted SPA, legacy redirects, descriptor-derived namespace reservations, OAuth redirects, frontend paths, scripts, documentation, and regression coverage are internally consistent. No concrete blocking issue was found.
Findings
None.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_webui_v2/src/static_assets/router.rs`:
- Around line 50-74: Update StaticRouterConfigError to derive thiserror::Error
alongside its existing traits, add per-variant #[error(...)] messages matching
the current Display output, and remove the manual fmt::Display and
std::error::Error implementations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 0dedcc6c-63c2-431d-8810-168f6481f7a0
⛔ Files ignored due to path filters (1)
CHANGELOG.mdis excluded by!CHANGELOG.md
📒 Files selected for processing (61)
FEATURE_PARITY.mdcrates/ironclaw_reborn_composition/CLAUDE.mdcrates/ironclaw_reborn_composition/Cargo.tomlcrates/ironclaw_reborn_composition/src/llm_admin/nearai_login_serve.rscrates/ironclaw_reborn_composition/src/webui/webui_serve.rscrates/ironclaw_reborn_composition/tests/webui_v2_serve.rscrates/ironclaw_reborn_webui_ingress/CLAUDE.mdcrates/ironclaw_reborn_webui_ingress/src/auth/pending.rscrates/ironclaw_reborn_webui_ingress/src/auth/routes.rscrates/ironclaw_reborn_webui_ingress/tests/github_oauth_routes.rscrates/ironclaw_reborn_webui_ingress/tests/google_oauth_routes.rscrates/ironclaw_reborn_webui_ingress/tests/network_limits_contract.rscrates/ironclaw_reborn_webui_ingress/tests/session_round_trip.rscrates/ironclaw_reborn_webui_ingress/tests/signed_session_multi_user.rscrates/ironclaw_webui_v2/CLAUDE.mdcrates/ironclaw_webui_v2/build.rscrates/ironclaw_webui_v2/frontend/index.htmlcrates/ironclaw_webui_v2/frontend/public/assets/site.webmanifestcrates/ironclaw_webui_v2/frontend/public/wallet-connect.htmlcrates/ironclaw_webui_v2/frontend/src/app/app.tsxcrates/ironclaw_webui_v2/frontend/src/app/auth.tscrates/ironclaw_webui_v2/frontend/src/app/root-paths.test.tscrates/ironclaw_webui_v2/frontend/src/components/sidebar.tsxcrates/ironclaw_webui_v2/frontend/src/pages/login/login-page.tsxcrates/ironclaw_webui_v2/frontend/src/pages/logs/lib/logs-data.test.tscrates/ironclaw_webui_v2/frontend/src/pages/logs/lib/logs-data.tscrates/ironclaw_webui_v2/frontend/src/pages/logs/logs-page.tsxcrates/ironclaw_webui_v2/frontend/src/pages/settings/components/provider-components.test.tscrates/ironclaw_webui_v2/frontend/src/pages/settings/hooks/useProviderLogin.tscrates/ironclaw_webui_v2/frontend/vite.config.tscrates/ironclaw_webui_v2/src/lib.rscrates/ironclaw_webui_v2/src/static_assets/assets.rscrates/ironclaw_webui_v2/src/static_assets/mod.rscrates/ironclaw_webui_v2/src/static_assets/router.rsdocs/reborn-binary.mddocs/reborn/security-parity/03-headers-errors.mdscripts/reborn_qa_matrix/audit_surface_inventory.pyscripts/reborn_qa_matrix/test_audit_surface_inventory.pyscripts/reborn_webui_v2_live_qa/run_live_qa.pyscripts/reborn_webui_v2_live_qa/test_run_live_qa.pyscripts/run-reborn-webui.shtests/e2e/CLAUDE.mdtests/e2e/helpers.pytests/e2e/reborn_webui_harness.pytests/e2e/scenarios/test_admin_api.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_approval.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_auth_flows.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_chat_actions.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_core.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_dom_resource_limits.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_extensions.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_pending_messages.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_projects.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_settings_search.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_skills.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_sse_history.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_tool_execution.pytests/e2e/scenarios/test_reborn_webui_v2_legacy_tool_permissions.pytests/e2e/scenarios/test_reborn_webui_v2_notifications.pytests/e2e/scenarios/test_reborn_webui_v2_smoke.pytests/e2e/scenarios/test_skills.py
|
/canary |
|
Started Reborn WebUI v2 live canary for |
There was a problem hiding this comment.
Pull request overview
This PR migrates the Reborn WebUI v2 SPA from the /v2 mount to root-level browser routes (e.g. /chat, /settings, /extensions) while keeping compatibility redirects from legacy /v2/* URLs and preserving query parameters (notably OAuth/login tickets). It also tightens the “fail-closed” behavior of the SPA wildcard by deriving reserved root namespaces from the final mounted route descriptor set during composition.
Changes:
- Serve the WebUI SPA and its static assets from
/(and adjust Vite/React Router, wallet-connect, and logs URLs accordingly). - Add
/v2,/v2/, and/v2/*temporary redirects to root equivalents, with normalization to prevent open-redirect / external-navigation edge cases. - Derive and validate reserved root namespaces from mounted
IngressRouteDescriptors to keep unknown host/API namespaces returning 404 instead of SPA HTML.
Reviewed changes
Copilot reviewed 63 out of 64 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/e2e/scenarios/test_skills.py | Update v2 skills navigation to root-mounted settings route. |
| tests/e2e/scenarios/test_reborn_webui_v2_smoke.py | Update root routing assertions and add explicit /v2/* redirect coverage. |
| tests/e2e/scenarios/test_reborn_webui_v2_notifications.py | Update default v2 open path and URL expectations to root routes. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_tool_permissions.py | Update legacy tool-permissions browser navigation to /settings/*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_tool_execution.py | Update legacy tool-execution scenario navigation to /chat/*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_sse_history.py | Update SSE history scenarios to root /chat/* navigation. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_skills.py | Update skills scenarios to /settings/skills. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_settings_search.py | Update settings search deep links to /settings/*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_projects.py | Update project routes and URL assertions to /projects/* and /chat/*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_pending_messages.py | Update pending-messages SPA path assertions to /chat/*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_extensions.py | Update extensions routes and navigation checks to /extensions/* and /settings. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_dom_resource_limits.py | Update resource-limit scenarios to root /chat/* routes. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_core.py | Update core v2 shell/login navigation to root paths. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_chat_actions.py | Update command palette URL expectation to /settings*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_auth_flows.py | Update auth-flow scenario navigation to /chat/*. |
| tests/e2e/scenarios/test_reborn_webui_v2_legacy_approval.py | Update approval scenario navigation to /chat/*. |
| tests/e2e/scenarios/test_admin_api.py | Update admin navigation to /admin/*. |
| tests/e2e/reborn_webui_harness.py | Update shared harness helper default v2 path to /. |
| tests/e2e/helpers.py | Update v2 selector documentation to reflect root mounting. |
| tests/e2e/CLAUDE.md | Update E2E docs to describe root-mounted v2 SPA routes. |
| scripts/run-reborn-webui.sh | Update printed onboarding URL from /v2 to /. |
| scripts/reborn_webui_v2_live_qa/test_run_live_qa.py | Update live QA expectations for root routes and surfaces. |
| scripts/reborn_webui_v2_live_qa/run_live_qa.py | Update live QA navigation targets to root-mounted routes. |
| scripts/reborn_qa_matrix/test_audit_surface_inventory.py | Update QA audit extraction test to TSX route syntax and root route identifiers. |
| scripts/reborn_qa_matrix/audit_surface_inventory.py | Update route extraction to app.tsx and root identifiers (no /v2 prefix). |
| FEATURE_PARITY.md | Document canonical root SPA routes and /v2/* redirect behavior. |
| docs/reborn/security-parity/03-headers-errors.md | Update CSP/security-parity documentation for root-mounted SPA and wallet popup route. |
| docs/reborn-binary.md | Update runbook to reflect / serving UI and /v2 redirect compatibility. |
| crates/ironclaw_webui_v2/src/static_assets/router.rs | Implement root SPA router, /v2/* redirects, configurable reserved namespaces, and related security tests. |
| crates/ironclaw_webui_v2/src/static_assets/mod.rs | Re-export new static router config types and factories; remove prefix-mount API. |
| crates/ironclaw_webui_v2/src/static_assets/assets.rs | Update asset-path documentation to “gateway root” semantics. |
| crates/ironclaw_webui_v2/src/lib.rs | Update crate-level re-exports for new static router APIs. |
| crates/ironclaw_webui_v2/frontend/vite.config.ts | Switch Vite base to / and update dev proxy paths to root assets/wallet routes. |
| crates/ironclaw_webui_v2/frontend/src/pages/settings/hooks/useProviderLogin.ts | Update wallet popup route from /v2/wallet/connect to /wallet/connect. |
| crates/ironclaw_webui_v2/frontend/src/pages/settings/components/provider-components.test.ts | Add assertion that wallet popup uses the new root path. |
| crates/ironclaw_webui_v2/frontend/src/pages/logs/logs-page.tsx | Update “clear scope” link to /logs. |
| crates/ironclaw_webui_v2/frontend/src/pages/logs/lib/logs-data.ts | Remove legacy /v2 absolute-path option; always return root /logs paths. |
| crates/ironclaw_webui_v2/frontend/src/pages/logs/lib/logs-data.test.ts | Update logs-path tests for root /logs behavior. |
| crates/ironclaw_webui_v2/frontend/src/pages/login/login-page.tsx | Default OAuth redirect landing to / (root) instead of /v2. |
| crates/ironclaw_webui_v2/frontend/src/components/sidebar.tsx | Update logo asset URL to /assets/logo.jpg. |
| crates/ironclaw_webui_v2/frontend/src/app/root-paths.test.ts | Add regression tests ensuring root asset paths and router/login defaults no longer use /v2. |
| crates/ironclaw_webui_v2/frontend/src/app/auth.ts | Update OAuth/login-ticket comments to describe /?login_ticket= contract. |
| crates/ironclaw_webui_v2/frontend/src/app/app.tsx | Remove React Router basename and legacy /v2 redirect prefixing. |
| crates/ironclaw_webui_v2/frontend/public/wallet-connect.html | Update wallet-connect script URL to /wallet-connect.js. |
| crates/ironclaw_webui_v2/frontend/public/assets/site.webmanifest | Update manifest start/scope to / while preserving install identity via id: "/v2/". |
| crates/ironclaw_webui_v2/frontend/index.html | Update shell asset URLs from /v2/* to root /assets/* and /vendor/*. |
| crates/ironclaw_webui_v2/CLAUDE.md | Update crate guidance for composition to use root static_router* APIs and namespace reservation. |
| crates/ironclaw_webui_v2/Cargo.toml | Add optional thiserror for config errors under webui-v2-beta. |
| crates/ironclaw_webui_v2/build.rs | Update build output documentation for root-relative asset keys. |
| crates/ironclaw_reborn_webui_ingress/tests/signed_session_multi_user.rs | Update OAuth login redirect_after default to %2F. |
| crates/ironclaw_reborn_webui_ingress/tests/session_round_trip.rs | Update OAuth redirect_after default to %2F. |
| crates/ironclaw_reborn_webui_ingress/tests/network_limits_contract.rs | Update OAuth redirect_after default to %2F. |
| crates/ironclaw_reborn_webui_ingress/tests/google_oauth_routes.rs | Update OAuth landing redirects to /?login_ticket= and add legacy /v2 redirect_after preservation test. |
| crates/ironclaw_reborn_webui_ingress/tests/github_oauth_routes.rs | Update GitHub OAuth landing redirects to /?login_ticket= and root error redirects. |
| crates/ironclaw_reborn_webui_ingress/src/auth/routes.rs | Change default redirect_after and SPA error redirects from /v2 to /. |
| crates/ironclaw_reborn_webui_ingress/src/auth/pending.rs | Keep accepting safe legacy /v2 redirect_after while defaulting to /. |
| crates/ironclaw_reborn_webui_ingress/CLAUDE.md | Update ingress documentation for root SPA ticket/error redirects. |
| crates/ironclaw_reborn_composition/tests/webui_v2_serve.rs | Update composed-router integration tests for root static SPA mount and /v2 compatibility redirects. |
| crates/ironclaw_reborn_composition/src/webui/webui_serve.rs | Compose root static router with reserved namespaces derived from mounted descriptors; fail composition on unsafe conflicts. |
| crates/ironclaw_reborn_composition/src/llm_admin/nearai_login_serve.rs | Update NEAR AI login callback redirects to root routes and add redirect contract tests. |
| crates/ironclaw_reborn_composition/CLAUDE.md | Document reserved namespace derivation and root SPA mount behavior. |
| crates/ironclaw_reborn_composition/Cargo.toml | Update dependency comment to reflect root-mounted static SPA surface. |
| CHANGELOG.md | Document root-mounted WebUI and temporary /v2 compatibility redirects (issue #6142). |
| Cargo.lock | Add thiserror to the ironclaw_webui_v2 feature dependency closure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
FEATURE_PARITY.md (1)
554-554: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winCite the implementation behind this parity claim.
This row now asserts launchd/systemd support, token-file fallback, and atomic rollback. Link the defining implementation or test; otherwise this load-bearing Markdown claim is only a summary.
As per coding guidelines,
**/*.mdload-bearing claims affecting architectural or implementation decisions must cite evidence from the definition or write site, not merely a summary.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@FEATURE_PARITY.md` at line 554, Update the wrapper-based service install row in FEATURE_PARITY.md to cite the defining implementation or test for launchd/systemd support, webui-token-file fallback, and atomic install rollback. Link directly to the relevant symbols or evidence locations, preserving the existing parity summary while making each load-bearing claim verifiable.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@FEATURE_PARITY.md`:
- Line 554: Update the wrapper-based service install row in FEATURE_PARITY.md to
cite the defining implementation or test for launchd/systemd support,
webui-token-file fallback, and atomic install rollback. Link directly to the
relevant symbols or evidence locations, preserving the existing parity summary
while making each load-bearing claim verifiable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 861e6507-0422-45f6-bcaa-d6679241ba3a
⛔ Files ignored due to path filters (2)
CHANGELOG.mdis excluded by!CHANGELOG.mdCargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (2)
FEATURE_PARITY.mdtests/e2e/reborn_webui_harness.py
v2
Reconciles the 6 new main commits (serve-webui-at-root #6152, workspace download failures #6150, theme controls #6148, toast lifecycle #6151, serve durability e2e #5523, safety ReDoS CI #6181) with the WebUI host-stack merge + `ironclaw_webui` rename. Conflict resolutions: - Frontend (app/auth/sidebar/toast/theme/i18n + 3 new tests): rename detection auto-applied main's edits to the moved `crates/ironclaw_webui/frontend/` path. - `webui_serve.rs`: took main's #6152 root-serving surface — `static_router_with_config(static_router_config)` replaces the old `mount_at_prefix("/v2")`, plus the new `WebuiServeError` root-namespace variants and `static_router_config_from_descriptors` validator — adapted to this crate's module paths (`crate::webui_v2::`, `crate::webui_rate_limit::`). - `webui_v2/mod.rs`: took main's static-router exports (`StaticRouterConfig`, `StaticRouterConfigError`, `static_router_with_config`; dropped `mount_at_prefix`) without the `webui-v2-beta` cfg gate, since the folded module is unconditional here. - Deleted `crates/ironclaw_webui_v2/{CLAUDE.md,Cargo.toml}` (main modified the now-removed crate); dropped composition's optional `ironclaw_webui_v2` dep. - `composition/tests/webui_v2_serve.rs`: imports of the moved `Webui*`/`webui_v2_app` (incl. main's newly-used `WebuiServeError`) now come from `ironclaw_webui`. - Cargo.lock regenerated. Verified: `ironclaw_webui` builds + full test suite green (175 lib + integration, 0 failures) under default and `--all-features`; `reborn_cli` builds with `slack-v2-host-beta,openai-compat-beta` (wiring survived the merge). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#6152 moved the WebUI SPA to root-path serving: an unmounted /login now falls through to the SPA wildcard (200, generic shell) rather than 404, since composition only reserves root namespaces for routes it actually mounts (static_router_config_from_descriptors). Update the two must-not-mount smoke tests to assert on the property that actually matters — this route's own redirect-with-session-ticket handler never runs (no Location header) — instead of a status code the new routing model no longer produces. Also fixes the onboard-journey smoke test's redirect assertion, which still expected the pre-#6152 /v2 landing path.
…t setup, login link (#6174) * feat(reborn): keychain-backed master key for local-dev, dotfile-first Ports v1's os_keychain_suppressed() test-suppression gate into ironclaw_secrets::keychain (never existed there before this), then inserts an OS-keychain lookup step into the local-dev master-key resolver chain in ironclaw_reborn_composition::factory, between "no cached dotfile" and "generate fresh". New resolution order: env var (SECRETS_MASTER_KEY) -> cached dotfile -> OS keychain -> generate + cache a fresh dotfile. A keychain hit is returned as-is and never also written to the dotfile — the two are alternative sources, not layered. This makes the resolver chain (and build_local_dev_secret_store) async; the production call site and all test callers are widened accordingly. Onboard now provisions a keychain entry on a fresh desktop: if no cached dotfile exists and the keychain has no key, it generates and stores one. Under suppression/failure (headless Linux, CI, denied prompt) it prints a SECRETS_MASTER_KEY/dotfile fallback note and continues (exit 0) rather than failing onboarding — verifying an actual successful keychain write needs a real OS keychain and stays manual/E2E only. Also hardens the CLI test suite for the newly-real OS-keychain touchpoint: every spawned-subprocess test that env_clear()s before invoking the real binary now re-adds IRONCLAW_DISABLE_OS_KEYCHAIN=1, matching how CI already sets it at the job level — a spawned binary's cfg!(test) doesn't apply, so only the env var protects against a real keychain prompt/touch in-process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(reborn-ingress): tokenized login route feeding the existing session-ticket exchange GET /login?token= verifies against the host's WebuiAuthenticator (constant-time reuse of EnvBearerAuthenticator), mints a session bearer via SessionStore, and redirects with a one-time login_ticket — same query convention as the OAuth callback. POST /auth/session/exchange redeems it with the identical {ticket}->{token} contract, so the SPA's existing exchangeLoginTicket needs no changes. Owns its own ticket store rather than sharing the OAuth surface's private one, since this route must work with no OAuth provider configured (the CLI-onboarding case). Constructor (CliTokenLoginConfig) takes tenant_id/authenticator/session_store so serve.rs can wire it in one call alongside the existing admin_session_store construction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(reborn-cli): serve derives webui user id from config, env overrides serve previously hard-failed at startup when IRONCLAW_REBORN_WEBUI_USER_ID was unset, which crash-loops a service-installed serve (launchd/systemd/ Railway) whose unit environment carries only HOME/PROFILE. Extract the resolution into resolve_webui_user_id_raw: env value when set and non-empty, else crate::runtime::default_owner_id(config_file) — the same config default resolve_webui_runtime_owner already accepts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(reborn-cli): suppress OS keychain in lane-merged smoke spawns The B3-lane serve tests predate the keychain-suppression convention the keychain lane introduced; without it their env_clear() spawns can reach a real macOS keychain and hang. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(reborn-cli): onboard prompts for provider/API key into the encrypted store Adds the narrow pre-composition `/secrets` opener (`ironclaw_reborn_composition::open_local_dev_secret_store`) so onboard can write into the same physical local-dev libSQL file `serve` later opens, without building the full CompositeRootFilesystem. Adds the `PromptSource` trait (provider id, masked API key) with a stdin production impl that owns the only `IsTerminal` check in this path; non-interactive sessions get a typed `LlmCredentialPromptError`, never a panic/exit. `OnboardCommand::execute` treats a non-interactive prompt as a soft skip (mirrors `MasterKeyProvisionOutcome::Suppressed`), so headless/CI onboard runs keep succeeding. `provision_llm_credentials` writes the provider selection to `[llm.default]` via the existing `RebornProviderAdmin::set_provider` config machinery, and the key via the canonical `LlmKeyStore` handle (`llm_provider_<id>_api_key`) — the same handle the webui2 settings surface and boot-time `apply_startup_stored_llm_key` already use, so no new read-side mapping is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(reborn-cli): wire onboarding journey end-to-end (login link, service start, status) Mounts the CLI-token login route on `serve` (gated off when SSO is configured, avoiding a duplicate /auth/session/exchange registration), adds service::install_and_start for onboard's finale, prints the login link + service outcome from onboard, reprints the link from `status`, and truths up onboard's steps_pending to reflect whether the LLM key was actually configured this run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(reborn-cli): onboarding review fixes — bootable headless default, store-first writes, idempotent reruns - Config stub now seeds nearai/deepseek-ai/DeepSeek-V4-Flash/NEARAI_API_KEY (was openai/gpt-4o-mini, mismatched onboard's own interactive default and left a headless-CI boot broken); provider/model/api-key-env now come from one shared constant in config/init.rs so the two paths can't drift again. Deleted the capstone smoke test's config-surgery workaround. - provision_llm_credentials now stores the LLM key BEFORE writing [llm.default] to config.toml, via an injectable LlmKeyStoreOpener seam; a failed store write leaves config.toml untouched. - Reruns skip prompting when [llm.default] is already configured AND the store already has a key for it, unless --force. - Moved login_link into webui_token.rs (its documented shared home) behind webui-v2-beta; onboard and status both call it from there. - should_install_service now takes interactivity from the same PromptSource reading the LLM-credential step already made, instead of an independent IsTerminal check. - Restored v1's drain_pending_events() before the masked API-key read loop. - Split onboard.rs into commands/onboard/{mod,prompts,master_key}.rs (pure move, no behavior change). - smoke.rs: added a shared reborn_command() builder and routed every real-binary spawn through it; fixed the accidental double insertion of the keychain-suppression env line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(reborn-cli): adapt login_link to Result-returning webui_token_file_is_valid Rebase onto main pulled in webui_token_file_is_valid's signature change (bool -> anyhow::Result<bool>, propagating real I/O errors instead of silently treating them as invalid). login_link stayed Option<String> per its callers (onboard, status) — an I/O error now falls back to None (no login link) rather than failing to compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-cli): drop direct ironclaw_secrets dependency from onboard production code reborn_dependency_boundaries::reborn_cli_binary_crate_stays_separate_from_v1_root pins ironclaw_reborn_cli's allowed workspace deps to exactly {ironclaw_reborn_composition, ironclaw_reborn_config, ironclaw_reborn_traces, ironclaw_reborn_webui_ingress} — onboard's LLM-credential and keychain provisioning steps (from the rebased-in PR B commits) named ironclaw_secrets types directly, which the rebase's earlier composition-side churn made a normal (non-dev) dependency again. Adds two facades so ironclaw_secrets stays fully behind ironclaw_reborn_composition: - provision_local_dev_keychain_master_key()/LocalDevKeychainMasterKeyOutcome in factory.rs, wrapping ironclaw_secrets::keychain directly. - LlmKeyStore::put_plaintext(provider_id, String), wrapping SecretMaterial::from() so callers never construct it themselves. onboard::master_key and onboard::mod now go through these; LlmKeyStoreOpener returns ironclaw_reborn_composition::LlmKeyStore instead of Arc<dyn ironclaw_secrets::SecretStore>. ironclaw_secrets moves to [dev-dependencies] in ironclaw_reborn_cli's Cargo.toml — the crate's only remaining use is the test-only FailingSecretStore fake that proves provision_llm_credentials' store-before-config write ordering, matching the existing ironclaw_host_api dev-dependency for the same test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-cli): onboarding review fixes round 2 - cfg-gate LlmKeyStoreOpener/already_configured_outcome to match the libsql+root-llm-provider gate on ironclaw_reborn_composition::LlmKeyStore, fixing a --no-default-features compile break; add a feature-off stub so execute()'s call site still compiles. - StdinPromptSource::is_interactive now checks stdout is also a terminal, so redirected stdout no longer receives masked `*` characters. - reject blank/whitespace-only API keys: StdinPromptSource::api_key retries up to 3 times, and provision_llm_credentials adds a defense-in-depth guard for any PromptSource impl. - log the swallowed OS-keychain store error (tracing::debug!) instead of silently falling back to Suppressed. - log the swallowed secret-store-open error in already_configured_outcome before falling through to prompting. - StatusDto.login_link is now #[serde(skip_serializing)] so `status --json` never leaks the webui bearer token embedded in the login link's query string; the human-readable text renderer is unaffected since it reads the field directly. - document two previously-unexplained accepted-risk fall-throughs: the keychain-error blanket match in resolve_local_dev_secret_master_key_with_env (headless containers have no secret-service daemon) and the onboard-vs-onboard TOCTOU in provision_master_key (single-operator LocalDev, serve never writes the keychain). * fix(reborn-cli): let a stored LLM key resolve at serve boot resolve_reborn_runtime_llm failed closed on ApiKeyEnvUnset for api_key_required providers (openai/anthropic) before apply_startup_stored_llm_key ever got a chance to inject a key an operator stored via onboard/models set-provider. nearai (api_key_required=false) never hit this path, so the existing onboard-then-serve capstone didn't catch it: onboard would succeed but serve would refuse to boot. Fix lives entirely in the CLI runtime seam (build_runtime_input_with_options): on ApiKeyEnvUnset specifically, check the local-dev secret store for a key under the same LlmKeyStore handle serve reads at startup; if present, resolve the selection with a new composition helper (resolve_llm_selection_allow_missing_key) that treats the selected provider as keyless for this resolution only, letting apply_startup_stored_llm_key overlay the real key moments later. composition's llm_catalog stays store-agnostic; the store check is CLI-only. Feature-gated to mirror onboard's own libsql+ root-llm-provider cfg — a non-libsql build's behavior is unchanged. Scoped to RuntimeInputCaller::Serve only (not `run`): opening the secret store needs the local master key, which falls through to the OS keychain absent a cached dotfile — on `run`, a synchronous one-shot CLI invocation, that turned an ordinary "forgot to export the key" mistake into a hang instead of today's fast, clear error. Caught by running the full `run_rejects_empty_required_api_key_env` / `run_resolves_provider_from_config_and_demands_api_key_env` tests, which don't set IRONCLAW_DISABLE_OS_KEYCHAIN the way the smoke helper does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-cli): mount CLI-token login only for file-sourced tokens serve mounted the CLI-printed /login?token= route whenever SSO was off, regardless of where the webui bearer token came from. On a Railway-style deployment the token comes from IRONCLAW_REBORN_WEBUI_TOKEN, so mounting that route put the master bearer in a public route's query string, where an edge/proxy would capture it in access logs. resolve_webui_token now reports which source (env var vs the onboarding-provisioned token file) produced the resolved token (WebuiTokenSource); serve's cli_login_mount condition adds `source == File` alongside the existing `!sso_enabled` check. onboard's finale and status's login-link resolver both switch to resolve_login_link_announcement, printing a one-line note instead of a login link when the env var is active — a file-token link would otherwise point at a route serve no longer mounts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn-onboard): extract llm_credentials.rs from onboard/mod.rs Pure relocation of the LLM-credential provisioning machinery (provision_llm_credentials, already_configured_outcome, LlmKeyStoreOpener + impls, LlmCredentialProvisionOutcome, and their tests) into a new sibling module, matching the master_key/prompts split already established in this directory. No behavior change. * feat(reborn-composition): add RebornProviderAdmin::menu_entries() Adds a facade method returning the providers eligible for onboard's numbered menu (ApiKey/OpenAiCompatible/SessionToken SetupHint kinds), excluding ollama/bedrock/gemini_oauth/openai_codex/github_copilot by design — those stay reachable via config set / models set-provider. Returns a serializable ProviderMenuEntry DTO so the CLI never needs to depend on ironclaw_llm's SetupHint taxonomy. * feat(reborn-onboard): numbered provider menu with conditional API key prompt Reorders onboard's LLM-credential step: menu_entries() -> provider_menu() -> resolve_provider_id() as a canonical second check -> API key prompt only when the selected entry's api_key_required is true -> model prompt -> set_provider(). No-key providers (nearai) skip the key prompt and secret-store write entirely. Fixes an idempotency bug the reorder surfaced: already_configured_outcome previously required a stored key to short-circuit a rerun, so a no-key provider like nearai was never recognized as already configured and every onboard rerun re-prompted for it. Now "already configured" means [llm.default] is set AND (the provider doesn't require a key OR the store already has one) — see provision_llm_credentials_is_idempotent_for_a_no_key_provider for the red/green regression. Non-interactive (headless) onboarding behavior is unchanged; the menu only engages from a real terminal. * fix(reborn-onboard): drop openai_compatible from provider menu openai_compatible requires a base URL (base_url_required: true) that the numbered onboard menu never prompts for, so selecting it lands a config that fails serve boot with LLM_BASE_URL unset. Restrict the menu filter to ApiKey/SessionToken setup kinds only (drops openai_compatible and cloudflare, which share the same setup kind); both remain reachable via `models set-provider`, which does collect a base URL. * test(reborn-cli): dedup serve-banner polling, fix silent read/blocking-output smoke gaps - serve_boots_without_user_id_env_var and a_real_env_var_beats_the_config_default_end_to_end now call the shared wait_for_serve_banner helper instead of hand-rolling stderr-polling/timeout/cleanup. - http_response propagates a body-read failure instead of discarding it via `let _ =`. - serve_fails_closed_when_neither_env_nor_store_has_the_key spawns and polls try_wait() against a deadline instead of blocking on Command::output(), so a regression that makes serve bind instead of exiting can't hang the test/CI forever. Coderabbit fixes on PR #6174. * fix(reborn-cli): mark onboard's config-load fallback with silent-ok Adds the repo's literal `// silent-ok: <reason>` marker to the existing rationale comment on onboard's `.ok().flatten()` config-load fallback. Coderabbit fix on PR #6174. * fix(reborn-cli): redact bearer token from Debug, fail closed on non-UTF-8 token env var SECURITY: ResolvedWebuiToken now hand-writes Debug instead of deriving it, redacting `value` (the WebChat v2 bearer / session-signing key) while still showing `source`. env_token_is_active now returns Result<bool>, reusing commands::serve::present_unicode_env_var's unset-vs-not-unicode distinction: a genuinely unset/empty var is Ok(false), but a present-but-not-UTF-8 value is Err rather than silently read as "inactive". Before this fix, onboard/status could disagree with serve about which credential source is live for a mangled-UTF-8 token. resolve_login_link_announcement and its two callers (onboard's finale, status's login-link resolver) now propagate that error. Coderabbit fixes on PR #6174. * fix(reborn-composition): stop keyless retry masking real LLM catalog misconfig resolve_llm_selection_allow_missing_key tried the keyless registry unconditionally, so a genuinely misconfigured provider (api_key_required but no api_key_env, i.e. ApiKeyEnvUnconfigured) silently resolved keyless instead of surfacing. Now it tries normal resolution first and only retries keyless on the exact ApiKeyEnvUnset outcome; every other error propagates. The registry-level logic is factored into resolve_allow_missing_key_against_registry for direct unit testing. Coderabbit fix on PR #6174. * fix(reborn-webui-ingress): sanitize CliTokenLoginConfig::with_redirect_after with_redirect_after accepted an absolute or scheme-relative redirect target unvalidated. Unreachable today (serve never calls this setter), but hardened by reusing auth::pending::sanitize_redirect (now pub(crate)) — the same validation the OAuth login surface's own redirect_after query param goes through — falling back to the default "/v2" on an unsafe value. Coderabbit fix on PR #6174. * fix(reborn-cli): address coderabbit review on onboard provider menu Reorder provision_llm_credentials so both prompts (api_key, model) run before either write — a Ctrl-D/error on the model prompt can no longer leave an orphan key already committed to the secret store. Propagate provider-registry lookup failures in provider_api_key_required instead of swallowing them into "unconfigured", which previously masked a broken registry as a normal re-prompt case. Match menu_entries()'s onboarding-eligibility filter on the typed SetupHint enum instead of its string kind() representation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reborn-cli,webui-ingress): close onboarding-journey coverage gaps Add the full-chain capstone (onboard's login link authorizes a real webchat v2 request), pin the scripted model from `models set-provider` reaching the boot-time resolved-LLM trace, and prove the CLI-token login mount's exchanged bearer authenticates through the real SessionAuthenticator layer, not just decodes as JSON. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(reborn-composition): update pub-use snapshot for onboarding facade additions PR B intentionally adds new composition facade exports: open_local_dev_secret_store, LocalDevKeychainMasterKeyOutcome, provision_local_dev_keychain_master_key, LOCAL_DEV_SECRETS_MASTER_KEY_PATH, resolve_llm_selection_allow_missing_key, and ProviderMenuEntry. Refresh docs/plans/composition-pubuse.snapshot to match. * fix(reborn-cli): anchor service WorkingDirectory + report live service state launchd/systemd units now set WorkingDirectory to the Reborn home, fixing the crash-loop where cwd=/ made the local-dev workspace root overlap the default skill root /skills. `status` now queries the real OS service state (ServicePlatform::current_state) instead of only checking config/token files, and suppresses the login link with restart guidance once it knows the service isn't running. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-cli): stop config init/onboard from implicitly seeding [llm.default] A bare header parses as an empty slot, so removing only the fields would fail closed with MissingProviderId instead of falling through to env — comment out the whole [llm.default] section. config.toml is now the single source of truth for the LLM slot, written only by an explicit act. * feat(reborn-composition): add RebornProviderAdmin::detect_env_llm Composition-owned facade over ironclaw_llm's env-only provider resolution so ironclaw_reborn_cli (which may not depend on ironclaw_llm directly) can detect a fully env-configured LLM without reaching into that crate. Distinguishes "nothing set", "a complete provider detected", and "partial/ invalid env" so callers can react to each differently. * feat(reborn-cli): onboard env-detect-and-confirm / silent-seed LLM step Before the numbered provider menu, onboard now checks whether a complete LLM configuration is already detectable from the environment: - Interactive + detected: asks "Found <provider> in environment — use it?" (new PromptSource::confirm). Yes seeds [llm.default] via set_provider, storing no key (the env var stays the key source at runtime). No falls through to the full menu, unchanged. - Interactive + partial/invalid env: prints one line, falls through to menu. - Interactive + nothing detected: falls through to menu, unchanged. - Headless + detected: seeds silently, reported in onboard's output. - Headless + partial/nothing detected: seeds nothing; output teaches how to configure an LLM afterward (onboard interactively, config set/models set-provider, or export env vars). The already-configured idempotency check now runs before the interactivity gate (needed so headless reruns also skip re-seeding), so it also gained tolerance for an unparseable pre-existing config.toml — that check's job is deciding whether to skip work, not validating the file. * test(reborn-cli): rewrite onboard/serve capstones for the de-seeded stub onboard_then_serve_boots_with_an_empty_environment's old assertions pinned the stub-seeding bug (asserting a hardcoded nearai slot an operator never asked for). Rewritten as onboard_then_serve_boots_in_degraded_mode_with_an_ empty_environment: onboard completes with no LLM configured and teaches next steps; serve still binds (runtime resolution is unchanged — Ok(None) is not a boot-time hard failure) but logs the existing degraded-mode warning. New sibling onboard_with_complete_llm_env_then_serve_boots_from_the_env_ seeded_slot becomes the bootable-daemon-case capstone, proving a headless env-detected seed is actually WRITTEN to config.toml (via openai, since a no-key provider like nearai also satisfies the pre-existing env-based idempotency short-circuit and never reaches the write) and that serve resolves the PERSISTED model, not a fresh env re-resolution. Also: a first-run regression pin (fresh home + interactive + clean env must still invoke the numbered menu), config_text_has_live_provider_id helper (a plain string search also matches the stub's own commented-out example line), and touch-ups to the full-chain and Railway-shape tests for the same de-seeded stub. * fix(docker-reborn): stop shipping a baked-in [llm.default] stub docker/reborn/config.toml no longer ships an [llm.default] slot — Railway's env list is complete, so the runtime resolves the LLM via its existing env-fallback path with no slot needed. entrypoint.sh gains a narrowly-gated one-time volume migration: an existing deployment's persisted config.toml may still carry the identical stub every shipped profile config used to bake in (the entrypoint only installs a default config when none exists yet, so a pre-existing volume never picks up config.toml's edit on its own). Strips the section only on an EXACT match of the known old stub shape, backing up the pre-migration file alongside; anything operator-modified is left completely untouched. * chore(reborn-composition): update pub-use snapshot for detect_env_llm facade DetectedEnvLlm joins provider_admin's public re-export list. * fix(reborn-cli): anchor service WorkingDirectory at <reborn_home>/workspace The prior fix set WorkingDirectory to the Reborn home itself, but the Reborn home is an ancestor of every default local-dev skill/extension root, so composition's paths_overlap prefix check still refused to boot — same crash-loop, confirmed on the reporting user's machine. WorkingDirectory now points at a leaf directory, <reborn_home>/workspace, that overlaps none of them; install creates it (0755) before writing the unit/plist. Adds a crate-smoke test that boots the real binary with that cwd through to the ready banner, plus a permanent regression pin that reproduces the exact overlap error when cwd=reborn_home. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(onboard): arrow-key provider menu, live key/model probe, nearai required key Item 1: StdinPromptSource::provider_menu gains an interactive Up/Down/Enter menu (raw-mode, RAII-guarded, Esc/Ctrl-C cancels cleanly) with a pure apply_menu_key reducer; falls back byte-identically to the numbered-list prompt when the terminal can't support it or the operator starts typing. menu_entries() also excludes the onboard-written example overlay provider (id acme-openrouter) from the menu. Item 2: provision_via_menu probes the candidate key/model (via RebornProviderAdmin::probe_candidate, reusing the same transient-provider machinery the webui2 "test connection" probe uses) before any write. ProviderProbeOutcome carries no auth-vs-transport signal, so every failure takes one branch: show the message, offer "store anyway?" (yes stores, no reprompts up to 3 attempts). A model outside the reported list warns but still writes. Scope addition: nearai is session_token kind in providers.json (raw api_key_required: false), but reborn has no session-token auth wired (no SessionRenewer attaches at serve boot), so it dead-ends on the first chat turn without a key. menu_entries() now overrides api_key_required to true for session_token-kind providers; the onboard idempotency check uses the same effective_api_key_required so a nearai slot with no stored key is never treated as already configured. providers.json is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-webui-ingress): stamp operator capability on session mint, not validation The onboard `/login?token=` link authenticates against the same operator-capable EnvBearerAuthenticator as a raw `Authorization: Bearer` token, but `SessionAuthenticator::authenticate` hardcoded `WebuiAuthentication::user(...)` for every session bearer regardless of how it was minted — so a login-link session could never see provider settings / admin nav (`isAdmin` reads `capabilities.operator_webui_config`). Fix is provenance-based: `SessionRecord` gains `operator: bool` (serde default false, so pre-fix records and unmarked mints stay non-operator). `SessionStore::create_session` takes an explicit `operator` argument stamped by the caller at mint time; the CLI-token login handler passes the authenticate() result's operator bit, while OAuth/SSO and the admin-created-user token minter explicitly pass `false` per the "SSO/multi-user sessions stay non-operator" rule. `SessionAuthenticator::authenticate` reads the stamp instead of hardcoding `::user`. Regression coverage: an ingress-crate test proving the CLI-token-login exchange yields operator capabilities, an escalation-pin test proving operator=false never escalates, a serde-compat test for pre-fix records, and an extended cli/smoke.rs capstone that hits an operator-gated route with the login-link bearer. * style: prune narrative comments to codebase density (keep flow/edge bullets) Compress PR-added narrative/PR-story/signature-restating comments across onboard, serve/service/status, composition LLM admin, and webui_ingress auth code down to codebase-density bullet notes for flows and edge cases. Reviewer-demanded accepted-risk rationale (TOCTOU, keychain headless fallback, effective_api_key_required override, SSO-non-operator invariants, entrypoint volume migration) and silent-ok/dispatch-exempt markers are left untouched. No behavior changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-composition): probe nearai candidates against the coded base URL RebornProviderAdmin::probe_candidate passed providers.json's default_base_url straight through for the live key/model probe. nearai's catalog entry has no default_base_url (its default lives in code, cloud vs. private by key presence), so every nearai probe resolved an empty base URL and — through the resolver's key-presence check running before the candidate key is applied — always fell to the private endpoint, failing "could not reach the provider endpoint" even for valid keys. Compute the probe base URL from the candidate's own key presence via default_nearai_base_url (honoring NEARAI_BASE_URL), matching what the runtime resolver does once a key is actually attached. Swept the other providers.json entries with no default_base_url (gemini_oauth, openai_codex, openai, anthropic, cloudflare, bedrock): none of them resolve a coded default that depends on key presence, so nearai is the only special case. Also fixes two pre-existing clippy doc_lazy_continuation warnings surfaced by the current toolchain in files touched by the wrap-up gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): nearai always resolves the cloud-api base URL nearai's coded base-URL default keyed on API-key presence at resolve time (cloud when a key was already attached, private otherwise). An operator-stored key -- attached to the runtime after config resolution, via apply_startup_stored_llm_key -- always missed that window, so a key stored through onboard/models set-provider left the live runtime pinned to the keyless private endpoint even though the same key made the admin test_connection probe and the settings-panel snapshot correctly report cloud. Collapse default_nearai_base_url to a single unconditional default: explicit override (config slot or NEARAI_BASE_URL) wins, otherwise cloud-api.near.ai always. Resolution order no longer matters, so every caller (runtime resolution, the provider-admin probe, the settings snapshot) agrees without needing to know about key timing. v1 has no callers of the changed signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(webui): resolve WebuiAuthenticator through crate-local re-export Post-#6194 the trait moved from ironclaw_reborn_composition into ironclaw_webui itself (defined in webui_serve.rs, re-exported at the crate root). Rebase-era references to the old composition-root path didn't compile against main's current layout. * fix(reborn-cli): adapt CLI-token-login smoke tests to root-path serving #6152 moved the WebUI SPA to root-path serving: an unmounted /login now falls through to the SPA wildcard (200, generic shell) rather than 404, since composition only reserves root namespaces for routes it actually mounts (static_router_config_from_descriptors). Update the two must-not-mount smoke tests to assert on the property that actually matters — this route's own redirect-with-session-ticket handler never runs (no Location header) — instead of a status code the new routing model no longer produces. Also fixes the onboard-journey smoke test's redirect assertion, which still expected the pre-#6152 /v2 landing path. * fix(reborn-onboard): apply final review-fix batch for PR #6174 Comment fixes: non-operator mirror test for CLI-token login (closes hardcoded-operator blind spot); already_configured_outcome now fails loud on a malformed config.toml / registry lookup instead of silently re-prompting; TERM=dumb check case-insensitive; arrow-menu fallback carries the triggering keystroke into the typed prompt instead of dropping it; typed EnvDetection error instead of a bare reason string; overlay-exclusion test now built from the real PROVIDERS_STUB JSON; ScriptedProbe records and asserts its call args. Audit cuts: removed CliTokenLoginConfig::with_redirect_after (zero production callers); DEFAULT_LLM_* consts made private with a drift-pinning test against providers.json; ServiceStateDto switched from kebab-case to snake_case to match sibling enums; status.rs's exact-column text assertion switched to contains()-based. CI harness fix: serialize smoke.rs's serve-spawning tests behind a shared Mutex to kill a proven port bind-close-reuse race. Unresolved thread R4iWD: added a smoke test exercising the late stored-key path (apply_startup_stored_llm_key) for nearai's cloud base-url regression pin, which the existing test didn't cover. Also updates onboard_preserves_existing_config_without_force: a pre-existing unparseable config.toml is now a real onboard failure (matching the already_configured_outcome fix) rather than a silent success, so the test now pins the failure plus the Preserve write still landing correctly on disk beforehand. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-onboard): apply second review-fix batch for PR #6174 Addresses 6 of 11 fresh CodeRabbit findings: correct the stale nearai session-token comment, harden a drift test against ambient LLM env vars, test write_onboarding_marker through its caller, propagate a swallowed config-load error in status's credential-source selection, remove a TOCTOU double-read of the webui token file, route an LLM catalog test through the real allow-missing-key retry path, and reclassify EnvDetection as InvalidBindingRequest. The other 5 findings (dispatcher-bypass on onboard's pre-boot credential write, a raw base_url debug log, a duplicate-tier ticket-replay assertion, and the keychain has_master_key() error-collapse) are declined or documented as accepted risk in review replies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): apply stored LLM key to boot gateway via one reload chokepoint Cold boot always starts the placeholder-backed swappable gateway, then invokes RebornLlmReloadAdapter::reload() exactly once (the same path Settings -> Inference uses) to apply the resolved config and any stored key. Deletes apply_startup_stored_llm_key and build_llm_gateway's Some(cfg) fast path, which duplicated this and never reached the turn-serving provider. Adds one debug! trace (provider id + key-applied, never key material) to the reload adapter. Regression tests: two crate-smoke real-turn tests drive a spawned `serve` through the WebUI HTTP API against a local chat-completions stub, asserting the stub observes the stored key's Bearer header on a single boot and across two independent fresh boots. Also updates the composition-tier stored-key boot test to route through boot config + reload instead of a directly-injected ResolvedRebornLlm, since the gateway no longer takes that path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-onboard): store LLM keys in the runtime storage root serve reads Onboard's secret-store opener received the bare reborn home while serve opens <home>/<profile-subdir> — two different databases, so onboarded keys were invisible to the runtime (webui-saved keys worked because they write through serve's own store). The opener call sites now use the same local_runtime_storage_root as serve boot, creating it if missing; test verifications read back through that runtime root, pinning convergence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): address PR #6174 review threads A-E - smoke stub only reports auth for POST /v1/chat/completions, so an authenticated non-chat probe can't satisfy the chat-auth assertion - onboard's secret-store dir is created lazily right before a store is actually opened, so a headless no-op run leaves the filesystem untouched - add active_base_url dispatch-coverage test in ironclaw_llm config - EnvDetection errors are debug-logged with source and surfaced to the product boundary as a stable message instead of interpolating internals - service-state detection failures are debug-logged before falling back to Unknown Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-onboard): persist env-detected keys, rename LocalDev* types, discriminating smoke seed - Env-accepted providers (interactive [Y/n] and headless seed) now persist the detected API key to the encrypted secret store via the same path the menu flow uses. The installed service only carries IRONCLAW_REBORN_HOME, so a key living solely in the operator's shell env was unreachable at service boot — the slot was claimed but resolution failed and the placeholder gateway stayed wired. Flipped the two tests that pinned the old store-never-written behavior; their env-resolvable rationale doesn't hold under a service manager. - Renamed LocalDevKeychainMasterKeyOutcome -> KeychainMasterKeyOutcome and LocalDevLlmKeyStoreOpener -> EncryptedLlmKeyStoreOpener to satisfy the LocalDev* typename ratchet that landed on main (deployment mode is config, not a type). Restored the PromptSource trait import dropped as "unused" under default features (the call site is webui-v2-beta-gated) — fixes E0599 on all-features CI legs. - onboard_nearai_stored_key_then_serve_boots_with_cloud_base_url now seeds the runtime storage root serve actually reads and asserts the reload adapter's key_applied=true trace; proven discriminating (seed removed -> fails). - Master-key resolver doc comment now states the real precedence (cached dotfile -> env -> keychain -> generate) and why the cached key must win: the existing secret store is encrypted under it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-cli): resolve stored-key fallback against the runtime storage root resolve_reborn_runtime_llm_with_stored_key_fallback checked the bare reborn home for a stored LLM key, but onboarding writes it under <home>/<profile>/ (local_runtime_storage_root) — the same two-database class of bug fixed for onboarding in d7f84ea, missed at this call site. serve now reads the same root onboarding writes to, and treats a not-yet-created storage root as "no stored key" (fail through to the original ApiKeyEnvUnset) instead of surfacing a raw filesystem error. The existing regression test masked this because it seeded the stored key at the bare root, matching the bug instead of the fix; it now seeds at the runtime root to match what onboarding actually writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-composition,webui): cap NEAR AI login state, fix codex login race NearAiLoginStateStore::issue only pruned expired entries, growing unbounded within the 15-min TTL if callers mint redirects but never complete them. Cap it and evict-oldest, mirroring LoginTicketStore's MAX_TICKETS pattern. start_codex_login's "already in flight" check and attempt-map write were in separate lock sections with an unlocked initiate_device_code() await between them, so two concurrent calls for the same tenant+user could both start a device-code flow, with the second's insert silently orphaning the first's tracked attempt id. Reserve a placeholder entry under the lock before requesting a device code; a concurrent caller sees the placeholder and fails fast instead of racing a second request. Also adds the two ironclaw_webui LoginTicketStore tests from the same ledger: MAX_TICKETS eviction and single-redemption under concurrent take(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reborn): update composition pub-use snapshot for ratchet-mandated rename LocalDevKeychainMasterKeyOutcome -> KeychainMasterKeyOutcome changed the composition public facade intentionally (LocalDev* typename ratchet); the snapshot pins that facade and must follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): apply stored LLM key in reload for key-required providers RebornLlmReloadAdapter::reload used the strict resolver first, so an api_key_required provider (e.g. openai) configured only through the onboarding stored-key path (no env var) failed closed on ApiKeyEnvUnset before ever reaching the stored-key lookup, leaving the placeholder gateway wired forever on both boot-time reload and Settings -> Inference save. Fall back to the stored-key-tolerant resolution when a stored key exists for the provider, mirroring the CLI serve boot path's existing fallback. Adds a regression pin driving boot with openai configured via the stored-key path and no env var. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn-onboard): master-key root check, env-accept disclosure, codex finalize guard - master_key.rs: provision_master_key now checks the dotfile at local_runtime_storage_root, not the bare RebornHome root, mirroring 2571a42's fix for the same two-root-class mismatch. The bare-root check was always false, so onboarding re-attempted keychain provisioning on every rerun. Adds a regression test seeding the dotfile at the runtime root and asserting the no-op path. - llm_credentials.rs / mod.rs: corrects the env-accept disclosure text (doc comment and the onboarding println) — the detected API key is persisted to the encrypted secret store so a background service can resolve it, not left solely in the env var as previously stated. - llm_config_service.rs: finalize_codex_login_slot guards a stale device-code finalize from clobbering a newer reservation that was made after the stale attempt's TTL lapsed. Adds a regression test driving reserve -> expire -> reserve -> stale finalize and asserting the newer reservation survives untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reborn): pin probe_candidate wiring through a live HTTP stub RebornProviderAdmin::probe_candidate had zero coverage: onboard tests always inject fake probes. This seam already produced a real bug (empty base URL -> always "could not reach"). Add a local loopback HTTP stub asserting the probe hits the CONFIGURED base URL with the ENTERED key (200 -> ok, 401 -> not ok). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): tighten codex-login finalize to absent-or-mismatch abort finalize_codex_login_slot only blocked overwriting a DIFFERENT attempt id; an absent entry (superseding reservation failed and removed itself before the stale device-code request returned) passed through and let the stale finalize reinsert itself. Require the entry to still be present and match, returning false otherwise so the caller aborts without inserting or spawning a poller. Also moves probe_candidate's live-stub tests out of provider_admin.rs into tests/provider_admin_probe.rs: the architecture boundary test reborn_product_api_crates_do_not_bind_http_ingress text-scans every src/ file for a loopback bind with no #[cfg(test)] awareness, and the in-module stub tripped it (matches webui_v2_serve.rs's existing pattern). While moving, fix probe_candidate_reports_401_as_not_ok to await the captured request and assert method/path + Authorization — previously it discarded the receiver, so a transport failure would have passed identically to a real 401. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
13-commit window af0b9de..c1a9807 re-expressed onto the unified generic extension architecture. Dominant event: #6194's webui consolidation (ingress + webui_v2 + composition webui_serve -> single ironclaw_webui) adopted with the retired Slack host-beta lane excised; #6173 runtime.rs test extraction adopted with this branch's test module landing in runtime/tests/core.rs; #6195/#6197 §4.3 filesystem-backed store refactors adopted; #5978 run_id threaded through this branch's dispatcher/engine construction sites; #6172/#5523/#6152/#6148/#6150/ #6151/#6181 present-verbatim. Full dispositions in the PR-body fifth-fold ledger. Verified locally: workspace check + clippy -D warnings (all targets, all features) green; architecture 43/0; composition CI-bucket 1533/0; product_workflow 633/0; runner 673/0 (CI features); cli 432/0 (CI features); webui 378/0; loop_host 492/0; host_runtime 344/0 + 42 sandbox tests with Docker; auth 128/0; dispatcher/capabilities/ extension_host/authorization/approvals all green; frontend tsc + vitest 773/0; oauth_connect integration 21/0 with colima up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(ci): dev metrics + composition mass ratchet gate (#6167)
* chore(ci): dev metrics + composition mass ratchet gate
Adds a three-tier development-metrics tool and a guardrail that stops the
ironclaw_reborn_composition crate from accreting more of the codebase.
scripts/dev_metrics.py — three tiers from git + GitHub + working tree:
- Tier 1 flow/speed: PR lead time, size distribution, merge cadence
- Tier 2 quality/stability: change-failure proxy, rework, test share
- Tier 3 codebase health: composition mass, v1 src burndown, file sprawl,
abstraction density, boundary-test coverage
Composition mass ratchet — the dependency-boundary tests police edges
*between* crates but are blind to mass piling up *inside* one crate.
ironclaw_reborn_composition is charter-bound to assembly-only wiring yet is
now ~26.7% of all production crate code. This gate is that missing guard:
- scripts/ci/composition-budget.toml — committed ceiling (enforce +
tolerance), modeled on the existing coverage-floor ratchet
- scripts/ci/check-composition-budget.sh — pure-bash gate; one-directional
(fails only on growth past the ceiling), emits a down-ratchet nudge as
carve-outs free up slack
- scripts/ci/test-check-composition-budget.sh — 22 assertions / 10 fixture
cases incl. a guard that the real tree passes the committed budget
Wiring:
- CI: new composition-budget job in code_style.yml (runs the gate + self-
tests it, registered in the aggregating code-style gate)
- Local: pre-commit-safety.sh runs the gate when composition or the gate
itself is staged; dev-setup.sh install message updated
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): address review — production-only metric, script hardening, dev-metrics tests
Review feedback on #6167 (gemini, ironloopai, coderabbit):
Blocking — gate counted test-only code despite its documented "tests
excluded" contract. Exclude test-only FILES (tests.rs/test_*.rs/*_tests.rs
and /tests/ dirs) from both numerator and denominator; rebaseline the
ceiling 2670 -> 2398 bp (26.70% -> 23.98%). Inline #[cfg(test)] modules
remain a documented, symmetric residual (a line-counter can't parse them).
Added a regression case proving test files are excluded.
check-composition-budget.sh: toml_get no longer aborts under set -e +
pipefail when a key is missing (|| true) so schema validation is reached;
added a missing-key regression case.
test-check-composition-budget.sh: set -euo pipefail (repo invariant);
SIGPIPE-safe capture + fixture generation; pure-bash asserts (no pipes).
dev_metrics.py: bound `gh` with a 30s timeout and treat non-JSON output as
unavailable; fix the trait-impl density regex to count `impl<T> ... for`
generics; harden find/grep/wc probes with pipefail + rc checks (no more
false-zero metrics); UTF-8 file writes; surface the gate-aligned production
share as the ratchet metric and relabel the byte-based trend as a distinct,
coarser measurement; extract a pure classify_commit helper.
New scripts/test_dev_metrics.py — caller-level unit tests for
classification, percentiles, change-failure bucketing, rendering, and the
test-file/impl regexes; wired into the composition-budget CI job.
pre-commit hook: trigger on any staged crates/**.rs change (the metric is a
ratio) and document the working-tree/CI-authoritative limitation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): harden PR classifier against transient GitHub API flakes
The classify job (#6167 CI) failed with `invalid character '<' looking
for beginning of value`: a transient API error returned an HTML page,
`gh --jq` aborted, and under `set -e` the whole labels-only job failed
and blocked the PR.
pr-labeler.sh now:
- routes every gh call through a `gh_retry` wrapper (retry + linear
backoff), and
- treats each classifier as best-effort — a step that still can't fetch
after retries only emits a `::warning::` and the script exits 0, so
labeling never gates a merge.
Two bash traps fixed along the way, both caught by the new test:
- a bare `if cmd; then …; fi` resets `$?` to 0 after `fi`, so gh_retry's
give-up looked like success — capture rc in the `else`;
- `set -e` is suppressed inside a function on the left of `||`, so the
classifiers check their own fetches explicitly instead of relying on
errexit.
Regression test: .github/scripts/test-pr-labeler.sh (retry/backoff,
give-up, and end-to-end non-fatal + happy-path via a fake `gh`), wired
into the code_style "Static-check self-tests" step and the has_code
path filter so it runs when the labeler or its test changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(ci): add dispatch (Arc<dyn>) ratchet + dev-metrics dispatch signals
Companion to the mass ratchet for the "reduce traits & dispatch" goal
(#6168 / runtime-decomposition plan #4471).
check-composition-budget.sh now enforces TWO metrics: composition's share of
production crate code (existing) AND its Arc<dyn> dispatch count. The dispatch
count is scoped to composition production files EXCLUDING src/slack and
src/extension_host — those are owned by the separate channel/extension
refactor, so this gate must not govern or trip on their work. One-directional
like the mass ratchet: only trips on growth; nudges when slack accrues.
composition-budget.toml: arc_dyn_ceiling = 1093 (current governed count),
tolerance 15.
test harness: +6 dispatch cases (within / breach / dry-run / slack+extension
exclusion / missing-key schema error); budget() helper carries the dispatch
keys; count_arc_dyn tolerates no-match under set -e + pipefail. 36 cases pass.
dev_metrics.py: Tier-3 reports governed Arc<dyn> count and distinct dyn-trait
count (the dispatch-breadth trend), matching the ratchet scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(reborn-cli): background service install (launchd/systemd) + service restart (#6172)
* feat(reborn-cli): background service install (launchd/systemd) for ironclaw-reborn
Extracted from #6157 (service half only; TUI stays parked there). Adds
`service install/uninstall/start/stop/status`, the serve-invocation
plist/unit contract (IRONCLAW_REBORN_HOME only, no secrets), and the
`full` feature bundle with libsql as default storage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(reborn-cli): add `service restart` verb
Composes the existing stop+start through the shared ServiceCommandRunner
dispatch. Stopped service starts cleanly; uninstalled service errors with
guidance; a failed start after a successful stop reports the service as
stopped rather than half-restarted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(reborn-cli): pin service-PR surface invariants
Extend help_mentions_reborn_commands to assert `service` is listed
under webui-v2-beta and that no `tui` subcommand exists; add
service_help_lists_all_verbs pinning the six service verbs
(install/start/stop/status/restart/uninstall).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(reborn-cli): dedupe service restart, normalize status output
Extracts the shared restart decision tree into restart_generic (fn-pointer
seam; platforms keep only their own detection), normalizes `service status`
to one running/stopped/not-installed vocabulary on both platforms, hoists
write_atomic so launchd plist writes are crash-safe, and fixes a stale
verb-count doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): preserve raw systemd ActiveState as a status detail line
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): harden service module per PR review (PID parse, systemctl parsing, perms, reload, orphan status, rollback, preflight)
Addresses 9 verified findings from PR #6172 review:
1. launchd service_running misread `-` (loaded-but-stopped) PID as
running; mirrors operator_service_lifecycle's launchd_status_from_line
shape, split into service_running (has PID) vs service_loaded (any
status) since uninstall/install genuinely need the latter.
2. systemd query_unit_state now parses Key=Value lines (order-independent,
no --value) and errors on a missing required key instead of
unwrap_or_default(), which silently read as enabled=false.
3. write_atomic sets 0600 on unix before create_new, matching
operator_service_lifecycle's write_service_file.
4. launchd install now unloads/reloads a currently-loaded job after
rewriting the plist, so a reinstall actually picks up the new
ProgramArguments/EnvironmentVariables.
5. status now queries the manager unconditionally on both platforms so
an orphaned unit (file removed out-of-band, still loaded/enabled)
reports installed; two tests that pinned the old skip-when-absent
behavior were pinning the orphan-hiding bug and are updated/renamed.
6. systemd uninstall's remove_file failure now routes through the same
rollback path (restore file + reload + re-enable) as a daemon-reload
failure, via an extracted rollback_uninstall helper.
7. preflight_warnings gained a webui_token_file_is_valid check; doc
comment corrected to describe what's actually checked.
8. Documented (not built) that launchd's StandardOutPath/StandardErrorPath
logs are unrotated, in both a code comment and `service install --help`.
9. Cargo.toml `full` feature now includes root-llm-provider so
`--no-default-features --features full` stays self-contained.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(reborn-cli): adopt the canonical service identity (com.ironclaw.reborn)
The CLI service surface and the WebUI operator facade
(RebornLocalServiceLifecycle) now deliberately share one unit name and
launchd label. CLI installs atomically replace a facade-installed unit —
a security improvement, since the facade bakes the WebUI token into the
unit file while the CLI unit is secret-free. Consolidating the two
implementations is a documented follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): CI mock fidelity, token-file hygiene, review fixes
1. mod.rs: add the "systemctl show unit state" arm to the shared
SuccessfulServiceCommandRunner mock (install_with_runner now queries
unit state pre-write). Production code and the strict parser are
correct; only the mock modeled reality incompletely.
2. webui_token.rs: propagate real I/O errors instead of treating them
as "absent" (was silently overwriting unreadable tokens); reject
symlinked/oversized token files; repair (not reject) a wrongly
permissioned but valid token on accept; serve.rs no longer collapses
VarError::NotUnicode into "unset".
3. launchd.rs/mod.rs: suppress the "keeps the OLD definition" advisory
when install already reloaded a loaded job in place (the definition
is live immediately in that case); systemd's advisory is unaffected.
4. mod.rs: preflight-warning coverage now drives service install
(runner-injectable, warnings returned) instead of only unit-testing
the preflight_warnings helper directly.
5. systemd.rs: uninstall's remove_file step is now injectable so its
rollback test forces a deterministic failure, replacing the
chmod-0o555 approach that silently no-ops under a root test runner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): preserve error source chain in service rollback failures
combined_failure flattened the primary error and rollback outcomes into
one anyhow!() string, losing the source chain. Use .context() so the
primary stays inspectable via source()/{:#} beneath the rollback text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): single-handle token read closes TOCTOU window
read_token_file_checked previously stat'd then read the token file as
two separate syscalls, letting a symlink/FIFO/oversized file be swapped
in between them; a FIFO also passed the length check and could block
serve startup indefinitely. Now opens once with O_NOFOLLOW|O_NONBLOCK,
checks type/size via fstat on that handle, and bounds the read to
MAX_BYTES+1 from the same fd. Non-unix keeps the prior best-effort path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): uninstall disable rollback, hermetic verb dispatch, dry-run coverage, docs
Four verified findings from PR #6172 review round: (1) systemd uninstall's
disable failure now rolls back like every sibling failure branch instead of
propagating with a bare `?`; (2) a smoke test pins that a directory at the
webui-token path fails `onboard --dry-run` non-zero without mutating home;
(3) start/stop/restart/status/uninstall get the same runner-injectable split
`install` already had (`ServicePlatform::*_with_runner`), with one
consolidated clap-dispatch test instead of duplicating per-verb coverage;
(4) FEATURE_PARITY.md and CHANGELOG.md reflect the shipped service-install
feature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): single status line per service restart
restart_generic's stop/start fn pointers called the loud
start_with_runner/stop_with_runner, which each print their own
"Service started"/"Service stopped" line in addition to
restart_generic's own summary line, so `service restart` printed two
lines. Add quiet variants (start_with_runner_quiet/
stop_with_runner_quiet) that skip the println, used only by
restart_with_runner; the public start/stop commands keep printing as
before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): stop/restart honor manager-loaded state like status/uninstall
launchd `stop` gated on service_running alone, leaving a loaded-but-not-
running KeepAlive job (`-` PID) registered for respawn; `restart` derived
`was_running` the same way, so it bare-loaded an already-loaded label
(which launchd errors on) instead of reloading. systemd `stop` gated only
on unit-file existence, silently no-opping on a unit removed out-of-band
while still loaded/enabled. All three now check manager state the way
status/uninstall already do.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(reborn-cli): honor XDG_CONFIG_HOME for systemd units, guard launchd start on loaded labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(reborn-cli): make service tests hermetic over XDG_CONFIG_HOME
commit 5b3f39eb9 made config_home() honor $XDG_CONFIG_HOME, which
unit_path() now reads. Service tests that fake $HOME into a tempdir
never cleared XDG_CONFIG_HOME, so on hosts where it's set (CI runners
observed setting it to $HOME/.config), unit_path() resolved to the
real path instead of the tempdir — causing
systemd::tests::restart_not_installed_errors_with_install_guidance
and
tests::install_then_uninstall_linux_writes_and_removes_unit_file to
fail. Production config_home() behavior is unchanged and correct.
Extends the TempHomeGuard helpers in mod.rs and systemd.rs (new,
mirroring mod.rs's) to also clear/restore XDG_CONFIG_HOME, and
switches all HOME-faking tests in systemd.rs onto the guard instead of
manual set/restore blocks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ci): stop safety ReDoS timing guards flaking under coverage [skip-regression-check] (#6181)
The `*_100kb_near_miss` adversarial tests in ironclaw_safety are ReDoS
guards: they feed a ~100 KB near-miss payload to a regex scan and assert
it finishes fast enough to rule out catastrophic backtracking (which
would take seconds or hang). They used a hard 100 ms bound.
Under `cargo llvm-cov` instrumentation on shared CI runners the linear
scan is ~100x slower, so the Coverage (default) job flaked with
"anthropic_api_key pattern took 101ms on 100KB near-miss" — 1 ms over
the threshold. Only the instrumented coverage job is affected.
Replace the per-test hard thresholds (100 ms in leak_detector/validator/
sanitizer, 500 ms already in policy) with one documented shared constant
REDOS_SCAN_BUDGET_MS = 2000 in the crate root. 2 s keeps a large margin
below any real ReDoS while tolerating instrumentation overhead, and the
guards still fail loudly on genuine catastrophic backtracking.
Test-only change; no production behavior touched — hence the
regression-check skip.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(e2e): black-box smoke for ironclaw-reborn serve — restart + kill-9 durability (#5523)
The in-process Reborn integration harness cannot prove real process
startup, real HTTP end-to-end, or process-death durability — its
new_at_path() reopen approximates a restart but never actually kills a
process. Add a thin, permanent black-box smoke suite that boots the
real ironclaw-reborn binary and drives it purely over HTTP:
- boot -> /api/health -> scripted chat round-trip
- tool-call turn executes and finalizes a reply
- graceful restart (SIGINT) preserves thread history
- kill -9 durability: on-disk libsql state survives an unclean death,
server comes back healthy, no leaked child processes
- bearer-auth boundary (401 without token, 200 with)
Fixture design: reuses the existing reborn_v2_restartable_server
fixture (already restart-capable against a persistent home dir) rather
than porting the legacy ManagedIronclawServer class. Extends its
stop() closure with a `hard: bool = False` flag for SIGKILL, so the
fixture's tuple shape and existing consumer are untouched. Promotes
the capability-preview polling helpers out of
test_reborn_webui_v2_legacy_tool_execution.py into the shared harness
(now used by both files) instead of adding a second copy for the new
suite.
Mutation-verified the durability scenario: temporarily pointed
restart() at a fresh home dir per call, confirmed the kill-9 test goes
red for the right reason (persisted thread missing after "restart"),
then reverted to a clean diff.
Wires a `blackbox-smoke` CI job into the existing reborn-e2e.yml
job-per-file pattern (mirrors webui-v2-smoke's build step, no
Playwright/Node needed since this suite is HTTP-only).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(webui-v2): improve toast lifecycle and accessibility (#6151)
* fix(webui): improve toast lifecycle and accessibility
* test(e2e): cover toast lifecycle and stacking
* fix(webui): type toast presentation mappings
* test(e2e): fast-forward toast hover timing
* test(e2e): align toast clock requirements
* fix(webui-v2): add theme selection controls to Appearance settings (#6148)
* fix(webui): add theme controls to appearance settings
* test(e2e): cover appearance theme persistence
* fix(webui): address appearance accessibility review
* fix(webui): type appearance theme controls
* fix(webui): use native theme radios
* feat(reborn): serve webui at root path instead of `v2` (#6152)
* feat(reborn): serve the WebUI from root paths
* test(e2e): cover root-mounted Reborn WebUI
* fix(webui): reject noncanonical SPA paths
* fix(webui): address root-mount review feedback
* refactor(webui): derive static router config errors
* fix(webui-v2): surface workspace download failures (#6150)
* fix(webui-v2): surface workspace download failures
* test(e2e): cover workspace download failure feedback
* test(e2e): centralize workspace download selectors
* test(e2e): navigate workspace downloads through UI
* docs(reborn): propose architecture simplification — fewer DTOs, less dyn, no local-specific structs (#6175)
* docs(reborn): propose architecture simplification — fewer DTOs, less dyn, no local-specific structs
Design note proposing a fundamental simplification of the Reborn host/runtime
internals, grounded in a code audit and a cross-reference against the last ~30
days of PRs/issues.
Thesis: DTO proliferation (~14 mirror structs per capability call), dyn
proliferation (~6 hot-path trait objects, most single-impl), and local-specific
store structs are three symptoms of one decision — treating every crate boundary
as a trust boundary when Reborn has exactly one (loop <-> host).
Proposes: one canonical payload type in ironclaw_host_api (Invocation/Authority/
Outcome), authority as a single fold, a closed RuntimeLane enum instead of dyn
RuntimeAdapter, backend-generic stores (RowBackend) to delete the InMemory*/
Filesystem* tree, and DeploymentConfig-as-data instead of composition-mode
struct families. Preserves all security invariants; incremental migration with
a first-party-lane proof-of-concept slice. Complements #6168.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): show the field-level "why" behind the ~14 re-wraps
Fold the mechanistic root cause into §1.1: a hop-by-hop field diff of the five
request types, showing only three are genuinely distinct states and the other
two are duplication forced by the crate DAG plus dead transitional fields
(trust_decision is ignored by DefaultHostRuntime; idempotency_key is
unimplemented). Names the four mechanisms and quantifies the ~40% that is pure
duplication.
Add §3.1 mapping the five request types onto the three real states
(Invocation -> +Authority -> +resolved handles), showing how each mechanism is
eliminated or made explicit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): show impl/store-level "why" for the dyn and stores sections
§1.3: replace the flat dyn table with prod-vs-test-double counts and storage
(Arc<dyn>), and name the three mechanisms — trait-as-test-seam (HostRuntime: 1
prod + 6 doubles; CapabilityDispatcher 1 + 2), speculative replaceability, and
generic-and-dyn double indirection. Correct a mislabel: CapabilityHost is a
concrete generic struct, not a trait; the dyn on that path is the dispatcher it
holds. RuntimeAdapter = 5 impls (4 lanes + resolver), a closed set.
§1.4: quantify the per-backend, per-domain store duplication with LOC (turns
~4,260 in-memory vs ~1,710 filesystem) and a domain table (turns/processes/
approvals/authorization/run_state), and name the two mechanisms — logic welded
to storage, multiplied by the TurnRun/processes lifecycle split.
§4.2: drop CapabilityHost from the "delete trait" list (already concrete) and
route the test-seam need through generics/one boundary fake.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): OS mechanism/policy framing — kill in-memory stores and Local*
Fold in five directives:
- §2.1: the operating-system lens — kernel = mechanism (small, frozen, feature-
agnostic vocabulary + a few real seams); everything that varies by feature or
deployment is policy resolved to data at the edge. Adding a feature must not
change the kernel.
- §4.3 (rewritten): the storage seam already exists — RootFilesystem, with a
first-class InMemoryBackend. Delete every hand-written InMemory*Store; tests use
FilesystemXStore<InMemoryBackend>. No RowBackend to invent.
- §4.4 (new): local-dev is a policy config (a DeploymentConfig value), NOT an
implementation. The ~66-identifier LocalDev* shadow runtime across 42 composition
files collapses to one config literal selecting shared substrates. Rename the two
genuine resource types (LocalFilesystem->DiskFilesystem, LocalHostProcessPort->
HostProcessPort). Enforce with a no-"Local*"-type-names boundary test.
- §4.5 (new): enumerate and freeze the kernel boundary — host_api's ~124 types +
the ~13 AgentLoopDriverHost ports + the mediators. Move runtime_policy (mode
enums) out of the vocabulary; freeze the neutral authority survivors by test.
- §5/§7/refs updated: before/after rows for in-memory stores and Local*; migration
resequenced by risk (delete-in-memory and Local*->config are the low-risk first
slices); new evidence pointers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): audit result — does DeploymentConfig express everything?
Four-cluster audit of the ~40 LocalDev* types (policies, stores, capability
wiring, trust/evidence) against "config not code". Adds §4.4.1 with the verified
verdict: DeploymentConfig expresses every local-dev *selection*, but the LocalDev*
family is three things, and zero-LocalDev is three moves not one —
1. Already config: the capability policy is literally a TOML file; stores are the
same shared types prod uses (production_turn_state_store<F> called by both),
backend-selected; LocalDevOverride trust seam is inert.
2. Mis-prefixed shared substrate (gate-evidence readers, lease-terms provider,
auth read-model, capability IO): genuine code but not local — de-prefix and
share, not configify.
3. Genuine local-only mechanism (capability-port decorator stack: synthetic tools,
surface disclosure, mid-run refresh): behavior stays code, but config-GATED
shared middleware, not a LocalDev* factory. Synthetic product-ops
(project_create/skill_activate/result_read/outbound_delivery) should become
first-party capabilities on the normal lane.
Security note: no trust/approval bypass found — override inert, provider trust
only user_trusted, gate-evidence readers fail closed. §8 Q3 answered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): add target-structure/interfaces section + shell-escape case study
§5 (new) — Target structure: the minimal kernel and clean interfaces. Component
table (kernel = authority/recovery; substrates = mechanism behind ports; loops
and products = replaceable userland). Interface sketches: the one generic
ProductSurface (open/submit_turn/events/reply/resolve_gate/cancel — feature-
agnostic), the kernel authorize/dispatch, the AgentLoopHost trust membrane, the
substrate ports (RootFilesystem, ProcessSandbox with scope-only SandboxMount,
SecretBroker, NetworkPolicy), and DeploymentConfig-as-data. Structure diagram.
§6 (new) — Case study: the shell cross-tenant escape (#6170). Verified root cause
(shell is a real OS subprocess the virtual FS doesn't bound; unsafe host port is
the default; HostedSingleTenant -> LocalSingleUser -> LocalHost) and how the §5
structure makes it structurally impossible (ProcessSandbox as the only path,
unconstructible host port, mode-from-fact, fail-closed, two-user containment test).
Renumbered subsequent sections (7 before/after, 8 invariants, 9 migration,
10 open questions); added #6170 to references.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(rules): add process/shell tenant-isolation invariant; point architecture rule at the plan
safety-and-sandbox.md: new "Process and shell execution: real OS isolation, per
tenant" section — the standing invariant issue #6170 violated. Codifies that the
virtual ScopedFilesystem does not contain a subprocess; multi-user/served
deployments must route process spawns through TenantSandboxProcessPort with a
scope-derived mount (never LocalHostProcessPort); deployment mode must reflect
multi-user serving; fail closed (no sandbox => no shell, never host shell); and
requires a two-user cross-tenant escape test for changes to process ports,
planner backend rules, or the profile->mode mapping.
architecture.md: add a "Direction" pointer to the simplification design doc as
the owning plan for the DTO/dyn/InMemory*/LocalDev* debt the smells describe, and
cross-reference type-placement.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): §5.8 — products are adapters over ProductSurface, collapse composition-split surfaces
Every product owns its whole host side (protocol + transport + identity) as one
adapter consuming the kernel ProductSurface; composition holds no product/transport
code. Quantifies the split: WebUI across 4 places (webui_v2 + webui_ingress +
static + composition/webui), and ~108K LOC of product code in the god-crate
(slack 40.6K, product_auth 32.7K, runtime 14.5K, llm_admin 8.5K, automation 6.1K,
webui 4.6K, outbound 1.8K). Telegram is the closest-to-clean reference shape.
Invariant enforceable by an ironclaw_architecture test banning slack/webui/
telegram/openai/transport identifiers from composition. Ties to §4.4 and #6168.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): §10 — enforcement / anti-slippage ratchets pulling together all checks
Consolidates the per-axis static checks into one table: process isolation (#6170
two-user escape test), mirror DTOs (check-type-duplicates.py), dyn mediators,
InMemory*Store, Local* types, host_api freeze, and products-in-composition — each
with its home, the addable-now ratchet (freeze current count/allowlist, fail on
new), and the hard ban that is also its definition-of-done when the axis lands.
Notes the guardrail self-test + two-hook-path requirement and that Local*/
InMemory*/composition bans must start as frozen allowlists (can't hard-ban today).
Renumbered Open questions to §11.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): add §11 testing (state-machine invariants, idempotency from any state); distinguish crate seams from trust boundaries
§11 Testing (new): specifies the behavioral suite — the state machines to pin
(turn/run, capability invoke, lease, gate/resume), the invariants that must hold
from ANY reachable state, the idempotency contract, and how to reach arbitrary
states (model-based stateful property tests, exhaustive state×op enumeration,
fault injection), cross-backend parity, fail-closed/adversarial, interface
conformance harnesses, and integration-first tiering. Design only.
Trust-boundary correction (per review): the doc overstated "exactly one trust
boundary." Reborn has several — the untrusted loop, untrusted runtime-lane
execution (WASM/script/MCP/containers/external services), and untrusted
runtime-supplied data (egress/worker output) — all mediated and all preserved.
What collapses is the trusted mediation-chain CRATE SEAMS, not a trust boundary.
Fixed intro, §2 (retitled + enumerates the boundaries), §5.4, §8 invariants
(added lane + data boundaries; fixed stale RowBackend/TurnStore refs to
RootFilesystem), and §11.6 (lane/worker/egress adversarial). Renumbered Open
questions to §12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): fix last 'the one trust membrane' → the loop's (one of several)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): consistency pass + address PR review feedback
End-to-end review fixes (also addressing gemini/coderabbit comments; note some
reviewed a superseded head that still proposed RowBackend/TurnStore<B>):
- authorize() no longer takes a separate `scope` that can diverge from
Invocation.scope — derive from inv.scope (§3, §5.3). [coderabbit]
- Clarify the result contract: Outcome carries success OR recoverable failure;
the seam is Result<Outcome, Blocked>; no separate Err(terminate) (§3). [coderabbit]
- Mark `Authority` SEALED — private fields, host-only construction via authorize()
(§3, §5.3); resolve open-question 2 accordingly. [gemini + coderabbit]
- Align §4.4 LOCAL_DEV process field with §5.6/§6: HostUnsandboxed(LocalOnly),
gated by a local-only token a served boot can't mint.
- Retire the RowBackend framing (superseded by RootFilesystem) and note deleting
InMemory*Store has zero persistence-compat impact; durable backends untouched
(§4.3). [gemini/coderabbit]
- Add file:line evidence for the five request types + audit date/window (§1.1).
[coderabbit]
- Intro: "most with exactly one prod impl" -> "several" (only 2 of 5).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): §12 — performance-critical paths (locking, remote-store latency, event fan-out)
Adds a performance section grounded in the actual hotspots: consolidating stores
onto RootFilesystem (§4.3) makes the backend latency profile the kernel's, and it
can be remote (libSQL/Postgres). Ranked critical-path table (per-turn ~11-store
fan-out; heartbeat vs store-lock with lease-TTL coupling; libSQL BEGIN IMMEDIATE
single-writer + #5751/#6089 contention; authorize() per-tool-call reads; active-
thread lock; event append/projection fan-out; recovery poll). Plus the
no-lock-across-remote-I/O rule (turn_scheduler.rs:787/881), what the refactor
helps vs risks (centralizing latency/writer contention onto one seam), and design
guidance (batched per-transition write, isolated heartbeat, cached read-mostly
authority, async coalesced events, writer sharding). Renumbered Open questions
to §13.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): §13 — answer the open questions directly (now Decisions)
1. TurnRun vs ironclaw_processes: converge the mechanism (shared LeasedWorkUnit —
§4.3 already collapses the store layer), keep the policy distinct (turn resume-
from-checkpoint vs process terminal+re-spawn); bonus, the shared lease-recovery
gives processes the reconciler they lack.
2. Authority: one sealed value (host-only construction via authorize()); narrowed
read projection if an adapter needs a field.
3. DeploymentConfig: surface-disclosure derived from process:HostUnsandboxed;
mid-run refresh gated by session:LongLived|PerRun; synthetic tools promoted to
first-party capabilities (default hidden in hosted) — a security improvement
(they gain authorize/scope-binding). Remaining items are tuning knobs, not
architecture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): cross-reference the in-progress Unified Extension Runtime (BenKurrek gist)
Adds §5.9 mapping this doc against the URT extension/adapter/auth design: strong,
independent convergence (no-product-code-in-composition; config-not-code as
recipe+engine auth; runtime-kind=closed lane set; built-ins on the identical
pipeline; the trust boundaries; ProductSurface above the host pipelines).
Complementary scope: URT is the deep extension/adapter/auth axis, this doc the
broader kernel refactor; they compose (URT's dispatcher pipeline = authorize+
dispatch; adapter invoke/deliver = RuntimeLane execution).
Adopts two URT refinements: (a) product_auth collapses to recipe data + one host
AuthEngine, not per-adapter code (§5.8); (b) the Deletion/Addition/retired-
taxonomy tests are the products-in-composition ratchet (§10). Clarifies WebUI
consumes ProductSurface directly and is not a ChannelAdapter. Adds a reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): §5.9 — RuntimeLane reconciliation + ToolPorts↔Authority integration points
Name the two seams where this doc and the Unified Extension Runtime must agree:
1. One closed execution enum RuntimeLane = {FirstParty|Wasm|Mcp|Process}; the URT's
extension-declarable runtime *kinds* (first_party/wasm/mcp) are a strict subset.
Process (OS-subprocess/script sandbox) is host-only — no manifest can select it;
only host built-ins (shell/script) dispatch to it via ProcessSandbox. Load kind
(URT) vs execution lane (this doc) are different axes; don't merge them.
2. ToolPorts is derived from Authority, never independent: dispatch() materializes
egress (NetworkPolicy + host-side SecretBroker lease), state (ScopedFilesystem =
Authority.mounts), logging from (&Invocation, &Authority, descriptor). ToolPorts
can't be wider than Authority grants; the adapter never sees Authority itself. So
URT's ToolAdapter::invoke(call, ports) IS the body of dispatch(inv, auth, lane).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(reborn): address CodeRabbit review findings on the latest head
Nine substantive design-contract fixes:
- §3 core model: LoopRequest (loop pre-trust, input-by-ref) resolved to Invocation
at the membrane; Authorized = sealed AND invocation-bound (actor/scope/activity_id
provenance) so dispatch can't be handed a mismatched (inv,auth); activity_id IS
the invocation idempotency identity (idempotency_key unified with it, not deleted,
satisfying §11.3); three distinct outcome channels Blocked | HostFailure | Outcome
(no Ok(Failed)/Err ambiguity). Type count 3→4. §3.1/§5.3/§5.4/§11.1 aligned;
Authority→Authorized throughout.
- §11.2/§6: scope cross-tenant isolation to multi-user/served deployments (matrix
test), not "any deployment state" (single-user local legitimately allows host proc).
- §9/§10: quarantine the known-red two-user test (#[ignore]/expected-fail until the
fix merges); ratchets freeze checked-in symbol allowlists (set membership), not
aggregate counts (a swap evades a count).
- §12: durable event append is atomic with the state transition (same tx/outbox);
only subscriber fan-out is decoupled.
- §5.8: adapters resolved via a product-neutral ExtensionId-keyed factory registry
passed to composition as input; config lists ids, not types.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(reborn): approval stores over RootFilesystem, delete InMemory*Store (§4.3) (#6195)
* refactor(reborn): approval stores over RootFilesystem, delete InMemory*Store (§4.3)
First slice of the architecture-simplification note
(docs/reborn/2026-07-17-architecture-simplification-dto-dyn-local.md §4.3):
"in-memory" stops being a bespoke store and becomes a filesystem backend, so
each approval domain has one production Filesystem*Store<F> exercised over the
in-memory backend in tests and libSQL/Postgres in production — no parallel
hand-written implementation to keep in lock-step.
Deletes the three hand-written approval stores in ironclaw_approvals
(InMemoryAutoApproveSettingStore, InMemoryPersistentApprovalPolicyStore,
InMemoryCapabilityPermissionOverrideStore, plus the InMemoryToolPermissionOverrideStore
alias). Everything now runs the existing Filesystem*Store<F>:
- ironclaw_approvals: adds a `test-support`-gated helper module with
in_memory_backed_* constructors (the production store over a fresh
InMemoryBackend mounted at /approvals). The stores' own unit tests move onto
Filesystem*Store<InMemoryBackend>, proving it covers the deleted stores' cases.
- composition factory.rs: the LocalDev* approval-store aliases collapse to one
unconditional Filesystem*Store<LocalDevRootFilesystem>; the no-durable-features
local-dev builder wires them over the composite root filesystem (in-memory
backed) via the existing scoped-filesystem path instead of the deleted
InMemory* stores. wrap_scoped / invocation_mount_view and the /approvals mount
machinery are un-gated so both builders share one path.
- host_runtime production-wiring guard: the fail-closed LocalOnly classification
now keys on FilesystemPersistentApprovalPolicyStore<InMemoryBackend> instead of
the deleted InMemory type. Production (<LibSql>/<Postgres>) and durable-local-dev
(<Composite>) classifications are unchanged; the guard contract test is
repointed and still asserts LocalOnly.
- downstream test suites (host_runtime, composition, product_workflow) repoint to
the test-support helpers; the affected crates enable ironclaw_approvals/test-support
in [dev-dependencies].
Net subtractive (−136 LOC). No trust boundary or persistence-compatibility change:
the in-memory approval stores were volatile/local; the durable libSQL/Postgres
backends are untouched. Boundary tests (ironclaw_architecture) stay green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(reborn): address review — migrate root harness, honest volatile approval-store type
Two review findings on the approvals-store consolidation:
1. Root integration harness left uncompilable. `tests/integration/support/
harness/mod.rs` still constructed the deleted `InMemory*Store`s (as
`Arc<dyn …>` defaults). Repoint to the `in_memory_backed_*` helpers and enable
`ironclaw_approvals/test-support` in the root `[dev-dependencies]`.
2. Guard weakened for the no-durable composition. The composite-unified alias made
the no-durable-features build wire
`FilesystemPersistentApprovalPolicyStore<CompositeRootFilesystem>`, whose
TypeId misses the guard's `<InMemoryBackend>` branch, so the volatile store was
classified `ProductionCandidate`. Fix by making the store type honestly reflect
its volatility: the no-durable build now backs the three approval stores with a
dedicated `InMemoryBackend` directly (via `wrap_scoped`), so the concrete type
is `Filesystem*Store<InMemoryBackend>` — which the production-wiring guard
classifies `LocalOnly`, exactly as the sibling `InMemoryRunStateStore` /
`InMemoryCapabilityLeaseStore` are. Durable builds keep the composite-backed
type (distinct, correctly a production candidate). The `LocalDev*` approval
aliases go back to cfg-split (InMemoryBackend vs composite); the guard contract
test now documents that it exercises the exact type the no-durable composition
wires. `local_dev_scoped_filesystem` is re-gated to durable-only (the no-durable
builder no longer uses it); `wrap_scoped`/`invocation_mount_view` stay ungated
since the no-durable builder now calls `wrap_scoped` directly.
Verified: composition compiles + clippy clean on default (no-durable) and libSQL;
guard contract test green; local_dev_authorization tests green; root
reborn_integration_* targets compile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(reborn): consolidate WebUI host stack into a single ironclaw_webui crate (+ Slack/OpenAI-compat wiring) (#6194)
* refactor(reborn): merge WebUI host stack into ironclaw_reborn_webui_ingress
Fold `ironclaw_webui_v2` (route surface + SPA bundle) and composition's WebUI
middleware/assembly into `ironclaw_reborn_webui_ingress` so the whole WebUI host
stack is one crate above composition, and composition shrinks.
Move-only for behavior; the composed `webui_v2_app` Router, middleware order,
descriptors, and security invariants are unchanged (locked by the moved contract
tests + the composition/ingress router tests, all green under default features).
Structure:
- `ironclaw_webui_v2/src/*` -> ingress `src/webui_v2/` (public module,
unconditional); `build.rs` + `frontend/` moved to ingress; crate deleted and
removed from workspace members (68 -> 67).
- Composition WebUI middleware (`webui_body_limit`, `webui_operator_auth`,
`webui_rate_limit`, `webui_route_match`, `webui_ws_origin`) + `webui_serve.rs`
-> ingress `src/`.
- `webui_serve.rs` split: `WebuiServeConfig`/`webui_v2_app`/`WebuiV2App`/
`Webui{Serve,Config}Error`/`WebuiAuthenticator`/`WebuiAuthentication` move to
ingress; the mount vocabulary (`PublicRouteMount`/`ProtectedRouteMount`/
`PublicRouteDrain(s)`) stays in composition (`webui/route_mounts.rs`) because
nearai/openai/runtime construct it — moving it up would cycle.
- Product-auth decoupled: `ProductAuthRouteState`, `product_auth_route_mount`,
`ProductAuthRouteMount` exposed `pub` + re-exported from composition root;
ingress imports them (+ `RebornWebuiBundle`, `GoogleOAuthRouteConfig`) via the
composition facade. Composition no longer depends on `ironclaw_webui_v2`.
- Callers repointed: composition tests, ingress tests, reborn_cli
(serve/webui_auth), root v1 int-tier tests + dev-dep, Dockerfile.reborn +
smoke test frontend path, and the ironclaw_architecture boundary spec.
Deferred (out of scope, feature-gated off by default): the
`slack-v2-host-beta` / `openai-compat-beta` blocks in `webui_serve.rs` still
reference composition-internal surfaces and compile out under default features
(declared as known cfgs). Wiring the Slack/OpenAI-compat host surface through
ingress is a follow-up; composition's slack feature will not build until then.
[skip-regression-check] move-only refactor; behavior covered by relocated
contract tests and existing composition/ingress router suites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reborn): make Slack + OpenAI-compat host-beta build and wire after WebUI merge
The WebUI host-stack merge (parent commit) hoisted `webui_v2_app` + its config,
authenticator, and middleware surface from `ironclaw_reborn_composition` up into
`ironclaw_reborn_webui_ingress`, but left the `slack-v2-host-beta` /
`openai-compat-beta` blocks in the moved `webui_serve.rs` pointing at
`crate::slack::*` / composition internals that don't exist in ingress. Those
features were declared only as known-cfgs and compiled out, so:
- composition failed to build under `slack-v2-host-beta` (two mount-vocabulary
imports still on the old `webui::webui_serve` path);
- the ingress serve blocks were permanently dead, so the CLI's slack/openai
features forwarded to composition but never mounted the Slack routes —
a functional parity break, not just a compile break;
- composition's slack-gated tests still imported the moved `webui_v2_app`.
Wiring (behavior-preserving; restores pre-merge parity):
- ingress now defines real `slack-v2-host-beta` / `openai-compat-beta` features
that forward to composition (+ optional `ironclaw_reborn_openai_compat`); the
moved `webui_serve.rs` reaches Slack setup/route types and the
OpenAI-compat bearer-evidence helper through composition's public facade
(`ironclaw_reborn_composition::{SlackPersonalSetupServiceSlot,
SlackChannelRouteAdminRouteConfig, slack_channel_route_admin_route_mount,
SlackPersonalOAuthBindingConfig, mark_bearer_token_verified_for_tenant}`).
Ingress does NOT depend on `ironclaw_product_adapters` directly — the
architecture boundary (`reborn_dependency_boundaries.rs`) forbids it, so the
evidence helper is re-exported from composition instead.
- composition promotes `slack_channel_route_admin_route_mount` + its
`SlackChannelRouteAdminRouteMount` return type to `pub` (its sole caller,
`webui_v2_app`, moved up), mirroring the already-public `ProtectedRouteMount`.
- CLI forwards `slack-v2-host-beta` / `openai-compat-beta` to the ingress crate
as well as composition, so the serve blocks compile in and the routes mount.
Tests:
- The 7 composition slack unit tests that drove the now-relocated `webui_v2_app`
move to `ironclaw_reborn_webui_ingress/tests/slack_host_beta_webui_v2.rs`.
They use only composition's public builders, so ingress (which normal-deps
composition — single crate copy, no dev-dep cycle) is their correct home; a
composition lib-test cannot call the ingress `webui_v2_app` without cargo
building two incompatible copies of composition. composition's ingress
dev-dep gains `slack-v2-host-beta` so its own `webui_v2_product_auth*` tests'
`with_slack_*` blocks compile.
Verified (clean env): composition/ingress/cli build + clippy `-D warnings` under
both beta features; ingress `--all-features` suite green incl. the 7 relocated
tests; composition lib (1589) + router (webui_v2_serve 44 / product_auth 51) +
cli (440 incl. Dockerfile smoke) green; `ironclaw_architecture` boundaries hold.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(reborn): rename crate ironclaw_reborn_webui_ingress -> ironclaw_webui + doc pass
Now that the crate owns the whole WebUI host stack (route surface + SPA +
gateway assembly/middleware + serve loop + host auth), "reborn_webui_ingress"
undersells it. Rename the crate to `ironclaw_webui` and refresh its docs to
describe the composed subsystems.
Rename (pure identifier swap, no behavior change):
- `git mv crates/ironclaw_reborn_webui_ingress crates/ironclaw_webui`; package
`name` + workspace members + root dep alias updated.
- Every `ironclaw_reborn_webui_ingress` reference repointed across Rust, Cargo
manifests, Cargo.lock, Dockerfile.reborn, CI scripts (.sh/.py), the
`ironclaw_architecture` boundary spec (crate_name / forbidden lists / layer
exception / source-path prefixes), root + crate CLAUDE/AGENTS docs, .claude
rules & skills, and the security-parity docs. `openwiki/` (auto-generated) and
`docs/plans/` (historical) intentionally left for their own regen/record.
Docs (README.md new; AGENTS.md + CLAUDE.md restructured):
- README.md: human-facing overview with the three-piece fold-in map
(route surface + SPA from the former `ironclaw_webui_v2`; gateway assembly +
middleware from `ironclaw_reborn_composition::webui`; serve loop + host auth
from this crate's original scope), layering/boundaries, feature flags, build/test.
- AGENTS.md: replaced the stale "deliberately small" framing with an accurate
agent map — composed subsystems, do-not-move-in, allowed deps, how to add a
route / authenticator / OAuth provider.
- CLAUDE.md: reframed opening (it no longer is a "counterpart to webui_v2_app" —
that fn lives here now); Surface table gains the route/gateway symbols
(`webui_v2_router`, `webui_v2_routes`, `WebUiV2State`, `WebUiV2HttpError`,
`webui_v2_app`, `WebuiServeConfig`); folded in the WebChat v2 route table +
streaming/SSE model + SPA build detail; test layout now lists the
route-surface/gateway suites. OAuth login security contract retained verbatim.
Verified: `cargo metadata` resolves; `cargo build -p ironclaw_webui` and
`-p ironclaw_reborn_cli --features slack-v2-host-beta,openai-compat-beta` green;
`cargo test -p ironclaw_architecture reborn` (boundaries, new name) green;
`cargo test -p ironclaw_webui --features slack-v2-host-beta --test
slack_host_beta_webui_v2` green; 0 stale `ironclaw_reborn_webui_ingress` refs
outside openwiki/docs-plans.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reborn): address PR review findings + stale ironclaw_webui_v2 refs post-merge
Review findings on PR #6194 (gemini-code-assist + ironloopai) and the leftover
references the `ironclaw_webui_v2` → `ironclaw_webui` fold-in left behind.
CI / build path migration (ironloopai "path migration incomplete"):
- Repointed the deleted `crates/ironclaw_webui_v2/frontend` build path to
`crates/ironclaw_webui/frontend` across all workflows (code_style, coverage,
ironclaw-stress, platform-and-compat, reborn-e2e, reborn-playwright),
`.dockerignore`, `scripts/run-reborn-webui.sh`, `scripts/ci/quality_gate_strict.sh`,
and the `regression-test-check.yml` frontend-test detector.
- Test bucketing: dropped the dead `ironclaw_webui_v2` entries from
`reborn-crate-test-buckets.sh` + `package-feature-flags.sh` (the renamed
`ironclaw_webui` entries already exist), repointed `classify-test-scope.sh`,
and widened the `reborn-tests.yml` jq filter to `startswith("ironclaw_webui")`
so the folded crate's tests still land in the webui bucket.
- QA inventory (`scripts/reborn_qa_matrix/audit_surface_inventory.py`) now reads
`crates/ironclaw_webui/src/webui_v2/descriptors.rs`.
- Regenerated `harness/latency/runner/Cargo.lock` (transitively referenced the
deleted crate via composition's `webui-v2-beta`).
Broken doc links / stale comments (gemini):
- `nearai_login_serve.rs` + `runtime.rs`: the broken intra-doc link
`crate::webui::route_mounts::WebuiServeConfig` (type moved out of composition)
is now a plain code span `ironclaw_webui::WebuiServeConfig::with_public_route_mount`
— composition cannot link into `ironclaw_webui` (not a dependency).
- `webui/facade.rs`: comment now says routing/auth/static/SSE live in
`ironclaw_webui`; only the route-mount vocabulary stays in `route_mounts`.
build.rs frontend opt-out (gemini):
- `SKIP_FRONTEND_BUILD=1` skips the Node/pnpm frontend build for backend-only
dev / docs.rs / minimal CI images (`webui_enabled = env::var_os(...).is_none()`).
Guidance docs (ironloopai "update AGENTS/CLAUDE + crates/AGENTS.md"):
- `crates/AGENTS.md`: rewrote the `ironclaw_webui` row to the whole WebUI host
stack, removed the deleted `ironclaw_webui_v2` row, repointed cross-refs.
- Refreshed `ironclaw_webui_v2` → `ironclaw_webui` across living guidance
(`.claude/` rules/skills/commands, `crates/README.md`, `crates/Architecture.md`,
`crates/ironclaw_projects/CLAUDE.md`, `ironclaw_reborn_composition/CLAUDE.md`,
product_workflow comments, security-parity docs) and the `-p ironclaw_webui_v2
--features webui-v2-beta` commands. `ironclaw_webui_v2_static` (a distinct,
still-live v1 crate) left untouched.
Already addressed earlier in this PR, confirmed still green post-merge:
- Slack / OpenAI-compat host-beta compile + wiring (the `slack-v2-host-beta` /
`openai-compat-beta` findings) — commit `a2ed602`.
- The obsolete `/v2` SPA mount — replaced by main's root-serving
`static_router_with_config` in the merge (`c000a16`).
Verified: clippy `-D warnings` on composition (`webui-v2-beta`) and
`ironclaw_webui` (`--all-features`); root int-tier webui tests compile;
QA-inventory path resolves; harness lock clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): rustfmt import ordering after crate rename + refresh composition pub-use snapshot
Two CI failures on PR #6194:
- **Formatting / Code Style (fmt+clippy)**: the `ironclaw_reborn_webui_ingress`
→ `ironclaw_webui` rename shifted where the crate sorts in `use` blocks, so
rustfmt wanted to reorder imports across ~26 files. I had wrongly reverted
those fmt-only files during the rename commit (assuming rustfmt-version
drift); the reordering is deterministic and CI's gate caught it. Ran
`cargo fmt --all`.
- **Test Reborn crate bucket (adapters-misc)** →
`composition_public_pub_use_surface_matches_snapshot`: this PR intentionally
changed composition's public facade — `webui_serve`/`Webui*`/`webui_v2_app`
moved out to `ironclaw_webui` (so composition's `webui` re-export is now just
`route_mounts::*`), the product-auth mount builders were exposed, and the
Slack channel-route mount + `mark_bearer_token_verified_for_tenant` were
promoted. Regenerated `docs/plans/composition-pubuse.snapshot` by replaying
the test's own `extract_pub_use_surface` extraction; the diff is exactly those
intended facade changes.
Verified: `cargo fmt --all -- --check` clean; `cargo test -p
ironclaw_architecture --test reborn_composition_boundaries` green (8 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(composition): extract runtime.rs inline test module (Phase 0) (#6173)
* refactor(composition): extract runtime.rs inline test module to sibling file
Moves runtime.rs's trailing `#[cfg(test)] mod tests { … }` (~6.9k lines) into
`runtime/tests/core.rs` via the crate's existing `#[path = "runtime/tests/…"]`
convention. Pure move — the module keeps its identity (`crate::runtime::tests`),
so all `super::`/`crate::` refs resolve unchanged; cargo fmt de-indented the
relocated items. runtime.rs: 11,673 -> 4,709 lines.
Phase 0 of the composition decomposition (parent #4471, plan #6168): single-
crate, zero cross-crate coupling, does not touch slack/ or extension_host/.
Fixes the crate's worst file-size violation and — because the inline test block
no longer counts as production LOC (it's now a test-only file, excluded) —
ticks the composition mass ratchet down (~23.98% -> ~23.2%).
Verified: `cargo test -p ironclaw_reborn_composition --all-features --no-run`
compiles all relocated tests unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(composition): rustfmt runtime test module + merge main
Remove stray leading blank line in runtime/tests/core.rs flagged by the
Formatting CI check, and merge origin/main to bring the branch current.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Require read-before-edit and reject stale edits in reborn coding tools (#5978)
* Ride out provider outages and drop the 32-call turn cap in the reborn loop
Two failure modes discovered via claw-swe-bench-lite run 9ca133e5 (30% vs
hermes 65% on the same model) discarded hours of agent work:
- A transient provider 5xx storm aborted the whole run after 2 quick
retries (max_attempts_per_class=2, backoff capped at 5s). Availability-
class model errors (transient/unavailable/internal) now retry on their
own deeper budget: max_model_availability_attempts=12 with a 1s..60s
exponential backoff, riding out ~7 minutes of sustained provider
failure. MAX_MODEL_RETRIES raised 8 -> 16 to let the strategy govern.
- DefaultBudgetStrategy's iteration_limit=32 failed closed mid-task with
no synthesis (llm_calls in failed bench tasks clustered at exactly
63/64/127/128). The default is now DEFAULT_ITERATION_BACKSTOP=1024
(subagent 16 -> 256), documented as a runaway backstop: operational
bounds are the resource budget system and stop-condition strategy.
New seam mirroring IRONCLAW_REBORN_PLANNED_DEFAULT_ITERATION_LIMIT:
IRONCLAW_REBORN_MODEL_AVAILABILITY_RETRY_ATTEMPTS ->
DefaultPlannedRuntimeConfig.planned_model_availability_retry_attempts ->
families::default_with_overrides. The integration group harness pins
attempts=1 so scenarios that deliberately script provider failures
(failure_category_demasked) reach Failed in seconds, not minutes; the
availability-retry tests run under paused tokio time.
Family fingerprint digests regenerated for the new strategy parameters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Surface tool-failure reasons to the model for shell and coding tools
Benchmark traces showed the model retrying identical failing calls blind:
builtin.shell parameter errors and coding-tool path rejections reached it
as a bare category ("the tool input could not be encoded") because the
handlers built FirstPartyCapabilityError/CodingCapabilityError with no
safe_summary — the model-visible Diagnostic detail channel downstream was
already wired but starved (one agent burned 13 apply_patch calls against
an out-of-scope /testbed path with empty errors).
- shell.rs: shell_error/process_error now carry the concrete reason
("missing 'command' parameter", timeout duration, spawn failure),
bounded to 512 chars. The strict safe-summary validator still falls
back to the fixed category string; the reason always survives on the
secret-scrubbed diagnostic channel.
- coding/paths.rs: scoped-path rejections name the offending path and
the available scoped roots; permission rejections say the operation is
not permitted on that mount.
Covered at the dispatch tier (coding state dispatch, host-runtime
invoke_capability) per test-through-the-caller.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Run agent_loop scenario test targets under paused tokio time
The deep availability-retry backoff added for provider-outage ride-out
made outage-scripting scenario tests sleep for real: safety_nets alone
took ~423s (the exact cumulative backoff schedule) because scripted or
script-exhausted model errors now retry for minutes. Pause the clock on
all executor scenario targets — they drive the in-process mock host
exclusively, so timers auto-advance and the suites return to seconds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fail fast when no LLM provider is configured instead of riding availability retries
The placeholder unconfigured provider's RequestFailed was mapped through the
catch-all Unavailable arm, so the new deep availability retry budget rode a
permanent configuration fault through ~7 minutes of exponential backoff.
Users with no LLM configured waited minutes for an error that retrying can
never fix, and the Reborn CLI smoke tests that pin fast nonzero exits timed
out (the 4 failures on CI run 29136954176).
Map errors carrying the shared UNCONFIGURED_PROVIDER_ID to
CredentialUnavailable, which is unclassified in loop recovery and therefore
terminal on first sight; the Settings → Inference hint travels on the
scrubbed detail channel. The provider id moves to a shared constant in
ironclaw_llm so the composition placeholder and the runner mapping cannot
drift.
Regression tests: unconfigured_provider_error_maps_to_credential_unavailable_
not_availability and unconfigured_provider_detection_requires_the_placeholder_
provider_id in model_gateway.rs; the existing smoke tests
(*_exits_nonzero_when_runtime_does_not_produce_reply) pin the fast-fail at
the caller tier.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Derive override-built default-family replay identity from resolved config
families::default_with_overrides swapped budget/recovery strategies but
reused the planner's static version digest, so an overridden composition
carried the pure-default replay identity — violating the component-identity
contract (family.rs: the digest identifies replay-relevant configuration).
- Turn the cfg(test) fingerprint const into a runtime
default_family_fingerprint(iteration_limit, model_availability_attempts)
builder; override-built families hash it with their resolved values at
composition time (BLAKE3, same path as the pinned const). The pure-default
composition keeps the static DEFAULT_FAMILY_DIGEST, and overrides spelling
out the production defaults hash to that same digest.
- Collapse the two Option args into a FamilyOverrides struct and drop the
now-dead (None, None) branch in the runner's registry factory.
- Tests: digest differs per override knob and is deterministic; explicit
production defaults reproduce the static digest; an attempts=1 override
reaches the composed recovery strategy (one retry then abort).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Let the recovery strategy own the model retry guard; wake backoff on cancel
Two model-stage fixes from the PR 5959 review:
MAX_MODEL_RETRIES=16 silently capped any configured availability budget of
16+: the retry loop fell through to a generic ModelError exit with
FailedExitDetails::default() — no failure category, no diagnostic ref —
before the strategy could reach its own Abort. The executor now derives the
loop bound from the composed strategy via
RecoveryStrategy::max_total_model_attempts() (DefaultRecoveryStrategy
computes it from its per-class + availability budgets with margin), so
every accepted override reaches the strategy's abort boundary. The
contract-bug fall-through now carries the last observed model error's
category and diagnostic ref instead of empty details.
The availability backoff sleep (up to 60s per attempt) was not
cancellation-aware: a cancel request could wait out the full delay. The
sleep now selects over the host's cancellation_requested() future (same
pattern as the prompt-compaction and failure-explanation waits), and a
boundary cancel check right after the alteration turns the wake into a
checkpointed Cancelled exit without issuing another model call.
Tests (paused tokio time): an availability budget of 20 — past the old
executor cap — fails with the strategy's model_unavailable category and
diagnostic ref after exactly 21 model calls; cancellation requested during
the first 1s backoff exits Cancelled without riding out the sleep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Clarify DEFAULT_ITERATION_BACKSTOP doc: resource budgets are not yet enforced
The doc claimed operational bounds come from the resource budget system,
but ResourceBudgetPolicy.max_model_calls and the wall-clock cap are defined
and not applied; until they are, this backstop and the stop-condition
strategy are the only live ceilings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Carry tool-failure reasons to the model past the strict summary validator
PR 5959's headline feature (model-visible tool-failure reasons) never
reached the model for path-bearing reasons: LoopSafeSummary rejects
path/payload delimiters and newlines, and dispatch_failure_message
silently degraded every such reason to the generic category sentence
before it could reach the diagnostic channel.
- production.rs (failure_from): a host-authored safe_summary that fails
LoopSafeSummary validation is preserved as the new
DispatchFailureDetail::Diagnostic instead of being dropped; the
message keeps the fixed category sentence (host-authored, Invariant 2).
- capability_port.rs: maps the Diagnostic detail into the model-visible
CapabilityFailureDetail::Diagnostic, scrubbing secret values and
normalizing control characters the observation validator rejects (so
one stray escape byte cannot drop the whole observation); newlines
are preserved. The RetrySameCall arm now forwards structured detail
too.
- coding/paths.rs: scoped-path rejection summaries render the path and
available roots delimiter-free ("path testbed replacer.go",
"available roots: workspace") so they pass the strict validator —
FilesystemDenied surfaces as a Denied loop outcome whose only
model-visible channel is the summary itself.
- shell.rs: bounded_failure_reason documents the (now real) diagnostic
flow; truncation remains char-based (no byte-boundary panics).
Regre…
Summary
/v2URLs while preserving query parametersLinked Issue
Closes #6142
Validation
cargo clippy --workspace --all-targets --all-features -- -D warnings/v2compatibility E2ETesting
ironclaw_webui_v2test suitewebui_v2_servecomposition integration suiteSecurity Impact
The root SPA wildcard keeps
/api,/auth,/v1, and/webhooksfail-closed. Legacy redirects normalize slash and backslash variants to prevent external navigation, and the wallet popup retains its isolated CSP.Database Impact
None.
Blast Radius
Affects Reborn WebUI browser routes, static/PWA assets, wallet-connect paths,
and OAuth/NEAR AI browser redirects. Deployments and reverse proxies must
forward
/,/assets/*,/vendor/*, and/wallet/connect.Legacy
/v2browser URLs temporarily redirect to their root equivalents.WebChat API paths under
/api/webchat/v2/*and the legacy v1 gateway remainunchanged.
Rollback Plan
Revert this PR, or its resulting merge/squash commit, to restore the
/v2SPA mount. The compatibility redirects use temporary 307 responses, so no
permanent redirect should remain cached.
No data migration is involved. Root-path bookmarks created after rollout will
not work against the rolled-back binary until the root-mounted version is
deployed again.
Review track: C