Repository navigation
refactor(reborn): delete host_runtime pre-auth + enforce policy on resume in the kernel (§5.3.2/§9) - #6392
Conversation
… host_runtime pre-authorization (§5.3.2/§9, R-A)
Extends the capability kernel to be the single authority site for resume as it
already is for invoke, then deletes host_runtime's now-truly-redundant
pre-authorization. Concretely:
(a) Kernel resume authority. authorize_resumed and the resume_spawn_json fold now
run runtime-policy planning on resume via a new resume_preflight that resolves
the descriptor and enforces plan_capability BEFORE the run-state lookup —
reversing #6386's "planning is NOT re-run on resume" so a policy tightened
between invoke and resume fails closed. Descriptor/trust existence is checked
first, so an unknown resume capability short-circuits to UnknownCapability
(-> MissingRuntime) instead of the run-state-not-found Backend path; failure
fails ONLY the matching blocked run (scope isolation), keyed by
scope+invocation+status+capability[+approval]. This restores host_runtime's
old production precedence (its deleted open_pre_authorization resolved the
registry before the kernel's mismatch check ran). The run-state fail records
the planner-specific INTERNAL error_kind (planner_error_kind, e.g.
"process_backend_none") while the model-visible message stays the sanitized
DenyReason::PolicyDenied — the planner enum name never reaches the model. The
kernel still stamps context.trust on the resume authorize_context clone before
the authorizer.
(b) host_runtime deletion. open_pre_authorization + its invoke/spawn/resume/
auth-resume/resume-spawn callers, the fail_matching_blocked_{,auth_}resume
helpers, and the now-dead evaluate_invocation_trust / enforce_runtime_policy /
TrustEvaluationError / RuntimePolicyEvaluationError / failure + trust-input
helpers are removed; the local-manifest trust-input unit test moves to
ironclaw_capabilities::trust. Kept: trust_policy/runtime_policy fields (wired to
the kernel), plan_capability for surface.rs visibility, the context.validate()
forged-scope guard, the credential/gate helpers, and the HostPolicyFacts impl.
(c) Descriptor/start reorder. authorize() and authorize_spawn() resolve the
descriptor BEFORE run_state.start, so an unknown capability creates no run
record (restoring the no-record-for-unknown behavior the deleted pre-check gave).
(d) Message sanitization tests. Invoke-path runtime-policy-denial tests
(builtin_http + tool_surface extension/secret/mcp) now assert the sanitized
DenyReason message ("denied", not the leaked NetworkMode::/SecretMode:: planner
enum tokens) — the #6386 sanitization, now observable once the redundant
host_runtime pre-check is gone.
(e) Mismatch tests corrected. The three capability-id-mismatch resume tests now
trigger the mismatch with a known-but-different, plannable capability
(echo.other) so the run-state mismatch check fires (ResumeContextMismatch),
isolating "mismatch" from "unknown-capability"; a new kernel test pins
"unknown capability on resume -> UnknownCapability + fail matching run".
Verified context.trust has no reader except the kernel-stamped authorize_context
(ironclaw_authorization::authorize_from_grants_with_trust).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
🔎 IronLoop Review StatusHead: Current reviewers:
Reviewer summaries
Recent activity
Available commands
Run metadataAdmission: webhook accepted the request and IronLoop persisted reviewer state before this projection. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
✅ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ✅ Approved | 0 | 0 | 0 | e7dbca8bb2a3 |
Head: e7dbca8bb2a303c928e16e688c750d2cd03d44ca
Next: No reviewer action needed.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Approved stack-layer review of 8 files (438 additions, 656 deletions). The kernel now performs resume policy preflight and the removed host-runtime preauthorization paths are consistently delegated.
Findings
None.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
|
🚅 Deployed to the ironclaw-pr-6392 environment in ironclaw-ci-preview
|
9cc6fde
into
feat/authorize-inline-policy
…r' review discipline (#6399) Adds a section to review-discipline.md drawn from the authorize() policy consolidation (PRs #6386/#6392): deleting a layer believed redundant exposes behaviors it was silently backstopping. Codifies the discipline that surfaced five masked behaviors there — run the full unfiltered suite (--no-fail-fast, no head/tail), treat each surfaced failure as a real behavior to preserve (never weaken the assertion to go green), the load-bearing observable is the failure kind/durable state not the message, 'redundant' is per-path, and sliced subagents stop-and-report rather than commit green. Documentation-only. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(§5.3.2/§9 security milestone) (#6386) * refactor(reborn): relocate planner to runtime_policy + add HostPolicyFacts port (§5.3.2/§9 groundwork) Move-only relocation of plan_capability/ExecutionPlan/PlannerError from ironclaw_host_runtime to ironclaw_runtime_policy (its charter home — 'picks backend kinds'), so the capability kernel can run runtime-policy enforcement inside authorize() without an upward dependency. Repoints host_runtime's four consumers; behavior unchanged. Adds the inverted HostPolicyFacts port to ironclaw_capabilities (facts-only: credential presence + active persistent grants), implemented up-layer in a later step. Zero new dependency edge on the kernel — all types are host_api or kernel-local. Groundwork for inlining the four/five policy checks into authorize() (the §5.3.2 security milestone that makes the Authorized seal non-vacuous). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(capabilities): route CapabilityHost construction through a support builder Extract the ~121 inline CapabilityHost::new(®,&disp,&auth) call sites across the 10 capability-host test files into one support::capability_host helper (forwards verbatim for now). This collapses the blast radius of the upcoming §5.3.2 construction-signature change (trust-policy/runtime-policy/policy-facts inputs) from ~121 sites to a single helper. Also lands the (unwired) trust.rs classifier relocated from host_runtime, consumed by authorize() in the next step. Pure refactor; all tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): compute trust + run runtime-policy planning inside authorize() (§5.3.2/§9) The capability kernel now computes provider trust and runs runtime-policy planning in-fold instead of receiving a pre-stamped `trust_decision` request field. `CapabilityHost` gains an injected `&dyn TrustPolicy` and `&EffectiveRuntimePolicy`; `authorize()` (and the spawn/resume folds) call the relocated `evaluate_invocation_trust` and `plan_capability`, stamping `context.trust` from the kernel-computed decision exactly as host_runtime's `open_pre_authorization` did before the authorizer runs. `trust_decision` is removed from all four request DTOs (`CapabilityInvocationRequest`, `CapabilityResumeRequest`, `CapabilityAuthResumeRequest`, `CapabilitySpawnRequest`) and from `ResumedDispatchParams`. Trust is now recomputed locally in each entry path (invoke/spawn/resume/auth-resume/resume-spawn); runtime-policy planning is enforced on the invoke and spawn paths (not re-run on dispatch resume, matching today's behavior). host_runtime still computes both redundantly: `open_pre_authorization` / `evaluate_invocation_trust` / `enforce_runtime_policy` remain (they feed `apply_persistent_approval_policy` and still stamp `context.trust`); the redundancy is intentional and behavior-preserving, cleaned up in later slices. Failure kinds are preserved: trust "unknown capability" -> `UnknownCapability` -> `MissingRuntime`; every other trust failure and any planner refusal -> `AuthorizationDenied` -> `Authorization`. Known delta: the model-visible *message* for these rare failures degrades to the generic `DenyReason` text (`AuthorizationDenied` carries no free-text). Test seam: `support::capability_host` supplies permissive `&'static` trust and runtime policies; a new `capability_host_with_trust_policy` + `FixedTrustPolicy` lets the four trust-ceiling tests inject the restricted ceiling they previously stamped on the request. Also repoints a pre-existing stale import in `runtime_policy_planner_contract.rs` (`plan_capability`/`PlannerError` moved to `ironclaw_runtime_policy` in the planner relocation but the test wasn't updated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): credential pre-flight moves into authorize() via HostPolicyFacts (§5.3.2/§9) The capability kernel's authorize() fold now reads HostPolicyFacts::credential_presence before the approval decision and maps a missing credential to CapabilityInvocationError::AuthorizationRequiresAuth. The host mapping to auth_required_outcome is unchanged (same fields, same stable gate id via stable_auth_gate_id / translate_invocation_error), so the AuthRequired outcome is byte-identical — it is now produced by the kernel instead of a host-runtime preamble. A transient secret-store fault maps to CredentialPresence::Indeterminate (fail-open, skip pre-flight) so it never burns a user auth interaction. DefaultHostRuntime now impls HostPolicyFacts (credential_presence reusing capability_credential_requirements + secret_owner_scope; persistent_grants mirroring apply_persistent_approval_policy's scope×grantee lookup). persistent_grants is implemented but not yet consumed by the kernel (the approval relocation slice wires the re-authorize loop). host_runtime's two credential_preflight_check call sites (invoke + spawn) and the now-dead private fn are removed; its coverage is preserved through the host_runtime_credential_preflight_contract integration suite (driven through the DefaultHostRuntime caller) plus a new kernel regression test (capability_host_missing_credential_blocks_before_approval_decision) proving credential-before-approval ordering. The context.validate() guard stays in the host_runtime preamble so forged-scope requests still fail with InvalidRequest. CapabilityHost::new gains a 6th policy_facts arg; test builders/doubles updated. Credential-before-approval ordering preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): persistent-approval decision moves into authorize() (§5.2.7/§5.3.2) The kernel now runs the persistent-approval re-authorize loop inside its authorize()/authorize_spawn folds (and the auth-resume authorize_resumed fold), reading candidate grants via HostPolicyFacts::persistent_grants and adopting the first grant that flips the trust-aware decision to Allow before the main authorization — so a prior scoped approval authorizes dispatch without raising a fresh approval gate. HostPolicyFacts::persistent_grants evolved to take &ExecutionContext instead of &ResourceScope so the impl derives the full grantee fan-out (via the existing persistent_approval_grantees + persistent_approval_lookup_scopes helpers), recovering the Principal::Extension grantee that a bare ResourceScope cannot carry — without this the extension-principal persistent approvals would silently stop auto-applying. The scope-only persistent_approval_grantees_from_scope helper is deleted. host_runtime's apply_persistent_approval_policy and its three call sites (invoke, spawn, auth-resume) are removed; the permission-mode gating predicate is relocated into the kernel (pure over PermissionMode, no host_runtime/approvals dependency). open_pre_authorization is kept for its planning gate + trust stamp side effects; its now-unused TrustDecision is discarded via if-let-Err on the invoke/spawn/auth-resume paths. Observable dispatch-vs-gate outcome preserved, incl. the extension grantee (covered by host_runtime_persistent_approvals_contract's dispatch/spawn/ auth-resume authority tests). New kernel regression: capability_host_persistent_approval_contract asserts a flipping grant dispatches without a gate and a non-flipping grant still raises the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): seal the real reservation + fact-derived deadline on Authorized (§5.3.2) Authorized.reservation becomes Option carrying the real obligation-produced reservation (the estimate is already reserved in-fold by the ReserveResources obligation; NO new governor path — that would double-reserve); the synthesized- fake reservation placeholder in seal_authorization is removed, so the witness carries Some only when a resource obligation ran and None otherwise. The witness deadline is derived from the shortest-lived frozen fact — the adopted persistent-grant expiry (invoke/spawn) or the claimed approval lease's expiry (resume) — via a witness_deadline helper, falling back to a bounded WITNESS_DEFAULT_TTL (5 minutes) instead of the fixed 5-minute window when no frozen fact is present. apply_persistent_approval now returns the adopted grant's expires_at so callers can thread it in. Witness still does not drive dispatch (dispatch uses obligation_outcome.resource_reservation directly through the dispatcher guard); this only makes the seal truthful. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(capabilities): bound resume witness deadline by lease expiry; log folded-away causes Thread PendingClaim's approval-lease expiry through the pending-claim spec so the sealed authorization witness minted in `authorize_resumed` is bounded by the approval that authorized it, instead of falling back to the 5-minute default TTL. Previously a `resume_json` (PendingClaim) resume dropped the lease expiry (it retained only the grant id), so a shorter-lived approval could be outlived by its witness. The claim is deferred past the seal, so the expiry must travel on the spec rather than being read back from a not-yet-claimed lease. Regression test drives `authorize_resumed` directly (the resume seal is discarded before `resume_json` returns, so no public caller surfaces it). Also preserve underlying causes in three authorize-fold error mappers that discarded them: `runtime_policy_error_to_invocation_error` now `debug!`s the bound `PlannerError`, and trust.rs `debug!`s the bound `TrustError` and manifest `ExtensionError` before collapsing to `Policy`/`TrustInput`. Uses `debug!` (not `info!`/`warn!`) to avoid REPL/TUI corruption; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): move resume-path authority into the kernel + delete host_runtime pre-authorization (§5.3.2/§9, R-A) (#6392) Extends the capability kernel to be the single authority site for resume as it already is for invoke, then deletes host_runtime's now-truly-redundant pre-authorization. Concretely: (a) Kernel resume authority. authorize_resumed and the resume_spawn_json fold now run runtime-policy planning on resume via a new resume_preflight that resolves the descriptor and enforces plan_capability BEFORE the run-state lookup — reversing #6386's "planning is NOT re-run on resume" so a policy tightened between invoke and resume fails closed. Descriptor/trust existence is checked first, so an unknown resume capability short-circuits to UnknownCapability (-> MissingRuntime) instead of the run-state-not-found Backend path; failure fails ONLY the matching blocked run (scope isolation), keyed by scope+invocation+status+capability[+approval]. This restores host_runtime's old production precedence (its deleted open_pre_authorization resolved the registry before the kernel's mismatch check ran). The run-state fail records the planner-specific INTERNAL error_kind (planner_error_kind, e.g. "process_backend_none") while the model-visible message stays the sanitized DenyReason::PolicyDenied — the planner enum name never reaches the model. The kernel still stamps context.trust on the resume authorize_context clone before the authorizer. (b) host_runtime deletion. open_pre_authorization + its invoke/spawn/resume/ auth-resume/resume-spawn callers, the fail_matching_blocked_{,auth_}resume helpers, and the now-dead evaluate_invocation_trust / enforce_runtime_policy / TrustEvaluationError / RuntimePolicyEvaluationError / failure + trust-input helpers are removed; the local-manifest trust-input unit test moves to ironclaw_capabilities::trust. Kept: trust_policy/runtime_policy fields (wired to the kernel), plan_capability for surface.rs visibility, the context.validate() forged-scope guard, the credential/gate helpers, and the HostPolicyFacts impl. (c) Descriptor/start reorder. authorize() and authorize_spawn() resolve the descriptor BEFORE run_state.start, so an unknown capability creates no run record (restoring the no-record-for-unknown behavior the deleted pre-check gave). (d) Message sanitization tests. Invoke-path runtime-policy-denial tests (builtin_http + tool_surface extension/secret/mcp) now assert the sanitized DenyReason message ("denied", not the leaked NetworkMode::/SecretMode:: planner enum tokens) — the #6386 sanitization, now observable once the redundant host_runtime pre-check is gone. (e) Mismatch tests corrected. The three capability-id-mismatch resume tests now trigger the mismatch with a known-but-different, plannable capability (echo.other) so the run-state mismatch check fires (ResumeContextMismatch), isolating "mismatch" from "unknown-capability"; a new kernel test pins "unknown capability on resume -> UnknownCapability + fail matching run". Verified context.trust has no reader except the kernel-stamped authorize_context (ironclaw_authorization::authorize_from_grants_with_trust). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(capabilities): explain policy denials in plain language, without leaking planner enum tokens The authorize() consolidation collapsed a runtime-policy planner refusal into a bare AuthorizationDenied { PolicyDenied }, which tells the model nothing about why it was denied. Add an optional `detail` to AuthorizationDenied that the planner mapper fills with a sanitized, plain-language reason (e.g. "this capability needs to run a process, but process execution is disabled by policy for this runtime"); sanitized_failure_message surfaces it. The full message with the internal ProcessBackendKind::/NetworkMode::/SecretMode:: enum tokens stays server-side in the debug! log only — the model never sees those tokens (matching the builtin_http_runtime_policy_denial_stops_before_egress invariant). facade_factory now asserts the readable reason (and that no planner enum token leaks); the two process-capability tests and the HTTP denial test all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…spatch-through-witness (#6396) (#6432) * refactor(reborn): thread real InvocationOrigin + Actor::System into the seal — witness always-present (§5.2.1/§9, S1) First slice of the "witness always-present + dispatch-routes-through-witness" arc (#6396). Retires the two fakes in `CapabilityHost::seal_authorization` so the sealed `Authorized` witness carries authoritative facts for every dispatchable invocation, WITHOUT changing any authorization decision: - Origin: deleted the `Product(ProductKind::new("provisional"))` placeholder. Added an ingress-stamped `ExecutionContext.origin: Option<InvocationOrigin>`; the seal resolves `context.origin` → else `LoopRun(run_id)` (transitional compat) → else fail-closed (Option `?`, never a placeholder). The loop host (`invocation_context_from_visible`) now stamps `LoopRun` explicitly. - Actor: an actor-less/host-internal context now seals `Actor::System` as its own class instead of early-returning `None`, so the witness is minted for it too. The witness is `None` now only for the `System` runtime-kind (no lane). Behavior-neutral: the witness is still a forward-looking artifact not consumed by dispatch (that lands in a later slice), so origin/actor become accurate recorded facts on the sealed `Invocation` without influencing allow/deny/gate. Verified there is no production `Product`/`Automation` capability ingress today (invocation is loop-initiated; the former `"provisional"` fallback only ever fired for test contexts), so those origin variants are wired for future ingresses with no live producer yet. Tests: extended the seal allow-path test to assert the real origin; added `authorize_seals_system_actor_and_real_origin_across_ingresses` (System actor + Product/LoopRun/Automation origins driven through `authorize`); extended the loop-host context test to assert the stamped `LoopRun` origin. No test weakened. Gates: cargo fmt; cargo test -p ironclaw_capabilities -p ironclaw_host_api -p ironclaw_host_runtime -p ironclaw_loop_host (1796 pass); clippy -p ironclaw_capabilities -p ironclaw_host_api --all-targets --all-features -D warnings; cargo test -p ironclaw_architecture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): add OriginGatePolicy matrix as declarative capability data (§5.2.1, S2) Second slice of #6396. Introduces the §5.2.1 origin→gate matrix as declarative data threaded from the manifest to the kernel descriptor, with NO authorization logic reading it yet (behavior-neutral): - host_api: `OriginGatePolicy` (Forbidden/AskAlways/GatedUnlessGranted/ ConsentSufficient/Ungated; wire-stable snake_case; default Forbidden) and `OriginGateMatrix` (per-origin loop_run/product/automation, each defaulting to Forbidden) with `policy_for(&InvocationOrigin)`. `CapabilityDescriptor` gains `origin_gate_matrix: Option<OriginGateMatrix>` — `None` = undeclared, treated as all-Forbidden (fail-closed) and targeted by the §5 ratchet (S5). - manifest: `CapabilityDeclV2` and `RawCapabilityV2` gain the same optional field (`#[serde(default)]`, so existing TOML without the key parses to `None`; a partial matrix defaults omitted origins to Forbidden). `from_raw` passes it through. Threaded into both descriptor-mint sites (`capability_descriptors_from_manifest`, hosted-MCP discovery). Absence = Forbidden (deny-by-default). No real matrices are populated here — that is the next slice; every fan-out literal (22 sites, mostly tests) is `None`. Tests: OriginGatePolicy snake_case round-trip; OriginGateMatrix per-origin default-Forbidden; policy_for variant mapping; v2 manifest parse (absent key → None, partial matrix → omitted origin Forbidden). No authz test touched. Gates: cargo fmt; cargo build --workspace; cargo test -p ironclaw_host_api -p ironclaw_extensions; clippy --all-targets --all-features -D warnings; cargo test -p ironclaw_architecture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): populate origin_gate_matrix for every capability + seed Ungated allowlist (§5.2.1, S3) Third slice of #6396. Declares the per-origin gate matrix on every production capability, behavior-preservingly (mirrors today's effect-based LoopRun gate). Still behavior-neutral: nothing reads the matrix yet (the fold is S4). - `loop_run` mirrors current gating under the canonical LocalDev/AskDestructive profile (the only live LoopRun producer). Since `ask_writes` ≡ `ask_destructive`, GATED-today ⇔ effects reach beyond {read_filesystem, dispatch_capability}; nothing is hard-floored, so no AskAlways. Result: 17 builtin caps `Ungated`, the other 160 (19 builtin + all 141 extension — every extension carries `network`) `GatedUnlessGranted`. - `product`/`automation` = `Forbidden` everywhere: deny-by-default, no live producer today; a later reviewed ingress slice fills them. Behavior-neutral now. - Checked-in `UNGATED_LOOP_RUN_CAPABILITIES` allowlist (host_api) pins the 17 ungated-for-model builtins (§5.2.1/§10 seed; the S5 ratchet enforces it). Builtin `loop_run` derives from this allowlist via `builtin_loop_run_seed` — fail-safe: an unlisted builtin defaults to GatedUnlessGranted. - Extension matrices are hand-authored into the 13 first-party TOML assets (the production descriptor source; gsuite Rust builder is test-only), so the ratchet does real work there. Discovered hosted-MCP tools inherit from their template. Load-bearing invariant (allowlist ⇔ builtin loop_run==Ungated) verified by construction + tests; every in-scope descriptor now declares Some(matrix); test fixtures stay None; SyntheticCapabilityDescriptor untouched. Gates: cargo fmt; cargo build --workspace; cargo test across host_api/extensions/ composition/host_runtime (one unrelated pre-existing env-sensitive failure, detect_env_llm_*, caused by LLM_BACKEND set in the shell); clippy --all-targets --all-features -D warnings clean; cargo test -p ironclaw_architecture 10/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): fold origin→gate matrix into authorize() as a two-tier gate (§5.2.1/§5.2.7, S4) Fourth slice of #6396. Authorization now CONSULTS the origin→gate matrix, keyed on the invocation's resolved InvocationOrigin (context.origin, else LoopRun from run_id — same resolution as seal_authorization) and the descriptor's origin_gate_matrix. Composed inside require_approval_for_profile_policy at the class-A intrinsic-gate layer, so per-scope class-B state (leases, auto-approve, always-allow) stays above it unchanged. Provably behavior-neutral in production. Two gate tiers, each mapped onto the existing gate machinery so semantics are correct — not just present: - `Forbidden` → hard Deny (sanitized DenyReason::PolicyDenied; internal reason in debug log), short-circuited ahead of every class-B step. - `AskAlways` → hard-floor gate composed at step 3 with effects_force_approval: beats class-B auto-approve/always-allow and is NOT suppressed by the Minimal (yolo) bypass — "every invocation gates; persistent grants never honored" (§5.2.7). Only a genuine one-shot approval lease satisfies it. - `GatedUnlessGranted` → soft gate OR'd at step 9: satisfied by the same scoped grant/always-allow machinery as the effect gate, and suppressed under the Minimal-bypass guard so yolo stays "no prompts". - `ConsentSufficient` / `Ungated` → no gate contribution. Behavior-neutral: no production capability declares loop_run == AskAlways or Forbidden (S3 seeds only Ungated / GatedUnlessGranted), so the hard-floor and deny paths are unreachable in production today; GatedUnlessGranted is neutral by the invariant matrix_gate ⟹ effect_gate under every non-Minimal profile, and is yolo-suppressed. The mechanism is fully wired and fail-closed for future Product/Automation ingresses. Tests (driven through the real RuntimeProfileApprovalGatePolicy + profile_approval_authorizer): neutrality across 5 caps × 4 profiles × 2 permission postures (LoopRun decision identical with vs without the matrix — zero flips); ask_always_matrix_gates_even_when_class_b_would_allow (the §5.2.7 proof: AskAlways gates despite global_auto_approve + always_allow); AskAlways gates under Minimal-yolo; GatedUnlessGranted suppressed under yolo; Forbidden→Deny; Ungated/ConsentSufficient add no gate. 27 prior profile-approval tests unchanged. Gates: cargo fmt; test -p ironclaw_reborn_composition -p ironclaw_capabilities -p ironclaw_host_api -p ironclaw_approvals (1678 pass; only the pre-existing env-sensitive detect_env_llm_* fails in this shell); clippy --all-targets --all-features -D warnings clean; cargo test -p ironclaw_architecture. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): ratchet the origin→gate matrix invariants + per-descriptor well-formedness (§5.2.1/§10, S5) Fifth slice of #6396. Locks in the S2–S4 origin→gate matrix invariants as guardrails; test-only, no production behavior change. New `crates/ironclaw_architecture/tests/reborn_origin_gate_matrix_ratchet.rs` (mirrors the sibling reborn_*_ratchet.rs pattern) enforces the invariants a leaf test crate can own soundly: - The `UNGATED_LOOP_RUN_CAPABILITIES` allowlist (imported from its owner, so the pin checks the REAL constant) is frozen to the reviewed 17-id seed — any addition/removal must update the checked-in EXPECTED_UNGATED_SEED in the same PR, so ungating a capability for the model is a review-visible diff. (This is the behavior-preserving grandfathered seed, deliberately not the doc's "starts empty" §10 ideal — documented in the file header; a future tightening removes entries with review.) - The 13 hand-authored first-party extension TOML assets each declare an origin_gate_matrix with a loop_run, never `consent_sufficient` on loop_run/automation (Product-only per §5.2.1), and no `ungated` loop_run outside the allowlist — the source where a typo could silently ungate a networked capability. "Every production descriptor declares Some(matrix)" (invariant 1) is enforced at the owning-crate build sites (ironclaw_architecture cannot depend on the composition/host_runtime crates that assemble descriptors): the builtin enumeration test in ironclaw_host_runtime (extended here to assert every builtin declares Some, loop_run matches the allowlist, product/automation Forbidden, no Product-only policy misused) plus the existing S3 extension-lifecycle and bundled-extension tests. The ratchet header cross-references all three. Proportionate §11.7 conformance: per-descriptor well-formedness/self-consistency checks added; a full golden-file origin→gate conformance harness (none exists today) is left as a follow-up. Dev-only deps on ironclaw_architecture (ironclaw_host_api, toml), documented as non-production edges. No STOP-item surfaced: no TOML uses ungated/consent_sufficient, no production descriptor is None. Gates: cargo fmt; cargo test -p ironclaw_architecture (10 pass, new ratchet 3/3); cargo test -p ironclaw_host_api -p ironclaw_extensions; the extended builtin enumeration test passes; clippy -p ironclaw_architecture --all-targets --all-features -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): dispatch consumes the Authorized witness — single-use, expiry-fail-closed, byte-neutral (§5.3.2/§9, S6) Sixth/final slice of #6396 (its "Slice 2"). `invoke_json`/`spawn_json` now take the sealed `Authorized` out of the authorize fold and route dispatch through it instead of re-deriving from loose request fields: - The witness is consumed single-use via `Authorized::into_parts(now)` (moves it — a second dispatch is a compile error). Its sealed mounts/reservation drive dispatch; the lane is consumed as proof (the dispatcher owns closed-lane routing and re-derives the identical descriptor lane). - Expired witness at dispatch fails closed: aborts the prepared obligations (releasing the reservation through the obligation lifecycle), consumes the witness via `Authorized::abort` (never `Drop`), fails the run, returns a terminal denial. Unreachable in a synchronous authorize→dispatch today; new-behavior-by-design for the async/held-witness future. Byte-neutral: the witness carries the fold's `obligation_outcome.{mounts, resource_reservation}` verbatim, so dispatch inputs are identical to the pre-S6 path for every capability that seals a witness. Findings from the security-critical review (issue warned these paths hide masked behaviors): - Mounts kept as `Option<MountView>` end-to-end (witness `mounts()` → `Option<&MountView>`; seal no longer collapses `None` to default): a downstream filesystem-plan consumer distinguishes `None` (fail-closed) from `Some(empty)`, so preserving the Option keeps dispatch byte-identical rather than behaviorally-equivalent. - The issue's "remove the dispatcher's reserve-when-None fallback" premise was stale: `ironclaw_dispatcher` has no such fallback (it only holds/releases a passed reservation); the real reserve-when-None lives in the runtime adapters and is load-bearing. Removed nothing. - `None`-witness path is NOT failed closed: `system.process_sandbox` (RuntimeKind::System → no lane → no witness) is legitimately spawned through `spawn_json`, so a `None` witness falls back to the obligation-derived dispatch inputs (exactly today's values). Failing closed would regress production. This leaves a documented dual path (witness for untrusted lanes, obligation fallback for host-internal System spawns); collapsing it fully requires representing host-internal spawns in the lane model — a follow-up beyond S6's neutrality mandate. Tests (ironclaw_capabilities + host_api): witness-none-mounts dispatched verbatim (not a collapsed default); spawn uses context-mounts fallback via the witness Option; expired witness fails closed and releases the reservation via abort (invoke + spawn); single-use is structural. Pre-existing byte-neutral dispatch tests remain green unchanged. Gates: cargo fmt; workspace clippy --all-targets --all-features -D warnings clean; targeted Reborn crate suites green (host_api/capabilities/dispatcher/host_runtime/ loop_host/extensions/reborn_composition/approvals/architecture). Two pre-existing facade_factory failures inherited from the #6392 base (its runtime-policy message sanitization vs stale test expectations) are unrelated to this stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): code-quality pass on the #6396 stack (dedup witness consumption; centralize origin resolution) Addresses findings from a strict code-quality review of the six-slice stack. No behavior change — pure structural cleanup; all touched-crate tests green (4164 passed / 0 failed), clippy -D warnings clean, fmt clean. - Dedup (S6): `invoke_json` and `spawn_json` carried an identical ~35-line witness-consumption block (single-use `into_parts`, expiry fail-closed + `abort`, None→obligation fallback) differing only in the obligation phase. Extracted `CapabilityHost::dispatch_inputs_from_witness`, consolidating the logic and its (security-sensitive) rationale in one place and shrinking the oversized host.rs. The helper carries the `abort_obligations` arg set plus the witness; invoke's and spawn's request types differ so no shared bundle unifies them (same justification as the adjacent `seal_authorization` exemption) — annotated arch-exempt, folds into a prepared-invocation bundle with plan #6175. - Centralize origin resolution: the `context.origin` else `LoopRun(run_id)` rule was duplicated in `seal_authorization` (capabilities) and `require_approval_for_profile_policy` (composition). Moved to `ExecutionContext::resolved_origin()` in host_api — the single definition of the "run_id implies LoopRun" rule, resolved through the type that owns the fields. Dropped the now-unused `InvocationOrigin` import from host.rs. Also fixes a rebase-induced test breakage: main replaced the `UnusedDispatcher` test double with `dispatch_test_support::TestDispatcher`; updated the S1 seal test accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): address origin gate review feedback --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What & why
#6386 moved the five policy decisions into the kernel's
authorize()fold, but left host_runtime'sopen_pre_authorizationrunning redundantly in front of the kernel (documented there as a transitional wart). This PR removes it — and in doing so uncovered that the redundancy was load-bearing on the resume paths: the kernel did not reproduce runtime-policy enforcement on resume/auth-resume. So this is not a pure deletion; it completes the kernel's authority over the resume paths and then deletes the now-truly-redundant host_runtime copy.Net −218 LOC (
production.rs−638; kernel +232 for the resume preflight).Changes
resume_preflightinironclaw_capabilitiesresolves the descriptor and runsplan_capability(runtime-policy) on the resume/auth-resume/resume-spawn folds — reversing refactor(reborn): consolidate all pre-flight policy into authorize() (§5.3.2/§9 security milestone) #6386's "no planning on resume" decision, which was only safe because host_runtime was still doing it. This is strictly more correct: resume now fail-closes if runtime policy tightened between the original invoke and the resume. Existence-first ordering, scope-isolated failure of the matching blocked run (BlockedResumeKind), and the internal run-stateerror_kind(e.g.process_backend_none) preserved.open_pre_authorization,evaluate_invocation_trust,enforce_runtime_policy,PreAuthorizationRejected, the trust/runtime-policy failure helpers, and the resume fail-matching helpers. Now genuinely redundant on every path. Kept:trust_policy/runtime_policyfields (wired to the kernel),plan_capabilityfor surface-visibility, thecontext.validate()forged-scope guard, and the credential/gate helpers used bytranslate_invocation_error+ theHostPolicyFactsimpl.run_state.startinauthorize()/authorize_spawn— an unknown capability now short-circuits before a run record is created, restoring the "no record for unknown capability" behavior the deleted host_runtime check used to provide.Behavior (production parity, verified)
Everything is behavior-neutral versus pre-#6386 production. The redundant host_runtime layer had been masking four behaviors that only became observable once it was removed — each was resolved to match production, none by weakening a test:
DenyReason::PolicyDeniedmessage instead of a leaked planner enum token (NetworkMode::Deny). This is refactor(reborn): consolidate all pre-flight policy into authorize() (§5.3.2/§9 security milestone) #6386's documented sanitization, now observable on the invoke path; the internal auditerror_kindstill records the planner-specific reason.ResumeContextMismatch) from "unknown" (UnknownCapability), and a new test pins the unknown-on-resume path.context.trust: verified its only reader is the trust-aware authorizer, which always receives the kernel-stampedauthorize_contextclone — so removing host_runtime's stamp is inert.Testing
Full unfiltered suites green:
ironclaw_host_runtime982/0,ironclaw_capabilities136/0;ironclaw_reborn_compositionbuilds;ironclaw_architecture, workspace clippy-D warnings, andcargo fmtclean. The three resume runtime-policy/trust contract tests pass unweakened; every modified test preserves its original intent (only triggers/sanitized-message assertions changed).🤖 Generated with Claude Code