feat(extensions): register hosted MCP servers - #6930
Conversation
…d lifecycle group Three test-infrastructure capabilities the MCP-registration workstream needs: - MockMcpServer: start_mock_mcp_server_paginated() serves tools/list paginated with nextCursor, honoring an incoming cursor param. New integration test hosted_mcp_discovery_reads_only_first_page_of_paginated_tools_list pins today's client behavior: discover_tools reads only page 1 and never follows nextCursor (parse_tools_list_result never inspects it). Documents the bug, does not fix it (production untouched). - assert_extension_present mirrors assert_extension_absent in extension_user_lifecycle_isolation.rs; wired into the existing install/remove symmetry test. - RebornIntegrationGroup::extension_lifecycle_no_auto_approve(): same profile as extension_lifecycle() with auto-approve off, so builtin.extension_install (PermissionMode::Ask) raises a real BlockedApproval gate. New test proves the block and discriminates against the auto-approve-on group, which completes the same install gate-free. No production code changed.
…namespace
Seed change for user-registered MCP servers (dark, no client/UI):
- ManifestSource::UserRegistered, never eligible for first-party/system
trust (sits with InstalledLocal), with a wire label ("user_registered")
that round-trips and doesn't break deserializing older persisted rows.
- Reserved `mcp-` extension-id namespace enforced as a single arm in
parse_v3, mirroring the existing `ironclaw.` host-bundled reservation,
so both the direct parse_v3 call and the public
ExtensionManifestRecord::from_toml entry point inherit the rule.
- Pin generic_host.rs's no-durable-record classification fallback (keys
off RequestedTrustClass, not ManifestSource — adding a ManifestSource
variant produces no compiler error there) so it can't silently start
granting elevated trust to a new source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Relocate assert_user_present calls in extension_user_lifecycle_isolation to the one spot where presence isn't immediately re-implied by the next assert_user_phase call (the normalized-restart test's post-install checkpoint, before any phase check happens). - Document why extension_lifecycle_approval_gate.rs keeps its own test target instead of folding into group_extensions/. - Replace the dangling "see brief" pointer in mcp.rs's paginated-discovery test comment with a concrete description of the pending fix and the assertion it should flip to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ExtensionManifestV2::from_raw only enforced the ironclaw. host-bundled reservation; the mcp- user-registered reservation lived solely in parse_v3, so a v2-schema manifest with a mcp- id and a non-UserRegistered source parsed successfully. Extract a shared, predicate-per-entry reserved-prefix table (check_reserved_id_prefix) and call it from both parsers so the two prefix rules can't drift apart again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three capabilities the integration suite could not previously express, each blocking later MCP-registration work: - MockMcpServer pagination (nextCursor across >=2 pages). Without it a page-1-only client bug passes every test. Ships with a test that pins the current truncation so the later fix has a concrete regression to flip; the repo's own testing rule forbids ignored/TODO-pinned tests, so a green test documenting the bug beats a red one nobody runs. - assert_extension_present, the mirror of the existing absent helper. - A lifecycle group with auto-approve OFF, wired like live_approvals(). extension_lifecycle() has it ON, so until now no test could prove any lifecycle gate actually blocks. Reviewed by code-review and thermo-nuclear at low effort: no severe findings. Both flagged the redundant presence assertion and a dangling doc pointer; both fixed. Test-only — no production file changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The foundation for user-registered MCP servers. Dark — no surface, no MCP client code, no activation changes. - ManifestSource::UserRegistered, never eligible for first-party or system trust, round-tripping through the persisted wire form as "user_registered". - The mcp- id prefix is reserved: only a UserRegistered manifest may claim it. - A pinning test for generic_host.rs's fallback, which matches on RequestedTrustClass rather than ManifestSource and so gives no compiler signal when a variant is added. Review found a real hole in the first attempt: the namespace check lived only in parse_v3, but from_toml routes to parse_v3 only for v3 manifests, so a v2 manifest could claim an mcp- id with any source. The "both import paths" test missed it by using a v3 manifest. Both parsers now call one shared reserved-prefix helper, which closes the gap and removes the copy-paste seam a third prefix would have widened. The helper carries a predicate per rule rather than a source value, so the host-bundled rule keeps its capability semantics (allows_first_party) instead of being flattened to source equality. Gate proofs: the v2 gap was demonstrated red before the fix; the trust and classification pins were proven by inverse assertion, since the harness correctly refuses to let a trust gate be weakened even transiently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # crates/ironclaw_extension_host/src/available_extension_import.rs # crates/ironclaw_extension_host/src/test_support/lifecycle.rs # crates/ironclaw_product/src/auth_continuation.rs # crates/ironclaw_reborn_composition/src/factory.rs # crates/ironclaw_reborn_composition/src/runtime.rs
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
🚅 Deployed to the ironclaw-pr-6930 environment in ironclaw-ci-preview
|
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughAdds hosted MCP registration across host API contracts, OAuth admission, durable preparation, lifecycle activation, runtime discovery, trust handling, WebUI routes, modal UI, and integration coverage. ChangesHosted MCP registration
Estimated code review effort: 5 (Critical) | ~180 minutes Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 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 |
🔎 Review · PR #6930
The target changed before this Run could finish. Automatic · PR opened · attempt 0 of 3 · cancelled after <1s Run details
|
# Conflicts: # crates/ironclaw_webui/src/webui_v2/handlers.rs # crates/ironclaw_webui/src/webui_v2/mod.rs
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f48abc584
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Resolve the eight content conflicts and relocate host_api::hosted_mcp into ironclaw_extension_contracts: it and package_lifecycle reference each other, so once package_lifecycle moved, leaving hosted_mcp behind would have required host_api to depend on a workspace crate. That cycle produces no git conflict -- the two sides are different files -- so it is recorded in the crate guide, the checklist row, and the location scan's frozen names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on (#6979) #6930 (2e65225, 153 files, +15,002/-1,818) merged after the north-star docs were written, and it landed entirely inside the extension family the restructure is about to reshape. Docs-only reconciliation, house style: dated amendments beside the original text, nothing rewritten or deleted. PROPOSAL: new #6930 entry in §2.7 (the anchor, same shape as #6691/#6696/ #6863); re-measured §2.4 extension_host (56,940/93) and §2.5 host_api (27,898/67/50, prelude gone); §2.2 + §11.1 gain the new registration- pipeline gate and record that the manifest-reparse gate was tightened, not deleted; §6.1.1 gains host_api::hosted_mcp and its package_lifecycle coupling; §6.6.3 mcp 2,475 -> 2,709; §6.8.1 gains the registered-package- definition record class; §6.8.2 gains the registration pipeline and the split question it creates; §6.9.4 webui 91 -> 92 routes. CHECKLIST: WS3 mcp row annotated (the four registry DTOs are unchanged as an import list, but their shape grew and a second contracts module joined); WS6 rename row and WS10 path-pattern row annotated with the new gate, whose hardcoded path prefixes fail *silently* under the rename and the family mv. PLAN, families/extensions.md, crates/AGENTS.md: matching notes so the Wave 2 executor and any agent routing hosted-MCP work land in the right place. Every amended claim was verified against post-#6930 main with file:line evidence; "still accurate" findings are recorded in the PR body, not the docs. No code or test touched. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…S1.4 row The WS1.4 ruling on package_lifecycle survived the parent's #6930 reconciliation; the LifecyclePackageId split is the recorded resolution, now stated as what happened rather than what was recommended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adapter half (WS1.4) (#6980) * refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1) `ironclaw_host_api::turn` becomes the complete canonical turn vocabulary: it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason` gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three `ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted — `src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/` module, whose `fakes.rs` moves beside the traits it implements in `host_api::product_adapter::test_support`. `ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a host_api type that collided with the unrelated `ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus turns' bounded `gate:`-prefixed routing string). The alias is retired rather than relocated, so the workspace now has exactly one `GateRef`. The six vocabulary-only consumers — auth, event_streams, outbound, telegram_extension, triggers, event_projections — import from `ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not waived but obsolete: the edges no longer exist. The §11.2.2 ratchet baseline moves 20 → 15. No behavior change. `RunOriginAdapter`'s validation error becomes `Result<_, String>` (matching every other bounded ref in `host_api::turn`) with a byte-identical message pinned by a test, so both production `e.to_string()` call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(target-architecture): tick WS1.1 and close PLAN decision round #1 WS1.1's box is ticked with what the change actually landed, including the three lead-sheet corrections it turned up: the row named `TurnStatus` but not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely needed), `GateKind`/`BlockedReason` could not be left behind without duplicating the single `GateKind -> TurnStatus` match table, and deleting `ids.rs` forced retiring its `GateRef` alias rather than relocating it. Two decisions confirmed outside the doc and never recorded: - Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the owner, recorded retroactively; it was made in practice at program start. - The `tools/` row's `default-members` trim — resolved as no trim. Also surfaces #6963 on the WS0 blocking-prerequisite row's first line (it was already cited mid-paragraph) and records the §11.2.2 exception ratchet moving 20 -> 15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract CodeRabbit review round on #6967. Import repoints (accepted): every `use` line this PR already rewrote now names `ironclaw_host_api::turn` directly instead of routing through `ironclaw_turns`' prelude — 57 files across extension_host, product, composition, runner, loop_host, conversations, the integration harness, and the stress tool, plus three inside `ironclaw_turns` itself so the crate stops consuming its own facade. Import lines this PR did not touch are left for their consumer's own repoint slot. TurnGateRef contract pinned (refutation): two review comments claimed `TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty, <= 256 bytes, no control characters); `LoopGateRef` is the prefix- validated family via `loop_ref!(.., "gate:")`. The misreading traces to this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing string"), which stated a minting convention as if it were validation. That wording is corrected and the distinction is now pinned by a test. Also: drop a stale cross-file line reference in a product test comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(target-architecture): remove the row-97 self-contradiction The `tools/` row resolved the `default-members` trim as "no trim" but kept a trailing "The `tools/`/`default-members` half is still open." from before that decision, so the row asserted both states. Drop the stale sentence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(coverage): complete the WS1.1 changed-coverage exemptions Finishes the remediation deferred last round, now that the settled run (job 91246493989) provides authoritative line numbers. Manifest-only — no .rs file changes, so the changed-line set the gate computes is unchanged and these numbers stay valid for the next run. Derived, not transcribed: the gate was replayed locally against its own merged lcov from that run, reproducing CI's failure byte-identically first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after each entry. Final local result: 100.00% (138/138), branch 100% (4/4), exit 0. All 45 gate self-tests pass. Two classes, both verified rather than asserted: - 13 files x 20 lines - declaration lines (fn params, return types, struct fields) whose only edit is the type NAME: GateRef -> TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X. Declarations are not executable, so these files contribute a zero denominator and trip the fail-closed empty_denominator branch. - 7 lines x 3 files - executable, instrumented, and genuinely not exercised by the integration tier. Each checked against the base merged lcov (main @ 67088a4, the PR's own base sha): identical 0 hits before and after, so no coverage was lost. approval_prompt_ context_view is uncovered across its whole signature at base (lines 505-511); the background spawn-mode arm and the invalid-gate- ref error path likewise. This includes the two entries I refused to guess last round - turn_events.rs (three identical candidate lines by text; the settled run disambiguates it as 510) and await_edge/store.rs (no verbatim twin after the repoint; authoritatively 268-272). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2) Carve the loop tier's neutral contracts out of the turn kernel into a new contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): pin the loop-contract boundary and register the new crate Enforcement, CI registration, and guidance for the WS1.2 extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reborn): split the loop-exit contract's ownership claim across the two crates The claim types moved to ironclaw_loop_contracts with WS1.2; the validator policy and the trusted applier stayed in the turn kernel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(contracts): repoint the three intra-doc links the crate split broke The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate links to them no longer resolve; the TurnRunId link target became redundant when the import repoint fully qualified it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): repoint the loop-exit evidence imports the port rule moved Removing the ironclaw_runner re-export (required by the new port-location scan) left two workspace-root test-support files importing the turn kernel's evidence types through it. They now import from ironclaw_turns::loop_exit directly, which is the single sanctioned path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collapse): reconcile the lock pin and the moved failure-category scan Two artifacts of collapsing onto main: - Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts entry while main's dependency bump moved the workspace to 2.0.19. The auto-merge kept the stale pin because the bump predates the crate, so --locked builds failed. - ironclaw_product's failure-summary test reaches into another crate's source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2 moved that impl to ironclaw_loop_contracts, so the include still resolved and matched nothing. Repointed to follow the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3) Carve the extension tier's neutral contracts out of the host API into a new contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): repoint the exact-test selector WS1.2 moved scripts/reborn-e2e-rust.sh pins exact test names for the deterministic gate. The capability-failure rehydration test moved from ironclaw_turns::run_profile::host::capability to ironclaw_loop_contracts::host::capability, so its selector matched zero tests and the gate failed closed. Swept all 10 pinned selectors in that script (4 lib + 6 integration target); this was the only stale one. Each now resolves to exactly one test, verified by running the selector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collapse): reconcile the new crate's lock pins with main's dep bumps The extension_contracts lock entry was generated before the parent branch collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions that no longer have [[package]] blocks. --locked lanes would have failed to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arch): register the new contracts crate in the two gates that enumerate deps The composition pub-use snapshot still carried product's PreferenceTargetCodec re-export, and the CLI's exact-dependency allowlist did not know the extension tier's contracts crate. Both are enumerating gates, so both failed loudly rather than passing vacuously. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(contracts): record why the extension tier's traits are not sealed The visibility kit's sealed-strategy template exists to close a strategy set; every trait here exists to be implemented outside the crate. State that, so the absence reads as a decision rather than an omission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coverage): drop the exemption WS1.2 made stale The changed-coverage manifest carried a WS1.1 exemption for crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2 moved that file into ironclaw_loop_contracts, so the gate's fail-closed path validator rejected the manifest before reaching its line-level verdict. Deleted rather than repointed: WS1.1 merged, so those lines are baseline on main, and this PR's diff pairs the file as a 99%-similarity rename whose only changed lines are imports. Repointing would re-exempt lines the gate no longer flags. All 19 remaining entries verified to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py, added on main after the last merge-down; the WebUI-smoke and E2E roll-up reds were purely the missing file. Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py against this PR's own merged lcov artifact until it exits 0 (100% line 244/244, 100% branch 8/8) - never estimated. Three classes: - type-path repoints on declaration/expression fragments; - verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution: those files are instrumented in this lcov and partially hit (loop_exit 195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is measured. The same bodies were equally unexercised by the integration tier before the move, when they sat in ironclaw_turns and simply were not changed lines; - one crate-root inner attribute the uninstrumentable-line classifier does not recognise on a declaration-only facade. Also adds the #6524 declaration-only facade entry for the new crate's lib.rs to the informational per-crate coverage summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4) Carve the product tier's neutral contracts out of `ironclaw_host_api` into `crates/ironclaw_product_contracts`, and — in the same change, because it is what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress` into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them. `host_api::product_adapter` is one connected component: `channel_adapter` names `inbound::ProductTriggerReason`, `inbound` names both `channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`, `projection` names inbound and outbound, `interaction_commands` names inbound, and `product_surface` names all of them. Since `ironclaw_host_api` may hold no internal dependency, the adapters could only leave once nothing that stays behind names them — so the product DTO modules moved with them. `git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move. Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2 real gates + 5 self-tests with positive and negative fixtures) pins one home and one import path for the product tier's ports; it fails on the four re-export chains this change deletes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row The WS1.4 ruling on package_lifecycle survived the parent's #6930 reconciliation; the LifecyclePackageId split is the recorded resolution, now stated as what happened rather than what was recommended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(contracts): cover the extracted auth-prompt and lifecycle-id surface The changed-coverage gate on #6980 flagged 134 uncovered lines and 22 uncovered branch arms, all in the two modules WS1.4 created. That was a real regression, not an attribution artifact: the code moved out of host_api::product_adapter::outbound and host_api::package_lifecycle, and the owning crates' unit suites did not move with it. Fourteen tests close every one of those lines: render_channel_auth_prompt in both the DM and mention shapes and with/without a pairing deep link, the AuthPromptContextView constructors and wire round-trip, each nested validator arm rejecting independently, and the bounded-id accessor and rejection surface. Verified by replaying reborn_changed_coverage.py against the PR's own merged lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head: 134 -> 0. Three sites remain exempted with per-site evidence, all unreachable by test: a declaration-only crate facade's inner attribute, and two pre-existing error arms whose only change is a repointed type path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(contracts): cover the newline/tab carve-out in bounded prompt text The changed-coverage gate reached 100% line but left three branch arms on validate_bounded_text's control-character predicate. Newline and tab are deliberately legal in prompt copy — channels render multi-line instructions — and every other control character is a rejection; both halves are now driven. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(coverage): exempt the four type-position lines the gate cannot instrument With the auth-prompt and lifecycle-id holes tested, the gate reached 100% line and 100% branch but still failed on four files 'contributing no instrumented lines'. Each is a single type-position line — an enum variant field, two parameter types, a struct field — whose only change is the repointed path for a moved contract, wrapped onto its own line by rustfmt. LLVM emits no coverage region for a type annotation, so no test can reach them. Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38 branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ss grants (WS1.5) (#6981) * refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1) `ironclaw_host_api::turn` becomes the complete canonical turn vocabulary: it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason` gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three `ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted — `src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/` module, whose `fakes.rs` moves beside the traits it implements in `host_api::product_adapter::test_support`. `ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a host_api type that collided with the unrelated `ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus turns' bounded `gate:`-prefixed routing string). The alias is retired rather than relocated, so the workspace now has exactly one `GateRef`. The six vocabulary-only consumers — auth, event_streams, outbound, telegram_extension, triggers, event_projections — import from `ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not waived but obsolete: the edges no longer exist. The §11.2.2 ratchet baseline moves 20 → 15. No behavior change. `RunOriginAdapter`'s validation error becomes `Result<_, String>` (matching every other bounded ref in `host_api::turn`) with a byte-identical message pinned by a test, so both production `e.to_string()` call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(target-architecture): tick WS1.1 and close PLAN decision round #1 WS1.1's box is ticked with what the change actually landed, including the three lead-sheet corrections it turned up: the row named `TurnStatus` but not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely needed), `GateKind`/`BlockedReason` could not be left behind without duplicating the single `GateKind -> TurnStatus` match table, and deleting `ids.rs` forced retiring its `GateRef` alias rather than relocating it. Two decisions confirmed outside the doc and never recorded: - Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the owner, recorded retroactively; it was made in practice at program start. - The `tools/` row's `default-members` trim — resolved as no trim. Also surfaces #6963 on the WS0 blocking-prerequisite row's first line (it was already cited mid-paragraph) and records the §11.2.2 exception ratchet moving 20 -> 15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract CodeRabbit review round on #6967. Import repoints (accepted): every `use` line this PR already rewrote now names `ironclaw_host_api::turn` directly instead of routing through `ironclaw_turns`' prelude — 57 files across extension_host, product, composition, runner, loop_host, conversations, the integration harness, and the stress tool, plus three inside `ironclaw_turns` itself so the crate stops consuming its own facade. Import lines this PR did not touch are left for their consumer's own repoint slot. TurnGateRef contract pinned (refutation): two review comments claimed `TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty, <= 256 bytes, no control characters); `LoopGateRef` is the prefix- validated family via `loop_ref!(.., "gate:")`. The misreading traces to this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing string"), which stated a minting convention as if it were validation. That wording is corrected and the distinction is now pinned by a test. Also: drop a stale cross-file line reference in a product test comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(target-architecture): remove the row-97 self-contradiction The `tools/` row resolved the `default-members` trim as "no trim" but kept a trailing "The `tools/`/`default-members` half is still open." from before that decision, so the row asserted both states. Drop the stale sentence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(coverage): complete the WS1.1 changed-coverage exemptions Finishes the remediation deferred last round, now that the settled run (job 91246493989) provides authoritative line numbers. Manifest-only — no .rs file changes, so the changed-line set the gate computes is unchanged and these numbers stay valid for the next run. Derived, not transcribed: the gate was replayed locally against its own merged lcov from that run, reproducing CI's failure byte-identically first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after each entry. Final local result: 100.00% (138/138), branch 100% (4/4), exit 0. All 45 gate self-tests pass. Two classes, both verified rather than asserted: - 13 files x 20 lines - declaration lines (fn params, return types, struct fields) whose only edit is the type NAME: GateRef -> TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X. Declarations are not executable, so these files contribute a zero denominator and trip the fail-closed empty_denominator branch. - 7 lines x 3 files - executable, instrumented, and genuinely not exercised by the integration tier. Each checked against the base merged lcov (main @ 67088a4, the PR's own base sha): identical 0 hits before and after, so no coverage was lost. approval_prompt_ context_view is uncovered across its whole signature at base (lines 505-511); the background spawn-mode arm and the invalid-gate- ref error path likewise. This includes the two entries I refused to guess last round - turn_events.rs (three identical candidate lines by text; the settled run disambiguates it as 510) and await_edge/store.rs (no verbatim twin after the repoint; authoritatively 268-272). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2) Carve the loop tier's neutral contracts out of the turn kernel into a new contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): pin the loop-contract boundary and register the new crate Enforcement, CI registration, and guidance for the WS1.2 extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reborn): split the loop-exit contract's ownership claim across the two crates The claim types moved to ironclaw_loop_contracts with WS1.2; the validator policy and the trusted applier stayed in the turn kernel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(contracts): repoint the three intra-doc links the crate split broke The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate links to them no longer resolve; the TurnRunId link target became redundant when the import repoint fully qualified it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): repoint the loop-exit evidence imports the port rule moved Removing the ironclaw_runner re-export (required by the new port-location scan) left two workspace-root test-support files importing the turn kernel's evidence types through it. They now import from ironclaw_turns::loop_exit directly, which is the single sanctioned path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collapse): reconcile the lock pin and the moved failure-category scan Two artifacts of collapsing onto main: - Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts entry while main's dependency bump moved the workspace to 2.0.19. The auto-merge kept the stale pin because the bump predates the crate, so --locked builds failed. - ironclaw_product's failure-summary test reaches into another crate's source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2 moved that impl to ironclaw_loop_contracts, so the include still resolved and matched nothing. Repointed to follow the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3) Carve the extension tier's neutral contracts out of the host API into a new contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): repoint the exact-test selector WS1.2 moved scripts/reborn-e2e-rust.sh pins exact test names for the deterministic gate. The capability-failure rehydration test moved from ironclaw_turns::run_profile::host::capability to ironclaw_loop_contracts::host::capability, so its selector matched zero tests and the gate failed closed. Swept all 10 pinned selectors in that script (4 lib + 6 integration target); this was the only stale one. Each now resolves to exactly one test, verified by running the selector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collapse): reconcile the new crate's lock pins with main's dep bumps The extension_contracts lock entry was generated before the parent branch collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions that no longer have [[package]] blocks. --locked lanes would have failed to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arch): register the new contracts crate in the two gates that enumerate deps The composition pub-use snapshot still carried product's PreferenceTargetCodec re-export, and the CLI's exact-dependency allowlist did not know the extension tier's contracts crate. Both are enumerating gates, so both failed loudly rather than passing vacuously. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(contracts): record why the extension tier's traits are not sealed The visibility kit's sealed-strategy template exists to close a strategy set; every trait here exists to be implemented outside the crate. State that, so the absence reads as a decision rather than an omission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coverage): drop the exemption WS1.2 made stale The changed-coverage manifest carried a WS1.1 exemption for crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2 moved that file into ironclaw_loop_contracts, so the gate's fail-closed path validator rejected the manifest before reaching its line-level verdict. Deleted rather than repointed: WS1.1 merged, so those lines are baseline on main, and this PR's diff pairs the file as a 99%-similarity rename whose only changed lines are imports. Repointing would re-exempt lines the gate no longer flags. All 19 remaining entries verified to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py, added on main after the last merge-down; the WebUI-smoke and E2E roll-up reds were purely the missing file. Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py against this PR's own merged lcov artifact until it exits 0 (100% line 244/244, 100% branch 8/8) - never estimated. Three classes: - type-path repoints on declaration/expression fragments; - verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution: those files are instrumented in this lcov and partially hit (loop_exit 195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is measured. The same bodies were equally unexercised by the integration tier before the move, when they sat in ironclaw_turns and simply were not changed lines; - one crate-root inner attribute the uninstrumentable-line classifier does not recognise on a declaration-only facade. Also adds the #6524 declaration-only facade entry for the new crate's lib.rs to the informational per-crate coverage summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4) Carve the product tier's neutral contracts out of `ironclaw_host_api` into `crates/ironclaw_product_contracts`, and — in the same change, because it is what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress` into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them. `host_api::product_adapter` is one connected component: `channel_adapter` names `inbound::ProductTriggerReason`, `inbound` names both `channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`, `projection` names inbound and outbound, `interaction_commands` names inbound, and `product_surface` names all of them. Since `ironclaw_host_api` may hold no internal dependency, the adapters could only leave once nothing that stays behind names them — so the product DTO modules moved with them. `git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move. Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2 real gates + 5 self-tests with positive and negative fixtures) pins one home and one import path for the product tier's ports; it fails on the four re-export chains this change deletes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row The WS1.4 ruling on package_lifecycle survived the parent's #6930 reconciliation; the LifecyclePackageId split is the recorded resolution, now stated as what happened rather than what was recommended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): consolidate sealed evidence minting behind witness grants (WS1.5) CHECKLIST WS1's evidence-mint row: one sealed seam owns construction of protocol-auth evidence, every other construction path is deleted, and the refute-tests land with it. The `host-auth-mint` cargo feature was not a seal. Cargo unifies features across the packages selected in one invocation, so `ironclaw_webui`'s opt-in (-> turns -> host_api) compiled `ironclaw_host_api` once with the gate ON for every other crate in the same build. Measured before touching anything: a probe in `ironclaw_agent_loop` — whose manifest names `ironclaw_host_api` with no features — minted a verified bearer claim; it failed to compile alone and passed as soon as `ironclaw_webui` joined the `cargo test`. Every workspace build is the second case. Replaced with the repo's existing witness-token idiom (`host_api::authorized`), which no other crate's manifest can switch on: - `HostAuthenticationGrant` <- `HostProtocolAuthenticator`, sole implementor `ironclaw_webui` (module-private `AuthLayerState`, trust stage T1). - `VerifiedInboundGrant` <- `ChannelIngressVerifier`, sole implementor `ironclaw_extension_host` (`VerifiedEvidenceMint`, trust stage T2). - Channel/webhook mint family -> `ironclaw_extension_contracts::verified_inbound` (§6.1.2); bearer/session family stays in `host_api` (§6.1.1). The evidence type does not move; `extension_contracts` reaches the private verified variant through the grant-gated `ProtocolAuthEvidence::seal_verified_inbound`. Closed: the feature in 5 manifests + 1 CI recipe; four re-export chains (`host_api::product_adapter`, `ironclaw_product` root, `ironclaw_product::auth`, and composition's zero-consumer re-export). Enforcement: `reborn_sealed_evidence_mint_ratchet` (10 tests, §11.2.5) plus `host_api/tests/protocol_auth_evidence_seal.rs` (5) and `extension_contracts/tests/verified_inbound_seal.rs` (4). +19 tests, 0 removed, no surviving assertion edited. The production-struct dead-code baseline shrinks 81/282 -> 80/277: the five `dead_code` suppressions in `auth.rs` existed only because the constructors were feature-gated. * test(contracts): cover the extracted auth-prompt and lifecycle-id surface The changed-coverage gate on #6980 flagged 134 uncovered lines and 22 uncovered branch arms, all in the two modules WS1.4 created. That was a real regression, not an attribution artifact: the code moved out of host_api::product_adapter::outbound and host_api::package_lifecycle, and the owning crates' unit suites did not move with it. Fourteen tests close every one of those lines: render_channel_auth_prompt in both the DM and mention shapes and with/without a pairing deep link, the AuthPromptContextView constructors and wire round-trip, each nested validator arm rejecting independently, and the bounded-id accessor and rejection surface. Verified by replaying reborn_changed_coverage.py against the PR's own merged lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head: 134 -> 0. Three sites remain exempted with per-site evidence, all unreachable by test: a declaration-only crate facade's inner attribute, and two pre-existing error arms whose only change is a repointed type path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(contracts): cover the newline/tab carve-out in bounded prompt text The changed-coverage gate reached 100% line but left three branch arms on validate_bounded_text's control-character predicate. Newline and tab are deliberately legal in prompt copy — channels render multi-line instructions — and every other control character is a rejection; both halves are now driven. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(coverage): exempt the four type-position lines the gate cannot instrument With the auth-prompt and lifecycle-id holes tested, the gate reached 100% line and 100% branch but still failed on four files 'contributing no instrumented lines'. Each is a single type-position line — an enum variant field, two parameter types, a struct field — whose only change is the repointed path for a moved contract, wrapped onto its own line by rustfmt. LLVM emits no coverage region for a type annotation, so no test can reach them. Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38 branches. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…7) (#6982) * refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1) `ironclaw_host_api::turn` becomes the complete canonical turn vocabulary: it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason` gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three `ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted — `src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/` module, whose `fakes.rs` moves beside the traits it implements in `host_api::product_adapter::test_support`. `ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a host_api type that collided with the unrelated `ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus turns' bounded `gate:`-prefixed routing string). The alias is retired rather than relocated, so the workspace now has exactly one `GateRef`. The six vocabulary-only consumers — auth, event_streams, outbound, telegram_extension, triggers, event_projections — import from `ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not waived but obsolete: the edges no longer exist. The §11.2.2 ratchet baseline moves 20 → 15. No behavior change. `RunOriginAdapter`'s validation error becomes `Result<_, String>` (matching every other bounded ref in `host_api::turn`) with a byte-identical message pinned by a test, so both production `e.to_string()` call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(target-architecture): tick WS1.1 and close PLAN decision round #1 WS1.1's box is ticked with what the change actually landed, including the three lead-sheet corrections it turned up: the row named `TurnStatus` but not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely needed), `GateKind`/`BlockedReason` could not be left behind without duplicating the single `GateKind -> TurnStatus` match table, and deleting `ids.rs` forced retiring its `GateRef` alias rather than relocating it. Two decisions confirmed outside the doc and never recorded: - Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the owner, recorded retroactively; it was made in practice at program start. - The `tools/` row's `default-members` trim — resolved as no trim. Also surfaces #6963 on the WS0 blocking-prerequisite row's first line (it was already cited mid-paragraph) and records the §11.2.2 exception ratchet moving 20 -> 15. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract CodeRabbit review round on #6967. Import repoints (accepted): every `use` line this PR already rewrote now names `ironclaw_host_api::turn` directly instead of routing through `ironclaw_turns`' prelude — 57 files across extension_host, product, composition, runner, loop_host, conversations, the integration harness, and the stress tool, plus three inside `ironclaw_turns` itself so the crate stops consuming its own facade. Import lines this PR did not touch are left for their consumer's own repoint slot. TurnGateRef contract pinned (refutation): two review comments claimed `TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty, <= 256 bytes, no control characters); `LoopGateRef` is the prefix- validated family via `loop_ref!(.., "gate:")`. The misreading traces to this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing string"), which stated a minting convention as if it were validation. That wording is corrected and the distinction is now pinned by a test. Also: drop a stale cross-file line reference in a product test comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(target-architecture): remove the row-97 self-contradiction The `tools/` row resolved the `default-members` trim as "no trim" but kept a trailing "The `tools/`/`default-members` half is still open." from before that decision, so the row asserted both states. Drop the stale sentence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(coverage): complete the WS1.1 changed-coverage exemptions Finishes the remediation deferred last round, now that the settled run (job 91246493989) provides authoritative line numbers. Manifest-only — no .rs file changes, so the changed-line set the gate computes is unchanged and these numbers stay valid for the next run. Derived, not transcribed: the gate was replayed locally against its own merged lcov from that run, reproducing CI's failure byte-identically first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after each entry. Final local result: 100.00% (138/138), branch 100% (4/4), exit 0. All 45 gate self-tests pass. Two classes, both verified rather than asserted: - 13 files x 20 lines - declaration lines (fn params, return types, struct fields) whose only edit is the type NAME: GateRef -> TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X. Declarations are not executable, so these files contribute a zero denominator and trip the fail-closed empty_denominator branch. - 7 lines x 3 files - executable, instrumented, and genuinely not exercised by the integration tier. Each checked against the base merged lcov (main @ 67088a4, the PR's own base sha): identical 0 hits before and after, so no coverage was lost. approval_prompt_ context_view is uncovered across its whole signature at base (lines 505-511); the background spawn-mode arm and the invalid-gate- ref error path likewise. This includes the two entries I refused to guess last round - turn_events.rs (three identical candidate lines by text; the settled run disambiguates it as 510) and await_edge/store.rs (no verbatim twin after the repoint; authoritatively 268-272). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2) Carve the loop tier's neutral contracts out of the turn kernel into a new contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): pin the loop-contract boundary and register the new crate Enforcement, CI registration, and guidance for the WS1.2 extraction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reborn): split the loop-exit contract's ownership claim across the two crates The claim types moved to ironclaw_loop_contracts with WS1.2; the validator policy and the trusted applier stayed in the turn kernel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(contracts): repoint the three intra-doc links the crate split broke The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate links to them no longer resolve; the TurnRunId link target became redundant when the import repoint fully qualified it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): repoint the loop-exit evidence imports the port rule moved Removing the ironclaw_runner re-export (required by the new port-location scan) left two workspace-root test-support files importing the turn kernel's evidence types through it. They now import from ironclaw_turns::loop_exit directly, which is the single sanctioned path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collapse): reconcile the lock pin and the moved failure-category scan Two artifacts of collapsing onto main: - Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts entry while main's dependency bump moved the workspace to 2.0.19. The auto-merge kept the stale pin because the bump predates the crate, so --locked builds failed. - ironclaw_product's failure-summary test reaches into another crate's source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2 moved that impl to ironclaw_loop_contracts, so the include still resolved and matched nothing. Repointed to follow the code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3) Carve the extension tier's neutral contracts out of the host API into a new contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): repoint the exact-test selector WS1.2 moved scripts/reborn-e2e-rust.sh pins exact test names for the deterministic gate. The capability-failure rehydration test moved from ironclaw_turns::run_profile::host::capability to ironclaw_loop_contracts::host::capability, so its selector matched zero tests and the gate failed closed. Swept all 10 pinned selectors in that script (4 lib + 6 integration target); this was the only stale one. Each now resolves to exactly one test, verified by running the selector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(collapse): reconcile the new crate's lock pins with main's dep bumps The extension_contracts lock entry was generated before the parent branch collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions that no longer have [[package]] blocks. --locked lanes would have failed to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arch): register the new contracts crate in the two gates that enumerate deps The composition pub-use snapshot still carried product's PreferenceTargetCodec re-export, and the CLI's exact-dependency allowlist did not know the extension tier's contracts crate. Both are enumerating gates, so both failed loudly rather than passing vacuously. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(contracts): record why the extension tier's traits are not sealed The visibility kit's sealed-strategy template exists to close a strategy set; every trait here exists to be implemented outside the crate. State that, so the absence reads as a decision rather than an omission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coverage): drop the exemption WS1.2 made stale The changed-coverage manifest carried a WS1.1 exemption for crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2 moved that file into ironclaw_loop_contracts, so the gate's fail-closed path validator rejected the manifest before reaching its line-level verdict. Deleted rather than repointed: WS1.1 merged, so those lines are baseline on main, and this PR's diff pairs the file as a 99%-similarity rename whose only changed lines are imports. Repointing would re-exempt lines the gate no longer flags. All 19 remaining entries verified to resolve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py, added on main after the last merge-down; the WebUI-smoke and E2E roll-up reds were purely the missing file. Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py against this PR's own merged lcov artifact until it exits 0 (100% line 244/244, 100% branch 8/8) - never estimated. Three classes: - type-path repoints on declaration/expression fragments; - verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution: those files are instrumented in this lcov and partially hit (loop_exit 195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is measured. The same bodies were equally unexercised by the integration tier before the move, when they sat in ironclaw_turns and simply were not changed lines; - one crate-root inner attribute the uninstrumentable-line classifier does not recognise on a declaration-only facade. Also adds the #6524 declaration-only facade entry for the new crate's lib.rs to the informational per-crate coverage summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4) Carve the product tier's neutral contracts out of `ironclaw_host_api` into `crates/ironclaw_product_contracts`, and — in the same change, because it is what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress` into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them. `host_api::product_adapter` is one connected component: `channel_adapter` names `inbound::ProductTriggerReason`, `inbound` names both `channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`, `projection` names inbound and outbound, `interaction_commands` names inbound, and `product_surface` names all of them. Since `ironclaw_host_api` may hold no internal dependency, the adapters could only leave once nothing that stays behind names them — so the product DTO modules moved with them. `git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move. Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2 real gates + 5 self-tests with positive and negative fixtures) pins one home and one import path for the product tier's ports; it fails on the four re-export chains this change deletes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row The WS1.4 ruling on package_lifecycle survived the parent's #6930 reconciliation; the LifecyclePackageId split is the recorded resolution, now stated as what happened rather than what was recommended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(contracts): consolidate sealed evidence minting behind witness grants (WS1.5) CHECKLIST WS1's evidence-mint row: one sealed seam owns construction of protocol-auth evidence, every other construction path is deleted, and the refute-tests land with it. The `host-auth-mint` cargo feature was not a seal. Cargo unifies features across the packages selected in one invocation, so `ironclaw_webui`'s opt-in (-> turns -> host_api) compiled `ironclaw_host_api` once with the gate ON for every other crate in the same build. Measured before touching anything: a probe in `ironclaw_agent_loop` — whose manifest names `ironclaw_host_api` with no features — minted a verified bearer claim; it failed to compile alone and passed as soon as `ironclaw_webui` joined the `cargo test`. Every workspace build is the second case. Replaced with the repo's existing witness-token idiom (`host_api::authorized`), which no other crate's manifest can switch on: - `HostAuthenticationGrant` <- `HostProtocolAuthenticator`, sole implementor `ironclaw_webui` (module-private `AuthLayerState`, trust stage T1). - `VerifiedInboundGrant` <- `ChannelIngressVerifier`, sole implementor `ironclaw_extension_host` (`VerifiedEvidenceMint`, trust stage T2). - Channel/webhook mint family -> `ironclaw_extension_contracts::verified_inbound` (§6.1.2); bearer/session family stays in `host_api` (§6.1.1). The evidence type does not move; `extension_contracts` reaches the private verified variant through the grant-gated `ProtocolAuthEvidence::seal_verified_inbound`. Closed: the feature in 5 manifests + 1 CI recipe; four re-export chains (`host_api::product_adapter`, `ironclaw_product` root, `ironclaw_product::auth`, and composition's zero-consumer re-export). Enforcement: `reborn_sealed_evidence_mint_ratchet` (10 tests, §11.2.5) plus `host_api/tests/protocol_auth_evidence_seal.rs` (5) and `extension_contracts/tests/verified_inbound_seal.rs` (4). +19 tests, 0 removed, no surviving assertion edited. The production-struct dead-code baseline shrinks 81/282 -> 80/277: the five `dead_code` suppressions in `auth.rs` existed only because the constructors were feature-gated. * refactor(common): narrow ironclaw_common to its §6.1.5 contract (WS1.6) `ironclaw_common` now matches PROPOSAL §6.1.5's *Owns* list exactly, plus three named exceptions that a pinned rule blocks from moving. Deleted, not relocated — each re-verified with `git grep` over tracked files on this base: * `event.rs` (1,234 lines). All seven exported symbols have zero consumers outside the crate; the only live workspace references are negative ones (the architecture test asserting the OpenAI-compatible routes must *not* stream `AppEvent`, one doc comment, one Python docstring). This confirms WS1.4's disposition 7 rather than repeating it, and closes the WS8 deletion-candidate row. The `AppEvent` `ForbiddenUse` entry stays as a reintroduction pin with its reason updated to say the enum is gone — the convention the file already applies to the retired v1 `ironclaw::` crate. * `platform.rs`. `PlatformInfo` has zero references anywhere; §6.1.5's "→ its consumer" is unsatisfiable because there is no consumer. * The four budget constants. Zero consumers workspace-wide, and `ironclaw_resources` already governs the same concern with a live, differently-shaped mechanism (`ResourceLimits::max_wall_clock_ms`) that references none of them — so "→ `resources`" would have seeded four unreachable constants beside a working governor. Moved: `automation` → `ironclaw_triggers`, which owns automations and had already defined `MAX_TRIGGER_NAME_BYTES` as an alias of the common constant. This eviction is in §6.1.5 but missing from the CHECKLIST row. `ironclaw_product`'s cross-crate `pub use` of the newtype (a second import path with zero external consumers) is deleted rather than re-sourced, following the WS1.3/WS1.4 dual-path rule. Already done, so recorded rather than redone: `trust_boundary` went with #6943, and the `AttachmentRef` collision is already resolved as `ChannelAttachmentRef`. That rename left a wrong cross-reference in its own doc comment (`ironclaw_common::ChannelAttachmentRef` for `ironclaw_common::AttachmentRef`), fixed here. Not moved, with evidence: `provider_transcript` (its `agent_loop` consumer is contracts-only by pinned rule; `ironclaw_llm` is substrates), `model_selection` (`openai_compat`'s boundary rule forbids `ironclaw_llm` outright, and `llm` never uses the module), and `llm_costs` (`llm` uses 2 of its 7 public items; moving it would hand `ironclaw_product` a reqwest/rig-core cone for a pricing table — the `ModelCostTable` port is the real seam, and that is a WS4 design change). All three are documented in `crates/ironclaw_common/AGENTS.md`, which also stops claiming ownership of the long-deleted `trust_boundary`. Un-masking: `ironclaw_common` 123 → 110 tests (10 `event::tests::*`, each classified to a deleted symbol; 3 `automation::tests::*` reappearing verbatim in `ironclaw_triggers`, 166 → 169). Zero unclassified, no surviving test edited. The extension-specificity gate demanded its now-stale `platform.rs` carve-out be deleted — the property it was built with. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(host-api): move failure-summary data out of the runner (WS1.7) PROPOSAL §6.1.1 assigns the category→summary tables to `host_api::failure` "so product stops depending on runner". They are now `ironclaw_host_api::failure::{categories, summary}`, and `ironclaw_product`'s manifest no longer names `ironclaw_runner` under `[dependencies]` — the dev-dep stays and is documented, because product's harnesses legitimately build a full turn stack. The row calls these "the two single-symbol product edges". Measured on this base, neither is single-symbol: * `product → runner` was two modules — a category constant plus four `failure_summary` items. **Severed.** What could not follow, because `ironclaw_host_api` may hold no internal dependency: the envelope *writer* (typed on `agent_loop`'s `CheckpointKind` and `loop_contracts`' `LoopSafeSummary`) and every classifier. Both runner modules are now private. * `product → loop_host` has **three** production sites, and only the prompt constant was one. `FAILURE_EXPLANATION_SYSTEM_PROMPT` and its asset are now product-owned (prompt *content* is out of charter for the loop tier, §6.1.4/§6.7.2), but `project_create_capability.rs` (the #6691 arrival §2.3 already flags) and — not previously recorded anywhere — `scoped_fs/attachment_landing.rs`, which consumes `LoopAttachmentReadPort`, both carry real behavior. The edge survives; severing it is WS5/WS6 work. One disposition worth review: the envelope reader moved and now revalidates its cause with `SafeSummary::new` rather than `LoopSafeSummary::new`. That is behavior-preserving, and the claim is pinned rather than asserted — `validate_loop_safe_summary` delegates to `SafeSummary::new` with exactly one bypass, the fixed `INPUT_ENCODE_HUMAN_SUMMARY` literal, which independently satisfies the canonical rule (`loop_input_encode_sentinel_needs_no_bypass_here`). Splitting writer from reader also split the checkpoint-stage vocabulary across two crates, so the pre-existing round-trip (which covered only `BeforeModel`) gains a sibling driving all four `CheckpointKind`s writer→reader across the boundary. Neither edge was ever a `LAYER_MATRIX_EXCEPTION` — `products → kernel` and `products → loops` are matrix-legal — so this cannot move the count, which stays at 13. The value is graph narrowing and prompt-content placement. Enumerating gates, all shrink-only: the composition pub-use snapshot loses exactly one line, because the `reborn_failure_summary_for_category` re-export is deleted rather than re-sourced (its sole consumer, the CLI, already depends on `ironclaw_host_api`, so the facade hop bought nothing). The product-side category-coverage `include_str!` is repointed, not added, so the §11.2.7 inventory is unchanged at 19. Also hardens `reborn_loop_port_location_scan`'s directory walk, which excluded only `target` and so descended the whole npm tree on any checkout with the frontend installed — the same defect already fixed in the sibling scanners. Un-masking: 10 table tests moved runner → host_api with identical names and no content edits (runner 467 → 458, host_api 248 → 260; deltas are the 10 moved plus 1 new round-trip and 2 new pins). loop_host and product rosters are byte-identical at 572 and 1032. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(checklist): tick WS1.6, record WS1.7's partial sever, close Wave 1 Ticks the `ironclaw_common` narrowing row with the seven dispositions that differ from how it was written (five of six clauses moved differently: two deletions where the row said relocate, one unsatisfiable "→ its consumer", two clauses already done, one eviction present in §6.1.5 but missing from the row), and the three LLM-data evictions each refuted by a pinned rule. Leaves the WS1.7 row **open** rather than ticking it. Two of three clauses landed — the data move and the `product → runner` sever — but the `product → loop_host` sever did not and cannot here: the edge has three production sites, two of them behavioral, one of which (`attachment_landing.rs`'s `LoopAttachmentReadPort`) was not recorded anywhere before. Ticking it would over-claim. Records the Wave 1 exit state on the WS1 verify row. The milestone's "20 → 12" is not met and **the 12 was wrong**: the true end-state is 13, because the 13th survivor, `conversations → turns`, is turn *admission authority* rather than vocabulary, so no contracts crate can dissolve it and it falls in WS5. The wave's other clauses did land — three contracts crates, `agent_loop` contracts-only with zero exceptions, evidence mint sealed. The mint row's measurement is carried forward as the reusable finding: the `host-auth-mint` cargo feature was never a seal, because feature unification compiled the gate on for every crate in any workspace-wide build. Adds the dated one-line Wave 1 summary to PLAN's Wave 1 section, including the discipline every slot in this wave independently confirmed: re-measure rows against your own base, never inherit a predecessor's count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…the gate crate Three slices of the #6963 class inside crates/ironclaw_architecture. 1. reborn_registration_pipeline_boundary (#6963 comment, arrived with #6930). workspace_root() walked up a fixed two levels, so under a family move the 'root' resolved to crates/, the scan targeted crates/crates, and the gate passed having visited ZERO files. Its two hardcoded hosted_mcp_ prefixes also stopped matching, which would have false-positived against the registration pipeline's own files with baseline 0 blocking the fix. Now inventory-driven, with measured_scan() asserting inventory size, scanned file count, and that every owned scope resolves to at least one real file. Its self-test now exercises is_owned(), flat and nested. 2. reborn_sealed_evidence_mint_ratchet. HostProtocolAuthenticator and ChannelIngressVerifier are unsealed traits whose mint methods are provided, so a bare anywhere confers the power to mint ProtocolAuthEvidence::Verified. The source census IS the enforcement, and it evaded on a multiline impl header, on aliases (plain, braced, and raw-identifier), and across a re-export split over two files. Headers are now extracted and whitespace-collapsed, in-file aliases resolved, matching is identifier-bounded, a re-export guard removes the cross-file shape, and a headers-parsed floor keeps the new normalizer from degrading silently. Closes the #6995 fail-open; seam origin PR #6981. 3. The shared root idiom. 23 of 24 gate files resolved the workspace root by walking up a fixed number of levels. ratchet_support::workspace_root() now searches for the nearest ancestor holding both crates/ and Cargo.toml, and 11 private copies were deleted in its favour. Two gates that went silently green under nesting (reborn_authorized_seal_ratchet — worst under a PARTIAL move, 1309 -> 45 files scanned with no error; reborn_retired_taxonomy — 1492 -> 0) gained measurement assertions. Two vacuous assert!(!path.exists()) absence checks in telegram_extension_gates now require their containing directory to exist. Five stale entries removed, each matching zero files today and therefore behavior-free: crates/ironclaw_gateway/ and extension_host/ extension_installation_store.rs from two SANCTIONED_PATHS allowlists (both now carry stale-entry detection), crates/ironclaw_reborn_api/src and two duplicate crates/ironclaw_product/src entries from the dependency-boundary roots, and the deleted repo-root src/ monolith from the manifest reparse scan. Regression tests: +8 in the family sweep, +7 in the registration boundary, +4 in the sealed-evidence census; every added assertion sabotage-tested red then green. 26 binaries / 146 passed / 0 failed; clippy -D warnings clean. Refs #6963, #6995
Summary
Change Type
Linked Issue
None — implementation follows the reviewed hosted-MCP extension-lifecycle design. Future in-process multi-tenant lifecycle scoping remains separate from this standalone runtime feature.
Verified behavior and root causes
The implementation keeps lifecycle transitions generic. Hosted-MCP-specific work is limited to admission, manifest projection, discovery safety, and preparation; the existing lifecycle service decides install/setup/activation outcomes for every auth type.
Validation
cargo fmt --all -- --checkcargo clippy --all --tests --examples --all-features -- -D warnings— now run and clean. It was not clean before: this PR had renamedExtensionPackage::roottoroot_bindingand added a requiredNewAuthFlow::requester_extensionwithout updating call sites, so roughly a dozen test targets did not compile. None of that is visible tocargo checkor to focused per-crate tests.cargo build—cargo build -p ironclaw --bin ironclawcargo test -p ironclaw_reborn_integration_tests --test reborn_integration_hosted_mcp_registration— 9 passed, 2 explicitly ignored live canariescargo test -p ironclaw_architecture— all pass (three PR-introduced violations fixed; see below)cargo test -p ironclaw_auth— 172 + 30 + 76 passedcargo test -p ironclaw_extension_host --lib— 362 passedscripts/pre-commit-safety.sh— composition within mass + dispatch budgetcargo test -p ironclaw_extension_host discovery_enforces_the_manifest_declared_tool_limitcargo test -p ironclaw_auth metadata_urls_are_well_formedcargo test -p ironclaw_extensions trust_policy_source_uses_the_package_root_bindingcorepack pnpm test— 112 files, 925 testscorepack pnpm typecheckcargo test --features integration— not applicable: the selected Reborn integration binary directly covers this cross-crate behavior without database/Docker features.Test Strategy
User behavior:
Register a hosted MCP endpoint once for the standalone tenant runtime; users discover it but install and configure their own membership/account. Registration auto-detects auth. Setup may remain unfinished or reject bad credentials without publishing tools. Successful no-auth, bearer, or OAuth setup activates the ordinary extension and permits mediated invocation.
Risk areas:
Tests added or updated:
tests/integration/hosted_mcp_registration.rscovers validation rejection, no-auth, bearer missing/wrong/correct, OAuth unfinished/success/rejected-token, restart/restore, shared discovery with per-user install/remove, trust publication, credential injection, and invocation.tests/fixtures/hosted_mcp/microsoft_mrc_streamable_http.jsonreplays a real streamable-HTTP server shape through the lifecycle.tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.pycovers registration and setup UI; final browser verification was performed manually against the rebuilt binary.What the tests prove:
Commands run:
Security Impact
Adds an authenticated registration route for external HTTPS MCP endpoints and new trust-policy publication for activated remote packages. Endpoint admission rejects unsafe URL shapes; OAuth metadata and registration endpoints use bounded mediated egress; credentials remain encrypted/host-side; tool catalogs are bounded and safety-scanned; trust is pinned to package ID, canonical endpoint, manifest digest, and declared effects. Registration alone grants no execution authority.
Reborn Trust-Boundary Checklist
serde(default)fields fail closed or have migration tests: legacy manifest/root rows and new definition wire shapes have compatibility tests.UserRegisteredis provenance, not authority.Database Impact
None. No SQL migration or backend schema change. Durable extension definitions and lifecycle state use the existing filesystem-backed extension store and compatibility encoding.
Blast Radius
Extension manifest/persistence, lifecycle product surface, product-auth OAuth admission and account setup, MCP discovery/invocation, host trust evaluation, composition wiring, WebUI extension routes, and the extensions SPA. Existing first-party extension setup paths continue through the same lifecycle/auth services.
Rollback Plan
Revert this PR. Existing bundled/registry extension lifecycle and first-party auth paths remain intact; registered hosted-MCP definitions are additive persisted records and become unreachable if the registration surface is removed. No database downgrade is required.
Review Follow-Through
An earlier revision of this description said local review had converged with no
unresolved findings. That was not accurate. A full-scope local review (149 files,
three lanes) was re-run against the current head and found real defects, listed
below with what was done about them.
Production defects found and fixed (each with a regression test that fails before its fix)
registerheldoperation_lockwhile awaitingcatalog.write(), inverting the catalog-before-operation orderimport_bundle/installrely onpreparation_ready = truePreparationRequirement::Requiredreported visible placeholder capabilities and an installed response even when discovery failedrestore_extension_lifecycle_stateskipped pending installs before registering them with the lifecycle serviceowner_extension, then dropped that identity before building the refresh requestCrossScopeDeniedbefore any vendor call — hosted MCP OAuth tokens would silently never refreshhosted_mcp_preparationwere verbatim copies ofExtensionLifecycleManagermethodsThe first regression test written for the lock-order fix passed against the bug —
timing-based, the tasks never interleaved. It was replaced with a deterministic
crate-tier test that pauses
registerbetween its two lock acquisitions, and itfails (times out) against the old order.
Build and architecture debt this PR introduced, now repaired
ExtensionPackage::root→root_binding,NewAuthFlow::requester_extension) were not propagated; ~12 test targets didnot compile.
Default, items aftermod tests, and an 8-argument constructor aggregatedinto a dependencies struct rather than silenced with
#[allow].HostedMcpEndpointwas defined twice for two different concepts; thecrate-local egress matcher is now
HostedMcpEgressEndpoint.(this PR removed those reparses), tightening the gate.
#[cfg]-gated test-support field had been added to a production struct incomposition; it was dead — the PR's own refactor had moved that egress onto
HostedMcpPreparationDependencies, and the tests use the existingtry_with_host_http_egressseam. Removed rather than re-baselined.No test was weakened, deleted, or re-baselined to reach green.
Known gaps, stated rather than closed
registered-definitions/{id}.jsonis durable, but nothing enumerates it atboot, so the rebuilt catalog omits it until the user re-registers.
Re-registration is idempotent (the admission CAS returns
ExactExistingfor abyte-identical record), so the recovery is to register again. Deliberately
deferred: closing it needs a new enumeration method on
ExtensionInstallationStorePortand every implementation.crates/ironclaw_extensions/src/installations.rsis 5487 lines and itsarch-exempt: large_filejustifications predate this PR's addition; the newCAS-admission logic arguably belongs in its own module.
setup-success path, so that branch had never executed. Fixed; the scenario
still needs a live run to count as evidence.
Review track: C