From 431a43118e6ed6378cb1eb663015d4db29049b39 Mon Sep 17 00:00:00 2001 From: Jeff Larson Date: Thu, 6 Aug 2026 18:45:16 -0700 Subject: [PATCH] docs(adr): ADR-0041 narrow the blanket notable-exec corroboration arm to shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design pass (/idea) on unmasking the deferred reverse-shell corroboration shape. - ADR-0041 (Proposed): `Behavior::ProcessExec → false` in the flat corroboration arm (a bare interactive-shell/pkg-mgr exec becomes model-evidence-only, symmetric with PrivilegeChange/PtraceAttach/ModuleLoad, restoring the ADR-0011 on-call FP guard); exec-based corroboration re-homes in a new entry-scoped `reverse_shell_on_foothold` shape (interactive-shell exec + internet NetworkConnection within a symmetric 60s window, foothold-gated), clearing the ADR-0024 gate on the deferred shape. - Corrects the framing: the model never sees `corroborated` (removed by ADR-0034); narrowing buys deterministic auto-apply-gate precision + honest latent/live presentation + ADR-0024 unmasking, not model-input precision. - Safe now: Falco's Alert arm still backstops (falco.enabled: true), pre-arming in audit. One behavior change (inert under audit): shell-only + decisive-attack becomes a proposal not an auto-cut in enforce (ADR-0011-correct). A narrowing-delta counter feeds the retire-Falco parity bake (ADR-0037). - docs/ideas/corroboration-floor-narrowing.md: the settled brief. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP --- ...rrow-blanket-notable-exec-corroboration.md | 143 +++++++++++++++ docs/adr/README.md | 1 + docs/ideas/corroboration-floor-narrowing.md | 167 ++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 docs/adr/0041-narrow-blanket-notable-exec-corroboration.md create mode 100644 docs/ideas/corroboration-floor-narrowing.md diff --git a/docs/adr/0041-narrow-blanket-notable-exec-corroboration.md b/docs/adr/0041-narrow-blanket-notable-exec-corroboration.md new file mode 100644 index 0000000..b1ce2c2 --- /dev/null +++ b/docs/adr/0041-narrow-blanket-notable-exec-corroboration.md @@ -0,0 +1,143 @@ +# 0041. Narrow the blanket notable-exec corroboration arm to shapes + +- Status: Proposed +- Date: 2026-08-06 + +## Context + +`corroborates()` (`engine/src/engine/reason/proof/corroborate.rs`) carries a **blanket arm**: +any `ProcessExec` classified as `is_interactive_shell()` or `is_package_manager()` corroborates +**every** objective on an internet-facing entry. It was introduced as the Falco-parity floor — +the direct replacement for Falco's "terminal shell / package-management in container" criticals +— deliberately broad while Falco is being retired, and it is the one place where a *bare* +notable exec (as opposed to `PrivilegeChange` / `PtraceAttach` / `ModuleLoad`, which are +non-corroborating on their own) flips `corroborated_for`. + +Two problems follow. **(1)** Because the arm fires on the exec alone, no *exec-correlated* +corroboration shape can independently flip `corroborated_for`; the deferred reverse-shell +(exec↔egress-timing) shape is therefore **redundant-by-construction**, and +[ADR-0024](0024-no-redundant-by-construction-predicates.md) rightly blocks landing it until the +blanket arm is narrowed. **(2)** The arm's precision is exactly the +[ADR-0011](0011-positive-judgement.md) on-call-engineer false positive: an operator's +`kubectl exec -it … bash` flips every chain on that entry to corroborated/live. + +### The reframing this ADR resolves + +A tempting but **false** framing is that the blanket arm "floods the model's evidence with +'corroborated' on every objective." Verified against the code, it does not: + +- **The model never sees `corroborated`.** The incident prompt carries per-behavior evidence + lines (`observe::exec_class::annotated_summary`); the `corroborated` flag was *removed* from + the model's view by [ADR-0034](0034-cut-choice-contract.md)'s 4→3 verdict collapse and now + appears in `reason/adjudicate/incident/` only in comments explaining its exclusion. Narrowing + `corroborates()` changes nothing the model reads. +- **`corroborated` remains load-bearing in three *deterministic* places** post-[ADR-0032](0032-model-is-incident-responder.md)/[ADR-0034](0034-cut-choice-contract.md): + the auto-apply gate `(corroborated || promoted) && adjudicated && breach_relevant` + (`respond/mod.rs`); lane selection (`adj_pass.rs` — corroborated → veto/auto-eligible lane; + uncorroborated → needs an affirmative verdict + the opt-in arming class); and + presentation/telemetry (latent-vs-live, disposition strings, the + [ADR-0037](0037-shadow-bake-arm-readiness.md) bake read). + +So narrowing buys **deterministic-gate precision + honest latent/live presentation + the +unmasking of every exec-correlated shape** — not model-input precision. + +## Decision + +**A shell exec is *evidence*; a shell exec with a correlated C2-shaped egress is +*corroboration*.** Narrow the flat blanket arm and re-home exec-based corroboration in +entry-scoped shapes. + +1. **Flat arm → `false`.** `Behavior::ProcessExec` no longer corroborates on its own — it joins + `PrivilegeChange` / `PtraceAttach` / `ModuleLoad` as **model-evidence-only; shapes live at + the entry-scoped seam.** This restores [ADR-0011](0011-positive-judgement.md) as stated + (bare interactive shell = the on-call-engineer case, not corroboration) rather than as the + parity-driven suspension it had been. + +2. **New shape `reverse_shell_on_foothold`.** An `is_interactive_shell` `ProcessExec` and an + internet `NetworkConnection` on the same entry within a **symmetric 60s window** + (`|Δt| ≤ 60s` over `provenance.observed_at`, both directions — covers `bash -i >& /dev/tcp/…` + exec-then-connect and connect-back-then-spawn), **foothold-gated** (`entry.is_foothold`, the + pattern every sibling shape follows), corroborating **any objective** (a live C2 session + evidences intrusion on every chain from that entry — the blanket's semantic, now in shaped + form). Window constant sits beside the drop-then-execute constant. This lands the previously + deferred reverse-shell shape as load-bearing. + +3. **Package managers are excluded from the shape.** They always egress (fetching packages), so + including them re-creates a blanket for that class. Real installs corroborate via + `alarming_write` (writes under `/usr/bin`, `/lib`, …); a no-op package-manager exec stays + model evidence. + +4. **`observe::alarm_class::is_alarming_now`, `exec_class`, the incident-menu seeding, and the + zero-anchor guard are untouched.** The model keeps its "hands-on-keyboard happened" evidence + line, its actively-exploited marking, and its ability to name a shelled downstream pod in + `contain` ([ADR-0034](0034-cut-choice-contract.md) §4). A drift-guard test asserts a notable + exec still satisfies `is_alarming_now` after the flat arm narrows. + +5. **Foothold gate on the shape**, with the revisit trigger named: a no-CVE app-level RCE + reverse shell on an exposed-but-not-compromisable entry stops *deterministically* + corroborating (the model lane still covers it — [ADR-0034](0034-cut-choice-contract.md) + already requires a decisive model `attack` for any cut). Un-gating is a one-line, + shadow-measurable change if the Falco-parity matrix shows the gap biting. + +6. **A narrowing-delta counter** — a structured log + counter emitted when an entry has a + notable exec in `runtime` but its chain is uncorroborated (exactly the cases the old blanket + would have corroborated). This is the retire-Falco parity-matrix input and the bake + instrument; the model-vs-deterministic `cut_divergence` comparator is the read-only-bake + precedent ([ADR-0037](0037-shadow-bake-arm-readiness.md)). + +7. **Tests must flip `corroborated_for`** ([ADR-0024](0024-no-redundant-by-construction-predicates.md)'s + bar): shape-positive (shell + egress corroborates where a bare shell would not), bare-shell + negative, package-manager negative, window-expiry negative, non-foothold negative. + +8. **No config toggle, no wire change, no tier.** Repo convention: fix noise by scope, not a + flag; [ADR-0034](0034-cut-choice-contract.md) just *collapsed* corroboration vocabulary, so a + weak/strong tier would cut against the grain for a benefit presentation already serves. + +### Why now — and the safety floor + +Falco ingest is still live (`charts/protector/values.yaml`: `falco.enabled: true`), so Falco +criticals corroborate via the **`Behavior::Alert` arm regardless of the exec arm**. While Falco +coexists, the exec arm is a *redundant* second floor — so narrowing now, in `audit` (the default +posture, pre-arming), changes **zero operational recall**: the Alert arm still backstops. The +Falco-retirement decision then consumes the narrowing-delta counter over a continuous bake +([ADR-0037](0037-shadow-bake-arm-readiness.md)) against the parity matrix *before* the Alert +floor is ever removed. Narrowing after Falco retires would be strictly riskier; this is the +cheapest safe window. + +## Consequences + +- The deferred exec-correlated shapes become buildable ([ADR-0024](0024-no-redundant-by-construction-predicates.md) + gate cleared): `reverse_shell_on_foothold` lands here; future exec↔X shapes can follow. +- The deterministic auto-apply gate and the latent-vs-live presentation gain precision — a bare + admin shell no longer marks every chain on an entry corroborated/live during the arm-readiness + bake. +- **The one real behavioral change — a lane shift:** in `enforce` mode *without* the opt-in + arming class, a shell-only + decisive-`attack` incident becomes a **proposal instead of an + auto-cut**. Deliberate ([ADR-0011](0011-positive-judgement.md) — a bare shell with no + secondary signal is the on-call false positive), **inert under the `audit` default**, and + called out here so the arming-ladder operator reads it before flipping `enforce`. Amends + [ADR-0009](0009-asymmetric-action-bar.md)'s action-bar corroboration input; the + `corroborated ∧ adjudicated` auto-gate itself is unchanged. +- **Residual, bake-measured:** the shape's false-positive set is a strict *subset* of the + blanket's, not empty — an sh-entrypoint foothold pod egressing at startup, or an on-call shell + into a routinely-egressing foothold pod, still fires (the wire type carries no tty/argv, so + process-linked discrimination needs a wire change — deferred). `RuntimeEvents` eviction can + also drop the exec before the egress on a chatty pod (same profile as drop-then-execute). The + narrowing-delta counter measures both. +- Reversible: re-widening the flat arm is a one-line change, measured in shadow. + +## References + +- `docs/ideas/corroboration-floor-narrowing.md` — the design brief this ADR records. +- [ADR-0024](0024-no-redundant-by-construction-predicates.md) — the governing constraint + (shapes must be load-bearing when merged); this ADR clears its gate for the reverse-shell + shape. +- [ADR-0011](0011-positive-judgement.md) — the on-call-engineer false positive that keeps bare + exec non-corroborating; restored here. +- [ADR-0014](0014-behavioral-telemetry-ebpf.md) — the corroboration port; the Falco-parity bar + = measured decision-path coverage, which the narrowing-delta counter feeds. +- [ADR-0032](0032-model-is-incident-responder.md) / [ADR-0034](0034-cut-choice-contract.md) — + the model decides; `corroborated` feeds determinism (auto-apply gate + lane), not the model. +- [ADR-0037](0037-shadow-bake-arm-readiness.md) — the read-only shadow-bake instrument pattern + the narrowing-delta counter follows. +- [ADR-0009](0009-asymmetric-action-bar.md) — the action bar this amends. diff --git a/docs/adr/README.md b/docs/adr/README.md index 9696136..c393181 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -46,5 +46,6 @@ Copy [`0000-template.md`](0000-template.md) to start one. | [0037](0037-shadow-bake-arm-readiness.md) | Shadow-bake arm-readiness: the human-read exit criterion (bake duration, zero unexplained model-over-cut on a clean workload, 0033's bench threshold, a model-under-cut spot-check, continuous coverage) an operator reads from the model-vs-deterministic cut-divergence comparator before the single ADR-0021 `enforce` flip, narrowest ADR-0035 rung first — the comparator only informs; nothing here or in code auto-arms | Proposed | | [0040](0040-node-scoped-containment-mechanism-escalation.md) | Node-scoped containment is a **deterministic mechanism escalation** of a model-decided target: when typed evidence proves the adversary broke the *pod* boundary (host-cred read; root + escape edge; ptrace/module-load; or ≥2 model-confirmed actively-exploited pods co-resident), the resolver escalates the model-named workload's cut from a pod NetworkPolicy to `ContainNode` (cordon + co-resident default-deny) — the model keeps *what*, determinism owns *how* (rejects a model-selectable node menu line: highest blast on the weakest measured axis). Own `node` action class at ladder rung 3 (above quarantine), propose-first by construction (a node always has alive collateral), reversible + ownership-marked + one-node-cap + control-plane-excluded + worker-floor; posture-derived `nodes` RBAC; ships shadow-first (trigger/proposal, then actuator). Also records: adversary-reach is a **presentation annotation, never judge context** (context-not-evidence enforced by absence from the prompt), and off-path auto-containment is a **won't-build** (reaffirms 0032 §6, strengthened by 0038). Refines 0032/0034; reuses 0010/0017/0021/0035/0036 rails | Proposed | +| [0041](0041-narrow-blanket-notable-exec-corroboration.md) | Narrow the blanket notable-exec corroboration arm to shapes: a bare interactive-shell/pkg-mgr `ProcessExec` stops blanket-corroborating every objective (`Behavior::ProcessExec → false` in the flat arm — symmetric with PrivilegeChange/PtraceAttach/ModuleLoad, restoring the 0011 on-call-engineer FP guard), and exec-based corroboration re-homes in an entry-scoped `reverse_shell_on_foothold` shape (interactive-shell exec + internet NetworkConnection within a symmetric 60s window, foothold-gated) — unmasking the deferred reverse-shell shape per 0024. Corrects the framing: the model never sees `corroborated` (removed from the prompt by 0034); narrowing buys deterministic auto-apply-gate precision + honest latent/live presentation + 0024 unmasking, NOT model-input precision. Safe **now** because Falco's `Alert` arm still backstops (`falco.enabled: true`), pre-arming in `audit`. One real behavior change (inert under audit): a shell-only + decisive-attack incident becomes a proposal not an auto-cut in enforce (0011-correct). Package managers excluded (always egress); `is_alarming_now`/menu seeding untouched; a narrowing-delta counter feeds the retire-Falco parity bake (0037). Amends 0009's action-bar input | Proposed | See also [`../VISION.md`](../VISION.md) for the longer-form narrative this ADR realizes. diff --git a/docs/ideas/corroboration-floor-narrowing.md b/docs/ideas/corroboration-floor-narrowing.md new file mode 100644 index 0000000..3af8879 --- /dev/null +++ b/docs/ideas/corroboration-floor-narrowing.md @@ -0,0 +1,167 @@ +# Idea brief — Narrow the blanket notable-exec corroboration arm to shapes + +**Status:** settled → ticketed via `/plan-sprint`. Load-bearing decisions recorded in +[ADR-0041](../adr/0041-narrow-blanket-notable-exec-corroboration.md). + +## The idea (as posed) + +`corroborates()` in `engine/src/engine/reason/proof/corroborate.rs` has a **blanket arm**: +any `ProcessExec` classified as `is_interactive_shell()` or `is_package_manager()` +corroborates **every** objective on an internet-facing entry. That arm was the Falco-parity +floor (the direct replacement for Falco's "terminal shell / package-management in +container" criticals). Because it fires on the exec alone, no *exec-correlated* corroboration +shape can independently flip `corroborated_for` — so +[ADR-0024](../adr/0024-no-redundant-by-construction-predicates.md) (no +redundant-by-construction predicates) rightly blocks landing the deferred reverse-shell +(exec↔egress-timing) shape until the blanket arm is narrowed. This brief designs that +narrowing. + +## The reframing that redirects the idea + +The original worry — *"the blanket arm floods the judge's evidence with 'corroborated' on +every objective"* — is **factually wrong in this codebase**, and correcting it is the whole +point of the design pass: + +- **The model never sees `corroborated`.** The incident prompt carries per-behavior evidence + lines rendered by `observe::exec_class::annotated_summary` (e.g. *"executed /bin/bash + (interactive shell in container)"*). The `corroborated` flag appears in + `reason/adjudicate/incident/` only in comments explaining why it is *deliberately excluded* + — [ADR-0034](../adr/0034-cut-choice-contract.md)'s 4→3 verdict collapse removed the model's + restatement of that deterministic fact. Narrowing `corroborates()` changes **nothing the + model reads**. (Evidence lines also carry no inter-event timestamps, so the model cannot + self-derive the exec↔egress correlation — only determinism can.) +- **`corroborated` is still load-bearing post-[ADR-0032](../adr/0032-model-is-incident-responder.md)/[ADR-0034](../adr/0034-cut-choice-contract.md), in three *deterministic* places:** + 1. **The auto-apply gate** — a model-chosen cut auto-applies only through + `(corroborated || promoted) && adjudicated && breach_relevant` (`respond/mod.rs`). + `corroborated` is half of the deterministic AND-gate on actuation. + 2. **Lane selection** (`adj_pass.rs`) — a corroborated chain sits in the *veto* lane + (auto-eligible unless the model demurs); an uncorroborated chain needs an affirmative + verdict **plus the opt-in arming class** to become auto-eligible. + 3. **Presentation/telemetry** — latent-vs-live, the finding-disposition strings in + `state/findings.rs`, chain logs and metrics the operator reads during the + [ADR-0037](../adr/0037-shadow-bake-arm-readiness.md) arm-readiness bake. + +**So narrowing buys:** precision of the deterministic auto-apply gate, honest latent-vs-live +presentation during the bake, and the unmasking of every exec-correlated shape +([ADR-0024](../adr/0024-no-redundant-by-construction-predicates.md)). It does **not** buy +model-input precision — that was never at stake. + +## Assumptions corrected + +- **Falco is still live** (`charts/protector/values.yaml`: `falco.enabled: true`). Falco + criticals arrive as `Behavior::Alert` and corroborate via the *Alert* arm regardless of the + exec arm. While Falco coexists, the exec arm is a **redundant second floor** — so **now + (coexistence + `audit` default) is the cheapest, safest window to narrow**: zero + operational recall change, because the Alert arm still backstops. +- **Only exec-correlated shapes are masked.** The drop-then-execute shape fires on + *non-notable* execs of a recently-written path (independent flips exist); the + privilege-escalation shape keys on `PrivilegeChange`, never in the blanket arm. So "what + replaces the net" is *mostly already built* — seven entry-scoped shapes + the alarming-write + and Alert blankets + the per-objective arms. +- **Narrowing does not reopen the parity gap much.** Post-[ADR-0034](../adr/0034-cut-choice-contract.md) + nothing auto-cuts without a decisive model `attack`; a bare-shell compromise still reaches + the decision path as annotated model evidence, as `observe::alarm_class::is_alarming_now` + (which marks the pod actively-exploited and seeds the downstream `contain` menu line), and + via the Alert arm while Falco runs. The residual is one lane shift (below). + +## Recommended approach — narrow the flat arm to shapes + +- **`Behavior::ProcessExec → false` in the flat corroboration arm.** A bare shell/pkg-mgr exec + becomes **model evidence, not corroboration** — restoring exact symmetry with + `PrivilegeChange` / `PtraceAttach` / `ModuleLoad` (all of which already defer to + entry-scoped shapes) and restoring [ADR-0011](../adr/0011-positive-judgement.md)'s on-call- + engineer false-positive guard that the blanket had suspended. +- **Add one entry-scoped shape, `reverse_shell_on_foothold`** — an `is_interactive_shell` exec + and an internet `NetworkConnection` within a **symmetric 60s window** (covers both + exec-then-connect and connect-back-then-spawn), **foothold-gated** (`entry.is_foothold`, the + house pattern every sibling shape follows), corroborating **any objective** (a live C2 + session evidences intrusion on every chain from that entry — the blanket's semantic, now in + shaped form). This is the deferred reverse-shell shape, now load-bearing. +- **Package managers are excluded from the shape** — they *always* egress (fetching packages), + so including them re-creates a blanket for that class. Real installs corroborate via + `alarming_write` (writes under `/usr/bin`, `/lib`, …); a no-op pkg-mgr exec stays model + evidence. + +Approaches rejected: **(A) don't narrow, add timing as model-evidence enrichment** — leaves +the deterministic gate low-precision and makes the [ADR-0024](../adr/0024-no-redundant-by-construction-predicates.md) +friction permanent (every future exec-correlated shape stays unbuildable); **(C) tier +corroboration weak/strong** — adds a concept to `ProvenChain`/journal/ledger against the grain +of [ADR-0034](../adr/0034-cut-choice-contract.md)'s vocabulary collapse, for a weak tier whose +only consumer is presentation the evidence pane already serves. + +## Key decisions (see [ADR-0041](../adr/0041-narrow-blanket-notable-exec-corroboration.md)) + +1. Flat arm → `false`; `ProcessExec` joins the "model-evidence-only, shapes at the entry-scoped + seam" family. +2. `reverse_shell_on_foothold`: interactive-shell exec + internet `NetworkConnection`, + symmetric `|Δt| ≤ 60s` from `provenance.observed_at`, foothold-gated, corroborates any + objective. Constant beside the drop-then-execute window. +3. Interactive shells only — package managers excluded. +4. **`observe::alarm_class::is_alarming_now` / `exec_class` / menu seeding / the zero-anchor + guard are untouched** — the model keeps its "hands-on-keyboard happened" signal and its + ability to name a shelled downstream pod in `contain`. A drift-guard test asserts a notable + exec still satisfies `is_alarming_now` after the flat arm narrows. +5. Foothold gate on the shape, with the revisit trigger named: a no-CVE app-level RCE reverse + shell on an exposed-but-not-compromisable entry stops *deterministically* corroborating + (the model lane still covers it). Un-gating is a one-line, shadow-measurable change if the + parity matrix shows it biting. +6. A **narrowing-delta counter** — emitted when an entry has a notable exec in `runtime` but + its chain is uncorroborated (exactly the cases the old blanket would have corroborated). + This is the retire-Falco parity-matrix input and the bake instrument (the `cut_divergence` + comparator is the read-only-bake-instrument precedent, [ADR-0037](../adr/0037-shadow-bake-arm-readiness.md)). +7. Tests must **flip `corroborated_for`** ([ADR-0024](../adr/0024-no-redundant-by-construction-predicates.md)'s + own bar): shape-positive, bare-shell negative, pkg-mgr negative, window-expiry negative, + non-foothold negative. +8. No config toggle, no wire change, no tier (repo convention: fix noise by scope, not a flag). + +## The one real behavioral change — the lane shift + +In `enforce` mode **without** the opt-in arming class, a shell-only + decisive-`attack` +incident becomes a **proposal instead of an auto-cut**. This is deliberate — a bare shell with +zero secondary signal *is* the [ADR-0011](../adr/0011-positive-judgement.md) on-call-engineer +scenario, and auto-severing on it is the false positive that ADR exists to prevent. It is +**inert under today's `audit` default**, but the arming-ladder operator must read it, so the +ADR states it explicitly. + +## What we are explicitly NOT doing + +- **Not retiring Falco / the Alert arm** — that is gated on the bake this work enables, not + part of it. The Alert arm stays the live floor through coexistence. +- **Not touching `is_alarming_now`** — narrowing it would strip the downstream `contain` menu + lines and the actively-exploited marking. Leave it. +- **Not adding wire enrichment** (tty/argv/ppid, pid-linked exec↔flow attribution) — the only + path to a materially better shape discriminator, but its own agent-side design; deferred. +- **No weak/strong tier, no config toggle, no un-gating the foothold requirement** now. + +## Residuals & risks (bake-measured) + +- The shape's false-positive set is a strict *subset* of the blanket's, not empty — an + sh-entrypoint foothold pod that egresses at startup, or an on-call shell into a routinely- + egressing foothold pod, still fires (the wire type carries no tty/argv, so process-linked + discrimination needs a wire change — deferred). Strict improvement, imperfect; the + narrowing-delta counter measures it. +- `RuntimeEvents` eviction (`MAX_EVENTS`/TTL) can drop the exec before the egress lands on a + chatty pod, missing the correlation — the same accepted risk profile as drop-then-execute. +- One genuinely open item to settle during the build: verify the assessment→`promotes()` + mapping so the uncorroborated + decisive-attack lane behaves as assumed (read/add one test). + +## Rough shape & sequence + +1. **ADR-0041** (this brief's decisions; amends the Falco-parity-floor stance; clears the + ADR-0024 gate for the shape; names the lane shift + the retire-Falco measurement bar). +2. **One PR** (must land together per [ADR-0024](../adr/0024-no-redundant-by-construction-predicates.md)): + flat-arm narrowing + `reverse_shell_on_foothold` + flip-capable tests + doc updates in + `corroborate.rs`/`exec_class.rs`. +3. **Small PR:** the narrowing-delta counter (+ optionally a bake read). +4. **Not in scope:** the bake window → the Falco-parity matrix → the Falco-retirement decision. + +## HANDOFF TO PLAN-SPRINT + +Theme: **"Shape the corroboration floor — narrow the blanket notable-exec arm to the +reverse-shell shape while Falco still backstops it."** Plan against one ADR + one load-bearing +engine PR (flat-arm narrowing + `reverse_shell_on_foothold` + flip tests, landing together per +ADR-0024) + one observability PR (the narrowing-delta counter feeding the retire-Falco parity +matrix). Everything is engine-internal (`engine/src/engine/reason/proof/`, `observe/`), ships +in `audit` mode with the Falco `Alert` arm as the unchanged safety floor. Panel: **architect + +devops** — no product-surface change beyond finding-disposition labels; no data-schema change +beyond one counter.