Skip to content

refactor(mcp,auth,webui): the remaining three WS6 module charters - #7179

Closed
BenKurrek wants to merge 104 commits into
mainfrom
ws6/charters-remainder
Closed

BenKurrek wants to merge 104 commits into
mainfrom
ws6/charters-remainder

Conversation

@BenKurrek

@BenKurrek BenKurrek commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

Closes the WS6 module-charters row. The llm sub-owner map landed separately (#7139); this PR is the remaining three clauses.

Clause Section Shape
mcp single-file split §6.6.3 Real code move — 2,767-line lib.rs → 7 chartered modules
auth two-engine split §6.4.8 Charter — the two modules already existed and were already severed
webui handlers.rs charter map §6.9.4 Charter — 19 sub-owners over a file that stays one file

Each ships an enforcing test, and each test is sabotage-proved. Two of the three clauses moved zero production lines — stated plainly rather than dressed up as refactors.


1. mcp — 2,767 lines → seven chartered modules (§6.6.3)

§6.6.3 records crates/ironclaw_mcp/src/lib.rs at 2,709 lines, measured 2026-07-31. It was 2,767 — a third recorded value for one file, which is the argument for splitting it rather than measuring it again.

Module Owns Lines
contract The vocabulary a caller names: config, DTOs, McpClient/McpExecutor, the McpError/McpClientError taxonomy 545
runtime Reserve → call → reconcile/release, descriptor admission, the manifest credential context 505
client The Streamable-HTTP McpClient: handshake, per-invocation session lifecycle, the tools/list paging loop 452
jsonrpc The JSON-RPC 2.0 codec and response hygiene: framing, id matching, session-id/protocol-version validation, auth challenge, per-method credential routing 658
discovery tools/list catalog admission: ceilings, per-tool classification, schema bounds, tool-name grammar 447
egress The McpHostHttp port and the host-owned egress plan/planner 213
diagnostics Every stable, bounded failure token the lane surfaces 209

Two charter rules are load-bearing rather than decorative — the code already depended on both, and neither was checkable while it was one file:

  • No module builds a failure string of its own. Every reason comes from diagnostics' three cause enums, so the model-visible token set stays enumerable in one file. diagnostics is now the only module with no crate-internal dependency, which is what makes that verifiable.
  • discovery owns the catalog rules, client owns the paging loop. The three ceilings live in discovery and the loop reads them — the drift-proofing MAX_DISCOVERED_MCP_TOOLS' own doc comment already claimed but could not enforce with both enforcement points in one file.

No API change. Submodules are private and lib.rs re-exports without globs, so ironclaw_mcp::X remains the single import path. Zero files changed outside the crate except one architecture test and two docs.

The waiver is deleted, not carried forward. lib.rs:1 carried // arch-exempt: large_file, … plan #4088 — the split this is. Largest file is now 658 lines against the 1,500-line ARCH-SPRAWL threshold scripts/pre-commit-safety.sh enforces with exit 1.

⚠ A gate would have gone silently green

reborn_dependency_boundaries.rs:1124 read crates/ironclaw_mcp/src/lib.rs as one string and scanned it for forbidden dispatcher-composition surface. After the split that file is 61 lines of pub use, so the scan would have found nothing and passed for the wrong reason. Repointed to the whole src/ tree with a non-vacuity assertion — the identical shape the ironclaw_sandbox lane three lines above already carries, from WS3 hitting this exact trap.

Two lanes, two hits. Any gate that names a single lib.rs is a landmine for the crate it guards, and the survivors should be swept before the family git mv, not after.

Two placement calls (delegated authority)

McpAuthContext and PreparedMcpClientRequest are not in contract despite being vocabulary by shape: both are constructed and consumed entirely inside runtime and name no public type, so contract would have become the owner of the runtime's private plumbing. requires_host_http_egress is in egress, not contract, because it is a transport predicate consumed by both client and runtime — charging it to either would have made one depend on the other.

Roster proof

Check Result
Top-level item roster, name+kind 105 → 105, zero added, zero removed
Items declared pub 21 → 21 (public surface unchanged)
Visibility widenings 28 priv → pub(crate); 0 priv → pub
Unfiltered test --list 75 → 75; leaf-name diff empty

Test paths moved from tests::<name> to <owner>::tests::<name>; leaf names are byte-identical and were diffed as such. The 32 lib tests bucket to the owner they exercise (discovery 15, jsonrpc 13, diagnostics 2, client 1, runtime 1). No test helper crossed an owner, so no shared test-support module was needed.


2. auth — charter the two engines, enforce the severance (§6.4.8)

§6.4.8 asks for the "internal two-engine split (engine vs product_auth)" to become "two chartered top-level modules". Measured: both modules already exist as top-level modules, and neither names the other — zero references in both directions. The split was never structural. What was missing is the charter, and a severance nobody checks is an observation that lapses on the next PR.

⚠ Two owners were not enough, measured

Charting only the two engines leaves the crate's 11 shared top-level modules unowned. Counted symbol-by-symbol — the right instrument, because both engines import through the crate root's flat pub use list, so counting crate::<module>:: paths reads zero and is silently wrong:

  • 6 of 11 are named by both engines — credential, provider, oauth, scope, ids, error. Charging them to either engine would make one engine the owner of the other's dependencies.
  • 4 are product_auth-only (cleanup, domain, flow, interaction); 1 is engine-only (account_state).

So the map has four owners, not two: engine, product-auth, vocabulary, test-support. This is the same refutation §6.4.13's five-sub-owner claim met in the llm map, arriving independently on a different crate — worth noting for the remaining charter rows.

Three placement calls (delegated authority)

  • account_state.rs → engine, not vocabulary, despite sitting at the crate root: AuthAccountState is named by engine/ and by zero files in product_auth/, and engine/mod.rs's doc already claimed the state machine.
  • cleanup.rs/domain.rs/flow.rs/interaction.rs → product-auth on the same measured test. They are the four files a later slice could physically git mv; the map says so and names the blocker — domain.rs needs a rename first, because product_auth/durable/domain.rs already holds that name.
  • credential.rs is the one genuinely two-owner file (18 of 25 symbols product_auth-only, 6 named by both, including CredentialAccountService and ProviderBackedCredentialAccountService, which engine/keepalive.rs drives for the refresh sweep). Charged to vocabulary — the shared half is what makes it un-movable — with the service split recorded as owed work. The gemini_oauth.rs precedent from the llm map.

Three other §6.4.8 clauses were already discharged

Struck in the docs rather than left to be re-attempted by a future agent:

  • loopback_oauth + its urlencoding dep — neither is in the tree. Both CLAUDE.md and AGENTS.md still described it as a live "temporary exception"; corrected.
  • fakes.rs behind test-support — lib.rs:21-22 already carries #[cfg(any(test, feature = "test-support"))].
  • Drop the turns dep — ironclaw_turns appears nowhere in crates/ironclaw_auth/Cargo.toml.

Proof — charter, not move

No production code moved, so the roster is 609 → 609 byte-identical, visibility included — zero widenings, the strongest form of the move-only property rather than a weaker one. Test list 288 → 291, the +3 being exactly the new gate; no pre-existing test renamed, moved, or removed.


3. webui — the handlers.rs charter map (§6.9.4)

§6.9.4 contained no charter-map clause at all. The CHECKLIST row names one, so the definition had to be reconstructed from §6.9.1 ("module-charter map … the audited ≥11 sub-owners") and §6.4.15 ("module-charter work, not a split"). §6.9.4 now carries the clause so the next reader does not reconstruct it a third time.

src/webui_v2/handlers.rs is 4,593 lines and gains a 19-sub-owner map in CLAUDE.md: session, threads, admin-users, workspace-fs, projects, attachments, streaming, runs, commands, automations, traces, outbound, skills, extensions, admin-config, dispatch, operator, llm-admin, run-artifact.

The waiver stays, and a test now says so

the_large_file_waiver_survives_the_charter_map fails if // arch-exempt: large_file is deleted or if it stops naming plan #5985 — the plan number is the only thing that makes the waiver revocable. This is the opposite disposition to §6.4.14's contribution.rs waiver, which was deleted with the traces split; the difference is that this plan has not landed. Without the test, the natural next move for someone reading a charter map is to delete the waiver as "handled", silently dropping the file out of ARCH-SPRAWL tracking.

⚠ Owners are conceptual, not positional — forced by the row's own constraint

The obvious mechanism for a single file is banner-delimited regions, one per owner. It is unbuildable here without moving code: threads holds two regions (create_thread/delete_thread at :265-303 and send_message/get_timeline at :591-654), split by the admin-users block. Making them contiguous is exactly the movement §6.4.15 forbids for this row. The gate is therefore item-granular — every top-level item maps to exactly one owner, positions irrelevant. Recorded because the next reader will reach for banners first.

Three placement calls (delegated authority)

  • The *_activity_id family splits three ways. product_capability_activity_id and product_surface_activity_id are dispatch (the generic derivation); extension_lifecycle_/llm_provider_upsert_/outbound_preferences_/admin_configuration_activity_id go to the concern whose request fields each one reads.
  • capability_failure_http_class is outbound, not dispatch, despite the generic name — every caller is in that owner. The promotion trigger is stated in advance (a second concern calling it moves it to dispatch) rather than argued later.
  • get_attachment is attachments, not workspace-fs. Both serve bytes, but attachment identity is a thread-scoped ref rather than a mount path. Keeping them apart is what stops a future path-scoping fix from being assumed to cover attachment downloads.

Proof — map, not move

git diff --stat <base> -- crates/ironclaw_webui/src is empty: zero source lines changed, so the item roster is identical by construction rather than by comparison. Coverage is 219 of 219 top-level items in handlers.rs plus the 5 in handlers/run_artifact.rs; zero uncharted, zero phantom, zero double-claimed.

What this does not do: it is not plan #5985 and does not shrink the file by a line. What it buys is that #5985 inherits a decided seam list — each of the 19 rows is one candidate module — instead of re-litigating boundaries when the split is attempted.


Verification

Check Result
cargo test -p ironclaw_mcp -p ironclaw_auth --all-features 366 passed / 0 failed
cargo test -p ironclaw_webui --all-features 469 passed / 0 failed
cargo test -p ironclaw_architecture --all-features 259 passed / 0 failed
cargo test -p ironclaw_host_runtime (mcp consumer) 1097 passed / 0 failed
cargo test -p ironclaw_extension_host (mcp consumer) 386 passed / 0 failed
cargo check --workspace --all-features clean
cargo clippy -p ironclaw_mcp -p ironclaw_auth -p ironclaw_webui -p ironclaw_architecture --all-features --all-targets -- -D warnings clean
cargo fmt --check clean

⚠ What CI on this PR does and does not cover

All 10 checks pass and the PR is MERGEABLE / CLEAN — but that is a weaker statement than it looks on a batch-stacked PR, so it is worth stating precisely rather than letting "CI green" stand in for it:

  • Tests (Reborn) did not run. Its pull_request trigger is filtered to main, release-fix-* and release/* (.github/workflows/reborn-tests.yml), and this PR targets ws/waves-0-4-batch. The three new gates were therefore not executed by CI here; they execute when the batch branch is PR'd to main.
  • What did run covers compilation of them: Code Style has no branch filter and runs cargo clippy --all --tests --examples -- -D warnings, so every new test file compiles and lints in CI, plus Clippy (all-features), Regression test enforcement, and the JS lint lane.
  • Execution evidence is local, in the table above — 366 + 469 + 259 + 1484 tests, all suites unfiltered.

This is a property of the stacked-batch workflow rather than anything this PR introduced, and it applies equally to the sibling charter PRs already merged into this batch. Flagging it because "CI green" on a ws/*-targeted PR means fmt + clippy, not tests.

Every gate is sabotage-proved, each restored green

auth (5 directions): drop a file from the map → "1 source file(s) have no sub-owner"; phantom path → "no longer exists"; double claim → "claimed by more than one"; use crate::product_auth::… inside engine/ → severance failure naming the probe; use crate::engine::… inside product_auth/ → the mirror.

webui (5 directions): the same three map mutations, plus deleting the large_file waiver → the waiver test, plus adding a brand-new uncharted handler to the file → "no sub-owner" (the real-world case).

Both gates also self-guard against going vacuous: they fail if the table parses to zero rows, if the source walk finds implausibly few items, if an engine directory is missing, or (webui) if zero submodule items are collected — which would leave the run-artifact row unchecked. The auth severance scan strips comment lines, because both charters deliberately name the other engine in prose and a scan counting those would be unsatisfiable by construction.

An an_aligned_separator_row_is_not_parsed_as_data fixture in each gate pins a parser trap the checked-in tables do not currently trigger: matching the markdown separator with starts_with("---") lets |:---| through as a data row, which sets the saw-a-row flag and leaves the zero-rows guard quiet.


Docs

Both target-architecture docs carry the findings, per the docs-truth rule — every measurement, refutation, and already-discharged clause lands in PROPOSAL.md §6.6.3 / §6.4.8 / §6.9.4 and the CHECKLIST.md row, not only here. The CHECKLIST module-charters row is now closed.

One commit that is not a charter clause

f2093b342 repairs a clippy break this stack inherited from its base, not one it introduced: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE's doc block accumulated four dated recount notes across the Waves 0-4 batch merges, and one join left a bare blank line between two /// runs on the same constant — empty_line_after_doc_comments, one error. Fix is a single character (blank line → ///), no assertion or baseline touched. It is in this PR because cargo clippy -p ironclaw_architecture -- -D warnings had to be clean before the new gates could be trusted to fail for their own reasons.

Coordination

The ChannelAuthAccountState family a sibling is relocating is declared in ironclaw_product (reborn_services.rs:677), not in ironclaw_auth — no file collision. If that relocation lands a new file under crates/ironclaw_auth/src/, the coverage gate fails until it is given a row. By design, and the failure message states the rule to apply.

Base is ws/waves-0-4-batch at 89080c516 (kept deliberately; batch-2 reconciles the newer tip).

🤖 Generated with Claude Code

BenKurrek and others added 30 commits August 3, 2026 09:38
…contract (WS3)

Deletes the two `-> ironclaw_extensions` layer-matrix exceptions
(`ironclaw_mcp`, `ironclaw_scripts`) by giving the runtimes-layer lanes a
contracts home for the descriptors they read, instead of the registry crate
they may not depend on. Exceptions 13 -> 11; baseline lowered in the same
change.

Moved to `ironclaw_extension_contracts`:
- `runtime::{ExtensionRuntime, ExtensionAssetPath, ExtensionAssetPathError}`
- `hosted_mcp::{HostedMcpDiscoveredTool, HostedMcpDiscoveredToolAnnotations}`

`ExtensionPackage`/`ExtensionManifest` deliberately stay in
`ironclaw_extensions`: they carry the whole parsed manifest tree and a
`PackageRootBinding` typed on `ironclaw_filesystem::VirtualPath`, which the
§11.2.3 contracts-purity allowlist (`{ironclaw_host_api}` only) forbids the
contracts crate from naming. Measured instead: both lanes read exactly three
things off the package — `id`, `capabilities`, `manifest.runtime` — so the
lane request structs now take those three and the caller (which owns the
package) projects them.

Also repointed `ResourceReceipt` to its real owner: `ironclaw_resources`
only re-exports `ironclaw_host_api::resource::ResourceReceipt`, so the lanes'
import was a §11.2.4 two-import-paths hop, not a dependency.

No `pub use` shims (§11.3): every consumer is repointed in this change, and
`resolve_under` becomes the free function `ironclaw_extensions::resolve_asset_under`
because the orphan rule forbids an inherent impl on the moved type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Creates `ironclaw_sandbox` (runtimes) from the three halves of "run an
already-authorized command away from the host", and deletes the two crates
PROPOSAL §6.6.4 marks for merge:

- `ironclaw_process_sandbox` (plan contract)      -> `src/plan.rs`, `src/validation.rs`
- `ironclaw_host_runtime::sandbox_process`        -> `src/sandbox_process/**`
- `ironclaw_scripts` (script lane + Docker path)  -> `src/script.rs`

The kernel sheds the Docker/CA cone: `bollard`, `rcgen`, `x509-parser` and
`time` are gone from `ironclaw_host_runtime`'s manifest, and `bollard`/`rcgen`
are now declared by exactly one crate in the workspace.

Two migration details PROPOSAL §6.6.4 and CHECKLIST WS10 call load-bearing:
- `PROCESS_SANDBOX_CAPABILITY_ID` -> `ironclaw_host_api::capability`, so
  `ironclaw_loop_host` drops its lane dependency (production dep gone; a
  dev-dep remains for the tests that build plans).
- `SandboxCommandTransport` -> `ironclaw_host_api::process`, with the shapes
  it names (`CommandExecutionRequest`/`Output`, `RuntimeProcessError`,
  `SavedCommandOutput`, `SavedCommandOutputSanitization`). Without this the
  runtimes-layer lane could not implement what the kernel consumes.

Enumerating gates were repointed, never relaxed: the specificity carve-outs and
the struct/test-support ratchet entries moved with their files (both baselines
unchanged at 129 and their prior values), the panic-gate baseline row moved,
`reborn-crate-test-buckets.sh` registers the new crate, and the three
`reborn-e2e-rust.sh` script selectors follow the tests (plus `docker_security`,
which had no selector before).

One gate would have gone silently vacuous and was fixed rather than moved: the
script-lane surface scan in `reborn_dependency_boundaries.rs` read a hardcoded
`src/lib.rs`, which after the merge no longer holds the lane. It now scans the
whole crate source tree with a fatal-read walk and a non-vacuity assertion.

One deletion, recorded: `RebornScopedSandboxCommandTransport::into_process_port`
returned a kernel type a runtimes crate may not name. It had zero callers
workspace-wide; the kernel wraps the transport, which is the direction the port
inversion requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ence

Three dated amendments, each quoting the text it replaces:

1. CHECKLIST WS3 sandbox row + PROPOSAL §6.6.4 — "all pieces currently
   unwired/test-only" is REFUTED. Three production paths cross the merged
   crate (spawn-path plan validation, the process_executor routing check, and
   the saved-command-output scope digest). The accurate claim is narrower:
   no production *execution backend*. Behavior preservation is therefore
   argued at the diff (11 of 26 moved files byte-identical, 9 more differing
   by one import line, +63/-36 overall), not inferred from deadness.

2. CHECKLIST WS3 mcp row + PROPOSAL §6.6.3 — the prior wave's "structurally
   blocked" finding is half right, and the wrong half is load-bearing: only
   `ExtensionPackage` is un-absorbable, and no lane ever needed it (both read
   `id`, `capabilities`, `manifest.runtime` and nothing else). The registry
   half of the flip is done; the `resources` half is refuted as phrased —
   the estimate/usage vocabulary the row asks about is already in
   `host_api::resource` and already imported from there, while the real
   blocker is the `ResourceGovernor` authority port and `ResourceError`'s
   denial cone.

3. Recorded as a structural finding, not a note: the sandbox row and the mcp
   row are ONE problem. `ironclaw_scripts` imports the identical DTO set, so
   the merge alone deletes zero exceptions and only the mcp carve-out lets
   either lane shed the registry edge.

Also reconciled: PROPOSAL §6.1.2's as-built inventory gains the two modules
WS3 landed (and states why `ExtensionPackage` stayed); §2's package count
66 -> 65; the §9 disposition rows for `ironclaw_scripts`/`ironclaw_process_sandbox`/
`ironclaw_mcp`; the §11.2.2 ratchet rows (13 -> 11); the WS3 verify row; the
stale WS1.3 sentence asserting the blocker as settled fact; and
`reborn_restructure_baselines.rs`'s doc table, which still read 15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`process_port.rs` no longer names `MountView` or `thiserror::Error` (both went
to `host_api::process` with the types that used them), and `sandbox_process.rs`
no longer needs `sync::Arc` after `into_process_port` was deleted. Found by
per-crate `clippy --all-targets --all-features -D warnings`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tions

Three fail-closed gaps in `reborn_pr_test_plan.py`, all hit by this PR and all
live on `main` today — any PR with the same change shape is unplannable.

1. `.claude/**` was unclassified, so the planner refused outright. It is agent
   guidance in exactly the sense `docs/**` is human guidance: no Rust test
   reads either as data (the only in-tree references are prose citations in
   test doc comments). Added to `IGNORED_PREFIXES`. Without this, "guidance
   travels with the change" — the restructure's own discipline — cannot be
   satisfied in a single PR.

2. `crates/AGENTS.md`, `crates/README.md`, `crates/Architecture.md` raised
   "unmapped crate path": they sit under `crates/` but belong to no package.
   Now classified as crate-tree prose, matched by "Markdown no package
   directory owns" so a genuinely unmapped crate path is unaffected.

3. An unmapped crate path used to raise. `git diff` reports a deleted crate's
   old paths and CI feeds the planner that diff, so **every crate deletion or
   rename was unplannable** — including the six deletions PROPOSAL §2 plans.
   It now widens to the exhaustive plan. This is a semantic change and it is
   the safe direction: the full plan is a superset of any narrowing, so an
   unattributable path can never cause under-selection, whereas refusing to
   plan blocks the PR instead of protecting it. Malformed input is still
   rejected by the unclassified-path branch.

Each lands with fixtures per WS10's rule, positive and negative: guidance
paths select nothing while non-guidance paths still fail closed; crate-tree
prose selects nothing while crate *code* under the same unmapped directory
widens to `full` (so the Markdown carve-out cannot swallow code). The
pre-existing `test_unmapped_crate_path_fails_fast` is renamed and rewritten to
pin the new contract rather than deleted.

Verified against this PR's real 130-path diff: the planner returns `mode:
full`, and the workflow's own exhaustiveness guard passes on that output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… a wave

Review (#7065) caught that both surviving `-> ironclaw_resources` exceptions
declared `removes_in = "WS3"` — the wave this PR *is*, which does not remove
them. That is precisely the defect §11.2.2 already records against
`conversations -> turns` ("`removes_in = "WS5"` and WS5 has partly shipped
without it falling"), and it would have been repeated here.

Both now point at issue #7067, which owns the design work that actually clears
them: replacing the `ResourceGovernor` dependency with a narrow
reserve/reconcile/release port. The issue carries the measurements — 3 of 10
methods used, zero implementors, and the `ResourceError` denial cone — plus the
two open questions (error shape, port home) that make it a design slice rather
than a move.

An owning issue is also what §11.2.2 asks for and what the ratchet still cannot
enforce (there is no `owning_issue` field yet), so this is the strongest form
currently expressible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on_contracts

`validate_asset_path` moved here with `ExtensionAssetPath`, the type it
constructs. In `ironclaw_extensions` it was only ever reached indirectly
through manifest parsing, so its six rejection branches had no direct test —
and a contracts crate that carries validation owes that validation one.

Two tests: every reject branch with its exact reason and `Display` output
(empty, NUL/control, URL, absolute, Windows drive and backslash, and the
empty/`.`/`..` segment cases) plus the manifest-relative shapes that must keep
being accepted; and `ExtensionRuntime::kind()` over all five variants, since
that projection is what every lane uses to reject a runtime it does not serve.

Also removes a changed-line coverage risk this PR would otherwise carry into
the merge queue: the gate does not run on ordinary PRs (#7036), so ~100
newly-added lines of validator would first be measured where a failure is
expensive to diagnose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…andbox lane

`RATCHET FAIL: ironclaw_host_runtime` — observed 18854 covered vs a
`floor_covered_lines` of 20538. This is the shrinkage case the ratchet's own
"To fix" text describes, not a coverage regression: `sandbox_process/**` moved
to `ironclaw_sandbox`, so the crate's denominator fell 23277 -> 21267 (-2010
instrumented lines) and its covered lines fell with it.

The percentage floor is **raised, not lowered**: observed 88.65% against an old
floor of 88.23%, so the entry now reads 88.65. Only the absolute line count
moves down, and it must — those lines are no longer in this crate.

To keep that from being a net loss of protection, `ironclaw_sandbox` is floored
on arrival at its observed 87.09% (3185 / 3657). This is a net *increase* in
ratchet coverage: neither `ironclaw_scripts` nor `ironclaw_process_sandbox` was
ever floored, and the `sandbox_process` half was protected only as part of
host_runtime's line count, which this PR necessarily reduces. Floored crates
16 -> 17.

Verified by replaying the ratchet arithmetic against CI's observed numbers:
both crates pass on percentage and on covered lines. Numbers taken from the
failing run's own report (job 91740733521), which is the authority for this
gate.

The `Tests (Reborn)` roll-up failed solely on this sub-job
("coverage-report result 'failure' did not match planned=true"); no other lane
failed — 50 pass, 2 fail, both this root cause and its roll-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…itive gate

WS3 hit a gate no move row had named. `tests/integration/coverage-floor.toml`
is keyed on crate identity plus absolute covered-line counts, so it is
invisible to WS10's path-keyed gate audit and yet it fails on every crate move,
merge, rename, or family `git mv` that shifts instrumented lines between
crates — as it did here, while the percentage floor was *improving*.

Recorded on WS10 with the three rules WS7 will need: re-capture in the same PR,
raise the percentage floor rather than leaving it, and floor the destination
crate or the move silently drops that code out of the ratchet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in #6780 (ironhub deep-link register/install + the REBORN_COV_COLLECT
hermetic-env allowlist fix), #7050, and #7033.

Two conflicts, both resolved as a union with each side's contribution verified
present afterwards:

- `crates/ironclaw_extension_host/src/available_extension_import.rs` — main
  added `use ironclaw_extension_contracts::recipe::VendorAuthRecipe;` at the
  same import position this branch added
  `use ironclaw_extension_contracts::runtime::ExtensionAssetPath;`. Both kept.

- `scripts/ci/test_reborn_pr_test_plan.py` — main added
  `test_selected_integration_lane_keeps_msrv_override` and kept
  `test_unmapped_crate_path_fails_fast`; this branch had replaced the latter
  with `test_unmapped_crate_path_widens_instead_of_refusing`. Resolution keeps
  main's new test verbatim and this branch's rewrite, and drops the superseded
  original — it asserts the exact behavior this branch deliberately changed
  (an unmapped crate path now widens instead of refusing, so crate deletions
  are plannable). Keeping both would have been contradictory, not a union.
  37 tests pass (36 here + main's 1).

Post-merge verification of both sides: exceptions 11 with baseline 11 and
`ironclaw_scripts` absent from the matrix (this branch); 9 #7033 decision
markers and the `REBORN_COV_COLLECT` allowlist entry present (main).

Note on the coverage floors this branch re-captured: #6780's fix stops the
hermetic env filter stripping `REBORN_COV_COLLECT`, which gates *whether* a
lane collects coverage. This branch's numbers were captured on a `full` plan,
where every lane collects either way, so they are expected to hold — CI
re-measures and will say so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Path

A semantic conflict the merge could not see: #6780 landed
`ironhub/{package,catalog}.rs` importing `ExtensionAssetPath` from
`ironclaw_extensions`, while this branch moved that type to
`ironclaw_extension_contracts::runtime`. Different files, so git auto-merged
cleanly and the breakage surfaced only at `cargo check`.

Repointed both sites to the contracts crate (no shim, per §11.3). The manifest
already named `ironclaw_extension_contracts`, so this is imports only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gate

The changed-lines coverage gate went red on four files while changed-line
coverage was 95.35% against a 90% floor: the failure was its two fail-closed
STRUCTURAL assertions, not any percentage.

Every line below was derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov (run 30831658659) with the base lcov the gate
itself resolved (run 30828540055 @ b89fcd3), until the replay reproduced the
CI verdict byte-identically. Line numbers come from the gate's own
`candidate_lines - mechanically_uninstrumentable_lines()`, not from the log.

- host_api/src/process.rs (31 lines): new placement-neutral process vocabulary
  with no function body anywhere in the file; rustc emits no LCOV record for it
  at all. Same shape already exempted for product_contracts/loop_contracts.
- extension_contracts/src/hosted_mcp.rs (12): field declarations of the two new
  tools/list descriptor structs. The file is plainly instrumented (191 DA, 164
  hit), so this is a no-region artifact, not an instrumentation gap.
- host_runtime/src/services/runtime_adapters.rs (13): continuation lines of
  three rewritten calls, all PROVEN EXECUTING by their region-start heads
  (lines 380/434/977 score 24/16/63 hits). The four genuinely-uncovered lines
  in the same rewrite are deliberately NOT exempted -- the gate already
  subtracts them as pre-existing debt inherited from base.
- composition capability_host_tests/approval_gates.rs (6): type positions in a
  test double whose body region scores 1 hit.

The last one is a finding, not just a waiver: that file is 100% test code
behind `#[cfg(test)] mod capability_host_tests;`, but the gate's
test_only_path() recognises /tests/, /test_support/, */tests.rs and *_tests.rs
and NOT a cfg(test) module DIRECTORY, so it measures it as production. It is
the only such directory in crates/ today.

Docs (target-architecture, same PR per the docs-truth rule):
- CHECKLIST WS10 gains the changed-lines gate beside the ratchet row, cross-
  referencing the WS2.1 note rather than restating it: percentages are not what
  fail a move; derive lines by byte-identical replay (--fetch-base-coverage
  silently degrades without --github-repo); and a stranded exemption path is an
  ABORT with no verdict, not a loud failure.
- CHECKLIST WS10 exception-ratchet row: the constant was cited at :4063 and
  sits at :4164 -- corrected by removing the line pin, since the file is edited
  every wave. Records that the baseline is a UNION across parallel WS3 lanes.
- families/contracts.md: records extension_contracts' new ownership of the
  runtime descriptor vocabulary -- the carve-out that let BOTH lanes drop the
  registry edge -- and the orphan-rule seam that keeps resolve_asset_under in
  the registry crate.
- families/lanes.md: two "Never" claims were reading as satisfied when they are
  not. ironclaw_mcp's "never depends on the resource-governor crate directly"
  is refuted (the compiled edge survives; #7067 tracks the narrow port), and
  ironclaw_sandbox's "no direct process spawning outside the transport seam" is
  aspirational -- script.rs:454 still builds Command::new("docker").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion cost

Two review findings verified against the tree; three refuted with evidence in
the PR threads.

Valid — the sandbox wiring inventory was self-contradictory. `CLAUDE.md` said
"Two production call paths ... and both are plan validation" directly above a
list of THREE bullets, and `lib.rs` omitted the third entirely. The third is
real and is not validation: `host_runtime/src/process_output.rs:482` derives the
scoped saved-output directory through `RebornSandboxScopeKey::from_scope`. That
inventory is what tells a future agent which paths are live, so an undercount
invites deleting a production path as dead code. Both surfaces now say three and
no longer claim they are all plan validation (the `loop_host` capability-id
comparison never was either).

Valid, and recorded rather than redesigned — the registry carve-out cost a
type-level invariant. Replacing `package: &ExtensionPackage` with independent
`extension` / `capabilities` / `runtime` borrows is what deleted the
`mcp -> extensions` and `scripts -> extensions` exceptions, but it also means
the type no longer guarantees the three came from one package.
`execute_extension_json` re-checks the descriptor half
(`descriptor.provider == extension`); the runtime half cannot be re-derived,
because nothing in an `&ExtensionRuntime` names its owning extension. No caller
can trip it today -- there is exactly one production caller
(`runtime_adapters`) and it projects all three from one package in one
expression -- so this is a latent structural weakening, not a live defect.
Restoring the compile-time binding needs a sealed projection minted by the
package owner; a check inside the lane cannot express it, and re-taking the
registry edge would undo the carve-out. Both request types now carry the caller
obligation in their field docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pport (WS3)

WS3's first-party-tools row, family 1 of 6: skill management / URL install.

`skill_url_install.rs` and its `bundle`/`github`/`zip_bundle` submodules,
plus the install-input normalizer, move out of
`ironclaw_host_runtime::first_party_tools` into
`ironclaw_extension_support::skills::{url_install, resolve_install_input}`,
where the skill executor half already lived. Move-only: no behavior change,
no test edited for content.

`ironclaw_host_runtime -> ironclaw_skills` is deleted from
LAYER_MATRIX_EXCEPTIONS — the edge is gone, not waived (exceptions 13 -> 12,
WS0_LAYER_MATRIX_EXCEPTION_BASELINE drops with it). `ironclaw_skills` and
`zip` survive as dev-dependencies for host_runtime's own tests; dev edges are
outside the matrix by construction.

Two doc ambiguities are resolved in the same diff, as dated PROPOSAL
amendments quoting the text they replace:

- §6.8.4's "the builtin first-party tool handlers absorbed from
  host_runtime/first_party_tools" contradicted §8.2's "kernel: ✗ (ports only)"
  row and the enforced BoundaryRule. Resolution: the seam splits executor from
  adapter — the executor moves behind a neutral request/error pair, the
  FirstPartyCapabilityHandler / CapabilityManifest / registry wiring stay
  host-side. Same shape the groupware and web-access tools already ship.
- §8.2's "ports only" cell now says what it means: contracts-layer ports the
  kernel also consumes, not permission to name a kernel trait.

Two cost corrections recorded for the remaining families:
`host_runtime -> extension_support` is not divisible family-by-family (mod.rs
holds it via `extension_support::coding`), and
`host_runtime -> ironclaw_extensions` is not reachable by this row at all.

PATH_TERM_COLLISIONS shrinks by two: the installer's github carve-outs now sit
inside a scan-exempt crate.

Test accounting (un-masking discipline), unfiltered `--list` over both crates:
1398 -> 1398, with exactly two tests renamed by module path and none lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nothing

Review asked why the migrated docker_security test can pass with no daemon.
The skip is pre-existing (the file differs from its pre-merge original by one
import line); WS3 only enrolled it in the required Rust e2e lane, where it was
not run at all before.

The real defect the question surfaced is worse and also pre-existing: this
crate's tests/support/docker_gate.rs states that IRONCLAW_REQUIRE_DOCKER_TESTS=1
makes a missing daemon a hard failure and that "CI sets this" -- and nothing
sets it. Repo-wide the name occurs only in docker_gate.rs and
attribution_tests.rs, here and on main. So every real-Docker test in the crate
skips-and-passes everywhere, which is exactly the gap the gate's own comment
says let sandbox security bugs ship unnoticed. docker_security.rs additionally
open-codes its own check rather than using the gate, so it would stay fail-open
even once something did set the variable.

Recorded rather than fixed: setting the variable is a CI-behavior change that
would hard-fail any lane without a daemon or the ironclaw-worker image, which
is not verifiable from inside a move PR whose evidence claim is behavior
preservation. Filed as the #6945 guardrail-claim-vs-reality class with the
two-part fix stated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The crate's CLAUDE.md said "first-party runtime tools belong under
`first_party_tools/`" without saying that only the host half does. WS3 moves
each tool's executor into `ironclaw_extension_support`, which may not name this
crate, so the rule now names both halves and points at the skill-install family
as the worked example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The moved executor returns `SkillManagementCapabilityError`, and routing it
through `skill_management_error` would have added a `debug!` line to a path
that had none before the move. A move-only change must not add one, so the
install-input arm maps the kind directly and the `dispatch` arm keeps the
record it already had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…move

The ratchet does not run on `pull_request` (`reborn_pr_test_plan.py:21`; issue
#7036), so this PR's green checks were not evidence on this axis. A full-plan
`workflow_dispatch` run on this exact head reported:

  RATCHET FAIL: ironclaw_host_runtime
    observed: 88.59% (20485 / 23124 lines)
    floor:    88.23% ... floor_covered_lines: 20538 (effective floor 20518)

The percentage went UP while `floor_covered_lines` went DOWN — shedding
well-covered code lowers the absolute numerator, which is a separate assertion
from the percentage one. Re-captured to the observed numbers (floor raised
88.23 -> 88.59, not merely held). Verified locally against that run's own merged
lcov artifact: ENFORCING mode, 17 PASS / 0 FAIL, exit 0.

  run: https://github.com/nearai/ironclaw/actions/runs/30858257594
  head: e07b3b0

The destination crate is deliberately not floored, because it cannot be: every
crate under `crates/extensions/` is invisible to the coverage tooling —
`reborn_coverage_lcov.py:19`'s CRATE_RE still requires a crate directory
directly under `crates/`, which #7037's colocation broke. Filed as #7083 with
the measurement; the global floor is left alone rather than re-captured onto
that hole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CHECKLIST WS4 + WS10 `wit/` rows. `wit/{tool,channel}.wit` moves from the
repo root to `crates/ironclaw_wasm/wit/` — the crate that owns the ABI —
per PROPOSAL §6.6.1. Behavior-free: same bytes, same generated bindings.

Wave-3 coordinates: the docs write the destination as
`crates/lanes/ironclaw_wasm/wit/`, but `crates/lanes/` does not exist until
WS7. Because the files now sit *inside* the crate, the WS7 family move
carries them with no further path edit anywhere — which is the whole point
of putting them there.

Ten wit-bindgen `path:` args repointed (the host plus nine guests: six under
`crates/extensions/packages/*/wasm-src/`, three under `test-tools/*/wasm-src/`
— the CHECKLIST row said six). All nine guests verified building against the
moved WIT on wasm32-wasip2.

The four `include_str!` readers of the ABI text do NOT get repointed
literals. Doing that would turn the two `ironclaw_host_runtime` sites from
repo-root reach-ins into *cross-crate* ones — §11.2.7's strict class, the
one WS2 turns into hard failures — taking the scan from 19 to 21 while
ticking a box that says "§11.2.7 scan passes". Instead the ABI text gets one
owner, `ironclaw_wasm::TOOL_WIT` (`src/config.rs`, beside `WIT_TOOL_VERSION`),
and all four sites read the const over cargo edges that already exist.
Measured with the scan: 133 -> 129 escaping sites, cross-crate 19 -> 19,
zero `wit/` entries remaining.

Path-keyed gates repointed: `scripts/check-version-bumps.sh` (both ABI
paths), `.githooks/pre-commit`, and `platform-and-compat.yml`'s
`has_direct_wasm_abi_risk` filter — where the bare `wit/` alternative is
*deleted* rather than rewritten, because the filter's existing
`crates/([^/]+/)*ironclaw_wasm/` alternative already matches both the
Wave-3 and the WS7 location. `scripts/ci/ws12_workflow_contracts.py`
anchored on that deleted string, so its anchor moves to
`build-wasm-extensions` and its in-scope probe now pins both locations.

`Dockerfile` loses two `COPY wit/ wit/` lines in the planner and builder
stages: both already run `COPY crates/ crates/`, so the files arrive with
the crate and the old line would COPY a path that no longer exists.

Docs: the WS4 row's `crates/lanes/wit/` destination was the only doc site
placing the directory beside the crate rather than inside it; corrected
there and in README's tree, with dated amendments in CHECKLIST, PROPOSAL
§6.6.1 and PLAN Wave 3 recording what the move found.

Test accounting (unfiltered `--list`, name-by-name, quiescent tree):
ironclaw_wasm 51 -> 51, ironclaw_host_runtime 1246 -> 1246,
ironclaw_architecture 198 -> 198. Zero diff, no test edited for content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Forced by the previous commit, not incidental to it.
`scripts/ci/check-wasm-artifact-freshness.py` keys each package's committed
`wasm/<name>.wasm` to a digest of the `wasm-src/` tree that produced it, so
editing a guest's `wit_bindgen::generate!` `path:` — which the `wit/` move
requires in all six shipped guests — invalidates the recorded digest and
fails the gate.

The gate's own contract forbids the shortcut: "Re-record only after
`./scripts/build-wasm-extensions.sh --first-party` and committing the rebuilt
artifact — the digest asserts a claim about the artifact, and updating it
without rebuilding launders a stale one." So the artifacts are genuinely
rebuilt (`--first-party`, exit 0, 6 OK / 2 host-native SKIP), not re-recorded
in place.

Byte sizes move by more than the source change accounts for because these
builds are not reproducible by design — the guests pin no toolchain and
resolve their own `Cargo.lock` at build time, which is the documented reason
the gate hashes sources rather than artifact bytes.

Verified: `check-wasm-artifact-freshness.py` OK (6 packages), and
`cargo test -p ironclaw_extension_support` green (102/46/4) — that crate
`include_bytes!`s these artifacts, so it exercises the rebuilt components.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… edits

The `wit/` move had to rebuild six shipped WASM binaries because
`check-wasm-artifact-freshness.py` digests each guest's whole `wasm-src/`
tree. WS7 hits the same wall from the other direction: the six package
guests reach the ABI across two trees, so moving either `ironclaw_wasm` or
`extensions/packages` rewrites all six `path:` literals and forces the same
rebuild. Recorded on CHECKLIST WS10's `wit/` row (point 6), on the
loud-path-pattern row that owns the WS7 repoint (also corrected six -> nine
guests there), and on PLAN's Wave 5 block with the cheap mitigation: move
the two crates in one PR and pay it once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	docs/reborn/target-architecture/CHECKLIST.md
Reconciles this lane with #7064, which landed the parallel WS3/WS4 runner
sheds and edited the same coordination files.

Five conflicts, all in shared coordination surfaces — no code move in this
PR was altered (all 117 PR-only files are byte-identical to the pre-merge
tip; the 5 apparent diffs are deletions absent on both sides).

- `reborn_dependency_boundaries.rs`: the array auto-merged to the union of
  both sides' removals; only `WS0_LAYER_MATRIX_EXCEPTION_BASELINE`
  conflicted. Recomputed as `len()` of the merged list — 13 base, minus
  #7064's three (`hooks -> wasm_limiter`, `runner -> agent_loop`,
  `runner -> loop_host`) and this PR's three (`mcp -> extensions`,
  `scripts -> extensions`, `scripts -> resources`), plus this PR's
  justified `ironclaw_sandbox -> ironclaw_resources` = **8**. Counted by
  parsing only the entries between the const and its closing `];`, so the
  struct definition and the four test fixtures are excluded.
- `loop_host/Cargo.toml`: both sides added a `[dev-dependencies]` line;
  kept both (`http` from main, `ironclaw_sandbox` from this PR).
- `CHECKLIST.md`: kept both dated amendments in date order — #7064's
  `13 -> 10` and this PR's, with its count corrected from the
  authored-in-isolation `11` to the merged `8` exactly as the §11.2.2 row
  instructs. Also kept this PR's two new coverage-gate rows beside main's
  amended loud-inventory row.
- `reborn_pr_test_plan.py`: both sides made the same `.claude/` fix; took
  main's landed wording (`startswith` makes tuple order irrelevant).
- `test_reborn_pr_test_plan.py`: union of both sides' new tests, no name
  collisions — 46 tests pass.

Also repointed the one PR-authored `changed-coverage-exemptions.toml`
entry the merge shifted: `hosted_mcp.rs` moved +1 because main added a
doc-comment line, so its line-keyed exemption now resolves to
byte-identical source lines. The other stale entries in that manifest are
inherited and already stale on main; left untouched.

Verified: architecture suite 206 passed / 0 failed, `cargo check
--all-targets` clean, `cargo fmt` a no-op, zero conflict markers, and both
sides' `coverage-floor.toml` recaptures intact (runner 82.53 from #7064;
host_runtime 88.65 and the new ironclaw_sandbox 87.09 from this PR).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reconciles this lane with #7064, which landed the parallel WS3/WS4 runner
sheds and edited the same coordination files.

One conflict: `WS0_LAYER_MATRIX_EXCEPTION_BASELINE` in
`reborn_dependency_boundaries.rs`. The array itself auto-merged to the
union of both sides' removals — #7064's three (`hooks -> wasm_limiter`,
`runner -> agent_loop`, `runner -> loop_host`) and this PR's one
(`host_runtime -> skills`). Recomputed the constant as `len()` of the
merged list: main is at 10, so this slice takes it to **9**. Counted by
parsing only the entries between the const and its closing `];`, which
excludes the struct definition and the four test fixtures.

This slice was authored off 13 and computed `13 -> 12` in isolation; the
ratchet narrative and the two doc rows that quoted that figure (PLAN's
"First-party tools" bullet and CHECKLIST's W7-progress row) now read
`10 -> 9` and record why, per the union rule on the CHECKLIST §11.2.2 row.
The edge deleted is unchanged; only the total moved.

Everything else auto-merged and was verified rather than assumed: both
sides' `coverage-floor.toml` recaptures are intact (runner 82.53 and the
new `ironclaw_loop_host` 90.89 from #7064; `host_runtime` 88.59 from this
PR), and both sides' dated amendments survive in CHECKLIST, PROPOSAL and
PLAN. All 14 PR-only files are byte-identical to the pre-merge tip, so no
code move was altered.

Verified: architecture suite 206 passed / 0 failed, `cargo check
--all-targets` clean, `cargo fmt` a no-op, zero conflict markers, and the
changed-coverage manifest validates with no exemption stranded or shifted
by this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Detect Reborn test scope` exits 1 on any pull request whose diff holds a
path `reborn_pr_test_plan.py` has no rule for, which made this PR
unmergeable: it must edit `Dockerfile` (the moved directory's
`COPY wit/ wit/` no longer resolves) and `scripts/check-version-bumps.sh`
(the ABI gate would otherwise grep dead paths and silently stop
enforcing). 18 of its 46 paths were unclassified.

Same class as the `.claude/` gap #7064 fixed, and classified the same
way — one rule per class, recorded beside the constant:

  * `Dockerfile` / `.dockerignore` — `platform-and-compat.yml` keys
    `has_docker_risk` off exactly this pair and owns the image build.
  * `.githooks/**` — Code Style triggers on the tree and lints its
    contents (`test-ci-comm-locale-pin.sh`); no Reborn lane runs a hook.
  * `scripts/{build-wasm-extensions,check-version-bumps}.sh` —
    `platform-and-compat.yml`'s `has_direct_wasm_abi_risk` classifier
    both scopes and runs them.
  * markdown owned by no crate (`crates/AGENTS.md`,
    `test-tools/README.md`) — prose, like `docs/` and `.claude/`. A
    crate-resident doc still selects its own crate's lane.

The first-party extension package assets are deliberately NOT ignored.
`crates/extensions/packages/*/wasm/*.wasm` is a shipped artifact that
`ironclaw_extension_support` embeds with `include_bytes!`, and
`test-tools/*/manifest.toml` is `include_str!`d by
`ironclaw_extension_host`. Calling either prose would convert today's
loud failure into a silent under-schedule of a change to production
output — the WS10 failure mode. `EMBEDDED_ASSET_OWNERS` routes each tree
to the crate that compiles it instead, so this PR now additionally
schedules `ironclaw_extension_{support,host,manager}`: the crates that
consume the six rebuilt WASM artifacts.

Also fixes #7085 in a file this PR already touches. The WIT version
extractors used the GNU-only BRE `\+`, so on BSD sed (macOS) they matched
nothing, and because the `WIT_TOOL_VERSION` cross-check is guarded on a
non-empty version the hook printed "All version checks passed" having
compared nothing. `[[:space:]][[:space:]]*` is identical under GNU sed,
so the enforced Linux CI lane is unchanged; verified on BSD sed that both
`wit/tool.wit` (0.3.0) and `wit/channel.wit` (0.3.1) now extract.

Regression tests: every classified class gets a case in
`test_reborn_pr_test_plan.py`, including the paired assertion that the
embedded assets *select a lane* rather than merely being accepted (the
inverse of the `.claude/` prose test), and a staleness pin that fails if
an asset tree or its owning crate moves. All ten new cases fail against
the planner on `main`. `test_unclassified_build_input_fails_fast` moves
off `Dockerfile` onto a still-undecided input so the fail-closed arm
stays exercised.

Refs #7087, #7085

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ners (WS3)

`crates/ironclaw_host_runtime/src/obligations.rs` was 3,122 lines fusing the
three owners PROPOSAL §6.5.9 charters separately, held apart only by an
`// arch-exempt: large_file` waiver. It is now one module per owner:

- `obligations::handler` — which obligations apply and what each does
  before/after dispatch, plus the audit/redaction/ceiling/mount validation.
- `obligations::staged_handoffs` — material staged for a later consumer:
  the runtime-secret and network-policy stores and the credential-account
  resolver port.
- `obligations::process_store` — post-start handoff discard and reservation
  reconciliation.
- `obligations::mod` — only `BuiltinObligationServices`, the assembly seam,
  and deliberately the one place naming all three at once.

Every module is under the 1,500-line gate, so the waiver is deleted rather
than carried forward: re-fusing the owners now trips `pre-commit-safety.sh`.
`mod obligations;` stays private and the crate's `pub use obligations::{…}`
names are unchanged, so no consumer outside the crate sees this.

Behavior-free. Cross-owner access is `pub(super)` (three methods), not
`pub(crate)`. The split revealed one narrowing in the other direction:
`secret_present` was `pub(crate)` with no caller outside its own file and is
now private.

Also from the same CHECKLIST row, the bounded half of "shrink
`services/builder.rs` toward composition-facing factories": three builder
methods whose only callers are inside the crate's `src` narrow to
`pub(crate)`. The rest of that clause is measured and deferred in the
CHECKLIST amendment — 17 methods need a `test-support` cargo feature, three
are callerless and belong to WS8, and the remaining 33 are a redesign of the
fluent surface rather than a shrink of it. `+production_wiring` is refuted
there: it is readiness diagnostics, not assembly.

Two loud path-keyed gates fired and were repointed, not relaxed:
`reborn_host_runtime_services_do_not_expose_lower_substrate_handles` now
scans the whole `obligations/` directory and asserts it read ≥ 4 files
(`collect_runtime_rs` returns a count; both its callers now assert non-zero),
and `reborn_struct_test_support_ratchet`'s frozen per-file count moves to
`staged_handoffs.rs` with its count unchanged at 1.

Test accounting (un-masking discipline): `cargo test -p ironclaw_host_runtime
--all-targets -- --list` is 1,246 before and 1,246 after, name-by-name
identical — zero added, removed or renamed. `LAYER_MATRIX_EXCEPTIONS` is 10
before and after; an intra-crate split cannot move the register.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t_contracts port (WS3)

`ironclaw_operator` is a products-tier crate and held `ironclaw_secrets`, the
substrate that owns CAS one-shot leases, AAD/crypto and the OS keychain master
key. PROPOSAL §8.2's product row says the products tier loses that edge, and
§12.1b requires the port replacement to land before the edge is removed. Both
happen here, in that order.

- Port: `ironclaw_product_contracts::operator_secrets::OperatorSecretValueStore`.
- Implementor: `ironclaw_reborn_composition::RuntimeOperatorSecretValueStore`,
  the same placement as `OperatorStatusService` — assembly is the only layer
  that may name both a products-tier port and a substrate. Registered in
  `INVERTED_PORTS` beside it.
- `ironclaw_secrets` is gone from the operator manifest under every dependency
  kind, and `"ironclaw_secrets"` is now in the crate's `boundary_rules()`
  forbidden list. That gate's comment previously said the entry was
  deliberately absent because "the row owns it"; the row now owns it.

The port is deliberately narrower than the substrate, so this is a tightening
rather than a relocation: it takes no `ResourceScope` (the implementor fixes
the operator scope, where the caller used to pass one), exposes no
lease/consume protocol, and carries only a `&'static str` classification
instead of the substrate's error `Display` — asserted, including that the
backend message and the handle name are both absent from what crosses.

Two tests travelled with the behavior rather than being pointed at a fake:
`read_is_repeatable_across_reloads` (repeatability is a property of the lease
protocol) and the #4673 production-store reproduction (its value is wiring the
store exactly as production does, which now means the real store *behind the
adapter*). Two `FaultInjecting`-over-real-store fixtures became per-operation
port fakes, with the substrate error mapping re-pinned at the adapter; a third
assertion got stronger — batched-vs-N+1 stored-key lookup is now observed at
the port rather than by counting filesystem ops.

Test accounting: operator 154 -> 153, product_contracts 142 -> 143,
composition 937 -> 942 with zero removed; name-by-name diffs on a quiescent
tree.

Two findings the row could not have anticipated, both recorded in the
CHECKLIST amendment:

- The `webui` half of the row was already closed and was never a production
  edge. `ironclaw_secrets` has been a dev-dependency of `ironclaw_webui` since
  the commit that added it (#6619), both src mentions are `#[cfg(test)]`, and
  webui's boundary rule already forbade it.
- `ironclaw_extension_manager` (layer `products`) still holds a normal
  `ironclaw_secrets` edge in `admin_configuration.rs`. §8.2 covers it; the row
  does not, because the crate landed with WS2.4 after the row was written, and
  the substrate sits in the service's type parameters so it is not a
  like-for-like swap. Filed as #7095.

`LAYER_MATRIX_EXCEPTIONS` is 10 before and after: `products -> substrates` is
matrix-legal, so this edge was always an §8.2 rule and never a layer exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review asked why the required Rust e2e lane can report `docker_security` as
passing with no daemon. Half of that is #7081 (nothing sets
IRONCLAW_REQUIRE_DOCKER_TESTS=1, so the switch is inert) and is not fixable
from here -- arming it hard-fails any lane lacking a daemon or the worker
image, which needs a runner guaranteed to have both.

The other half is fixable here and is fixed: docker_security.rs open-coded its
own `docker version` / `image inspect` checks with three bare `return`s, so it
sat entirely outside docker_gate and would have stayed fail-open even once
something did set the variable. It now takes both preconditions from
docker_gate::{docker_available, docker_image_available} and skips with the
visible `SKIP:` line that gate's module doc requires.

Measured, same machine, image absent:

  before, IRONCLAW_REQUIRE_DOCKER_TESTS=1 -> "skipping ..." / 1 passed
  after,  IRONCLAW_REQUIRE_DOCKER_TESTS=1 -> panic at docker_gate.rs:74 / FAILED
  after,  variable unset                  -> "SKIP: ..." / 1 passed

The third line is the no-op proof: the variable is set nowhere in this tree or
on main, so no lane's behavior changes today. The daemon-down path already
reached the image check and skipped there, so the outcome is identical; only
the branch it takes differs.

Two stale comments in docker_gate.rs corrected with it (they claimed
docker_security used its own gate, and that docker_image_available had no
consumer), and the crate's Known debt entry now splits the done half from the
#7081 half instead of describing both as open.

cargo test -p ironclaw_sandbox: 193 passed, 0 failed
cargo clippy -p ironclaw_sandbox --tests --all-features -- -D warnings: exit 0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two review findings, both correct, both artifacts of this PR's own renames.

1. engine-v2-to-reborn-parity.md note 4 read "a native script/software
   execution lane (`ironclaw_sandbox`, `RuntimeKind::Script`) sandboxed via
   `ironclaw_sandbox`" -- self-referential after the merge collapsed
   ironclaw_scripts and ironclaw_process_sandbox into one crate, and it
   contradicts note 5 four paragraphs down ("no production execution backend
   is wired for it"). Re-stated as the typed runtime contract it is, citing
   the measurement: `with_script_runtime` has zero production callers
   (`rg` finds only the builder itself, docs, and 30 test call sites).

2. CHECKLIST WS10 ratchet note 2 said "raise the percentage floor ...; only
   the line count should fall". That generalises WS3's sandbox merge, where
   observed coverage happened to rise. It is wrong as guidance for WS7, and
   the counterexample is in this same file: the 2026-08-03 entry from #7064
   records ironclaw_runner falling 85.55% -> 82.53% because the shed removed
   the crate's better-covered half, holding the floor, and RATCHET FAILing in
   the merge queue. Note 2 now says re-capture from the merged artifact, and
   lower only with that entry's move-not-regression counterfactual (add the
   moved files back, confirm the union clears the old floor, plus a zero-tests-
   lost name set-diff).

cargo test -p ironclaw_architecture: 32 targets, 206 passed, 0 failed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three review findings on the `wit/` move, each verified before it was acted on.

1. `ws12_workflow_contracts.py` probed `crates/ironclaw_wasm/wit/host.wit` and
   its nested twin. No `host.wit` exists in this repository — `git ls-files
   '*.wit'` returns only `tool.wit` and `channel.wit` — so both probes sat
   under the `crates/([^/]+/)*ironclaw_wasm/` alternative and re-asserted the
   crate-name term while saying nothing about the canonical ABI contracts. In
   a validator whose stated design is "probe derived from reality rather than
   from a guessed layout", a fabricated filename is a defect on its own terms.
   Replaced with a `crate_globs` entry, `("ironclaw_wasm", "wit/*.wit")`, which
   discovers the contracts on disk, requires each in scope, and synthesises the
   nested WS7 form — so a third contract, or the directory leaving the crate,
   fails the pin instead of passing on a stale name. Verified non-vacuous:
   narrowing the workflow alternative to `.../ironclaw_wasm/src/` now reports
   `tool.wit`, `channel.wit` and the nested probe as out of scope.

2. The embedded-asset routing test substituted `alpha`/`beta` owners so it
   could reuse the synthetic workspace. That exercised the real prefix strings
   through the real routing, but left the prefix->owner *pairing* — the table's
   entire semantic content — asserted nowhere: swapping
   `ironclaw_extension_support` and `ironclaw_extension_host` passed. Fixed in
   two halves. The routing test now drives the real `EMBEDDED_ASSET_OWNERS`
   against a workspace carrying the real owners' names and real manifest paths
   (the synthetic one could not: `build_plan` rejects a changed package outside
   the canonical set), asserting the real owner is selected. And the not-stale
   test now derives the same pairing from the tree instead of restating the
   constant: it resolves every literal `include_str!`/`include_bytes!` in every
   workspace crate through `crate_tree`, keeps the targets no crate owns — the
   ones that actually reach the table — and asserts that every crate compiling
   one of them is the routed owner or a dependent of it.

   That surfaced a property worth pinning: `crates/extensions/packages/` is
   embedded by four crates, not one. `ironclaw_extension_host`,
   `ironclaw_extension_manager` and `ironclaw_reborn_composition` reach into it
   alongside `ironclaw_extension_support`, and routing to the support crate
   covers them only because each depends on it. If that edge goes, a shipped
   artifact change stops scheduling a crate that embeds it — the silent
   under-schedule the table exists to prevent.

   Regression coverage verified red by sabotage, all three wrong tables:
   owners swapped (7 failures), `packages/` -> `ironclaw_llm` ("embeds nothing
   from it"), and the hardest case, `packages/` -> `ironclaw_reborn_composition`
   — a real embedder that the other embedders do not depend on
   ("...does not depend on..., so routing there never schedules it").

3. CHECKLIST WS10 claimed each of the nine `wit_bindgen` guest edits forces a
   committed WASM artifact rebuild. Only six do:
   `scripts/ci/check-wasm-artifact-freshness.py` scans
   `crates/extensions/packages/*/wasm-src` alone, `wasm-src-digests.toml` holds
   exactly six entries, and `git ls-files '*.wasm'` returns exactly those six.
   The three `test-tools/*/wasm-src/` guests commit no artifact; the tenth site
   is the host's `bindings.rs`, not a guest. Corrected, and the `wit/` row now
   states the boundary rather than implying it.

Guest paths, `wit/` contents and the six rebuilt artifacts are untouched.

Verified: `test_reborn_pr_test_plan.py` 46/46, `test_ws12_workflow_contracts.py`
25/25, `ws12_workflow_contracts.py` green on the real tree,
`cargo test -p ironclaw_architecture` 206/206 across 32 binaries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BenKurrek and others added 17 commits August 4, 2026 13:18
`scripts/ci/run-hermetic-deterministic-suite.sh` resolves the WebUI frontend
directory through `scripts/ci/crate-dir.sh`; without a pin, a literal
`crates/ironclaw_webui/frontend` regressing back in is a silent break — the
suite would `cd` into a directory that used to exist and report nothing wrong
until the frontend build actually runs.

The assertion matches the exact removed literal (with the `/frontend` suffix)
rather than the bare crate name, so it does not trip on its own explanatory
prose, and it also requires `resolve_webui_frontend_dir` to still be present.

Regression test: `bash scripts/ci/test-hermetic-test-process.sh` -> OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Git kept the shared issue/review_after tail of both sides' final entries
outside the conflict markers; the union reorder handed it to the wrong
block, leaving the tool_payloads.rs entry (#166) without its policy
fields. Validated with CI's own invocation this time
(--validate-manifest-only), not just a TOML parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`Detect Reborn test scope` failed on this branch:

    Reborn PR test planner failed: unmapped test or CI path: scripts/check-version-bumps.sh

Same shape as the two planner gaps the WS10 CHECKLIST row already records:
`scripts/ci/reborn_pr_test_plan.py` fails closed on any path it has no rule
for, so an unclassified class makes "never edit this file" the only satisfiable
behaviour — and the failure takes `Tests (Reborn)` down with it, since every
downstream lane reports `skipping` when the scope job is red.

Repo-root `scripts/` is deliberately not prefix-classified, so each file needs
a decision recorded beside the constant. Four were missing:

- `scripts/check-version-bumps.sh` -> PR_STATIC_CONTROL_PATHS. Invoked only by
  `platform-and-compat.yml`, behind that workflow's own `has_direct_wasm_abi_risk`
  filter (which already names the script). No `Tests (Reborn)` lane runs it.
- `scripts/run-reborn-webui.sh` -> PR_STATIC_CONTROL_PATHS. A local developer
  launcher referenced by no workflow at all, so no lane can be selected for it.
- `scripts/reborn_qa_matrix/` -> QA_HARNESS_PREFIXES, beside `live-canary/` and
  `reborn_webui_v2_live_qa/`. Offline QA tooling over the route descriptors.

The fail-closed arm is untouched: an undecided repo-root script still refuses,
pinned by the existing second half of
`test_decided_repo_root_script_paths_are_owned_by_other_workflows`.

Regression tests: the two existing classification tests are extended to cover
all four paths. Sabotage-verified by removing the classifications and observing
4 errors (`ERROR: ... (path='scripts/check-version-bumps.sh')` and the three
siblings), then restoring -> 45 tests OK. The planner also now runs clean over
this PR's exact 45-path changed set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`code_style.yml`'s architecture step is `cargo test -p ironclaw_architecture
reborn` — a NAME filter, not a binary filter. None of the twelve new test
functions matched it, so all twelve of this PR's guardrails were invisible in
that lane: green, and checking nothing there.

`cargo test -p ironclaw_architecture reborn -- --list` counted 45 before this
change and 57 after, with every new gate now named:

    reborn_crate_inventory_measures_the_real_tree
    reborn_rust_and_python_crate_inventories_agree
    reborn_logical_spellings_resolve_to_each_crates_real_directory
    reborn_resolution_is_the_identity_on_a_flat_fixture_tree
    reborn_crate_moved_into_a_family_directory_still_resolves
    reborn_crate_that_no_longer_exists_is_refused_not_answered
    reborn_ambiguous_crate_name_is_refused_not_picked
    reborn_truncated_tree_refuses_rather_than_reporting_an_empty_inventory
    reborn_separate_workspaces_nested_manifests_and_build_output_are_excluded
    reborn_allowlist_entries_follow_a_crate_into_its_family_directory
    reborn_build_scripts_do_not_derive_the_repo_root_by_counted_parent_hops
    reborn_fixed_depth_matcher_catches_the_banned_shapes_and_ignores_prose

Rename only; no assertion changed. Full suite still 219 passed / 0 failed,
fmt clean, clippy zero warnings.

Note for the WS10 "guardrails must fail loudly on their own regressions" row:
that filter means Code Style runs 57 of the crate's 219 architecture tests. The
`Tests (Reborn)` bucket lane runs the crate unfiltered (`cargo test -p <pkg>
--all-targets`), so nothing is unrun overall — but a gate whose name misses
`reborn` is absent from the lane most reviewers read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The row's amendment listed four defects found while converting. Two more turned
up afterwards, from the PR's own CI run, and belong on the same row because
both are the fail-closed-with-no-rule / guardrail-that-checks-nothing shape it
already documents twice:

- `reborn_pr_test_plan.py` had no rule for four repo-root `scripts/` files the
  conversion touched, failing `Detect Reborn test scope` outright and skipping
  every downstream Reborn lane.
- `code_style.yml`'s architecture step filters on the test NAME `reborn`, so the
  twelve new gates were absent from it (45 -> 57 listed after the rename), and
  the lane as a whole runs 57 of the crate's 219 architecture tests.

Docs-only; the code changes both landed in earlier commits on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	crates/ironclaw_architecture/tests/reborn_restructure_baselines.rs
#	docs/reborn/target-architecture/CHECKLIST.md
ironclaw_conversations drops ironclaw_turns from [dependencies] and declares
the one coordinator call its inbound orchestration makes as a port. Zero
production behaviour moved: the orchestration, the trusted-trigger submitter
and every one of their tests stay in the crate that owned them.

The port (src/turn_submission.rs): ConversationTurnSubmitter, one method
submit_conversation_turn; ConversationTurnSubmission carrying only
host_api::turn vocabulary plus ConversationInboundClassification, the trust
value the orchestration derives from its own binding policy and never from the
adapter string; TurnSubmissionError with retry() and category()/
adapter_status_code() over the host's verbatim rendered cause.

The adapter (composition, automation/conversation_turn_submitter.rs, +158 net
production lines): holds the TurnCoordinator handle composition already
constructed for the trigger poller, calls product_context::resolve_inbound, and
maps TurnError -> port error totally (no wildcard arm).

CORRECTION to the pre-build analysis: the retry class is NOT derivable from the
category. The Conflict category straddles retryable TurnError::Conflict and
permanent LeaseMismatch/InvalidTransition/RunNotRetryable, so the port error
carries two independent axes, not one three-valued one. Same branches, same
ordering, same user-visible messages at every effect.

Invariants amended in the same diff, not silently contradicted: both
ironclaw_conversations/AGENTS.md and CLAUDE.md now name the port error and its
class partition where they named ironclaw_turns::TurnError, and both gained the
standing rule that a TurnCoordinator handle or an ironclaw_turns normal
dependency must not come back.

untrusted_trigger_adapter_records_product_inbound_not_scheduled_trigger is
byte-identical (verified) and still in inbound.rs. It asserts on the
SubmitTurnRequest a coordinator receives, so the fakes swapped to the port and
gained a documented mirror of the production adapter; ironclaw_turns is
retained as a DEV-dependency for that, with the reason in the manifest.
Dev-deps are not layer-matrix edges (is_normal_dependency filters them), and
cargo metadata confirms kind = dev with normal deps exactly
{extension_contracts, filesystem, host_api, safety, triggers} -- PROPOSAL
6.4.2's Deps clause, literally.

New seam coverage at the real adapter:
conversation_turn_submitter_maps_every_turn_error_to_its_class (16 rows: all 12
TurnError variants, AdmissionRejected once per reason; asserts category, retry,
that the port status equals the kernel's, and that the cause is verbatim);
conversation_turn_submitter_covers_every_turn_error_variant (discriminant
census); conversation_turn_submitter_mints_scheduled_trigger_only_for_trusted_trigger
(the composition half of the spoof guard). Composition's five
classify_materializer_inbound_error submission tests now build inputs through
the production mapping instead of a stand-in.

One consumer arm changed shape and is provably unreachable: ironclaw_product's
map_conversation_error only ever sees ConversationBindingService failures, which
never submit a turn (product has its own DefaultInboundTurnService). It now
yields TurnSubmissionRejected carrying the port error's rendering rather than
fabricating a TurnError to satisfy a variant no caller can reach. Recorded in
the CHECKLIST row rather than hidden.

Register: the conversations -> turns entry is deleted and
WS0_LAYER_MATRIX_EXCEPTION_BASELINE lowered 4 -> 3. No other entry touched.
Docs in the same diff: CHECKLIST WS5 row ticked with the as-built shape, WS1's
"count <= 12" verify row ticked (its enumerated clause is now fully true -- no
*->turns exception remains), PROPOSAL 6.4.2 amended with the built shape.
docs/plans/composition-pubuse.snapshot 131 -> 132 for the one deliberate
export, the module-owned adapter factory the integration harness uses instead
of hand-mirroring the wiring.

Verification (all unfiltered, none piped through head/tail):
  cargo fmt --all                                        clean
  clippy (6 crates, --all-targets --all-features -Dwarn) zero warnings
  cargo test -p ironclaw_conversations                   99 passed / 0 failed
  cargo test -p ironclaw_product                       1050 passed / 0 failed
  cargo test -p ironclaw_reborn_composition             945 passed / 0 failed
  cargo test -p ironclaw_architecture                    207 passed / 0 failed
  cargo test --test reborn_group_triggers                 15 passed / 0 failed
  cargo test --test reborn_group_journeys                 16 passed / 0 failed
  cargo check --workspace --all-targets                  clean (one
    pre-existing dead_code warning, unused_fetch_context in
    extension_support/src/skills.rs:572, confirmed on the base via git stash)
Register reads 3 entries against baseline 3; the ratchet and the staleness
check both pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hat failed changed-coverage

The full-mode PR run failed the changed-line gate two ways: 74.74% vs
the 90% floor (1,080 misses — 1,065 of them the capabilities host.rs
six-workflow split, the obligations three-owner split, and the
first-party-tools move re-attributed as new code) and the generated
wasm bindings.rs tripping the empty-denominator fail-closed rule on its
single changed line (the wit path arg). Same-run proof of no real
loss: the global floor and every configured per-crate floor PASSED in
the failing run. Exact-line exemptions per manifest policy (#6963
class); the 15 uncovered lines in other crates stay measured.
Offline arithmetic on the gate's own numbers: 3,195/3,210 = 99.53%
post-exemption. Validated with --validate-manifest-only (191 entries).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nto ws/waves-0-4-batch

# Conflicts:
#	crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
#	crates/ironclaw_architecture/tests/reborn_restructure_baselines.rs
…s/waves-0-4-batch

# Conflicts:
#	crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
#	docs/reborn/target-architecture/CHECKLIST.md
#	scripts/check-version-bumps.sh
#	scripts/ci/reborn_pr_test_plan.py
…ws/waves-0-4-batch

# Conflicts:
#	crates/ironclaw_architecture/tests/reborn_extension_specificity.rs
#	crates/ironclaw_architecture/tests/reborn_restructure_baselines.rs
…th the batch's re-layers

The #7156 gates met the batch's real movement and demanded the full
delta: ironclaw_sandbox's layer-origin row; five new same-layer edges
(four kernel edges made same-layer by the processes re-layer, one
substrates edge by the skills re-layer) with the baseline raised
70->75 then banked back to 72 as three stale skills edges deleted;
the skills DowngradePin freezing its six consumers at the move; and
two stale rows (deleted crates' origins, mcp's dead extensions
consumer entry). Every finding a real batch effect, none suppressed.
Composition absolute ceiling re-seeded to the batch tree's measured
45127 with the test record moved in lockstep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ratchet's doc block

Not one of the WS6 module-charter clauses — a base repair this stack needs
before its own gate can run.

`WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE`'s doc block accumulated four
dated recount notes across the Waves 0-4 batch merges, and one of the joins
left a bare blank line between two `///` runs documenting the same constant.
`clippy::empty_line_after_doc_comments` rejects that, so
`cargo clippy --all --tests --examples --all-features -- -D warnings` fails on
`crates/ironclaw_architecture/tests/reborn_extension_specificity.rs`.

Why it is invisible on the batch's own PR checks: `.github/workflows/
code_style.yml` lints `--lib --bins` on `pull_request` and only runs the
`--all --tests --examples` sweep on `push`. Test targets are therefore
unlinted until the merge queue, where this would have gone red for everything
stacked on the batch. Reproduced on the untouched base `89080c5160` by
stashing this branch's work.

The fix is the blank line only — `///` restored so the two runs are one doc
block. Two adjacent merge artifacts in the same block are recorded rather than
edited, because repairing them is editorial rather than mechanical: two
paragraphs end `...was 124).///` and `...optional.///`, where a following
note's `///` marker was glued to the previous line instead of starting one.

Verification: `cargo clippy -p ironclaw_architecture --all-features
--all-targets -- -D warnings` clean (was: 1 error); `cargo test -p
ironclaw_architecture --all-features` 259 passed / 0 failed; `cargo fmt
--check` clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s (WS6, §6.6.3)

`crates/ironclaw_mcp/src/lib.rs` was **2,767 lines** — PROPOSAL §6.6.3 records
2,709 (measured 2026-07-31), so the figure had drifted +58 and this is the
third recorded value for one file. WS6's module-charter row and §6.6.3 both
call for splitting it.

It becomes **seven private modules** plus a 61-line `lib.rs` that is the
charter table and the re-export list and nothing else:

| Module | Owns |
|---|---|
| `contract` | The vocabulary a caller names: config, DTOs, `McpClient`/`McpExecutor`, the `McpError`/`McpClientError` taxonomy |
| `runtime` | Reserve -> call -> reconcile/release, descriptor admission, the manifest credential context |
| `client` | The Streamable-HTTP `McpClient`: handshake, per-invocation session lifecycle, the `tools/list` paging loop |
| `jsonrpc` | The JSON-RPC 2.0 codec and response hygiene: framing, id matching, session-id/protocol-version validation, auth challenge, per-method credential routing |
| `discovery` | `tools/list` catalog admission: ceilings, per-tool classification, schema bounds, tool-name grammar |
| `egress` | The `McpHostHttp` port and the host-owned egress plan/planner |
| `diagnostics` | Every stable, bounded failure token the lane surfaces |

Two rules in the charter are load-bearing rather than decorative, because the
code already depended on both and neither was checkable while it was one file:

- **No module builds a failure string of its own.** Every reason comes from
  `diagnostics`' three cause enums, so the model-visible token set stays
  enumerable in one 209-line file. `diagnostics` is now the only module with
  no crate-internal dependency, which is what makes that verifiable.
- **`discovery` owns the catalog rules, `client` owns the paging loop.** The
  three ceilings live in `discovery` and the loop reads them — the
  drift-proofing `MAX_DISCOVERED_MCP_TOOLS`' own doc comment already claimed
  but could not enforce with both enforcement points in one file.

## No API change, no consumer edits

The submodules are private and `lib.rs` glob-free `pub use`s them, so
`ironclaw_mcp::X` remains the single import path for all consumers
(`ironclaw_host_runtime`, `ironclaw_extension_host`, the integration harness).
Zero files changed outside `ironclaw_mcp` except one architecture test and the
two target-architecture docs. Items that newly cross a module line were
widened to `pub(crate)` — never to `pub`.

## The waiver is deleted, not carried forward

`lib.rs:1` carried `// arch-exempt: large_file, ... pending the adapter module
split, plan #4088` — the split this commit is. No replacement was added:
largest file is now 658 lines (`jsonrpc.rs`), clearing the 1,500-line
ARCH-SPRAWL threshold `scripts/pre-commit-safety.sh` enforces with `exit 1`.

## A gate would have gone silently green

`reborn_dependency_boundaries.rs:1124` read `crates/ironclaw_mcp/src/lib.rs`
**as one string** and scanned it for forbidden dispatcher-composition surface.
After the split that file is 61 lines of `pub use`, so the scan would have
found nothing and passed for the wrong reason. Repointed to
`concatenated_crate_sources(crates/ironclaw_mcp/src)` with a non-vacuity
assertion — the identical shape the `ironclaw_sandbox` lane three lines above
already carries, from WS3 hitting this exact trap. Two lanes for two: any gate
naming a single `lib.rs` is a landmine for the crate it guards.

## Two placement calls (delegated authority)

`McpAuthContext` and `PreparedMcpClientRequest` are **not** in `contract`
despite being vocabulary by shape: both are constructed and consumed entirely
inside `runtime` and name no public type, so `contract` would have become the
owner of the runtime's private plumbing. `requires_host_http_egress` is in
`egress`, not `contract`, because it is a transport predicate consumed by both
`client` and `runtime` — charging it to either would have made one depend on
the other.

## Verification (measured, not asserted)

| Check | Result |
|---|---|
| Top-level item roster, name+kind | **105 -> 105**, zero added, zero removed |
| Items declared `pub` | **21 -> 21** (public surface unchanged) |
| Visibility widenings | 28 `priv` -> `pub(crate)`; **0** `priv` -> `pub` |
| Unfiltered `cargo test -p ironclaw_mcp --all-features -- --list` | **75 -> 75**; leaf-name diff empty |
| `cargo test -p ironclaw_mcp --all-features` | 32 lib + 38 + 5 integration pass |
| `cargo test -p ironclaw_architecture --all-features` | 259 passed / 0 failed |
| `cargo test -p ironclaw_host_runtime` | 1097 passed / 0 failed |
| `cargo test -p ironclaw_extension_host` | 386 passed / 0 failed |
| `cargo clippy -p ironclaw_mcp -p ironclaw_architecture --all-features --all-targets -- -D warnings` | clean |
| `cargo fmt --check` | clean |

Test paths moved from `tests::<name>` to `<owner>::tests::<name>`; the **leaf
names are byte-identical** and were diffed as such. The 32 lib tests bucket to
the owner they exercise (discovery 15, jsonrpc 13, diagnostics 2, client 1,
runtime 1). No test helper crossed an owner, so no shared test-support module
was needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…WS6, §6.4.8)

§6.4.8 asks for the "internal two-engine split (engine vs product_auth)" to
become "two chartered top-level modules". Measured on the base: both modules
already exist as top-level modules, and **neither names the other — zero
references in both directions**. The split was never structural. What was
missing is the charter, and a severance nobody checks is an observation that
lapses on the next PR.

## Two owners were not enough, measured

Charting only the two engines leaves the crate's **11 shared top-level
modules** unowned. Counted symbol-by-symbol — the right instrument, because
both engines import through the crate root's flat `pub use` list, so counting
`crate::<module>::` paths reads zero and is silently wrong — **6 of the 11 are
named by BOTH engines** (`credential`, `provider`, `oauth`, `scope`, `ids`,
`error`). Charging those to either engine would make one engine the owner of
the other's dependencies.

So the map has **four** owners, not two: `engine`, `product-auth`,
`vocabulary` (what both engines stand on and neither owns), and
`test-support`. This is the same refutation §6.4.13's five-sub-owner claim met
in the `llm` map, arriving independently.

## What landed

- **A charter in each engine's `mod.rs`** — owns / never-contains, plus the
  severance invariant and where the two engines are allowed to meet.
- **`crates/ironclaw_auth/CLAUDE.md` gains an enforced `## Sub-owner map`**
  covering all 43 `src/**/*.rs` files across the four owners, with three
  placement calls stated.
- **`crates/ironclaw_auth/tests/module_charter.rs`** — coverage (every file
  exactly one owner, every charted path exists, no double claims) **and** the
  severance pin (`engine` must not name `product_auth`, and the reverse).

## Three placement calls (delegated authority)

- **`account_state.rs` -> `engine`**, not `vocabulary`, despite sitting at the
  crate root: `AuthAccountState` is named by `engine/` and by zero files in
  `product_auth/`, and `engine/mod.rs`'s doc already claimed the state machine.
- **`cleanup.rs`/`domain.rs`/`flow.rs`/`interaction.rs` -> `product-auth`** on
  the same measured test. They are the four files a later slice could `git mv`
  into `product_auth/`; the map says so, and names the blocker — `domain.rs`
  needs a rename first, because `product_auth/durable/domain.rs` exists.
- **`credential.rs` is the one genuinely two-owner file** (18 of 25 symbols
  `product_auth`-only, 6 named by both, including `CredentialAccountService`
  and `ProviderBackedCredentialAccountService`, which `engine/keepalive.rs`
  drives for the refresh sweep). Charged to `vocabulary` — the shared half is
  what makes it un-movable — with the service split recorded as owed work.

## Three other §6.4.8 clauses were already discharged

Struck in the docs rather than left to be re-attempted: `loopback_oauth` and
its `urlencoding` dep are **gone** (both `CLAUDE.md` and `AGENTS.md` still
described it as a live "temporary exception" — corrected); `fakes.rs` **is**
gated behind `test-support` (`lib.rs:21-22`); and `ironclaw_turns` appears
nowhere in `crates/ironclaw_auth/Cargo.toml`.

## Verification (measured, not asserted)

This clause moved **no production code**, and says so rather than dressing a
charter up as a move:

| Check | Result |
|---|---|
| Top-level item roster | **609 -> 609**, byte-identical **including visibility** (zero widenings) |
| Unfiltered `cargo test -p ironclaw_auth --all-features -- --list` | **288 -> 291**; the +3 are exactly the new gate, no pre-existing test renamed/moved/removed |
| `cargo test -p ironclaw_auth --all-features` | 291 passed / 0 failed |
| `cargo clippy -p ironclaw_auth --all-features --all-targets -- -D warnings` | clean |
| `cargo fmt --check` | clean |

**Sabotage-proved in five directions**, each restored green: drop a file from
the map -> "1 source file(s) have no sub-owner"; add a phantom path -> "no
longer exists"; claim a file twice -> "claimed by more than one";
`use crate::product_auth::...` inside `engine/` -> severance failure naming the
probe; `use crate::engine::...` inside `product_auth/` -> the mirror. The gate
self-guards against going vacuous in four ways (zero parsed rows, implausibly
few walked files, a missing engine directory, a module concatenating to
implausibly little code). The severance scan strips comment lines, because both
charters deliberately name the other engine in prose and a scan counting those
would be unsatisfiable by construction.

## Coordination note

The `ChannelAuthAccountState` family is declared in `ironclaw_product`
(`reborn_services.rs:677`), not in `ironclaw_auth` — this clause touches none
of its files, so there is no collision with the sibling relocating it. If that
relocation lands a new file under `crates/ironclaw_auth/src/`, the coverage
gate fails until it is given a row. That is by design, and the failure message
states the rule to apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (WS6, §6.9.4)

`src/webui_v2/handlers.rs` is **4,593 lines** — the largest file in the crate.
WS6's module-charters row names a "webui `handlers.rs` charter map (§6.9.4)",
but **§6.9.4 contains no such clause**: the definition had to be reconstructed
from §6.9.1 ("module-charter map ... the audited **>=11** sub-owners") and
§6.4.15 ("module-charter work, **not a split**"). §6.9.4 now carries the clause
so the next reader does not reconstruct it a third time.

`crates/ironclaw_webui/CLAUDE.md` gains a **19**-sub-owner map covering every
top-level item: `session`, `threads`, `admin-users`, `workspace-fs`,
`projects`, `attachments`, `streaming`, `runs`, `commands`, `automations`,
`traces`, `outbound`, `skills`, `extensions`, `admin-config`, `dispatch`,
`operator`, `llm-admin`, `run-artifact`.

## The waiver stays, and a test now says so

`the_large_file_waiver_survives_the_charter_map` fails if
`// arch-exempt: large_file` is deleted **or** if it stops naming plan #5985 —
the plan number is the only thing that makes the waiver revocable, and
`scripts/pre-commit-safety.sh` requires it. This is the opposite disposition to
§6.4.14's `contribution.rs` waiver, which was deleted with the traces split;
the difference is that this plan has not landed. Without the test, the natural
next move for someone reading a charter map is to delete the waiver as
"handled", silently dropping the file out of ARCH-SPRAWL tracking.

## Owners are conceptual, not positional — forced by "not a split"

The obvious mechanism for a single file is banner-delimited regions, one per
owner. It is unbuildable here without moving code: `threads` holds **two**
regions (`create_thread`/`delete_thread` at `:265-303` and
`send_message`/`get_timeline` at `:591-654`), split by the admin-users block.
Making them contiguous is exactly the movement §6.4.15 forbids for this row.
The gate is therefore **item**-granular — every top-level `fn`/`struct`/`enum`/
`const`/`type` in `handlers.rs` and its `handlers/` submodules maps to exactly
one owner, positions irrelevant. Recorded because the next reader will reach
for banners first.

## Three placement calls (delegated authority)

- **The `*_activity_id` family splits three ways.**
  `product_capability_activity_id` and `product_surface_activity_id` are
  `dispatch` (the generic derivation every owner reaches);
  `extension_lifecycle_`/`llm_provider_upsert_`/`outbound_preferences_`/
  `admin_configuration_activity_id` go to the concern whose request fields each
  one reads.
- **`capability_failure_http_class` is `outbound`, not `dispatch`**, despite
  the generic name: it is the classification the outbound-preferences routes
  introduced, and every caller is in that owner. The promotion trigger is
  stated in advance — a second concern calling it moves it to `dispatch` —
  rather than argued later.
- **`get_attachment` is `attachments`, not `workspace-fs`.** Both serve bytes,
  but attachment identity is a thread-scoped ref rather than a mount path, and
  the path-scoping rules in `workspace-fs` do not apply to it. Keeping them
  apart is what stops a future path-scoping fix from being *assumed* to cover
  attachment downloads.

## Verification (map, not move)

| Check | Result |
|---|---|
| `git diff --stat <base> -- crates/ironclaw_webui/src` | **empty** — zero source lines changed, so the item roster is identical by construction |
| Charter coverage | **219 of 219** top-level items in `handlers.rs`, plus 5 in `handlers/run_artifact.rs`; 0 uncharted, 0 phantom, 0 double-claimed |
| Sub-owner count | **19** vs §6.9.1's floor of 11 (pinned by a test) |
| Unfiltered `cargo test -p ironclaw_webui --all-features` | 469 passed / 0 failed (**+4**, exactly the new gate) |
| `cargo clippy -p ironclaw_webui --all-features --all-targets -- -D warnings` | clean |
| `cargo fmt --check` | clean |

**Sabotage-proved in five directions**, each restored green: drop an item from
the map -> "1 handler item(s) have no sub-owner"; add a phantom item -> "no
longer exists"; claim an item twice -> "claimed by more than one"; delete the
`large_file` waiver -> the waiver test; add a brand-new uncharted handler to
the file -> "no sub-owner" (the real-world case). The gate self-guards against
going vacuous three ways: zero parsed rows, implausibly few walked items, and
zero submodule items collected (which would leave the `run-artifact` row
unchecked).

## What this does not do

It is not plan #5985 and does not shrink the file by a line. What it buys is
that #5985 inherits a decided seam list — each of the 19 rows is one candidate
module — instead of re-litigating the boundaries when the split is attempted.

This closes the WS6 module-charters row: all four clauses are now done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7179 August 4, 2026 21:14 Destroyed
@railway-app

railway-app Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-7179 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Aug 4, 2026 at 9:24 pm

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • staging
  • reborn-integration

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 784c497d-bd5f-45c6-82de-3cd39aef002b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added scope: docs Documentation size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Aug 4, 2026
@ironloopai

ironloopai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #7179

🔴 Failed

GitHub request failed

IronLoop could not complete a required GitHub request.

Automatic · PR opened · attempt 1 of 3 · failed after 2m 17s

Failure details
  • Repository: nearai/ironclaw
  • Base: ws/waves-0-4-batch at a651aa3
  • Head: ws6/charters-remainder at e2faa82
  • Created: Aug 4, 2026, 9:19 PM UTC
  • Updated: Aug 4, 2026, 9:21 PM UTC
  • Run: ce0bf8dc-2b74-403d-ac24-26115f94707a
  • Latest attempt: 1 · Completed · 0af8041f-9102-4ca4-a0c8-39b3f3fc1a3e
  • Failed during: GitHub writeback
  • Retryable: No
  • Failure: 416eb512-84eb-4fb0-8305-142d93d4e8b8

Base automatically changed from ws/waves-0-4-batch to main August 4, 2026 22:45
BenKurrek added a commit that referenced this pull request Aug 4, 2026
… for the moved auth files

The residue fold's auth-prompt re-export was pub for both names while
only product_auth_challenge_provider has an external path (the slimmed
wall); split to pub(crate)/pub matching the two consumers — the exact
shape #7186's original lines had, now pointed at ironclaw_auth. The
auth module-charter gate (from #7179) correctly demanded sub-owner
rows for #7189's two new files; both are product-auth surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@BenKurrek

Copy link
Copy Markdown
Collaborator Author

Superseded: this branch's content merged to main in the Waves 0–4 batch-2 PR #7181 (squash 57c685f). Verified before closing: this branch tip is an ancestor of ws/waves-0-4-batch-2, so every commit was carried. Closing as merged-via-batch, not abandoned.

@BenKurrek BenKurrek closed this Aug 5, 2026

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-7179 — e2faa820 Deployed Aug 4, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant