Close Wave 2: extension registry re-layer, include_str! kills, nested-tree coverage (WS2 + #7083) - #7094
Conversation
…WS2)
`ironclaw_extensions` moves `loops` -> `substrates`, which is PROPOSAL
§6.8.1's assignment for the crate. Four `LAYER_MATRIX_EXCEPTIONS` fall out
with it and the WS0 ratchet baseline drops 10 -> 6.
The four were one edge under four names: `host_runtime`, `capabilities`,
`mcp` and `scripts` — kernel/runtimes-tier crates — reaching the registry for
its manifest DTOs. None is waived and none of those edges is deleted; the
registry moved down to a layer every tier above may legally reach. §6.8.1
predicted two of them ("Layer substrates legalizes `capabilities ->
extensions` and `host_runtime -> extensions`"); it undercounted, and the
amendment records that.
The one edge that blocked the move is gone rather than waived. At
`substrates` a crate may name only `contracts`/`substrates`, and
`ironclaw_extensions` named `ironclaw_trust` (kernel) for exactly one type,
`TrustPolicyInput`, used by one method. Every field of that type is already
`host_api` vocabulary — `PackageIdentity`, `RequestedTrustClass`,
`BTreeSet<CapabilityId>` — and the type names no decision, ceiling, or
provenance, so it is requested-trust vocabulary like everything else in
`ironclaw_host_api::trust`. It moves there, which is §6.8.1's own
prescription ("`trust`-vocabulary via `host_api`"), and every consumer's
import is repointed rather than shimmed behind a re-export
(.claude/rules/type-placement.md).
Behavior-free: a type relocation, a layer declaration, and the exception
entries the layer change makes unreachable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t include_str! (WS2) The WS2 `include_str!` row's named targets — gmail, github, nearai-mcp — plus the two cross-crate manifest reach-ins #7018 added. **nearai-mcp gets its inventory module.** It was the one asset directory of twelve with no module in `ironclaw_extension_support::packages`, so `available_extensions.rs` embedded its manifest and three assets directly. Those embeds move to `packages/nearai.rs` beside every other package's, and `available_extensions.rs` consumes `nearai_bundle()`. It is deliberately **not** a `PACKAGES` entry, and the module says why at length: every other entry is a config-free `fn() -> PackageBundle`, but NEAR AI's shipped `[mcp].server` is a placeholder the host rewrites from the operator's LLM-admin bootstrap config. A config-free builder cannot produce that value, so the *embeds* live with the inventory and the *patch* stays with the endpoint authority. `PackageBundle::manifest_toml` is already a `Cow` precisely so the patched manifest is representable. This makes `ironclaw_extension_support` a normal dependency of `ironclaw_extension_host` instead of a `test-support`-only one. No new dependency cone: the binary already links it, and `extension_host` already built against it under that feature. **github and gmail fixtures are inlined.** Both were test-only reach-ins into a shipped product manifest. Each test needs one property — a v3 manifest asserting first-party trust; a no-channel manifest with an `[admin_configuration]` group — now spelled out in the test that needs it instead of borrowed from 200 lines it does not own. **The two cross-crate sites route through `bundled_packages()`.** slack and telegram carry adapter crates, so `extension_manager`'s manifest reach-ins were classified cross-crate. The tests' stated intent — project the *shipped* field set, not a drifting fixture — is unchanged; only the path changed, from a relative file path to the inventory that owns the bytes. Measured with the §11.2.7 scan: escaping sites 133 -> 128, cross-crate 19 -> 17. `REPORT_ONLY` stays `true` — the 17 survivors belong to three other owners (the support crate's own slack/telegram package crates, host_runtime's seven memory-provider embeds, and four test-only doc reach-ins in `operator`/`product`), none of which this row owns. The doc amendment names each. The `("…/available_extensions.rs", "nearai-mcp")` specificity carve-out is deleted with the embed it covered; the allowlist is shrink-only and staleness-checked, so leaving it would fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Coverage was structurally dark for every crate under `crates/extensions/`. `reborn_coverage_lcov.py` keyed on `crates/(ironclaw_[A-Za-z0-9_]+)/` — a literal path shape requiring `ironclaw_*` *directly* under `crates/` — and the `if match:` it gated guards the global aggregate as well as the per-crate table, so an unmatched record left both numerator and denominator. Five crate directories (~33.7k instrumented lines) contributed nothing to a gate reading `enforce = true`, and nothing said so: there is no `else`, no counter, no warning. This is a regression, not a never-worked condition. All five were flat `crates/ironclaw_*` until #7037 colocated packages three days ago; the module has one commit in its history and predates the move. The `[global]` floor was captured 2026-07-30, before that, so the denominator has silently shrunk under the gate that enforces it. A better regex cannot fix it. Four of the five directory basenames contain no `ironclaw_` at all (`packages/slack`, `telegram`, `mem0`, `memory-native` — PROPOSAL §5.1 names package directories by extension identity), and a greedy nested pattern mis-attributes an in-crate `src/ironclaw_*/` module directory to a crate that does not exist. So the fix is the one the *merge* script one step upstream already applies: anchor on the discovered crate inventory (`crate_tree.py`). The data was always in the merged lcov; only the aggregator was blind, and the two disagreeing is what made the hole silent. Three consequences worth stating: - **The accounting key is the crate directory basename** — what `crate_tree.crate_directory()` resolves by and what `classify-test-scope.sh` keys on. Every existing floor and exemption key is already a basename, so none churns; basenames also survive the family moves still ahead (`crates/ironclaw_llm` -> `crates/substrates/ironclaw_llm`). - **Separate workspace roots are excluded explicitly**, checked *before* the crate pattern. "Outermost wins" would otherwise attribute `packages/slack/wasm-src/` to `packages/slack/`, putting never-compiled guest code in a denominator. Same precedence `reborn_changed_coverage.py` applies. - **It fails closed.** No discoverable crate tree is now a refusal, not a percentage computed over an empty inventory — the WS10 rule this whole class of bug violates. Regression proof: six new cases (A6b/A6c/A6d/A6e, R19/R19b) covering a nested crate in the table *and* the aggregate, a non-`ironclaw` basename, a separate-workspace guest, a vendored third-party `crates/` subtree, the fail-closed refusal, and a floored nested crate passing and failing its covered-lines floor. The suite gains a shared fixture crate tree, because the aggregator now needs one — the old shape needed no tree at all, which is exactly why every case stayed green while 11 crates went dark. Local: 166/178, with the same 12 pre-existing macOS bash-3.2 `mapfile` failures in the C section that the tree has today (148/160 before this change). Floors for the newly-visible crates are captured separately, from a real coverage run — floors invented without a measurement would bake the hole in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…user migration `remove_retired_internal_installation` has had **no behavioural coverage since #6616**, which deleted `restore_removes_retired_slack_user_installation_without_catalog_entry` and replaced it with `assert_eq!(RETIRED_SLACK_USER_EXTENSION_ID, "slack_user")` — a constant compared to its own literal. That gap matters more than most: the branch runs on **every boot** and **destructively** deletes persisted installation rows, and its disposition is an open owner decision (PROPOSAL §12.11 D-I, escalated 2026-08-02, deliberately not ruled by the delegated-authority pass). D-I's recommended sequencing lists restoring this coverage as step (i), "required either way" — so it lands now, whichever way the owner rules, and the behavior is untouched. Extends the existing crate-integration suite rather than adding a file: it already drives `restore_extension_lifecycle_state` over a real `ExtensionInstallationStore` on a real `RootFilesystem`, which is the whole seam this branch lives on. Pins the ACTUAL behavior, including the parts that read as surprising: - Both port reads return `None` — `delete_installation` alone deliberately leaves the manifest projection authoritative, so the branch's second store call is load-bearing and the test says so. - **"Deleted" means tombstoned, not erased.** The v2 record survives with `removed_at` stamped, `removal_cleanup_pending` converged, and the embedded manifest retained; only the two legacy projections are hard-deleted. A test asserting erasure would pin a contract this code does not implement and would hide that the migration is recoverable evidence rather than data loss. - **The control**: a second, equally uncatalogued installation must survive. Deletion keys on the extension id, never on "the catalog could not resolve it". Both halves red-checked against the live tree: disabling the branch fails the removal assertions (and only this test); widening it to delete every catalog-miss row fails the control. Neither the branch nor any other production file is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rrections Amendments for the WS2 work in this PR, each quoting the text it replaces. **PLAN Wave 2 ✎ note** — its open-item list was stale within a day: strays landed as #7040, and package colocation, the telegram merge and the memory-provider move all landed as #7037. Four carry-forwards: the re-layer is two independent halves and only one was reachable; a *downward* re-layer is costed from the crate's own manifest, mirroring Wave 3's finding that an *upward* one is costed from its consumer set; a stale wave list costs a slot its first hour, so re-measure the list itself; and branch names collide across parallel worktrees — verify `git ls-remote` matches your tip before trusting a run attached to it. **PROPOSAL §6.8.1** — "two more W7 exceptions gone" is wrong by two. Four fall: `mcp` and `scripts` reach the registry for the same manifest DTOs `capabilities` and `host_runtime` do. Baseline 10 -> 6. The entry's own `Deps` line turned out to be the executable instruction for the one blocking edge. **PROPOSAL §12.11 D-A** — the ruling stands; its sizing does not. "The seam is narrow, which is why this is cheap" is true of `channel_host.rs` and false of the crate: twelve production files name `ironclaw_product`, and `ironclaw_host_ingress` is a second blocking edge D-A never named. Recorded as an amendment rather than a silent re-scope so the next slot costs it from evidence. Filed as #7092. **CHECKLIST WS2** — the `include_str!` row ticks with the per-owner breakdown of the 17 surviving cross-crate sites and why `REPORT_ONLY` cannot flip on them (#7093); the re-layer row records the half that landed and the half that did not, with the twelve-file measurement; the escalated §12.11 D-I row records that its own step (i) is done and the escalation is unaffected. **CHECKLIST WS10** — the path-keyed-gates row missed a sixth gate, one step down the same pipeline it audited (#7083). Two corrections to how that row framed the risk: fix a path-keyed gate along its whole pipeline, not at the file the audit opened; and the dark-verdict failure is not only about `git mv` — package colocation broke this one first, because a gate keyed on a name shape fails for any tree change, not just the scheduled one. Crate guides travelling with the change: `ironclaw_trust`'s AGENTS/CLAUDE/ CONTRACT stop claiming `TrustPolicyInput`; `ironclaw_extensions`'s AGENTS records its `substrates` layer and that it must not regain `ironclaw_trust`; `ironclaw_extension_support`'s AGENTS records why `nearai` is a package module that is deliberately not a `PACKAGES` entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
🚅 Deployed to the ironclaw-pr-7094 environment in ironclaw-ci-preview
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds a host-wired NEAR AI package bundle, moves ChangesExtension inventory and host wiring
Trust ownership and crate boundaries
Lifecycle restore migration
Dynamic LCOV crate discovery
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ironclaw_extension_support
participant ironclaw_extension_host
participant available_extensions
ironclaw_extension_support->>ironclaw_extension_host: provide nearai_bundle()
ironclaw_extension_host->>ironclaw_extension_host: patch MCP server endpoint
ironclaw_extension_host->>available_extensions: project patched manifest and assets
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔎 Review · PR #7094
Submitted review →Reviewed the complete trusted base-to-head comparison. No concrete correctness, security, architectural, maintainability, or test-coverage defects were found in the introduced changes. Automatic · PR opened · attempt 1 of 3 · completed in 1m 34s Run details
|
There was a problem hiding this comment.
🔍 Review complete · PR #7094
✅ No actionable findings
Reviewed the complete trusted base-to-head comparison. No concrete correctness, security, architectural, maintainability, or test-coverage defects were found in the introduced changes.
Validation and technical details
- Inspected all 38 changed files and surrounding implementation, including the extension-registry re-layering, TrustPolicyInput relocation, NEAR AI package embedding, lifecycle regression coverage, dependency-boundary updates, and nested-crate LCOV aggregation.
- Verified the trusted comparison refs resolve to base 0f897e9 and head 4c841a4 via the complete refs/ironloop/base..refs/ironloop/head diff.
git diff --check refs/ironloop/base..refs/ironloop/headcompleted without errors.scripts/ci/test-reborn-coverage.shpassed 166 of 178 cases, including every newly added nested-crate, separate-workspace, vendored-source, fail-closed discovery, and ratchet case. The 12 failures were confined to the pre-existing comment-script fixture group described in the supplied context.- The repository code graph was unavailable, so review tracing used live source and targeted
rgsearches as prescribed by repository guidance. - Rust test execution was unavailable because
cargois not installed in the review environment; source-level review included affected manifests, implementations, consumers, architecture tests, and test doubles. - Base:
main - Head:
w2-closeoutat4c841a4 - Run:
71b81903-7680-4b3c-9ad3-44bdd6bb5cf4
Un-masking accounting (principle 4)Unfiltered
Removed: zero. Every one of the 2811 base tests is present by name at head. The three additions are all new coverage this PR owes:
No test was edited for content. Two existing tests had a fixture source changed ( Not in this table: Exception-count methodCounted with Python over the text between i = s.index("const LAYER_MATRIX_EXCEPTIONS"); j = s.index("];", i)
s[i:j].count("LayerMatrixException {")
Size296 added / 76 deleted lines of production Rust. The rest of the +949 is self-test cases, the restored behavioural test, doc comments, and the target-architecture amendments. CI evidence
Read it per job, never at the roll-up — dispatch roll-ups are structurally red (#6978: |
Captured from this PR's own dispatch run 30865483401 at `4c841a4321`, **after** the aggregator fix. Capturing beforehand would have recorded zeros and pinned the hole shut, which is the one outcome #7083 exists to prevent. Four new `[[crate]]` entries — `ironclaw_extension_support` (82.64%, 6826 / 8260), `slack` (93.95%, 3697 / 3935), `telegram` (90.31%, 1435 / 1589), `memory-native` (82.85%, 2850 / 3440). None of the four lacked a floor because anyone judged it unworthy of one; they were invisible to the gate. All four were compiled, instrumented, and present in the merged tracefile the whole time. Keys are crate **directory** basenames, which is what the aggregator keys on and what every pre-existing entry already is — they coincide with package names only for flat `crates/ironclaw_*` crates. `mem0` is deliberately absent and the file says why: it compiles only behind the `memory-mem0` feature, which no coverage lane enables, so it contributes no instrumented lines and a floor would enforce nothing. `[global]` recaptured 85.11% / 375097 → **86.96% / 386885**. The old denominator never described the tree it was enforcing: #7037 landed on 2026-08-03 and four crate directories left both numerator and denominator silently, under `enforce = true`. Both numbers are read off the same `RATCHET PASS: global` line of the same `reborn-coverage-ratchet.sh` invocation that enforces this file, so the mapping is the enforcing mapping by construction — the existing comment's caution is about comparing *across* toolchains, which this does not do. `ironclaw_extension_host` recaptured 84.83% / 19907 / 23467 → **87.99% / 21605 / 24554**. It is the SOURCE side of a move (the NEAR AI embeds left for the package inventory), and a source floor is the one that silently stops describing its crate when code leaves it. Recorded honestly as a ratchet tightening rather than a repair: the denominator moved only −1.46%… +4.63%, below this file's own 5% materiality threshold, and both fields rose — partly because this PR also adds the retired-`slack_user` test the crate had been missing since #6616. Verified locally against the run's own `reborn-integration-merged.lcov`: `reborn-coverage-ratchet.sh` exits 0 with 22 `RATCHET PASS` and zero `FAIL`, and every observed figure matches the CI job line-for-line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_extension_host/tests/lifecycle_restore_contract.rs`:
- Around line 482-495: Update the removed_at assertion in the lifecycle restore
contract test to require a non-null JSON value, not merely the field’s presence;
use the Value access semantics already relevant to the neighboring
removal_cleanup_pending assertion so an explicit null fails while a valid
timestamp continues to pass.
In `@scripts/ci/lib/reborn_coverage_lcov.py`:
- Around line 72-80: Validate crate basename uniqueness while constructing the
inventory used by crate_directories, raising CrateTreeError when two distinct
directories share the same basename before aggregation or crate_key()
processing. Preserve existing directory ownership behavior for unique basenames,
and add a regression fixture covering two nested crates with the same basename.
- Around line 197-201: Restrict crate matching in the current_file handling
around crate_re.search to absolute LCOV paths contained within the resolved
IRONCLAW_REPO_ROOT, while continuing to accept relative repository paths. Add a
regression fixture using an actual discovered crate path under a vendored
directory and verify it is not attributed to that crate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 2b540ff8-60fd-4e78-be72-33fe6572078f
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (37)
crates/extensions/ironclaw_extension_support/AGENTS.mdcrates/extensions/ironclaw_extension_support/src/packages/mod.rscrates/extensions/ironclaw_extension_support/src/packages/nearai.rscrates/ironclaw_architecture/tests/reborn_dependency_boundaries.rscrates/ironclaw_architecture/tests/reborn_extension_specificity.rscrates/ironclaw_capabilities/src/host.rscrates/ironclaw_capabilities/src/trust.rscrates/ironclaw_capabilities/tests/support/mod.rscrates/ironclaw_extension_host/Cargo.tomlcrates/ironclaw_extension_host/src/active_publication.rscrates/ironclaw_extension_host/src/available_extension_import.rscrates/ironclaw_extension_host/src/available_extensions.rscrates/ironclaw_extension_host/src/channel_config.rscrates/ironclaw_extension_host/tests/lifecycle_restore_contract.rscrates/ironclaw_extension_manager/src/channel_config_product_service.rscrates/ironclaw_extensions/AGENTS.mdcrates/ironclaw_extensions/Cargo.tomlcrates/ironclaw_extensions/src/package.rscrates/ironclaw_extensions/tests/extension_contract.rscrates/ironclaw_host_api/src/trust.rscrates/ironclaw_host_runtime/tests/production_trust_contract.rscrates/ironclaw_host_runtime/tests/tool_surface_contract.rscrates/ironclaw_reborn_composition/src/factory/tests.rscrates/ironclaw_trust/AGENTS.mdcrates/ironclaw_trust/CLAUDE.mdcrates/ironclaw_trust/CONTRACT.mdcrates/ironclaw_trust/src/lib.rscrates/ironclaw_trust/src/policy.rscrates/ironclaw_trust/src/sources.rscrates/ironclaw_trust/src/tests/policy_contract.rscrates/ironclaw_trust/tests/public_api_contract.rsdocs/reborn/target-architecture/CHECKLIST.mddocs/reborn/target-architecture/PLAN.mddocs/reborn/target-architecture/PROPOSAL.mdscripts/ci/lib/reborn_coverage_lcov.pyscripts/ci/test-reborn-coverage-ratchet-cases.shscripts/ci/test-reborn-coverage.sh
Coverage evidenceNo PR-triggered lane produces a coverage verdict (#7036): Run: https://github.com/nearai/ironclaw/actions/runs/30865483401 The crates the fix made visibleCaptured after the aggregator fix, from that run's own
|
| entry | before (%, covered, total) | after |
|---|---|---|
[global] |
85.11, —, 375097 | 86.96, —, 386885 |
ironclaw_extension_host |
84.83, 19907, 23467 | 87.99, 21605, 24554 |
ironclaw_extension_host is the source side of a move (the NEAR AI manifest/asset embeds left for the package inventory), and a source floor is the one that silently stops describing its crate when code leaves it. Recorded honestly as a ratchet tightening, not a repair: its denominator moved +4.63%, below this file's own 5% materiality threshold, and both fields rose — partly because this PR also adds the retired-slack_user test the crate had been missing since #6616.
Local re-verification
Ran the enforcing script against the run's own artifact on this tree:
scripts/ci/reborn-coverage-ratchet.sh /tmp/…/reborn-integration-merged.lcov \
tests/integration/coverage-exemptions.toml tests/integration/coverage-floor.toml
→ exit 0 · 22 RATCHET PASS · 0 RATCHET FAIL
Every observed figure matches the CI job line-for-line.
⚠ One trap worth recording, because it produced confidently wrong output that looked like proof: the first gh run download into a directory that already held an artifact from an earlier run silently kept the stale file (error extracting … file exists) and exited 0. The ratchet then reported four bogus failures against the wrong tracefile. Always download into a fresh directory and diff the checksum before trusting it.
Side effect worth naming
Two entries in tests/integration/coverage-exemptions.toml — crates/extensions/packages/{slack,telegram}/src/lib.rs — validated fine but could never fire, because the aggregator dropped those paths whether they were exempt or not. They now actually exempt. Same net accounting; the difference is that the manifest now means what it says.
tests/integration/changed-coverage-exemptions.toml is untouched and has no stranded entries (its validator is already crate_tree-based, which is exactly why nobody noticed the lcov aggregator was blind).
…rtion Review triage on #7094. Two of three findings accepted; the third is declined with reasons, recorded in the PR thread rather than silently skipped. **Accepted — colliding crate basenames must be a refusal (Major).** `crate_key()` reduces a discovered directory to its basename, so two crate directories sharing one would silently fold into a single coverage bucket and a single ratchet floor. That is a *quieter* version of the bug this PR fixes: the merged number looks entirely plausible and nothing reports the merge. `crate_tree.crate_directory()` already refuses an ambiguous basename rather than picking one; this applies the same rule to the aggregation key, raising `CrateTreeError` so it lands on the existing fail-closed path. Unreachable on today's tree — all 65 basenames are distinct — and reachable the moment crates move under family directories, which is the next wave. Pinned by a new self-test case (A6f) with `crates/{domains,substrates}/ironclaw_threads`, asserting the refusal, that both colliding directories are named, and that the message says why a merged number is not offered. **Accepted — `removed_at` presence is not enough.** `Value::get` returns `Some(Value::Null)` for an explicit JSON null, so the tombstone claim would go vacuous if the serializer ever emitted one. Now rejects null explicitly, and the message says why presence alone was insufficient. **Declined — requiring absolute `SF:` paths to be contained under the resolved repo root.** Real in principle; wrong to apply here. `reborn-coverage-merge-lcov.sh` — which produces the tracefile this module reads, and which already filtered every record in it — anchors on the discovered inventory with the identical `(?:^|/)` form and no root containment. Adding containment to the consumer and not the producer re-creates exactly the producer/consumer divergence that made #7083 silent. The documented real-world case (`.../wasmtime-46.0.1/crates/wasmtime/`) is already excluded by inventory anchoring in both, and the scenario the finding describes needs a vendored tree that reproduces a full IronClaw crate directory path *inside an already-filtered lcov*. It would also require rewriting every fixture path in the suite, since they use synthetic absolute prefixes. Self-test 178 -> 181 cases; same 12 pre-existing macOS bash-3.2 C-section failures. Ratchet re-verified against the run's own artifact: exit 0, 22 PASS, 0 FAIL — the captured floors are unaffected (a test-file assertion and a script-level guard change no instrumented line). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review triageThree findings from CodeRabbit. Two accepted and fixed in ✅ Colliding crate basenames must be a refusal (Major) — acceptedCorrect and worth more than it looks. Unreachable on today's tree (all 65 basenames distinct); reachable the moment crates move under family directories, which is literally the next wave. Pinned by self-test A6f — ✅
|
`aggregate()` gained a `repo_root` parameter with the #7083 fix and the module header still advertised the three-argument form. Names the default resolution order and points at `crate_pattern()` for why the accounting scope is inventory-derived rather than a path shape. Comment only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Final stateHead Both automated reviews are resolved: Coverage evidence remains pinned to What is deliberately NOT in this PR
|
…llision Review catch (#7094): the vendored fixtures guarding the #7083 coverage fix (M5, A6d) pass because their `wasmtime` path matches no inventory entry — so they prove the inventory filter *runs*, not that it is contained. The adversarial input — a path outside the repository that repeats a discovered crate directory verbatim — was never in the suite. A fixture that passes because its input never reaches the code under test is exactly the failure mode this file exists to kill. A6g supplies that input (`.../foreign-1.0.0/crates/extensions/packages/slack/src/lib.rs`) and pins the property that actually protects the accounting: the producer (`reborn-coverage-merge-lcov.sh`, which filters every record the aggregator ever sees) and the consumer (`lib/reborn_coverage_lcov.py`) make the SAME call on it. Both are asked about the same fixture in parallel — chaining the consumer onto the merge's output would only ever compare it against a record the producer had already dropped, so a producer-only change would stay invisible. That mistake was made and caught here before the case landed. Restricting the consumer alone to paths contained under the resolved repo root was reviewed and not taken: the producer applies the identical `(?:^|/)` inventory anchor with no containment, and producer/consumer disagreement is what made #7083 silent instead of loud. Whichever way the rule goes, it goes in both halves at once. This case is what turns a one-sided change red. Sabotage-probed in both directions rather than assumed green: consumer-only "repo-owned" filter -> FAIL A6g: producer and consumer make the same call ... producer-only "repo-owned" filter -> FAIL A6g: producer and consumer make the same call ... and unsabotaged: 174/186, the same 12 pre-existing macOS bash-3.2 C-section failures as before the change (181 -> 186 cases). Not reachable from the real pipeline as it stands, measured rather than asserted: `cargo llvm-cov` emits only workspace-member sources, so every `SF:` record in a lane tracefile already lives under the checkout root — 1067 of 1067 per lane and 1139 of 1139 merged, read off run 30865483401's own artifacts, with zero records dropped by the inventory filter and zero carrying a `/crates/` segment anywhere but the repo-root position. The guard is for the day that stops being true. Test-only. No production behavior changes and no instrumented line moves, so the coverage floors captured for this PR are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees.
…e-baseline Caught in review of #7139. Both ledgers still recorded #7117's measurement, `Extension-specificity allowlist **127 → 125**`, taken against `origin/main` @ `1e2a294083`. #7094 then deleted an entry on `main` (127 → 126), so the same two net removals land on **124**, which is what the shipped baseline says. This is the cross-slice-number failure mode the consolidation exists to catch, one layer down: the code was corrected in 811bfed and the prose was not. Both amendments quote the text they replace and record the method — read off the ratchet's own failure message with the baseline temporarily set to 0, never counted by eye. No checkbox state changed.
…h deletion `tests/integration/coverage-floor.toml` requires a same-PR floor update in both directions, and names this exact case: "shrinkage (a legitimate code+test deletion lowering covered lines, even when the denominator barely moves)". The `ironclaw_extension_host` entry was captured two days ago on a tree that still had the retired-identity boot migration — its own rationale says the figure rose "partly because this PR also adds the retired-`slack_user` migration test". This PR deletes that branch by owner ruling, and those 29 production lines were *fully covered* by exactly the test deleted with them. Left alone, `floor_covered_lines = 21605` with its default 20-line tolerance would red the next dispatch on a floor that no longer describes the crate — the same class of staleness #7083 just fixed from the other direction. Deducted 25 instrumented lines from both fields (21605 -> 21580, 24554 -> 24529), one more than the ~24 measured, so a rounding difference cannot red the lane. `floor_percent` 87.99 -> 87.97: removing fully-covered lines barely moves a ratio. **Labelled an adjustment, not a recapture**, in the file itself. No PR-triggered lane produces a coverage verdict (#7036), so these could not be read off this PR's own run; the block says in as many words that the next dispatch should recapture and replace it. The arithmetic is written out so a reviewer can check it rather than trust it. `scripts/ci/test-reborn-coverage.sh`: 174/186, with the same 12 pre-existing macOS bash-3.2 failures in the comment-script section that `main` has today (#7094 documented the identical set; CI runs bash 5). The ratchet cases this edit actually touches — R19/R19b, covered-lines floors — pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both real. **`WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE`: recounted, not picked.** #7143 set **125** measuring its own branch; this branch had **124** measuring its own. Neither is right for the union: #7143 also *deleted* an entry (`lifecycle_restore.rs`/`slack`) this branch never saw, so the merged list is lower than either side. 126 on `main` after #7094, minus this branch's two net vendor-config removals, minus #7143's one, is **123** — which is what the ratchet reports with the constant temporarily set to `0` (`ALLOWLIST grew to 123 entries`). Read off the compiler, never by eye: a paren count over the literal answers 142, because the entries' comments contain parens. Both sides' doc amendments are kept and a third records the union recount, flagging #7141/#7152 (#7147) so whichever merges last recounts rather than inheriting 123. **`PROPOSAL.md` was a pure append collision** (empty merge base): #7143 adds the D-I owner ruling, this branch adds §12.12 D-K. Both survive, in document order — the D-I blockquote attaches to the entry directly above it, §12.12 opens the new section before §13. Verified to the #7018 standard: **19 files differ from this branch's pre-merge tip, and all 19 are files `main` touched — zero unexplained.** Across both ledgers, 45 lines #7143 added and 90 lines this branch added are all present. `LAYER_MATRIX_EXCEPTIONS` is 6 on both sides and the file is untouched by us. The widened driver-boundary gate is **byte-identical to the pre-merge tip** — it did not regress to the `take(module_start)` form that let a nested `postgres/pool.rs` leak pass silently. Re-sabotaged after the merge: the nested leak still fails the gate naming `pool.rs:1`.
…te the eight doc-truth corrections (nearai#7155) * test(architecture): itemize extension_host's product references as a frozen ledger The products -> loops re-layer has been sized five times from proxies and was wrong five times (D-A's one-file seam, nearai#7092's twelve files, three more since). The trait residue is trait-shaped and cannot see constants, free functions, or inline concrete construction; the manifest biconditional sees the sum but only as a boolean. This adds the itemization: EXTENSION_HOST_PRODUCTION_FILES_STILL_NAMING_PRODUCT — exact-match in both directions, shrink-only under a baseline ceiling, one reason per file, on a whole-token crate matcher (the raw-substring helper would count ironclaw_product_contracts importers). A ledger<->manifest consistency assert keeps the itemization and the biconditional agreeing about whether the edge exists, so the ledger cannot read empty while the manifest still carries the dependency. Sabotage-verified before trusting it: a planted production file naming ironclaw_product reds the gate naming that file; a planted stale row reds the stale direction; comment and string-literal mentions do not register (channel_delivery.rs and skill_learning.rs are the standing comment-only exclusions, and channel_subject_routes.rs's usage is cfg(test)-only). Part of the WS2 re-layer re-scope (nearai#7145). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): amend D-A — re-cite its precedent, record unstarted execution The ruling's two measured legs stand. Its option-(b) refutation cited ChannelWorkflowStateFactory as the in-file precedent; measured, that trait is a sole-impl same-file convenience no architecture test names — the load-bearing precedent is the landed nearai#7004 operator inversion and the ten INVERTED_PORT_IMPLEMENTORS ports, so the amendment re-cites it. Also records that the factory port exists on no ref (decision, not partial execution; shape still open) and that the residue is now mechanically itemized by the reference ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): correct the Wave 2 closeout's twelve-file sizing Six of the twelve were re-export repoints, executed in nearai#7143. The enforced remainder is four reference classes (trait residue, adapter-registry, product free functions, D-A assembly), now itemized mechanically by the reference ledger, with the inventory carried by nearai#7145. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): correct the Wave 3 milestone and the W7 label reading The milestone was wrong three ways: the start was 13 (live register is 6); the ratchet is ceiling-only and pins nothing; and zero is WS12's gate, not this wave's reachable exit — the lane edges are nearai#7067's (whose measurement refutes the WS3 mcp row's vocabulary premise) and conversations->turns is WS5's. Also records that removes_in=W7 is a retired July-train label (nearai#5852 era, introduced 2026-07-09), not Wave 5 or WS7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): give conversations->turns an owning WS5 row; key the register to it The exception's removal condition lived only inside WS1's verify-row explanation — no row owned it, which is how a milestone silently expires (its removes_in=WS5 date already passed once without it falling, as PROPOSAL 8.3's 2026-08-02 amendment records while asking for exactly this re-milestone). Adds the owning WS5 slice row, re-keys the register entry to it, re-keys host_runtime->extension_support to WS3, documents that W7 is the retired July-train label (not Wave 5 or WS7), and marks the WS5 product-narrows adapter_registry clause as a prerequisite of the extension_host re-layer (nearai#7145). The two entries nearai#7141 deletes and the two it re-keys to nearai#7067 are deliberately left untouched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): refute the stale 'delete reasoning.rs (dead)' claims Sections 9 (row 29) and 12.4 still said delete-it-outright while WS8's own execution (nearai#6964) deleted only the dead half and the surviving module is live on main (mod reasoning; + re-exports in llm/src/lib.rs). Acting on the rows as written would have deleted production surface. 6.4.13's own line is amended by the in-flight nearai#7128 and deliberately not touched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): amend 8.3 — row 7's blocker refuted by nearai#7067; live register is 6 Row 7 promised the lane->resources edges dissolve as vocabulary; nearai#7067's measurement (raised on nearai#7065) shows the vocabulary is already in host_api::resource and imported from there — the real holders are ResourceGovernor (3 of 10 methods used) and the ResourceError cone, whose relocation is an authority carve-out. Also refreshes the live register to 6 post-nearai#7094 and notes the conversations->turns re-milestone landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…earai#7117, nearai#7106, nearai#7099, nearai#7101, nearai#7128) (nearai#7139) * refactor(loop-host): move system-prompt content out of the composition root (WS6) CHECKLIST WS6 "Composition behavior evictions" — the `system-prompt content → owning prompt asset` clause. PROPOSAL §6.10.1 lists it among the items still resident in `ironclaw_reborn_composition`; `families/app.md` already says "prompt content of any kind" never belongs to the app family. The four assets move from `ironclaw_reborn_composition/assets/prompts/` to `ironclaw_loop_host/prompts/`, beside the five prompt assets that crate already ships and beside `identity_context.rs`, whose `HostIdentityContextSource` is what puts them in front of a model. `system_prompt_assets.rs` exports them as `pub const`; composition consumes the consts instead of `include_str!`. Resolved owner is the **loop** half of "loop/product owner": the port is loop_host's, and loop_host already owns `prompts/`. What deliberately did *not* travel: the seeding/validation of the on-disk, user-editable `SYSTEM.md`. That is boot-time `std::fs` work on a real host path and `ironclaw_loop_host` has zero `std::fs` uses — moving it would put host-path I/O into a loops crate. Composition keeps assembly + seeding. The runtime storage path `system/prompts/default-system.md` is unchanged; it is where existing installs' user-edited file lives, so renaming it would be a behavior change, not a move. Enforcement (new, in the same diff): `reborn_composition_boundaries.rs::composition_root_embeds_no_prompt_content` fails on either half of the debt — a re-added `include_str!("….md")` in composition source, or a re-added shipped `.md` asset under the crate that is not crate guidance. Sabotage-checked both halves independently. It is keyed on markdown, not on `include_str!`, so `builtin_capability_policy.toml` (config-as-data, composition's charter) is untouched. Un-masking: - `ironclaw_loop_host` 803 → 806 tests; the diff of the unfiltered `--list` rosters is exactly the three new `system_prompt_assets::tests::*`. - `ironclaw_reborn_composition` 928 → 928; roster diff is empty. - No existing test edited. Docs corrections, each quoting the text it replaces: - CHECKLIST WS6 + PROPOSAL §6.10.1: the `local_dev` misnomer's "one residue: the local variable at `runtime.rs:3016`" is wrong twice. The variable is at `runtime.rs:3095`, and `local_runtime` appears 191 times in composition's `src` — including six public API symbols, the public type `RebornLocalRuntimeIdentity`, and an assembly struct field. `reborn_standalone_typename_ratchet` stayed green because it governs *type* names only. Tracked as #7098 as a pure-rename PR, not folded in here. - PROPOSAL §2: `root/default_system_prompt.rs` is re-described as assembly + seeding now that its content assets are gone. - `families/loop.md` + loop_host `AGENTS.md`/`CLAUDE.md` record the new owner and the enforcing test. Refs #7098 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(ws6): fail-close the markdown ownership gate; fix two stale doc measurements Addresses both CodeRabbit threads on #7099. Both were right; verified before fixing, and each fix is sabotage-checked. **1. The markdown ownership gate had three false-negative paths.** - `include_str!` / `include_bytes!` were matched per *line*, so a `rustfmt`-wrapped invocation — `include_str!(\n "…/some-prompt.md"\n)`, which is what the formatter produces for a long path — evaded the scan entirely. Replaced with `markdown_include_sites()`, which scans complete invocations across line breaks, plus four unit tests including the multiline regression case. Verified by planting a multiline `include_str!("../../AGENTS.md")` in composition source: the gate now fails and names the flattened site. - `markdown_assets()` skipped unreadable directories and entries with `let Ok(..) else { continue }`, so "the walk could not see it" and "there is nothing there" looked identical to an ownership gate. It now panics on a failed `read_dir`, entry, or `file_type`. - Extensions were compared case-sensitively; `.MD` slipped past. Now `eq_ignore_ascii_case`, on both the extension and the guidance-file exemption. Also added a scanned-file floor (>= 50 sources) so a broken walk fails instead of reporting clean — the same "measured scan" idiom `reborn_registration_pipeline_boundary.rs` uses. **2. PROPOSAL §2.4 still carried the pre-correction `local_runtime` measurement.** Line 81 said `runtime.rs:3016` and "the local *variable* name survived" while §6.10.1 (line 670) already carried the correction — a document contradicting itself. §2.4 now cites `runtime.rs:3095`, states the 191-occurrence scope, and points at §6.10.1 and #7098. The one surviving `:3016` in the file is inside the verbatim quote of the text being replaced, which is deliberate. **Also in this commit — two WS6 rows re-measured, because they would otherwise have been redone.** `RebornRuntime` slimming, at `origin/main` @ `0f897e9366`: - "~40 `_for_test` accessors behind `test-support`" is **already done**: `runtime.rs` has 38 and zero are ungated; crate-wide 149, and all 13 without their own attribute sit in a module gated at its declaration site (`lib.rs:64-65`, `factory.rs:1388-1389`). No `_for_test` function compiles into a production build. - "delete the dead `product_live_adapters` export block" is **refuted**: it is live cross-crate test-support API. `ironclaw_product` declares `ironclaw_reborn_composition = { …, features = ["test-support"] }` as a dev-dependency and its `tests/support/planned_agent_loop.rs` imports seven of the eight names; composition has a suite dedicated to them. Deleting it would strand a sibling crate's test support. Only the third clause (re-export wall vs. snapshot) is still live. `crates/AGENTS.md`'s `ironclaw_loop_host` row now names the prompt assets and says the seeding stays in the composition root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): stop the Reborn test planner failing closed on the crate-family map `crates/AGENTS.md`, `crates/Architecture.md` and `crates/README.md` sit directly under `crates/` and belong to no package directory. The planner skips markdown only at the repository root (`path.endswith(".md") and "/" not in path`), and `IGNORED_PREFIXES` does not include `crates/`, so all three fell through to the fail-closed package-resolution arm: Reborn PR test planner failed: unmapped crate path: crates/AGENTS.md That failed `Detect Reborn test scope`, which failed the `Tests (Reborn)` roll-up — on **any** PR that edited them. Hit while updating `crates/AGENTS.md` in this branch; filed as #7100 with the blast radius. It blocks the exact maintenance the house rule asks for: `crates/AGENTS.md` is the crate-level map WS11 requires updating when crate ownership changes, and `crates/Architecture.md` is already recorded in PROPOSAL §2 as carrying a stale `build_reborn_services` reference that WS11 has to fix. Fix: classify markdown *directly* under `crates/` as crate-family guidance with no test surface, ahead of the package-resolution arm. Deliberately narrow: - markdown *inside* a package directory is untouched and stays package-owned (`test_nested_crate_markdown_remains_package_owned` still passes); - anything non-markdown directly under `crates/` still falls through to the explicit-decision arm, which is the point of that arm. Two regression tests beside the existing nested-markdown one: all three family-map files plan to `mode=none` with no changed packages, and `crates/unexpected.txt` still raises `unmapped crate path`. Sabotage-checked by breaking the new arm's path-depth test — 3 errors, restored to green. Verified end to end: the planner run over this branch's own 14-file diff now succeeds and selects `ironclaw_architecture`, `ironclaw_loop_host`, `ironclaw_reborn_composition`. 44/44 planner tests pass. Fixes #7100 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert(ci): back out the planner fix — #7084 already carries it, better I hit `Reborn PR test planner failed: unmapped crate path: crates/AGENTS.md` after adding one line to the crate-family map, diagnosed it as an unhandled fail-closed arm, filed #7100 and fixed it. Then I checked whether other open PRs touch those files — #7084 and #7065 do — and expected them to be red for the same reason. **They are green**, which refuted the "any PR that edits them fails" framing and sent me to look at why. #7065 branched before the planner existed (#6952). **#7084 already modifies `scripts/ci/reborn_pr_test_plan.py` and already fixes this**, in the same function and the same arm I was editing: if package is None: # Markdown that belongs to no crate is prose, in the same class # as `docs/` and `.claude/` … Depth-independent by construction, # so it keeps holding for `crates/AGENTS.md` and for a future # `crates/<family>/AGENTS.md` after the WS7 family move. if path.endswith(".md"): continue with a regression test (`test_markdown_owned_by_no_crate_is_prose`) covering `crates/AGENTS.md`. Their rule is **strictly better than mine**: mine keyed on `path.count("/") == 1`, which would silently stop covering the file the moment WS7 moves crates under family directories. Theirs is depth-independent. So this reverts my planner change and its two tests, and drops the `crates/AGENTS.md` edit that provoked it — #7084 is on the do-not-disturb list and this would have collided with it line-for-line. The guidance follow-up is recorded on the CHECKLIST WS6 row with the exact text owed and the condition (#7084 landing) that unblocks it. #7100 is updated to say it is already fixed rather than left implying open work. Everything else on this branch is unchanged: the system-prompt asset eviction, the markdown ownership gate, and the doc corrections all stand. Refs #7100, #7084 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(ws6): statement-bounded include scan; fail-close the Rust-source walk Second CodeRabbit round on #7099. Both findings verified against the code before fixing; both were right. **1. `markdown_include_sites` missed a nested argument macro.** Confirmed: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/prompt.md")) The first-`)` scan stopped at `(concat!(env!("CARGO_MANIFEST_DIR")` — before the path — and reported clean. Rather than teach the scan balanced-delimiter parsing (which then also owes string-literal, raw-string and comment handling — each an independent silent leak), the span is now bounded by the **statement**: from the macro-name occurrence to the next `;`. Whatever the nesting, spacing or line breaks, the path literal is inside that span. It also requires the name to be a whole identifier followed by optional whitespace and `!`, so `my_include_str!` and a plain `include_str_path` variable are not findings. It over-reports rather than under-reports — a comment mentioning `.md` inside an include statement is flagged — and says so. A false positive is a loud failure a human clears in one line; a false negative is prompt content silently back in the composition root. Seven scanner unit tests now: single-line, multiline, nested argument macro, whitespace before `!`, a comment inside the argument, uppercase `.MD`, non-markdown (`builtin_capability_policy.toml`, which must stay clean), and similar identifiers. Sabotage-checked against the real crate with the exact nested form above: the gate fails and prints the flattened site. **2. The file-count floor did not close the `rust_sources` hole.** Right — it only catches an empty-ish walk; an unreadable directory *after* 50 files still passed silently. `rust_sources` now panics on a failed `read_dir` and a failed entry, matching what it already did for unreadable file contents — this is consistency inside that function, not a new policy, and it hardens the three other tests in the file that share it. The floor is kept and re-justified for the case that stays silent even so: a walk that reads a perfectly good directory which is no longer the crate. After the WS7 family move relocates `crates/…` under family directories, a stale path can resolve to something small and readable rather than erroring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(arch): restore four tests my previous commit silently deleted `fe641b7709` rewrote `reborn_composition_boundaries.rs` by replacing a *span* between two doc-comment anchors. The two anchors were at opposite ends of the file — `markdown_include_sites` near the top, `markdown_assets` near the bottom — so the replacement swallowed everything between them: - `composition_public_pub_use_surface_matches_snapshot` - `extension_host_cluster_stays_internal` - `reborn_binary_main_is_thin_bootstrap` - `composition_crate_installs_installed_tier_only_through_registrar` - helpers `composition_src_path`, `extract_pub_use_surface`, `has_module_decl`, `is_test_module_file`, `strip_test_module` It compiled and the file's own suite went green, because each deleted test left with the helpers only it used — which is exactly why "the suite passed" is not evidence. It was caught by diffing the function roster against `origin/main` rather than by a test, and by the commit's own −301/+114 line count. This restores the file from `origin/main` and re-applies the change with targeted edits instead of a span replacement. The roster is now **purely additive** against `origin/main` — 9 functions added, **0 removed**, verified with `comm -23`: - `composition_root_embeds_no_prompt_content` (the gate) - `markdown_include_sites`, `markdown_assets` (helpers) - 8 scanner unit tests 7 tests on `origin/main` -> 16 here. Both halves of the gate re-sabotage-checked after the restore: a nested `include_str!(concat!(env!(…), "…default_system.md"))` fails it, and a shipped `assets/prompts/s.MD` fails it. Also fixes what `Fast deterministic checks` caught on `fe641b7709`: clippy's `items after a test module` (the scan's test module now sits at the end of the file, after every helper) and two `doc list item without indentation` warnings (the doc comment is prose, not a list). `cargo clippy -p ironclaw_architecture --benches --tests --examples --all-features` is clean. The substance of `fe641b7709` is unchanged and still stands: statement-bounded include scanning, and `rust_sources` failing closed on unreadable directories and entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): skip Rust trivia when bounding the include statement Third CodeRabbit round on #7099. Both findings verified, both real, both fixed. **1. `.find(';')` could end the span before the path.** A semicolon inside a comment above the argument (`// see the note; below`) or inside the path literal itself (`"../a;b/prompt.md"`) terminated the scan early — and an ownership gate that ends early goes quiet, which is the failure mode this gate exists to prevent. `statement_end_after` now finds the first `;` that actually terminates a statement, skipping line comments, nestable block comments, normal strings with escapes, raw strings with any number of hashes, and char literals (while not mistaking a lifetime for one). It only has to locate a delimiter, not parse the expression, which keeps it ~50 lines. Three new tests, and the third is the one that keeps the fix honest: the span must still *stop*, or a markdown path in the **next** statement would make every non-markdown include a false positive. Sabotage-checked against the real crate with a semicolon-in-comment form — the gate fails. **2. `path.is_dir()` swallowed metadata errors in `rust_sources`.** Right: `Path::is_dir()` returns `false` on an error, so an unreadable directory left the walk silently. It now asks `entry.file_type()` and panics, matching `markdown_assets`. **Not done, with a reason rather than silently:** the suggested regression test for "an unreadable directory beneath an otherwise readable workspace". The only portable way to create one is `chmod 000`, which does not make a directory unreadable for `root` — and the CI containers run as root, so the test would pass locally and be vacuous in CI. A test that cannot fail where it matters is worse than none. The invariant is instead carried by construction: every read in both walks is `unwrap_or_else(panic!)`, with no `let Ok(..) else` and no `is_dir()` left in either. `reborn_composition_boundaries.rs` is 7 tests on `origin/main` -> 19 here, and the function roster is still purely additive (`comm -23` empty). Full `ironclaw_architecture` suite green; clippy `--all-features` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): reject symlinks in both composition ownership walks Fourth CodeRabbit round on #7099, and it is right. `DirEntry::file_type()` reports the **link's** type without following it, so a symlink pointing at a source directory is neither `is_dir()` nor an `.rs` file: both walks stepped over the entire subtree and the gate reported clean on source it never opened. Same "uninspected reads as absent" failure the fail-closed reads added in the previous round exist to prevent — one level further out. `reject_symlink` now panics for either walk, naming the path and the two ways forward. Rejecting is chosen over following deliberately: following needs canonical-root containment plus cycle detection to be safe, and neither scanned crate has ever contained a symlink (`find crates/ironclaw_reborn_composition/src -type l` is empty). The panic is where that decision gets made on purpose rather than silently. Regression test `a_symlinked_subtree_fails_the_walk_instead_of_being_skipped` builds a tempdir with a real source directory plus a symlink to it and asserts **both** `rust_sources` and `markdown_assets` panic. `#[cfg(unix)]`, since the workspace has a Windows lane and `std::os::unix::fs::symlink` is not portable. Sabotage-checked: commenting out both `reject_symlink` call sites turns the test red ("a symlinked subtree must fail the walk, not be skipped"); restoring them returns 20/20. `reborn_composition_boundaries.rs`: 7 tests on `origin/main` -> 20 here, roster still purely additive (`comm -23` empty). Full `ironclaw_architecture` suite green; clippy `--all-features` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(event-store): stop leaking the Postgres driver in the public API (WS6) CHECKLIST WS6 / PROPOSAL §6.3.2: "stop leaking `deadpool_postgres::Pool` in the public API (wrap)". `ironclaw_reborn_event_store`'s public API now names `deadpool_postgres` zero times; the driver survives only inside its private `postgres_backed` module, which is where the TLS policy and pool construction §6.3.2 assigns this crate actually live. "Wrap" turned out to be three things, not one. **1. Half the leak was dead code, so it is deleted rather than wrapped.** `open_postgres_pool` and `open_postgres_pool_with_max_size` had exactly one caller each — composition's `open_reborn_postgres_pool` and `open_reborn_postgres_pool_with_max_size` — and those two had **zero** callers anywhere in `crates/`, `tests/`, `tools/` or `scripts/`. A four-function pass-through chain across two crates whose only remaining effect was to publish a third-party type in two public APIs. **2. The survivors take a carrier.** `open_postgres_pool_with_tls_options` returns `ironclaw_filesystem::PostgresConnectionPool` and `RebornEventStoreConfig::PostgresPool` holds one. The newtype lives in `ironclaw_filesystem`, not in event_store, for two reasons: it is the only crate `event_store`, `auth` and `composition` can all name without a new dependency edge, and that crate *is* the Postgres substrate, so the driver is chartered there (§11.2.6) rather than leaked. It is a carrier, not an abstraction — `driver()` / `into_driver()` exist for code that runs SQL — and it deliberately has no `Deref` (an implicit unwrap re-admits the driver into a signature unnoticed) and a hand-written `Debug` that renders nothing. The driver's own `Debug` prints its `tokio_postgres::Config`, which redacts the password (`tokio-postgres-0.7.16/src/config.rs:766-776`) but still prints `user`, `dbname`, `host`, `hostaddr`, `port` and `ssl_mode` — deployment topology that a derived `Debug` on any holder would inherit. **3. Stated residue: composition still names the driver, by charter.** §11.2.6 makes it "the one app-layer crate permitted a database driver", and it needs the raw pool for `PostgresRootFilesystem::new` and `CredentialRefreshLeaderLock::for_postgres`. It unwraps the carrier at exactly one site (`factory.rs`, `open_postgres_pool_from_source`). Pushing the carrier further down means changing `PostgresRootFilesystem::new`, which has **13 call sites across 5 crates plus `tests/integration/support/builder.rs`** — a separate test-wide slice, not this row. Recorded in both docs rather than left implied. **Enforcement (new file, lands with the change):** `crates/ironclaw_architecture/tests/reborn_persistence_driver_boundary.rs` - a shrink-only ratchet on which crates may hold a *normal* `deadpool-postgres` dependency (8 today, read from `cargo metadata`, not by eye), and - a scan proving event_store names the driver only below its private `postgres_backed` module — including that the module stays private, since a `pub mod` would silently defeat the scan. Both halves sabotage-checked: a planted `pub fn sabotage(p: deadpool_postgres::Pool)` fails the second and names the line; a planted `deadpool-postgres` dep on `ironclaw_projects` fails the first and names the crate. **Un-masking** (unfiltered `--list`, name-by-name, against `origin/main` in a clean baseline worktree): - `ironclaw_reborn_event_store` 71 → 71, roster identical - `ironclaw_reborn_composition` 928 → 928, roster identical - `ironclaw_filesystem` 296 → 296, roster identical - `ironclaw_architecture` 206 → 208, exactly the two new gate tests Deleting the four dead functions surfaced nothing, which is the evidence they were dead. No existing test edited. Guidance travels: `ironclaw_filesystem/CLAUDE.md` documents the carrier and its two deliberate omissions; `ironclaw_reborn_event_store/AGENTS.md` records that the driver cone is owned but not exported, and names the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): reject a symlink handed in as the walk root too Fifth CodeRabbit round on #7099, and right again — the previous fix closed the hole one level too late. `reject_symlink` only sees entries `read_dir` yields, but both walks push their **root** onto the stack before that ever runs, so a symlinked root was followed to its target silently. The regression test I added covered symlinked children only. `reject_symlink_root` now validates the root with `symlink_metadata` (which does not follow) before either walk starts, reusing the same rejection so the message and the policy stay in one place. The regression test is extended rather than duplicated: it now also symlinks a root and asserts **both** `rust_sources` and `markdown_assets` panic on it. Sabotage-checked — removing the two `reject_symlink_root` calls turns it red ("a symlinked walk root must fail rust_sources, not be followed"). Roster still purely additive against `origin/main` (`comm -23` empty); 20 tests in this file; full `ironclaw_architecture` suite green; clippy `--all-features` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): widen the driver-boundary scan past its two blind spots Three CodeRabbit threads on #7101, all naming the same real defect from different angles, and all correct: `take(module_start)` stopped the scan at the `mod postgres_backed` **header**, so the gate was strictly weaker than the three places documenting it claimed. Two blind spots, both now sabotage-fixtures rather than prose: - anything **after** the module body in `lib.rs` — a `pub fn` there naming `deadpool_postgres::Pool` kept the gate green; - **every sibling file** in the crate (`coalescing_sink.rs`, `durable_log.rs`), which the scan never opened at all. The scan now reads every `.rs` file under `crates/ironclaw_reborn_event_store/ src/` minus the brace-matched **body** of the private module. The brace match is trivia-aware (line comments, nestable block comments, strings, raw strings, char literals) so a `}` inside a literal cannot end the body early and silently drag the rest of the file into the exempt range — the same failure class one level down. It panics on an unterminated body rather than exempting to end-of-file, and asserts it saw at least two source files. Four unit tests on the brace matcher: a mention inside the body is exempt, a mention after the body is not, a brace in a literal does not end the body, and a file without the module has no exempt range. Sabotage-checked against the real crate for both former blind spots: - `pub fn sabotage_after_body(p: deadpool_postgres::Pool)` appended to `lib.rs` -> fails, naming `lib.rs:2215` - the same appended to `coalescing_sink.rs` -> fails, naming `coalescing_sink.rs:321` Also corrected the prose the reviewer flagged as over-claiming, in both places: `ironclaw_reborn_event_store/AGENTS.md` and the CHECKLIST WS6 row now say "module **body**" and state that the scan covers every file in the crate, with the earlier revision's blind spots recorded rather than quietly fixed. Clippy `--all-features` clean (the scan's test module moved to the end of the file for `items after a test module`); full `ironclaw_architecture` suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(extractors,observability): typed extraction failures and a one-dependency latency crate (WS6) CHECKLIST WS6 row "extractors: typed error across the boundary + delete caller-less `extract_text` (§6.4.10); observability: `json_value_bytes` eviction (§6.2.5)". Measurements from #7102. ## extractors (§6.4.10) Failures now cross the boundary as `ExtractionError`, not `String`, at both public sites (`DocumentExtraction::Failed` and `extract_document_text_by_filename`). Two variants: `UnsupportedType { mime }` (nothing was attempted) and `NotExtractable { detail }` (an extractor ran and could not produce text). `Display` renders the classification and nothing else; `Debug` carries the payload. That is not a shape change. The invariant — "carries the error reason for logging only; callers render a model-safe marker, never this string" — lived as a doc comment on one of the two boundary sites, and the *other* one leaked: `ironclaw_extension_support`'s `read_file` interpolated the raw extractor diagnostic into a model-facing safe summary (`coding/file.rs:325-329`) while carefully redacting the path one argument earlier. With `Display` content-free that call site is safe unchanged. Its regression test sits at the call site, not on `Display`, because the wrapper composing the summary is what leaked. `extract_text` and `TRUNCATION_MARKER` were both `pub` with zero external callers; both are private now. The row only named the first. The second mattered more: `ironclaw_agent_loop` and `ironclaw_mcp` each declare their own `TRUNCATION_MARKER` with a different value, so it must be resolved by crate, not by name. The census is exact — no crate writes `use ironclaw_extractors::…`, so a full-path grep is complete. The private ZIP-safety enum was renamed `ExtractionError` -> `ZipEntryError` to free the natural name. ## observability (§6.2.5) — delegated ruling, PROPOSAL §12.12 D-K `json_value_bytes` and its `JsonByteCounter` are localized into the two consumers; `serde_json` leaves the manifest with them, so the crate now holds exactly one dependency, `tracing`. The row's stated reason ("gravity-well hygiene") was wrong; the ruling survives on a measured one. Of five call sites in extension_support, three feed `ResourceUsage::set_output_bytes` — resource accounting, not a trace field — so "it is a latency helper, in charter" is false. And sharing bought no invariant: `output_bytes` is already computed three different ways in production (this counter, `output.stdout.len()` in `ironclaw_scripts`, `Value::to_string().len()` in `ironclaw_loop_host`), because each producer measures what it produced. `ironclaw_common` was rejected (the crate the restructure is actively narrowing) and `ironclaw_host_api` was rejected explicitly rather than by omission (behavior in the contracts leaf is the specific criticism already on record against it). Cost, stated: ~18 lines and 2 unit tests duplicated across two crates. ## Guidance and docs New `AGENTS.md` for both crates (both rows asked for one). PROPOSAL §6.4.10 and §6.2.5 amended with dated notes quoting what they replace; §12.12 opened as the Wave 4 delegated-decision log, continuing §12.11's lettering and marking discipline. `families/domains.md` and `families/substrates.md` updated, including a sharpened "never contains" test for observability and a corrected security role for extractors (its failure type is a redaction boundary; "none" was wrong). ## Tests Unfiltered per-crate `--list`, before -> after: extractors 26 -> 28, observability 2 -> 2, attachments 39 -> 39, host_runtime 1247 -> 1249, extension_support 152 -> 156, architecture 206 -> 206. Nothing deleted; nothing edited for content. Observability's two tests moved with the function and are now duplicated in both consumers (2 -> 4 workspace-wide); its two replacements pin what actually remains in the crate. Both new guards were sabotage-verified: break the invariant, confirm red with the right message, restore, confirm green. Coverage floors untouched and deliberately so: the source crate (`ironclaw_observability`) has no floor entry, and the destination `ironclaw_host_runtime` gains covered lines rather than losing them. Found and filed rather than patched: #7103 (the coding tool computes its JSON byte count before checking whether latency tracing is on) and #7104 ("no text found" classifies as `Failed` rather than `Empty`, so the model is told the wrong thing about a valid but text-free document). * fix(extractors): ASCII-only extension normalization + narrow the Debug-payload guidance Review triage for #7106. **CodeRabbit thread 2 — accepted.** `.claude/rules/types.md:170` and `review-discipline.md:45` require case-insensitive external values to be normalized with `to_ascii_lowercase()`, not Unicode case folding. Both extension registries in this crate used `to_lowercase()`; the sibling registry in `ironclaw_extension_support::coding::file` (`should_extract_document_before_text`) already got it right, so this is the outlier. Note it is a latent-hazard fix, not a live bug: the eight keys (pdf/docx/pptx/xlsx/doc/ppt/xls/rtf) contain none of the letters a Unicode fold can produce from a foreign codepoint, so I could not construct an input where the two differ today. It removes the hazard for the next key added. Test pins both halves: ASCII case-insensitivity still works, and a non-ASCII extension is not folded into an ASCII key. **CodeRabbit thread 1 — guidance tightened, code change refuted.** The reviewer is right that this crate's doc told callers to `tracing::debug!(?error, …)` without naming a ceiling, while `ironclaw_host_runtime/AGENTS.md:28` forbids unredacted user content in that crate's logs. Both docs now say the payload belongs in an operator log and nowhere else, and record what it actually carries. The proposed code change is refused with measurement in the PR thread: it would log strictly less than `main` does today. * fix(extractors): the Unicode extension fold was a live bug, not a latent one Correcting my own claim in 0e7d14e and in the #7106 review reply. I wrote that `to_lowercase()` vs `to_ascii_lowercase()` was observationally equivalent here and that I "could not construct an input where the two differ". That was measured against only ONE of the two extension registries. `try_extract_by_extension`'s key set is much larger than `extract_document_text_by_filename`'s eight, and it contains `markdown`: "MAR\u{212A}DOWN".to_lowercase() == "markdown" // U+212A KELVIN SIGN -> k "MAR\u{212A}DOWN".to_ascii_lowercase() == "MAR\u{212A}DOWN" So on `main`, a file named `notes.MAR<U+212A>DOWN` carrying an unrecognized MIME type took the filename fallback in `extract_text`, was UTF-8-decoded, and reached the model as markdown instead of being rejected as an unsupported type. `bash` and `zsh` are in the same key set for the same reason. Caught by CodeRabbit on #7106, which constructed the input I said did not exist. Recorded here rather than quietly repaired: the earlier reply's measurement was wrong and the switch at :707 is a behaviour fix. Regression test extends `extension_matching_is_ascii_case_insensitive_and_ nothing_more` with the `markdown` fold in both registries plus the public `extract_document` path that actually reaches the fallback. Sabotage-verified: reverting :707 to `to_lowercase()` turns it red on the named assertion. * fix(arch): make the driver-boundary visibility check reachable and the scan multi-line safe Review found this gate weaker than its docs for the third time. Both findings were real; both are fixed at the seam and pinned in both directions. 1. The `pub mod` assertion could never fire. The header was matched with `starts_with("mod postgres_backed {")`, so a line beginning `pub ` was not the matched header and the `!starts_with("pub ")` assertion below it was dead. A visible module was simply not found: the exempt range came back empty and the failure blamed whichever driver mention was reported first rather than the visibility change that broke containment. The header now keys on the `mod postgres_backed {` token and asserts on the captured visibility prefix, so `pub` and `pub(crate)` both fail by name. 2. String state did not survive a newline, and that was fail-open. Block comments were carried across lines; regular and raw strings were not, so the continuation lines of a multi-line literal were scanned as code. A `}` there truncated the body, and a `{` there stretched it past the module's real end and swallowed every driver mention after it. With an unbalanced `{` in a multi-line literal and a `deadpool_postgres::Pool` in a public signature after the body, the old scan reported ok; the new one fails on lib.rs:2217. The raw-string terminator is now searched over bytes, so a multi-byte character in a literal cannot leave the index off a char boundary and panic. Regression tests (all failed before the fix, except the last which had no fixture at all): multi-line literal boundary in both directions plus raw strings, `pub mod` and `pub(crate) mod` rejection, the widened header match not mistaking a comment or string for the declaration, and the unterminated-body panic that AGENTS.md and CHECKLIST.md both present as part of the guarantee. Both fixes sabotage-checked against the real event_store source, not only fixtures. The weakness is recorded in the CHECKLIST row and AGENTS.md rather than quietly repaired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(config): retire the vendor config sections behind a generic window (WS6) `[slack]` and `[telegram]` were the last per-vendor sections in `ironclaw_reborn_config`. Nothing reads them: the enablement gate they fed was deleted with the unified extension runtime (#6116), so `config set slack.enabled true` printed "saved" for a value with no runtime consumer. Replaces the typed vendor schema with a generic retired-section table: - delete `SlackSection`, `SlackChannelRouteSection`, `TelegramSection`, their three builders, and `update_slack_enabled` - `RebornConfigFile` no longer names a vendor; retired sections are split off the raw document before the typed parse, so the schema stays `deny_unknown_fields` - `reject_legacy_slack_config` becomes `reject_retired_config_sections`, data-driven by the same table (PROPOSAL §12.2's "relocated shape") - `config set slack.enabled` now answers with migration guidance instead of writing a value nothing reads Compatibility window preserved and widened: an existing `config.toml` still parses, a retired *setup* key still fails the boot closed with the same message, an inert section still boots — and now says so instead of being silently ignored. Inline-secret rejection over retired sections goes from nine hardcoded keys to every string at any depth. Parse diagnostics: files with no retired section keep the line/column span on unknown-field errors (the split re-parses the original text); only files already carrying a retired section see the degraded form. Measured, and pinned by a test. Sabotage-testing the new guards found one of them inert: the scalar re-insert test only covered `slack = 1` alone, which takes the fast path and would catch it either way. Widened to `slack = 1` beside a genuine retired section, which is the case that actually bypasses `deny_unknown_fields` without the re-insert. The reachability-vs-fidelity limit of the table-driven key test is recorded in its doc rather than papered over. Extension-specificity allowlist 127 -> 125 (baseline lowered to match): the two surviving vendor tokens are the TOML table names, quarantined in `retired_sections.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: correct the Slack/Telegram enablement gate that no longer exists The retired `[slack]`/`[telegram]` sections had a documentation half. Five operator-facing docs still taught a gate deleted by #6116 (2026-07-21): `setup-slack-for-reborn-binary.md` called it the binary's "one gate" and described `IRONCLAW_REBORN_SLACK_ENABLED=false` as a "deployment kill switch" (it is not — Slack stays mounted), and its troubleshooting step could never fix anything. README instructed a `config set slack.enabled` command that now fails. Replaces the gate story with the real one everywhere: the ingress route is compiled in and mounted unconditionally, answers 503 until the extension's signing secret is registered, and 401 on signature mismatch — Slack and Telegram go live by installing the extension and finishing setup at /extensions. Adds a migration note where an operator with an existing file would look. Also removes `IRONCLAW_REBORN_SLACK_PERSONAL_OAUTH_REDIRECT_URI` from `docs/channels/slack.mdx`: zero readers in `crates/`. The CLI already had a regression test asserting that variable must never be advertised in remediation text, so its retirement was known — only the docs kept saying it. Records amendments in the target-architecture docs (CHECKLIST WS6 rows, PROPOSAL §6.10.3 with the placement decision and rejected alternatives, §12.2's compat constraint) and corrects a phantom test citation in the extension-runtime checklist. Filed rather than patched: #7115 (docker entrypoint gates its migration on the dead env var, so following the docs skipped it) and #7116 (live-QA runner gates Slack cases on a value it writes itself). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(planner): classify `.env.example` so a comment fix is not a full-matrix failure The Reborn PR test planner is fail-closed on unknown paths, and had no rule for `.env.example`. Repo-root `*.md` was classified; its non-`.md` sibling was not, so this PR's env-var comment correction aborted the planner with `unclassified pull-request path: .env.example` and failed the whole `Tests (Reborn)` roll-up on a change with no build surface. Nothing reads the file — no crate, test, or workflow; only doc comments name it by name. Classified rather than exempted, following the `.claude/` precedent added 2026-08-03, whose comment states the rule this follows: classify the path, do not loosen the arm that catches genuinely unknown ones. Regression test asserts all three halves: the path is accepted, it selects no Rust lane (so a future "classification" that turns a comment fix into a full matrix also fails), a real change riding along still selects its lane, and an unknown root file (`.env.local`) still raises. Verified by sabotage — removing the classification turns the new test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(composition): gate three test-support-only imports so dependency builds lint clean `origin/main` already fails `Code Style` clippy for the package set `{ironclaw, ironclaw_reborn_config}` — verified on a clean detached checkout of `dfdd02b9fb`, exit 101, three unused imports in `composition/src/runtime.rs`. This PR is simply the first to produce that set, so it inherited the failure. Mechanism: the PR clippy lane derives `-p` from the diff and adds `--all-features`, which applies to *selected* packages only. All three imports are named solely by `#[cfg(any(test, feature = "test-support"))]` accessors, so when composition is a mere dependency its `test-support` is off, `--lib --bins` also drops `#[cfg(test)]`, and the imports go unused. With composition in the selected set, `--all-features` turns the gate on and the same command passes. Gating the imports to match their users is the minimal correct fix — they are used, so deleting them would be wrong and `#[allow]` would hide the real property. Verified both directions: the PR-lane invocation and `-p ironclaw_reborn_composition --all-targets --all-features` are now both exit 0. The class of bug — a lint gate whose verdict depends on which packages a PR happened to touch — is #7119; this commit only unblocks. Touching an otherwise-occupied crate deliberately kept to three `#[cfg]` attributes and a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: review fixes — google CLI path, Slack setup location, retired-key wording Three CodeRabbit findings, each verified before acting: - `capabilities/configuration.mdx`: `config set google.*` is still a supported path (README and `using/cli.mdx` both document it), so "configure it from the web interface rather than by hand" was wrong. Names both paths now. - `reborn/setup-slack-for-reborn-binary.md`: the 503 troubleshooting step pointed at `/extensions` generically and then called the same thing "Admin Configuration" — a third name for a place `docs/channels/slack.mdx` documents precisely (Extensions -> Channels tab -> Configure on the Slack card), including a warning that Extensions opens on the Registry tab, which is not it. Aligned to that wording, since it is the more specific of the two and matches the UI. - `using/cli.mdx`: "everything else is edited in config.toml directly" no longer holds for retired keys. The fourth finding is refuted in the thread: it asked for a "retired setup keys fail at serve" caveat on the `[telegram]` note, but `RETIRED_SECTIONS` gives telegram `rejected_keys: &[]` — it never had a setup field, so no `[telegram]` section can fail a boot. Adding the caveat would document behaviour that does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(slack): tie "Admin Configuration" to the Slack card once, in the guide The setup guide names the operator-facing concept ("Admin Configuration for Slack", 7 references) while docs/channels/slack.mdx names the UI path (Extensions -> Channels tab -> Configure on the Slack card). They are the same dialog, but nothing said so, and my earlier fix only rewrote the troubleshooting paragraph — leaving one place described two ways. Defines the equivalence once, next to the first use, and points the 503/401 steps back at it instead of restating the UI path a second time. Rewriting all seven references would churn a guide this PR is otherwise only correcting for the retired enablement gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(traces): split contribution.rs into chartered modules `crates/ironclaw_reborn_traces/src/contribution.rs` was 17,470 lines — the largest single file in the tree — and carried an `// arch-exempt: large_file` waiver from a 2026 mechanical rename (plan #6168). WS6's domain-internal cleanup row and PROPOSAL §6.4.14 both call for splitting it into chartered modules. It becomes a directory module of 13 production submodules plus a mirrored test tree, each named for one owner in the pipeline (capture → redact → classify → score → queue → submit). `src/contribution/mod.rs` carries the charter table that says which module a new item belongs to, plus the two rules that keep it honest: redaction is split by key (pattern vs tool-name), and `queue` owns state / `remote` owns the wire / `submission` is the only caller of both. The waiver is deleted rather than carried forward, and no new one is added: every file is under the 1,500-line ARCH-SPRAWL threshold (largest is 1,290). No public API change and no consumer edits. The submodules are private and `mod.rs` glob-re-exports them, so `contribution::X` remains the single public path for all four consumer crates. Items that newly cross a module line were widened to `pub(crate)`, never to `pub`. Verification: - Item roster diffed against origin/main: 501 top-level items before, 501 after, zero missing and zero extra. - Unfiltered `--list` before and after: 216 lib tests, leaf names identical. All 216 + 2 integration tests pass. - `cargo clippy --benches --tests --examples --all-features` clean on ironclaw_reborn_traces and ironclaw_architecture. The four `PATH_TERM_COLLISIONS` carve-outs that pinned the old file path are repointed and, in the process, narrowed: the vendor-name safety denylist now resolves to `tool_payloads.rs` (the rule tables) and `classification.rs` (external-write detection, `slack` only) instead of one 17k-line whole-file carve-out, so the specificity gate now polices the rest of the module. Those entries are staleness-checked, so the old path would have failed loudly. Adds the crate's first guidance file, recording the glob-re-export invariant and the three known gaps on §6.4.14's row that this PR does not close (ScopedFilesystem adoption, the two re-export modules, the crate rename). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(reborn): record the traces contribution.rs split and correct two stale clauses Amends CHECKLIST WS6's domain-internal-cleanups row and PROPOSAL §6.4.14 (plus the anti-pattern inventory and the crate-disposition table) with what landed, quoting the text each amendment replaces. Two corrections the work surfaced, recorded rather than silently fixed: - §6.4.14's "17,467-line contribution.rs" measured 17,470 on main; the file drifted after the entry was written. - The CHECKLIST's shorthand "`ScopedFilesystem` + re-export modules dropped" is worded backwards for the first clause. `ScopedFilesystem` is `ironclaw_filesystem`'s type, is used by ~170 files across the workspace, and is absent from `ironclaw_reborn_traces` entirely — there is nothing to drop. §6.4.14's actual instruction is adoption ("take a `ScopedFilesystem` instead of raw `dirs`/env access"), which is a persistence-plane change across ~91 raw fs call sites, not a deletion. Left as-is with the reason stated, so the next reader measures rather than inherits. Also records why the two remaining traces clauses did not land in this wave: dropping the `recording`/`paths` re-export shims needs edits in `ironclaw_reborn_cli`, and `recording` additionally needs a decision because the CLI has no `ironclaw_llm` dependency to fall back on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(traces): serialize test process-env mutation behind lock_env() The split re-surfaced five unguarded `std::env::set_var`/`remove_var` call sites that CI's `check-hermetic-env.sh` had been grandfathering: they are byte-identical pre-existing lines (contribution.rs:10501/10513/10515/15648/ 15661 on origin/main), and the gate only skipped them because it is delta-scoped and the file had not been re-added since it was written. This is a real gap, not a false positive, so it is fixed rather than annotated. `EnvVarRestore` restored the previous value on drop but took no lock, so two tests mutating the environment on different threads still raced — undefined behavior on Rust 1.82+ regardless of whether they name the same variable. `workload_token_env_mode_reads_env_unchanged` used a uniquely named variable, which avoids logical interference but not the setenv/getenv data race. Both now acquire `ironclaw_common::env_helpers::lock_env()`, the sanctioned helper the gate's message names. `EnvVarRestore` holds the guard as a field declared last, so it is released only after `Drop::drop` has restored the value — the restore is inside the critical section, not after it. The real process environment is kept (not `env_helpers::set_runtime_env`'s overlay) because the sidecar isolation test needs a value a child process would inherit, to prove `CommandPrivacyFilterAdapter` clears it. One `#[allow(clippy::await_holding_lock)]` on the async test, matching the precedent in `ironclaw_operator/src/llm_admin/llm_config_service.rs`: holding the lock across the await is the intent, and `#[tokio::test]` drives the future on a current-thread runtime so the guard never crosses threads. Verified: `check-hermetic-env.sh` exits 0, clippy clean, 216 + 2 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(traces): apply CodeRabbit review — carried waiver, inert test, charter drift Six findings verified against the code; four were defects this PR introduced or carried, and each is fixed. 1. **A second file-size waiver was carried forward after all.** `queue.rs` still held the in-body "File-size justification … already-oversized module … decomposition tracked in issue #4088" block, which contradicts a PR whose whole point is performing that decomposition. Deleted; the coupling rationale it was wrapped around (why credential resolution lives beside the policy/scope-dir helpers) is kept, since that still explains the layout. 2. **`invite_code_gated_by_auth_mode` was inert.** It re-implemented the `match policy.auth_mode` expression from `build_trace_upload_claim_issuer_request` and asserted against its own copy, so deleting the `DeviceKey => None` arm in production left it green. It now calls the production builder and asserts on the *serialized* request, so a field rename cannot hide a leak either. Sabotage-proved: removing that arm now fails with the leaked invite code visible in the body. 3. **The charter claimed "each stage owns one file"**, which `remote`'s four files contradict. Reworded to module-level ownership, naming `remote` as a directory module and why. `CLAUDE.md`'s test-layout paragraph gets the same correction plus the explicit `remote` → four-test-module mapping. 4. **Five policy-serde tests sat in `claims.rs`.** They verify `StandingTraceContributionPolicy`, whose owner is `policy.rs`, and the PR's own rule is that a test lives with its production owner. Moved to a new `tests/policy.rs`; leaf names unchanged. 5. **Three orphan section headers** left behind by the split, describing tests that now live in other modules (`credentials.rs`, `profile.rs`, `value.rs`). Deleted. The remaining two findings are real but pre-existing and need behavior changes, so they are filed as #7127 rather than fixed here: the case-sensitive remote `status` comparison that skips the local revocation record, and `fetch_account_traces` taking two adjacent `&str` where its sibling takes `&TenantId, &UserId` (its fix needs an edit in `ironclaw_product`). The issue also carries the `trace_scope_has_pending_queue` doc/code mismatch, which needs an intent decision rather than a guess. Re-verified: 501/501 production items, 216 tests with identical leaf names, clippy clean, hermetic-env clean, every file under 1,500 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(traces): use the RAII env guard and cover the bearer at the caller Second CodeRabbit pass, both findings on the test this PR had already touched. 1. **RAII guard instead of manual cleanup.** `workload_token_env_mode_reads_env_unchanged` set the variable, awaited, asserted, then removed it — so any panic before the last line leaked the variable into every later test. It now uses `EnvVarRestore::set`, whose `Drop` restores during unwinding while holding the same process-env lock. That also deletes both `unsafe` blocks and the `#[allow(clippy::await_holding_lock)]`: the guard lives in a struct field, which the lint does not flag, so the suppression is no longer needed. 2. **The bearer token had no caller-tier coverage.** Five tests assert what `issuer_request_bearer` returns; none asserted the token reaches the wire. The direct issuer path attaches it conditionally (`if let Some(bearer) = issuer_bearer { request.bearer_auth(bearer) }`), so a helper regressing to `None` would send an unauthenticated request with every existing test green — the repo's "test through the caller" rule names exactly this shape. Adds `workload_token_reaches_the_issuer_request_as_a_bearer_header`: a mock issuer captures the `Authorization` header while `fetch_trace_upload_claim_from_issuer` drives the real path. Sabotage-proved — dropping the `bearer_auth` attach fails it with `left: None, right: Some("Bearer wire-bearer-xyz")`; restored, green. Test accounting: 216 → 217. All 216 original leaf names still present (diffed against the `origin/main` baseline); the one addition is the new caller-tier test. Clippy clean, hermetic-env clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(llm): add the enforced sub-owner map (WS6 module charters) PROPOSAL §6.4.13 asks `ironclaw_llm` for "internal module charters for its five sub-owners". This adds the map to `crates/ironclaw_llm/CLAUDE.md` and, because a charter nobody checks rots within a release, a test that pins it. **Five sub-owners were not enough, measured.** `providers` / `auth-sessions` / `registry` / `decorators` / `recording` own 28 of 48 files (79.6% of lines), leaving 20 unowned — including `lib.rs`, `provider.rs`, `error.rs` and `config.rs`. Five more are named, each with a stated reason rather than a residual bucket: `core-contract` (the trait, vocabulary, error taxonomy and config are *upstream* of every implementor, so charging them to `providers` would make providers own decorators' and recording's own dependencies), `normalization` (cross-provider wire hygiene, as opposed to the single-provider shims that stay beside their provider), `model-catalog` (facts about *models*, a different noun from registry's catalog of *providers*), `transcription` (`TranscriptionProvider` is a different trait; nothing there implements `LlmProvider`), and `test-support` (a published feature with its own compatibility obligation). **`tests/module_charter.rs` enforces it.** Every `src/**/*.rs` must appear in exactly one row, every path in a row must exist, and no file may be claimed twice. Sabotage-proved in all three directions — dropping `retry.rs` from the table, adding a phantom path, and double-claiming `registry.rs` each fail with the right message; restored green. The test also guards itself: it fails if the table parses to zero rows or if the source walk finds implausibly few files, so a table-shape change cannot silently turn it into a no-op. **§6.4.13's "Deletes: reasoning.rs (4.5k lines, zero external references)" is refuted.** The file is 1,299 lines after #6964 removed its dead half, and the survivor is live: `lib.rs:88-91` re-exports three helpers with five production call sites in `crates/ironclaw_loop_host/src/model_gateway.rs`. It is charted under `normalization`. `AGENTS.md` carried the same staleness ("legacy reasoning engine") and is corrected; it also now points at the map as authoritative so its informal buckets cannot quietly become a second source of truth. Four placement calls are recorded rather than left implicit: `token_refreshing.rs` is auth-sessions not decorators (CLAUDE.md and AGENTS.md disagreed); `runtime.rs` and `smart_routing.rs` force the decorator definition to widen from "reliability wrapper" to "wraps `dyn LlmProvider` and is not credential work"; `url_check.rs` is core-contract; and `gemini_oauth.rs` is genuinely two owners in one file, charged to the larger half with the split recorded as owed. CHECKLIST and PROPOSAL §6.4.13 carry dated amendments quoting the text they replace, including why the row's `providers.json` clause is blocked (its load-bearing include site is in `ironclaw_reborn_cli`, which is occupied, and it needs a new mechanism rather than a new path). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(traces): correct the claims/policy test-module docs after the move The script that moved the five policy-serde tests copied `claims.rs`'s preamble verbatim, so `policy.rs` ended up with two module docs — its own and a carried-over line describing claims. And `claims.rs`'s own doc still opened with "Standing-policy serde", which stopped being true the moment those tests left. `policy.rs` keeps only its own doc; `claims.rs` now describes what it actually covers (upload-claim cache keys, issuer error labels, the bearer the issuer request carries, device-key auth modes) and points at `policy.rs` for the policy serde contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(arch): lower the specificity ALLOWLIST baseline 125 -> 124 after the re-baseline #7117 measured `ALLOWLIST` 127 -> 125 against `origin/main` @ `1e2a294083`. #7094 then deleted one entry on `main` (127 -> 126), so this branch's two net removals now land on 124, not 125. The ratchet is `<=`, so it stayed green at 125 while carrying a unit of untracked slack — exactly what the constant's own doc forbids: "Lower it in the same PR that deletes entries so the new floor is locked in." Read off the ratchet's own failure message with the baseline temporarily set to `0` ("ALLOWLIST grew to 124 entries"), never counted by eye — a plain paren count over the literal answers 142, because the entries' comments contain parentheses too. Sabotage-verified in both directions: baseline 123 goes red naming 124, and 124 is green 7/7. The file's function roster is unchanged. * docs(checklist): map the WS6 "Domain-internal cleanups" row clause by clause The row bundles eight clauses and the Wave 4 part-1 consolidation closes one of them (the `traces` `contribution.rs` split). It stays open, correctly — but a reader of the row could not tell which of the remaining seven had been measured and which had not, and the `llm` `providers.json` measurement lived on the "Module charters" row two rows down because that is where the agent who made it was working. Adds item 7: a clause-by-clause status map — one done, three measured with the blocker named (including a pointer to where `providers.json` was measured), four untouched. No box is ticked; the row's real condition is unmet and stays unmet. Also fixes a stray space-semicolon left in the "Composition behavior evictions" row where the system-prompt clause was struck through. * review(ws6): fix seven findings on code this consolidation introduced CodeRabbit's pass over the consolidation raised 40 threads. 29 are on production code #7124 only *moved* and are filed as #7144. These seven are on code this program wrote, and all seven were correct. **A gate that was not scanning what its doc claimed.** The driver-boundary walk used a flat `read_dir` while its doc said it scans "**every** `.rs` file in the crate". `crates/ironclaw_reborn_event_store/src` is flat today, so nothing escaped — but `src/postgres/pool.rs` is exactly where a driver mention would go, and a skipped file is indistinguishable from a clean one. Now recursive and symlink-rejecting, matching the shape `reborn_composition_boundaries.rs` already uses in this same PR. Sabotage-proved against the real crate: a nested `postgres/pool.rs` naming `deadpool_postgres::Pool` now fails the gate naming `pool.rs:1`, and passed silently before. This is the third revision of this gate found weaker than its own docs; the doc now says why. **A charter gate that a table reformat would have broken.** `module_charter.rs` matched the separator row with `cells[0].starts_with("---")`, so an aligned separator (`|:---|:---|`) parsed as a *data* row: `:---` became an assigned path, `saw_row` went true so the shape guard stayed quiet, and the stale assertion reported `:---` instead of a diagnosis. Sabotage-proved both ways — with the fix reverted and the table rewritten in aligned form the test goes red on `:---`; with the fix it passes. Also: - `CONTRACT.MD` added to the composition guidance allowlist. The repo already ships it as crate-local guidance (`ironclaw_reborn_identity`, `ironclaw_trust`) and CLAUDE.md's module-spec table names it, so a composition `CONTRACT.md` would have been reported as prompt content and sent the author to the wrong fix. - `markdown_assets` gains its first real test: the case-insensitive `.md` match and the caller's guidance filter were both unpinned, and both drift quiet. - Two fixtures for comment-braced module bodies (line comment, nested block comment) — the scan handled them, nothing pinned it. - The symlink rationale doc block moved onto `reject_symlink`, which it describes; it was stacked above `reject_symlink_root` with no item between, so both attached to the wrong function and `reject_symlink` was undocumented. - The retired-section deprecation warn gains `target = "ironclaw::reborn::cli::serve"`, like every other warn on that path. Announcing an inert section is pointless if an operator filtering the documented startup target cannot see it. - `ironclaw_reborn_traces/CLAUDE.md` claimed a one-to-one test mapping that `tests/credentials.rs` breaks (it spans `queue.rs` and `remote/claim.rs`). The exception is now stated rather than left to be inferred. Rosters in both architecture test files are purely additive; no test removed. * docs: correct the extension-specificity allowlist numbers after the re-baseline Caught in review of #7139. Both ledgers still recorded #7117's measurement, `Extension-specificity allowlist **127 → 125**`, taken against `origin/main` @ `1e2a294083`. #7094 then deleted an entry on `main` (127 → 126), so the same two net removals land on **124**, which is what the shipped baseline says. This is the cross-slice-number failure mode the consolidation exists to catch, one layer down: the code was corrected in 811bfedeff and the prose was not. Both amendments quote the text they replace and record the method — read off the ratchet's own failure message with the baseline temporarily set to 0, never counted by eye. No checkbox state changed. * review(ws6): three more review findings, one of which broke my own fix **My `target =` fix did not work, and CodeRabbit was right to call it.** `tracing::warn!(target = "…")` records a *field* named `target`; it does not set the event's metadata target, which stays the module path. So the retired-section notice — given a target in #7117 precisely so operators would see an inert `[slack]`/`[telegram]` section announced — was still invisible to a subscriber filtering `ironclaw::reborn::cli::serve`. Measured with a capturing subscriber rather than argued: EQUALS-SYNTAX target = "target_probe" <- module path COLON-SYNTAX target = "ironclaw::reborn::cli::serve" <- correct Now `target:`, and pinned by `retired_section_notice_is_emitted_on_the_serve_target`, which asserts the emitted **metadata** target through the real `reject_retired_config_sections` call. Sabotage-proved: the `=` form makes it red with `observed targets: ["ironclaw::commands::serve"]`. This is repo-wide — **121 sites** use the `=` form against an `ironclaw::…` target, including the three sibling warns on this same serve path (`:318`, `:387`, `:454`). Filed as #7146 rather than fixed here; a consolidation should not carry a 121-site mechanical change. **The markdown gate's test was testing a copy of itself.** My new test carried its own duplicate of the guidance allowlist, so the production filter could drop `CONTRACT.MD` and the test would still pass — the "test through the caller" rule. Extracted `is_crate_guidance` / `shipped_non_guidance_markdown`; the gate and the test now share one path. Sabotage-proved by dropping `CONTRACT.MD` from the shared helper: red with `left: ["CONTRACT.md", "seed.MD"]`. **The separator fix had no committed regression test.** It was sabotage-proved by hand, which does not survive the session. `parse_sub_owner_table` is split out from the file read so a fixture can supply separator shapes the checked-in `CLAUDE.md` does not use, and `an_aligned_separator_row_is_not_parsed_as_data` covers unaligned, left-aligned and centred. Red when the fix is reverted. Rosters purely additive in all three files; no test remove…
…ations sever + WS10 inventory keying + enforcement gates (nearai#7170) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone able to set it could redirect all credentialed vendor egress. That was WRONG. RewriteNetworkTransport::from_env_value already returned UnavailableInRelease when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither [profile.release] nor [profile.dist] sets debug-assertions, so a shipped binary with the variable set REFUSES TO BOOT. It was fail-closed before this PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime path. What was genuinely wrong, and is fixed: 1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile proxy, not a build-kind guarantee. A release profile with debug-assertions turned on (normal when chasing a production bug) silently re-arms it. 2. The refusal arm had NO TEST. The one guard between a shipped binary and redirectable vendor egress was unpinned. Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and its four re-exports are now cfg(any(debug_assertions, feature=test-support)), and default_host_http_egress is a compile-time pair - production builds PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite wrapper absent from the binary. The runtime check stays as defence in depth. E2E needs no change: those harnesses build DEBUG binaries, so they satisfy debug_assertions and keep redirecting with no feature flag and no workflow edit. The feature-forwarding-into-CI risk I flagged earlier does not arise. test-support is still forwarded composition -> network for a release-PROFILE build that needs the seam. Both halves proven rather than assumed: (a) release refuses - new regression test a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds a well-formed map and asserts on profile. Under 'cargo test --release -p ironclaw_network --features test-support' it passes on the UnavailableInRelease branch; under debug 'cargo test -p ironclaw_network' it passes on the active branch. 56 passed, 0 failed. (b) production compiles without the seam - 'cargo check --release -p ironclaw_reborn_composition' (no test-support) is clean, which only compiles if the cfg(not(..)) arm is right. Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant had drifted ABOVE the real list length; the ratchet is shrink-only so it passed silently while buying back two unearned slots. Measured off the compiler (set baseline to 0, read the reported length), identical on main and on every slice, so pre-existing drift rather than something this PR caused. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(coverage): verify the extension_support floor drop is composition, independently The 82.64 -> 75.31 recapture carried a rationale that was recorded but explicitly NOT verified. Re-derived it from scratch between the two capture refs (f946a93fae -> 939af4847d) rather than inheriting the claim: - 0 test names lost in the crate (158 -> 160 test fns; both new names belong to the arriving executor). - 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759 unique). This is the check that separates a relocation from a deletion: host_runtime's roster drops 156 names over the same range and every one reappears in another crate. - Exactly four files arrived, 1367 source lines, all of them the family-1 skill-install executor (src/skills/url_install.rs + url_install/{github, zip_bundle,bundle}.rs). No pre-existing file left the crate. - The arithmetic closes with the pre-existing numerator held CONSTANT: (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero covered lines. The arriving block's own coverage is 316/1224 = 25.82%. Composition, confirmed rather than assumed. No test regression to fix; the 25.82% arrival is what earns the follow-up already recorded above the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn! Three verified review findings from the #7141 round. Each was confirmed against the code before being acted on; nothing was changed on assertion alone. 1. obligations/handler.rs — `obligation_supported_before_dispatch` and `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies (verified by exact line-by-line comparison). Both were private, each called exactly once, both taking the same `phase` argument. The two names asserted a pre/post-dispatch distinction the code never implemented, while the pair gates admission of RedactOutput, EnforceOutputLimit and EnforceResourceCeiling — so editing one copy alone would have left the other stage accepting an obligation the host cannot honour (a fail-open). Collapsed to one `obligation_supported`, with the reasoning recorded so the pair is not reintroduced. 2. obligations/process_store.rs — `cleanup_terminal` is reached from `observe_process_commit` (an async background journal callback, call sites at :363/:379/:394), so its `tracing::warn!` violates the repo rule that background tasks never use info!/warn! — they corrupt the REPL/TUI display. Lowered to `debug!`; the error is still returned to the caller on the next line, so nothing is swallowed. 3. reborn_restructure_baselines.rs — the doc table said the LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type annotation opens a bracket on the same line and silently yields 0): the real count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected. Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0; obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane Review finding on #7141, confirmed empirically before acting. The Markdown prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup. A prompt is a `.md` file that no package *directory* owns, so a change to `crates/extensions/packages/*/prompts/**.md` took the prose arm and planned: mode=none crate_buckets=[] "crate-tree guidance changed: ..." while its sibling `manifest.toml` in the same package planned `mode=selected` onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped production output that `ironclaw_extension_support` compiles in, and the comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and built wasm/*.wasm" as exactly what that table owns — so this was the "silent under-schedule of a change to production output" that comment forbids. 145 of the 149 `.md` files under `packages/` are prompts. The rule is keyed on the `prompts/` path segment, not on the asset prefixes. That distinction is load-bearing: the first attempt yielded to the asset prefixes wholesale and broke `test-tools/README.md`, which is documentation of the fixture bundles and is deliberately pinned as prose. Of the four asset kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas .json, wasm .wasm), so `.md` asset <=> prompt is exact. Sabotage-tested in both directions: * `_is_package_prompt` -> False (reinstates the bug): RED, "AssertionError: 'none' != 'selected'". * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on both the new test and the pre-existing `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`. * restored: 52 passed, 51 subtests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(harness): refresh the latency-runner lockfile after the sandbox consolidation Review finding on #7141, reproduced before fixing. The latency harness keeps its own committed `Cargo.lock`, separate from the workspace lockfile, and the crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox` with `ironclaw_sandbox` never regenerated it. It still carried entries for both removed packages (lines 3244 and 3602) and the old host-runtime/loop-host dependency graphs. Reproduced exactly as reported: $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml error: cannot update the lock file ... because --locked was passed exit 101 so any reproducible invocation of the harness was broken, while the documented unlocked command silently rewrote the lockfile as a side effect of running. Regenerated with `cargo update --workspace`, which re-resolves the path dependencies. Verified after: `--locked` exits 0, the two removed packages are gone (0 entries), and `ironclaw_sandbox` is present (1 entry). Note: the re-resolve also carried three registry deps forward (wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser 0.251.0 -> 0.252.0). That is contained — this lockfile governs only the standalone benchmark harness and is not the workspace lockfile, and it was already unusable under `--locked` before this change. Co-Authored-By: Claude Opus 5…
…isions (accumulating the fleet) (nearai#7181) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone able to set it could redirect all credentialed vendor egress. That was WRONG. RewriteNetworkTransport::from_env_value already returned UnavailableInRelease when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither [profile.release] nor [profile.dist] sets debug-assertions, so a shipped binary with the variable set REFUSES TO BOOT. It was fail-closed before this PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime path. What was genuinely wrong, and is fixed: 1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile proxy, not a build-kind guarantee. A release profile with debug-assertions turned on (normal when chasing a production bug) silently re-arms it. 2. The refusal arm had NO TEST. The one guard between a shipped binary and redirectable vendor egress was unpinned. Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and its four re-exports are now cfg(any(debug_assertions, feature=test-support)), and default_host_http_egress is a compile-time pair - production builds PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite wrapper absent from the binary. The runtime check stays as defence in depth. E2E needs no change: those harnesses build DEBUG binaries, so they satisfy debug_assertions and keep redirecting with no feature flag and no workflow edit. The feature-forwarding-into-CI risk I flagged earlier does not arise. test-support is still forwarded composition -> network for a release-PROFILE build that needs the seam. Both halves proven rather than assumed: (a) release refuses - new regression test a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds a well-formed map and asserts on profile. Under 'cargo test --release -p ironclaw_network --features test-support' it passes on the UnavailableInRelease branch; under debug 'cargo test -p ironclaw_network' it passes on the active branch. 56 passed, 0 failed. (b) production compiles without the seam - 'cargo check --release -p ironclaw_reborn_composition' (no test-support) is clean, which only compiles if the cfg(not(..)) arm is right. Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant had drifted ABOVE the real list length; the ratchet is shrink-only so it passed silently while buying back two unearned slots. Measured off the compiler (set baseline to 0, read the reported length), identical on main and on every slice, so pre-existing drift rather than something this PR caused. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(coverage): verify the extension_support floor drop is composition, independently The 82.64 -> 75.31 recapture carried a rationale that was recorded but explicitly NOT verified. Re-derived it from scratch between the two capture refs (f946a93fae -> 939af4847d) rather than inheriting the claim: - 0 test names lost in the crate (158 -> 160 test fns; both new names belong to the arriving executor). - 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759 unique). This is the check that separates a relocation from a deletion: host_runtime's roster drops 156 names over the same range and every one reappears in another crate. - Exactly four files arrived, 1367 source lines, all of them the family-1 skill-install executor (src/skills/url_install.rs + url_install/{github, zip_bundle,bundle}.rs). No pre-existing file left the crate. - The arithmetic closes with the pre-existing numerator held CONSTANT: (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero covered lines. The arriving block's own coverage is 316/1224 = 25.82%. Composition, confirmed rather than assumed. No test regression to fix; the 25.82% arrival is what earns the follow-up already recorded above the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn! Three verified review findings from the #7141 round. Each was confirmed against the code before being acted on; nothing was changed on assertion alone. 1. obligations/handler.rs — `obligation_supported_before_dispatch` and `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies (verified by exact line-by-line comparison). Both were private, each called exactly once, both taking the same `phase` argument. The two names asserted a pre/post-dispatch distinction the code never implemented, while the pair gates admission of RedactOutput, EnforceOutputLimit and EnforceResourceCeiling — so editing one copy alone would have left the other stage accepting an obligation the host cannot honour (a fail-open). Collapsed to one `obligation_supported`, with the reasoning recorded so the pair is not reintroduced. 2. obligations/process_store.rs — `cleanup_terminal` is reached from `observe_process_commit` (an async background journal callback, call sites at :363/:379/:394), so its `tracing::warn!` violates the repo rule that background tasks never use info!/warn! — they corrupt the REPL/TUI display. Lowered to `debug!`; the error is still returned to the caller on the next line, so nothing is swallowed. 3. reborn_restructure_baselines.rs — the doc table said the LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type annotation opens a bracket on the same line and silently yields 0): the real count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected. Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0; obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane Review finding on #7141, confirmed empirically before acting. The Markdown prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup. A prompt is a `.md` file that no package *directory* owns, so a change to `crates/extensions/packages/*/prompts/**.md` took the prose arm and planned: mode=none crate_buckets=[] "crate-tree guidance changed: ..." while its sibling `manifest.toml` in the same package planned `mode=selected` onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped production output that `ironclaw_extension_support` compiles in, and the comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and built wasm/*.wasm" as exactly what that table owns — so this was the "silent under-schedule of a change to production output" that comment forbids. 145 of the 149 `.md` files under `packages/` are prompts. The rule is keyed on the `prompts/` path segment, not on the asset prefixes. That distinction is load-bearing: the first attempt yielded to the asset prefixes wholesale and broke `test-tools/README.md`, which is documentation of the fixture bundles and is deliberately pinned as prose. Of the four asset kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas .json, wasm .wasm), so `.md` asset <=> prompt is exact. Sabotage-tested in both directions: * `_is_package_prompt` -> False (reinstates the bug): RED, "AssertionError: 'none' != 'selected'". * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on both the new test and the pre-existing `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`. * restored: 52 passed, 51 subtests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(harness): refresh the latency-runner lockfile after the sandbox consolidation Review finding on #7141, reproduced before fixing. The latency harness keeps its own committed `Cargo.lock`, separate from the workspace lockfile, and the crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox` with `ironclaw_sandbox` never regenerated it. It still carried entries for both removed packages (lines 3244 and 3602) and the old host-runtime/loop-host dependency graphs. Reproduced exactly as reported: $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml error: cannot update the lock file ... because --locked was passed exit 101 so any reproducible invocation of the harness was broken, while the documented unlocked command silently rewrote the lockfile as a side effect of running. Regenerated with `cargo update --workspace`, which re-resolves the path dependencies. Verified after: `--locked` exits 0, the two removed packages are gone (0 entries), and `ironclaw_sandbox` is present (1 entry). Note: the re-resolve also carried three registry deps forward (wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser 0.251.0 -> 0.252.0). That is contained — this lockfile governs only the standalone benchmark harness and is not the workspace lockfile, and it was already unusable under `--locked` before this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> …
…factory port (nearai#7202) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone able to set it could redirect all credentialed vendor egress. That was WRONG. RewriteNetworkTransport::from_env_value already returned UnavailableInRelease when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither [profile.release] nor [profile.dist] sets debug-assertions, so a shipped binary with the variable set REFUSES TO BOOT. It was fail-closed before this PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime path. What was genuinely wrong, and is fixed: 1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile proxy, not a build-kind guarantee. A release profile with debug-assertions turned on (normal when chasing a production bug) silently re-arms it. 2. The refusal arm had NO TEST. The one guard between a shipped binary and redirectable vendor egress was unpinned. Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and its four re-exports are now cfg(any(debug_assertions, feature=test-support)), and default_host_http_egress is a compile-time pair - production builds PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite wrapper absent from the binary. The runtime check stays as defence in depth. E2E needs no change: those harnesses build DEBUG binaries, so they satisfy debug_assertions and keep redirecting with no feature flag and no workflow edit. The feature-forwarding-into-CI risk I flagged earlier does not arise. test-support is still forwarded composition -> network for a release-PROFILE build that needs the seam. Both halves proven rather than assumed: (a) release refuses - new regression test a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds a well-formed map and asserts on profile. Under 'cargo test --release -p ironclaw_network --features test-support' it passes on the UnavailableInRelease branch; under debug 'cargo test -p ironclaw_network' it passes on the active branch. 56 passed, 0 failed. (b) production compiles without the seam - 'cargo check --release -p ironclaw_reborn_composition' (no test-support) is clean, which only compiles if the cfg(not(..)) arm is right. Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant had drifted ABOVE the real list length; the ratchet is shrink-only so it passed silently while buying back two unearned slots. Measured off the compiler (set baseline to 0, read the reported length), identical on main and on every slice, so pre-existing drift rather than something this PR caused. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(coverage): verify the extension_support floor drop is composition, independently The 82.64 -> 75.31 recapture carried a rationale that was recorded but explicitly NOT verified. Re-derived it from scratch between the two capture refs (f946a93fae -> 939af4847d) rather than inheriting the claim: - 0 test names lost in the crate (158 -> 160 test fns; both new names belong to the arriving executor). - 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759 unique). This is the check that separates a relocation from a deletion: host_runtime's roster drops 156 names over the same range and every one reappears in another crate. - Exactly four files arrived, 1367 source lines, all of them the family-1 skill-install executor (src/skills/url_install.rs + url_install/{github, zip_bundle,bundle}.rs). No pre-existing file left the crate. - The arithmetic closes with the pre-existing numerator held CONSTANT: (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero covered lines. The arriving block's own coverage is 316/1224 = 25.82%. Composition, confirmed rather than assumed. No test regression to fix; the 25.82% arrival is what earns the follow-up already recorded above the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn! Three verified review findings from the #7141 round. Each was confirmed against the code before being acted on; nothing was changed on assertion alone. 1. obligations/handler.rs — `obligation_supported_before_dispatch` and `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies (verified by exact line-by-line comparison). Both were private, each called exactly once, both taking the same `phase` argument. The two names asserted a pre/post-dispatch distinction the code never implemented, while the pair gates admission of RedactOutput, EnforceOutputLimit and EnforceResourceCeiling — so editing one copy alone would have left the other stage accepting an obligation the host cannot honour (a fail-open). Collapsed to one `obligation_supported`, with the reasoning recorded so the pair is not reintroduced. 2. obligations/process_store.rs — `cleanup_terminal` is reached from `observe_process_commit` (an async background journal callback, call sites at :363/:379/:394), so its `tracing::warn!` violates the repo rule that background tasks never use info!/warn! — they corrupt the REPL/TUI display. Lowered to `debug!`; the error is still returned to the caller on the next line, so nothing is swallowed. 3. reborn_restructure_baselines.rs — the doc table said the LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type annotation opens a bracket on the same line and silently yields 0): the real count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected. Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0; obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane Review finding on #7141, confirmed empirically before acting. The Markdown prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup. A prompt is a `.md` file that no package *directory* owns, so a change to `crates/extensions/packages/*/prompts/**.md` took the prose arm and planned: mode=none crate_buckets=[] "crate-tree guidance changed: ..." while its sibling `manifest.toml` in the same package planned `mode=selected` onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped production output that `ironclaw_extension_support` compiles in, and the comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and built wasm/*.wasm" as exactly what that table owns — so this was the "silent under-schedule of a change to production output" that comment forbids. 145 of the 149 `.md` files under `packages/` are prompts. The rule is keyed on the `prompts/` path segment, not on the asset prefixes. That distinction is load-bearing: the first attempt yielded to the asset prefixes wholesale and broke `test-tools/README.md`, which is documentation of the fixture bundles and is deliberately pinned as prose. Of the four asset kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas .json, wasm .wasm), so `.md` asset <=> prompt is exact. Sabotage-tested in both directions: * `_is_package_prompt` -> False (reinstates the bug): RED, "AssertionError: 'none' != 'selected'". * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on both the new test and the pre-existing `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`. * restored: 52 passed, 51 subtests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(harness): refresh the latency-runner lockfile after the sandbox consolidation Review finding on #7141, reproduced before fixing. The latency harness keeps its own committed `Cargo.lock`, separate from the workspace lockfile, and the crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox` with `ironclaw_sandbox` never regenerated it. It still carried entries for both removed packages (lines 3244 and 3602) and the old host-runtime/loop-host dependency graphs. Reproduced exactly as reported: $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml error: cannot update the lock file ... because --locked was passed exit 101 so any reproducible invocation of the harness was broken, while the documented unlocked command silently rewrote the lockfile as a side effect of running. Regenerated with `cargo update --workspace`, which re-resolves the path dependencies. Verified after: `--locked` exits 0, the two removed packages are gone (0 entries), and `ironclaw_sandbox` is present (1 entry). Note: the re-resolve also carried three registry deps forward (wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser 0.251.0 -> 0.252.0). That is contained — this lockfile governs only the standalone benchmark harness and is not the workspace lockfile, and it was already unusable under `--locked` before this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): stop …
…e 4 rows (nearai#7152) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6) PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move "behind the triggers/kernel seam it guards". It was not a module: it was three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire` — one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter` — holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`. That placement is a fail-open: a guard that lives inside one implementation of a port is lost the moment a second implementation exists, and nothing in the tree forced a new submitter to re-run it. The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed `TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now fallible and calls the new `ironclaw_triggers::prompt_safety` first, making "this prompt passed the trusted-prompt scan" an invariant of the type rather than a step some submitter performs. `new_for_test` delegates to `new`, so the test-support seal bypasses visibility only, never the scan. Behaviour at the fire level is unchanged — same rejection point, same `TriggerError::InvalidMaterialization`, same permanent disposition — and composition's pre-materialization scan is untouched, so defence in depth survives with the second scan relocated and now covering every submitter. `ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its only use). Enforcement: triggers' boundary rule stops forbidding `ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge, not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations` forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"), a crate that was unruled until now. Regression coverage at the caller tier, not on the helper: `tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached` drives the real `TriggerPollerWorker::tick_once` with a materializer that does NOT scan and a submitter configured to accept, and asserts the submitter is never reached. A companion pins that a medium-severity-only prompt still submits, so the mint cannot drift into a blanket filter. Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173 (+2 worker, +2 prompt_safety unit), architecture 206 -> 206. LAYER_MATRIX_EXCEPTIONS unchanged at 10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * refactor(traces): drop the boundary-laundering re-export modules (WS6) PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules (`recording`, `paths`) — consumers import the owners". `ironclaw_reborn_traces::{recording, paths}` were two `pub use <other crate>::*` passthroughs whose own doc comments stated their purpose plainly: "so reborn-cli does not need a direct `ironclaw_llm` dependency, preserving the architectural boundary". They preserved nothing — the edge existed either way; the wildcard only hid which crate owned the type, so the dependency graph read as a lie. All three call sites were in `ironclaw_reborn_cli`. Note the literal reading of "consumers import the owners" is not available here: the CLI's dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`) deliberately excludes `ironclaw_llm`, so importing the owner would have traded a laundered re-export for a breached, tested boundary. Satisfied instead by giving the owning crate the operation, which is what the laundering was standing in for: - `onboarding::onboard_instance(invite, consents)` — resolves the contribution root itself. Path layout under the base dir is this crate's own knowledge; the CLI no longer needs base-dir vocabulary. - `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)` — parses `ironclaw_llm::recording::TraceFile` inside the crate that already depends on `ironclaw_llm`. The CLI hands over raw JSON. - the CLI's private `trace_contribution_dir()` now delegates to `contribution::trace_contribution_dir_for_scope(None)` instead of re-deriving `<base>/trace_contributions`. Verified byte-identical: `trace_contribution_dir_for_scope(None)` is `trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`, whose `None` arm returns `base.join("trace_contributions")`. No dependency was added to any crate. Semantics unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(llm): make providers.json a crate asset with a boundary rule (WS6) CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition input + boundary rule added". The provider catalog sat at the **repository root**. A root-level data file has no owning crate, so no boundary rule could govern who edits it, and every consumer compiled it in behind Cargo's back with an escaping `include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json` and the 20 `include_str!("../../../providers.json")` sites in `registry.rs` become in-crate `../assets/providers.json`. ⚠ Correcting the row's inherited premise: a prior lane recorded the "load-bearing include site is in `ironclaw_reborn_cli`" and judged the item "needs a new mechanism, not a new path". Measured on main: the load-bearing site is `crates/ironclaw_llm/src/registry.rs:383` (`builtin_provider_definitions`), inside the owning crate. No new mechanism was needed — only the path. **Path-keyed gates rewritten in the same commit** (WS10: these fail *silently* under a move): - `Dockerfile` — both `COPY providers.json providers.json` lines deleted; `COPY crates/ crates/` already covers the new location in both stages. Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs). - `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path filter and its regex alternative removed; the depth-independent `crates/**` entry already matches. `ws12_workflow_contracts.py` passes. - `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both lanes) classification under the new path rather than letting it fall through to crate scope, so CI breadth does not silently narrow; the now-redundant entry in the reborn-only branch is dropped. **The one consumer that could not simply be repointed.** The CLI's `default_llm_consts_match_the_real_providers_json_nearai_entry` embedded the catalog from five directories up to check its mirrored `DEFAULT_LLM_*` constants. Repointing it would have turned a repo-root reach-in into a *cross-crate* reach-in — the category §11.2.7 turns into a hard failure — and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency rule belongs in the cross-crate suite, so the assertions moved into `ironclaw_architecture` and read both files from disk at runtime, needing no compile-time coupling at all. Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate` in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the asset's location, the repo root's emptiness, and single-embedder ownership). Net test count +0. **The new rule is sabotage-tested** — five cases, each red with the right message, each restored to green: 1. catalog copied back to the repo root -> "must not sit at the repository root" 2. a foreign crate `include_str!`s it -> names the offending file 3. catalog `default_model` drifts from the CLI mirror -> names the const, the field and both files 4. walker pointed at a non-existent dir -> "walked only 0 Rust files ... would pass no matter what the tree contained" (reachability) 5. mirrored const renamed -> "no longer declared as a plain const ... update the extraction rather than deleting the drift check" Case 2 caught a real false positive in the first draft of the guard: a file-level `include_str!` AND `providers.json` conjunction flagged `cli/tests/smoke.rs`, which names the *runtime* `$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something else. The matcher now inspects the macro argument, not the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * docs(skills): rewrite the stale v1 lib.rs charter note (WS6) CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc rewritten". The crate doc claimed "In v1, trust-based tool filtering happens via `src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust labels and the policy engine controls tool access via capability leases." Both halves are dead vocabulary: there is no `src/` monolith on this tree and no Python orchestrator anywhere in Reborn. Replaced with what is true and checkable — this crate owns the trust *label* and none of its enforcement; the ceiling is applied at the capability tier (`host_api` capability/invocation attenuation via `first_party_extension_ports`' activation and execution paths) and the decision belongs to `ironclaw_authorization`. Also points at the existing `SkillTrust` `Ord` safety note, which the old text left unconnected. Doc-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE…
* refactor(contracts): move extension runtime descriptors to a neutral 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>
* refactor(sandbox): merge the sandbox lane into one crate (WS3)
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>
* docs(target-architecture): record the WS3 corrections with their evidence
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>
* chore(sandbox): drop imports the merge left unused
`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>
* fix(ci): let the Reborn PR planner plan guidance edits and crate deletions
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>
* fix(arch): give the retained resource exceptions an owning issue, not 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>
* test(contracts): pin the asset-path validator that moved into extension_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>
* test(coverage): re-capture the host_runtime floor and floor the new sandbox 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>
* docs(target-architecture): record the coverage ratchet as a move-sensitive 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>
* fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath
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>
* test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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>
* docs(sandbox,mcp): correct the wiring inventory and record the projection 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>
* refactor(extensions): move the skill-install executor to extension_support (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>
* docs(sandbox): record that the Docker fail-closed switch is wired to 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>
* docs(host_runtime): record the executor/adapter seam in crate guidance
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>
* refactor(host_runtime): keep the install-input error path log-free
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>
* ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832
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>
* refactor(wasm): move wit/ inside its owning crate (Wave 3)
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>
* build(wasm): rebuild first-party artifacts for the moved wit/ path
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>
* docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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>
* ci(planner): classify the path classes that blocked the wit/ move
`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>
* refactor(host-runtime): split obligations into its three chartered owners (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>
* refactor(operator,contracts): route operator secrets through a product_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>
* test(sandbox): put the Docker security check behind the fail-closed gate
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>
* docs(reborn): stop calling the unwired script lane an execution lane
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>
* fix(ci): pin the WIT scope probes and the embedded-asset owner pairing
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>
* docs(host-runtime): state the obligation visibility rule as it holds
Review catch (#7090): the guardrail sentence promised "cross-owner access is
`pub(super)`, never `pub(crate)`", which is stronger than the code. Verified:
`RuntimeSecretInjectionStore::{insert, take, clone_material,
discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take,
discard_for_capability}` and both constructors are `pub(crate)` and must stay
so — `src/egress/{mod,host_port,credential}.rs` call them, and that is
host-runtime composition outside `obligations/`.
The rule is restated as the property that actually holds: a method whose only
callers are inside `obligations/` is `pub(super)` (the three that are), and
`pub(crate)` is what the stores expose to the egress pipeline they exist to
serve. A future agent reading the old sentence would have read the existing
`pub(crate)` methods as violations.
Guidance-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(architecture): put the operator secrets boundary entry on the right rule
Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"`
entry landed in `ironclaw_extension_contracts`'s forbidden vector, not
`ironclaw_operator`'s. The suite still passed, because `extension_contracts`
has no such dependency and `ironclaw_operator` then had no entry at all — so
the guard this row exists to add was inert, and a green architecture suite was
evidence of nothing. Reintroducing the edge would have passed every check.
Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its
`origin/main` content byte-for-byte.
Negative-probed rather than assumed. With `ironclaw_secrets` temporarily
re-added to `crates/ironclaw_operator/Cargo.toml`:
reborn_crate_dependency_boundaries_hold ... FAILED
ironclaw_operator must not have a normal dependency on ironclaw_secrets
and with the manifest restored, 35/35 pass.
Two further review findings, both verified before being accepted:
- `ironclaw_extension_manager` **does** have a `boundary_rules()` entry
(`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL
§8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale
and is marked superseded. The real gap is narrower and now stated: the rule
exists and simply does not forbid `ironclaw_secrets` (#7095).
- `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules".
Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated
`test_support`), and the table was missing `ironhub` **before** this branch
touched it. Count corrected to twenty-six and the missing `ironhub` row
added, so the inventory matches `lib.rs`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(sandbox): state the Docker-gate claim as the search that checks it
Review caught a false inventory in the Known debt entry, and the previous
commit is what made it false: "the name appears only in docker_gate.rs and
attribution_tests.rs" stopped holding the moment docker_security.rs gained a
module doc naming the variable, and CLAUDE.md itself was already a third
counterexample.
The narrower claim is the one that was always meant and is the one that
matters, so it now carries its own reproduction: no workflow, script, env file
or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/
*.toml/*.py/*.json/.env* is empty here and on main -- and the sole code
reference is a read, std::env::var(...) at docker_gate.rs:23. Every other
occurrence is a doc comment or a panic message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6)
PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move
"behind the triggers/kernel seam it guards". It was not a module: it was
three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire`
— one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter`
— holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`.
That placement is a fail-open: a guard that lives inside one implementation
of a port is lost the moment a second implementation exists, and nothing in
the tree forced a new submitter to re-run it.
The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed
`TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter
of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now
fallible and calls the new `ironclaw_triggers::prompt_safety` first, making
"this prompt passed the trusted-prompt scan" an invariant of the type rather
than a step some submitter performs. `new_for_test` delegates to `new`, so
the test-support seal bypasses visibility only, never the scan.
Behaviour at the fire level is unchanged — same rejection point, same
`TriggerError::InvalidMaterialization`, same permanent disposition — and
composition's pre-materialization scan is untouched, so defence in depth
survives with the second scan relocated and now covering every submitter.
`ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its
only use). Enforcement: triggers' boundary rule stops forbidding
`ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge,
not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations`
forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"),
a crate that was unruled until now.
Regression coverage at the caller tier, not on the helper:
`tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached`
drives the real `TriggerPollerWorker::tick_once` with a materializer that
does NOT scan and a submitter configured to accept, and asserts the
submitter is never reached. A companion pins that a medium-severity-only
prompt still submits, so the mint cannot drift into a blanket filter.
Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173
(+2 worker, +2 prompt_safety unit), architecture 206 -> 206.
LAYER_MATRIX_EXCEPTIONS unchanged at 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(coverage): re-anchor the exemptions the merge shifted
tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and
auto-merges silently. #7096's additions to ironclaw_reborn_composition moved
four entries' subject lines by +2 without anything flagging it; a stranded
entry makes the changed-coverage validator abort with no verdict at all.
Re-anchored by content (difflib line map from the #7065 tree, which the file
was validated against, to the union) rather than by arithmetic:
runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085]
runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435]
lib.rs [616] -> [618]
All 142 entries / 1124 line references re-verified against the merged tree:
0 drift, 0 out-of-bounds, 0 missing paths.
* refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4)
Two CHECKLIST rows, both of which were a one-line manifest correction rather
than a code move: the family docs already placed both crates where the rows
want them and only `Cargo.toml`'s `layer =` disagreed.
processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes
among the kernel crates. The re-layer makes processes -> resources a
kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate
said so itself:
Stale IronClaw crate layer matrix exceptions:
ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed
in W7: runtime process management still depends on resource contracts
currently classed with kernel behavior
That is the gate's verdict, not a judgement call - deleting the entry is the
only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list).
Checked the direction both ways: all nine crates that take a normal dependency
on processes (capabilities, turns, host_runtime, extension_host, loop_host,
extension_manager, runner, reborn_composition, stress) are kernel or above, so
the move legalizes an edge without forbidding an existing one.
skills -> substrates (WS4 SS3.D). families/domains.md already lists
ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies
are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both
at or below substrates, and its six consumers are all loops or above. No
exception moves in either direction.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
cargo check --workspace --all-targets: clean.
* docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence
Every tick was verified against the merged tree, never against a PR title.
TICKED:
- sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and
ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one
manifest in the workspace.
- mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only,
0 production ironclaw_extensions:: refs in src/.
- skills -> substrates: landed here.
- hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four
rejected alternatives and the evidence they are already converged on one
trait plus a shared conformance suite. #6945 read first as the row demands,
and explicitly NOT discharged: this PR changes nothing in the dispatch path.
- WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10
exceptions carried removes_in = W7). Corrected with the replaced text
quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder
named with its owning field value. Ticked on the corrected condition.
LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave:
- first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime.
Ticking would be false.
- processes/capabilities row: re-layer DONE; the capabilities/host.rs split is
deferred with every module boundary already computed (4,560 lines, the six
workflow ranges, and the arch-exempt waiver that must be deleted with it).
- host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs
RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same
section's Keeps clause; zero external references to either). Catalog half
cannot go to extension_host at all - host_runtime is itself a production
consumer at memory_native_extension.rs:96,101, so the move is a
kernel -> products edge and a Cargo cycle. Correct destination is downward.
- network test_rewrite: NOT executed. Recorded the security shape (production
binaries compile the seam and honour the rewrite env var at runtime) and the
full 6-step plan, because the env var is how the entire E2E suite redirects
vendor traffic through the production binary and the change needs feature
forwarding into CI lanes I cannot verify here.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
* refactor(traces): drop the boundary-laundering re-export modules (WS6)
PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules
(`recording`, `paths`) — consumers import the owners".
`ironclaw_reborn_traces::{recording, paths}` were two `pub use <other
crate>::*` passthroughs whose own doc comments stated their purpose
plainly: "so reborn-cli does not need a direct `ironclaw_llm`
dependency, preserving the architectural boundary". They preserved
nothing — the edge existed either way; the wildcard only hid which crate
owned the type, so the dependency graph read as a lie.
All three call sites were in `ironclaw_reborn_cli`. Note the literal
reading of "consumers import the owners" is not available here: the CLI's
dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`)
deliberately excludes `ironclaw_llm`, so importing the owner would have
traded a laundered re-export for a breached, tested boundary. Satisfied
instead by giving the owning crate the operation, which is what the
laundering was standing in for:
- `onboarding::onboard_instance(invite, consents)` — resolves the
contribution root itself. Path layout under the base dir is this
crate's own knowledge; the CLI no longer needs base-dir vocabulary.
- `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)`
— parses `ironclaw_llm::recording::TraceFile` inside the crate that
already depends on `ironclaw_llm`. The CLI hands over raw JSON.
- the CLI's private `trace_contribution_dir()` now delegates to
`contribution::trace_contribution_dir_for_scope(None)` instead of
re-deriving `<base>/trace_contributions`. Verified byte-identical:
`trace_contribution_dir_for_scope(None)` is
`trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`,
whose `None` arm returns `base.join("trace_contributions")`.
No dependency was added to any crate. Semantics unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(llm): make providers.json a crate asset with a boundary rule (WS6)
CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition
input + boundary rule added".
The provider catalog sat at the **repository root**. A root-level data
file has no owning crate, so no boundary rule could govern who edits it,
and every consumer compiled it in behind Cargo's back with an escaping
`include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner
inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json`
and the 20 `include_str!("../../../providers.json")` sites in
`registry.rs` become in-crate `../assets/providers.json`.
⚠ Correcting the row's inherited premise: a prior lane recorded the
"load-bearing include site is in `ironclaw_reborn_cli`" and judged the
item "needs a new mechanism, not a new path". Measured on main: the
load-bearing site is `crates/ironclaw_llm/src/registry.rs:383`
(`builtin_provider_definitions`), inside the owning crate. No new
mechanism was needed — only the path.
**Path-keyed gates rewritten in the same commit** (WS10: these fail
*silently* under a move):
- `Dockerfile` — both `COPY providers.json providers.json` lines deleted;
`COPY crates/ crates/` already covers the new location in both stages.
Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs).
- `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path
filter and its regex alternative removed; the depth-independent
`crates/**` entry already matches. `ws12_workflow_contracts.py` passes.
- `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both
lanes) classification under the new path rather than letting it fall
through to crate scope, so CI breadth does not silently narrow; the
now-redundant entry in the reborn-only branch is dropped.
**The one consumer that could not simply be repointed.** The CLI's
`default_llm_consts_match_the_real_providers_json_nearai_entry` embedded
the catalog from five directories up to check its mirrored `DEFAULT_LLM_*`
constants. Repointing it would have turned a repo-root reach-in into a
*cross-crate* reach-in — the category §11.2.7 turns into a hard failure —
and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency
rule belongs in the cross-crate suite, so the assertions moved into
`ironclaw_architecture` and read both files from disk at runtime, needing
no compile-time coupling at all.
Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the
removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate`
in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the
asset's location, the repo root's emptiness, and single-embedder
ownership). Net test count +0.
**The new rule is sabotage-tested** — five cases, each red with the right
message, each restored to green:
1. catalog copied back to the repo root -> "must not sit at the
repository root"
2. a foreign crate `include_str!`s it -> names the offending file
3. catalog `default_model` drifts from the CLI mirror -> names the const,
the field and both files
4. walker pointed at a non-existent dir -> "walked only 0 Rust files ...
would pass no matter what the tree contained" (reachability)
5. mirrored const renamed -> "no longer declared as a plain const ...
update the extraction rather than deleting the drift check"
Case 2 caught a real false positive in the first draft of the guard: a
file-level `include_str!` AND `providers.json` conjunction flagged
`cli/tests/smoke.rs`, which names the *runtime*
`$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something
else. The matcher now inspects the macro argument, not the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci(coverage): recapture the two composed floors from a real measurement
The provisional values were arithmetic - the sum of the two slices' recorded
deltas - and the dispatch caught them, which is the whole reason the brief
demanded a measurement rather than a reconciliation.
Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd:
26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one
skip is the pull_request-gated mutation gate; the two failures are the coverage
report and the roll-up it drags down, i.e. this file doing its job.
ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63%
(17562 / 19814). The composition was wrong by 1300 denominator lines because
both slices measured their delta under the pre-#7083 aggregator, which could
not see crates/extensions/** at all - lines leaving host_runtime for
extension_support vanished from the tree it could measure, so neither branch's
recorded delta describes the post-#7094 world.
ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's
82.64% (6826 / 8260), captured before #7080's executor lines arrived.
floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye
rather than written quietly. Evidence it is composition and not lost tests:
floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more
absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398
with zero test names lost. Same shape as #7094's own ironclaw_runner recapture.
ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657).
The [global] entry is untouched: both moves are crate-to-crate inside the set
the fixed aggregator sees.
* docs(skills): rewrite the stale v1 lib.rs charter note (WS6)
CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc
rewritten".
The crate doc claimed "In v1, trust-based tool filtering happens via
`src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust
labels and the policy engine controls tool access via capability leases."
Both halves are dead vocabulary: there is no `src/` monolith on this tree
and no Python orchestrator anywhere in Reborn.
Replaced with what is true and checkable — this crate owns the trust
*label* and none of its enforcement; the ceiling is applied at the
capability tier (`host_api` capability/invocation attenuation via
`first_party_extension_ports`' activation and execution paths) and the
decision belongs to `ironclaw_authorization`. Also points at the existing
`SkillTrust` `Ord` safety note, which the old text left unconnected.
Doc-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(network): compile the test rewrite seam out of production builds (WS3)
Closes the WS3 network row. Also RETRACTS an overstatement I made in this
row's earlier annotation.
CORRECTION FIRST. The earlier note claimed production binaries compile the
seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone
abl…
* refactor(contracts): move extension runtime descriptors to a neutral 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>
* refactor(sandbox): merge the sandbox lane into one crate (WS3)
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>
* docs(target-architecture): record the WS3 corrections with their evidence
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>
* chore(sandbox): drop imports the merge left unused
`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>
* fix(ci): let the Reborn PR planner plan guidance edits and crate deletions
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>
* fix(arch): give the retained resource exceptions an owning issue, not 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>
* test(contracts): pin the asset-path validator that moved into extension_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>
* test(coverage): re-capture the host_runtime floor and floor the new sandbox 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>
* docs(target-architecture): record the coverage ratchet as a move-sensitive 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>
* fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath
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>
* test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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>
* docs(sandbox,mcp): correct the wiring inventory and record the projection 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>
* refactor(extensions): move the skill-install executor to extension_support (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>
* docs(sandbox): record that the Docker fail-closed switch is wired to 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>
* docs(host_runtime): record the executor/adapter seam in crate guidance
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>
* refactor(host_runtime): keep the install-input error path log-free
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>
* ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832
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>
* refactor(wasm): move wit/ inside its owning crate (Wave 3)
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>
* build(wasm): rebuild first-party artifacts for the moved wit/ path
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>
* docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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>
* ci(planner): classify the path classes that blocked the wit/ move
`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>
* refactor(host-runtime): split obligations into its three chartered owners (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>
* refactor(operator,contracts): route operator secrets through a product_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>
* test(sandbox): put the Docker security check behind the fail-closed gate
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>
* docs(reborn): stop calling the unwired script lane an execution lane
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>
* fix(ci): pin the WIT scope probes and the embedded-asset owner pairing
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>
* docs(host-runtime): state the obligation visibility rule as it holds
Review catch (#7090): the guardrail sentence promised "cross-owner access is
`pub(super)`, never `pub(crate)`", which is stronger than the code. Verified:
`RuntimeSecretInjectionStore::{insert, take, clone_material,
discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take,
discard_for_capability}` and both constructors are `pub(crate)` and must stay
so — `src/egress/{mod,host_port,credential}.rs` call them, and that is
host-runtime composition outside `obligations/`.
The rule is restated as the property that actually holds: a method whose only
callers are inside `obligations/` is `pub(super)` (the three that are), and
`pub(crate)` is what the stores expose to the egress pipeline they exist to
serve. A future agent reading the old sentence would have read the existing
`pub(crate)` methods as violations.
Guidance-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(architecture): put the operator secrets boundary entry on the right rule
Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"`
entry landed in `ironclaw_extension_contracts`'s forbidden vector, not
`ironclaw_operator`'s. The suite still passed, because `extension_contracts`
has no such dependency and `ironclaw_operator` then had no entry at all — so
the guard this row exists to add was inert, and a green architecture suite was
evidence of nothing. Reintroducing the edge would have passed every check.
Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its
`origin/main` content byte-for-byte.
Negative-probed rather than assumed. With `ironclaw_secrets` temporarily
re-added to `crates/ironclaw_operator/Cargo.toml`:
reborn_crate_dependency_boundaries_hold ... FAILED
ironclaw_operator must not have a normal dependency on ironclaw_secrets
and with the manifest restored, 35/35 pass.
Two further review findings, both verified before being accepted:
- `ironclaw_extension_manager` **does** have a `boundary_rules()` entry
(`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL
§8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale
and is marked superseded. The real gap is narrower and now stated: the rule
exists and simply does not forbid `ironclaw_secrets` (#7095).
- `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules".
Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated
`test_support`), and the table was missing `ironhub` **before** this branch
touched it. Count corrected to twenty-six and the missing `ironhub` row
added, so the inventory matches `lib.rs`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(sandbox): state the Docker-gate claim as the search that checks it
Review caught a false inventory in the Known debt entry, and the previous
commit is what made it false: "the name appears only in docker_gate.rs and
attribution_tests.rs" stopped holding the moment docker_security.rs gained a
module doc naming the variable, and CLAUDE.md itself was already a third
counterexample.
The narrower claim is the one that was always meant and is the one that
matters, so it now carries its own reproduction: no workflow, script, env file
or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/
*.toml/*.py/*.json/.env* is empty here and on main -- and the sole code
reference is a read, std::env::var(...) at docker_gate.rs:23. Every other
occurrence is a doc comment or a panic message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6)
PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move
"behind the triggers/kernel seam it guards". It was not a module: it was
three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire`
— one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter`
— holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`.
That placement is a fail-open: a guard that lives inside one implementation
of a port is lost the moment a second implementation exists, and nothing in
the tree forced a new submitter to re-run it.
The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed
`TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter
of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now
fallible and calls the new `ironclaw_triggers::prompt_safety` first, making
"this prompt passed the trusted-prompt scan" an invariant of the type rather
than a step some submitter performs. `new_for_test` delegates to `new`, so
the test-support seal bypasses visibility only, never the scan.
Behaviour at the fire level is unchanged — same rejection point, same
`TriggerError::InvalidMaterialization`, same permanent disposition — and
composition's pre-materialization scan is untouched, so defence in depth
survives with the second scan relocated and now covering every submitter.
`ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its
only use). Enforcement: triggers' boundary rule stops forbidding
`ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge,
not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations`
forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"),
a crate that was unruled until now.
Regression coverage at the caller tier, not on the helper:
`tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached`
drives the real `TriggerPollerWorker::tick_once` with a materializer that
does NOT scan and a submitter configured to accept, and asserts the
submitter is never reached. A companion pins that a medium-severity-only
prompt still submits, so the mint cannot drift into a blanket filter.
Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173
(+2 worker, +2 prompt_safety unit), architecture 206 -> 206.
LAYER_MATRIX_EXCEPTIONS unchanged at 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(coverage): re-anchor the exemptions the merge shifted
tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and
auto-merges silently. #7096's additions to ironclaw_reborn_composition moved
four entries' subject lines by +2 without anything flagging it; a stranded
entry makes the changed-coverage validator abort with no verdict at all.
Re-anchored by content (difflib line map from the #7065 tree, which the file
was validated against, to the union) rather than by arithmetic:
runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085]
runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435]
lib.rs [616] -> [618]
All 142 entries / 1124 line references re-verified against the merged tree:
0 drift, 0 out-of-bounds, 0 missing paths.
* refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4)
Two CHECKLIST rows, both of which were a one-line manifest correction rather
than a code move: the family docs already placed both crates where the rows
want them and only `Cargo.toml`'s `layer =` disagreed.
processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes
among the kernel crates. The re-layer makes processes -> resources a
kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate
said so itself:
Stale IronClaw crate layer matrix exceptions:
ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed
in W7: runtime process management still depends on resource contracts
currently classed with kernel behavior
That is the gate's verdict, not a judgement call - deleting the entry is the
only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list).
Checked the direction both ways: all nine crates that take a normal dependency
on processes (capabilities, turns, host_runtime, extension_host, loop_host,
extension_manager, runner, reborn_composition, stress) are kernel or above, so
the move legalizes an edge without forbidding an existing one.
skills -> substrates (WS4 SS3.D). families/domains.md already lists
ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies
are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both
at or below substrates, and its six consumers are all loops or above. No
exception moves in either direction.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
cargo check --workspace --all-targets: clean.
* docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence
Every tick was verified against the merged tree, never against a PR title.
TICKED:
- sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and
ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one
manifest in the workspace.
- mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only,
0 production ironclaw_extensions:: refs in src/.
- skills -> substrates: landed here.
- hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four
rejected alternatives and the evidence they are already converged on one
trait plus a shared conformance suite. #6945 read first as the row demands,
and explicitly NOT discharged: this PR changes nothing in the dispatch path.
- WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10
exceptions carried removes_in = W7). Corrected with the replaced text
quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder
named with its owning field value. Ticked on the corrected condition.
LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave:
- first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime.
Ticking would be false.
- processes/capabilities row: re-layer DONE; the capabilities/host.rs split is
deferred with every module boundary already computed (4,560 lines, the six
workflow ranges, and the arch-exempt waiver that must be deleted with it).
- host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs
RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same
section's Keeps clause; zero external references to either). Catalog half
cannot go to extension_host at all - host_runtime is itself a production
consumer at memory_native_extension.rs:96,101, so the move is a
kernel -> products edge and a Cargo cycle. Correct destination is downward.
- network test_rewrite: NOT executed. Recorded the security shape (production
binaries compile the seam and honour the rewrite env var at runtime) and the
full 6-step plan, because the env var is how the entire E2E suite redirects
vendor traffic through the production binary and the change needs feature
forwarding into CI lanes I cannot verify here.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
* refactor(traces): drop the boundary-laundering re-export modules (WS6)
PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules
(`recording`, `paths`) — consumers import the owners".
`ironclaw_reborn_traces::{recording, paths}` were two `pub use <other
crate>::*` passthroughs whose own doc comments stated their purpose
plainly: "so reborn-cli does not need a direct `ironclaw_llm`
dependency, preserving the architectural boundary". They preserved
nothing — the edge existed either way; the wildcard only hid which crate
owned the type, so the dependency graph read as a lie.
All three call sites were in `ironclaw_reborn_cli`. Note the literal
reading of "consumers import the owners" is not available here: the CLI's
dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`)
deliberately excludes `ironclaw_llm`, so importing the owner would have
traded a laundered re-export for a breached, tested boundary. Satisfied
instead by giving the owning crate the operation, which is what the
laundering was standing in for:
- `onboarding::onboard_instance(invite, consents)` — resolves the
contribution root itself. Path layout under the base dir is this
crate's own knowledge; the CLI no longer needs base-dir vocabulary.
- `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)`
— parses `ironclaw_llm::recording::TraceFile` inside the crate that
already depends on `ironclaw_llm`. The CLI hands over raw JSON.
- the CLI's private `trace_contribution_dir()` now delegates to
`contribution::trace_contribution_dir_for_scope(None)` instead of
re-deriving `<base>/trace_contributions`. Verified byte-identical:
`trace_contribution_dir_for_scope(None)` is
`trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`,
whose `None` arm returns `base.join("trace_contributions")`.
No dependency was added to any crate. Semantics unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(llm): make providers.json a crate asset with a boundary rule (WS6)
CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition
input + boundary rule added".
The provider catalog sat at the **repository root**. A root-level data
file has no owning crate, so no boundary rule could govern who edits it,
and every consumer compiled it in behind Cargo's back with an escaping
`include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner
inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json`
and the 20 `include_str!("../../../providers.json")` sites in
`registry.rs` become in-crate `../assets/providers.json`.
⚠ Correcting the row's inherited premise: a prior lane recorded the
"load-bearing include site is in `ironclaw_reborn_cli`" and judged the
item "needs a new mechanism, not a new path". Measured on main: the
load-bearing site is `crates/ironclaw_llm/src/registry.rs:383`
(`builtin_provider_definitions`), inside the owning crate. No new
mechanism was needed — only the path.
**Path-keyed gates rewritten in the same commit** (WS10: these fail
*silently* under a move):
- `Dockerfile` — both `COPY providers.json providers.json` lines deleted;
`COPY crates/ crates/` already covers the new location in both stages.
Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs).
- `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path
filter and its regex alternative removed; the depth-independent
`crates/**` entry already matches. `ws12_workflow_contracts.py` passes.
- `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both
lanes) classification under the new path rather than letting it fall
through to crate scope, so CI breadth does not silently narrow; the
now-redundant entry in the reborn-only branch is dropped.
**The one consumer that could not simply be repointed.** The CLI's
`default_llm_consts_match_the_real_providers_json_nearai_entry` embedded
the catalog from five directories up to check its mirrored `DEFAULT_LLM_*`
constants. Repointing it would have turned a repo-root reach-in into a
*cross-crate* reach-in — the category §11.2.7 turns into a hard failure —
and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency
rule belongs in the cross-crate suite, so the assertions moved into
`ironclaw_architecture` and read both files from disk at runtime, needing
no compile-time coupling at all.
Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the
removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate`
in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the
asset's location, the repo root's emptiness, and single-embedder
ownership). Net test count +0.
**The new rule is sabotage-tested** — five cases, each red with the right
message, each restored to green:
1. catalog copied back to the repo root -> "must not sit at the
repository root"
2. a foreign crate `include_str!`s it -> names the offending file
3. catalog `default_model` drifts from the CLI mirror -> names the const,
the field and both files
4. walker pointed at a non-existent dir -> "walked only 0 Rust files ...
would pass no matter what the tree contained" (reachability)
5. mirrored const renamed -> "no longer declared as a plain const ...
update the extraction rather than deleting the drift check"
Case 2 caught a real false positive in the first draft of the guard: a
file-level `include_str!` AND `providers.json` conjunction flagged
`cli/tests/smoke.rs`, which names the *runtime*
`$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something
else. The matcher now inspects the macro argument, not the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci(coverage): recapture the two composed floors from a real measurement
The provisional values were arithmetic - the sum of the two slices' recorded
deltas - and the dispatch caught them, which is the whole reason the brief
demanded a measurement rather than a reconciliation.
Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd:
26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one
skip is the pull_request-gated mutation gate; the two failures are the coverage
report and the roll-up it drags down, i.e. this file doing its job.
ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63%
(17562 / 19814). The composition was wrong by 1300 denominator lines because
both slices measured their delta under the pre-#7083 aggregator, which could
not see crates/extensions/** at all - lines leaving host_runtime for
extension_support vanished from the tree it could measure, so neither branch's
recorded delta describes the post-#7094 world.
ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's
82.64% (6826 / 8260), captured before #7080's executor lines arrived.
floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye
rather than written quietly. Evidence it is composition and not lost tests:
floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more
absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398
with zero test names lost. Same shape as #7094's own ironclaw_runner recapture.
ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657).
The [global] entry is untouched: both moves are crate-to-crate inside the set
the fixed aggregator sees.
* docs(skills): rewrite the stale v1 lib.rs charter note (WS6)
CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc
rewritten".
The crate doc claimed "In v1, trust-based tool filtering happens via
`src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust
labels and the policy engine controls tool access via capability leases."
Both halves are dead vocabulary: there is no `src/` monolith on this tree
and no Python orchestrator anywhere in Reborn.
Replaced with what is true and checkable — this crate owns the trust
*label* and none of its enforcement; the ceiling is applied at the
capability tier (`host_api` capability/invocation attenuation via
`first_party_extension_ports`' activation and execution paths) and the
decision belongs to `ironclaw_authorization`. Also points at the existing
`SkillTrust` `Ord` safety note, which the old text left unconnected.
Doc-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(network): compile the test rewrite seam out of production builds (WS3)
Closes the WS3 network row. Also RETRACTS an overstatement I made in this
row's earlier annotation.
CORRECTION FIRST. The earlier note claimed production binaries compile the
seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone
able to…
…ns (batch of 7 slices) (nearai#7258) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6) PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move "behind the triggers/kernel seam it guards". It was not a module: it was three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire` — one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter` — holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`. That placement is a fail-open: a guard that lives inside one implementation of a port is lost the moment a second implementation exists, and nothing in the tree forced a new submitter to re-run it. The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed `TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now fallible and calls the new `ironclaw_triggers::prompt_safety` first, making "this prompt passed the trusted-prompt scan" an invariant of the type rather than a step some submitter performs. `new_for_test` delegates to `new`, so the test-support seal bypasses visibility only, never the scan. Behaviour at the fire level is unchanged — same rejection point, same `TriggerError::InvalidMaterialization`, same permanent disposition — and composition's pre-materialization scan is untouched, so defence in depth survives with the second scan relocated and now covering every submitter. `ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its only use). Enforcement: triggers' boundary rule stops forbidding `ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge, not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations` forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"), a crate that was unruled until now. Regression coverage at the caller tier, not on the helper: `tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached` drives the real `TriggerPollerWorker::tick_once` with a materializer that does NOT scan and a submitter configured to accept, and asserts the submitter is never reached. A companion pins that a medium-severity-only prompt still submits, so the mint cannot drift into a blanket filter. Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173 (+2 worker, +2 prompt_safety unit), architecture 206 -> 206. LAYER_MATRIX_EXCEPTIONS unchanged at 10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * refactor(traces): drop the boundary-laundering re-export modules (WS6) PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules (`recording`, `paths`) — consumers import the owners". `ironclaw_reborn_traces::{recording, paths}` were two `pub use <other crate>::*` passthroughs whose own doc comments stated their purpose plainly: "so reborn-cli does not need a direct `ironclaw_llm` dependency, preserving the architectural boundary". They preserved nothing — the edge existed either way; the wildcard only hid which crate owned the type, so the dependency graph read as a lie. All three call sites were in `ironclaw_reborn_cli`. Note the literal reading of "consumers import the owners" is not available here: the CLI's dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`) deliberately excludes `ironclaw_llm`, so importing the owner would have traded a laundered re-export for a breached, tested boundary. Satisfied instead by giving the owning crate the operation, which is what the laundering was standing in for: - `onboarding::onboard_instance(invite, consents)` — resolves the contribution root itself. Path layout under the base dir is this crate's own knowledge; the CLI no longer needs base-dir vocabulary. - `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)` — parses `ironclaw_llm::recording::TraceFile` inside the crate that already depends on `ironclaw_llm`. The CLI hands over raw JSON. - the CLI's private `trace_contribution_dir()` now delegates to `contribution::trace_contribution_dir_for_scope(None)` instead of re-deriving `<base>/trace_contributions`. Verified byte-identical: `trace_contribution_dir_for_scope(None)` is `trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`, whose `None` arm returns `base.join("trace_contributions")`. No dependency was added to any crate. Semantics unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(llm): make providers.json a crate asset with a boundary rule (WS6) CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition input + boundary rule added". The provider catalog sat at the **repository root**. A root-level data file has no owning crate, so no boundary rule could govern who edits it, and every consumer compiled it in behind Cargo's back with an escaping `include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json` and the 20 `include_str!("../../../providers.json")` sites in `registry.rs` become in-crate `../assets/providers.json`. ⚠ Correcting the row's inherited premise: a prior lane recorded the "load-bearing include site is in `ironclaw_reborn_cli`" and judged the item "needs a new mechanism, not a new path". Measured on main: the load-bearing site is `crates/ironclaw_llm/src/registry.rs:383` (`builtin_provider_definitions`), inside the owning crate. No new mechanism was needed — only the path. **Path-keyed gates rewritten in the same commit** (WS10: these fail *silently* under a move): - `Dockerfile` — both `COPY providers.json providers.json` lines deleted; `COPY crates/ crates/` already covers the new location in both stages. Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs). - `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path filter and its regex alternative removed; the depth-independent `crates/**` entry already matches. `ws12_workflow_contracts.py` passes. - `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both lanes) classification under the new path rather than letting it fall through to crate scope, so CI breadth does not silently narrow; the now-redundant entry in the reborn-only branch is dropped. **The one consumer that could not simply be repointed.** The CLI's `default_llm_consts_match_the_real_providers_json_nearai_entry` embedded the catalog from five directories up to check its mirrored `DEFAULT_LLM_*` constants. Repointing it would have turned a repo-root reach-in into a *cross-crate* reach-in — the category §11.2.7 turns into a hard failure — and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency rule belongs in the cross-crate suite, so the assertions moved into `ironclaw_architecture` and read both files from disk at runtime, needing no compile-time coupling at all. Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate` in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the asset's location, the repo root's emptiness, and single-embedder ownership). Net test count +0. **The new rule is sabotage-tested** — five cases, each red with the right message, each restored to green: 1. catalog copied back to the repo root -> "must not sit at the repository root" 2. a foreign crate `include_str!`s it -> names the offending file 3. catalog `default_model` drifts from the CLI mirror -> names the const, the field and both files 4. walker pointed at a non-existent dir -> "walked only 0 Rust files ... would pass no matter what the tree contained" (reachability) 5. mirrored const renamed -> "no longer declared as a plain const ... update the extraction rather than deleting the drift check" Case 2 caught a real false positive in the first draft of the guard: a file-level `include_str!` AND `providers.json` conjunction flagged `cli/tests/smoke.rs`, which names the *runtime* `$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something else. The matcher now inspects the macro argument, not the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * docs(skills): rewrite the stale v1 lib.rs charter note (WS6) CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc rewritten". The crate doc claimed "In v1, trust-based tool filtering happens via `src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust labels and the policy engine controls tool access via capability leases." Both halves are dead vocabulary: there is no `src/` monolith on this tree and no Python orchestrator anywhere in Reborn. Replaced with what is true and checkable — this crate owns the trust *label* and none of its enforcement; the ceiling is applied at the capability tier (`host_api` capability/invocation attenuation via `first_party_extension_ports`' activation and execution paths) and the decision belongs to `ironclaw_authorization`. Also points at the existing `SkillTrust` `Ord` safety note, which the old text left unconnected. Doc-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TES…
…-tree coverage (WS2 + nearai#7083) (nearai#7094) * refactor(extensions): re-layer the extension registry to substrates (WS2) `ironclaw_extensions` moves `loops` -> `substrates`, which is PROPOSAL §6.8.1's assignment for the crate. Four `LAYER_MATRIX_EXCEPTIONS` fall out with it and the WS0 ratchet baseline drops 10 -> 6. The four were one edge under four names: `host_runtime`, `capabilities`, `mcp` and `scripts` — kernel/runtimes-tier crates — reaching the registry for its manifest DTOs. None is waived and none of those edges is deleted; the registry moved down to a layer every tier above may legally reach. §6.8.1 predicted two of them ("Layer substrates legalizes `capabilities -> extensions` and `host_runtime -> extensions`"); it undercounted, and the amendment records that. The one edge that blocked the move is gone rather than waived. At `substrates` a crate may name only `contracts`/`substrates`, and `ironclaw_extensions` named `ironclaw_trust` (kernel) for exactly one type, `TrustPolicyInput`, used by one method. Every field of that type is already `host_api` vocabulary — `PackageIdentity`, `RequestedTrustClass`, `BTreeSet<CapabilityId>` — and the type names no decision, ceiling, or provenance, so it is requested-trust vocabulary like everything else in `ironclaw_host_api::trust`. It moves there, which is §6.8.1's own prescription ("`trust`-vocabulary via `host_api`"), and every consumer's import is repointed rather than shimmed behind a re-export (.claude/rules/type-placement.md). Behavior-free: a type relocation, a layer declaration, and the exception entries the layer change makes unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(extensions): source package manifests from the inventory, not include_str! (WS2) The WS2 `include_str!` row's named targets — gmail, github, nearai-mcp — plus the two cross-crate manifest reach-ins nearai#7018 added. **nearai-mcp gets its inventory module.** It was the one asset directory of twelve with no module in `ironclaw_extension_support::packages`, so `available_extensions.rs` embedded its manifest and three assets directly. Those embeds move to `packages/nearai.rs` beside every other package's, and `available_extensions.rs` consumes `nearai_bundle()`. It is deliberately **not** a `PACKAGES` entry, and the module says why at length: every other entry is a config-free `fn() -> PackageBundle`, but NEAR AI's shipped `[mcp].server` is a placeholder the host rewrites from the operator's LLM-admin bootstrap config. A config-free builder cannot produce that value, so the *embeds* live with the inventory and the *patch* stays with the endpoint authority. `PackageBundle::manifest_toml` is already a `Cow` precisely so the patched manifest is representable. This makes `ironclaw_extension_support` a normal dependency of `ironclaw_extension_host` instead of a `test-support`-only one. No new dependency cone: the binary already links it, and `extension_host` already built against it under that feature. **github and gmail fixtures are inlined.** Both were test-only reach-ins into a shipped product manifest. Each test needs one property — a v3 manifest asserting first-party trust; a no-channel manifest with an `[admin_configuration]` group — now spelled out in the test that needs it instead of borrowed from 200 lines it does not own. **The two cross-crate sites route through `bundled_packages()`.** slack and telegram carry adapter crates, so `extension_manager`'s manifest reach-ins were classified cross-crate. The tests' stated intent — project the *shipped* field set, not a drifting fixture — is unchanged; only the path changed, from a relative file path to the inventory that owns the bytes. Measured with the §11.2.7 scan: escaping sites 133 -> 128, cross-crate 19 -> 17. `REPORT_ONLY` stays `true` — the 17 survivors belong to three other owners (the support crate's own slack/telegram package crates, host_runtime's seven memory-provider embeds, and four test-only doc reach-ins in `operator`/`product`), none of which this row owns. The doc amendment names each. The `("…/available_extensions.rs", "nearai-mcp")` specificity carve-out is deleted with the embed it covered; the allowlist is shrink-only and staleness-checked, so leaving it would fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: make Reborn coverage aggregation nested-tree-safe (nearai#7083) Coverage was structurally dark for every crate under `crates/extensions/`. `reborn_coverage_lcov.py` keyed on `crates/(ironclaw_[A-Za-z0-9_]+)/` — a literal path shape requiring `ironclaw_*` *directly* under `crates/` — and the `if match:` it gated guards the global aggregate as well as the per-crate table, so an unmatched record left both numerator and denominator. Five crate directories (~33.7k instrumented lines) contributed nothing to a gate reading `enforce = true`, and nothing said so: there is no `else`, no counter, no warning. This is a regression, not a never-worked condition. All five were flat `crates/ironclaw_*` until nearai#7037 colocated packages three days ago; the module has one commit in its history and predates the move. The `[global]` floor was captured 2026-07-30, before that, so the denominator has silently shrunk under the gate that enforces it. A better regex cannot fix it. Four of the five directory basenames contain no `ironclaw_` at all (`packages/slack`, `telegram`, `mem0`, `memory-native` — PROPOSAL §5.1 names package directories by extension identity), and a greedy nested pattern mis-attributes an in-crate `src/ironclaw_*/` module directory to a crate that does not exist. So the fix is the one the *merge* script one step upstream already applies: anchor on the discovered crate inventory (`crate_tree.py`). The data was always in the merged lcov; only the aggregator was blind, and the two disagreeing is what made the hole silent. Three consequences worth stating: - **The accounting key is the crate directory basename** — what `crate_tree.crate_directory()` resolves by and what `classify-test-scope.sh` keys on. Every existing floor and exemption key is already a basename, so none churns; basenames also survive the family moves still ahead (`crates/ironclaw_llm` -> `crates/substrates/ironclaw_llm`). - **Separate workspace roots are excluded explicitly**, checked *before* the crate pattern. "Outermost wins" would otherwise attribute `packages/slack/wasm-src/` to `packages/slack/`, putting never-compiled guest code in a denominator. Same precedence `reborn_changed_coverage.py` applies. - **It fails closed.** No discoverable crate tree is now a refusal, not a percentage computed over an empty inventory — the WS10 rule this whole class of bug violates. Regression proof: six new cases (A6b/A6c/A6d/A6e, R19/R19b) covering a nested crate in the table *and* the aggregate, a non-`ironclaw` basename, a separate-workspace guest, a vendored third-party `crates/` subtree, the fail-closed refusal, and a floored nested crate passing and failing its covered-lines floor. The suite gains a shared fixture crate tree, because the aggregator now needs one — the old shape needed no tree at all, which is exactly why every case stayed green while 11 crates went dark. Local: 166/178, with the same 12 pre-existing macOS bash-3.2 `mapfile` failures in the C section that the tree has today (148/160 before this change). Floors for the newly-visible crates are captured separately, from a real coverage run — floors invented without a measurement would bake the hole in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(extensions): restore behavioural coverage for the retired slack_user migration `remove_retired_internal_installation` has had **no behavioural coverage since nearai#6616**, which deleted `restore_removes_retired_slack_user_installation_without_catalog_entry` and replaced it with `assert_eq!(RETIRED_SLACK_USER_EXTENSION_ID, "slack_user")` — a constant compared to its own literal. That gap matters more than most: the branch runs on **every boot** and **destructively** deletes persisted installation rows, and its disposition is an open owner decision (PROPOSAL §12.11 D-I, escalated 2026-08-02, deliberately not ruled by the delegated-authority pass). D-I's recommended sequencing lists restoring this coverage as step (i), "required either way" — so it lands now, whichever way the owner rules, and the behavior is untouched. Extends the existing crate-integration suite rather than adding a file: it already drives `restore_extension_lifecycle_state` over a real `ExtensionInstallationStore` on a real `RootFilesystem`, which is the whole seam this branch lives on. Pins the ACTUAL behavior, including the parts that read as surprising: - Both port reads return `None` — `delete_installation` alone deliberately leaves the manifest projection authoritative, so the branch's second store call is load-bearing and the test says so. - **"Deleted" means tombstoned, not erased.** The v2 record survives with `removed_at` stamped, `removal_cleanup_pending` converged, and the embedded manifest retained; only the two legacy projections are hard-deleted. A test asserting erasure would pin a contract this code does not implement and would hide that the migration is recoverable evidence rather than data loss. - **The control**: a second, equally uncatalogued installation must survive. Deletion keys on the extension id, never on "the catalog could not resolve it". Both halves red-checked against the live tree: disabling the branch fails the removal assertions (and only this test); widening it to delete every catalog-miss row fails the control. Neither the branch nor any other production file is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): record the Wave 2 closeout and its five corrections Amendments for the WS2 work in this PR, each quoting the text it replaces. **PLAN Wave 2 ✎ note** — its open-item list was stale within a day: strays landed as nearai#7040, and package colocation, the telegram merge and the memory-provider move all landed as nearai#7037. Four carry-forwards: the re-layer is two independent halves and only one was reachable; a *downward* re-layer is costed from the crate's own manifest, mirroring Wave 3's finding that an *upward* one is costed from its consumer set; a stale wave list costs a slot its first hour, so re-measure the list itself; and branch names collide across parallel worktrees — verify `git ls-remote` matches your tip before trusting a run attached to it. **PROPOSAL §6.8.1** — "two more W7 exceptions gone" is wrong by two. Four fall: `mcp` and `scripts` reach the registry for the same manifest DTOs `capabilities` and `host_runtime` do. Baseline 10 -> 6. The entry's own `Deps` line turned out to be the executable instruction for the one blocking edge. **PROPOSAL §12.11 D-A** — the ruling stands; its sizing does not. "The seam is narrow, which is why this is cheap" is true of `channel_host.rs` and false of the crate: twelve production files name `ironclaw_product`, and `ironclaw_host_ingress` is a second blocking edge D-A never named. Recorded as an amendment rather than a silent re-scope so the next slot costs it from evidence. Filed as nearai#7092. **CHECKLIST WS2** — the `include_str!` row ticks with the per-owner breakdown of the 17 surviving cross-crate sites and why `REPORT_ONLY` cannot flip on them (nearai#7093); the re-layer row records the half that landed and the half that did not, with the twelve-file measurement; the escalated §12.11 D-I row records that its own step (i) is done and the escalation is unaffected. **CHECKLIST WS10** — the path-keyed-gates row missed a sixth gate, one step down the same pipeline it audited (nearai#7083). Two corrections to how that row framed the risk: fix a path-keyed gate along its whole pipeline, not at the file the audit opened; and the dark-verdict failure is not only about `git mv` — package colocation broke this one first, because a gate keyed on a name shape fails for any tree change, not just the scheduled one. Crate guides travelling with the change: `ironclaw_trust`'s AGENTS/CLAUDE/ CONTRACT stop claiming `TrustPolicyInput`; `ironclaw_extensions`'s AGENTS records its `substrates` layer and that it must not regain `ironclaw_trust`; `ironclaw_extension_support`'s AGENTS records why `nearai` is a package module that is deliberately not a `PACKAGES` entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: floor the crates the coverage fix made visible (nearai#7083) Captured from this PR's own dispatch run 30865483401 at `4c841a4321`, **after** the aggregator fix. Capturing beforehand would have recorded zeros and pinned the hole shut, which is the one outcome nearai#7083 exists to prevent. Four new `[[crate]]` entries — `ironclaw_extension_support` (82.64%, 6826 / 8260), `slack` (93.95%, 3697 / 3935), `telegram` (90.31%, 1435 / 1589), `memory-native` (82.85%, 2850 / 3440). None of the four lacked a floor because anyone judged it unworthy of one; they were invisible to the gate. All four were compiled, instrumented, and present in the merged tracefile the whole time. Keys are crate **directory** basenames, which is what the aggregator keys on and what every pre-existing entry already is — they coincide with package names only for flat `crates/ironclaw_*` crates. `mem0` is deliberately absent and the file says why: it compiles only behind the `memory-mem0` feature, which no coverage lane enables, so it contributes no instrumented lines and a floor would enforce nothing. `[global]` recaptured 85.11% / 375097 → **86.96% / 386885**. The old denominator never described the tree it was enforcing: nearai#7037 landed on 2026-08-03 and four crate directories left both numerator and denominator silently, under `enforce = true`. Both numbers are read off the same `RATCHET PASS: global` line of the same `reborn-coverage-ratchet.sh` invocation that enforces this file, so the mapping is the enforcing mapping by construction — the existing comment's caution is about comparing *across* toolchains, which this does not do. `ironclaw_extension_host` recaptured 84.83% / 19907 / 23467 → **87.99% / 21605 / 24554**. It is the SOURCE side of a move (the NEAR AI embeds left for the package inventory), and a source floor is the one that silently stops describing its crate when code leaves it. Recorded honestly as a ratchet tightening rather than a repair: the denominator moved only −1.46%… +4.63%, below this file's own 5% materiality threshold, and both fields rose — partly because this PR also adds the retired-`slack_user` test the crate had been missing since nearai#6616. Verified locally against the run's own `reborn-integration-merged.lcov`: `reborn-coverage-ratchet.sh` exits 0 with 22 `RATCHET PASS` and zero `FAIL`, and every observed figure matches the CI job line-for-line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): refuse colliding crate basenames; tighten the tombstone assertion Review triage on nearai#7094. Two of three findings accepted; the third is declined with reasons, recorded in the PR thread rather than silently skipped. **Accepted — colliding crate basenames must be a refusal (Major).** `crate_key()` reduces a discovered directory to its basename, so two crate directories sharing one would silently fold into a single coverage bucket and a single ratchet floor. That is a *quieter* version of the bug this PR fixes: the merged number looks entirely plausible and nothing reports the merge. `crate_tree.crate_directory()` already refuses an ambiguous basename rather than picking one; this applies the same rule to the aggregation key, raising `CrateTreeError` so it lands on the existing fail-closed path. Unreachable on today's tree — all 65 basenames are distinct — and reachable the moment crates move under family directories, which is the next wave. Pinned by a new self-test case (A6f) with `crates/{domains,substrates}/ironclaw_threads`, asserting the refusal, that both colliding directories are named, and that the message says why a merged number is not offered. **Accepted — `removed_at` presence is not enough.** `Value::get` returns `Some(Value::Null)` for an explicit JSON null, so the tombstone claim would go vacuous if the serializer ever emitted one. Now rejects null explicitly, and the message says why presence alone was insufficient. **Declined — requiring absolute `SF:` paths to be contained under the resolved repo root.** Real in principle; wrong to apply here. `reborn-coverage-merge-lcov.sh` — which produces the tracefile this module reads, and which already filtered every record in it — anchors on the discovered inventory with the identical `(?:^|/)` form and no root containment. Adding containment to the consumer and not the producer re-creates exactly the producer/consumer divergence that made nearai#7083 silent. The documented real-world case (`.../wasmtime-46.0.1/crates/wasmtime/`) is already excluded by inventory anchoring in both, and the scenario the finding describes needs a vendored tree that reproduces a full IronClaw crate directory path *inside an already-filtered lcov*. It would also require rewriting every fixture path in the suite, since they use synthetic absolute prefixes. Self-test 178 -> 181 cases; same 12 pre-existing macOS bash-3.2 C-section failures. Ratchet re-verified against the run's own artifact: exit 0, 22 PASS, 0 FAIL — the captured floors are unaffected (a test-file assertion and a script-level guard change no instrumented line). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(ci): correct the coverage-lib header's entry-point signature `aggregate()` gained a `repo_root` parameter with the nearai#7083 fix and the module header still advertised the three-argument form. Names the default resolution order and points at `crate_pattern()` for why the accounting scope is inventory-derived rather than a path shape. Comment only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(ci): pin producer/consumer agreement on a vendored crate-path collision Review catch (nearai#7094): the vendored fixtures guarding the nearai#7083 coverage fix (M5, A6d) pass because their `wasmtime` path matches no inventory entry — so they prove the inventory filter *runs*, not that it is contained. The adversarial input — a path outside the repository that repeats a discovered crate directory verbatim — was never in the suite. A fixture that passes because its input never reaches the code under test is exactly the failure mode this file exists to kill. A6g supplies that input (`.../foreign-1.0.0/crates/extensions/packages/slack/src/lib.rs`) and pins the property that actually protects the accounting: the producer (`reborn-coverage-merge-lcov.sh`, which filters every record the aggregator ever sees) and the consumer (`lib/reborn_coverage_lcov.py`) make the SAME call on it. Both are asked about the same fixture in parallel — chaining the consumer onto the merge's output would only ever compare it against a record the producer had already dropped, so a producer-only change would stay invisible. That mistake was made and caught here before the case landed. Restricting the consumer alone to paths contained under the resolved repo root was reviewed and not taken: the producer applies the identical `(?:^|/)` inventory anchor with no containment, and producer/consumer disagreement is what made nearai#7083 silent instead of loud. Whichever way the rule goes, it goes in both halves at once. This case is what turns a one-sided change red. Sabotage-probed in both directions rather than assumed green: consumer-only "repo-owned" filter -> FAIL A6g: producer and consumer make the same call ... producer-only "repo-owned" filter -> FAIL A6g: producer and consumer make the same call ... and unsabotaged: 174/186, the same 12 pre-existing macOS bash-3.2 C-section failures as before the change (181 -> 186 cases). Not reachable from the real pipeline as it stands, measured rather than asserted: `cargo llvm-cov` emits only workspace-member sources, so every `SF:` record in a lane tracefile already lives under the checkout root — 1067 of 1067 per lane and 1139 of 1139 merged, read off run 30865483401's own artifacts, with zero records dropped by the inventory filter and zero carrying a `/crates/` segment anywhere but the repo-root position. The guard is for the day that stops being true. Test-only. No production behavior changes and no instrumented line moves, so the coverage floors captured for this PR are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ose four WS2 rows (nearai#7143) * refactor(host-ingress): re-layer products -> substrates (WS2, nearai#7092) `ironclaw_host_ingress` was `layer = "products"` while depending on nothing above `contracts`. That was inherited from the family directory rather than earned, and it was one of the two edges blocking `ironclaw_extension_host`'s `products -> loops` flip: `extension_ingress.rs` holds its `PublicRouteMount` in production code behind the `serve` feature, and `loops -> products` is forbidden. nearai#7092 suspected this was cheaper to fix by re-examining the crate's own layer than by building a seam for it. Measured: the crate is 107 lines of carrier vocabulary — `axum::Router` paired with `ironclaw_host_api` ingress descriptors — with exactly one workspace dependency, `ironclaw_host_api` (`contracts`). The only reason it is not a contracts crate is that `axum` is on the contracts-tier denied list, which is this crate's entire stated purpose ("keeps Axum out of contracts"). `substrates` is the lowest rung it can legally occupy, and families are discoverability groupings rather than trust boundaries, so it stays in `product/` — the same shape as `ironclaw_extension_registry` (`substrates`, in `extensions/`). Proof this unblocks what it claimed: flipping `extension_host` to `loops` locally now reports `ironclaw_product` as the *only* layer-matrix violation. Before this change it reported `ironclaw_host_ingress` as well. The matrix cannot protect the crate on its own — `substrates -> substrates` is legal, so it would admit `ironclaw_filesystem` or `ironclaw_secrets`, either of which would hand every carrier consumer a dependency cone the crate exists to avoid. The ceiling is therefore pinned as an **allowlist** (`{ironclaw_host_ingress, ironclaw_host_api}`) beside the three contracts-purity allowlists, deriving its forbidden set as "every workspace ironclaw crate except these two" so an unanticipated dependency still fails. The crate's CLAUDE.md claimed this rule already had "architecture-test coverage"; it did not — there was no `BoundaryRule` for it and no allowlist. Now there is. Red-checked both halves: adding `ironclaw_filesystem` (a `substrates` crate the layer matrix would have waved through) fails with "ironclaw_host_ingress must not have a normal dependency on ironclaw_filesystem"; restoring it passes. `cargo test -p ironclaw_architecture` — 32 suites, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(extension-host): import product-boundary DTOs from their owner (WS2, nearai#7092) Six of the `ironclaw_product` references `nearai#7092` inventoried are not product dependencies at all — they reach *through* `ironclaw_product` for types `ironclaw_product_contracts` already owns. Repointed to the owner: - `extension_ingress.rs` — `ChannelInboundSurface{Request,Outcome,Admission, RejectedAdmission}` are defined in `product_contracts::surface` (`surface.rs:33-63`); product only re-exports them (`reborn_services.rs:159`). The file already imported `ChannelInboundProductSurface` from that exact module, so the repoint merges into the existing `use`. **This file now names `ironclaw_product` nowhere** — 2 references to 0. - `available_extensions.rs`, `channel_lifecycle.rs`, `product_lifecycle.rs`, `channel_pairing/tests.rs` — `RebornChannelConnectStrategy` is not a product type either. It is `pub use ironclaw_product_contracts::package_lifecycle:: ChannelConnectStrategy as RebornChannelConnectStrategy` (`reborn_services.rs:190`) — an aliased re-export. Only the import *path* moves; the local name is deliberately left as `RebornChannelConnectStrategy` via `as`. Retiring the `Reborn*` prefix is CHECKLIST WS10's type-name row, and `reborn_extension_contract_location_scan.rs:38-42` says so explicitly ("a second *name*, not just a second path; retiring the `Reborn*` prefix is CHECKLIST WS10's type-name row and is not smuggled in here"). A private `use … as` at the call site is an import, not a re-export, which `.claude/rules/type-placement.md` permits for exactly this case. No behaviour changes: every repointed name resolves to the same type it did before, through one fewer crate. `channel_pairing/tests.rs` was already using the `product_contracts::surface` path for two sibling types, so this makes the crate self-consistent. Test accounting: unfiltered `--list` before and after, 417 → 417, byte-identical rosters. `cargo test -p ironclaw_extension_host --all-features` green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(extension-host)!: delete the retired-identity boot migration (owner ruling, D-I) PROPOSAL §12.11 D-I was escalated rather than decided: the delegated pass declined because the branch destructively deletes user-visible installation rows on a path with no audit record, which is empirical about production data. The owner has now ruled (2026-08-04, verbatim): "just get rid of slack_bot and slack_personal etc.. those can get nuked. Don't worry about migration data" Deleted from `lifecycle_restore.rs`: the retired-extension-id constant, the removal branch keyed on it (which ran on every boot and called `delete_installation` + `delete_manifest`), and the string-assertion unit test nearai#6616 left in place of real coverage. **No tombstone, shim, or migration path replaces them** — "don't worry about migration data" is a direction, not just a permission. `slack_bot` / `slack_personal` needed no work. Both were already at zero as extension identities — retired by the NEA-25 stack with no migration at all — and `reborn_retired_taxonomy.rs`'s `RETIRED_IDENTITY_FORMS` has been pinning them there. Re-measured before touching anything; the owner's "etc." turned up no third live identity, only `slack_user` had surviving machinery. Behaviour for a host still carrying such a row: it falls through to the catalog lookup, which cannot resolve it, so restore logs one `warn!` and continues. The row is inert — `installed_summaries` drops it on the catalog miss — and `ironclaw extension remove slack_user` remains the strictly more thorough manual remedy. Coverage, which D-I said was owed either way, is paid against the NEW behaviour. The test nearai#7094 added to pin the branch as built is **replaced, not deleted**: same fixtures, same control row, every assertion about the retired id inverted, and the durable-shape half re-pointed (no `removed_at` tombstone; both legacy projections survive). Two guards, both red-checked: - `restore_special_cases_no_extension_id_and_leaves_every_uncatalogued_row_intact` — re-introducing the branch fails it on the first assertion with "boot restore must not delete any installation by extension id". - `reborn_retired_taxonomy.rs` gains the two identifiers as `RETIRED_TERMS` — re-introducing the constant fails with the file and term named. The `"slack_user"` *string* is deliberately not banned: it is still a legitimate persisted id, a test fixture, and the prefix of the live `slack_user_token` credential handle. What must never return is code keying behaviour on it. The extension-specificity allowlist **shrinks by one**: deleting the branch removed the last "slack" mention from `lifecycle_restore.rs`, and that gate is staleness-checked, so it failed until the now-dead entry was removed. The gate caught this itself. Test accounting (unfiltered `--list`, before/after): 417 -> 416. One removed (`retired_slack_user_id_remains_stable` — its subject is deleted), one renamed in place. Nothing else changed. `cargo test -p ironclaw_architecture` 32 suites 0 failures; `-p ironclaw_extension_host --all-features` green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): close four WS2 rows and size the re-layer from its real blocker Row-by-row, with the evidence each tick rests on. **WS2 heading corrected.** It promised "kills `extension_host→product`", which §6.1.3 structurally forbids — a caller must name product's frozen command/view/capability constants. Row 1 recorded that on 2026-08-01 and PLAN repeats it, but the heading kept promising it and every slot re-derived the same correction. The wave's goal is the layer flip. **§12.11 D-A ticked** — a `[decision]` row whose box was left unticked. Verified against the merged tree: `channel_host.rs` is still in the host, its two `Arc<GenericChannelHostAssembly>` holders still are, `ChannelWorkflowStateFactory` is still the injected precedent. The factory port is NOT built; that is the re-layer row's, which stays open. **§12.11 D-C ticked** — same shape. Pipeline still in the host, ownership constant still names it. Option (c)'s mechanical half is not built; it is intra-crate, creates and removes no cross-crate edge, and is carried forward. **§12.11 D-I ticked** — resolved by OWNER RULING (separate commit), not delegated authority. D-I's findings are kept; the resolution is added beneath. **Strays row ticked, against its own stated criterion** — "a stray only blocks it if it holds an upward edge". Re-measured: none does. `nearai_mcp.rs`, `bundled_skills.rs`, `skill_listing.rs`, `skill_learning.rs` name no products/app symbol; `build.rs` names only `ironclaw_skills` (`loops`). `channel_pairing_serve.rs` is absent from the host and present in webui. The two unexecuted items are dispositions, not deferrals: `bundled_skills`' three candidate homes were each proven illegal, and deleting the `nearai_mcp` fork in favour of operator's would *create* an upward edge. **Verify row ticked, after correcting a category error in its own wording.** It is a *verify* row — the deliverable is the proof mechanism, which its own note already called "built and enforcing". It then stayed open for the end state, which belongs to the re-layer row. A verify row cannot be blocked on the thing it verifies; that makes a biconditional proof unclosable until the property holds, which is backwards for a proof whose value is firing *before* then. Both its clauses hold: the manifest resolves through `cargo metadata` with the dep pinned un-renamed and biconditional on the residue, and the specificity allowlist shrank by one this session. **Re-layer row stays OPEN — flagged, with the remainder measured.** One of the two blocking edges is gone (`host_ingress`, by re-layer). `ironclaw_product` remains at 10 production files / 20 references, down from twelve files, because six references were never product dependencies. But the binding constraint is neither: it is the four-port residue frozen in `reborn_extension_host_port_inversion.rs`, each blocked by a contract-purity fact — `product_contracts` may name only `host_api` + `extension_contracts`, and each port carries `ironclaw_auth` or `ironclaw_conversations` vocabulary. **The work that unblocks the flip is narrowing those two domain vocabularies, not anything inside `extension_host`.** D-A is amended to say so, quoting the "sized from twelve files and two edges" clause it replaces — sizing the flip by file count is the same error as D-A's original sizing by one file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): point the carried-forward WS2 work at nearai#7145 Three rows said "the successor issue to nearai#7092" while that issue did not yet exist. It does now — nearai#7145 — and it carries the measurement the re-layer row was re-scoped onto: the four-port residue, why three of the four need auth/conversations vocabulary narrowed in those domain crates rather than in `extension_host`, the ordered sequence, and the two non-blocking items (D-C option (c), the `nearai_mcp` fork). It also records a correction found while measuring: the residue note says the error "no longer blocks" `ConversationBindingService`, but the trait still literally returns `Result<_, ProductSurfaceFailure>` — the note means the error is no longer an *obstacle* (product absorbs `ProductOperationFailure` with a total `From`), not that the signature was already changed. That swap is part of the slice, not a precondition already met. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(coverage): adjust the extension_host floor for the retired-branch deletion `tests/integration/coverage-floor.toml` requires a same-PR floor update in both directions, and names this exact case: "shrinkage (a legitimate code+test deletion lowering covered lines, even when the denominator barely moves)". The `ironclaw_extension_host` entry was captured two days ago on a tree that still had the retired-identity boot migration — its own rationale says the figure rose "partly because this PR also adds the retired-`slack_user` migration test". This PR deletes that branch by owner ruling, and those 29 production lines were *fully covered* by exactly the test deleted with them. Left alone, `floor_covered_lines = 21605` with its default 20-line tolerance would red the next dispatch on a floor that no longer describes the crate — the same class of staleness nearai#7083 just fixed from the other direction. Deducted 25 instrumented lines from both fields (21605 -> 21580, 24554 -> 24529), one more than the ~24 measured, so a rounding difference cannot red the lane. `floor_percent` 87.99 -> 87.97: removing fully-covered lines barely moves a ratio. **Labelled an adjustment, not a recapture**, in the file itself. No PR-triggered lane produces a coverage verdict (nearai#7036), so these could not be read off this PR's own run; the block says in as many words that the next dispatch should recapture and replace it. The arithmetic is written out so a reviewer can check it rather than trust it. `scripts/ci/test-reborn-coverage.sh`: 174/186, with the same 12 pre-existing macOS bash-3.2 failures in the comment-script section that `main` has today (nearai#7094 documented the identical set; CI runs bash 5). The ratchet cases this edit actually touches — R19/R19b, covered-lines floors — pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(architecture): enforce the host_ingress ceiling in every dependency kind CodeRabbit review, and it is right: the allowlist added in `b50d415` routed through `assert_no_normal_workspace_deps`, which filters to `normal` dependencies. So `[dev-dependencies] ironclaw_filesystem` on `ironclaw_host_ingress` would have passed a gate whose crate guide claimed the crate "may depend on Axum and `ironclaw_host_api`, and on nothing else in the workspace". A guard claiming more than it checks is exactly the class this repo has been bitten by repeatedly, and I wrote the over-claim myself. Replaced with `assert_host_ingress_names_no_other_workspace_crate`, which reads `cargo metadata`'s dependency list directly and rejects any workspace `ironclaw_*` crate but `ironclaw_host_api` in **any** kind — `normal`, `dev`, or `build` — naming the offending kind in the failure. The all-kinds scope is deliberate and deliberately unlike the layer matrix beside it. The matrix checks normal deps only, and correctly: a dev-dependency on a higher layer is legal and used (`extension_host` dev-depends on `ironclaw_product`). But this crate's charter is absolute — `families/product.md` says "Never depends on: anything else, without exception" — and the property it protects, that a consumer wanting only the carrier shapes compiles nothing else, is defeated just as thoroughly by a dev- or build-dependency, which still resolves and builds. The crate has zero of both today, so this costs nothing. The package is looked up with a panic rather than a silent `return`: the shared helper's early-return-when-absent would make the whole check vacuous the day the crate is renamed. Sabotage-tested in all three kinds — `ironclaw_secrets` added as a dev-, then build-, then normal dependency, each caught and each naming its own kind ("Offenders: ironclaw_secrets (dev dependency)"). The previous version caught only the third. Restored green after each. Also fixes the second review finding: the verify row's pre-existing note still read "the box stays open because the end state it verifies is not reached" one sentence after the row was ticked. Quoted and superseded in place rather than deleted, so the reversal is legible — the residue and the dep it names are real and remain tracked on the (still open) re-layer row. `cargo test -p ironclaw_architecture` 32 suites 0 failures; clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): correct the WS2 heading note; lower the specificity ceiling Three fixes from a doc-truth audit of this PR. The first is the important one: the note I added earlier today to correct a mis-statement was itself a mis-statement — the sixth of this seam, landing inside the PR whose stated purpose was finally getting it right. **1. The heading note claimed the edge never has to die. It does.** Superseded text, quoted in place rather than overwritten: "which is not reachable and never was: a caller must name product's frozen command/view/capability constants … the edge becomes legal-by-layer or is replaced by contracts-declared ports; it does not have to vanish." Wrong twice, and it contradicted §5 of this same PR: - **The frozen-constants argument belongs to §12.11 D-B's crates, not this one.** It is what makes `webui`/`openai_compat`/`extension_manager → product` permanent; borrowing it here was a category error. Measured on this branch: `ironclaw_extension_host` production code names **zero** of the **136** `*_COMMAND`/`*_VIEW`/`*_CAPABILITY` constants `ironclaw_product` defines. It names ports, DTOs, concrete drivers, and `PRODUCT_ADAPTER_HOST_API_ID` — nothing from the frozen inventory, and nothing since WS2.4 moved the product face to `extension_manager`. - **"It does not have to vanish" is backwards.** At `loops`, `loops → products` is illegal, so the edge *must* vanish — no legal-by-layer outcome exists for it. The re-layer row already says "delete the manifest edge and flip the layer line in one change", and `the_extension_host_manifest_names_product_only_while_a_residue_needs_it` enforces it as a biconditional. The correct framing is **mis-sequenced, not structurally forbidden**: the edge dies *with* the flip, not at that row. The note now says that and cites the biconditional as the mechanism. **2. The verify row was a checked box asserting something false.** Row text read "Verify …: the `ironclaw_extension_host` manifest lists no product dep under any name" while the manifest still names product. My appended CLOSED note argued the deliverable is the proof mechanism — which holds — but per the CHECKLIST's own convention a checked box means the item landed, and a fast reader would see a false claim. **Reworded the row text**, not just annotated: it now reads as building the move-order proof that *will* verify that end state, armed as a standing biconditional. The end state stays on the still-open re-layer row. **3. `WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE` 129 -> 125.** The constant's own doc says "Lower it in the same PR that deletes entries". This PR deleted the stale `("…/lifecycle_restore.rs", "slack")` entry and did not. Worse, recounting found the ceiling was **already carrying 3 entries of slack on `origin/main`** (126 live vs 129), so three earlier deletions had banked slack instead of a floor — silently, since the ratchet only refuses growth. Setting it to the live count closes both. Counted by script over the entries between `const ALLOWLIST` and its closing `];`, ignoring comment lines, on the pushed ref — not by eye, not with `grep` (which also matches prose). Tracked as nearai#7147; whichever concurrent branch touching this list merges last must recount the union. Sabotage-tested: adding a 126th entry now fails with "ALLOWLIST grew to 126 entries (WS0 baseline 125)" — growth the old 129 ceiling would have absorbed silently. Restored green. `cargo test -p ironclaw_architecture` — 32 suites, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…te the eight doc-truth corrections (nearai#7155) * test(architecture): itemize extension_host's product references as a frozen ledger The products -> loops re-layer has been sized five times from proxies and was wrong five times (D-A's one-file seam, nearai#7092's twelve files, three more since). The trait residue is trait-shaped and cannot see constants, free functions, or inline concrete construction; the manifest biconditional sees the sum but only as a boolean. This adds the itemization: EXTENSION_HOST_PRODUCTION_FILES_STILL_NAMING_PRODUCT — exact-match in both directions, shrink-only under a baseline ceiling, one reason per file, on a whole-token crate matcher (the raw-substring helper would count ironclaw_product_contracts importers). A ledger<->manifest consistency assert keeps the itemization and the biconditional agreeing about whether the edge exists, so the ledger cannot read empty while the manifest still carries the dependency. Sabotage-verified before trusting it: a planted production file naming ironclaw_product reds the gate naming that file; a planted stale row reds the stale direction; comment and string-literal mentions do not register (channel_delivery.rs and skill_learning.rs are the standing comment-only exclusions, and channel_subject_routes.rs's usage is cfg(test)-only). Part of the WS2 re-layer re-scope (nearai#7145). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): amend D-A — re-cite its precedent, record unstarted execution The ruling's two measured legs stand. Its option-(b) refutation cited ChannelWorkflowStateFactory as the in-file precedent; measured, that trait is a sole-impl same-file convenience no architecture test names — the load-bearing precedent is the landed nearai#7004 operator inversion and the ten INVERTED_PORT_IMPLEMENTORS ports, so the amendment re-cites it. Also records that the factory port exists on no ref (decision, not partial execution; shape still open) and that the residue is now mechanically itemized by the reference ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): correct the Wave 2 closeout's twelve-file sizing Six of the twelve were re-export repoints, executed in nearai#7143. The enforced remainder is four reference classes (trait residue, adapter-registry, product free functions, D-A assembly), now itemized mechanically by the reference ledger, with the inventory carried by nearai#7145. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): correct the Wave 3 milestone and the W7 label reading The milestone was wrong three ways: the start was 13 (live register is 6); the ratchet is ceiling-only and pins nothing; and zero is WS12's gate, not this wave's reachable exit — the lane edges are nearai#7067's (whose measurement refutes the WS3 mcp row's vocabulary premise) and conversations->turns is WS5's. Also records that removes_in=W7 is a retired July-train label (nearai#5852 era, introduced 2026-07-09), not Wave 5 or WS7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): give conversations->turns an owning WS5 row; key the register to it The exception's removal condition lived only inside WS1's verify-row explanation — no row owned it, which is how a milestone silently expires (its removes_in=WS5 date already passed once without it falling, as PROPOSAL 8.3's 2026-08-02 amendment records while asking for exactly this re-milestone). Adds the owning WS5 slice row, re-keys the register entry to it, re-keys host_runtime->extension_support to WS3, documents that W7 is the retired July-train label (not Wave 5 or WS7), and marks the WS5 product-narrows adapter_registry clause as a prerequisite of the extension_host re-layer (nearai#7145). The two entries nearai#7141 deletes and the two it re-keys to nearai#7067 are deliberately left untouched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): refute the stale 'delete reasoning.rs (dead)' claims Sections 9 (row 29) and 12.4 still said delete-it-outright while WS8's own execution (nearai#6964) deleted only the dead half and the surviving module is live on main (mod reasoning; + re-exports in llm/src/lib.rs). Acting on the rows as written would have deleted production surface. 6.4.13's own line is amended by the in-flight nearai#7128 and deliberately not touched here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(target-architecture): amend 8.3 — row 7's blocker refuted by nearai#7067; live register is 6 Row 7 promised the lane->resources edges dissolve as vocabulary; nearai#7067's measurement (raised on nearai#7065) shows the vocabulary is already in host_api::resource and imported from there — the real holders are ResourceGovernor (3 of 10 methods used) and the ResourceError cone, whose relocation is an authority carve-out. Also refreshes the live register to 6 post-nearai#7094 and notes the conversations->turns re-milestone landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…earai#7117, nearai#7106, nearai#7099, nearai#7101, nearai#7128) (nearai#7139) * refactor(loop-host): move system-prompt content out of the composition root (WS6) CHECKLIST WS6 "Composition behavior evictions" — the `system-prompt content → owning prompt asset` clause. PROPOSAL §6.10.1 lists it among the items still resident in `ironclaw_reborn_composition`; `families/app.md` already says "prompt content of any kind" never belongs to the app family. The four assets move from `ironclaw_reborn_composition/assets/prompts/` to `ironclaw_loop_host/prompts/`, beside the five prompt assets that crate already ships and beside `identity_context.rs`, whose `HostIdentityContextSource` is what puts them in front of a model. `system_prompt_assets.rs` exports them as `pub const`; composition consumes the consts instead of `include_str!`. Resolved owner is the **loop** half of "loop/product owner": the port is loop_host's, and loop_host already owns `prompts/`. What deliberately did *not* travel: the seeding/validation of the on-disk, user-editable `SYSTEM.md`. That is boot-time `std::fs` work on a real host path and `ironclaw_loop_host` has zero `std::fs` uses — moving it would put host-path I/O into a loops crate. Composition keeps assembly + seeding. The runtime storage path `system/prompts/default-system.md` is unchanged; it is where existing installs' user-edited file lives, so renaming it would be a behavior change, not a move. Enforcement (new, in the same diff): `reborn_composition_boundaries.rs::composition_root_embeds_no_prompt_content` fails on either half of the debt — a re-added `include_str!("….md")` in composition source, or a re-added shipped `.md` asset under the crate that is not crate guidance. Sabotage-checked both halves independently. It is keyed on markdown, not on `include_str!`, so `builtin_capability_policy.toml` (config-as-data, composition's charter) is untouched. Un-masking: - `ironclaw_loop_host` 803 → 806 tests; the diff of the unfiltered `--list` rosters is exactly the three new `system_prompt_assets::tests::*`. - `ironclaw_reborn_composition` 928 → 928; roster diff is empty. - No existing test edited. Docs corrections, each quoting the text it replaces: - CHECKLIST WS6 + PROPOSAL §6.10.1: the `local_dev` misnomer's "one residue: the local variable at `runtime.rs:3016`" is wrong twice. The variable is at `runtime.rs:3095`, and `local_runtime` appears 191 times in composition's `src` — including six public API symbols, the public type `RebornLocalRuntimeIdentity`, and an assembly struct field. `reborn_standalone_typename_ratchet` stayed green because it governs *type* names only. Tracked as #7098 as a pure-rename PR, not folded in here. - PROPOSAL §2: `root/default_system_prompt.rs` is re-described as assembly + seeding now that its content assets are gone. - `families/loop.md` + loop_host `AGENTS.md`/`CLAUDE.md` record the new owner and the enforcing test. Refs #7098 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(ws6): fail-close the markdown ownership gate; fix two stale doc measurements Addresses both CodeRabbit threads on #7099. Both were right; verified before fixing, and each fix is sabotage-checked. **1. The markdown ownership gate had three false-negative paths.** - `include_str!` / `include_bytes!` were matched per *line*, so a `rustfmt`-wrapped invocation — `include_str!(\n "…/some-prompt.md"\n)`, which is what the formatter produces for a long path — evaded the scan entirely. Replaced with `markdown_include_sites()`, which scans complete invocations across line breaks, plus four unit tests including the multiline regression case. Verified by planting a multiline `include_str!("../../AGENTS.md")` in composition source: the gate now fails and names the flattened site. - `markdown_assets()` skipped unreadable directories and entries with `let Ok(..) else { continue }`, so "the walk could not see it" and "there is nothing there" looked identical to an ownership gate. It now panics on a failed `read_dir`, entry, or `file_type`. - Extensions were compared case-sensitively; `.MD` slipped past. Now `eq_ignore_ascii_case`, on both the extension and the guidance-file exemption. Also added a scanned-file floor (>= 50 sources) so a broken walk fails instead of reporting clean — the same "measured scan" idiom `reborn_registration_pipeline_boundary.rs` uses. **2. PROPOSAL §2.4 still carried the pre-correction `local_runtime` measurement.** Line 81 said `runtime.rs:3016` and "the local *variable* name survived" while §6.10.1 (line 670) already carried the correction — a document contradicting itself. §2.4 now cites `runtime.rs:3095`, states the 191-occurrence scope, and points at §6.10.1 and #7098. The one surviving `:3016` in the file is inside the verbatim quote of the text being replaced, which is deliberate. **Also in this commit — two WS6 rows re-measured, because they would otherwise have been redone.** `RebornRuntime` slimming, at `origin/main` @ `0f897e9366`: - "~40 `_for_test` accessors behind `test-support`" is **already done**: `runtime.rs` has 38 and zero are ungated; crate-wide 149, and all 13 without their own attribute sit in a module gated at its declaration site (`lib.rs:64-65`, `factory.rs:1388-1389`). No `_for_test` function compiles into a production build. - "delete the dead `product_live_adapters` export block" is **refuted**: it is live cross-crate test-support API. `ironclaw_product` declares `ironclaw_reborn_composition = { …, features = ["test-support"] }` as a dev-dependency and its `tests/support/planned_agent_loop.rs` imports seven of the eight names; composition has a suite dedicated to them. Deleting it would strand a sibling crate's test support. Only the third clause (re-export wall vs. snapshot) is still live. `crates/AGENTS.md`'s `ironclaw_loop_host` row now names the prompt assets and says the seeding stays in the composition root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): stop the Reborn test planner failing closed on the crate-family map `crates/AGENTS.md`, `crates/Architecture.md` and `crates/README.md` sit directly under `crates/` and belong to no package directory. The planner skips markdown only at the repository root (`path.endswith(".md") and "/" not in path`), and `IGNORED_PREFIXES` does not include `crates/`, so all three fell through to the fail-closed package-resolution arm: Reborn PR test planner failed: unmapped crate path: crates/AGENTS.md That failed `Detect Reborn test scope`, which failed the `Tests (Reborn)` roll-up — on **any** PR that edited them. Hit while updating `crates/AGENTS.md` in this branch; filed as #7100 with the blast radius. It blocks the exact maintenance the house rule asks for: `crates/AGENTS.md` is the crate-level map WS11 requires updating when crate ownership changes, and `crates/Architecture.md` is already recorded in PROPOSAL §2 as carrying a stale `build_reborn_services` reference that WS11 has to fix. Fix: classify markdown *directly* under `crates/` as crate-family guidance with no test surface, ahead of the package-resolution arm. Deliberately narrow: - markdown *inside* a package directory is untouched and stays package-owned (`test_nested_crate_markdown_remains_package_owned` still passes); - anything non-markdown directly under `crates/` still falls through to the explicit-decision arm, which is the point of that arm. Two regression tests beside the existing nested-markdown one: all three family-map files plan to `mode=none` with no changed packages, and `crates/unexpected.txt` still raises `unmapped crate path`. Sabotage-checked by breaking the new arm's path-depth test — 3 errors, restored to green. Verified end to end: the planner run over this branch's own 14-file diff now succeeds and selects `ironclaw_architecture`, `ironclaw_loop_host`, `ironclaw_reborn_composition`. 44/44 planner tests pass. Fixes #7100 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert(ci): back out the planner fix — #7084 already carries it, better I hit `Reborn PR test planner failed: unmapped crate path: crates/AGENTS.md` after adding one line to the crate-family map, diagnosed it as an unhandled fail-closed arm, filed #7100 and fixed it. Then I checked whether other open PRs touch those files — #7084 and #7065 do — and expected them to be red for the same reason. **They are green**, which refuted the "any PR that edits them fails" framing and sent me to look at why. #7065 branched before the planner existed (#6952). **#7084 already modifies `scripts/ci/reborn_pr_test_plan.py` and already fixes this**, in the same function and the same arm I was editing: if package is None: # Markdown that belongs to no crate is prose, in the same class # as `docs/` and `.claude/` … Depth-independent by construction, # so it keeps holding for `crates/AGENTS.md` and for a future # `crates/<family>/AGENTS.md` after the WS7 family move. if path.endswith(".md"): continue with a regression test (`test_markdown_owned_by_no_crate_is_prose`) covering `crates/AGENTS.md`. Their rule is **strictly better than mine**: mine keyed on `path.count("/") == 1`, which would silently stop covering the file the moment WS7 moves crates under family directories. Theirs is depth-independent. So this reverts my planner change and its two tests, and drops the `crates/AGENTS.md` edit that provoked it — #7084 is on the do-not-disturb list and this would have collided with it line-for-line. The guidance follow-up is recorded on the CHECKLIST WS6 row with the exact text owed and the condition (#7084 landing) that unblocks it. #7100 is updated to say it is already fixed rather than left implying open work. Everything else on this branch is unchanged: the system-prompt asset eviction, the markdown ownership gate, and the doc corrections all stand. Refs #7100, #7084 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(ws6): statement-bounded include scan; fail-close the Rust-source walk Second CodeRabbit round on #7099. Both findings verified against the code before fixing; both were right. **1. `markdown_include_sites` missed a nested argument macro.** Confirmed: include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/prompt.md")) The first-`)` scan stopped at `(concat!(env!("CARGO_MANIFEST_DIR")` — before the path — and reported clean. Rather than teach the scan balanced-delimiter parsing (which then also owes string-literal, raw-string and comment handling — each an independent silent leak), the span is now bounded by the **statement**: from the macro-name occurrence to the next `;`. Whatever the nesting, spacing or line breaks, the path literal is inside that span. It also requires the name to be a whole identifier followed by optional whitespace and `!`, so `my_include_str!` and a plain `include_str_path` variable are not findings. It over-reports rather than under-reports — a comment mentioning `.md` inside an include statement is flagged — and says so. A false positive is a loud failure a human clears in one line; a false negative is prompt content silently back in the composition root. Seven scanner unit tests now: single-line, multiline, nested argument macro, whitespace before `!`, a comment inside the argument, uppercase `.MD`, non-markdown (`builtin_capability_policy.toml`, which must stay clean), and similar identifiers. Sabotage-checked against the real crate with the exact nested form above: the gate fails and prints the flattened site. **2. The file-count floor did not close the `rust_sources` hole.** Right — it only catches an empty-ish walk; an unreadable directory *after* 50 files still passed silently. `rust_sources` now panics on a failed `read_dir` and a failed entry, matching what it already did for unreadable file contents — this is consistency inside that function, not a new policy, and it hardens the three other tests in the file that share it. The floor is kept and re-justified for the case that stays silent even so: a walk that reads a perfectly good directory which is no longer the crate. After the WS7 family move relocates `crates/…` under family directories, a stale path can resolve to something small and readable rather than erroring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(arch): restore four tests my previous commit silently deleted `fe641b7709` rewrote `reborn_composition_boundaries.rs` by replacing a *span* between two doc-comment anchors. The two anchors were at opposite ends of the file — `markdown_include_sites` near the top, `markdown_assets` near the bottom — so the replacement swallowed everything between them: - `composition_public_pub_use_surface_matches_snapshot` - `extension_host_cluster_stays_internal` - `reborn_binary_main_is_thin_bootstrap` - `composition_crate_installs_installed_tier_only_through_registrar` - helpers `composition_src_path`, `extract_pub_use_surface`, `has_module_decl`, `is_test_module_file`, `strip_test_module` It compiled and the file's own suite went green, because each deleted test left with the helpers only it used — which is exactly why "the suite passed" is not evidence. It was caught by diffing the function roster against `origin/main` rather than by a test, and by the commit's own −301/+114 line count. This restores the file from `origin/main` and re-applies the change with targeted edits instead of a span replacement. The roster is now **purely additive** against `origin/main` — 9 functions added, **0 removed**, verified with `comm -23`: - `composition_root_embeds_no_prompt_content` (the gate) - `markdown_include_sites`, `markdown_assets` (helpers) - 8 scanner unit tests 7 tests on `origin/main` -> 16 here. Both halves of the gate re-sabotage-checked after the restore: a nested `include_str!(concat!(env!(…), "…default_system.md"))` fails it, and a shipped `assets/prompts/s.MD` fails it. Also fixes what `Fast deterministic checks` caught on `fe641b7709`: clippy's `items after a test module` (the scan's test module now sits at the end of the file, after every helper) and two `doc list item without indentation` warnings (the doc comment is prose, not a list). `cargo clippy -p ironclaw_architecture --benches --tests --examples --all-features` is clean. The substance of `fe641b7709` is unchanged and still stands: statement-bounded include scanning, and `rust_sources` failing closed on unreadable directories and entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): skip Rust trivia when bounding the include statement Third CodeRabbit round on #7099. Both findings verified, both real, both fixed. **1. `.find(';')` could end the span before the path.** A semicolon inside a comment above the argument (`// see the note; below`) or inside the path literal itself (`"../a;b/prompt.md"`) terminated the scan early — and an ownership gate that ends early goes quiet, which is the failure mode this gate exists to prevent. `statement_end_after` now finds the first `;` that actually terminates a statement, skipping line comments, nestable block comments, normal strings with escapes, raw strings with any number of hashes, and char literals (while not mistaking a lifetime for one). It only has to locate a delimiter, not parse the expression, which keeps it ~50 lines. Three new tests, and the third is the one that keeps the fix honest: the span must still *stop*, or a markdown path in the **next** statement would make every non-markdown include a false positive. Sabotage-checked against the real crate with a semicolon-in-comment form — the gate fails. **2. `path.is_dir()` swallowed metadata errors in `rust_sources`.** Right: `Path::is_dir()` returns `false` on an error, so an unreadable directory left the walk silently. It now asks `entry.file_type()` and panics, matching `markdown_assets`. **Not done, with a reason rather than silently:** the suggested regression test for "an unreadable directory beneath an otherwise readable workspace". The only portable way to create one is `chmod 000`, which does not make a directory unreadable for `root` — and the CI containers run as root, so the test would pass locally and be vacuous in CI. A test that cannot fail where it matters is worse than none. The invariant is instead carried by construction: every read in both walks is `unwrap_or_else(panic!)`, with no `let Ok(..) else` and no `is_dir()` left in either. `reborn_composition_boundaries.rs` is 7 tests on `origin/main` -> 19 here, and the function roster is still purely additive (`comm -23` empty). Full `ironclaw_architecture` suite green; clippy `--all-features` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): reject symlinks in both composition ownership walks Fourth CodeRabbit round on #7099, and it is right. `DirEntry::file_type()` reports the **link's** type without following it, so a symlink pointing at a source directory is neither `is_dir()` nor an `.rs` file: both walks stepped over the entire subtree and the gate reported clean on source it never opened. Same "uninspected reads as absent" failure the fail-closed reads added in the previous round exist to prevent — one level further out. `reject_symlink` now panics for either walk, naming the path and the two ways forward. Rejecting is chosen over following deliberately: following needs canonical-root containment plus cycle detection to be safe, and neither scanned crate has ever contained a symlink (`find crates/ironclaw_reborn_composition/src -type l` is empty). The panic is where that decision gets made on purpose rather than silently. Regression test `a_symlinked_subtree_fails_the_walk_instead_of_being_skipped` builds a tempdir with a real source directory plus a symlink to it and asserts **both** `rust_sources` and `markdown_assets` panic. `#[cfg(unix)]`, since the workspace has a Windows lane and `std::os::unix::fs::symlink` is not portable. Sabotage-checked: commenting out both `reject_symlink` call sites turns the test red ("a symlinked subtree must fail the walk, not be skipped"); restoring them returns 20/20. `reborn_composition_boundaries.rs`: 7 tests on `origin/main` -> 20 here, roster still purely additive (`comm -23` empty). Full `ironclaw_architecture` suite green; clippy `--all-features` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(event-store): stop leaking the Postgres driver in the public API (WS6) CHECKLIST WS6 / PROPOSAL §6.3.2: "stop leaking `deadpool_postgres::Pool` in the public API (wrap)". `ironclaw_reborn_event_store`'s public API now names `deadpool_postgres` zero times; the driver survives only inside its private `postgres_backed` module, which is where the TLS policy and pool construction §6.3.2 assigns this crate actually live. "Wrap" turned out to be three things, not one. **1. Half the leak was dead code, so it is deleted rather than wrapped.** `open_postgres_pool` and `open_postgres_pool_with_max_size` had exactly one caller each — composition's `open_reborn_postgres_pool` and `open_reborn_postgres_pool_with_max_size` — and those two had **zero** callers anywhere in `crates/`, `tests/`, `tools/` or `scripts/`. A four-function pass-through chain across two crates whose only remaining effect was to publish a third-party type in two public APIs. **2. The survivors take a carrier.** `open_postgres_pool_with_tls_options` returns `ironclaw_filesystem::PostgresConnectionPool` and `RebornEventStoreConfig::PostgresPool` holds one. The newtype lives in `ironclaw_filesystem`, not in event_store, for two reasons: it is the only crate `event_store`, `auth` and `composition` can all name without a new dependency edge, and that crate *is* the Postgres substrate, so the driver is chartered there (§11.2.6) rather than leaked. It is a carrier, not an abstraction — `driver()` / `into_driver()` exist for code that runs SQL — and it deliberately has no `Deref` (an implicit unwrap re-admits the driver into a signature unnoticed) and a hand-written `Debug` that renders nothing. The driver's own `Debug` prints its `tokio_postgres::Config`, which redacts the password (`tokio-postgres-0.7.16/src/config.rs:766-776`) but still prints `user`, `dbname`, `host`, `hostaddr`, `port` and `ssl_mode` — deployment topology that a derived `Debug` on any holder would inherit. **3. Stated residue: composition still names the driver, by charter.** §11.2.6 makes it "the one app-layer crate permitted a database driver", and it needs the raw pool for `PostgresRootFilesystem::new` and `CredentialRefreshLeaderLock::for_postgres`. It unwraps the carrier at exactly one site (`factory.rs`, `open_postgres_pool_from_source`). Pushing the carrier further down means changing `PostgresRootFilesystem::new`, which has **13 call sites across 5 crates plus `tests/integration/support/builder.rs`** — a separate test-wide slice, not this row. Recorded in both docs rather than left implied. **Enforcement (new file, lands with the change):** `crates/ironclaw_architecture/tests/reborn_persistence_driver_boundary.rs` - a shrink-only ratchet on which crates may hold a *normal* `deadpool-postgres` dependency (8 today, read from `cargo metadata`, not by eye), and - a scan proving event_store names the driver only below its private `postgres_backed` module — including that the module stays private, since a `pub mod` would silently defeat the scan. Both halves sabotage-checked: a planted `pub fn sabotage(p: deadpool_postgres::Pool)` fails the second and names the line; a planted `deadpool-postgres` dep on `ironclaw_projects` fails the first and names the crate. **Un-masking** (unfiltered `--list`, name-by-name, against `origin/main` in a clean baseline worktree): - `ironclaw_reborn_event_store` 71 → 71, roster identical - `ironclaw_reborn_composition` 928 → 928, roster identical - `ironclaw_filesystem` 296 → 296, roster identical - `ironclaw_architecture` 206 → 208, exactly the two new gate tests Deleting the four dead functions surfaced nothing, which is the evidence they were dead. No existing test edited. Guidance travels: `ironclaw_filesystem/CLAUDE.md` documents the carrier and its two deliberate omissions; `ironclaw_reborn_event_store/AGENTS.md` records that the driver cone is owned but not exported, and names the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): reject a symlink handed in as the walk root too Fifth CodeRabbit round on #7099, and right again — the previous fix closed the hole one level too late. `reject_symlink` only sees entries `read_dir` yields, but both walks push their **root** onto the stack before that ever runs, so a symlinked root was followed to its target silently. The regression test I added covered symlinked children only. `reject_symlink_root` now validates the root with `symlink_metadata` (which does not follow) before either walk starts, reusing the same rejection so the message and the policy stay in one place. The regression test is extended rather than duplicated: it now also symlinks a root and asserts **both** `rust_sources` and `markdown_assets` panic on it. Sabotage-checked — removing the two `reject_symlink_root` calls turns it red ("a symlinked walk root must fail rust_sources, not be followed"). Roster still purely additive against `origin/main` (`comm -23` empty); 20 tests in this file; full `ironclaw_architecture` suite green; clippy `--all-features` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * review(arch): widen the driver-boundary scan past its two blind spots Three CodeRabbit threads on #7101, all naming the same real defect from different angles, and all correct: `take(module_start)` stopped the scan at the `mod postgres_backed` **header**, so the gate was strictly weaker than the three places documenting it claimed. Two blind spots, both now sabotage-fixtures rather than prose: - anything **after** the module body in `lib.rs` — a `pub fn` there naming `deadpool_postgres::Pool` kept the gate green; - **every sibling file** in the crate (`coalescing_sink.rs`, `durable_log.rs`), which the scan never opened at all. The scan now reads every `.rs` file under `crates/ironclaw_reborn_event_store/ src/` minus the brace-matched **body** of the private module. The brace match is trivia-aware (line comments, nestable block comments, strings, raw strings, char literals) so a `}` inside a literal cannot end the body early and silently drag the rest of the file into the exempt range — the same failure class one level down. It panics on an unterminated body rather than exempting to end-of-file, and asserts it saw at least two source files. Four unit tests on the brace matcher: a mention inside the body is exempt, a mention after the body is not, a brace in a literal does not end the body, and a file without the module has no exempt range. Sabotage-checked against the real crate for both former blind spots: - `pub fn sabotage_after_body(p: deadpool_postgres::Pool)` appended to `lib.rs` -> fails, naming `lib.rs:2215` - the same appended to `coalescing_sink.rs` -> fails, naming `coalescing_sink.rs:321` Also corrected the prose the reviewer flagged as over-claiming, in both places: `ironclaw_reborn_event_store/AGENTS.md` and the CHECKLIST WS6 row now say "module **body**" and state that the scan covers every file in the crate, with the earlier revision's blind spots recorded rather than quietly fixed. Clippy `--all-features` clean (the scan's test module moved to the end of the file for `items after a test module`); full `ironclaw_architecture` suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(extractors,observability): typed extraction failures and a one-dependency latency crate (WS6) CHECKLIST WS6 row "extractors: typed error across the boundary + delete caller-less `extract_text` (§6.4.10); observability: `json_value_bytes` eviction (§6.2.5)". Measurements from #7102. ## extractors (§6.4.10) Failures now cross the boundary as `ExtractionError`, not `String`, at both public sites (`DocumentExtraction::Failed` and `extract_document_text_by_filename`). Two variants: `UnsupportedType { mime }` (nothing was attempted) and `NotExtractable { detail }` (an extractor ran and could not produce text). `Display` renders the classification and nothing else; `Debug` carries the payload. That is not a shape change. The invariant — "carries the error reason for logging only; callers render a model-safe marker, never this string" — lived as a doc comment on one of the two boundary sites, and the *other* one leaked: `ironclaw_extension_support`'s `read_file` interpolated the raw extractor diagnostic into a model-facing safe summary (`coding/file.rs:325-329`) while carefully redacting the path one argument earlier. With `Display` content-free that call site is safe unchanged. Its regression test sits at the call site, not on `Display`, because the wrapper composing the summary is what leaked. `extract_text` and `TRUNCATION_MARKER` were both `pub` with zero external callers; both are private now. The row only named the first. The second mattered more: `ironclaw_agent_loop` and `ironclaw_mcp` each declare their own `TRUNCATION_MARKER` with a different value, so it must be resolved by crate, not by name. The census is exact — no crate writes `use ironclaw_extractors::…`, so a full-path grep is complete. The private ZIP-safety enum was renamed `ExtractionError` -> `ZipEntryError` to free the natural name. ## observability (§6.2.5) — delegated ruling, PROPOSAL §12.12 D-K `json_value_bytes` and its `JsonByteCounter` are localized into the two consumers; `serde_json` leaves the manifest with them, so the crate now holds exactly one dependency, `tracing`. The row's stated reason ("gravity-well hygiene") was wrong; the ruling survives on a measured one. Of five call sites in extension_support, three feed `ResourceUsage::set_output_bytes` — resource accounting, not a trace field — so "it is a latency helper, in charter" is false. And sharing bought no invariant: `output_bytes` is already computed three different ways in production (this counter, `output.stdout.len()` in `ironclaw_scripts`, `Value::to_string().len()` in `ironclaw_loop_host`), because each producer measures what it produced. `ironclaw_common` was rejected (the crate the restructure is actively narrowing) and `ironclaw_host_api` was rejected explicitly rather than by omission (behavior in the contracts leaf is the specific criticism already on record against it). Cost, stated: ~18 lines and 2 unit tests duplicated across two crates. ## Guidance and docs New `AGENTS.md` for both crates (both rows asked for one). PROPOSAL §6.4.10 and §6.2.5 amended with dated notes quoting what they replace; §12.12 opened as the Wave 4 delegated-decision log, continuing §12.11's lettering and marking discipline. `families/domains.md` and `families/substrates.md` updated, including a sharpened "never contains" test for observability and a corrected security role for extractors (its failure type is a redaction boundary; "none" was wrong). ## Tests Unfiltered per-crate `--list`, before -> after: extractors 26 -> 28, observability 2 -> 2, attachments 39 -> 39, host_runtime 1247 -> 1249, extension_support 152 -> 156, architecture 206 -> 206. Nothing deleted; nothing edited for content. Observability's two tests moved with the function and are now duplicated in both consumers (2 -> 4 workspace-wide); its two replacements pin what actually remains in the crate. Both new guards were sabotage-verified: break the invariant, confirm red with the right message, restore, confirm green. Coverage floors untouched and deliberately so: the source crate (`ironclaw_observability`) has no floor entry, and the destination `ironclaw_host_runtime` gains covered lines rather than losing them. Found and filed rather than patched: #7103 (the coding tool computes its JSON byte count before checking whether latency tracing is on) and #7104 ("no text found" classifies as `Failed` rather than `Empty`, so the model is told the wrong thing about a valid but text-free document). * fix(extractors): ASCII-only extension normalization + narrow the Debug-payload guidance Review triage for #7106. **CodeRabbit thread 2 — accepted.** `.claude/rules/types.md:170` and `review-discipline.md:45` require case-insensitive external values to be normalized with `to_ascii_lowercase()`, not Unicode case folding. Both extension registries in this crate used `to_lowercase()`; the sibling registry in `ironclaw_extension_support::coding::file` (`should_extract_document_before_text`) already got it right, so this is the outlier. Note it is a latent-hazard fix, not a live bug: the eight keys (pdf/docx/pptx/xlsx/doc/ppt/xls/rtf) contain none of the letters a Unicode fold can produce from a foreign codepoint, so I could not construct an input where the two differ today. It removes the hazard for the next key added. Test pins both halves: ASCII case-insensitivity still works, and a non-ASCII extension is not folded into an ASCII key. **CodeRabbit thread 1 — guidance tightened, code change refuted.** The reviewer is right that this crate's doc told callers to `tracing::debug!(?error, …)` without naming a ceiling, while `ironclaw_host_runtime/AGENTS.md:28` forbids unredacted user content in that crate's logs. Both docs now say the payload belongs in an operator log and nowhere else, and record what it actually carries. The proposed code change is refused with measurement in the PR thread: it would log strictly less than `main` does today. * fix(extractors): the Unicode extension fold was a live bug, not a latent one Correcting my own claim in 0e7d14e and in the #7106 review reply. I wrote that `to_lowercase()` vs `to_ascii_lowercase()` was observationally equivalent here and that I "could not construct an input where the two differ". That was measured against only ONE of the two extension registries. `try_extract_by_extension`'s key set is much larger than `extract_document_text_by_filename`'s eight, and it contains `markdown`: "MAR\u{212A}DOWN".to_lowercase() == "markdown" // U+212A KELVIN SIGN -> k "MAR\u{212A}DOWN".to_ascii_lowercase() == "MAR\u{212A}DOWN" So on `main`, a file named `notes.MAR<U+212A>DOWN` carrying an unrecognized MIME type took the filename fallback in `extract_text`, was UTF-8-decoded, and reached the model as markdown instead of being rejected as an unsupported type. `bash` and `zsh` are in the same key set for the same reason. Caught by CodeRabbit on #7106, which constructed the input I said did not exist. Recorded here rather than quietly repaired: the earlier reply's measurement was wrong and the switch at :707 is a behaviour fix. Regression test extends `extension_matching_is_ascii_case_insensitive_and_ nothing_more` with the `markdown` fold in both registries plus the public `extract_document` path that actually reaches the fallback. Sabotage-verified: reverting :707 to `to_lowercase()` turns it red on the named assertion. * fix(arch): make the driver-boundary visibility check reachable and the scan multi-line safe Review found this gate weaker than its docs for the third time. Both findings were real; both are fixed at the seam and pinned in both directions. 1. The `pub mod` assertion could never fire. The header was matched with `starts_with("mod postgres_backed {")`, so a line beginning `pub ` was not the matched header and the `!starts_with("pub ")` assertion below it was dead. A visible module was simply not found: the exempt range came back empty and the failure blamed whichever driver mention was reported first rather than the visibility change that broke containment. The header now keys on the `mod postgres_backed {` token and asserts on the captured visibility prefix, so `pub` and `pub(crate)` both fail by name. 2. String state did not survive a newline, and that was fail-open. Block comments were carried across lines; regular and raw strings were not, so the continuation lines of a multi-line literal were scanned as code. A `}` there truncated the body, and a `{` there stretched it past the module's real end and swallowed every driver mention after it. With an unbalanced `{` in a multi-line literal and a `deadpool_postgres::Pool` in a public signature after the body, the old scan reported ok; the new one fails on lib.rs:2217. The raw-string terminator is now searched over bytes, so a multi-byte character in a literal cannot leave the index off a char boundary and panic. Regression tests (all failed before the fix, except the last which had no fixture at all): multi-line literal boundary in both directions plus raw strings, `pub mod` and `pub(crate) mod` rejection, the widened header match not mistaking a comment or string for the declaration, and the unterminated-body panic that AGENTS.md and CHECKLIST.md both present as part of the guarantee. Both fixes sabotage-checked against the real event_store source, not only fixtures. The weakness is recorded in the CHECKLIST row and AGENTS.md rather than quietly repaired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(config): retire the vendor config sections behind a generic window (WS6) `[slack]` and `[telegram]` were the last per-vendor sections in `ironclaw_reborn_config`. Nothing reads them: the enablement gate they fed was deleted with the unified extension runtime (#6116), so `config set slack.enabled true` printed "saved" for a value with no runtime consumer. Replaces the typed vendor schema with a generic retired-section table: - delete `SlackSection`, `SlackChannelRouteSection`, `TelegramSection`, their three builders, and `update_slack_enabled` - `RebornConfigFile` no longer names a vendor; retired sections are split off the raw document before the typed parse, so the schema stays `deny_unknown_fields` - `reject_legacy_slack_config` becomes `reject_retired_config_sections`, data-driven by the same table (PROPOSAL §12.2's "relocated shape") - `config set slack.enabled` now answers with migration guidance instead of writing a value nothing reads Compatibility window preserved and widened: an existing `config.toml` still parses, a retired *setup* key still fails the boot closed with the same message, an inert section still boots — and now says so instead of being silently ignored. Inline-secret rejection over retired sections goes from nine hardcoded keys to every string at any depth. Parse diagnostics: files with no retired section keep the line/column span on unknown-field errors (the split re-parses the original text); only files already carrying a retired section see the degraded form. Measured, and pinned by a test. Sabotage-testing the new guards found one of them inert: the scalar re-insert test only covered `slack = 1` alone, which takes the fast path and would catch it either way. Widened to `slack = 1` beside a genuine retired section, which is the case that actually bypasses `deny_unknown_fields` without the re-insert. The reachability-vs-fidelity limit of the table-driven key test is recorded in its doc rather than papered over. Extension-specificity allowlist 127 -> 125 (baseline lowered to match): the two surviving vendor tokens are the TOML table names, quarantined in `retired_sections.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: correct the Slack/Telegram enablement gate that no longer exists The retired `[slack]`/`[telegram]` sections had a documentation half. Five operator-facing docs still taught a gate deleted by #6116 (2026-07-21): `setup-slack-for-reborn-binary.md` called it the binary's "one gate" and described `IRONCLAW_REBORN_SLACK_ENABLED=false` as a "deployment kill switch" (it is not — Slack stays mounted), and its troubleshooting step could never fix anything. README instructed a `config set slack.enabled` command that now fails. Replaces the gate story with the real one everywhere: the ingress route is compiled in and mounted unconditionally, answers 503 until the extension's signing secret is registered, and 401 on signature mismatch — Slack and Telegram go live by installing the extension and finishing setup at /extensions. Adds a migration note where an operator with an existing file would look. Also removes `IRONCLAW_REBORN_SLACK_PERSONAL_OAUTH_REDIRECT_URI` from `docs/channels/slack.mdx`: zero readers in `crates/`. The CLI already had a regression test asserting that variable must never be advertised in remediation text, so its retirement was known — only the docs kept saying it. Records amendments in the target-architecture docs (CHECKLIST WS6 rows, PROPOSAL §6.10.3 with the placement decision and rejected alternatives, §12.2's compat constraint) and corrects a phantom test citation in the extension-runtime checklist. Filed rather than patched: #7115 (docker entrypoint gates its migration on the dead env var, so following the docs skipped it) and #7116 (live-QA runner gates Slack cases on a value it writes itself). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(planner): classify `.env.example` so a comment fix is not a full-matrix failure The Reborn PR test planner is fail-closed on unknown paths, and had no rule for `.env.example`. Repo-root `*.md` was classified; its non-`.md` sibling was not, so this PR's env-var comment correction aborted the planner with `unclassified pull-request path: .env.example` and failed the whole `Tests (Reborn)` roll-up on a change with no build surface. Nothing reads the file — no crate, test, or workflow; only doc comments name it by name. Classified rather than exempted, following the `.claude/` precedent added 2026-08-03, whose comment states the rule this follows: classify the path, do not loosen the arm that catches genuinely unknown ones. Regression test asserts all three halves: the path is accepted, it selects no Rust lane (so a future "classification" that turns a comment fix into a full matrix also fails), a real change riding along still selects its lane, and an unknown root file (`.env.local`) still raises. Verified by sabotage — removing the classification turns the new test red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(composition): gate three test-support-only imports so dependency builds lint clean `origin/main` already fails `Code Style` clippy for the package set `{ironclaw, ironclaw_reborn_config}` — verified on a clean detached checkout of `dfdd02b9fb`, exit 101, three unused imports in `composition/src/runtime.rs`. This PR is simply the first to produce that set, so it inherited the failure. Mechanism: the PR clippy lane derives `-p` from the diff and adds `--all-features`, which applies to *selected* packages only. All three imports are named solely by `#[cfg(any(test, feature = "test-support"))]` accessors, so when composition is a mere dependency its `test-support` is off, `--lib --bins` also drops `#[cfg(test)]`, and the imports go unused. With composition in the selected set, `--all-features` turns the gate on and the same command passes. Gating the imports to match their users is the minimal correct fix — they are used, so deleting them would be wrong and `#[allow]` would hide the real property. Verified both directions: the PR-lane invocation and `-p ironclaw_reborn_composition --all-targets --all-features` are now both exit 0. The class of bug — a lint gate whose verdict depends on which packages a PR happened to touch — is #7119; this commit only unblocks. Touching an otherwise-occupied crate deliberately kept to three `#[cfg]` attributes and a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: review fixes — google CLI path, Slack setup location, retired-key wording Three CodeRabbit findings, each verified before acting: - `capabilities/configuration.mdx`: `config set google.*` is still a supported path (README and `using/cli.mdx` both document it), so "configure it from the web interface rather than by hand" was wrong. Names both paths now. - `reborn/setup-slack-for-reborn-binary.md`: the 503 troubleshooting step pointed at `/extensions` generically and then called the same thing "Admin Configuration" — a third name for a place `docs/channels/slack.mdx` documents precisely (Extensions -> Channels tab -> Configure on the Slack card), including a warning that Extensions opens on the Registry tab, which is not it. Aligned to that wording, since it is the more specific of the two and matches the UI. - `using/cli.mdx`: "everything else is edited in config.toml directly" no longer holds for retired keys. The fourth finding is refuted in the thread: it asked for a "retired setup keys fail at serve" caveat on the `[telegram]` note, but `RETIRED_SECTIONS` gives telegram `rejected_keys: &[]` — it never had a setup field, so no `[telegram]` section can fail a boot. Adding the caveat would document behaviour that does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(slack): tie "Admin Configuration" to the Slack card once, in the guide The setup guide names the operator-facing concept ("Admin Configuration for Slack", 7 references) while docs/channels/slack.mdx names the UI path (Extensions -> Channels tab -> Configure on the Slack card). They are the same dialog, but nothing said so, and my earlier fix only rewrote the troubleshooting paragraph — leaving one place described two ways. Defines the equivalence once, next to the first use, and points the 503/401 steps back at it instead of restating the UI path a second time. Rewriting all seven references would churn a guide this PR is otherwise only correcting for the retired enablement gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(traces): split contribution.rs into chartered modules `crates/ironclaw_reborn_traces/src/contribution.rs` was 17,470 lines — the largest single file in the tree — and carried an `// arch-exempt: large_file` waiver from a 2026 mechanical rename (plan #6168). WS6's domain-internal cleanup row and PROPOSAL §6.4.14 both call for splitting it into chartered modules. It becomes a directory module of 13 production submodules plus a mirrored test tree, each named for one owner in the pipeline (capture → redact → classify → score → queue → submit). `src/contribution/mod.rs` carries the charter table that says which module a new item belongs to, plus the two rules that keep it honest: redaction is split by key (pattern vs tool-name), and `queue` owns state / `remote` owns the wire / `submission` is the only caller of both. The waiver is deleted rather than carried forward, and no new one is added: every file is under the 1,500-line ARCH-SPRAWL threshold (largest is 1,290). No public API change and no consumer edits. The submodules are private and `mod.rs` glob-re-exports them, so `contribution::X` remains the single public path for all four consumer crates. Items that newly cross a module line were widened to `pub(crate)`, never to `pub`. Verification: - Item roster diffed against origin/main: 501 top-level items before, 501 after, zero missing and zero extra. - Unfiltered `--list` before and after: 216 lib tests, leaf names identical. All 216 + 2 integration tests pass. - `cargo clippy --benches --tests --examples --all-features` clean on ironclaw_reborn_traces and ironclaw_architecture. The four `PATH_TERM_COLLISIONS` carve-outs that pinned the old file path are repointed and, in the process, narrowed: the vendor-name safety denylist now resolves to `tool_payloads.rs` (the rule tables) and `classification.rs` (external-write detection, `slack` only) instead of one 17k-line whole-file carve-out, so the specificity gate now polices the rest of the module. Those entries are staleness-checked, so the old path would have failed loudly. Adds the crate's first guidance file, recording the glob-re-export invariant and the three known gaps on §6.4.14's row that this PR does not close (ScopedFilesystem adoption, the two re-export modules, the crate rename). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(reborn): record the traces contribution.rs split and correct two stale clauses Amends CHECKLIST WS6's domain-internal-cleanups row and PROPOSAL §6.4.14 (plus the anti-pattern inventory and the crate-disposition table) with what landed, quoting the text each amendment replaces. Two corrections the work surfaced, recorded rather than silently fixed: - §6.4.14's "17,467-line contribution.rs" measured 17,470 on main; the file drifted after the entry was written. - The CHECKLIST's shorthand "`ScopedFilesystem` + re-export modules dropped" is worded backwards for the first clause. `ScopedFilesystem` is `ironclaw_filesystem`'s type, is used by ~170 files across the workspace, and is absent from `ironclaw_reborn_traces` entirely — there is nothing to drop. §6.4.14's actual instruction is adoption ("take a `ScopedFilesystem` instead of raw `dirs`/env access"), which is a persistence-plane change across ~91 raw fs call sites, not a deletion. Left as-is with the reason stated, so the next reader measures rather than inherits. Also records why the two remaining traces clauses did not land in this wave: dropping the `recording`/`paths` re-export shims needs edits in `ironclaw_reborn_cli`, and `recording` additionally needs a decision because the CLI has no `ironclaw_llm` dependency to fall back on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(traces): serialize test process-env mutation behind lock_env() The split re-surfaced five unguarded `std::env::set_var`/`remove_var` call sites that CI's `check-hermetic-env.sh` had been grandfathering: they are byte-identical pre-existing lines (contribution.rs:10501/10513/10515/15648/ 15661 on origin/main), and the gate only skipped them because it is delta-scoped and the file had not been re-added since it was written. This is a real gap, not a false positive, so it is fixed rather than annotated. `EnvVarRestore` restored the previous value on drop but took no lock, so two tests mutating the environment on different threads still raced — undefined behavior on Rust 1.82+ regardless of whether they name the same variable. `workload_token_env_mode_reads_env_unchanged` used a uniquely named variable, which avoids logical interference but not the setenv/getenv data race. Both now acquire `ironclaw_common::env_helpers::lock_env()`, the sanctioned helper the gate's message names. `EnvVarRestore` holds the guard as a field declared last, so it is released only after `Drop::drop` has restored the value — the restore is inside the critical section, not after it. The real process environment is kept (not `env_helpers::set_runtime_env`'s overlay) because the sidecar isolation test needs a value a child process would inherit, to prove `CommandPrivacyFilterAdapter` clears it. One `#[allow(clippy::await_holding_lock)]` on the async test, matching the precedent in `ironclaw_operator/src/llm_admin/llm_config_service.rs`: holding the lock across the await is the intent, and `#[tokio::test]` drives the future on a current-thread runtime so the guard never crosses threads. Verified: `check-hermetic-env.sh` exits 0, clippy clean, 216 + 2 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(traces): apply CodeRabbit review — carried waiver, inert test, charter drift Six findings verified against the code; four were defects this PR introduced or carried, and each is fixed. 1. **A second file-size waiver was carried forward after all.** `queue.rs` still held the in-body "File-size justification … already-oversized module … decomposition tracked in issue #4088" block, which contradicts a PR whose whole point is performing that decomposition. Deleted; the coupling rationale it was wrapped around (why credential resolution lives beside the policy/scope-dir helpers) is kept, since that still explains the layout. 2. **`invite_code_gated_by_auth_mode` was inert.** It re-implemented the `match policy.auth_mode` expression from `build_trace_upload_claim_issuer_request` and asserted against its own copy, so deleting the `DeviceKey => None` arm in production left it green. It now calls the production builder and asserts on the *serialized* request, so a field rename cannot hide a leak either. Sabotage-proved: removing that arm now fails with the leaked invite code visible in the body. 3. **The charter claimed "each stage owns one file"**, which `remote`'s four files contradict. Reworded to module-level ownership, naming `remote` as a directory module and why. `CLAUDE.md`'s test-layout paragraph gets the same correction plus the explicit `remote` → four-test-module mapping. 4. **Five policy-serde tests sat in `claims.rs`.** They verify `StandingTraceContributionPolicy`, whose owner is `policy.rs`, and the PR's own rule is that a test lives with its production owner. Moved to a new `tests/policy.rs`; leaf names unchanged. 5. **Three orphan section headers** left behind by the split, describing tests that now live in other modules (`credentials.rs`, `profile.rs`, `value.rs`). Deleted. The remaining two findings are real but pre-existing and need behavior changes, so they are filed as #7127 rather than fixed here: the case-sensitive remote `status` comparison that skips the local revocation record, and `fetch_account_traces` taking two adjacent `&str` where its sibling takes `&TenantId, &UserId` (its fix needs an edit in `ironclaw_product`). The issue also carries the `trace_scope_has_pending_queue` doc/code mismatch, which needs an intent decision rather than a guess. Re-verified: 501/501 production items, 216 tests with identical leaf names, clippy clean, hermetic-env clean, every file under 1,500 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(traces): use the RAII env guard and cover the bearer at the caller Second CodeRabbit pass, both findings on the test this PR had already touched. 1. **RAII guard instead of manual cleanup.** `workload_token_env_mode_reads_env_unchanged` set the variable, awaited, asserted, then removed it — so any panic before the last line leaked the variable into every later test. It now uses `EnvVarRestore::set`, whose `Drop` restores during unwinding while holding the same process-env lock. That also deletes both `unsafe` blocks and the `#[allow(clippy::await_holding_lock)]`: the guard lives in a struct field, which the lint does not flag, so the suppression is no longer needed. 2. **The bearer token had no caller-tier coverage.** Five tests assert what `issuer_request_bearer` returns; none asserted the token reaches the wire. The direct issuer path attaches it conditionally (`if let Some(bearer) = issuer_bearer { request.bearer_auth(bearer) }`), so a helper regressing to `None` would send an unauthenticated request with every existing test green — the repo's "test through the caller" rule names exactly this shape. Adds `workload_token_reaches_the_issuer_request_as_a_bearer_header`: a mock issuer captures the `Authorization` header while `fetch_trace_upload_claim_from_issuer` drives the real path. Sabotage-proved — dropping the `bearer_auth` attach fails it with `left: None, right: Some("Bearer wire-bearer-xyz")`; restored, green. Test accounting: 216 → 217. All 216 original leaf names still present (diffed against the `origin/main` baseline); the one addition is the new caller-tier test. Clippy clean, hermetic-env clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(llm): add the enforced sub-owner map (WS6 module charters) PROPOSAL §6.4.13 asks `ironclaw_llm` for "internal module charters for its five sub-owners". This adds the map to `crates/ironclaw_llm/CLAUDE.md` and, because a charter nobody checks rots within a release, a test that pins it. **Five sub-owners were not enough, measured.** `providers` / `auth-sessions` / `registry` / `decorators` / `recording` own 28 of 48 files (79.6% of lines), leaving 20 unowned — including `lib.rs`, `provider.rs`, `error.rs` and `config.rs`. Five more are named, each with a stated reason rather than a residual bucket: `core-contract` (the trait, vocabulary, error taxonomy and config are *upstream* of every implementor, so charging them to `providers` would make providers own decorators' and recording's own dependencies), `normalization` (cross-provider wire hygiene, as opposed to the single-provider shims that stay beside their provider), `model-catalog` (facts about *models*, a different noun from registry's catalog of *providers*), `transcription` (`TranscriptionProvider` is a different trait; nothing there implements `LlmProvider`), and `test-support` (a published feature with its own compatibility obligation). **`tests/module_charter.rs` enforces it.** Every `src/**/*.rs` must appear in exactly one row, every path in a row must exist, and no file may be claimed twice. Sabotage-proved in all three directions — dropping `retry.rs` from the table, adding a phantom path, and double-claiming `registry.rs` each fail with the right message; restored green. The test also guards itself: it fails if the table parses to zero rows or if the source walk finds implausibly few files, so a table-shape change cannot silently turn it into a no-op. **§6.4.13's "Deletes: reasoning.rs (4.5k lines, zero external references)" is refuted.** The file is 1,299 lines after #6964 removed its dead half, and the survivor is live: `lib.rs:88-91` re-exports three helpers with five production call sites in `crates/ironclaw_loop_host/src/model_gateway.rs`. It is charted under `normalization`. `AGENTS.md` carried the same staleness ("legacy reasoning engine") and is corrected; it also now points at the map as authoritative so its informal buckets cannot quietly become a second source of truth. Four placement calls are recorded rather than left implicit: `token_refreshing.rs` is auth-sessions not decorators (CLAUDE.md and AGENTS.md disagreed); `runtime.rs` and `smart_routing.rs` force the decorator definition to widen from "reliability wrapper" to "wraps `dyn LlmProvider` and is not credential work"; `url_check.rs` is core-contract; and `gemini_oauth.rs` is genuinely two owners in one file, charged to the larger half with the split recorded as owed. CHECKLIST and PROPOSAL §6.4.13 carry dated amendments quoting the text they replace, including why the row's `providers.json` clause is blocked (its load-bearing include site is in `ironclaw_reborn_cli`, which is occupied, and it needs a new mechanism rather than a new path). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(traces): correct the claims/policy test-module docs after the move The script that moved the five policy-serde tests copied `claims.rs`'s preamble verbatim, so `policy.rs` ended up with two module docs — its own and a carried-over line describing claims. And `claims.rs`'s own doc still opened with "Standing-policy serde", which stopped being true the moment those tests left. `policy.rs` keeps only its own doc; `claims.rs` now describes what it actually covers (upload-claim cache keys, issuer error labels, the bearer the issuer request carries, device-key auth modes) and points at `policy.rs` for the policy serde contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(arch): lower the specificity ALLOWLIST baseline 125 -> 124 after the re-baseline #7117 measured `ALLOWLIST` 127 -> 125 against `origin/main` @ `1e2a294083`. #7094 then deleted one entry on `main` (127 -> 126), so this branch's two net removals now land on 124, not 125. The ratchet is `<=`, so it stayed green at 125 while carrying a unit of untracked slack — exactly what the constant's own doc forbids: "Lower it in the same PR that deletes entries so the new floor is locked in." Read off the ratchet's own failure message with the baseline temporarily set to `0` ("ALLOWLIST grew to 124 entries"), never counted by eye — a plain paren count over the literal answers 142, because the entries' comments contain parentheses too. Sabotage-verified in both directions: baseline 123 goes red naming 124, and 124 is green 7/7. The file's function roster is unchanged. * docs(checklist): map the WS6 "Domain-internal cleanups" row clause by clause The row bundles eight clauses and the Wave 4 part-1 consolidation closes one of them (the `traces` `contribution.rs` split). It stays open, correctly — but a reader of the row could not tell which of the remaining seven had been measured and which had not, and the `llm` `providers.json` measurement lived on the "Module charters" row two rows down because that is where the agent who made it was working. Adds item 7: a clause-by-clause status map — one done, three measured with the blocker named (including a pointer to where `providers.json` was measured), four untouched. No box is ticked; the row's real condition is unmet and stays unmet. Also fixes a stray space-semicolon left in the "Composition behavior evictions" row where the system-prompt clause was struck through. * review(ws6): fix seven findings on code this consolidation introduced CodeRabbit's pass over the consolidation raised 40 threads. 29 are on production code #7124 only *moved* and are filed as #7144. These seven are on code this program wrote, and all seven were correct. **A gate that was not scanning what its doc claimed.** The driver-boundary walk used a flat `read_dir` while its doc said it scans "**every** `.rs` file in the crate". `crates/ironclaw_reborn_event_store/src` is flat today, so nothing escaped — but `src/postgres/pool.rs` is exactly where a driver mention would go, and a skipped file is indistinguishable from a clean one. Now recursive and symlink-rejecting, matching the shape `reborn_composition_boundaries.rs` already uses in this same PR. Sabotage-proved against the real crate: a nested `postgres/pool.rs` naming `deadpool_postgres::Pool` now fails the gate naming `pool.rs:1`, and passed silently before. This is the third revision of this gate found weaker than its own docs; the doc now says why. **A charter gate that a table reformat would have broken.** `module_charter.rs` matched the separator row with `cells[0].starts_with("---")`, so an aligned separator (`|:---|:---|`) parsed as a *data* row: `:---` became an assigned path, `saw_row` went true so the shape guard stayed quiet, and the stale assertion reported `:---` instead of a diagnosis. Sabotage-proved both ways — with the fix reverted and the table rewritten in aligned form the test goes red on `:---`; with the fix it passes. Also: - `CONTRACT.MD` added to the composition guidance allowlist. The repo already ships it as crate-local guidance (`ironclaw_reborn_identity`, `ironclaw_trust`) and CLAUDE.md's module-spec table names it, so a composition `CONTRACT.md` would have been reported as prompt content and sent the author to the wrong fix. - `markdown_assets` gains its first real test: the case-insensitive `.md` match and the caller's guidance filter were both unpinned, and both drift quiet. - Two fixtures for comment-braced module bodies (line comment, nested block comment) — the scan handled them, nothing pinned it. - The symlink rationale doc block moved onto `reject_symlink`, which it describes; it was stacked above `reject_symlink_root` with no item between, so both attached to the wrong function and `reject_symlink` was undocumented. - The retired-section deprecation warn gains `target = "ironclaw::reborn::cli::serve"`, like every other warn on that path. Announcing an inert section is pointless if an operator filtering the documented startup target cannot see it. - `ironclaw_reborn_traces/CLAUDE.md` claimed a one-to-one test mapping that `tests/credentials.rs` breaks (it spans `queue.rs` and `remote/claim.rs`). The exception is now stated rather than left to be inferred. Rosters in both architecture test files are purely additive; no test removed. * docs: correct the extension-specificity allowlist numbers after the re-baseline Caught in review of #7139. Both ledgers still recorded #7117's measurement, `Extension-specificity allowlist **127 → 125**`, taken against `origin/main` @ `1e2a294083`. #7094 then deleted an entry on `main` (127 → 126), so the same two net removals land on **124**, which is what the shipped baseline says. This is the cross-slice-number failure mode the consolidation exists to catch, one layer down: the code was corrected in 811bfedeff and the prose was not. Both amendments quote the text they replace and record the method — read off the ratchet's own failure message with the baseline temporarily set to 0, never counted by eye. No checkbox state changed. * review(ws6): three more review findings, one of which broke my own fix **My `target =` fix did not work, and CodeRabbit was right to call it.** `tracing::warn!(target = "…")` records a *field* named `target`; it does not set the event's metadata target, which stays the module path. So the retired-section notice — given a target in #7117 precisely so operators would see an inert `[slack]`/`[telegram]` section announced — was still invisible to a subscriber filtering `ironclaw::reborn::cli::serve`. Measured with a capturing subscriber rather than argued: EQUALS-SYNTAX target = "target_probe" <- module path COLON-SYNTAX target = "ironclaw::reborn::cli::serve" <- correct Now `target:`, and pinned by `retired_section_notice_is_emitted_on_the_serve_target`, which asserts the emitted **metadata** target through the real `reject_retired_config_sections` call. Sabotage-proved: the `=` form makes it red with `observed targets: ["ironclaw::commands::serve"]`. This is repo-wide — **121 sites** use the `=` form against an `ironclaw::…` target, including the three sibling warns on this same serve path (`:318`, `:387`, `:454`). Filed as #7146 rather than fixed here; a consolidation should not carry a 121-site mechanical change. **The markdown gate's test was testing a copy of itself.** My new test carried its own duplicate of the guidance allowlist, so the production filter could drop `CONTRACT.MD` and the test would still pass — the "test through the caller" rule. Extracted `is_crate_guidance` / `shipped_non_guidance_markdown`; the gate and the test now share one path. Sabotage-proved by dropping `CONTRACT.MD` from the shared helper: red with `left: ["CONTRACT.md", "seed.MD"]`. **The separator fix had no committed regression test.** It was sabotage-proved by hand, which does not survive the session. `parse_sub_owner_table` is split out from the file read so a fixture can supply separator shapes the checked-in `CLAUDE.md` does not use, and `an_aligned_separator_row_is_not_parsed_as_data` covers unaligned, left-aligned and centred. Red when the fix is reverted. Rosters purely additive in all three files; no test remove…
…ations sever + WS10 inventory keying + enforcement gates (nearai#7170) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone able to set it could redirect all credentialed vendor egress. That was WRONG. RewriteNetworkTransport::from_env_value already returned UnavailableInRelease when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither [profile.release] nor [profile.dist] sets debug-assertions, so a shipped binary with the variable set REFUSES TO BOOT. It was fail-closed before this PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime path. What was genuinely wrong, and is fixed: 1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile proxy, not a build-kind guarantee. A release profile with debug-assertions turned on (normal when chasing a production bug) silently re-arms it. 2. The refusal arm had NO TEST. The one guard between a shipped binary and redirectable vendor egress was unpinned. Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and its four re-exports are now cfg(any(debug_assertions, feature=test-support)), and default_host_http_egress is a compile-time pair - production builds PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite wrapper absent from the binary. The runtime check stays as defence in depth. E2E needs no change: those harnesses build DEBUG binaries, so they satisfy debug_assertions and keep redirecting with no feature flag and no workflow edit. The feature-forwarding-into-CI risk I flagged earlier does not arise. test-support is still forwarded composition -> network for a release-PROFILE build that needs the seam. Both halves proven rather than assumed: (a) release refuses - new regression test a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds a well-formed map and asserts on profile. Under 'cargo test --release -p ironclaw_network --features test-support' it passes on the UnavailableInRelease branch; under debug 'cargo test -p ironclaw_network' it passes on the active branch. 56 passed, 0 failed. (b) production compiles without the seam - 'cargo check --release -p ironclaw_reborn_composition' (no test-support) is clean, which only compiles if the cfg(not(..)) arm is right. Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant had drifted ABOVE the real list length; the ratchet is shrink-only so it passed silently while buying back two unearned slots. Measured off the compiler (set baseline to 0, read the reported length), identical on main and on every slice, so pre-existing drift rather than something this PR caused. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(coverage): verify the extension_support floor drop is composition, independently The 82.64 -> 75.31 recapture carried a rationale that was recorded but explicitly NOT verified. Re-derived it from scratch between the two capture refs (f946a93fae -> 939af4847d) rather than inheriting the claim: - 0 test names lost in the crate (158 -> 160 test fns; both new names belong to the arriving executor). - 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759 unique). This is the check that separates a relocation from a deletion: host_runtime's roster drops 156 names over the same range and every one reappears in another crate. - Exactly four files arrived, 1367 source lines, all of them the family-1 skill-install executor (src/skills/url_install.rs + url_install/{github, zip_bundle,bundle}.rs). No pre-existing file left the crate. - The arithmetic closes with the pre-existing numerator held CONSTANT: (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero covered lines. The arriving block's own coverage is 316/1224 = 25.82%. Composition, confirmed rather than assumed. No test regression to fix; the 25.82% arrival is what earns the follow-up already recorded above the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn! Three verified review findings from the #7141 round. Each was confirmed against the code before being acted on; nothing was changed on assertion alone. 1. obligations/handler.rs — `obligation_supported_before_dispatch` and `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies (verified by exact line-by-line comparison). Both were private, each called exactly once, both taking the same `phase` argument. The two names asserted a pre/post-dispatch distinction the code never implemented, while the pair gates admission of RedactOutput, EnforceOutputLimit and EnforceResourceCeiling — so editing one copy alone would have left the other stage accepting an obligation the host cannot honour (a fail-open). Collapsed to one `obligation_supported`, with the reasoning recorded so the pair is not reintroduced. 2. obligations/process_store.rs — `cleanup_terminal` is reached from `observe_process_commit` (an async background journal callback, call sites at :363/:379/:394), so its `tracing::warn!` violates the repo rule that background tasks never use info!/warn! — they corrupt the REPL/TUI display. Lowered to `debug!`; the error is still returned to the caller on the next line, so nothing is swallowed. 3. reborn_restructure_baselines.rs — the doc table said the LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type annotation opens a bracket on the same line and silently yields 0): the real count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected. Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0; obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane Review finding on #7141, confirmed empirically before acting. The Markdown prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup. A prompt is a `.md` file that no package *directory* owns, so a change to `crates/extensions/packages/*/prompts/**.md` took the prose arm and planned: mode=none crate_buckets=[] "crate-tree guidance changed: ..." while its sibling `manifest.toml` in the same package planned `mode=selected` onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped production output that `ironclaw_extension_support` compiles in, and the comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and built wasm/*.wasm" as exactly what that table owns — so this was the "silent under-schedule of a change to production output" that comment forbids. 145 of the 149 `.md` files under `packages/` are prompts. The rule is keyed on the `prompts/` path segment, not on the asset prefixes. That distinction is load-bearing: the first attempt yielded to the asset prefixes wholesale and broke `test-tools/README.md`, which is documentation of the fixture bundles and is deliberately pinned as prose. Of the four asset kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas .json, wasm .wasm), so `.md` asset <=> prompt is exact. Sabotage-tested in both directions: * `_is_package_prompt` -> False (reinstates the bug): RED, "AssertionError: 'none' != 'selected'". * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on both the new test and the pre-existing `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`. * restored: 52 passed, 51 subtests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(harness): refresh the latency-runner lockfile after the sandbox consolidation Review finding on #7141, reproduced before fixing. The latency harness keeps its own committed `Cargo.lock`, separate from the workspace lockfile, and the crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox` with `ironclaw_sandbox` never regenerated it. It still carried entries for both removed packages (lines 3244 and 3602) and the old host-runtime/loop-host dependency graphs. Reproduced exactly as reported: $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml error: cannot update the lock file ... because --locked was passed exit 101 so any reproducible invocation of the harness was broken, while the documented unlocked command silently rewrote the lockfile as a side effect of running. Regenerated with `cargo update --workspace`, which re-resolves the path dependencies. Verified after: `--locked` exits 0, the two removed packages are gone (0 entries), and `ironclaw_sandbox` is present (1 entry). Note: the re-resolve also carried three registry deps forward (wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser 0.251.0 -> 0.252.0). That is contained — this lockfile governs only the standalone benchmark harness and is not the workspace lockfile, and it was already unusable under `--locked` before this change. Co-Authored-By: Claude Opus 5…
…isions (accumulating the fleet) (nearai#7181) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone able to set it could redirect all credentialed vendor egress. That was WRONG. RewriteNetworkTransport::from_env_value already returned UnavailableInRelease when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither [profile.release] nor [profile.dist] sets debug-assertions, so a shipped binary with the variable set REFUSES TO BOOT. It was fail-closed before this PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime path. What was genuinely wrong, and is fixed: 1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile proxy, not a build-kind guarantee. A release profile with debug-assertions turned on (normal when chasing a production bug) silently re-arms it. 2. The refusal arm had NO TEST. The one guard between a shipped binary and redirectable vendor egress was unpinned. Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and its four re-exports are now cfg(any(debug_assertions, feature=test-support)), and default_host_http_egress is a compile-time pair - production builds PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite wrapper absent from the binary. The runtime check stays as defence in depth. E2E needs no change: those harnesses build DEBUG binaries, so they satisfy debug_assertions and keep redirecting with no feature flag and no workflow edit. The feature-forwarding-into-CI risk I flagged earlier does not arise. test-support is still forwarded composition -> network for a release-PROFILE build that needs the seam. Both halves proven rather than assumed: (a) release refuses - new regression test a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds a well-formed map and asserts on profile. Under 'cargo test --release -p ironclaw_network --features test-support' it passes on the UnavailableInRelease branch; under debug 'cargo test -p ironclaw_network' it passes on the active branch. 56 passed, 0 failed. (b) production compiles without the seam - 'cargo check --release -p ironclaw_reborn_composition' (no test-support) is clean, which only compiles if the cfg(not(..)) arm is right. Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant had drifted ABOVE the real list length; the ratchet is shrink-only so it passed silently while buying back two unearned slots. Measured off the compiler (set baseline to 0, read the reported length), identical on main and on every slice, so pre-existing drift rather than something this PR caused. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(coverage): verify the extension_support floor drop is composition, independently The 82.64 -> 75.31 recapture carried a rationale that was recorded but explicitly NOT verified. Re-derived it from scratch between the two capture refs (f946a93fae -> 939af4847d) rather than inheriting the claim: - 0 test names lost in the crate (158 -> 160 test fns; both new names belong to the arriving executor). - 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759 unique). This is the check that separates a relocation from a deletion: host_runtime's roster drops 156 names over the same range and every one reappears in another crate. - Exactly four files arrived, 1367 source lines, all of them the family-1 skill-install executor (src/skills/url_install.rs + url_install/{github, zip_bundle,bundle}.rs). No pre-existing file left the crate. - The arithmetic closes with the pre-existing numerator held CONSTANT: (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero covered lines. The arriving block's own coverage is 316/1224 = 25.82%. Composition, confirmed rather than assumed. No test regression to fix; the 25.82% arrival is what earns the follow-up already recorded above the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn! Three verified review findings from the #7141 round. Each was confirmed against the code before being acted on; nothing was changed on assertion alone. 1. obligations/handler.rs — `obligation_supported_before_dispatch` and `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies (verified by exact line-by-line comparison). Both were private, each called exactly once, both taking the same `phase` argument. The two names asserted a pre/post-dispatch distinction the code never implemented, while the pair gates admission of RedactOutput, EnforceOutputLimit and EnforceResourceCeiling — so editing one copy alone would have left the other stage accepting an obligation the host cannot honour (a fail-open). Collapsed to one `obligation_supported`, with the reasoning recorded so the pair is not reintroduced. 2. obligations/process_store.rs — `cleanup_terminal` is reached from `observe_process_commit` (an async background journal callback, call sites at :363/:379/:394), so its `tracing::warn!` violates the repo rule that background tasks never use info!/warn! — they corrupt the REPL/TUI display. Lowered to `debug!`; the error is still returned to the caller on the next line, so nothing is swallowed. 3. reborn_restructure_baselines.rs — the doc table said the LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type annotation opens a bracket on the same line and silently yields 0): the real count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected. Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0; obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane Review finding on #7141, confirmed empirically before acting. The Markdown prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup. A prompt is a `.md` file that no package *directory* owns, so a change to `crates/extensions/packages/*/prompts/**.md` took the prose arm and planned: mode=none crate_buckets=[] "crate-tree guidance changed: ..." while its sibling `manifest.toml` in the same package planned `mode=selected` onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped production output that `ironclaw_extension_support` compiles in, and the comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and built wasm/*.wasm" as exactly what that table owns — so this was the "silent under-schedule of a change to production output" that comment forbids. 145 of the 149 `.md` files under `packages/` are prompts. The rule is keyed on the `prompts/` path segment, not on the asset prefixes. That distinction is load-bearing: the first attempt yielded to the asset prefixes wholesale and broke `test-tools/README.md`, which is documentation of the fixture bundles and is deliberately pinned as prose. Of the four asset kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas .json, wasm .wasm), so `.md` asset <=> prompt is exact. Sabotage-tested in both directions: * `_is_package_prompt` -> False (reinstates the bug): RED, "AssertionError: 'none' != 'selected'". * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on both the new test and the pre-existing `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`. * restored: 52 passed, 51 subtests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(harness): refresh the latency-runner lockfile after the sandbox consolidation Review finding on #7141, reproduced before fixing. The latency harness keeps its own committed `Cargo.lock`, separate from the workspace lockfile, and the crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox` with `ironclaw_sandbox` never regenerated it. It still carried entries for both removed packages (lines 3244 and 3602) and the old host-runtime/loop-host dependency graphs. Reproduced exactly as reported: $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml error: cannot update the lock file ... because --locked was passed exit 101 so any reproducible invocation of the harness was broken, while the documented unlocked command silently rewrote the lockfile as a side effect of running. Regenerated with `cargo update --workspace`, which re-resolves the path dependencies. Verified after: `--locked` exits 0, the two removed packages are gone (0 entries), and `ironclaw_sandbox` is present (1 entry). Note: the re-resolve also carried three registry deps forward (wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser 0.251.0 -> 0.252.0). That is contained — this lockfile governs only the standalone benchmark harness and is not the workspace lockfile, and it was already unusable under `--locked` before this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> …
…factory port (nearai#7202) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone able to set it could redirect all credentialed vendor egress. That was WRONG. RewriteNetworkTransport::from_env_value already returned UnavailableInRelease when !cfg!(debug_assertions) (test_rewrite.rs:150), and neither [profile.release] nor [profile.dist] sets debug-assertions, so a shipped binary with the variable set REFUSES TO BOOT. It was fail-closed before this PR. I had read the ungated `mod test_rewrite;` declaration as an ungated runtime path. What was genuinely wrong, and is fixed: 1. The guard was a RUNTIME check keyed on cfg!(debug_assertions) - a profile proxy, not a build-kind guarantee. A release profile with debug-assertions turned on (normal when chasing a production bug) silently re-arms it. 2. The refusal arm had NO TEST. The one guard between a shipped binary and redirectable vendor egress was unpinned. Fix: compile-time exclusion instead of a runtime check. mod test_rewrite and its four re-exports are now cfg(any(debug_assertions, feature=test-support)), and default_host_http_egress is a compile-time pair - production builds PolicyNetworkHttpEgress<ReqwestNetworkTransport> directly, with the rewrite wrapper absent from the binary. The runtime check stays as defence in depth. E2E needs no change: those harnesses build DEBUG binaries, so they satisfy debug_assertions and keep redirecting with no feature flag and no workflow edit. The feature-forwarding-into-CI risk I flagged earlier does not arise. test-support is still forwarded composition -> network for a release-PROFILE build that needs the seam. Both halves proven rather than assumed: (a) release refuses - new regression test a_set_rewrite_map_activates_only_in_debug_and_is_refused_in_release feeds a well-formed map and asserts on profile. Under 'cargo test --release -p ironclaw_network --features test-support' it passes on the UnavailableInRelease branch; under debug 'cargo test -p ironclaw_network' it passes on the active branch. 56 passed, 0 failed. (b) production compiles without the seam - 'cargo check --release -p ironclaw_reborn_composition' (no test-support) is clean, which only compiles if the cfg(not(..)) arm is right. Also: WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE 129 -> 127. The constant had drifted ABOVE the real list length; the ratchet is shrink-only so it passed silently while buying back two unearned slots. Measured off the compiler (set baseline to 0, read the reported length), identical on main and on every slice, so pre-existing drift rather than something this PR caused. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(coverage): verify the extension_support floor drop is composition, independently The 82.64 -> 75.31 recapture carried a rationale that was recorded but explicitly NOT verified. Re-derived it from scratch between the two capture refs (f946a93fae -> 939af4847d) rather than inheriting the claim: - 0 test names lost in the crate (158 -> 160 test fns; both new names belong to the arriving executor). - 0 test names lost WORKSPACE-WIDE (13836 -> 13843 test fns, 13752 -> 13759 unique). This is the check that separates a relocation from a deletion: host_runtime's roster drops 156 names over the same range and every one reappears in another crate. - Exactly four files arrived, 1367 source lines, all of them the family-1 skill-install executor (src/skills/url_install.rs + url_install/{github, zip_bundle,bundle}.rs). No pre-existing file left the crate. - The arithmetic closes with the pre-existing numerator held CONSTANT: (6826+316)/(8260+1224) = 75.31% exactly, so the pre-existing code lost zero covered lines. The arriving block's own coverage is 316/1224 = 25.82%. Composition, confirmed rather than assumed. No test regression to fix; the 25.82% arrival is what earns the follow-up already recorded above the entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(host_runtime): collapse a duplicated obligation predicate and quiet a background warn! Three verified review findings from the #7141 round. Each was confirmed against the code before being acted on; nothing was changed on assertion alone. 1. obligations/handler.rs — `obligation_supported_before_dispatch` and `obligation_supported_after_dispatch` had BYTE-IDENTICAL 19-line bodies (verified by exact line-by-line comparison). Both were private, each called exactly once, both taking the same `phase` argument. The two names asserted a pre/post-dispatch distinction the code never implemented, while the pair gates admission of RedactOutput, EnforceOutputLimit and EnforceResourceCeiling — so editing one copy alone would have left the other stage accepting an obligation the host cannot honour (a fail-open). Collapsed to one `obligation_supported`, with the reasoning recorded so the pair is not reintroduced. 2. obligations/process_store.rs — `cleanup_terminal` is reached from `observe_process_commit` (an async background journal callback, call sites at :363/:379/:394), so its `tracing::warn!` violates the repo rule that background tasks never use info!/warn! — they corrupt the REPL/TUI display. Lowered to `debug!`; the error is still returned to the caller on the next line, so nothing is swallowed. 3. reborn_restructure_baselines.rs — the doc table said the LAYER_MATRIX_EXCEPTIONS count was "now 11". Recomputed on this ref by anchoring on the `= &[` of the value (the `&[LayerMatrixException]` type annotation opens a bracket on the same line and silently yields 0): the real count is 4, matching WS0_LAYER_MATRIX_EXCEPTION_BASELINE = 4. Corrected. Verification: cargo check --all-targets -p ironclaw_host_runtime exit 0; obligation tests 13+26 passed, 0 failed; reborn_restructure_baselines 1 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): a shipped package prompt is an asset, not prose — it was selecting no lane Review finding on #7141, confirmed empirically before acting. The Markdown prose carve-out in the planner ran BEFORE the `EMBEDDED_ASSET_OWNERS` lookup. A prompt is a `.md` file that no package *directory* owns, so a change to `crates/extensions/packages/*/prompts/**.md` took the prose arm and planned: mode=none crate_buckets=[] "crate-tree guidance changed: ..." while its sibling `manifest.toml` in the same package planned `mode=selected` onto ironclaw_extension_support + ironclaw_extension_host. Prompts are shipped production output that `ironclaw_extension_support` compiles in, and the comment above `EMBEDDED_ASSET_OWNERS` names "manifests, prompts, schemas and built wasm/*.wasm" as exactly what that table owns — so this was the "silent under-schedule of a change to production output" that comment forbids. 145 of the 149 `.md` files under `packages/` are prompts. The rule is keyed on the `prompts/` path segment, not on the asset prefixes. That distinction is load-bearing: the first attempt yielded to the asset prefixes wholesale and broke `test-tools/README.md`, which is documentation of the fixture bundles and is deliberately pinned as prose. Of the four asset kinds the table owns, only a prompt is Markdown (manifests are .toml, schemas .json, wasm .wasm), so `.md` asset <=> prompt is exact. Sabotage-tested in both directions: * `_is_package_prompt` -> False (reinstates the bug): RED, "AssertionError: 'none' != 'selected'". * `_is_package_prompt` -> any .md under an asset prefix (over-broad): RED on both the new test and the pre-existing `test_markdown_owned_by_no_crate_is_prose`, at `test-tools/README.md`. * restored: 52 passed, 51 subtests, green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(harness): refresh the latency-runner lockfile after the sandbox consolidation Review finding on #7141, reproduced before fixing. The latency harness keeps its own committed `Cargo.lock`, separate from the workspace lockfile, and the crate consolidation that replaced `ironclaw_scripts` + `ironclaw_process_sandbox` with `ironclaw_sandbox` never regenerated it. It still carried entries for both removed packages (lines 3244 and 3602) and the old host-runtime/loop-host dependency graphs. Reproduced exactly as reported: $ cargo metadata --locked --manifest-path harness/latency/runner/Cargo.toml error: cannot update the lock file ... because --locked was passed exit 101 so any reproducible invocation of the harness was broken, while the documented unlocked command silently rewrote the lockfile as a side effect of running. Regenerated with `cargo update --workspace`, which re-resolves the path dependencies. Verified after: `--locked` exits 0, the two removed packages are gone (0 entries), and `ironclaw_sandbox` is present (1 entry). Note: the re-resolve also carried three registry deps forward (wasmtime-wasi 46.0.1 -> 47.0.3, wasmtime-wasi-io likewise, wit-parser 0.251.0 -> 0.252.0). That is contained — this lockfile governs only the standalone benchmark harness and is not the workspace lockfile, and it was already unusable under `--locked` before this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): stop …
…e 4 rows (nearai#7152) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6) PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move "behind the triggers/kernel seam it guards". It was not a module: it was three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire` — one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter` — holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`. That placement is a fail-open: a guard that lives inside one implementation of a port is lost the moment a second implementation exists, and nothing in the tree forced a new submitter to re-run it. The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed `TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now fallible and calls the new `ironclaw_triggers::prompt_safety` first, making "this prompt passed the trusted-prompt scan" an invariant of the type rather than a step some submitter performs. `new_for_test` delegates to `new`, so the test-support seal bypasses visibility only, never the scan. Behaviour at the fire level is unchanged — same rejection point, same `TriggerError::InvalidMaterialization`, same permanent disposition — and composition's pre-materialization scan is untouched, so defence in depth survives with the second scan relocated and now covering every submitter. `ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its only use). Enforcement: triggers' boundary rule stops forbidding `ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge, not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations` forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"), a crate that was unruled until now. Regression coverage at the caller tier, not on the helper: `tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached` drives the real `TriggerPollerWorker::tick_once` with a materializer that does NOT scan and a submitter configured to accept, and asserts the submitter is never reached. A companion pins that a medium-severity-only prompt still submits, so the mint cannot drift into a blanket filter. Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173 (+2 worker, +2 prompt_safety unit), architecture 206 -> 206. LAYER_MATRIX_EXCEPTIONS unchanged at 10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * refactor(traces): drop the boundary-laundering re-export modules (WS6) PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules (`recording`, `paths`) — consumers import the owners". `ironclaw_reborn_traces::{recording, paths}` were two `pub use <other crate>::*` passthroughs whose own doc comments stated their purpose plainly: "so reborn-cli does not need a direct `ironclaw_llm` dependency, preserving the architectural boundary". They preserved nothing — the edge existed either way; the wildcard only hid which crate owned the type, so the dependency graph read as a lie. All three call sites were in `ironclaw_reborn_cli`. Note the literal reading of "consumers import the owners" is not available here: the CLI's dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`) deliberately excludes `ironclaw_llm`, so importing the owner would have traded a laundered re-export for a breached, tested boundary. Satisfied instead by giving the owning crate the operation, which is what the laundering was standing in for: - `onboarding::onboard_instance(invite, consents)` — resolves the contribution root itself. Path layout under the base dir is this crate's own knowledge; the CLI no longer needs base-dir vocabulary. - `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)` — parses `ironclaw_llm::recording::TraceFile` inside the crate that already depends on `ironclaw_llm`. The CLI hands over raw JSON. - the CLI's private `trace_contribution_dir()` now delegates to `contribution::trace_contribution_dir_for_scope(None)` instead of re-deriving `<base>/trace_contributions`. Verified byte-identical: `trace_contribution_dir_for_scope(None)` is `trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`, whose `None` arm returns `base.join("trace_contributions")`. No dependency was added to any crate. Semantics unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(llm): make providers.json a crate asset with a boundary rule (WS6) CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition input + boundary rule added". The provider catalog sat at the **repository root**. A root-level data file has no owning crate, so no boundary rule could govern who edits it, and every consumer compiled it in behind Cargo's back with an escaping `include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json` and the 20 `include_str!("../../../providers.json")` sites in `registry.rs` become in-crate `../assets/providers.json`. ⚠ Correcting the row's inherited premise: a prior lane recorded the "load-bearing include site is in `ironclaw_reborn_cli`" and judged the item "needs a new mechanism, not a new path". Measured on main: the load-bearing site is `crates/ironclaw_llm/src/registry.rs:383` (`builtin_provider_definitions`), inside the owning crate. No new mechanism was needed — only the path. **Path-keyed gates rewritten in the same commit** (WS10: these fail *silently* under a move): - `Dockerfile` — both `COPY providers.json providers.json` lines deleted; `COPY crates/ crates/` already covers the new location in both stages. Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs). - `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path filter and its regex alternative removed; the depth-independent `crates/**` entry already matches. `ws12_workflow_contracts.py` passes. - `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both lanes) classification under the new path rather than letting it fall through to crate scope, so CI breadth does not silently narrow; the now-redundant entry in the reborn-only branch is dropped. **The one consumer that could not simply be repointed.** The CLI's `default_llm_consts_match_the_real_providers_json_nearai_entry` embedded the catalog from five directories up to check its mirrored `DEFAULT_LLM_*` constants. Repointing it would have turned a repo-root reach-in into a *cross-crate* reach-in — the category §11.2.7 turns into a hard failure — and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency rule belongs in the cross-crate suite, so the assertions moved into `ironclaw_architecture` and read both files from disk at runtime, needing no compile-time coupling at all. Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate` in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the asset's location, the repo root's emptiness, and single-embedder ownership). Net test count +0. **The new rule is sabotage-tested** — five cases, each red with the right message, each restored to green: 1. catalog copied back to the repo root -> "must not sit at the repository root" 2. a foreign crate `include_str!`s it -> names the offending file 3. catalog `default_model` drifts from the CLI mirror -> names the const, the field and both files 4. walker pointed at a non-existent dir -> "walked only 0 Rust files ... would pass no matter what the tree contained" (reachability) 5. mirrored const renamed -> "no longer declared as a plain const ... update the extraction rather than deleting the drift check" Case 2 caught a real false positive in the first draft of the guard: a file-level `include_str!` AND `providers.json` conjunction flagged `cli/tests/smoke.rs`, which names the *runtime* `$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something else. The matcher now inspects the macro argument, not the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * docs(skills): rewrite the stale v1 lib.rs charter note (WS6) CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc rewritten". The crate doc claimed "In v1, trust-based tool filtering happens via `src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust labels and the policy engine controls tool access via capability leases." Both halves are dead vocabulary: there is no `src/` monolith on this tree and no Python orchestrator anywhere in Reborn. Replaced with what is true and checkable — this crate owns the trust *label* and none of its enforcement; the ceiling is applied at the capability tier (`host_api` capability/invocation attenuation via `first_party_extension_ports`' activation and execution paths) and the decision belongs to `ironclaw_authorization`. Also points at the existing `SkillTrust` `Ord` safety note, which the old text left unconnected. Doc-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE…
* refactor(contracts): move extension runtime descriptors to a neutral 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>
* refactor(sandbox): merge the sandbox lane into one crate (WS3)
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>
* docs(target-architecture): record the WS3 corrections with their evidence
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>
* chore(sandbox): drop imports the merge left unused
`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>
* fix(ci): let the Reborn PR planner plan guidance edits and crate deletions
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>
* fix(arch): give the retained resource exceptions an owning issue, not 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>
* test(contracts): pin the asset-path validator that moved into extension_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>
* test(coverage): re-capture the host_runtime floor and floor the new sandbox 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>
* docs(target-architecture): record the coverage ratchet as a move-sensitive 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>
* fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath
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>
* test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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>
* docs(sandbox,mcp): correct the wiring inventory and record the projection 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>
* refactor(extensions): move the skill-install executor to extension_support (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>
* docs(sandbox): record that the Docker fail-closed switch is wired to 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>
* docs(host_runtime): record the executor/adapter seam in crate guidance
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>
* refactor(host_runtime): keep the install-input error path log-free
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>
* ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832
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>
* refactor(wasm): move wit/ inside its owning crate (Wave 3)
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>
* build(wasm): rebuild first-party artifacts for the moved wit/ path
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>
* docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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>
* ci(planner): classify the path classes that blocked the wit/ move
`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>
* refactor(host-runtime): split obligations into its three chartered owners (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>
* refactor(operator,contracts): route operator secrets through a product_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>
* test(sandbox): put the Docker security check behind the fail-closed gate
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>
* docs(reborn): stop calling the unwired script lane an execution lane
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>
* fix(ci): pin the WIT scope probes and the embedded-asset owner pairing
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>
* docs(host-runtime): state the obligation visibility rule as it holds
Review catch (#7090): the guardrail sentence promised "cross-owner access is
`pub(super)`, never `pub(crate)`", which is stronger than the code. Verified:
`RuntimeSecretInjectionStore::{insert, take, clone_material,
discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take,
discard_for_capability}` and both constructors are `pub(crate)` and must stay
so — `src/egress/{mod,host_port,credential}.rs` call them, and that is
host-runtime composition outside `obligations/`.
The rule is restated as the property that actually holds: a method whose only
callers are inside `obligations/` is `pub(super)` (the three that are), and
`pub(crate)` is what the stores expose to the egress pipeline they exist to
serve. A future agent reading the old sentence would have read the existing
`pub(crate)` methods as violations.
Guidance-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(architecture): put the operator secrets boundary entry on the right rule
Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"`
entry landed in `ironclaw_extension_contracts`'s forbidden vector, not
`ironclaw_operator`'s. The suite still passed, because `extension_contracts`
has no such dependency and `ironclaw_operator` then had no entry at all — so
the guard this row exists to add was inert, and a green architecture suite was
evidence of nothing. Reintroducing the edge would have passed every check.
Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its
`origin/main` content byte-for-byte.
Negative-probed rather than assumed. With `ironclaw_secrets` temporarily
re-added to `crates/ironclaw_operator/Cargo.toml`:
reborn_crate_dependency_boundaries_hold ... FAILED
ironclaw_operator must not have a normal dependency on ironclaw_secrets
and with the manifest restored, 35/35 pass.
Two further review findings, both verified before being accepted:
- `ironclaw_extension_manager` **does** have a `boundary_rules()` entry
(`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL
§8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale
and is marked superseded. The real gap is narrower and now stated: the rule
exists and simply does not forbid `ironclaw_secrets` (#7095).
- `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules".
Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated
`test_support`), and the table was missing `ironhub` **before** this branch
touched it. Count corrected to twenty-six and the missing `ironhub` row
added, so the inventory matches `lib.rs`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(sandbox): state the Docker-gate claim as the search that checks it
Review caught a false inventory in the Known debt entry, and the previous
commit is what made it false: "the name appears only in docker_gate.rs and
attribution_tests.rs" stopped holding the moment docker_security.rs gained a
module doc naming the variable, and CLAUDE.md itself was already a third
counterexample.
The narrower claim is the one that was always meant and is the one that
matters, so it now carries its own reproduction: no workflow, script, env file
or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/
*.toml/*.py/*.json/.env* is empty here and on main -- and the sole code
reference is a read, std::env::var(...) at docker_gate.rs:23. Every other
occurrence is a doc comment or a panic message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6)
PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move
"behind the triggers/kernel seam it guards". It was not a module: it was
three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire`
— one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter`
— holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`.
That placement is a fail-open: a guard that lives inside one implementation
of a port is lost the moment a second implementation exists, and nothing in
the tree forced a new submitter to re-run it.
The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed
`TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter
of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now
fallible and calls the new `ironclaw_triggers::prompt_safety` first, making
"this prompt passed the trusted-prompt scan" an invariant of the type rather
than a step some submitter performs. `new_for_test` delegates to `new`, so
the test-support seal bypasses visibility only, never the scan.
Behaviour at the fire level is unchanged — same rejection point, same
`TriggerError::InvalidMaterialization`, same permanent disposition — and
composition's pre-materialization scan is untouched, so defence in depth
survives with the second scan relocated and now covering every submitter.
`ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its
only use). Enforcement: triggers' boundary rule stops forbidding
`ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge,
not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations`
forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"),
a crate that was unruled until now.
Regression coverage at the caller tier, not on the helper:
`tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached`
drives the real `TriggerPollerWorker::tick_once` with a materializer that
does NOT scan and a submitter configured to accept, and asserts the
submitter is never reached. A companion pins that a medium-severity-only
prompt still submits, so the mint cannot drift into a blanket filter.
Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173
(+2 worker, +2 prompt_safety unit), architecture 206 -> 206.
LAYER_MATRIX_EXCEPTIONS unchanged at 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(coverage): re-anchor the exemptions the merge shifted
tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and
auto-merges silently. #7096's additions to ironclaw_reborn_composition moved
four entries' subject lines by +2 without anything flagging it; a stranded
entry makes the changed-coverage validator abort with no verdict at all.
Re-anchored by content (difflib line map from the #7065 tree, which the file
was validated against, to the union) rather than by arithmetic:
runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085]
runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435]
lib.rs [616] -> [618]
All 142 entries / 1124 line references re-verified against the merged tree:
0 drift, 0 out-of-bounds, 0 missing paths.
* refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4)
Two CHECKLIST rows, both of which were a one-line manifest correction rather
than a code move: the family docs already placed both crates where the rows
want them and only `Cargo.toml`'s `layer =` disagreed.
processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes
among the kernel crates. The re-layer makes processes -> resources a
kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate
said so itself:
Stale IronClaw crate layer matrix exceptions:
ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed
in W7: runtime process management still depends on resource contracts
currently classed with kernel behavior
That is the gate's verdict, not a judgement call - deleting the entry is the
only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list).
Checked the direction both ways: all nine crates that take a normal dependency
on processes (capabilities, turns, host_runtime, extension_host, loop_host,
extension_manager, runner, reborn_composition, stress) are kernel or above, so
the move legalizes an edge without forbidding an existing one.
skills -> substrates (WS4 SS3.D). families/domains.md already lists
ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies
are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both
at or below substrates, and its six consumers are all loops or above. No
exception moves in either direction.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
cargo check --workspace --all-targets: clean.
* docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence
Every tick was verified against the merged tree, never against a PR title.
TICKED:
- sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and
ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one
manifest in the workspace.
- mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only,
0 production ironclaw_extensions:: refs in src/.
- skills -> substrates: landed here.
- hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four
rejected alternatives and the evidence they are already converged on one
trait plus a shared conformance suite. #6945 read first as the row demands,
and explicitly NOT discharged: this PR changes nothing in the dispatch path.
- WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10
exceptions carried removes_in = W7). Corrected with the replaced text
quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder
named with its owning field value. Ticked on the corrected condition.
LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave:
- first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime.
Ticking would be false.
- processes/capabilities row: re-layer DONE; the capabilities/host.rs split is
deferred with every module boundary already computed (4,560 lines, the six
workflow ranges, and the arch-exempt waiver that must be deleted with it).
- host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs
RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same
section's Keeps clause; zero external references to either). Catalog half
cannot go to extension_host at all - host_runtime is itself a production
consumer at memory_native_extension.rs:96,101, so the move is a
kernel -> products edge and a Cargo cycle. Correct destination is downward.
- network test_rewrite: NOT executed. Recorded the security shape (production
binaries compile the seam and honour the rewrite env var at runtime) and the
full 6-step plan, because the env var is how the entire E2E suite redirects
vendor traffic through the production binary and the change needs feature
forwarding into CI lanes I cannot verify here.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
* refactor(traces): drop the boundary-laundering re-export modules (WS6)
PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules
(`recording`, `paths`) — consumers import the owners".
`ironclaw_reborn_traces::{recording, paths}` were two `pub use <other
crate>::*` passthroughs whose own doc comments stated their purpose
plainly: "so reborn-cli does not need a direct `ironclaw_llm`
dependency, preserving the architectural boundary". They preserved
nothing — the edge existed either way; the wildcard only hid which crate
owned the type, so the dependency graph read as a lie.
All three call sites were in `ironclaw_reborn_cli`. Note the literal
reading of "consumers import the owners" is not available here: the CLI's
dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`)
deliberately excludes `ironclaw_llm`, so importing the owner would have
traded a laundered re-export for a breached, tested boundary. Satisfied
instead by giving the owning crate the operation, which is what the
laundering was standing in for:
- `onboarding::onboard_instance(invite, consents)` — resolves the
contribution root itself. Path layout under the base dir is this
crate's own knowledge; the CLI no longer needs base-dir vocabulary.
- `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)`
— parses `ironclaw_llm::recording::TraceFile` inside the crate that
already depends on `ironclaw_llm`. The CLI hands over raw JSON.
- the CLI's private `trace_contribution_dir()` now delegates to
`contribution::trace_contribution_dir_for_scope(None)` instead of
re-deriving `<base>/trace_contributions`. Verified byte-identical:
`trace_contribution_dir_for_scope(None)` is
`trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`,
whose `None` arm returns `base.join("trace_contributions")`.
No dependency was added to any crate. Semantics unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(llm): make providers.json a crate asset with a boundary rule (WS6)
CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition
input + boundary rule added".
The provider catalog sat at the **repository root**. A root-level data
file has no owning crate, so no boundary rule could govern who edits it,
and every consumer compiled it in behind Cargo's back with an escaping
`include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner
inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json`
and the 20 `include_str!("../../../providers.json")` sites in
`registry.rs` become in-crate `../assets/providers.json`.
⚠ Correcting the row's inherited premise: a prior lane recorded the
"load-bearing include site is in `ironclaw_reborn_cli`" and judged the
item "needs a new mechanism, not a new path". Measured on main: the
load-bearing site is `crates/ironclaw_llm/src/registry.rs:383`
(`builtin_provider_definitions`), inside the owning crate. No new
mechanism was needed — only the path.
**Path-keyed gates rewritten in the same commit** (WS10: these fail
*silently* under a move):
- `Dockerfile` — both `COPY providers.json providers.json` lines deleted;
`COPY crates/ crates/` already covers the new location in both stages.
Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs).
- `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path
filter and its regex alternative removed; the depth-independent
`crates/**` entry already matches. `ws12_workflow_contracts.py` passes.
- `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both
lanes) classification under the new path rather than letting it fall
through to crate scope, so CI breadth does not silently narrow; the
now-redundant entry in the reborn-only branch is dropped.
**The one consumer that could not simply be repointed.** The CLI's
`default_llm_consts_match_the_real_providers_json_nearai_entry` embedded
the catalog from five directories up to check its mirrored `DEFAULT_LLM_*`
constants. Repointing it would have turned a repo-root reach-in into a
*cross-crate* reach-in — the category §11.2.7 turns into a hard failure —
and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency
rule belongs in the cross-crate suite, so the assertions moved into
`ironclaw_architecture` and read both files from disk at runtime, needing
no compile-time coupling at all.
Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the
removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate`
in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the
asset's location, the repo root's emptiness, and single-embedder
ownership). Net test count +0.
**The new rule is sabotage-tested** — five cases, each red with the right
message, each restored to green:
1. catalog copied back to the repo root -> "must not sit at the
repository root"
2. a foreign crate `include_str!`s it -> names the offending file
3. catalog `default_model` drifts from the CLI mirror -> names the const,
the field and both files
4. walker pointed at a non-existent dir -> "walked only 0 Rust files ...
would pass no matter what the tree contained" (reachability)
5. mirrored const renamed -> "no longer declared as a plain const ...
update the extraction rather than deleting the drift check"
Case 2 caught a real false positive in the first draft of the guard: a
file-level `include_str!` AND `providers.json` conjunction flagged
`cli/tests/smoke.rs`, which names the *runtime*
`$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something
else. The matcher now inspects the macro argument, not the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci(coverage): recapture the two composed floors from a real measurement
The provisional values were arithmetic - the sum of the two slices' recorded
deltas - and the dispatch caught them, which is the whole reason the brief
demanded a measurement rather than a reconciliation.
Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd:
26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one
skip is the pull_request-gated mutation gate; the two failures are the coverage
report and the roll-up it drags down, i.e. this file doing its job.
ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63%
(17562 / 19814). The composition was wrong by 1300 denominator lines because
both slices measured their delta under the pre-#7083 aggregator, which could
not see crates/extensions/** at all - lines leaving host_runtime for
extension_support vanished from the tree it could measure, so neither branch's
recorded delta describes the post-#7094 world.
ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's
82.64% (6826 / 8260), captured before #7080's executor lines arrived.
floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye
rather than written quietly. Evidence it is composition and not lost tests:
floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more
absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398
with zero test names lost. Same shape as #7094's own ironclaw_runner recapture.
ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657).
The [global] entry is untouched: both moves are crate-to-crate inside the set
the fixed aggregator sees.
* docs(skills): rewrite the stale v1 lib.rs charter note (WS6)
CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc
rewritten".
The crate doc claimed "In v1, trust-based tool filtering happens via
`src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust
labels and the policy engine controls tool access via capability leases."
Both halves are dead vocabulary: there is no `src/` monolith on this tree
and no Python orchestrator anywhere in Reborn.
Replaced with what is true and checkable — this crate owns the trust
*label* and none of its enforcement; the ceiling is applied at the
capability tier (`host_api` capability/invocation attenuation via
`first_party_extension_ports`' activation and execution paths) and the
decision belongs to `ironclaw_authorization`. Also points at the existing
`SkillTrust` `Ord` safety note, which the old text left unconnected.
Doc-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(network): compile the test rewrite seam out of production builds (WS3)
Closes the WS3 network row. Also RETRACTS an overstatement I made in this
row's earlier annotation.
CORRECTION FIRST. The earlier note claimed production binaries compile the
seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone
abl…
* refactor(contracts): move extension runtime descriptors to a neutral 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>
* refactor(sandbox): merge the sandbox lane into one crate (WS3)
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>
* docs(target-architecture): record the WS3 corrections with their evidence
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>
* chore(sandbox): drop imports the merge left unused
`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>
* fix(ci): let the Reborn PR planner plan guidance edits and crate deletions
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>
* fix(arch): give the retained resource exceptions an owning issue, not 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>
* test(contracts): pin the asset-path validator that moved into extension_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>
* test(coverage): re-capture the host_runtime floor and floor the new sandbox 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>
* docs(target-architecture): record the coverage ratchet as a move-sensitive 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>
* fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath
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>
* test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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>
* docs(sandbox,mcp): correct the wiring inventory and record the projection 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>
* refactor(extensions): move the skill-install executor to extension_support (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>
* docs(sandbox): record that the Docker fail-closed switch is wired to 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>
* docs(host_runtime): record the executor/adapter seam in crate guidance
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>
* refactor(host_runtime): keep the install-input error path log-free
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>
* ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832
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>
* refactor(wasm): move wit/ inside its owning crate (Wave 3)
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>
* build(wasm): rebuild first-party artifacts for the moved wit/ path
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>
* docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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>
* ci(planner): classify the path classes that blocked the wit/ move
`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>
* refactor(host-runtime): split obligations into its three chartered owners (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>
* refactor(operator,contracts): route operator secrets through a product_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>
* test(sandbox): put the Docker security check behind the fail-closed gate
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>
* docs(reborn): stop calling the unwired script lane an execution lane
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>
* fix(ci): pin the WIT scope probes and the embedded-asset owner pairing
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>
* docs(host-runtime): state the obligation visibility rule as it holds
Review catch (#7090): the guardrail sentence promised "cross-owner access is
`pub(super)`, never `pub(crate)`", which is stronger than the code. Verified:
`RuntimeSecretInjectionStore::{insert, take, clone_material,
discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take,
discard_for_capability}` and both constructors are `pub(crate)` and must stay
so — `src/egress/{mod,host_port,credential}.rs` call them, and that is
host-runtime composition outside `obligations/`.
The rule is restated as the property that actually holds: a method whose only
callers are inside `obligations/` is `pub(super)` (the three that are), and
`pub(crate)` is what the stores expose to the egress pipeline they exist to
serve. A future agent reading the old sentence would have read the existing
`pub(crate)` methods as violations.
Guidance-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(architecture): put the operator secrets boundary entry on the right rule
Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"`
entry landed in `ironclaw_extension_contracts`'s forbidden vector, not
`ironclaw_operator`'s. The suite still passed, because `extension_contracts`
has no such dependency and `ironclaw_operator` then had no entry at all — so
the guard this row exists to add was inert, and a green architecture suite was
evidence of nothing. Reintroducing the edge would have passed every check.
Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its
`origin/main` content byte-for-byte.
Negative-probed rather than assumed. With `ironclaw_secrets` temporarily
re-added to `crates/ironclaw_operator/Cargo.toml`:
reborn_crate_dependency_boundaries_hold ... FAILED
ironclaw_operator must not have a normal dependency on ironclaw_secrets
and with the manifest restored, 35/35 pass.
Two further review findings, both verified before being accepted:
- `ironclaw_extension_manager` **does** have a `boundary_rules()` entry
(`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL
§8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale
and is marked superseded. The real gap is narrower and now stated: the rule
exists and simply does not forbid `ironclaw_secrets` (#7095).
- `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules".
Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated
`test_support`), and the table was missing `ironhub` **before** this branch
touched it. Count corrected to twenty-six and the missing `ironhub` row
added, so the inventory matches `lib.rs`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(sandbox): state the Docker-gate claim as the search that checks it
Review caught a false inventory in the Known debt entry, and the previous
commit is what made it false: "the name appears only in docker_gate.rs and
attribution_tests.rs" stopped holding the moment docker_security.rs gained a
module doc naming the variable, and CLAUDE.md itself was already a third
counterexample.
The narrower claim is the one that was always meant and is the one that
matters, so it now carries its own reproduction: no workflow, script, env file
or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/
*.toml/*.py/*.json/.env* is empty here and on main -- and the sole code
reference is a read, std::env::var(...) at docker_gate.rs:23. Every other
occurrence is a doc comment or a panic message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6)
PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move
"behind the triggers/kernel seam it guards". It was not a module: it was
three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire`
— one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter`
— holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`.
That placement is a fail-open: a guard that lives inside one implementation
of a port is lost the moment a second implementation exists, and nothing in
the tree forced a new submitter to re-run it.
The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed
`TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter
of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now
fallible and calls the new `ironclaw_triggers::prompt_safety` first, making
"this prompt passed the trusted-prompt scan" an invariant of the type rather
than a step some submitter performs. `new_for_test` delegates to `new`, so
the test-support seal bypasses visibility only, never the scan.
Behaviour at the fire level is unchanged — same rejection point, same
`TriggerError::InvalidMaterialization`, same permanent disposition — and
composition's pre-materialization scan is untouched, so defence in depth
survives with the second scan relocated and now covering every submitter.
`ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its
only use). Enforcement: triggers' boundary rule stops forbidding
`ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge,
not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations`
forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"),
a crate that was unruled until now.
Regression coverage at the caller tier, not on the helper:
`tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached`
drives the real `TriggerPollerWorker::tick_once` with a materializer that
does NOT scan and a submitter configured to accept, and asserts the
submitter is never reached. A companion pins that a medium-severity-only
prompt still submits, so the mint cannot drift into a blanket filter.
Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173
(+2 worker, +2 prompt_safety unit), architecture 206 -> 206.
LAYER_MATRIX_EXCEPTIONS unchanged at 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(coverage): re-anchor the exemptions the merge shifted
tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and
auto-merges silently. #7096's additions to ironclaw_reborn_composition moved
four entries' subject lines by +2 without anything flagging it; a stranded
entry makes the changed-coverage validator abort with no verdict at all.
Re-anchored by content (difflib line map from the #7065 tree, which the file
was validated against, to the union) rather than by arithmetic:
runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085]
runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435]
lib.rs [616] -> [618]
All 142 entries / 1124 line references re-verified against the merged tree:
0 drift, 0 out-of-bounds, 0 missing paths.
* refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4)
Two CHECKLIST rows, both of which were a one-line manifest correction rather
than a code move: the family docs already placed both crates where the rows
want them and only `Cargo.toml`'s `layer =` disagreed.
processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes
among the kernel crates. The re-layer makes processes -> resources a
kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate
said so itself:
Stale IronClaw crate layer matrix exceptions:
ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed
in W7: runtime process management still depends on resource contracts
currently classed with kernel behavior
That is the gate's verdict, not a judgement call - deleting the entry is the
only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list).
Checked the direction both ways: all nine crates that take a normal dependency
on processes (capabilities, turns, host_runtime, extension_host, loop_host,
extension_manager, runner, reborn_composition, stress) are kernel or above, so
the move legalizes an edge without forbidding an existing one.
skills -> substrates (WS4 SS3.D). families/domains.md already lists
ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies
are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both
at or below substrates, and its six consumers are all loops or above. No
exception moves in either direction.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
cargo check --workspace --all-targets: clean.
* docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence
Every tick was verified against the merged tree, never against a PR title.
TICKED:
- sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and
ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one
manifest in the workspace.
- mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only,
0 production ironclaw_extensions:: refs in src/.
- skills -> substrates: landed here.
- hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four
rejected alternatives and the evidence they are already converged on one
trait plus a shared conformance suite. #6945 read first as the row demands,
and explicitly NOT discharged: this PR changes nothing in the dispatch path.
- WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10
exceptions carried removes_in = W7). Corrected with the replaced text
quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder
named with its owning field value. Ticked on the corrected condition.
LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave:
- first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime.
Ticking would be false.
- processes/capabilities row: re-layer DONE; the capabilities/host.rs split is
deferred with every module boundary already computed (4,560 lines, the six
workflow ranges, and the arch-exempt waiver that must be deleted with it).
- host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs
RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same
section's Keeps clause; zero external references to either). Catalog half
cannot go to extension_host at all - host_runtime is itself a production
consumer at memory_native_extension.rs:96,101, so the move is a
kernel -> products edge and a Cargo cycle. Correct destination is downward.
- network test_rewrite: NOT executed. Recorded the security shape (production
binaries compile the seam and honour the rewrite env var at runtime) and the
full 6-step plan, because the env var is how the entire E2E suite redirects
vendor traffic through the production binary and the change needs feature
forwarding into CI lanes I cannot verify here.
cargo test -p ironclaw_architecture: 206 passed, 0 failed.
* refactor(traces): drop the boundary-laundering re-export modules (WS6)
PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules
(`recording`, `paths`) — consumers import the owners".
`ironclaw_reborn_traces::{recording, paths}` were two `pub use <other
crate>::*` passthroughs whose own doc comments stated their purpose
plainly: "so reborn-cli does not need a direct `ironclaw_llm`
dependency, preserving the architectural boundary". They preserved
nothing — the edge existed either way; the wildcard only hid which crate
owned the type, so the dependency graph read as a lie.
All three call sites were in `ironclaw_reborn_cli`. Note the literal
reading of "consumers import the owners" is not available here: the CLI's
dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`)
deliberately excludes `ironclaw_llm`, so importing the owner would have
traded a laundered re-export for a breached, tested boundary. Satisfied
instead by giving the owning crate the operation, which is what the
laundering was standing in for:
- `onboarding::onboard_instance(invite, consents)` — resolves the
contribution root itself. Path layout under the base dir is this
crate's own knowledge; the CLI no longer needs base-dir vocabulary.
- `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)`
— parses `ironclaw_llm::recording::TraceFile` inside the crate that
already depends on `ironclaw_llm`. The CLI hands over raw JSON.
- the CLI's private `trace_contribution_dir()` now delegates to
`contribution::trace_contribution_dir_for_scope(None)` instead of
re-deriving `<base>/trace_contributions`. Verified byte-identical:
`trace_contribution_dir_for_scope(None)` is
`trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`,
whose `None` arm returns `base.join("trace_contributions")`.
No dependency was added to any crate. Semantics unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(llm): make providers.json a crate asset with a boundary rule (WS6)
CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition
input + boundary rule added".
The provider catalog sat at the **repository root**. A root-level data
file has no owning crate, so no boundary rule could govern who edits it,
and every consumer compiled it in behind Cargo's back with an escaping
`include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner
inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json`
and the 20 `include_str!("../../../providers.json")` sites in
`registry.rs` become in-crate `../assets/providers.json`.
⚠ Correcting the row's inherited premise: a prior lane recorded the
"load-bearing include site is in `ironclaw_reborn_cli`" and judged the
item "needs a new mechanism, not a new path". Measured on main: the
load-bearing site is `crates/ironclaw_llm/src/registry.rs:383`
(`builtin_provider_definitions`), inside the owning crate. No new
mechanism was needed — only the path.
**Path-keyed gates rewritten in the same commit** (WS10: these fail
*silently* under a move):
- `Dockerfile` — both `COPY providers.json providers.json` lines deleted;
`COPY crates/ crates/` already covers the new location in both stages.
Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs).
- `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path
filter and its regex alternative removed; the depth-independent
`crates/**` entry already matches. `ws12_workflow_contracts.py` passes.
- `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both
lanes) classification under the new path rather than letting it fall
through to crate scope, so CI breadth does not silently narrow; the
now-redundant entry in the reborn-only branch is dropped.
**The one consumer that could not simply be repointed.** The CLI's
`default_llm_consts_match_the_real_providers_json_nearai_entry` embedded
the catalog from five directories up to check its mirrored `DEFAULT_LLM_*`
constants. Repointing it would have turned a repo-root reach-in into a
*cross-crate* reach-in — the category §11.2.7 turns into a hard failure —
and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency
rule belongs in the cross-crate suite, so the assertions moved into
`ironclaw_architecture` and read both files from disk at runtime, needing
no compile-time coupling at all.
Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the
removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate`
in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the
asset's location, the repo root's emptiness, and single-embedder
ownership). Net test count +0.
**The new rule is sabotage-tested** — five cases, each red with the right
message, each restored to green:
1. catalog copied back to the repo root -> "must not sit at the
repository root"
2. a foreign crate `include_str!`s it -> names the offending file
3. catalog `default_model` drifts from the CLI mirror -> names the const,
the field and both files
4. walker pointed at a non-existent dir -> "walked only 0 Rust files ...
would pass no matter what the tree contained" (reachability)
5. mirrored const renamed -> "no longer declared as a plain const ...
update the extraction rather than deleting the drift check"
Case 2 caught a real false positive in the first draft of the guard: a
file-level `include_str!` AND `providers.json` conjunction flagged
`cli/tests/smoke.rs`, which names the *runtime*
`$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something
else. The matcher now inspects the macro argument, not the file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* ci(coverage): recapture the two composed floors from a real measurement
The provisional values were arithmetic - the sum of the two slices' recorded
deltas - and the dispatch caught them, which is the whole reason the brief
demanded a measurement rather than a reconciliation.
Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd:
26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one
skip is the pull_request-gated mutation gate; the two failures are the coverage
report and the roll-up it drags down, i.e. this file doing its job.
ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63%
(17562 / 19814). The composition was wrong by 1300 denominator lines because
both slices measured their delta under the pre-#7083 aggregator, which could
not see crates/extensions/** at all - lines leaving host_runtime for
extension_support vanished from the tree it could measure, so neither branch's
recorded delta describes the post-#7094 world.
ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's
82.64% (6826 / 8260), captured before #7080's executor lines arrived.
floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye
rather than written quietly. Evidence it is composition and not lost tests:
floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more
absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398
with zero test names lost. Same shape as #7094's own ironclaw_runner recapture.
ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657).
The [global] entry is untouched: both moves are crate-to-crate inside the set
the fixed aggregator sees.
* docs(skills): rewrite the stale v1 lib.rs charter note (WS6)
CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc
rewritten".
The crate doc claimed "In v1, trust-based tool filtering happens via
`src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust
labels and the policy engine controls tool access via capability leases."
Both halves are dead vocabulary: there is no `src/` monolith on this tree
and no Python orchestrator anywhere in Reborn.
Replaced with what is true and checkable — this crate owns the trust
*label* and none of its enforcement; the ceiling is applied at the
capability tier (`host_api` capability/invocation attenuation via
`first_party_extension_ports`' activation and execution paths) and the
decision belongs to `ironclaw_authorization`. Also points at the existing
`SkillTrust` `Ord` safety note, which the old text left unconnected.
Doc-only; no code change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(network): compile the test rewrite seam out of production builds (WS3)
Closes the WS3 network row. Also RETRACTS an overstatement I made in this
row's earlier annotation.
CORRECTION FIRST. The earlier note claimed production binaries compile the
seam and honour IRONCLAW_REBORN_TEST_HTTP_REWRITE_MAP at runtime, so anyone
able to…
…ns (batch of 7 slices) (nearai#7258) * refactor(contracts): move extension runtime descriptors to a neutral 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> * refactor(sandbox): merge the sandbox lane into one crate (WS3) 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> * docs(target-architecture): record the WS3 corrections with their evidence 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> * chore(sandbox): drop imports the merge left unused `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> * fix(ci): let the Reborn PR planner plan guidance edits and crate deletions 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> * fix(arch): give the retained resource exceptions an owning issue, not 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> * test(contracts): pin the asset-path validator that moved into extension_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> * test(coverage): re-capture the host_runtime floor and floor the new sandbox 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> * docs(target-architecture): record the coverage ratchet as a move-sensitive 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> * fix(extension-manager): repoint ironhub onto the moved ExtensionAssetPath 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> * test(coverage): exempt the WS3 move's no-region lines and record the 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 @ b89fcd3575), 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> * docs(sandbox,mcp): correct the wiring inventory and record the projection 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> * refactor(extensions): move the skill-install executor to extension_support (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> * docs(sandbox): record that the Docker fail-closed switch is wired to 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> * docs(host_runtime): record the executor/adapter seam in crate guidance 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> * refactor(host_runtime): keep the install-input error path log-free 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> * ci(coverage): re-capture the host_runtime floor for the WS3 executor 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: e07b3b0299b0add11117e9591da71d46d7a7c832 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> * refactor(wasm): move wit/ inside its owning crate (Wave 3) 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> * build(wasm): rebuild first-party artifacts for the moved wit/ path 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> * docs(target-arch): record the WS7 artifact-rebuild cost of guest path 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> * ci(planner): classify the path classes that blocked the wit/ move `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> * refactor(host-runtime): split obligations into its three chartered owners (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> * refactor(operator,contracts): route operator secrets through a product_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> * test(sandbox): put the Docker security check behind the fail-closed gate 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> * docs(reborn): stop calling the unwired script lane an execution lane 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> * fix(ci): pin the WIT scope probes and the embedded-asset owner pairing 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> * docs(host-runtime): state the obligation visibility rule as it holds Review catch (#7090): the guardrail sentence promised "cross-owner access is `pub(super)`, never `pub(crate)`", which is stronger than the code. Verified: `RuntimeSecretInjectionStore::{insert, take, clone_material, discard_for_capability}`, `NetworkObligationPolicyStore::{insert, get, take, discard_for_capability}` and both constructors are `pub(crate)` and must stay so — `src/egress/{mod,host_port,credential}.rs` call them, and that is host-runtime composition outside `obligations/`. The rule is restated as the property that actually holds: a method whose only callers are inside `obligations/` is `pub(super)` (the three that are), and `pub(crate)` is what the stores expose to the egress pipeline they exist to serve. A future agent reading the old sentence would have read the existing `pub(crate)` methods as violations. Guidance-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(architecture): put the operator secrets boundary entry on the right rule Review catch (#7096), and it is the serious kind: the `"ironclaw_secrets"` entry landed in `ironclaw_extension_contracts`'s forbidden vector, not `ironclaw_operator`'s. The suite still passed, because `extension_contracts` has no such dependency and `ironclaw_operator` then had no entry at all — so the guard this row exists to add was inert, and a green architecture suite was evidence of nothing. Reintroducing the edge would have passed every check. Moved to `ironclaw_operator`'s vector; `extension_contracts` restored to its `origin/main` content byte-for-byte. Negative-probed rather than assumed. With `ironclaw_secrets` temporarily re-added to `crates/ironclaw_operator/Cargo.toml`: reborn_crate_dependency_boundaries_hold ... FAILED ironclaw_operator must not have a normal dependency on ironclaw_secrets and with the manifest restored, 35/35 pass. Two further review findings, both verified before being accepted: - `ironclaw_extension_manager` **does** have a `boundary_rules()` entry (`:3543-3556`, added with WS2.4). The CHECKLIST residue note and PROPOSAL §8.2's 2026-08-02 amendment both said it had none; §8.2's sentence is stale and is marked superseded. The real gap is narrower and now stated: the rule exists and simply does not forbid `ironclaw_secrets` (#7095). - `ironclaw_product_contracts`'s guide claimed "twenty-four shipped modules". Measured: `src/lib.rs` has 26 shipped (27 `pub mod` less the gated `test_support`), and the table was missing `ironhub` **before** this branch touched it. Count corrected to twenty-six and the missing `ironhub` row added, so the inventory matches `lib.rs`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(sandbox): state the Docker-gate claim as the search that checks it Review caught a false inventory in the Known debt entry, and the previous commit is what made it false: "the name appears only in docker_gate.rs and attribution_tests.rs" stopped holding the moment docker_security.rs gained a module doc naming the variable, and CLAUDE.md itself was already a third counterexample. The narrower claim is the one that was always meant and is the one that matters, so it now carries its own reproduction: no workflow, script, env file or manifest mentions the name at all -- `git grep` over *.yml/*.yaml/*.sh/ *.toml/*.py/*.json/.env* is empty here and on main -- and the sole code reference is a read, std::env::var(...) at docker_gate.rs:23. Every other occurrence is a doc comment or a panic message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(triggers,conversations): scan trusted trigger prompts at the mint (WS6) PROPOSAL §6.4.2 asked for the trusted-trigger prompt safety scan to move "behind the triggers/kernel seam it guards". It was not a module: it was three lines inside `ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire` — one of the two implementations of `ironclaw_triggers::TrustedTriggerFireSubmitter` — holding its own `Arc<dyn InjectionScanner>` from `Sanitizer::new()`. That placement is a fail-open: a guard that lives inside one implementation of a port is lost the moment a second implementation exists, and nothing in the tree forced a new submitter to re-run it. The seam is `TrustedTriggerFireSubmitter`, whose only input is the sealed `TrustedTriggerSubmitRequest`, which `ironclaw_triggers` is the sole minter of. So the scan moved to the mint: `TrustedTriggerSubmitRequest::new` is now fallible and calls the new `ironclaw_triggers::prompt_safety` first, making "this prompt passed the trusted-prompt scan" an invariant of the type rather than a step some submitter performs. `new_for_test` delegates to `new`, so the test-support seal bypasses visibility only, never the scan. Behaviour at the fire level is unchanged — same rejection point, same `TriggerError::InvalidMaterialization`, same permanent disposition — and composition's pre-materialization scan is untouched, so defence in depth survives with the second scan relocated and now covering every submitter. `ironclaw_conversations` drops `ironclaw_safety` entirely (the scan was its only use). Enforcement: triggers' boundary rule stops forbidding `ironclaw_safety` (a same-layer, I/O-free `substrates` leaf — a peer edge, not a reach upward), and a NEW `BoundaryRule` for `ironclaw_conversations` forbids it, plus `ironclaw_threads` (§6.4.2's "Never: transcript content"), a crate that was unruled until now. Regression coverage at the caller tier, not on the helper: `tick_rejects_injection_prompt_before_any_trusted_submitter_is_reached` drives the real `TriggerPollerWorker::tick_once` with a materializer that does NOT scan and a submitter configured to accept, and asserts the submitter is never reached. A companion pins that a medium-severity-only prompt still submits, so the mint cannot drift into a blanket filter. Tests: conversations 97 -> 97 (name-identical), triggers 169 -> 173 (+2 worker, +2 prompt_safety unit), architecture 206 -> 206. LAYER_MATRIX_EXCEPTIONS unchanged at 10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(coverage): re-anchor the exemptions the merge shifted tests/integration/changed-coverage-exemptions.toml is exact-line-keyed and auto-merges silently. #7096's additions to ironclaw_reborn_composition moved four entries' subject lines by +2 without anything flagging it; a stranded entry makes the changed-coverage validator abort with no verdict at all. Re-anchored by content (difflib line map from the #7065 tree, which the file was validated against, to the union) rather than by arithmetic: runtime.rs [4068..4073, 4082, 4083] -> [4070..4075, 4084, 4085] runtime.rs [3701] -> [3703] ; runtime.rs [3433] -> [3435] lib.rs [616] -> [618] All 142 entries / 1124 line references re-verified against the merged tree: 0 drift, 0 out-of-bounds, 0 missing paths. * refactor(layers): re-layer processes -> kernel and skills -> substrates (WS3/WS4) Two CHECKLIST rows, both of which were a one-line manifest correction rather than a code move: the family docs already placed both crates where the rows want them and only `Cargo.toml`'s `layer =` disagreed. processes -> kernel (WS3). families/kernel.md already lists ironclaw_processes among the kernel crates. The re-layer makes processes -> resources a kernel -> kernel edge, so its LAYER_MATRIX_EXCEPTION went STALE and the gate said so itself: Stale IronClaw crate layer matrix exceptions: ironclaw_processes -> ironclaw_resources from 2026-07-09 should be removed in W7: runtime process management still depends on resource contracts currently classed with kernel behavior That is the gate's verdict, not a judgement call - deleting the entry is the only way to make it pass. Baseline 5 -> 4, recomputed as len(merged list). Checked the direction both ways: all nine crates that take a normal dependency on processes (capabilities, turns, host_runtime, extension_host, loop_host, extension_manager, runner, reborn_composition, stress) are kernel or above, so the move legalizes an edge without forbidding an existing one. skills -> substrates (WS4 SS3.D). families/domains.md already lists ironclaw_skills under 'Layer(s): substrates'. Its only two normal dependencies are ironclaw_filesystem (substrates) and ironclaw_host_api (contracts), both at or below substrates, and its six consumers are all loops or above. No exception moves in either direction. cargo test -p ironclaw_architecture: 206 passed, 0 failed. cargo check --workspace --all-targets: clean. * docs(target-arch): close the WS3/WS4 rows this work satisfies, with evidence Every tick was verified against the merged tree, never against a PR title. TICKED: - sandbox lane merge: ironclaw_sandbox exists, ironclaw_scripts and ironclaw_process_sandbox absent, bollard/rcgen declared by exactly one manifest in the workspace. - mcp drops the registry dep: ironclaw_extensions is [dev-dependencies] only, 0 production ironclaw_extensions:: refs in src/. - skills -> substrates: landed here. - hooks libSQL/Postgres [decision]: ADR recorded - keep both, with the four rejected alternatives and the evidence they are already converged on one trait plus a shared conformance suite. #6945 read first as the row demands, and explicitly NOT discharged: this PR changes nothing in the dispatch path. - WS3 verify row: the row conflated Wave 3 with Wave 5 work (9 of its 10 exceptions carried removes_in = W7). Corrected with the replaced text quoted, the Wave-3 half satisfied edge by edge, and the Wave-5 remainder named with its owning field value. Ticked on the corrected condition. LEFT OPEN OR PARTIAL, each with measurements rather than a hand-wave: - first_party_tools: 1 of 6 families moved; 15 modules still in host_runtime. Ticking would be false. - processes/capabilities row: re-layer DONE; the capabilities/host.rs split is deferred with every module boundary already computed (4,560 lines, the six workflow ranges, and the arch-exempt waiver that must be deleted with it). - host_runtime binding/catalog-defaults: binding half REFUTED (moving it needs RuntimeLaneExecutor/RuntimeLaneRequest made pub, contradicting the same section's Keeps clause; zero external references to either). Catalog half cannot go to extension_host at all - host_runtime is itself a production consumer at memory_native_extension.rs:96,101, so the move is a kernel -> products edge and a Cargo cycle. Correct destination is downward. - network test_rewrite: NOT executed. Recorded the security shape (production binaries compile the seam and honour the rewrite env var at runtime) and the full 6-step plan, because the env var is how the entire E2E suite redirects vendor traffic through the production binary and the change needs feature forwarding into CI lanes I cannot verify here. cargo test -p ironclaw_architecture: 206 passed, 0 failed. * refactor(traces): drop the boundary-laundering re-export modules (WS6) PROPOSAL §6.4.14: "drop the boundary-laundering re-export modules (`recording`, `paths`) — consumers import the owners". `ironclaw_reborn_traces::{recording, paths}` were two `pub use <other crate>::*` passthroughs whose own doc comments stated their purpose plainly: "so reborn-cli does not need a direct `ironclaw_llm` dependency, preserving the architectural boundary". They preserved nothing — the edge existed either way; the wildcard only hid which crate owned the type, so the dependency graph read as a lie. All three call sites were in `ironclaw_reborn_cli`. Note the literal reading of "consumers import the owners" is not available here: the CLI's dependency allowlist (`reborn_cli_binary_crate_stays_separate_from_v1_root`) deliberately excludes `ironclaw_llm`, so importing the owner would have traded a laundered re-export for a breached, tested boundary. Satisfied instead by giving the owning crate the operation, which is what the laundering was standing in for: - `onboarding::onboard_instance(invite, consents)` — resolves the contribution root itself. Path layout under the base dir is this crate's own knowledge; the CLI no longer needs base-dir vocabulary. - `TraceClientHost::build_envelope_from_recorded_trace_json(json, opts)` — parses `ironclaw_llm::recording::TraceFile` inside the crate that already depends on `ironclaw_llm`. The CLI hands over raw JSON. - the CLI's private `trace_contribution_dir()` now delegates to `contribution::trace_contribution_dir_for_scope(None)` instead of re-deriving `<base>/trace_contributions`. Verified byte-identical: `trace_contribution_dir_for_scope(None)` is `trace_contribution_dir_for_scope_at(&ironclaw_base_dir(), None)`, whose `None` arm returns `base.join("trace_contributions")`. No dependency was added to any crate. Semantics unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(llm): make providers.json a crate asset with a boundary rule (WS6) CHECKLIST WS6: "`llm` `providers.json` becomes a crate asset/composition input + boundary rule added". The provider catalog sat at the **repository root**. A root-level data file has no owning crate, so no boundary rule could govern who edits it, and every consumer compiled it in behind Cargo's back with an escaping `include_str!` — the "repo-root asset reach-in" shape §11.2.7's scanner inventories. `git mv`'d to `crates/ironclaw_llm/assets/providers.json` and the 20 `include_str!("../../../providers.json")` sites in `registry.rs` become in-crate `../assets/providers.json`. ⚠ Correcting the row's inherited premise: a prior lane recorded the "load-bearing include site is in `ironclaw_reborn_cli`" and judged the item "needs a new mechanism, not a new path". Measured on main: the load-bearing site is `crates/ironclaw_llm/src/registry.rs:383` (`builtin_provider_definitions`), inside the owning crate. No new mechanism was needed — only the path. **Path-keyed gates rewritten in the same commit** (WS10: these fail *silently* under a move): - `Dockerfile` — both `COPY providers.json providers.json` lines deleted; `COPY crates/ crates/` already covers the new location in both stages. Verified by `scripts/ci/check-include-str-paths.sh` (OK, 119 refs). - `.github/workflows/reborn-e2e.yml` — the literal `providers.json` path filter and its regex alternative removed; the depth-independent `crates/**` entry already matches. `ws12_workflow_contracts.py` passes. - `scripts/ci/classify-test-scope.sh` — kept at its **shared** (both lanes) classification under the new path rather than letting it fall through to crate scope, so CI breadth does not silently narrow; the now-redundant entry in the reborn-only branch is dropped. **The one consumer that could not simply be repointed.** The CLI's `default_llm_consts_match_the_real_providers_json_nearai_entry` embedded the catalog from five directories up to check its mirrored `DEFAULT_LLM_*` constants. Repointing it would have turned a repo-root reach-in into a *cross-crate* reach-in — the category §11.2.7 turns into a hard failure — and the CLI may not depend on `ironclaw_llm`. A cross-crate consistency rule belongs in the cross-crate suite, so the assertions moved into `ironclaw_architecture` and read both files from disk at runtime, needing no compile-time coupling at all. Test accounting: `ironclaw_reborn_cli` config-init tests 2 -> 1; the removed one is reborn as `reborn_provider_catalog_is_owned_by_its_crate` in `reborn_dependency_boundaries.rs`, strictly stronger (it also pins the asset's location, the repo root's emptiness, and single-embedder ownership). Net test count +0. **The new rule is sabotage-tested** — five cases, each red with the right message, each restored to green: 1. catalog copied back to the repo root -> "must not sit at the repository root" 2. a foreign crate `include_str!`s it -> names the offending file 3. catalog `default_model` drifts from the CLI mirror -> names the const, the field and both files 4. walker pointed at a non-existent dir -> "walked only 0 Rust files ... would pass no matter what the tree contained" (reachability) 5. mirrored const renamed -> "no longer declared as a plain const ... update the extraction rather than deleting the drift check" Case 2 caught a real false positive in the first draft of the guard: a file-level `include_str!` AND `providers.json` conjunction flagged `cli/tests/smoke.rs`, which names the *runtime* `$IRONCLAW_REBORN_HOME/providers.json` and separately embeds something else. The matcher now inspects the macro argument, not the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(coverage): recapture the two composed floors from a real measurement The provisional values were arithmetic - the sum of the two slices' recorded deltas - and the dispatch caught them, which is the whole reason the brief demanded a measurement rather than a reconciliation. Dispatch run 30907774036 at 4512e03e28f1df15b419d2e36f9f38f8f55d62fd: 26 success / 1 skipped / 2 failure, judged by per-job tally per #6978. The one skip is the pull_request-gated mutation gate; the two failures are the coverage report and the roll-up it drags down, i.e. this file doing its job. ironclaw_host_runtime: predicted 89.05% (18801 / 21114), MEASURED 88.63% (17562 / 19814). The composition was wrong by 1300 denominator lines because both slices measured their delta under the pre-#7083 aggregator, which could not see crates/extensions/** at all - lines leaving host_runtime for extension_support vanished from the tree it could measure, so neither branch's recorded delta describes the post-#7094 world. ironclaw_extension_support: MEASURED 75.31% (7142 / 9484) against #7094's 82.64% (6826 / 8260), captured before #7080's executor lines arrived. floor_percent FALLS 7.33pp and that is flagged in the file for an owner's eye rather than written quietly. Evidence it is composition and not lost tests: floor_covered_lines RISES 6826 -> 7142, so the crate is protected by more absolute lines than before, and #7080's un-masking accounting was 1398 -> 1398 with zero test names lost. Same shape as #7094's own ironclaw_runner recapture. ironclaw_sandbox passed unchanged at its arrival capture (87.09%, 3185 / 3657). The [global] entry is untouched: both moves are crate-to-crate inside the set the fixed aggregator sees. * docs(skills): rewrite the stale v1 lib.rs charter note (WS6) CHECKLIST WS6 domain-internal cleanups: "`skills` stale v1 lib.rs doc rewritten". The crate doc claimed "In v1, trust-based tool filtering happens via `src/skills/attenuation.rs`. In v2, the Python orchestrator handles trust labels and the policy engine controls tool access via capability leases." Both halves are dead vocabulary: there is no `src/` monolith on this tree and no Python orchestrator anywhere in Reborn. Replaced with what is true and checkable — this crate owns the trust *label* and none of its enforcement; the ceiling is applied at the capability tier (`host_api` capability/invocation attenuation via `first_party_extension_ports`' activation and execution paths) and the decision belongs to `ironclaw_authorization`. Also points at the existing `SkillTrust` `Ord` safety note, which the old text left unconnected. Doc-only; no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(network): compile the test rewrite seam out of production builds (WS3) Closes the WS3 network row. Also RETRACTS an overstatement I made in this row's earlier annotation. CORRECTION FIRST. The earlier note claimed production binaries compile the seam and honour IRONCLAW_REBORN_TES…
Closes the remaining Wave 2 sequence items in one PR, at the owner's explicit instruction (PLAN principle 1's owner-by-owner rule is overridden for this batch). Commits are separated per item and each says whether it is a move or carries semantics.
b13f140ironclaw_extensions→ substratesb60abb8include_str!killsc30589288f5612slack_userbehavioural coverage4c841a4e14af72Three of the six items the PLAN's Wave 2 ✎ note lists as open had already landed on
main(#7040 strays; #7037 colocation, telegram merge, memory-provider move). The note is corrected in this PR rather than worked around.1. Re-layer —
ironclaw_extensionsloops→substratesLAYER_MATRIX_EXCEPTIONS10 → 6. Baseline recomputed aslen(merged list)after mergingorigin/maindown (git merge, never rebase); the constant and the array agree at 6.The four that fell —
host_runtime,capabilities,mcp,scripts→ironclaw_extensions— are one edge under four names: kernel/runtimes-tier crates consuming the registry's manifest DTOs. None was waived and no edge was deleted; the registry moved to a layer every tier above may legally reach. PROPOSAL §6.8.1 predicted two of the four and is corrected.The one edge that blocked the move dissolved rather than being waived. At
substratesa crate may name onlycontracts/substrates;ironclaw_extensionsnamedironclaw_trust(kernel) for exactly one type used by one method. Every field ofTrustPolicyInputwas alreadyironclaw_host_apivocabulary, and the type names no decision, ceiling, or provenance — so it moved toironclaw_host_api::trust, which is §6.8.1's own prescription ("trust-vocabulary viahost_api"). Consumers repointed, nopub useshim.⚠ The
extension_host→loopshalf is NOT in this PR, and the reason is a correction to §12.11 D-A. D-A resolved the ownership question that blocked the flip and concluded "the seam is narrow, which is why this is cheap". True ofchannel_host.rs; false of the crate. Re-measured tree-wide: twelve production files nameironclaw_product, andironclaw_host_ingress(alsoproducts) is a second blocking edge D-A never names. Filed as #7092 with the full inventory; D-A amended in place.2.
include_str!killspackages/nearai.rs) — the last of twelve package directories without one. It is deliberately not aPACKAGESentry: every entry there is a config-freefn() -> PackageBundle, and NEAR AI's shipped[mcp].serveris a placeholder the host rewrites from LLM-admin bootstrap config. Embeds live with the inventory; the patch stays with the endpoint authority.extension_managerroute throughbundled_packages()— same stated intent (project the shipped field set), typed API instead of a relative path.Measured with the §11.2.7 scan: escaping 133 → 128, cross-crate 19 → 17.
REPORT_ONLYstaystrue: the 17 survivors belong to three other owners (the support crate's own package crates ×5,host_runtime's memory embeds ×7 — five of which the CHECKLIST never mentioned — and four test-only doc reach-ins). Filed as #7093, which also records that one survivor group may not be fixable as stated (the obvious inversion is an upwardloops → productsedge).The
("…/available_extensions.rs", "nearai-mcp")specificity carve-out is deleted with the embed it covered — the allowlist is shrink-only and staleness-checked.3. #7083 — coverage was structurally dark for all of
crates/extensions/reborn_coverage_lcov.pykeyed oncrates/(ironclaw_[A-Za-z0-9_]+)/, and theif match:it gated guards the global aggregate as well as the per-crate table. Five crate directories (~33.7k instrumented lines) contributed nothing to a gate readingenforce = true, with noelse, no counter, no warning. A regression, not a never-worked condition: all five were flat until #7037 three days ago.A better regex cannot fix it — four of five basenames contain no
ironclaw_at all (§5.1 names package directories by extension identity), and a greedy nested pattern mis-attributes an in-cratesrc/ironclaw_*/module directory. The aggregator now resolves throughcrate_tree.py, exactly like the merge script one step upstream that produces the lcov it reads. Their disagreeing is what made the hole silent: the data was in the merged tracefile the whole time.Six new self-test cases; the suite gains a shared fixture crate tree because the aggregator now needs one — the old shape needed no tree at all, which is exactly why every case stayed green while 11 crates went dark.
4. Retired-
slack_userbehavioural coverageThe escalated row (§12.11 D-I) is untouched — no ruling is made or implied. What is owed regardless is its own step (i): the branch has had zero behavioural coverage since #6616 replaced its test with
assert_eq!(RETIRED_SLACK_USER_EXTENSION_ID, "slack_user"), and it runs on every boot and destructively deletes user rows.Pins the behavior as built, including the surprising parts: "deleted" means the v2 record is tombstoned with its manifest retained and only the two legacy projections hard-deleted, and the control — an equally uncatalogued installation that is not the retired id must survive, because deletion keys on the extension id, never on the catalog miss.
Both halves red-checked: disabling the branch fails the removal assertions (and only this test); widening it to every catalog-miss row fails the control.
Verification
cargo test -p ironclaw_architecture— 32 suites, 0 failures, including the layer matrix, the exception ratchet at the new baseline, and the specificity gate.scripts/ci/test-reborn-coverage.sh— 166/178 locally, with the same 12 pre-existing macOS bash-3.2mapfilefailures in the C section thatmainhas today (148/160 before this change). CI runs bash 5.cargo fmtclean; snapshots regenerated after fmt.Coverage
No PR-triggered lane produces a coverage verdict (#7036), so the numbers come from a dispatch: run 30865483401 @
4c841a4321dd2d62b695120c60e6fe6bd4ac5ebf.Reborn integration-tier coverage report— success; every per-job result green.Floors for the four newly-visible crates were captured from that run, after the fix —
ironclaw_extension_support82.64% (6826/8260),slack93.95% (3697/3935),telegram90.31% (1435/1589),memory-native82.85% (2850/3440).mem0is deliberately not floored (compiles only behind thememory-mem0feature, which no lane enables).[global]recaptured 85.11%/375097 → 86.96%/386885: the old denominator never described the tree it was enforcing.ironclaw_extension_hostrecaptured 84.83%/19907/23467 → 87.99%/21605/24554 as the source side of a move, withfloor_covered_lineschecked separately from the percentage (#7080). Re-verified locally against the run's own artifact:reborn-coverage-ratchet.shexits 0, 22 PASS / 0 FAIL, every figure matching CI.Full detail, including the stale-artifact trap that produced four bogus failures before it was caught, is in the coverage comment below.
Test accounting
2811 → 2814 tests across the nine touched crates, unfiltered
--listname-by-name at both ends. Zero removed, three added (all new coverage this PR owes), none edited for content. Per-crate table in the accounting comment below.Exception count
origin/main10 → this branch 6, baseline constant lowered to match, counted with Python betweenconst LAYER_MATRIX_EXCEPTIONSand its closing];(not grep, which also matches the struct definition and four test fixtures) and recomputed aslen(merged list)after mergingorigin/maindown.🤖 Generated with Claude Code