Skip to content

feat(extensions): register hosted MCP servers - #6930

Merged
henrypark133 merged 51 commits into
mainfrom
mcp-registration-pr2
Jul 31, 2026
Merged

henrypark133 merged 51 commits into
mainfrom
mcp-registration-pr2

Conversation

@henrypark133

@henrypark133 henrypark133 commented Jul 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Add tenant-runtime registration for hosted MCP servers and hand the result to the existing extension install, setup, activation, invocation, deactivation, and removal lifecycle.
  • Detect no-auth, bearer, and standards-based OAuth automatically; credentials remain owned by the existing product-auth, secret, and mediated network paths.
  • Persist admitted definitions and discovered tool schemas, publish source/digest/effect-pinned trust only on activation, and keep per-user installation/credential ownership intact.
  • Add the three-step WebUI registration modal with bounded validation and actionable errors.
  • Cover the complete user journeys with deterministic Reborn integration, WebUI contract, frontend, recorded MCP trace, and browser-scenario tests.

Change Type

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • CI/Infrastructure
  • Security
  • Dependencies

Linked Issue

None — implementation follows the reviewed hosted-MCP extension-lifecycle design. Future in-process multi-tenant lifecycle scoping remains separate from this standalone runtime feature.

Verified behavior and root causes

  • The previous product surface had no caller path for admitting an arbitrary hosted MCP definition.
  • Initial local testing exposed three caller-visible regressions that are now protected: invalid registration errors were opaque, completed OAuth setup could remain visually/setup-blocked, and active custom MCP tools were visible but denied because publication and kernel authorization derived different trust inputs.
  • The final clean-slate run used a new Reborn home/database and a real Notion MCP endpoint: registration, automatic OAuth detection, ordinary setup, callback, activation, and authenticated tool invocation completed successfully.

The implementation keeps lifecycle transitions generic. Hosted-MCP-specific work is limited to admission, manifest projection, discovery safety, and preparation; the existing lifecycle service decides install/setup/activation outcomes for every auth type.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all --tests --examples --all-features -- -D warnings — now run and clean. It was not clean before: this PR had renamed ExtensionPackage::root to root_binding and added a required NewAuthFlow::requester_extension without updating call sites, so roughly a dozen test targets did not compile. None of that is visible to cargo check or to focused per-crate tests.
  • cargo build — cargo build -p ironclaw --bin ironclaw
  • Relevant tests pass:
    • cargo test -p ironclaw_reborn_integration_tests --test reborn_integration_hosted_mcp_registration — 9 passed, 2 explicitly ignored live canaries
    • cargo test -p ironclaw_architecture — all pass (three PR-introduced violations fixed; see below)
    • cargo test -p ironclaw_auth — 172 + 30 + 76 passed
    • cargo test -p ironclaw_extension_host --lib — 362 passed
    • scripts/pre-commit-safety.sh — composition within mass + dispatch budget
    • cargo test -p ironclaw_extension_host discovery_enforces_the_manifest_declared_tool_limit
    • cargo test -p ironclaw_auth metadata_urls_are_well_formed
    • cargo test -p ironclaw_extensions trust_policy_source_uses_the_package_root_binding
    • corepack pnpm test — 112 files, 925 tests
    • corepack pnpm typecheck
  • cargo test --features integration — not applicable: the selected Reborn integration binary directly covers this cross-crate behavior without database/Docker features.
  • Manual testing: fresh runtime/database, real Notion registration + OAuth + activation + authenticated MCP invocation
  • Local multi-lane review was re-run against the current head. It did not come back clean — see "Post-review fixes" below. An earlier claim in this description that review "converged with no unresolved findings" was inaccurate and has been corrected.

Test Strategy

User behavior:

Register a hosted MCP endpoint once for the standalone tenant runtime; users discover it but install and configure their own membership/account. Registration auto-detects auth. Setup may remain unfinished or reject bad credentials without publishing tools. Successful no-auth, bearer, or OAuth setup activates the ordinary extension and permits mediated invocation.

Risk areas:

  • Model behavior
  • Browser
  • Side effect
  • Persistence
  • Security or permissions
  • External provider
  • Cross-component behavior

Tests added or updated:

  • Unit or contract: manifest admission/canonicalization, RFC 8414/DCR, trust-source pinning, catalog limits/safety, WebUI descriptors/handlers, frontend modal/hook behavior.
  • Reborn integration: tests/integration/hosted_mcp_registration.rs covers validation rejection, no-auth, bearer missing/wrong/correct, OAuth unfinished/success/rejected-token, restart/restore, shared discovery with per-user install/remove, trust publication, credential injection, and invocation.
  • Recorded fixture: tests/fixtures/hosted_mcp/microsoft_mrc_streamable_http.json replays a real streamable-HTTP server shape through the lifecycle.
  • Browser E2E: tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py covers registration and setup UI; final browser verification was performed manually against the rebuilt binary.
  • Backend or runtime: production-composed extension/product-auth/runtime seams are exercised by the Reborn integration binary; no database schema was added.
  • Live canary: two ignored public-server checks remain opt-in; a clean-slate real Notion OAuth/invocation run passed manually.

What the tests prove:

  • Registration is bounded, idempotent for an identical definition, and fails without partial installation side effects.
  • The definition is shared within the standalone tenant runtime, while installation, credentials, activation visibility, and removal remain user-scoped.
  • Auth selection is server-driven, and all auth outcomes rejoin the existing setup/activation lifecycle.
  • Credentials stay host-side and reach MCP egress only through the existing mediated injection path.
  • Activation publishes an endpoint/digest/effect-pinned trust ceiling, and caller-visible grants still gate invocation.
  • Gap (being addressed): no hosted-MCP test asserted that removal revokes publication. The multi-user removal test checked list/search state only. Coverage for revocation is being added.
  • Catalog size and prompt-safety policy fail closed without exposing unsafe tool metadata.

Commands run:

cargo fmt --all -- --check
cargo build -p ironclaw --bin ironclaw
cargo test -p ironclaw_reborn_integration_tests --test reborn_integration_hosted_mcp_registration --no-fail-fast
cargo test -p ironclaw_extension_host discovery_enforces_the_manifest_declared_tool_limit
cargo test -p ironclaw_auth metadata_urls_are_well_formed
cargo test -p ironclaw_extensions trust_policy_source_uses_the_package_root_binding
corepack pnpm test
corepack pnpm typecheck
git diff --check

Security Impact

Adds an authenticated registration route for external HTTPS MCP endpoints and new trust-policy publication for activated remote packages. Endpoint admission rejects unsafe URL shapes; OAuth metadata and registration endpoints use bounded mediated egress; credentials remain encrypted/host-side; tool catalogs are bounded and safety-scanned; trust is pinned to package ID, canonical endpoint, manifest digest, and declared effects. Registration alone grants no execution authority.

Reborn Trust-Boundary Checklist

  • Public policy/evidence/trust-bearing types: effective trust entries are constructed only by host-mediated activation; untrusted manifests express requested trust only.
  • Untrusted content enters prompts only through an envelope/escaping primitive: discovered descriptions/schemas pass catalog admission and model-safety projection.
  • Hashes declare purpose; trust/binding/authenticity uses SHA-256/BLAKE3 or separate authenticity check: manifest trust identity uses the existing SHA-256 digest token.
  • New/changed status, exit, policy, runtime, or error variants: downstream match sites audited. Command/output: focused contract and integration tests listed above.
  • Security/durability serde(default) fields fail closed or have migration tests: legacy manifest/root rows and new definition wire shapes have compatibility tests.
  • Queues/maps/buffers/counters have bounds and overflow-safe arithmetic: request fields, HTTP responses, tool count, descriptions, schemas, pagination, and egress bytes are bounded.
  • Driver/operator-visible errors have stable class semantics: validation, transient discovery, credential rejection, and policy denial remain distinct redacted outcomes.
  • Sandbox/native/host names accurately describe trust boundary: hosted MCP execution remains behind host runtime/network mediation; UserRegistered is provenance, not authority.

Database Impact

None. No SQL migration or backend schema change. Durable extension definitions and lifecycle state use the existing filesystem-backed extension store and compatibility encoding.

Blast Radius

Extension manifest/persistence, lifecycle product surface, product-auth OAuth admission and account setup, MCP discovery/invocation, host trust evaluation, composition wiring, WebUI extension routes, and the extensions SPA. Existing first-party extension setup paths continue through the same lifecycle/auth services.

Rollback Plan

Revert this PR. Existing bundled/registry extension lifecycle and first-party auth paths remain intact; registered hosted-MCP definitions are additive persisted records and become unreachable if the registration surface is removed. No database downgrade is required.

Review Follow-Through

An earlier revision of this description said local review had converged with no
unresolved findings. That was not accurate. A full-scope local review (149 files,
three lanes) was re-run against the current head and found real defects, listed
below with what was done about them.

Production defects found and fixed (each with a regression test that fails before its fix)

Defect Consequence
register held operation_lock while awaiting catalog.write(), inverting the catalog-before-operation order import_bundle/install rely on Concurrent registration and bundle import could deadlock; both are reachable from independent routes
Fresh-install branch hardcoded preparation_ready = true A newly registered hosted MCP with PreparationRequirement::Required reported visible placeholder capabilities and an installed response even when discovery failed
restore_extension_lifecycle_state skipped pending installs before registering them with the lifecycle service After restart, a bare activate or OAuth resume failed with "extension is not installed"
Keepalive resolved an extension-owned account's recipe with owner_extension, then dropped that identity before building the refresh request Every extension-owned credential refresh died as CrossScopeDenied before any vendor call — hosted MCP OAuth tokens would silently never refresh
Two private helpers in hosted_mcp_preparation were verbatim copies of ExtensionLifecycleManager methods Two copies of registry-sync logic encoding what counts as "installed"; now delegated to one implementation

The first regression test written for the lock-order fix passed against the bug —
timing-based, the tasks never interleaved. It was replaced with a deterministic
crate-tier test that pauses register between its two lock acquisitions, and it
fails (times out) against the old order.

Build and architecture debt this PR introduced, now repaired

  • Renamed/extended shared types (ExtensionPackage::root → root_binding,
    NewAuthFlow::requester_extension) were not propagated; ~12 test targets did
    not compile.
  • Clippy errors on PR-added code, fixed rather than suppressed: derivable
    Default, items after mod tests, and an 8-argument constructor aggregated
    into a dependencies struct rather than silenced with #[allow].
  • HostedMcpEndpoint was defined twice for two different concepts; the
    crate-local egress matcher is now HostedMcpEgressEndpoint.
  • Six stale entries removed from the shrink-only manifest-reparse allowlist
    (this PR removed those reparses), tightening the gate.
  • A #[cfg]-gated test-support field had been added to a production struct in
    composition; it was dead — the PR's own refactor had moved that egress onto
    HostedMcpPreparationDependencies, and the tests use the existing
    try_with_host_http_egress seam. Removed rather than re-baselined.

No test was weakened, deleted, or re-baselined to reach green.

Known gaps, stated rather than closed

  • A hosted MCP registered but never installed does not survive a restart.
    registered-definitions/{id}.json is durable, but nothing enumerates it at
    boot, so the rebuilt catalog omits it until the user re-registers.
    Re-registration is idempotent (the admission CAS returns ExactExisting for a
    byte-identical record), so the recovery is to register again. Deliberately
    deferred: closing it needs a new enumeration method on
    ExtensionInstallationStorePort and every implementation.
  • crates/ironclaw_extensions/src/installations.rs is 5487 lines and its
    arch-exempt: large_file justifications predate this PR's addition; the new
    CAS-admission logic arguably belongs in its own module.
  • The browser E2E scenario called an undefined helper on the bearer/OAuth
    setup-success path, so that branch had never executed. Fixed; the scenario
    still needs a live run to count as evidence.

Review track: C

henrypark133 and others added 14 commits July 29, 2026 14:48
…d lifecycle group

Three test-infrastructure capabilities the MCP-registration workstream needs:

- MockMcpServer: start_mock_mcp_server_paginated() serves tools/list paginated
  with nextCursor, honoring an incoming cursor param. New integration test
  hosted_mcp_discovery_reads_only_first_page_of_paginated_tools_list pins
  today's client behavior: discover_tools reads only page 1 and never follows
  nextCursor (parse_tools_list_result never inspects it). Documents the bug,
  does not fix it (production untouched).
- assert_extension_present mirrors assert_extension_absent in
  extension_user_lifecycle_isolation.rs; wired into the existing
  install/remove symmetry test.
- RebornIntegrationGroup::extension_lifecycle_no_auto_approve(): same profile
  as extension_lifecycle() with auto-approve off, so builtin.extension_install
  (PermissionMode::Ask) raises a real BlockedApproval gate. New test proves
  the block and discriminates against the auto-approve-on group, which
  completes the same install gate-free.

No production code changed.
…namespace

Seed change for user-registered MCP servers (dark, no client/UI):

- ManifestSource::UserRegistered, never eligible for first-party/system
  trust (sits with InstalledLocal), with a wire label ("user_registered")
  that round-trips and doesn't break deserializing older persisted rows.
- Reserved `mcp-` extension-id namespace enforced as a single arm in
  parse_v3, mirroring the existing `ironclaw.` host-bundled reservation,
  so both the direct parse_v3 call and the public
  ExtensionManifestRecord::from_toml entry point inherit the rule.
- Pin generic_host.rs's no-durable-record classification fallback (keys
  off RequestedTrustClass, not ManifestSource — adding a ManifestSource
  variant produces no compiler error there) so it can't silently start
  granting elevated trust to a new source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Relocate assert_user_present calls in extension_user_lifecycle_isolation
  to the one spot where presence isn't immediately re-implied by the next
  assert_user_phase call (the normalized-restart test's post-install
  checkpoint, before any phase check happens).
- Document why extension_lifecycle_approval_gate.rs keeps its own test
  target instead of folding into group_extensions/.
- Replace the dangling "see brief" pointer in mcp.rs's paginated-discovery
  test comment with a concrete description of the pending fix and the
  assertion it should flip to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ExtensionManifestV2::from_raw only enforced the ironclaw. host-bundled
reservation; the mcp- user-registered reservation lived solely in
parse_v3, so a v2-schema manifest with a mcp- id and a non-UserRegistered
source parsed successfully. Extract a shared, predicate-per-entry
reserved-prefix table (check_reserved_id_prefix) and call it from both
parsers so the two prefix rules can't drift apart again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three capabilities the integration suite could not previously express,
each blocking later MCP-registration work:

- MockMcpServer pagination (nextCursor across >=2 pages). Without it a
  page-1-only client bug passes every test. Ships with a test that pins
  the current truncation so the later fix has a concrete regression to
  flip; the repo's own testing rule forbids ignored/TODO-pinned tests,
  so a green test documenting the bug beats a red one nobody runs.
- assert_extension_present, the mirror of the existing absent helper.
- A lifecycle group with auto-approve OFF, wired like live_approvals().
  extension_lifecycle() has it ON, so until now no test could prove any
  lifecycle gate actually blocks.

Reviewed by code-review and thermo-nuclear at low effort: no severe
findings. Both flagged the redundant presence assertion and a dangling
doc pointer; both fixed. Test-only — no production file changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The foundation for user-registered MCP servers. Dark — no surface, no
MCP client code, no activation changes.

- ManifestSource::UserRegistered, never eligible for first-party or
  system trust, round-tripping through the persisted wire form as
  "user_registered".
- The mcp- id prefix is reserved: only a UserRegistered manifest may
  claim it.
- A pinning test for generic_host.rs's fallback, which matches on
  RequestedTrustClass rather than ManifestSource and so gives no
  compiler signal when a variant is added.

Review found a real hole in the first attempt: the namespace check
lived only in parse_v3, but from_toml routes to parse_v3 only for v3
manifests, so a v2 manifest could claim an mcp- id with any source. The
"both import paths" test missed it by using a v3 manifest. Both parsers
now call one shared reserved-prefix helper, which closes the gap and
removes the copy-paste seam a third prefix would have widened.

The helper carries a predicate per rule rather than a source value, so
the host-bundled rule keeps its capability semantics
(allows_first_party) instead of being flattened to source equality.

Gate proofs: the v2 gap was demonstrated red before the fix; the trust
and classification pins were proven by inverse assertion, since the
harness correctly refuses to let a trust gate be weakened even
transiently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	crates/ironclaw_extension_host/src/available_extension_import.rs
#	crates/ironclaw_extension_host/src/test_support/lifecycle.rs
#	crates/ironclaw_product/src/auth_continuation.rs
#	crates/ironclaw_reborn_composition/src/factory.rs
#	crates/ironclaw_reborn_composition/src/runtime.rs
Copilot AI review requested due to automatic review settings July 30, 2026 18:57
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@railway-app

railway-app Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 31, 2026 at 8:53 pm

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6930 July 30, 2026 18:57 Destroyed
@github-actions github-actions Bot added scope: docs Documentation scope: dependencies Dependency updates labels Jul 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added custom MCP server registration through the WebUI and API.
    • Added bearer-token and OAuth authentication with automatic discovery and setup.
    • Added installation, persistence, restoration, and lifecycle management.
    • Added multilingual registration flows and expanded integration coverage.
  • Bug Fixes

    • Improved OAuth isolation and credential handling for extension-owned connections.
    • Strengthened endpoint, metadata, credential, and tool-catalog safety validation.
    • Improved hosted MCP error handling, pagination, and authentication metadata preservation.
    • Improved extension restoration, trust-policy consistency, and configuration-field accessibility.

Walkthrough

Adds hosted MCP registration across host API contracts, OAuth admission, durable preparation, lifecycle activation, runtime discovery, trust handling, WebUI routes, modal UI, and integration coverage.

Changes

Hosted MCP registration

Layer / File(s) Summary
Contracts and admission
crates/ironclaw_host_api/src/hosted_mcp.rs, crates/ironclaw_auth/src/engine/admission.rs, crates/ironclaw_extensions/src/v2.rs
Adds validated registration contracts, OAuth metadata admission, client-profile handling, and reserved mcp- identity rules.
Durable preparation and lifecycle
crates/ironclaw_extensions/src/installations.rs, crates/ironclaw_extension_host/src/hosted_mcp_preparation.rs, crates/ironclaw_extension_host/src/product_lifecycle.rs
Adds retained definitions, preparation leases, installation incarnations, pending checkpoints, restoration, and credential-gated activation.
Runtime and trust integration
crates/ironclaw_mcp/src/lib.rs, crates/ironclaw_extension_host/src/mcp_discovery.rs, crates/ironclaw_extension_host/src/mcp_catalog_safety.rs, crates/ironclaw_extensions/src/package.rs
Adds bounded discovery, redacted authentication challenges, catalog safety admission, typed package roots, and direct-remote trust sources.
OAuth requester scoping
crates/ironclaw_auth/src/engine/mod.rs, crates/ironclaw_auth/src/flow.rs, crates/ironclaw_auth/src/product_auth/...
Carries requester extension identity through recipe resolution, durable flows, callbacks, refreshes, and keepalive.
WebUI and validation
crates/ironclaw_webui/src/webui_v2/*, crates/ironclaw_webui/frontend/src/pages/extensions/*, crates/ironclaw_product/src/*
Adds the registration route, capability dispatch, validated three-step modal, mutation handling, and registry refresh.
Fixtures and coverage
tests/support/hosted_mcp_registration_server.rs, tests/integration/hosted_mcp_registration.rs, tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py
Adds hermetic MCP fixtures and coverage for registration, persistence, bearer authentication, OAuth, restoration, and runtime invocation.

Estimated code review effort: 5 (Critical) | ~180 minutes

Possibly related issues

  • #3288 — The changes implement hosted-MCP registration, lifecycle setup, readiness, and related UI flows.
  • #2599 — The changes add a dedicated hosted-MCP integration test target and crate-owned lifecycle coverage.

Suggested reviewers: copilot, ilblackdragon

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and follows the template, but it lists “None” for Linked Issue even though new features require an approved issue. Add the approved issue reference to the Linked Issue section.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits style and accurately summarizes hosted MCP server registration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@ironloopai

ironloopai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Review · PR #6930

⚫ Cancelled · Target changed

The target changed before this Run could finish.

Automatic · PR opened · attempt 0 of 3 · cancelled after <1s

Run details
  • Repository: nearai/ironclaw
  • Base: main at 9698704
  • Head: mcp-registration-pr2 at 5f48abc
  • Created: Jul 30, 2026, 7:02 PM UTC
  • Updated: Jul 30, 2026, 7:02 PM UTC
  • Run: 1aaa71eb-afa6-499e-8c56-01fc860ca7d8

# Conflicts:
#	crates/ironclaw_webui/src/webui_v2/handlers.rs
#	crates/ironclaw_webui/src/webui_v2/mod.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f48abc584

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/ironclaw_extension_host/src/lifecycle_restore.rs
Comment thread crates/ironclaw_extension_host/src/lifecycle_restore.rs Outdated
Comment thread crates/ironclaw_extension_host/src/product_lifecycle.rs Outdated
@henrypark133
henrypark133 merged commit 2e65225 into main Jul 31, 2026
62 of 64 checks passed
@henrypark133
henrypark133 deleted the mcp-registration-pr2 branch July 31, 2026 21:10
BenKurrek added a commit that referenced this pull request Jul 31, 2026
Resolve the eight content conflicts and relocate host_api::hosted_mcp into
ironclaw_extension_contracts: it and package_lifecycle reference each other, so
once package_lifecycle moved, leaving hosted_mcp behind would have required
host_api to depend on a workspace crate. That cycle produces no git conflict --
the two sides are different files -- so it is recorded in the crate guide, the
checklist row, and the location scan's frozen names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Jul 31, 2026
…on (#6979)

#6930 (2e65225, 153 files, +15,002/-1,818) merged after the north-star
docs were written, and it landed entirely inside the extension family the
restructure is about to reshape. Docs-only reconciliation, house style:
dated amendments beside the original text, nothing rewritten or deleted.

PROPOSAL: new #6930 entry in §2.7 (the anchor, same shape as #6691/#6696/
#6863); re-measured §2.4 extension_host (56,940/93) and §2.5 host_api
(27,898/67/50, prelude gone); §2.2 + §11.1 gain the new registration-
pipeline gate and record that the manifest-reparse gate was tightened, not
deleted; §6.1.1 gains host_api::hosted_mcp and its package_lifecycle
coupling; §6.6.3 mcp 2,475 -> 2,709; §6.8.1 gains the registered-package-
definition record class; §6.8.2 gains the registration pipeline and the
split question it creates; §6.9.4 webui 91 -> 92 routes.

CHECKLIST: WS3 mcp row annotated (the four registry DTOs are unchanged as
an import list, but their shape grew and a second contracts module joined);
WS6 rename row and WS10 path-pattern row annotated with the new gate, whose
hardcoded path prefixes fail *silently* under the rename and the family mv.

PLAN, families/extensions.md, crates/AGENTS.md: matching notes so the
Wave 2 executor and any agent routing hosted-MCP work land in the right
place.

Every amended claim was verified against post-#6930 main with file:line
evidence; "still accurate" findings are recorded in the PR body, not the
docs. No code or test touched.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Jul 31, 2026
…S1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's #6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Aug 1, 2026
…adapter half (WS1.4) (#6980)

* refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

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

* docs(target-architecture): tick WS1.1 and close PLAN decision round #1

WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces #6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

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

* refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract

CodeRabbit review round on #6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

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

* docs(target-architecture): remove the row-97 self-contradiction

The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

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

* ci(coverage): complete the WS1.1 changed-coverage exemptions

Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

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

* refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

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

* refactor(contracts): pin the loop-contract boundary and register the new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

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

* docs(reborn): split the loop-exit contract's ownership claim across the two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

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

* docs(contracts): repoint the three intra-doc links the crate split broke

The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

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

* fix(tests): repoint the loop-exit evidence imports the port rule moved

Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

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

* fix(collapse): reconcile the lock pin and the moved failure-category scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

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

* refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

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

* fix(ci): repoint the exact-test selector WS1.2 moved

scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

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

* fix(collapse): reconcile the new crate's lock pins with main's dep bumps

The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

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

* fix(arch): register the new contracts crate in the two gates that enumerate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

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

* docs(contracts): record why the extension tier's traits are not sealed

The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

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

* fix(coverage): drop the exemption WS1.2 made stale

The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

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

* fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the #6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

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

* refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

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

* docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's #6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

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

* test(contracts): cover the extracted auth-prompt and lifecycle-id surface

The changed-coverage gate on #6980 flagged 134 uncovered lines and 22
uncovered branch arms, all in the two modules WS1.4 created. That was a real
regression, not an attribution artifact: the code moved out of
host_api::product_adapter::outbound and host_api::package_lifecycle, and the
owning crates' unit suites did not move with it.

Fourteen tests close every one of those lines: render_channel_auth_prompt in
both the DM and mention shapes and with/without a pairing deep link, the
AuthPromptContextView constructors and wire round-trip, each nested validator
arm rejecting independently, and the bounded-id accessor and rejection surface.
Verified by replaying reborn_changed_coverage.py against the PR's own merged
lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head:
134 -> 0.

Three sites remain exempted with per-site evidence, all unreachable by test:
a declaration-only crate facade's inner attribute, and two pre-existing error
arms whose only change is a repointed type path.

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

* test(contracts): cover the newline/tab carve-out in bounded prompt text

The changed-coverage gate reached 100% line but left three branch arms on
validate_bounded_text's control-character predicate. Newline and tab are
deliberately legal in prompt copy — channels render multi-line instructions —
and every other control character is a rejection; both halves are now driven.

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

* test(coverage): exempt the four type-position lines the gate cannot instrument

With the auth-prompt and lifecycle-id holes tested, the gate reached 100%
line and 100% branch but still failed on four files 'contributing no
instrumented lines'. Each is a single type-position line — an enum variant
field, two parameter types, a struct field — whose only change is the
repointed path for a moved contract, wrapped onto its own line by rustfmt.
LLVM emits no coverage region for a type annotation, so no test can reach them.

Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38
branches.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Aug 1, 2026
…ss grants (WS1.5) (#6981)

* refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

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

* docs(target-architecture): tick WS1.1 and close PLAN decision round #1

WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces #6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

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

* refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract

CodeRabbit review round on #6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

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

* docs(target-architecture): remove the row-97 self-contradiction

The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

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

* ci(coverage): complete the WS1.1 changed-coverage exemptions

Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

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

* refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

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

* refactor(contracts): pin the loop-contract boundary and register the new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

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

* docs(reborn): split the loop-exit contract's ownership claim across the two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

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

* docs(contracts): repoint the three intra-doc links the crate split broke

The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

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

* fix(tests): repoint the loop-exit evidence imports the port rule moved

Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

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

* fix(collapse): reconcile the lock pin and the moved failure-category scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

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

* refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

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

* fix(ci): repoint the exact-test selector WS1.2 moved

scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

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

* fix(collapse): reconcile the new crate's lock pins with main's dep bumps

The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

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

* fix(arch): register the new contracts crate in the two gates that enumerate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

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

* docs(contracts): record why the extension tier's traits are not sealed

The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

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

* fix(coverage): drop the exemption WS1.2 made stale

The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

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

* fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the #6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

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

* refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

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

* docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's #6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

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

* refactor(contracts): consolidate sealed evidence minting behind witness grants (WS1.5)

CHECKLIST WS1's evidence-mint row: one sealed seam owns construction of
protocol-auth evidence, every other construction path is deleted, and the
refute-tests land with it.

The `host-auth-mint` cargo feature was not a seal. Cargo unifies features
across the packages selected in one invocation, so `ironclaw_webui`'s opt-in
(-> turns -> host_api) compiled `ironclaw_host_api` once with the gate ON for
every other crate in the same build. Measured before touching anything: a probe
in `ironclaw_agent_loop` — whose manifest names `ironclaw_host_api` with no
features — minted a verified bearer claim; it failed to compile alone and
passed as soon as `ironclaw_webui` joined the `cargo test`. Every workspace
build is the second case.

Replaced with the repo's existing witness-token idiom (`host_api::authorized`),
which no other crate's manifest can switch on:

- `HostAuthenticationGrant` <- `HostProtocolAuthenticator`, sole implementor
  `ironclaw_webui` (module-private `AuthLayerState`, trust stage T1).
- `VerifiedInboundGrant` <- `ChannelIngressVerifier`, sole implementor
  `ironclaw_extension_host` (`VerifiedEvidenceMint`, trust stage T2).
- Channel/webhook mint family -> `ironclaw_extension_contracts::verified_inbound`
  (§6.1.2); bearer/session family stays in `host_api` (§6.1.1). The evidence
  type does not move; `extension_contracts` reaches the private verified variant
  through the grant-gated `ProtocolAuthEvidence::seal_verified_inbound`.

Closed: the feature in 5 manifests + 1 CI recipe; four re-export chains
(`host_api::product_adapter`, `ironclaw_product` root, `ironclaw_product::auth`,
and composition's zero-consumer re-export).

Enforcement: `reborn_sealed_evidence_mint_ratchet` (10 tests, §11.2.5) plus
`host_api/tests/protocol_auth_evidence_seal.rs` (5) and
`extension_contracts/tests/verified_inbound_seal.rs` (4). +19 tests, 0 removed,
no surviving assertion edited. The production-struct dead-code baseline shrinks
81/282 -> 80/277: the five `dead_code` suppressions in `auth.rs` existed only
because the constructors were feature-gated.

* test(contracts): cover the extracted auth-prompt and lifecycle-id surface

The changed-coverage gate on #6980 flagged 134 uncovered lines and 22
uncovered branch arms, all in the two modules WS1.4 created. That was a real
regression, not an attribution artifact: the code moved out of
host_api::product_adapter::outbound and host_api::package_lifecycle, and the
owning crates' unit suites did not move with it.

Fourteen tests close every one of those lines: render_channel_auth_prompt in
both the DM and mention shapes and with/without a pairing deep link, the
AuthPromptContextView constructors and wire round-trip, each nested validator
arm rejecting independently, and the bounded-id accessor and rejection surface.
Verified by replaying reborn_changed_coverage.py against the PR's own merged
lcov (byte-identical to CI) and re-measuring with cargo llvm-cov at this head:
134 -> 0.

Three sites remain exempted with per-site evidence, all unreachable by test:
a declaration-only crate facade's inner attribute, and two pre-existing error
arms whose only change is a repointed type path.

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

* test(contracts): cover the newline/tab carve-out in bounded prompt text

The changed-coverage gate reached 100% line but left three branch arms on
validate_bounded_text's control-character predicate. Newline and tab are
deliberately legal in prompt copy — channels render multi-line instructions —
and every other control character is a rejection; both halves are now driven.

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

* test(coverage): exempt the four type-position lines the gate cannot instrument

With the auth-prompt and lifecycle-id holes tested, the gate reached 100%
line and 100% branch but still failed on four files 'contributing no
instrumented lines'. Each is a single type-position line — an enum variant
field, two parameter types, a struct field — whose only change is the
repointed path for a moved contract, wrapped onto its own line by rustfmt.
LLVM emits no coverage region for a type annotation, so no test can reach them.

Replayed against this PR's own merged lcov: exit 0, 285/285 lines, 38/38
branches.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Aug 1, 2026
…7) (#6982)

* refactor(contracts): complete the turn vocabulary in host_api and retire the turns shims (WS1.1)

`ironclaw_host_api::turn` becomes the complete canonical turn vocabulary:
it absorbs `TurnStatus` (with the inseparable `GateKind`/`BlockedReason`
gate correspondence), `EventCursor`, and `RunOriginAdapter`. The three
`ironclaw_turns` re-export shims named by CHECKLIST WS1.1 are deleted —
`src/ids.rs`, `src/scope.rs`, and the whole `src/product_adapter/`
module, whose `fakes.rs` moves beside the traits it implements in
`host_api::product_adapter::test_support`.

`ids.rs` carried `pub type GateRef = TurnGateRef`: a second name for a
host_api type that collided with the unrelated
`ironclaw_host_api::ids::GateRef` (an opaque uuid GateRecord key, versus
turns' bounded `gate:`-prefixed routing string). The alias is retired
rather than relocated, so the workspace now has exactly one `GateRef`.

The six vocabulary-only consumers — auth, event_streams, outbound,
telegram_extension, triggers, event_projections — import from
`ironclaw_host_api::turn` and drop their `ironclaw_turns` dependency
entirely. Five `*→turns` LAYER_MATRIX_EXCEPTIONS are therefore not
waived but obsolete: the edges no longer exist. The §11.2.2 ratchet
baseline moves 20 → 15.

No behavior change. `RunOriginAdapter`'s validation error becomes
`Result<_, String>` (matching every other bounded ref in
`host_api::turn`) with a byte-identical message pinned by a test, so
both production `e.to_string()` call sites are unchanged.

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

* docs(target-architecture): tick WS1.1 and close PLAN decision round #1

WS1.1's box is ticked with what the change actually landed, including the
three lead-sheet corrections it turned up: the row named `TurnStatus` but
not `EventCursor`/`RunOriginAdapter` (which the six consumers genuinely
needed), `GateKind`/`BlockedReason` could not be left behind without
duplicating the single `GateKind -> TurnStatus` match table, and deleting
`ids.rs` forced retiring its `GateRef` alias rather than relocating it.

Two decisions confirmed outside the doc and never recorded:

- Strategy B (family dirs + focused crates) — confirmed 2026-07-31 by the
  owner, recorded retroactively; it was made in practice at program start.
- The `tools/` row's `default-members` trim — resolved as no trim.

Also surfaces #6963 on the WS0 blocking-prerequisite row's first line
(it was already cited mid-paragraph) and records the §11.2.2 exception
ratchet moving 20 -> 15.

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

* refactor(contracts): repoint touched imports to host_api and pin the TurnGateRef contract

CodeRabbit review round on #6967.

Import repoints (accepted): every `use` line this PR already rewrote now
names `ironclaw_host_api::turn` directly instead of routing through
`ironclaw_turns`' prelude — 57 files across extension_host, product,
composition, runner, loop_host, conversations, the integration harness,
and the stress tool, plus three inside `ironclaw_turns` itself so the
crate stops consuming its own facade. Import lines this PR did not touch
are left for their consumer's own repoint slot.

TurnGateRef contract pinned (refutation): two review comments claimed
`TurnGateRef::new` only accepts `gate:approval-`/`gate:auth-` prefixes
and that fixtures like "gate-alpha" and "stress-gate:{run_id}" fail
construction. They do not — `TurnGateRef` is `bounded_ref!` (non-empty,
<= 256 bytes, no control characters); `LoopGateRef` is the prefix-
validated family via `loop_ref!(.., "gate:")`. The misreading traces to
this PR's own AGENTS.md wording ("bounded `gate:`-prefixed routing
string"), which stated a minting convention as if it were validation.
That wording is corrected and the distinction is now pinned by a test.

Also: drop a stale cross-file line reference in a product test comment.

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

* docs(target-architecture): remove the row-97 self-contradiction

The `tools/` row resolved the `default-members` trim as "no trim" but
kept a trailing "The `tools/`/`default-members` half is still open."
from before that decision, so the row asserted both states. Drop the
stale sentence.

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

* ci(coverage): complete the WS1.1 changed-coverage exemptions

Finishes the remediation deferred last round, now that the settled run
(job 91246493989) provides authoritative line numbers. Manifest-only —
no .rs file changes, so the changed-line set the gate computes is
unchanged and these numbers stay valid for the next run.

Derived, not transcribed: the gate was replayed locally against its own
merged lcov from that run, reproducing CI's failure byte-identically
first (95.17%, 138/145, same 13 files, same 7 lines), then re-run after
each entry. Final local result: 100.00% (138/138), branch 100% (4/4),
exit 0. All 45 gate self-tests pass.

Two classes, both verified rather than asserted:

- 13 files x 20 lines - declaration lines (fn params, return types,
  struct fields) whose only edit is the type NAME: GateRef ->
  TurnGateRef, or ironclaw_turns::X -> ironclaw_host_api::turn::X.
  Declarations are not executable, so these files contribute a zero
  denominator and trip the fail-closed empty_denominator branch.

- 7 lines x 3 files - executable, instrumented, and genuinely not
  exercised by the integration tier. Each checked against the base
  merged lcov (main @ 67088a4, the PR's own base sha): identical 0
  hits before and after, so no coverage was lost. approval_prompt_
  context_view is uncovered across its whole signature at base
  (lines 505-511); the background spawn-mode arm and the invalid-gate-
  ref error path likewise.

This includes the two entries I refused to guess last round -
turn_events.rs (three identical candidate lines by text; the settled
run disambiguates it as 510) and await_edge/store.rs (no verbatim twin
after the repoint; authoritatively 268-272).

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

* refactor(contracts): extract ironclaw_loop_contracts and flip agent_loop (WS1.2)

Carve the loop tier's neutral contracts out of the turn kernel into a new
contracts-layer crate per PROPOSAL 6.1.4, and repoint every consumer.

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

* refactor(contracts): pin the loop-contract boundary and register the new crate

Enforcement, CI registration, and guidance for the WS1.2 extraction.

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

* docs(reborn): split the loop-exit contract's ownership claim across the two crates

The claim types moved to ironclaw_loop_contracts with WS1.2; the validator
policy and the trusted applier stayed in the turn kernel.

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

* docs(contracts): repoint the three intra-doc links the crate split broke

The two HostManagedLoop*Port impls stayed in ironclaw_turns, so same-crate
links to them no longer resolve; the TurnRunId link target became redundant
when the import repoint fully qualified it.

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

* fix(tests): repoint the loop-exit evidence imports the port rule moved

Removing the ironclaw_runner re-export (required by the new port-location
scan) left two workspace-root test-support files importing the turn kernel's
evidence types through it. They now import from ironclaw_turns::loop_exit
directly, which is the single sanctioned path.

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

* fix(collapse): reconcile the lock pin and the moved failure-category scan

Two artifacts of collapsing onto main:

- Cargo.lock pinned thiserror 2.0.18 for the new ironclaw_loop_contracts
  entry while main's dependency bump moved the workspace to 2.0.19. The
  auto-merge kept the stale pin because the bump predates the crate, so
  --locked builds failed.
- ironclaw_product's failure-summary test reaches into another crate's
  source with include_str! and scans it for 'impl LoopFailureKind'. WS1.2
  moved that impl to ironclaw_loop_contracts, so the include still resolved
  and matched nothing. Repointed to follow the code.

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

* refactor(contracts): extract ironclaw_extension_contracts and close the dual import paths (WS1.3)

Carve the extension tier's neutral contracts out of the host API into a new
contracts-layer crate per PROPOSAL 6.1.2, repoint every consumer, and pin the
boundary with the 11.2.3 purity allowlist and the 11.2.4 location scan.

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

* fix(ci): repoint the exact-test selector WS1.2 moved

scripts/reborn-e2e-rust.sh pins exact test names for the deterministic
gate. The capability-failure rehydration test moved from
ironclaw_turns::run_profile::host::capability to
ironclaw_loop_contracts::host::capability, so its selector matched zero
tests and the gate failed closed.

Swept all 10 pinned selectors in that script (4 lib + 6 integration
target); this was the only stale one. Each now resolves to exactly one
test, verified by running the selector.

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

* fix(collapse): reconcile the new crate's lock pins with main's dep bumps

The extension_contracts lock entry was generated before the parent branch
collapsed against main, so it pinned thiserror 2.0.18 and toml 1.1.2 — versions
that no longer have [[package]] blocks. --locked lanes would have failed to
resolve.

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

* fix(arch): register the new contracts crate in the two gates that enumerate deps

The composition pub-use snapshot still carried product's PreferenceTargetCodec
re-export, and the CLI's exact-dependency allowlist did not know the extension
tier's contracts crate. Both are enumerating gates, so both failed loudly rather
than passing vacuously.

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

* docs(contracts): record why the extension tier's traits are not sealed

The visibility kit's sealed-strategy template exists to close a strategy set;
every trait here exists to be implemented outside the crate. State that, so the
absence reads as a decision rather than an omission.

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

* fix(coverage): drop the exemption WS1.2 made stale

The changed-coverage manifest carried a WS1.1 exemption for
crates/ironclaw_turns/src/run_profile/runtime_context.rs:575-576. WS1.2
moved that file into ironclaw_loop_contracts, so the gate's fail-closed
path validator rejected the manifest before reaching its line-level
verdict.

Deleted rather than repointed: WS1.1 merged, so those lines are baseline
on main, and this PR's diff pairs the file as a 99%-similarity rename
whose only changed lines are imports. Repointing would re-exempt lines the
gate no longer flags. All 19 remaining entries verified to resolve.

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

* fix(coverage): merge main and derive the WS1.2 changed-coverage exemptions

Merge brings in tests/e2e/scenarios/test_reborn_webui_v2_custom_mcp.py,
added on main after the last merge-down; the WebUI-smoke and E2E roll-up
reds were purely the missing file.

Exemptions derived by replaying scripts/ci/reborn_changed_coverage.py
against this PR's own merged lcov artifact until it exits 0 (100% line
244/244, 100% branch 8/8) - never estimated. Three classes:

- type-path repoints on declaration/expression fragments;
- verbatim-moved bodies in the new crate. Explicitly NOT counter-attribution:
  those files are instrumented in this lcov and partially hit (loop_exit
  195/109, model 86/52, checkpoint_payload 32/16), which proves the crate is
  measured. The same bodies were equally unexercised by the integration tier
  before the move, when they sat in ironclaw_turns and simply were not
  changed lines;
- one crate-root inner attribute the uninstrumentable-line classifier does
  not recognise on a declaration-only facade.

Also adds the #6524 declaration-only facade entry for the new crate's
lib.rs to the informational per-crate coverage summary.

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

* refactor(contracts): extract ironclaw_product_contracts and land the WS1.3 adapter half (WS1.4)

Carve the product tier's neutral contracts out of `ironclaw_host_api` into
`crates/ironclaw_product_contracts`, and — in the same change, because it is
what unblocks them — move `ChannelAdapter`/`ToolAdapter`/`RestrictedEgress`
into `ironclaw_extension_contracts` where PROPOSAL §6.1.2 assigns them.

`host_api::product_adapter` is one connected component: `channel_adapter`
names `inbound::ProductTriggerReason`, `inbound` names both
`channel_adapter::ChannelAttachmentRef` and `outbound::ProjectionCursor`,
`projection` names inbound and outbound, `interaction_commands` names inbound,
and `product_surface` names all of them. Since `ironclaw_host_api` may hold no
internal dependency, the adapters could only leave once nothing that stays
behind names them — so the product DTO modules moved with them.

`git mv` moved 15 modules at 83-100% similarity, so the diff reads as a move.

Regression coverage: `reborn_product_contract_location_scan.rs` (7 tests, 2
real gates + 5 self-tests with positive and negative fixtures) pins one home
and one import path for the product tier's ports; it fails on the four
re-export chains this change deletes.

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

* docs(checklist): record the hosted-MCP merge-down resolution on the WS1.4 row

The WS1.4 ruling on package_lifecycle survived the parent's #6930
reconciliation; the LifecyclePackageId split is the recorded resolution,
now stated as what happened rather than what was recommended.

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

* refactor(contracts): consolidate sealed evidence minting behind witness grants (WS1.5)

CHECKLIST WS1's evidence-mint row: one sealed seam owns construction of
protocol-auth evidence, every other construction path is deleted, and the
refute-tests land with it.

The `host-auth-mint` cargo feature was not a seal. Cargo unifies features
across the packages selected in one invocation, so `ironclaw_webui`'s opt-in
(-> turns -> host_api) compiled `ironclaw_host_api` once with the gate ON for
every other crate in the same build. Measured before touching anything: a probe
in `ironclaw_agent_loop` — whose manifest names `ironclaw_host_api` with no
features — minted a verified bearer claim; it failed to compile alone and
passed as soon as `ironclaw_webui` joined the `cargo test`. Every workspace
build is the second case.

Replaced with the repo's existing witness-token idiom (`host_api::authorized`),
which no other crate's manifest can switch on:

- `HostAuthenticationGrant` <- `HostProtocolAuthenticator`, sole implementor
  `ironclaw_webui` (module-private `AuthLayerState`, trust stage T1).
- `VerifiedInboundGrant` <- `ChannelIngressVerifier`, sole implementor
  `ironclaw_extension_host` (`VerifiedEvidenceMint`, trust stage T2).
- Channel/webhook mint family -> `ironclaw_extension_contracts::verified_inbound`
  (§6.1.2); bearer/session family stays in `host_api` (§6.1.1). The evidence
  type does not move; `extension_contracts` reaches the private verified variant
  through the grant-gated `ProtocolAuthEvidence::seal_verified_inbound`.

Closed: the feature in 5 manifests + 1 CI recipe; four re-export chains
(`host_api::product_adapter`, `ironclaw_product` root, `ironclaw_product::auth`,
and composition's zero-consumer re-export).

Enforcement: `reborn_sealed_evidence_mint_ratchet` (10 tests, §11.2.5) plus
`host_api/tests/protocol_auth_evidence_seal.rs` (5) and
`extension_contracts/tests/verified_inbound_seal.rs` (4). +19 tests, 0 removed,
no surviving assertion edited. The production-struct dead-code baseline shrinks
81/282 -> 80/277: the five `dead_code` suppressions in `auth.rs` existed only
because the constructors were feature-gated.

* refactor(common): narrow ironclaw_common to its §6.1.5 contract (WS1.6)

`ironclaw_common` now matches PROPOSAL §6.1.5's *Owns* list exactly, plus
three named exceptions that a pinned rule blocks from moving.

Deleted, not relocated — each re-verified with `git grep` over tracked files
on this base:

* `event.rs` (1,234 lines). All seven exported symbols have zero consumers
  outside the crate; the only live workspace references are negative ones
  (the architecture test asserting the OpenAI-compatible routes must *not*
  stream `AppEvent`, one doc comment, one Python docstring). This confirms
  WS1.4's disposition 7 rather than repeating it, and closes the WS8
  deletion-candidate row. The `AppEvent` `ForbiddenUse` entry stays as a
  reintroduction pin with its reason updated to say the enum is gone — the
  convention the file already applies to the retired v1 `ironclaw::` crate.
* `platform.rs`. `PlatformInfo` has zero references anywhere; §6.1.5's
  "→ its consumer" is unsatisfiable because there is no consumer.
* The four budget constants. Zero consumers workspace-wide, and
  `ironclaw_resources` already governs the same concern with a live,
  differently-shaped mechanism (`ResourceLimits::max_wall_clock_ms`) that
  references none of them — so "→ `resources`" would have seeded four
  unreachable constants beside a working governor.

Moved: `automation` → `ironclaw_triggers`, which owns automations and had
already defined `MAX_TRIGGER_NAME_BYTES` as an alias of the common constant.
This eviction is in §6.1.5 but missing from the CHECKLIST row.
`ironclaw_product`'s cross-crate `pub use` of the newtype (a second import
path with zero external consumers) is deleted rather than re-sourced,
following the WS1.3/WS1.4 dual-path rule.

Already done, so recorded rather than redone: `trust_boundary` went with
#6943, and the `AttachmentRef` collision is already resolved as
`ChannelAttachmentRef`. That rename left a wrong cross-reference in its own
doc comment (`ironclaw_common::ChannelAttachmentRef` for
`ironclaw_common::AttachmentRef`), fixed here.

Not moved, with evidence: `provider_transcript` (its `agent_loop` consumer
is contracts-only by pinned rule; `ironclaw_llm` is substrates),
`model_selection` (`openai_compat`'s boundary rule forbids `ironclaw_llm`
outright, and `llm` never uses the module), and `llm_costs` (`llm` uses 2 of
its 7 public items; moving it would hand `ironclaw_product` a
reqwest/rig-core cone for a pricing table — the `ModelCostTable` port is the
real seam, and that is a WS4 design change). All three are documented in
`crates/ironclaw_common/AGENTS.md`, which also stops claiming ownership of
the long-deleted `trust_boundary`.

Un-masking: `ironclaw_common` 123 → 110 tests (10 `event::tests::*`, each
classified to a deleted symbol; 3 `automation::tests::*` reappearing
verbatim in `ironclaw_triggers`, 166 → 169). Zero unclassified, no surviving
test edited. The extension-specificity gate demanded its now-stale
`platform.rs` carve-out be deleted — the property it was built with.

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

* refactor(host-api): move failure-summary data out of the runner (WS1.7)

PROPOSAL §6.1.1 assigns the category→summary tables to `host_api::failure`
"so product stops depending on runner". They are now
`ironclaw_host_api::failure::{categories, summary}`, and
`ironclaw_product`'s manifest no longer names `ironclaw_runner` under
`[dependencies]` — the dev-dep stays and is documented, because product's
harnesses legitimately build a full turn stack.

The row calls these "the two single-symbol product edges". Measured on this
base, neither is single-symbol:

* `product → runner` was two modules — a category constant plus four
  `failure_summary` items. **Severed.** What could not follow, because
  `ironclaw_host_api` may hold no internal dependency: the envelope *writer*
  (typed on `agent_loop`'s `CheckpointKind` and `loop_contracts`'
  `LoopSafeSummary`) and every classifier. Both runner modules are now
  private.
* `product → loop_host` has **three** production sites, and only the prompt
  constant was one. `FAILURE_EXPLANATION_SYSTEM_PROMPT` and its asset are
  now product-owned (prompt *content* is out of charter for the loop tier,
  §6.1.4/§6.7.2), but `project_create_capability.rs` (the #6691 arrival
  §2.3 already flags) and — not previously recorded anywhere —
  `scoped_fs/attachment_landing.rs`, which consumes `LoopAttachmentReadPort`,
  both carry real behavior. The edge survives; severing it is WS5/WS6 work.

One disposition worth review: the envelope reader moved and now revalidates
its cause with `SafeSummary::new` rather than `LoopSafeSummary::new`. That
is behavior-preserving, and the claim is pinned rather than asserted —
`validate_loop_safe_summary` delegates to `SafeSummary::new` with exactly
one bypass, the fixed `INPUT_ENCODE_HUMAN_SUMMARY` literal, which
independently satisfies the canonical rule
(`loop_input_encode_sentinel_needs_no_bypass_here`). Splitting writer from
reader also split the checkpoint-stage vocabulary across two crates, so the
pre-existing round-trip (which covered only `BeforeModel`) gains a sibling
driving all four `CheckpointKind`s writer→reader across the boundary.

Neither edge was ever a `LAYER_MATRIX_EXCEPTION` — `products → kernel` and
`products → loops` are matrix-legal — so this cannot move the count, which
stays at 13. The value is graph narrowing and prompt-content placement.

Enumerating gates, all shrink-only: the composition pub-use snapshot loses
exactly one line, because the `reborn_failure_summary_for_category`
re-export is deleted rather than re-sourced (its sole consumer, the CLI,
already depends on `ironclaw_host_api`, so the facade hop bought nothing).
The product-side category-coverage `include_str!` is repointed, not added,
so the §11.2.7 inventory is unchanged at 19.

Also hardens `reborn_loop_port_location_scan`'s directory walk, which
excluded only `target` and so descended the whole npm tree on any checkout
with the frontend installed — the same defect already fixed in the sibling
scanners.

Un-masking: 10 table tests moved runner → host_api with identical names and
no content edits (runner 467 → 458, host_api 248 → 260; deltas are the 10
moved plus 1 new round-trip and 2 new pins). loop_host and product rosters
are byte-identical at 572 and 1032.

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

* docs(checklist): tick WS1.6, record WS1.7's partial sever, close Wave 1

Ticks the `ironclaw_common` narrowing row with the seven dispositions that
differ from how it was written (five of six clauses moved differently:
two deletions where the row said relocate, one unsatisfiable "→ its
consumer", two clauses already done, one eviction present in §6.1.5 but
missing from the row), and the three LLM-data evictions each refuted by a
pinned rule.

Leaves the WS1.7 row **open** rather than ticking it. Two of three clauses
landed — the data move and the `product → runner` sever — but the
`product → loop_host` sever did not and cannot here: the edge has three
production sites, two of them behavioral, one of which
(`attachment_landing.rs`'s `LoopAttachmentReadPort`) was not recorded
anywhere before. Ticking it would over-claim.

Records the Wave 1 exit state on the WS1 verify row. The milestone's
"20 → 12" is not met and **the 12 was wrong**: the true end-state is 13,
because the 13th survivor, `conversations → turns`, is turn *admission
authority* rather than vocabulary, so no contracts crate can dissolve it and
it falls in WS5. The wave's other clauses did land — three contracts crates,
`agent_loop` contracts-only with zero exceptions, evidence mint sealed. The
mint row's measurement is carried forward as the reusable finding: the
`host-auth-mint` cargo feature was never a seal, because feature unification
compiled the gate on for every crate in any workspace-wide build.

Adds the dated one-line Wave 1 summary to PLAN's Wave 1 section, including
the discipline every slot in this wave independently confirmed: re-measure
rows against your own base, never inherit a predecessor's count.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BenKurrek added a commit that referenced this pull request Aug 1, 2026
…the gate crate

Three slices of the #6963 class inside crates/ironclaw_architecture.

1. reborn_registration_pipeline_boundary (#6963 comment, arrived with #6930).
   workspace_root() walked up a fixed two levels, so under a family move the
   'root' resolved to crates/, the scan targeted crates/crates, and the gate
   passed having visited ZERO files. Its two hardcoded hosted_mcp_ prefixes
   also stopped matching, which would have false-positived against the
   registration pipeline's own files with baseline 0 blocking the fix. Now
   inventory-driven, with measured_scan() asserting inventory size, scanned
   file count, and that every owned scope resolves to at least one real file.
   Its self-test now exercises is_owned(), flat and nested.

2. reborn_sealed_evidence_mint_ratchet. HostProtocolAuthenticator and
   ChannelIngressVerifier are unsealed traits whose mint methods are provided,
   so a bare  anywhere confers the power to mint
   ProtocolAuthEvidence::Verified. The source census IS the enforcement, and it
   evaded on a multiline impl header, on  aliases (plain, braced,
   and raw-identifier), and across a re-export split over two files. Headers
   are now extracted and whitespace-collapsed, in-file aliases resolved,
   matching is identifier-bounded, a re-export guard removes the cross-file
   shape, and a headers-parsed floor keeps the new normalizer from degrading
   silently. Closes the #6995 fail-open; seam origin PR #6981.

3. The shared root idiom. 23 of 24 gate files resolved the workspace root by
   walking up a fixed number of levels. ratchet_support::workspace_root() now
   searches for the nearest ancestor holding both crates/ and Cargo.toml, and
   11 private copies were deleted in its favour. Two gates that went silently
   green under nesting (reborn_authorized_seal_ratchet — worst under a PARTIAL
   move, 1309 -> 45 files scanned with no error; reborn_retired_taxonomy —
   1492 -> 0) gained measurement assertions. Two vacuous
   assert!(!path.exists()) absence checks in telegram_extension_gates now
   require their containing directory to exist.

Five stale entries removed, each matching zero files today and therefore
behavior-free: crates/ironclaw_gateway/ and extension_host/
extension_installation_store.rs from two SANCTIONED_PATHS allowlists (both now
carry stale-entry detection), crates/ironclaw_reborn_api/src and two duplicate
crates/ironclaw_product/src entries from the dependency-boundary roots, and the
deleted repo-root src/ monolith from the manifest reparse scan.

Regression tests: +8 in the family sweep, +7 in the registration boundary, +4
in the sealed-evidence census; every added assertion sabotage-tested red then
green. 26 binaries / 146 passed / 0 failed; clippy -D warnings clean.

Refs #6963, #6995

This branch was successfully deployed

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

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: ci CI/CD workflows scope: dependencies Dependency updates scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants