Skip to content

feat(autonomy): guardrail matrix, security binding, setup slice (WP5) - #377

Merged
kyle-sexton merged 45 commits into
mainfrom
feat/autonomy-guardrails
Jul 20, 2026
Merged

feat(autonomy): guardrail matrix, security binding, setup slice (WP5)#377
kyle-sexton merged 45 commits into
mainfrom
feat/autonomy-guardrails

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

WP5 guardrail-enforcement package (T2 + T3 + the security-review policy resolution), per the merged PLAN. Phases 1–3 complete; Phase 4's positive probe runs in this PR's CI (transcript below after the run).

No linked issue

Related

What ships

  • Guardrail hub (reference/guardrails.md): five-class × five-column matrix (T3 verbatim), glance-layer routing to four leaves, D8 permission-posture note, escalation event classes + org-bound routing + work-item/trace payload over the governed queue (no second channel).
  • Leaves (reference/guardrails/): isolation-ladder.md (L0–L3, attendance × input-provenance axes, rejected trigger-source axis with falsification reason, L2 unattended floor, fail-closed no-silent-degrade), work-classes.md (risk bundles; C1 repo-read-only with queue writes permitted; C5→L3 floor; promotion/demotion discipline with shipped default predicates — C2 auto-merge ≥20 completions/≥14 days/100% gates/0 reverts, C3 ai-review ≥30 advisory/0 missed-blocking; demotion one-event-suffices), security-review.md (two layers, per-class blocking knobs as floors — no override escape on verification), admission-policy.md (3-axis decision table with "*" wildcard + most-specific-wins, equal-specificity tie = invalid binding, shipped defaults C1/C2 autonomous within caps regardless of provenance, C3 per-item human, C4/C5 human-gated; caps autonomous_concurrency: 1, items_per_run: 3).
  • Security-binding schema (skills/setup/schemas/guardrails-security-binding.schema.json): draft 2020-12, additionalProperties: false, fail-closed semantics in-schema; executor_class, dispatch_posture, per-surface isolation_bindings (level → substrate + probe_evidence + runtime_markers), merge/verification knobs, promotion_state, escalation_routes, admission (classification + rules + caps).
  • Semantic validator (skills/setup/scripts/check-security-binding.mjs, exit 0/1/2): matrix merge caps, vendor-hosted human-gate cap, marker non-emptiness + pairwise joint-satisfiability ambiguity rejection, class-aware per-surface isolation verdicts (C5 requires L3), missing-L2 error under autonomous-enabled vs declared verdict under human-gated-only, non-promotable promotion entries rejected, admission weakening requires override_justification (admission-only escape), promotion-gated knobs require their ratified promotion_state entry, equal-specificity ties invalid, evaluation mode resolving EFFECTIVE promotion state against contrary evidence without writing the binding. 91 binding fixtures plus 67 transcript and 2 evidence files, each negative producing exactly one finding. Note: egress-probe targets must be multi-label DNS names or public literal IPs — encoded loopback/private forms are normalized and rejected.
  • Setup guardrail slice (SKILL.md): two-surface resolution (security binding in settings-as-code home behind an agent-unwritable org-policy-home bootstrap; repo-local non-security remaps), substrate detection per surface, probe-before-bind live validation, detect-diff-reconcile against existing org guardrail surfaces, security review folded in (one slice, no near-duplicate capability), paid SKUs advisory + cost surfaced, fail-closed no-L2. Plus templates/isolation-probe.md and five new evals; plugin 0.5.0.

Live-validation demonstration (Phase 4)

Positive probe — this PR carried a one-off workflow (wp5-isolation-probe-demo.yml, since removed per the PLAN; evidence persists in the run log below) that PROVISIONED a genuine egress-denied boundary on the hosted runner (nested docker run --network none container) and executed the probe recipe INSIDE it, with a networked-outer-job control step. Transcript from the PR run:

From run 29675620833:

outer-egress: HTTP 200 (networked, as expected)
egress-denial: PASS - external fetch failed inside the boundary (exit 4)
credential-absence: PASS - every tested credential path absent inside the boundary (exit 3)
captured live transcript:
  "schema_version": "1",
  "probed_at": "2026-07-19T05:56:39Z",
  "assertions": {
    "egress_denied": { "host": "example.com", "exit_code": "4", "outcome": "denied" },
    "credentials_absent": { "path": "$HOME/.ssh,$HOME/.netrc,$HOME/.docker/config.json", "exit_code": "3", "outcome": "absent-or-denied" }
  },
  "outer_context_networked": true
Security binding OK: live-evidence/demo-binding.json
- surface "ci-pool-a": C1 eligible, C2 eligible, C3 eligible, C4 eligible, C5 blocked (requires L3, proven L2)

The final line is check-security-binding.mjs exit 0 against a binding whose probe_evidence is the transcript captured seconds earlier in the same job — the checker passes only against live-captured, identity-matched evidence. (The recorded transcript predates the capture-contract fields that later review hardening added to the checker — per-target credential exit codes, host-side expansions, transport outcomes, and the outer-context reachability/existence codes on both assertions; it is preserved verbatim as the run's historical record.)

Negative pathcheck-security-binding.mjs on the missing-L2 autonomous-enabled fixture (exit 1):

- isolation_bindings: no bound surface reaches L2 under dispatch_posture autonomous-enabled — autonomous dispatch is blocked fail-closed; compliant paths: bind an L2-capable substrate (whole-process OS-sandbox wrap, default-deny-egress container) on an execution surface, or declare dispatch_posture human-gated-only

Declared posture — the same missing-L2 shape under human-gated-only validates (exit 0) with the blocked-dispatch verdict reported as the DECLARED posture, not a defect.

Demotion evaluation — promoted-C2 binding + contrary evidence (exit 0, binding file unchanged):

- C2-auto-merge: bound promoted -> effective unpromoted — ceiling lowered by contrary evidence (reverted-merge at 2026-07-10T12:00:00Z) WITHOUT modifying the binding; demotion files an escalation item on route "queue:escalations/demotion" requesting the human-ratified binding update

Near-duplicate audit

Security review is folded into the ONE guardrail slice of guided-setup (no separate security-review capability); escalation composes the governed queue itself via the trigger contract's acknowledgment symmetry — no second claim, dispatch, or escalation channel was created anywhere in this package.

Gates

validate-plugins.sh, validate-plugin-contracts.mjs (vendor deny-list clean over reference/), catalog --check in sync, markdownlint 0 errors, typos clean, lychee offline 0 errors, claude plugin validate --strict pass, skill-quality check-skill.sh setup PASS (2 pre-existing WARNs), evals.json schema-valid (ajv draft2020), actionlint clean on the demo workflow. Full plugin test suite rides CI. Validator exercised over the full fixture set with expected exit codes and single-finding assertions.

Boris check

Step-3 guardrails cell covered (agent sandboxing = isolation ladder; automatic code + security review = verification layers; token/model management = cost tiers); the step-4 sentence ("enforcing the right guardrails for each type of work") is instantiated as the matrix; the step-3 trap honored (human-ratified promotion, automatic evaluation-time demotion); step-1 guardrail untouched. Auto-mode classifier tuning + allowlist distribution explicitly deferred with named triggers.

PLAN.md (as merged, tags advanced)

ai-ladder-wp5-guardrails

Brief

TLDR

Guardrail-enforcement package (T2 + T3 + #241 instance): one guardrail contract shaped as a
progressive-disclosure hub (matrix doc as index/spine) with on-demand leaf docs, an org-binding
seam split by governance sensitivity, evidence-gated promotion with automatic demotion, and the
guardrail slice of guided-setup (detect → bind → live-validate → fail-closed).

Goal

Any adopting org can bind the five-class guardrail matrix to its own instances and get
per-work-class enforcement — isolation floor, verification layers, merge policy, cost tier,
escalation — with the security-sensitive bindings outside agent blast radius, promotion earned
on queryable evidence, and no silent degrade anywhere.

Locked decisions

# Decision
D1 ONE guardrail contract, hub-and-leaves progressive disclosure: matrix doc = index/spine (table + one-line class/column definitions, glance layer resolves the common question); leaf docs load on demand — e.g. isolation-ladder levels, per-class detail + promotion triggers, security-review policy, admission policy (examples, not a fixed list; split at architect). Plus the guardrail slice of guided-setup. Both in the capability-distribution home. Fleet materializations = /work-items backlog.
D2 Org-binding seam: contract-owned schema, one logical binding, two governance surfaces split by sensitivity. Security-sensitive axes (isolation substrates per level, merge policy, per-layer blocking knobs, promotion-trigger state, escalation routes, admission policy) bind in the settings-as-code home — outside the blast radius of agents working in the consuming repo (agent-writable guardrail binding = bypass channel). Non-security mappings (class→label strings, cost-tier→model names) may bind repo-locally per the work-item-tracker binding pattern. Layered resolution: org-policy-home defaults → settings-as-code per-repo binding → repo-local non-security remaps; contract defines resolution order, materialization at architect. Fail-closed on absent/invalid security-sensitive binding; documented defaults only for non-security axes.
D3 Promotion/demotion discipline: contract defines the trigger SHAPE — an evidence predicate over queryable telemetry (verification outcomes per the telemetry contract are the evidence base). Promotion = human-ratified knob flip, recorded as a reviewable change on the governance surface — never automatic. Demotion = automatic fail-closed on contrary evidence (gate failure, human-reverted merge), re-earn from there. Org binds threshold values; suggested defaults at architect. Near-term promotable cells: C2 auto-merge, C3 AI-review advisory→blocking (#241); C4/C5 merge never promotes.
D4 Sandbox-ladder setup slice: detect available substrates per level per machine surface → bind level→substrate in the security binding → live-validate BEFORE recording (empirical probe, e.g. denied-egress smoke test inside the boundary; a binding lands only after the sandbox provably blocks) → fail-closed verify (no L2 substrate → autonomous dispatch blocked for that surface, compliant paths named — no silent degrade).
D5 #241 three-part resolution folds into the matrix structure, nothing ships separate: security-review policy contract (two layers, per-layer blocking knob) = verification-column leaf; security-review setup = part of the one guardrail slice of guided-setup (near-duplicate capability ban); the adjustment/audit layer generalizes matrix-wide — guided-setup always detect-diff-reconcile against existing org guardrail surfaces (sandbox configs, branch protections, review workflows), never greenfield-assume, never silently overwrite.
D6 Escalation column sharpened from DIRECTIONAL: contract defines escalation event classes (gate failure, verification divergence, admission rejection, demotion event, structural-class plan approval, untrusted-provenance always), routing obligation (org-bound routes in the security binding), and payload (work-item ref + trace link, one causal tree). Mechanism reuses the governed queue: escalation lands as a human-gated work item + optional channel notification via the trigger-contract acknowledgment symmetry — no second channel. Only interactive escalation UX stays deferred; trigger: WP7 runner design pack.
D7 Admission-policy content (the trigger contract's admission seam enforces it; this package owns it): decision-table shape — signal-surface class × initiator provenance × work class → disposition (autonomous-eligible / human-gated / audited rejection) — plus caps (autonomous concurrency, per-run items). Shipped defaults: C1/C2 autonomous-eligible within caps regardless of provenance (blocking agent provenance would sever the agent-kicks-off-agent loop), C3 per-item human admission, C4/C5 human-gated. Provenance is recorded input to gating, never trusted as isolation; caps bound total autonomous fan-out.
D8 Permission posture: attended ergonomics (auto-mode classifier tuning, safe-command allowlist distribution) is NOT a matrix column — unattended runs replace per-action prompts with the whole-process boundary; the ladder leaf carries a one-line permission-posture note (L1 = attended ergonomics tier, L2+ = the boundary is the control). Classifier tuning + allowlist distribution = backlog seeds with trigger, not silent drops.

Constraints

  • Any fleet repo or vendor name in normative contract text is a defect; vendor names appear only
    as marked examples and in binding docs.
  • Security-sensitive bindings never live where the agents they govern can edit them.
  • Fail-closed everywhere: unavailable substrate, absent/invalid security binding, contrary
    promotion evidence — block and name the compliant path, never degrade silently.
  • No new cost by default: free-path scanners and substrates default; entitlement-gated tools
    (e.g. paid code-scanning SKUs) are advisory + explicit opt-in with cost surfaced.
  • One queue, one escalation channel (the queue itself); no second claim, dispatch, or
    escalation mechanism.
  • Boris-alignment is the standing acceptance criterion (no step-skipping, trust before scale).

Acceptance criteria

  • Matrix doc's glance layer alone answers "what governs class X" — leaf docs only for depth
    (progressive-disclosure convention conformance).
  • Binding schema names the two governance surfaces + layered resolution order; security axes
    fail closed when unbound.
  • Promotion is human-ratified + evidence-gated; demotion is automatic; both leave a reviewable
    audit trail on the governance surface.
  • Guided-setup slice detect-diff-reconciles existing guardrail surfaces, live-validates
    isolation before binding, and has zero paid dependencies on its default path.
  • Admission decision-table defaults match the matrix class defaults verbatim; agent provenance
    is not blocked by default; caps are org-bindable.
  • Boris check: step-3 guardrails cell covered (agent sandboxing = ladder; automatic code +
    security review = verification layers; token/model management = cost tiers); step-4 sentence
    ("enforcing the right guardrails for each type of work") instantiated as the matrix; step-3
    trap honored (human-ratified promotion, automatic demotion); step-1 guardrail untouched.
  • Auto-mode classifier tuning + allowlist distribution explicitly out-of-scope with named
    triggers, not dropped.

Captured assumptions

  • Verification-outcome telemetry (WP2 contract) is queryable at promotion-evaluation time; until
    wired, promotion evaluation is manual over the same evidence definition.
  • Free-path deterministic scanners remain available on the default path; paid code-scanning
    SKUs stay entitlement-gated (re-verified: private-repo CodeQL requires a paid license).
  • The work-item-tracker binding precedent (contract-owned schema, org-supplied values,
    stop-and-report on invalid binding) remains the fleet's binding idiom.

Out-of-scope (deferred with triggers)

  • Interactive escalation UX — trigger: WP7 runner design pack.
  • Auto-mode classifier tuning + safe-command allowlist distribution — trigger: 1→2 residue
    sweep / standards allowlist-distribution work (backlog seeds).
  • Cost ENFORCEMENT (hard spend caps) — trigger: 3→4 transition work; cost tiers here are
    policy vocabulary only.
  • Fleet materializations (binding instances, workflow gates, scanner wiring) — /work-items
    backlog post-graduation.

Deferred questions

  • Exact class/level/column/attribute tokens + binding schema fields — /architect (with the
    plugin naming pass).
  • Leaf-doc file split + glance-layer wording — /architect.
  • Promotion-trigger default values (run counts, windows) + demotion evidence set — /architect.
  • Setup-slice probe mechanics per substrate class — /architect.
  • Admission decision-table serialization format — /architect.

Plan

Recommendation-locked this round under the user's standing pre-authorization (same basis as
the WP2/WP3 round): hub doc reference/guardrails.md with leaf docs under
reference/guardrails/ (isolation-ladder.md, work-classes.md, security-review.md,
admission-policy.md — the D1 example split, adopted); class tokens C1C5 and level
tokens L0L3 carried verbatim from T3/T2; security binding = a contract-owned JSON
document in the settings-as-code home (schema_version from "1.0"); non-security remaps =
an ADDITIVE guardrails section of the WP1 repo-local binding; admission decision table
serialized as JSON inside the security binding.

Prerequisite: the WP4 implementation PR merged (reference/trigger-dispatch.md exists — the
admission leaf cites its seam; the interim is safe via WP4's fail-closed absent-binding
clause).

Phase 1: Guardrail hub + leaf docs [DONE]

File Action What changes
plugins/autonomy/reference/guardrails.md Create The hub/spine (D1): the five-class × five-column matrix table imported from T3 unchanged (C1 read-only / C2 mechanical / C3 scoped / C4 structural / C5 untrusted-provenance × min-isolation / verification / merge policy / cost tier / escalation), one-line class and column definitions, and the glance-layer rule — this table alone answers "what governs class X"; every deeper question routes to a named leaf. Permission-posture note per D8 (one line: L1 = attended ergonomics tier; L2+ = the boundary is the control). Escalation event classes per D6 (gate failure, verification divergence, admission rejection, demotion, structural-class plan approval, untrusted-provenance always) with the routing obligation (org-bound routes in the security binding) and payload (work-item ref + trace link); mechanism = the governed queue itself (human-gated item + optional channel notification via the trigger contract's acknowledgment symmetry — no second channel). Boris step-4 sentence cited as the matrix's source framing. Zero vendor/fleet names.
plugins/autonomy/reference/guardrails/isolation-ladder.md Create T2 ladder normative text: L0–L3 level definitions, attendance × input-provenance axes, the falsified trigger-source axis recorded as rejected (with reason), L2 unattended floor, fail-closed rule where L2 unavailable (block + name compliant paths, no silent degrade), permission-posture line per D8. Instances org-supplied; free-path substrate CLASSES named as marked examples only.
plugins/autonomy/reference/guardrails/work-classes.md Create Per-class detail: risk-property bundle per class (blast radius, reversibility, provenance, verifiability) — stating explicitly that C1 "read-only" scopes REPO surfaces and permits governed-queue/tracker writes (work-item filing, no repo mutation; the WP6 stress-test's F8 disambiguation) and that C5's min-isolation cell is the L3 floor WP7's C5-dispatch gate cites, per-cell promotion triggers (D3 shape: evidence predicate over queryable telemetry per the telemetry contract; promotion = human-ratified knob flip recorded as a reviewable change on the governance surface; demotion = automatic fail-closed on contrary evidence, re-earn from there). Suggested default predicates (org-bindable values): C2 auto-merge — ≥ 20 autonomous C2 completions over ≥ 14 days with 100% deterministic-gate pass and 0 human-reverted merges; C3 AI-review advisory→blocking — ≥ 30 advisory reviews with 0 human-confirmed missed-blocking findings. C4/C5 merge never promotes. Demotion evidence set: any post-merge gate failure, any human-reverted merge, any verification divergence — one event suffices.
plugins/autonomy/reference/guardrails/security-review.md Create The #241 security-review policy contract as the verification-column leaf: two layers (deterministic scanners + AI security review), per-layer blocking knob (advisory/blocking per class, bound on the governance surface), free-path scanners default, paid SKUs advisory + explicit opt-in with cost surfaced.
plugins/autonomy/reference/guardrails/admission-policy.md Create The D7 admission-policy content the WP4 seam enforces: decision-table shape (signal-surface class × initiator provenance × work class → disposition autonomous-eligible / human-gated / audited-rejection) + caps (autonomous_concurrency, items_per_run). Shipped defaults verbatim from D7: C1/C2 autonomous-eligible within caps regardless of provenance; C3 per-item human admission; C4/C5 human-gated; provenance recorded, never trusted as isolation. Default cap values (org-bindable): autonomous_concurrency: 1, items_per_run: 3 — conservative Boris trust-before-scale floor. Serialization: the admission object of the security binding (Phase 2 schema).

Sanity Check:

  • Hub glance test: grep -c '| C[1-5] |' plugins/autonomy/reference/guardrails.md = 5 (matrix rows present) and hub contains zero level-definition prose (grep -c 'microVM' plugins/autonomy/reference/guardrails.md = 0 — depth lives in leaves)
  • grep -c 'audited-rejection' plugins/autonomy/reference/guardrails/admission-policy.md ≥ 1
  • grep -ci 'fail-closed' plugins/autonomy/reference/guardrails/isolation-ladder.md ≥ 1
  • Promotion defaults present: grep -c '20 autonomous C2 completions' plugins/autonomy/reference/guardrails/work-classes.md ≥ 1 and grep -c '14 days' plugins/autonomy/reference/guardrails/work-classes.md ≥ 1
  • Vendor+fleet deny-list sweep exit 0; lychee lane passes

Phase 2: Binding schema (two governance surfaces) [DONE]

File Action What changes
plugins/autonomy/skills/setup/schemas/guardrails-security-binding.schema.json Create Contract-owned JSON Schema for the security binding document that lives in the settings-as-code home (outside agent blast radius per D2): schema_version (const "1.0"), executor_class (self-operated | vendor-hosted — the merge-policy gating input, security-surface data per the trigger contract), dispatch_posture (autonomous-enabled | human-gated-only, default autonomous-enabled — an org may intentionally record its security settings while keeping every dispatch human-gated; the PR-review finding that a missing L2 entry must not conflate that deliberate posture with an invalid binding), isolation_bindings keyed by EXECUTION SURFACE (org-named surface id — e.g. the CI pool, a self-run host class; the PR-review finding that a flat level→substrate map lets one surface's L2 binding satisfy the check while a different dispatch surface has no boundary at all) → level token → substrate instance id + probe evidence ref + runtime_markers (the per-surface TRUSTED identifying markers the trigger contract's dispatch seam matches platform-attested runner context against — a key/value set drawn from metadata the executing workload cannot forge, e.g. attested runner-pool/label identity; serialized HERE, on the agent-unwritable security surface, so surface attestation never falls back to the repo-local recorded id), merge_policy (per class, capped per matrix), verification_blocking (per layer per class), promotion_state (per PROMOTABLE cell only: state + ratifying change ref + evidence window), escalation_routes (event class → org route), admission (classification rules — the signal→work-class mappings the trigger contract's adapters stamp from, e.g. label→class for tracker-vcs-event — plus decision-table rules with a "*" wildcard per axis and most-specific-wins precedence (full triple > two axes > one axis > default), optional per-rule override_justification, and caps autonomous_concurrency / items_per_run). additionalProperties: false; fail-closed semantics documented in-schema (absent/invalid security binding blocks autonomous dispatch).
plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Create Validates a security-binding document against the schema + semantic rules the schema cannot express: merge-policy caps never exceed the matrix (C4/C5 human always; executor_class: vendor-hosted caps every class at human-gated); every surface bound under dispatch_posture: autonomous-enabled must declare non-empty runtime_markers (else the dispatch seam could never attest the actual surface and every dispatch would fail closed as unattestable), and marker sets must be PAIRWISE UNAMBIGUOUS under joint satisfiability — two conjunctive predicates are compatible (and the binding REJECTED) unless they require CONFLICTING values for at least one shared key, since a runtime context carrying the union of two non-conflicting predicates matches both (subset/identity checks alone miss e.g. {pool: blue, region: us} vs {pool: blue, os: linux}); the dispatch seam still requires EXACTLY ONE matching surface at runtime, failing closed on zero or multiple matches — the validator-level joint-satisfiability rejection is what keeps that runtime rule from turning validly bound fleets into ambiguity outages; isolation verdicts are PER SURFACE and CLASS-AWARE: each bound surface resolves, per work class, eligible (its bound level meets that class's min-isolation matrix cell — L2 is the floor for any autonomous dispatch, C5 requires L3) or blocked — admission/executor resolution must name the surface a dispatch would run on AND the item's work class, and verifies the surface meets the class's minimum at dispatch time, so an L2-only surface is never selected for a C5 item (even after human admission or a later policy change) and an unbound surface is blocked even when a sibling surface is bound; under dispatch_posture: autonomous-enabled it is an ERROR when NO bound surface reaches L2 (fail-closed, compliant paths named) — under human-gated-only the binding validates and the verdict reports blocked autonomous dispatch as the declared posture, not a defect; a promotion_state entry for a non-promotable cell (C4/C5 merge) is rejected; admission defaults not weaker than shipped defaults without the rule's override_justification (declared in the schema, so a justified override validates); every bound isolation level carries probe evidence. Plus an EVALUATION mode: given a binding + an evidence source, resolves the EFFECTIVE promotion state — the bound knob is a ceiling; live contrary evidence (gate failure, reverted merge, verification divergence) lowers it at evaluation time without writing the binding (automatic demotion has no write-back actor by design: the binding is agent-unwritable; the demotion event additionally files an escalation item requesting the human-ratified binding update). This evaluation is a LIVE-PATH OBLIGATION, not a demo artifact: the admission seam and the merge disposition MUST resolve the effective state (querying the promotion-evidence telemetry) before every autonomous dispatch/merge decision — reading the raw promotion_state alone is non-conforming, and unavailable evidence telemetry fail-closes to the unpromoted default — the checker's evaluation mode exists to exercise the same resolution mechanically. Exit contract 0/1/2.
plugins/autonomy/skills/setup/SKILL.md Modify Document the two-surface split + layered resolution order (org-policy-home defaults → settings-as-code per-repo security binding → repo-local non-security remaps in the WP1 binding's additive guardrails section: class→label strings, cost-tier→model names) — INCLUDING the security binding's resolvable locator: the org-policy home carries the repo→security-binding-document registry the dispatch seam resolves the binding through when settings-as-code is a separate repository — and for SECURITY resolution the org-policy-home identity itself must come from an agent-unwritable bootstrap, never the repo-local binding (the binding seam's known limitation that org_policy_home may persist repo-locally is tolerable for non-security defaults only; a repo-writable pointer would let an agent redirect the whole chain to a forged policy repository with a forged registry, binding, and matching runtime markers): the seam pins the org-policy-home identity from org-level platform configuration outside repo blast radius (an org-level setting/variable repo agents cannot write) or the executor's trusted deployment config, and any security resolution that would depend on a repo-writable pointer — or an unresolvable locator — fail-closes autonomous dispatch with the compliant path named. Fail-closed on absent/invalid security binding; documented defaults only for non-security axes.

Sanity Check:

  • check-security-binding.mjs exit 0 on a fixture-valid document, exit 1 on each negative fixture: C4 merge_policy auto, executor_class: vendor-hosted with any non-human-gated merge row, promotion_state entry for a C4 cell, missing probe evidence, missing L2 under dispatch_posture: autonomous-enabled, admission weakened without override_justification; exit 0 with a blocked-dispatch verdict on the missing-L2 + human-gated-only fixture (deliberate posture, not a defect) (fixtures under evals/fixtures/)
  • Evaluation mode: a promoted-C2 binding + contrary-evidence fixture resolves the EFFECTIVE state demoted (exit output asserts the ceiling was lowered without modifying the binding file)
  • Schema declares executor_class and override_justification (grep -c 'executor_class' …schema.json ≥ 1; grep -c 'override_justification' …schema.json ≥ 1)
  • Schema additionalProperties: false present (grep -c '"additionalProperties": false' …schema.json ≥ 1)
  • claude plugin validate --strict exit 0

Phase 3: Guided-setup guardrail slice [DONE]

Extends the setup skill: detect → bind → live-validate → fail-closed (D4), always
detect-diff-reconcile against existing org guardrail surfaces (D5 — sandbox configs, branch
protections, review workflows), never greenfield-assume, never silently overwrite.

File Action What changes
plugins/autonomy/skills/setup/SKILL.md Modify Guardrail slice: detect available substrates per level per machine surface; bind level→substrate into the security binding; LIVE-VALIDATE before recording — the empirical probe per substrate class: denied-egress smoke test inside the boundary (a network fetch to a well-known external host MUST fail) + host-credential-path read attempt (MUST be absent/denied); a binding lands only after the probe transcript proves the boundary; probe evidence ref recorded in the binding. Fail-closed verify: no L2 substrate on a surface → autonomous dispatch blocked for that surface, compliant paths named. Security-review slice folded here per D5 (one guardrail slice, no separate near-duplicate capability). Paid scanner SKUs advisory + opt-in with cost surfaced.
plugins/autonomy/skills/setup/templates/isolation-probe.md Create Probe recipe per substrate class (container / OS-sandbox wrap / VM-microVM), parameterized, vendor names as marked examples only: egress-denial probe command shape, credential-absence probe shape, expected-failure assertions, transcript capture shape.
plugins/autonomy/skills/setup/evals/evals.json Modify Add guardrail-slice cases: substrate detection interview, probe-before-bind ordering, fail-closed no-L2 path, detect-diff-reconcile against a pre-existing branch-protection surface, paid-SKU advisory refusal-to-default.
plugins/autonomy/README.md + plugins/autonomy/.claude-plugin/plugin.json Modify Capability list + description + minor version bump.

Sanity Check:

  • /skill-quality:check + validate-evals pass
  • grep -c 'probe' plugins/autonomy/skills/setup/templates/isolation-probe.md ≥ 3
  • grep -ci 'detect-diff-reconcile' plugins/autonomy/skills/setup/SKILL.md ≥ 1
  • Fleet-name sweep exit 0

Phase 4: Live-validation demonstration + gates [DONE]

Acceptance probe, BOTH paths mandatory: (positive) run the Phase 3 probe recipe against a
REAL, PROVISIONED egress-denied boundary — the CI job on this public repo (free minutes)
launches a nested no-network container via the container runtime present on the hosted
Linux runner image (network mode none / internal-only, no host env or secrets passed in)
and executes the probe INSIDE it; the outer job keeps normal networking, so the probe's
egress-denial assertion is satisfied by a genuine boundary the job itself created — never
by the bare runner (which has outbound access and would fail the assertion) and never by a
hardcoded "denied" — transcript shows denied egress + absent credentials inside the
boundary, proving the probe DETECTS a genuine boundary; (negative) the fail-closed path is demonstrated by
validating an autonomous-enabled binding with a MISSING L2 entry and observing the
blocked-dispatch error; the deliberate human-gated-only posture validates separately with
its blocked-dispatch verdict reported as declared, not as a defect. A
fixture security binding referencing the positive probe's evidence passes
check-security-binding.mjs; the demotion evaluation fixture (Phase 2) runs here as part of
the demo record. Then the full gate roster (WP2 Phase 4): validate-plugins.sh,
run-plugin-tests.sh, validate-plugin-contracts.mjs, markdown/typos/lychee,
claude plugin validate --strict, catalog regen. Near-duplicate audit statement: security
review folded into the one guardrail slice (D5); escalation composes the governed queue — no
second channel created.

Sanity Check:

  • Positive probe transcript (provisioned no-network container on the CI runner: egress denied + credentials absent inside; outer job networked) attached to the PR body
  • Negative path: check-security-binding.mjs exit 0 on the demo binding, exit 1 on the missing-L2 autonomous-enabled fixture, exit 0 + declared blocked-dispatch verdict on the human-gated-only fixture
  • Demotion evaluation output (effective state lowered, binding file unchanged) in the demo record
  • All gate scripts exit 0; catalog in-sync
  • Near-duplicate audit statement present in the PR body

Blast radius

MEDIUM-HIGH — one plugin's files, but this contract governs every autonomous dispatch
decision (admission), every isolation binding, and the promotion path to the closed loop;
security-posture content raises the review bar. Fully git-revertible; the security binding
itself ships as schema + fixtures only (no live org binding lands in this package).

Stress-test summary

Fresh-context plan review (WP4+WP5 batch): 14 findings, verdict FIX-THEN-SHIP, all folded.
WP5's share — F1b (HIGH, cross-package): the hosted human-merge-gate cap keyed on an
executor class the schema never carried → executor_class added to the security schema and
enforced by the check; F9 (MED): override_justification was required by the check but
undeclared under additionalProperties: false → declared in the admission-rule sub-schema;
F10 (MED): automatic demotion had no write-back actor against an agent-unwritable binding →
eval-time evidence-gating (bound state is a ceiling; live contrary evidence lowers the
effective state; the demotion event files an escalation item requesting the human-ratified
update), with an evaluation-mode fixture in the check and the demo; F11 (MED): the
fail-closed pivot alone never proved the probe detects a real boundary → positive probe
mandatory against a free-tier CI container, negative path demonstrated separately; F12
(LOW): fragile grep -c '20' → full-phrase grep; F13 (LOW): 3-D admission table gains a
"*" wildcard + most-specific-wins precedence, and promotion entries for non-promotable
cells are rejected; F14 (LOW, symmetry): the D8 backlog seeds are echoed in Open questions.
The classification-rules half of F1a lands here as the admission.classification schema
content (the WP4 contract's adapters stamp from it).

Execution shape

Fully sequential 1 → 2 → 3 → 4 — Phase 2's schema serializes Phase 1's admission leaf;
Phase 3 wires Phase 2's binding; Phase 4 probes Phase 3's recipe. Cross-package: after the
WP4 implementation PR (admission seam exists to cite).

Phase Surface Basis
1 main-session normative security-policy authoring, tightly coupled T2/T3 imports
2 main-session schema + semantic-validator judgment
3 main-session setup-skill judgment, probe design
4 main-session live probe + gate runs

Open questions

  • Fleet materializations (live org security binding, workflow gates, scanner wiring) —
    /work-items backlog post-merge (Brief out-of-scope, triggers recorded).
  • Interactive escalation UX — WP7 runner design pack (Brief trigger).
  • D8 backlog seeds (auto-mode classifier tuning + safe-command allowlist distribution) —
    carried in the Brief with named triggers; filed via the work-items flow.

Decisions made (gate-passed)

Decision What it changes in the plan Basis (evidence)
Leaf split: 4 leaves under reference/guardrails/ Phase 1 file set Brief D1 names exactly these four as the example split; each maps to one matrix column/axis
Hub-subdirectory layout (first reference/ subdir) Phase 1 paths Progressive-disclosure hub needs its leaves grouped; flat names would couple hub prose to 4 sibling filenames
Promotion defaults: C2 ≥20 runs/≥14 days/100% gates/0 reverts; C3 ≥30 advisory/0 missed-blocking work-classes leaf D3 requires suggested defaults at architect; values conservative per Boris trust-before-scale (org-bindable)
Cap defaults autonomous_concurrency: 1, items_per_run: 3 admission leaf + schema D7 requires caps; floor values bound total fan-out at the trust-earning stage
Security binding as JSON + JSON Schema + semantic validator Phase 2 deliverables WP1 binding precedent (schema-versioned JSON, stop-and-report on invalid); schema alone cannot express matrix caps
executor_class + classification rules live on the security surface; promotion is eval-time evidence-gated (ceiling semantics) Phase 2 schema + check Stress-test F1b/F10: gating inputs must sit outside agent blast radius; an agent-unwritable binding cannot receive an automated demotion write
Demo pivots to fail-closed path when no container substrate exists Phase 4 D4 makes fail-closed the required behavior — the pivot demonstrates the contract, not a skipped demo

[FALLBACK — confirm or override] check-security-binding.mjs as a NEW deliverable (Phase 2)
— invented beyond the Brief on the WP2/WP4 enforcement-surface precedent. Flag if unwanted.

Handoff to implementation

User-approval gates

  • The binding-check script above is [FALLBACK] — surface before authoring if contested.
  • Any change to the shipped admission defaults or matrix cells during implementation → STOP
    (these are T3/D7 user-locked content, not implementation discretion).
  • Any scope expansion beyond the four phases re-enters /architect review.

Execution shape ([EXEC-SHAPE] tagged)

Sequential 1→4, all main-session (table above). PLAN.md phase tags advance in the same commit
as each phase; live probe per Phase 4 with the documented fail-closed pivot.

Mechanical work

Commit per phase on the implementation branch (suggest feat/autonomy-guardrails); gates
re-run in full at Phase 4; PR body carries the probe transcript + near-duplicate audit
statement + this PLAN in a <details> block at close-out.

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits July 19, 2026 01:08
Hub carries the five-class x five-column matrix (T3 verbatim, C3 promotable
cell now linking the security-review leaf), glance-layer routing, D8
permission-posture note, and D6 escalation event classes over the governed
queue. Leaves: isolation ladder (L0-L3, rejected trigger-source axis,
fail-closed L2 floor), work classes (risk bundles, promotion/demotion
predicates with shipped defaults), security review (two layers, per-class
blocking knobs, floors without override escape), admission policy (3-axis
decision table, wildcard + most-specific-wins, equal-specificity tie =
invalid binding, caps 1/3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or (WP5 Phase 2)

Contract-owned JSON Schema (draft 2020-12, additionalProperties false,
fail-closed semantics in-schema) for the settings-as-code security binding:
executor_class, dispatch_posture, per-surface isolation bindings with
runtime_markers + probe evidence, merge/verification knobs, promotion_state,
escalation routes, admission object (classification + wildcard decision
table + caps). check-security-binding.mjs enforces the semantics the schema
cannot: matrix merge caps, vendor-hosted human-gate cap, marker non-emptiness
+ pairwise joint-satisfiability ambiguity rejection, class-aware per-surface
isolation verdicts (C5 requires L3), missing-L2 error under
autonomous-enabled vs declared verdict under human-gated-only, non-promotable
promotion entries, admission weakening without override_justification,
verification floors (admission-only override escape), equal-specificity
admission ties invalid, promotion-gated knobs require their ratified
promotion_state entry, and an evaluation mode resolving effective promotion
state against contrary evidence without writing the binding. 15 fixtures;
SKILL.md gains the two-surface resolution section with the agent-unwritable
org-policy-home bootstrap rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ect-diff-reconcile, fail-closed (WP5 Phase 3)

Setup gains the one guardrail slice (security review folded in, no separate
capability): per-surface substrate detection, live probe validation before
any binding lands (denied-egress + credential-absence transcripts recorded
as probe evidence), reviewable preparation of the agent-unwritable security
binding, detect-diff-reconcile against existing org guardrail surfaces, paid
SKUs advisory with cost surfaced, fail-closed no-L2 verify. Isolation-probe
template per substrate class; five new evals; plugin 0.5.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d before merge)

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 30s —— View job


PR Review: feat(autonomy): guardrail matrix, security binding, setup slice (WP5)

  • Gather context and understand the request
  • Review guardrail reference documents (hub + 4 leaves)
  • Review security-binding JSON schema
  • Review semantic validator script (check-security-binding.mjs)
  • Review SKILL.md setup slice changes
  • Review evals and fixtures
  • Review plugin.json version bump and templates
  • Review demo workflow handling
  • Compile findings and post review

Summary

Architecturally this is a well-designed package. The two-surface split (agent-unwritable settings-as-code home for security axes, repo-local for non-security remaps), the fail-closed semantics at every decision point, and the probe-before-bind discipline are the right model. The validator script is thorough and the contracts are internally consistent. One blocker, two correctness notes, and a few minor items below.


Blocker

Demo workflow must be removed before squash-merge

wp5-isolation-probe-demo.yml is still in the branch. The PR body says "removed before merge" and the commit message echoes it, but the file is present and would land in the squash commit as written. Per the PR flow: run CI to collect the transcript, then delete the file in a follow-up commit before merging.

The workflow itself is sound — permissions: {}, triggers only on edits to its own path, outer job stays networked while the inner boundary is sealed. No security concern with the file existing; the concern is the stale artifact landing in main.

Fix this →


Correctness

dispatch_posture schema default is annotation-only and won't be applied by validators

guardrails-security-binding.schema.json L26:

"dispatch_posture": {
  "enum": ["autonomous-enabled", "human-gated-only"],
  "default": "autonomous-enabled"
}

In JSON Schema 2020-12, default is a purely documentary annotation — no conformant validator (AJV, ajv-formats, etc.) materializes it. A consumer that validates an absent-dispatch_posture binding with the schema will see it as valid, but if they then also run check-security-binding.mjs, the validator correctly defaults to "autonomous-enabled" on line 348:

const posture = binding.dispatch_posture ?? "autonomous-enabled";

The script is the enforcement path and it handles this correctly. The risk is a consumer who validates with the schema alone, doesn't run the script, and assumes an absent field resolves to "autonomous-enabled" — when their runtime behavior (whatever enforces admission) may not apply any default. The schema description already says the field governs the posture; adding a note that the default is semantic (enforced by the validator script) and not schema-materialized would close the gap. Alternatively, make the field required.

Probe workflow uses a mutable image tag

.github/workflows/wp5-isolation-probe-demo.yml L28:

docker run --rm --network none ubuntu:24.04 bash -c '...'

ubuntu:24.04 is a mutable tag. A digest-pinned reference (ubuntu@sha256:...) would guarantee the same image across runs and remove the supply-chain dependency on Docker Hub tag immutability. Low severity given the workflow is temporary, has permissions: {}, and runs no checkout — but the probe transcript is meant to serve as evidence in the binding's probe_evidence field, and evidence produced by an unspecified image is slightly weaker. If the probe is already run and the transcript is captured, this is moot; flagging in case the run hasn't completed yet.


Validator notes (non-blocking)

bindingPath = undefined error message could be more specific (check-security-binding.mjs L620)

When extra positional arguments are supplied (e.g. binding.json unexpectedarg), the parser sets bindingPath = undefined and falls through to the usage error, which prints the generic usage string. The user sees "usage: ..." with no indication which argument triggered it. Adding a diagnostic line (unexpected positional argument: "${args[i]}") before the break would make CLI debugging faster. Very minor.

promotion_state schema allows any key; semantic restriction is validator-only

guardrails-security-binding.schema.json L58 uses additionalProperties: { "$ref": "#/$defs/PromotionEntry" }, which allows any key (including non-promotable cells like C4-merge). The schema description documents the restriction correctly, and the validator enforces it at lines 443–449. This is a reasonable design choice (JSON Schema cannot express the closed enum of promotable cell ids without a propertyNames constraint), but worth a comment in the schema description noting that key validation is semantic-only, so a schema-only check won't catch C4-merge. Already implicit in the validator's architecture; just noting for completeness.


CLAUDE.md compliance

Rule Status
Repo-agnostic (no hardcoded paths/repos) ✓ All paths use ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PROJECT_DIR}
Configurable without editing the plugin ✓ Security binding is org-supplied; plugin carries schema + validator only
Plugin-form-safe ✓ No ../ reach-outs; persisted state anchors at ${CLAUDE_PLUGIN_DATA}
No PII/secrets
Versioned plugin.json bumped 0.4.00.5.0
Security-reviewed (per-playbook) ✓ Near-duplicate audit present; no second egress/escalation channel; no agent-writable security axes

What looks right

  • Two-surface split is the correct architectural choice. Binding security axes (isolation, merge policy, verification knobs, admission rules) in a settings-as-code home outside the agent's blast radius, with a non-forgeable bootstrap for security resolution, is exactly the right model.
  • Fail-closed everywhere — absent/invalid binding blocks autonomous dispatch; absent evidence fails promotion cells to unpromoted; unattestable surface fails closed as unattestable. Consistent.
  • Probe-before-bind is enforced by making probe_evidence schema-required on every level binding, not just advisory. The validator and schema agree.
  • Pairwise joint-satisfiability check on runtime markers is the right level of rigor — subset/identity checks would miss non-conflicting union matches, which is exactly the ambiguity the dispatch seam can't resolve.
  • Evaluation mode (--evidence) correctly models demotion as a ceiling-lowering read with no write-back — clean separation between the governance surface and runtime evidence.
  • Fixture coverage — 15 fixtures exercising 14 negative paths (each with exactly one finding) plus the valid baseline and the evaluation-mode demonstration. The specificity counting in equal-specificity-tie.json is correct: rules[5] and rules[6] both have specificity 2 (two non-wildcard axes), producing exactly one tie finding.

One action required before merge: remove wp5-isolation-probe-demo.yml from the branch. Everything else is either a documentation clarification or a nitpick. The core package — guardrail hub, four leaves, security-binding schema, semantic validator, setup skill slice — is ready.

@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: b3c218418a

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
Comment thread plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
… evidence, promotion epochs, encoding

Probe evidence is now VERIFIED, not just present: refs resolve under
--probe-evidence-root and must parse as the isolation-probe template's JSON
transcript with denied/absent assertions and a networked outer context; an
unverifiable L2/L3 entry is UNPROVEN and excluded from isolation eligibility
(fail-closed). Contrary promotion evidence is scoped to the current epoch via
required ratified_at (pre-epoch events were consumed by the re-earn;
unparseable timestamps stay contrary fail-closed). NUL pair-separator
replaced (editorconfig binary flag). Demo workflow captures the live JSON
transcript and proves the checker passes only against live-captured evidence;
credential probe paths via HOME (machine-specific-paths lane).

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 14s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of d56ffc6 fixes

  • Check if demo workflow blocker was resolved
  • Review probe evidence verification fix (Codex P1)
  • Review promotion epoch scoping fix (Codex P2)
  • Review new fixtures added in d56ffc6
  • Check for any remaining issues

Blocker still open: demo workflow not removed

.github/workflows/wp5-isolation-probe-demo.yml is still present at d56ffc6 — the latest HEAD. The file needs to be deleted and committed before squash-merge. I can't remove it myself (GitHub App permissions don't allow workflow file modifications), so this needs a manual commit from the branch author.


Codex P1 fix: probe evidence verified — correct ✓

The fix is comprehensive. verifyProbeTranscript() resolves probe_evidence as a file path (relative to --probe-evidence-root when given, else as written), reads and parses the transcript JSON, and validates all three capture-shape predicates: egress_denied.outcome === "denied", credentials_absent.outcome === "absent-or-denied", and outer_context_networked === true. An arbitrary non-empty string can no longer satisfy this check.

Under autonomous-enabled, an unverifiable L2/L3 entry produces a finding and the level is excluded from eligibility (fail-closed, lines 486–490). Under human-gated-only, unverifiable entries are reported in the verdict output but don't produce findings — correct, since there's no autonomous dispatch to protect. The posture-asymmetry is the right model.

New fixtures cover the path correctly: unverifiable-probe-evidence.json references a nonexistent transcript file and produces one finding; probe-transcripts/ci-pool-a-l2.json is a well-formed transcript that passes all three checks.

One observation (non-blocking): verifyProbeTranscript validates the three security-critical fields but does not check schema_version. If a future capture-shape revision changes the assertion field names, the verifier would silently accept old transcripts (they'd fail the outcome checks anyway). This is currently harmless but noting for when the capture shape evolves.


Codex P2 fix: promotion epoch scoping — correct ✓

The fix is correct. ratified_at is now required in both the JSON schema (PromotionEntry) and the structural validator (lines 258–262, with ISO 8601 parseability enforced). In resolveEffectivePromotion, contrary events are now filtered against ratifiedAt = Date.parse(entry.ratified_at):

  • Events with at >= ratifiedAt → in-epoch, count as contrary
  • Events with at < ratifiedAt → pre-epoch, ignored with an explanatory note
  • Events with an unparseable at → treated as contrary (fail-closed)
  • If ratifiedAt itself is NaN (malformed binding) → all parseable events are treated as in-epoch (fail-closed, consistent with the structural finding that would already block exit 0)

The fixture pair exercises this cleanly: promoted-c2.json has ratified_at: "2026-06-30T00:00:00Z". evidence-contrary.json (event at 2026-07-10) is post-ratification → demotes. evidence-stale.json (event at 2026-06-15) is pre-ratification → ignored with note, no demotion. Both cases are correctly handled.


Remaining open items from prior review

The previous review had two additional notes beyond the blocker:

  • dispatch_posture schema default is annotation-only — still present, still non-blocking. A consumer using the schema alone (without the validator script) would not get the default materialized. The description in the schema accurately conveys the semantic; making the field required would be the clean fix but isn't required to merge.
  • Mutable ubuntu:24.04 image tag in the demo workflow — moot once the workflow file is deleted.

Verdict

The two Codex findings are correctly and thoroughly addressed in d56ffc6. The architecture is sound and the fix-coverage is complete. One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: d56ffc60bd

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
Comment thread .github/workflows/wp5-isolation-probe-demo.yml Outdated
… demo probe assertions

Transcript identity must match the citing binding entry (reused evidence for
another level/surface/substrate is UNPROVEN, fail-closed); demo workflow
checks each credential path independently, asserts exact denial exit codes
so container infra errors cannot masquerade as denial, and validates a
jq-patched demo binding whose substrate matches the live-captured transcript.
Spelling per typos lane.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 18s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of ee93774 fixes

  • Gather context and understand the request
  • Check if demo workflow blocker is resolved (was still present at d56ffc6)
  • Verify Codex P1 fix: probe evidence bound to surface/level/substrate (ee93774)
  • Verify Codex P2 fix: credential paths checked independently in demo workflow (ee93774)
  • Check for any remaining issues

Blocker still open: demo workflow not removed (third time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is still present at ee93774 — the current HEAD. The file has been present across every commit in this branch and has been flagged as a merge blocker in every prior review cycle. As noted previously, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All other items below are non-blocking.


Codex P1 fix: surface/level/substrate binding — correct ✓

The fix is complete and correctly structured. verifyProbeTranscript now takes surfaceId, level, and substrate and performs three equality checks against the parsed transcript before proceeding to the assertion checks:

if (transcript.surface !== surfaceId)   surface mismatch
if (transcript.level   !== level)       level mismatch
if (transcript.substrate !== substrate) substrate mismatch

The call site at L488 correctly passes surfaceId, level, and entry.substrate. Mismatch returns the reason string; the caller treats it as unproven and produces a finding under autonomous-enabled (fail-closed).

Fixture: probe-evidence-level-mismatch.json has isolation_bindings["ci-pool-a"]["L3"] citing the existing L2 transcript (ci-pool-a-l2.json, which carries level: "L2"). The level mismatch fires exactly one finding; the suppression logic at L519 (!anyUnprovenL2Plus) correctly prevents a redundant no-L2 finding, keeping the fixture at exactly one finding.

Demo workflow: The jq step at L75 patches the binding's substrate to "nested-no-network-container" to match the live-generated transcript before running the checker. Without this patch the substrate check would fail (valid.json carries "egress-denied-container-pool"). The patch is correct and tight.


Codex P2 fix: independent credential path checking — correct ✓

The demo workflow now checks each credential path independently using a shell loop (L43–L53):

for p in "$HOME/.ssh" "$HOME/.netrc" "$HOME/.docker/config.json"; do
  if [ -e "$p" ]; then echo "credential path readable: $p"; present=1; fi
done
[ "$present" = "0" ] && exit 3 || exit 0

Exit 3 = all paths absent (probe passes); exit 0 = at least one readable (probe fails). The outer check requires exactly exit 3. This correctly catches a partial credential leak that the previous ls-on-three-args approach would have masked (any missing path caused ls to exit nonzero, which the old logic read as "absent").

Egress check: Exit code 4 now signals denial (not just non-zero), so a Docker infra error (exit 125/126/127) can no longer masquerade as proven egress denial. Both sentinel exit codes (4 = denied, 3 = all-absent) are documented in the failure message with concrete interpretations for each non-sentinel value.


Observation (non-blocking)

PR body fixture count is stale. The PR body says "15 fixtures" but the branch now has 18 fixture files (17 negative + valid.json): 3 were added in d56ffc6 (unverifiable-probe-evidence.json, promoted-c2.json, evidence-stale.json, evidence-contrary.json — actually 4 evidence fixtures) and 1 more in ee93774 (probe-evidence-level-mismatch.json). No action needed; the PR body can be updated at merge-close.


What looks right (unchanged from prior reviews)

The architecture, fail-closed semantics, probe-before-bind discipline, and evaluation-mode demotion are sound and internally consistent. The prior Claude and Codex reviews' substantive findings are all addressed. The only outstanding item is the workflow file deletion.


One action required before merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: ee937745e2

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
An expected-failure probe recording exit_code 0 is internally inconsistent
and proves nothing; such a transcript is UNPROVEN, fail-closed.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 19s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of 10ae236 fixes

  • Gather context and understand the request
  • Check if demo workflow blocker (wp5-isolation-probe-demo.yml) is resolved
  • Review 10ae236 fix: probe transcript exit_code nonzero validation
  • Check for any remaining issues or regressions

Blocker still open: demo workflow not removed (fourth time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is present at 10ae236 — the current HEAD. This has been flagged as a merge blocker in every prior review cycle. As documented previously, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All other items are non-blocking.


Codex P1 fix (10ae236): nonzero exit_code validation — correct ✓

The fix at L400–L405 is correct.

const nonzeroExit = (value) => typeof value === "string" && /^[1-9][0-9]*$/.test(value);

The regex ^[1-9][0-9]*$ correctly rejects: "0" (starts with 0), numeric 0, null, undefined, "", "-1", "01" (leading zero). It accepts "1", "3", "4", "123". No implicit coercions — the typeof guard fires first.

Null safety: The two new checks at L401/L404 access transcript.assertions.egress_denied.exit_code and transcript.assertions.credentials_absent.exit_code without optional chaining. This is safe: the preceding checks at L390–L394 use ?.outcome !== "denied/absent-or-denied", which would return early if either assertions or the nested object were absent. By the time L401 executes, both objects are guaranteed to exist.

Fixture: ci-pool-a-l2-zero-exit.json records egress_denied.exit_code: "0" (fires first, returns UNPROVEN) and credentials_absent.exit_code: "1" (valid, never reached). probe-evidence-zero-exit.json cites it under autonomous-enabled — the unproven L2 produces exactly one finding and autonomous dispatch is blocked fail-closed. Correct.

One observation (non-blocking): there's no fixture testing credentials_absent.exit_code: "0" while egress_denied passes. Since both checks use the same nonzeroExit helper and the convention is one-finding-per-fixture, current coverage is sufficient — noting for completeness.


Remaining open items from prior reviews

  • dispatch_posture schema default annotation-only — still present, still non-blocking. Making the field required would be the clean fix; not required to merge.
  • PR body fixture count stale — now 19 fixture files in the branch vs. "15" in the PR body. Non-blocking; update at merge-close.

Summary

10ae236's fix is correct and well-exercised by the new fixture pair. The prior Codex findings (P1 surface/level/substrate binding, P2 promotion epochs, this round's exit_code nonzero check) are all addressed. One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: 10ae23637a

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
An assertion without its recorded target (egress host, credential path)
could be any failing command; such transcripts are UNPROVEN, fail-closed.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 8s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of 539c033 fixes

  • Gather context and understand the request
  • Check if demo workflow blocker (wp5-isolation-probe-demo.yml) is resolved
  • Review 539c033 fix: probe transcript target field validation
  • Check for any remaining issues or regressions

Blocker still open: demo workflow not removed (fifth time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is present at 539c033 — the current HEAD. As documented in every prior review cycle, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All items below are non-blocking.


Codex P1 fix (539c033): probe target field validation — correct ✓

The fix at L410–L416 is correct.

const egressHost = transcript.assertions.egress_denied.host;
if (typeof egressHost !== "string" || egressHost.length === 0 || /\s/.test(egressHost)) {
  return `transcript ${path} records assertions.egress_denied.host ... — requires the probed external host (non-empty, no whitespace)`;
}
if (!isNonEmptyString(transcript.assertions.credentials_absent.path)) {
  return `transcript ${path} records assertions.credentials_absent.path ... — requires the probed host-credential path`;
}

Both checks are correctly gated after the outcome and exit_code checks that guarantee assertions.egress_denied and assertions.credentials_absent are already present — no optional-chaining needed at L410/L414.

Backward compatibility verified: Both existing valid transcripts ci-pool-a-l2.json and ci-pool-b-l2.json carry "host": "resolver.example" and a non-empty path — both pass the new check. The demo workflow's dynamically generated transcript at L65 includes "host": "example.com" — also compatible.

Fixture: ci-pool-a-l2-no-host.json omits egress_denied.host while carrying valid outcomes and non-zero exit codes. probe-evidence-missing-target.json cites it under autonomous-enabled. The checker returns UNPROVEN at the host check, producing exactly one finding and blocking autonomous dispatch fail-closed. Correct.

One observation (non-blocking): there is no dedicated fixture for the credentials_absent.path missing case in isolation (where egress_denied.host is present). The validator returns early on the first failure, so the current no-host fixture cannot also exercise the path-missing branch in the same run. A second transcript/fixture pair would close that gap, but it follows the established one-finding-per-fixture pattern and is not required to merge.


All prior Codex findings addressed

Finding Commit Status
Probe evidence not verified (arbitrary string passes) d56ffc6 ✓ Fixed
Promotion epoch not scoped (stale events demote forever) d56ffc6 ✓ Fixed
Probe evidence not bound to surface/level/substrate ee93774 ✓ Fixed
Credential path check masks partial leak ee93774 ✓ Fixed
Probe exit codes not required to be nonzero 10ae236 ✓ Fixed
Probe targets not checked (any failing command qualifies) 539c033 ✓ Fixed

Remaining open items from prior reviews

  • dispatch_posture schema default annotation-only — still present, still non-blocking. Making the field required is the clean fix; not required to merge.
  • PR body fixture count stale — the PR body says "15 fixtures" but the branch now has 20 fixture files across the security-binding directory plus 4 probe transcripts. Non-blocking; update at merge-close.

Summary

539c033's fix is correct, backward-compatible, and exercised by the new ci-pool-a-l2-no-host.json / probe-evidence-missing-target.json pair. All six Codex findings across five review cycles are correctly addressed. The architecture, fail-closed semantics, and fixture discipline are sound.

One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: 539c033dfd

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
…rker ambiguity rejected

ratified_at and evidence timestamps must be strict ISO 8601 with explicit
offset (epoch classification must not depend on evaluator timezone); level
entries within one surface must carry mutually conflicting runtime markers
so attestation pins the exact level boundary — max-proven-level aggregation
is sound only under that condition.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 49s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of 18228fc fixes

  • Check if demo workflow blocker (wp5-isolation-probe-demo.yml) is resolved
  • Review 18228fc fix: strict ISO promotion timestamps
  • Review 18228fc fix: same-surface level-marker ambiguity rejected
  • Check for any remaining issues or regressions

Blocker still open: demo workflow not removed (sixth time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is present at 18228fc — the current HEAD. As documented in every prior review cycle, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All items below are non-blocking.


Codex P1 fix (18228fc): strict ISO promotion timestamps — correct ✓

The fix is correct and thorough. parseIsoStrict() gates Date.parse behind a regex:

const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
function parseIsoStrict(value) {
  return typeof value === "string" && ISO_DATE_TIME.test(value) ? Date.parse(value) : Number.NaN;
}

This rejects "06/30/2026" (US-style, no time component), "2026-06-30" (date-only), and "2026-06-30T00:00:00" (no explicit offset) — all of which the bare Date.parse would have accepted with timezone-dependent results. The fix is applied in both places that need it: validateStructure L267 (structural validation of ratified_at) and resolveEffectivePromotion (epoch scoping of evidence at timestamps). Both parse paths now use the strict function — no regressions from partial application.

Fixture: ratified-at-non-iso.json carries promotion_state.C2-auto-merge.ratified_at: "06/30/2026" with state: "promoted". The structural check fires at L267, producing exactly one finding. Correct.


Codex P2 fix (18228fc): same-surface level-marker ambiguity rejected — correct ✓

The fix is correct and precisely scoped. The old if (a.surfaceId === b.surfaceId) continue; guard that allowed levels within a single surface to bypass the joint-satisfiability check has been removed. Same-surface pairs are now evaluated, but the pairKey differentiates them from cross-surface pairs to prevent duplicate findings:

const sameSurface = a.surfaceId === b.surfaceId;
const pairKey = sameSurface
  ? JSON.stringify([a.surfaceId, a.level, b.level])
  : JSON.stringify([a.surfaceId, b.surfaceId]);

The finding message correctly distinguishes the two cases, naming the ambiguous levels and suggesting the fix (conflicting markers on a shared key, or split into distinct surfaces).

The comment at L503–L509 now accurately states that max-proven-level aggregation is sound only because the joint-satisfiability check above enforces that same-surface level entries carry conflicting markers — the two checks form a coherent pair.

Fixtures:

  • same-surface-level-ambiguity.json — L2 markers {runner-pool: "ci-pool-a", region: "us-east"}, L3 markers {runner-pool: "ci-pool-a", tier: "metal"}. No shared key conflict (runner-pool matches, region/tier are disjoint) → jointly satisfiable → one finding. Correct.
  • two-level-surface-valid.json — L2 markers {runner-pool: "ci-pool-a", region: "us-east"}, L3 markers {runner-pool: "ci-pool-a-metal", region: "us-east"}. runner-pool has distinct values → not jointly satisfiable → no finding → exits 0. Correct.
  • New ci-pool-a-l3.json probe transcript: surface: "ci-pool-a", level: "L3", substrate: "microvm-pool", both exit codes nonzero (7 and 1), host and path non-empty. Passes all verifier checks. ✓

All Codex findings addressed

Finding Commit Status
Probe evidence not verified (arbitrary string passes) d56ffc6 ✓ Fixed
Promotion epoch not scoped (stale events demote forever) d56ffc6 ✓ Fixed
Probe evidence not bound to surface/level/substrate ee93774 ✓ Fixed
Credential path check masks partial leak ee93774 ✓ Fixed
Probe exit codes not required to be nonzero 10ae236 ✓ Fixed
Probe targets not checked (any failing command qualifies) 539c033 ✓ Fixed
Non-ISO promotion timestamps accepted timezone-dependent 18228fc ✓ Fixed
Heterogeneous levels aggregated under one surface verdict 18228fc ✓ Fixed

Remaining open items from prior reviews

  • dispatch_posture schema default is annotation-only — still present, still non-blocking. Making the field required is the clean fix; not required to merge.
  • PR body fixture count stale — the PR body says "15 fixtures" but the branch now has 23 binding fixture files and 5 probe transcripts. Non-blocking; update at merge-close.

Summary

18228fc's two fixes are correct, mutually consistent, and well-covered by the three new fixtures. All eight Codex findings across six review cycles are correctly addressed. The architecture, fail-closed semantics, and fixture discipline remain sound throughout.

One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: 18228fcc9c

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Outdated
…t fake conflicts

Object.hasOwn across all parsed-JSON membership checks (20 sites); a marker
key like toString no longer reaches Object.prototype and falsely satisfies
the conflict requirement.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 52s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of 8b39025 fixes

  • Gather context and check current branch state
  • Check if demo workflow blocker (wp5-isolation-probe-demo.yml) is resolved
  • Review 8b39025 fix: own-property marker comparisons
  • Review new fixture marker-prototype-key.json
  • Check for any remaining issues or regressions

Blocker still open: demo workflow not removed (seventh time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is present at 8b39025 — the current HEAD. As documented in every prior review cycle, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All items below are non-blocking.


Codex P2 fix (8b39025): own-property marker comparisons — correct ✓

The fix is correct. jointlySatisfiable() now uses Object.hasOwn(markersB, key) instead of key in markersB:

function jointlySatisfiable(markersA, markersB) {
  for (const [key, value] of Object.entries(markersA)) {
    if (Object.hasOwn(markersB, key) && markersB[key] !== value) return false;
  }
  return true;
}

Why this matters: With the old key in markersB, a marker key of "toString" in markersA would always find it in markersB (via Object.prototype), then compare markersB["toString"] (the prototype function) against any string value — which would always differ, causing jointlySatisfiable to incorrectly return false. This means two marker sets that are actually jointly satisfiable (e.g. L2 { toString: "l2" } + L3 { tier: "metal" }) would pass the ambiguity check without a finding, enabling an L3/C5 surface verdict from a phantom conflict.

Fixture: marker-prototype-key.json has L2 { "toString": "l2" } and L3 { "tier": "metal" } on ci-pool-a. With Object.hasOwn, "toString" is not an own property of { "tier": "metal" }, so no conflict is detected, jointlySatisfiable returns true, and the same-surface ambiguity finding fires. Exactly one finding, exit 1. Correct.

Complete audit: grep -c "Object.hasOwn" returns 20 — all membership checks across the 822-line script now use Object.hasOwn. A separate grep for remaining in-operator lookups on parsed JSON objects returns zero hits. The commit message's "20 sites audited" claim is consistent with what's in the file.

Logic correctness: The one-direction iteration (for key of Object.entries(markersA), check against B) is sufficient — "jointly satisfiable" requires checking only shared keys for conflicts, and any shared key appears in markersA when iterating it. Unshared keys impose no mutual constraint. The function is sound.


All Codex findings addressed

Finding Commit Status
Probe evidence not verified (arbitrary string passes) d56ffc6 ✓ Fixed
Promotion epoch not scoped (stale events demote forever) d56ffc6 ✓ Fixed
Probe evidence not bound to surface/level/substrate ee93774 ✓ Fixed
Credential path check masks partial leak ee93774 ✓ Fixed
Probe exit codes not required to be nonzero 10ae236 ✓ Fixed
Probe targets not checked (any failing command qualifies) 539c033 ✓ Fixed
Non-ISO promotion timestamps accepted timezone-dependent 18228fc ✓ Fixed
Heterogeneous levels aggregated under one surface verdict 18228fc ✓ Fixed
Prototype keys can fake marker conflict, masking ambiguity 8b39025 ✓ Fixed

Remaining open items from prior reviews

  • dispatch_posture schema default is annotation-only — still present, still non-blocking. Making the field required is the clean fix; not required to merge.
  • PR body fixture count stale — 24 binding fixture files + 5 probe transcripts in the branch; PR body says "15 fixtures". Non-blocking; update at merge-close.

Summary

8b39025's fix is correct, comprehensive (all 20 membership checks audited), and exercised by marker-prototype-key.json. All nine Codex findings across seven review cycles are correctly addressed. The architecture, fail-closed semantics, and fixture discipline remain sound throughout.

One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: 8b39025899

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
…class

Transcript substrate_class is required and validated; an L3 entry accepts
only vm-microvm — a relabeled container transcript can no longer unlock C5.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 23s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of 680b077 fixes

  • Gather context and understand the request
  • Check if demo workflow blocker (wp5-isolation-probe-demo.yml) is resolved
  • Review 680b077 fix: L3 probe evidence requires kernel-separated substrate class
  • Check new fixtures (probe-evidence-l3-container.json, ci-pool-a-l3-container.json)
  • Verify all prior Codex findings remain addressed
  • Check for any remaining issues or regressions

Blocker still open: demo workflow not removed (eighth time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is present at 680b077 — the current HEAD. As documented in every prior review cycle, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All items below are non-blocking.


Codex P1 fix (680b077): L3 substrate_class validation — correct ✓

The fix is correctly placed and thorough. verifyProbeTranscript now gates L2/L3 acceptance on a two-tier class check:

const SUBSTRATE_CLASSES = new Set(["container", "os-sandbox", "vm-microvm"]); // L57

// Tier 1: unknown class → UNPROVEN
if (!SUBSTRATE_CLASSES.has(transcript.substrate_class)) {
  return `transcript ${path} records substrate_class ... — required, one of container | os-sandbox | vm-microvm`;
}
// Tier 2: L3 floor — only kernel-separated substrates qualify
if (level === "L3" && transcript.substrate_class !== "vm-microvm") {
  return `transcript ${path} records substrate_class ... for an L3 entry — L3 requires kernel separation, which only substrate_class "vm-microvm" provides`;
}

Sequencing is correct: the substrate_class checks at L405–410 are inserted after the surface/level/substrate identity checks (L392–400) and before the assertion checks (L411+). Since the identity checks guarantee the transcript claims to be L3 before the class check fires, the attack path ("relabel an L2 container transcript to level L3") is blocked at the class check regardless of whether the attacker also changes the level field.

Backward compatibility: All 6 probe transcripts have been backfilled with substrate_class. All prior L2 entries now carry substrate_class: "container" — they pass tier-1 (known class) and are not subject to tier-2 (L3-only check). Zero regressions on the existing fixture suite.

Template updated: isolation-probe.md now includes substrate_class in the transcript capture shape and the per-substrate-class section explicitly maps container/os-sandbox to L2 and vm-microvm to L3. The template and validator are now in sync.

New fixtures:

  • ci-pool-a-l3-container.json: level: "L3", substrate_class: "container" → passes identity checks, fails tier-2 class check → UNPROVEN
  • probe-evidence-l3-container.json: cites the container transcript under an L3 entry under autonomous-enabled → exactly one finding, no C5 eligibility. Correct.
  • ci-pool-a-l3.json: substrate: "microvm-pool", substrate_class: "vm-microvm" → passes all checks. Referenced by two-level-surface-valid.json's L3 entry (substrate matches). C5 eligible on that entry. Correct.

Demo workflow: The live-generated transcript at L62 correctly emits "substrate_class": "container" for the L2 demo boundary (a nested docker run --network none container). The demo binding is L2, so the tier-2 check does not fire — consistent.


All Codex findings addressed

Finding Commit Status
Probe evidence not verified (arbitrary string passes) d56ffc6 ✓ Fixed
Promotion epoch not scoped (stale events demote forever) d56ffc6 ✓ Fixed
Probe evidence not bound to surface/level/substrate ee93774 ✓ Fixed
Credential path check masks partial leak ee93774 ✓ Fixed
Probe exit codes not required to be nonzero 10ae236 ✓ Fixed
Probe targets not checked (any failing command qualifies) 539c033 ✓ Fixed
Non-ISO promotion timestamps accepted timezone-dependent 18228fc ✓ Fixed
Heterogeneous levels aggregated under one surface verdict 18228fc ✓ Fixed
Prototype keys can fake marker conflict, masking ambiguity 8b39025 ✓ Fixed
Container transcripts accepted for L3 bindings 680b077 ✓ Fixed

Remaining open items from prior reviews

  • dispatch_posture schema default is annotation-only — still present, still non-blocking. Making the field required is the clean fix; not required to merge.
  • PR body fixture count stale — the PR body says "15 fixtures" but the branch now has 25 binding fixture files and 6 probe transcripts. Non-blocking; update at merge-close.

Summary

680b077's fix is correct, sequenced properly within the verifier, and covered by the ci-pool-a-l3-container.json / probe-evidence-l3-container.json pair plus a valid positive path in ci-pool-a-l3.json. All ten Codex findings across eight review cycles are correctly addressed. The architecture, fail-closed semantics, probe verification chain, and fixture discipline remain sound throughout.

One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: 680b077be4

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Outdated
parseIsoStrict derives the epoch from captured components and requires a
full round trip — silent normalization (Feb 30, hour 25) fails instead of
shifting the promotion epoch.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 46s —— View job


Claude finished @kyle-sexton's task in 3m 23s —— View job


Claude finished reviewing @kyle-sexton's updates


Review of 1a1c4b9 fixes

  • Gather context and understand the request
  • Check if demo workflow blocker (wp5-isolation-probe-demo.yml) is resolved
  • Review 1a1c4b9 fix: calendar-valid promotion timestamp round-trip
  • Check for any remaining issues or regressions

Blocker still open: demo workflow not removed (ninth time flagging)

.github/workflows/wp5-isolation-probe-demo.yml is present at 1a1c4b9 — the current HEAD. As documented in every prior review cycle, I cannot modify files in .github/workflows/ due to GitHub App permission limits — this requires a manual commit from the branch author.

The file must be deleted before squash-merge. All items below are non-blocking.


Codex P1 fix (1a1c4b9): calendar-valid promotion timestamps — correct ✓

The fix is correct and complete. parseIsoStrict() now derives the epoch from captured regex groups via Date.UTC, then round-trips through all six UTC getters before accepting the value:

const utc = new Date(Date.UTC(+year, +month - 1, +day, +hour, +minute, +second, ms));
if (
  utc.getUTCFullYear() !== +year  ||
  utc.getUTCMonth()   !== +month - 1 ||
  utc.getUTCDate()    !== +day    ||
  utc.getUTCHours()   !== +hour   ||
  utc.getUTCMinutes() !== +minute ||
  utc.getUTCSeconds() !== +second
) { return Number.NaN; }

Date.UTC silently normalizes out-of-range fields (Feb 30 → Mar 2; hour 25 → next day hour 1). The round-trip check catches any such normalization. No Date.parse path remains — the epoch is derived entirely from Date.UTC over the captured components, so no engine-specific parsing behavior is load-bearing.

Offset math is correct: for +05:30, (5 * 60 + 30) * 60000 = 19,800,000 ms, then utc.getTime() - 19_800_000 yields the correct UTC epoch (a +05:30 local time is 5½ hours ahead of UTC, so we subtract to convert). Negative offsets flip the multiplier to -1, which correctly adds back to UTC. Both directions verified.

Backward compatibility: for any previously-valid, calendar-valid ISO 8601 timestamp, Date.UTC produces the same epoch as Date.parse would have (they're equivalent for well-formed inputs with explicit offsets). No regressions on the existing fixture suite.

Fixture: ratified-at-calendar-invalid.json carries ratified_at: "2026-02-30T00:00:00Z". Date.UTC(2026, 1, 30) normalizes to March 2; utc.getUTCDate() = 2 ≠ 30 → NaN → structural finding fires. Exactly one finding, exit 1. Correct. The structural validator's finding message at L297–L300 is updated to name both the prior non-ISO form and the new calendar-validity requirement.

One observation (non-blocking): the offset field is validated by the regex's (Z|[+-]\d{2}:\d{2}) pattern, which accepts offsets like ±99:59. Valid IANA timezone offsets are bounded at UTC±14:00 (840 minutes). An extreme offset could shift the computed epoch by up to ~100 hours, making a contrary evidence event (e.g. timestamped 2026-07-03) appear pre-ratification (if a large negative offset pushes the epoch to 2026-07-05) and therefore ignored — suppressing a legitimate demotion. The attack surface is narrow: the settings-as-code home is human-gated and not agent-writable, and a ±99:59 offset would be immediately visible to a human reviewer. Adding a check that the offset's hours/minutes are within the IANA-valid range (hours ≤ 14, minutes in {0, 30, 45} or simply ≤ 59 with hours ≤ 14) would close this completely, but it is not required to merge.


All Codex findings addressed

Finding Commit Status
Probe evidence not verified (arbitrary string passes) d56ffc6 ✓ Fixed
Promotion epoch not scoped (stale events demote forever) d56ffc6 ✓ Fixed
Probe evidence not bound to surface/level/substrate ee93774 ✓ Fixed
Credential path check masks partial leak ee93774 ✓ Fixed
Probe exit codes not required to be nonzero 10ae236 ✓ Fixed
Probe targets not checked (any failing command qualifies) 539c033 ✓ Fixed
Non-ISO promotion timestamps accepted timezone-dependent 18228fc ✓ Fixed
Heterogeneous levels aggregated under one surface verdict 18228fc ✓ Fixed
Prototype keys can fake marker conflict, masking ambiguity 8b39025 ✓ Fixed
Container transcripts accepted for L3 bindings 680b077 ✓ Fixed
Calendar-invalid ratification dates silently normalized 1a1c4b9 ✓ Fixed

Remaining open items from prior reviews

  • dispatch_posture schema default is annotation-only — still present, still non-blocking. Making the field required is the clean fix; not required to merge.
  • PR body fixture count stale — the PR body says "15 fixtures" but the branch now has 26 binding fixture files and 6 probe transcripts. Non-blocking; update at merge-close.

Summary

1a1c4b9's fix is correct, backward-compatible, and exercised by ratified-at-calendar-invalid.json. All eleven Codex findings across nine review cycles are correctly addressed. The architecture, fail-closed semantics, probe verification chain, and fixture discipline remain sound throughout.

One action still required before squash-merge: delete .github/workflows/wp5-isolation-probe-demo.yml from the branch.

@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: 1a1c4b99b0

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Outdated
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

#549 redesign implemented — landing blocked on concurrent-worker collision (escalating)

The deny-by-default redesign from #549 is complete and verified, but I am not pushing it onto this branch this cycle because a second, differently-identified autonomous session (git co-author "Claude Fable 5") is actively extending the superseded point-fix approach on the same branch (commits cca4d2c, 6c940b6 at 21:03Z/21:39Z) while I held the worker lease. Racing it would thrash; the landing needs that session stopped first (coordination gap corroborated on #524).

Redesign preserved on branch redesign/549-security-binding-allowlists (commit c35ff65). What it does:

  • Egress: removes the special-use-range enumeration entirely (isNonExternalEgressHost, expandIpv6, foldInetAton, isDeniedV4, IANA/RFC tables). External-egress evidence now requires the probed host to be on the operator's --egress-hosts allowlist; unconfigured => untrusted, fail-closed. Membership is the sole test, so the class converges by construction.
  • Credentials: adds --credential-roots. A filesystem credential path is accepted only when its recorded host_expanded resolves (lexical, filesystem-independent, ..-safe) under a configured trusted root; unconfigured => untrusted, fail-closed. Cloud-metadata routes and well-known credential env tokens stay bounded closed sets. Expansion tail-consistency retained.
  • Dissolves the two deferred P1 threads (L184 credential rooting, L659-class IPv6 discard-only): the mechanisms they were about no longer exist.
  • Prunes the enumeration/rooting-heuristic fixtures that tested the removed code; keeps 100::/64, NAT64, documentation hosts as not-on-allowlist regressions and /home/definitely-not-a-host-user/.ssh + /host-home as not-under-root regressions. Full local corpus (53 fixtures) passes, each for its intended vector. Docs + 0.5.0->0.6.0 bump included.

Reconciliation note for whoever lands it: Fable 5's two commits are a mix — the required --probe-evidence-root and the new hosted-ephemeral-executor L3 class are orthogonal/compatible and should be KEPT; transport_outcome and credential outer_exit_code need a per-merit call (the redesign supersedes the target-recognition enumeration, not every structural check). A trial merge shows 25 conflicts (1 in the validator, 23 modify/delete on removed fixtures, 1 transcript) — semantic, not mechanical.

A failed read must be attributable to a SECRET being absent or denied.
Recognition is now terminal-form based: the path must end at a
well-known concrete secret file (the OpenSSH private-key family,
.netrc, credentials, .credentials, .kube/config, .docker/config.json,
.config/gh/hosts.yml). A non-secret sibling under a credential
directory ($HOME/.ssh/known_hosts) passes the outer existence check
and fails the inner read while the actual private key stays exposed,
and a bare directory marker ($HOME/.ssh) has ambiguous read-tool
semantics and fails everywhere — both now reject. GnuPG drops from the
recognized set (no static concrete secret file; key-grip names are
arbitrary) and defers to the configured allow-list seam, like
org-named /run/secrets entries.

Transcripts with terminal directory markers migrate to their concrete
files; reject fixtures pin the known_hosts descendant and the bare
directory marker; an accept pin covers $HOME/.ssh/id_rsa. Sweep:
8 pass / 78 reject, zero flips.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 21s —— View job


Claude Code Review — 474b63a

Commit: 474b63a — fix(autonomy): credential entries must end at a concrete secret file
Scope: check-security-binding.mjs · 8 probe-transcript fixtures · 3 binding fixtures · isolation-probe.md template


Summary

This commit addresses the final Codex P1 finding: $HOME/.ssh/known_hosts was accepted as credential evidence because the old code checked for .ssh anywhere in the path. An attacker could probe against a non-secret sibling file (e.g. known_hosts, authorized_keys) — which the outer context can confirm exists — while the actual private key remains readable inside the boundary. The fix shifts from segment-anywhere matching to terminal-form matching: the path must end at a statically-enumerable secret file, not merely contain a credential directory name.


Core change — isRecognizedCredentialEntry

Old logic (segment-anywhere):

const CREDENTIAL_SEGMENTS = new Set([".ssh", ".netrc", ".aws", ".gnupg", "id_rsa", "credentials", ".credentials"]);
const CREDENTIAL_SEGMENT_PAIRS = [[".kube", "config"], [".docker", "config.json"], [".config", "gh"]];
// findIndex — any occurrence of a recognized segment in any position

New logic (terminal-form):

const TERMINAL_SECRET_FILES = new Set(["id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", ".netrc", "credentials", ".credentials"]);
const TERMINAL_SECRET_PAIRS = [[".kube", "config"], [".docker", "config.json"]];
const TERMINAL_SECRET_TRIPLES = [[".config", "gh", "hosts.yml"]];
// Checks ONLY segments[last], segments[last-1..last], segments[last-2..last]

Verdict: correct and complete. The three categories cover all static credential file names that can be named at probe time. Deliberately excluded (and correctly so):

Excluded pattern Reason
.ssh (directory alone) Directory, not a file — now rejected. $HOME/.ssh/known_hosts → terminal known_hosts ∉ any set.
.aws (directory alone) Same; $HOME/.aws/credentials still accepted (terminal credentialsTERMINAL_SECRET_FILES).
.gnupg/* GPG key grip filenames are dynamic hex strings — no static terminal form possible.
.config/gh (pair) Elevated to triple .config/gh/hosts.yml; bare directory is now rejected.

Edge cases verified:

  • id_rsa.pub → rejected (.pub suffix ∉ set). ✓
  • $HOME/.aws/config → rejected (config ∉ set; only credentials is). ✓
  • $HOME/.config/gh (directory) → rejected (terminal gh ∉ set; no pair match). ✓
  • $HOME/.config/gh/hosts.yml → accepted by triple. ✓

New fixtures — correctness

Fixture Path Expected outcome Logic
ci-pool-a-l2-known-hosts.json $HOME/.ssh/known_hosts UNPROVEN → 1 finding known_hostsTERMINAL_SECRET_FILES
ci-pool-a-l2-ssh-dir.json $HOME/.ssh UNPROVEN → 1 finding .sshTERMINAL_SECRET_FILES
ci-pool-a-l2-id-rsa.json $HOME/.ssh/id_rsa PROVEN → 0 findings id_rsaTERMINAL_SECRET_FILES; anchor $HOME; outer_exit_code=0

All three are structurally consistent with existing transcript fixtures (schema-valid, outer_context_networked: true, correct transport outcomes). ✓


Updated existing negative fixtures — single-defect invariant preserved

Each existing negative fixture had its path field updated from a directory form to a terminal file form so the ONE intended defect remains the only reason for rejection:

Fixture Was (path) Now (path) Defect (unchanged)
cred-outer-nonzero /root/.ssh /root/.ssh/id_rsa outer_exit_code: 1
invented-home-user /home/definitely-not-a-host-user/.ssh /home/definitely-not-a-host-user/.ssh/id_rsa Non-existent user in home path
dot-traversal /root/../var/empty/.ssh /root/../var/empty/.ssh/id_rsa Dot-segment /../ rejection
expansion-ephemeral $HOME/.ssh/tmp/.ssh $HOME/.ssh/id_rsa/tmp/.ssh/id_rsa Expansion to ephemeral /tmp/ base
expansion-tail-mismatch $HOME/.ssh/home/runner/.gnupg $HOME/.ssh/id_rsa/home/runner/.gnupg/id_rsa Tail /.ssh/id_rsa/.gnupg/id_rsa

All five correctly isolate the same single defect after the path update. ✓

The expansion-tail-mismatch case deserves a note: the new host_expanded ends in /id_rsa and the path tail is /.ssh/id_rsa — the suffixes share /id_rsa but the tail mismatch is still detected because the full 12-char tail /.ssh/id_rsa ≠ any suffix of /.gnupg/id_rsa. ✓


Template update

isolation-probe.md now reads: "ending at a concrete secret file rather than a directory or an arbitrary file beneath one". This accurately documents the new requirement for operators writing probe transcripts. ✓


No issues found

The fix is minimal, targeted, and complete. All 54 prior Codex findings remain addressed; this commit resolves the final P1 (credential directory bypass). The single-defect-per-fixture invariant is maintained across all eight modified fixtures. The terminal-form enumeration is correct for the SSH, AWS, kubeconfig, Docker, and GitHub CLI credential families.

This PR is ready to squash-merge.

@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: 474b63a7a0

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs
The substrate class was accepted solely from the probe transcript, so
doctoring an otherwise-valid L2 container capture to level L3 with
substrate_class vm-microvm reported C5 eligible although the two probe
assertions cannot establish kernel separation. Each level binding now
carries a required human-ratified substrate_class on the
agent-unwritable surface; eligibility keys off that ratified class (an
L3 entry ratifying a non-kernel-separated class is an invalid binding,
fail-closed), and the transcript's recorded class is capture evidence
that must equal it — so a doctored capture mismatches, and upgrading
the class itself requires a human change on the governance surface.
The dispatch seam attests runner identity via runtime_markers; the
class rides the ratified binding, never the capture.

All binding fixtures migrated with ratified classes matching their
transcripts; new fixtures pin the doctored-capture mismatch, the
non-kernel-separated L3 ratification, and the missing-class shape.
Sweep: 8 pass / 81 reject, zero flips.

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

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 56s —— View job


Review — commit 918a8e5

Scope: P1 finding — "Bind L3 substrate class to attested runner state." Prior to this commit, substrate_class was accepted solely from the probe transcript, a file an executing agent could modify after the human ratified the binding. The attack: relabel an L2 container transcript to substrate_class: "vm-microvm" and level: "L3" to falsely claim C5 eligibility.


Fix is correct

The commit closes the attack on all three layers:

1. Schema (guardrails-security-binding.schema.json)
substrate_class is added to required on LevelBinding with additionalProperties: false already in force — the JSON Schema validator rejects any binding entry that omits it before the semantic validator even runs. The enum (container | os-sandbox | vm-microvm | hosted-ephemeral-executor) is complete and matches SUBSTRATE_CLASSES / L3_SUBSTRATE_CLASSES in the script. L126–L134

2. Structural validation (validateStructure)
Two binding-side checks added: (a) !SUBSTRATE_CLASSES.has(entry.substrate_class) → structural finding for any unrecognized class; (b) level === "L3" && !L3_SUBSTRATE_CLASSES.has(entry.substrate_class) → structural finding when the ratified class is not kernel-separated. Level/class coherence is enforced against what the human wrote, never what a transcript records. L447–L463

3. Semantic verification (verifyProbeTranscript)
substrateClass (the ratified value) is threaded in as a new parameter. The transcript's recorded substrate_class is checked for equality against it before any capture-shape validation. A mismatch returns immediately unverified — the relabeling attack fails closed here: the transcript proves a different substrate than the ratified assertion names. L912–L924

Call-site: checkSemantics passes entry.substrate_class (the ratified field) through to verifyProbeTranscript — if the field is absent, undefined propagates, the mismatch check fires for any transcript that has a class set, and the entry is unproven. Fail-closed throughout. L1216


Three new fixtures cover the three new failure paths

Fixture Scenario Expected outcome
l3-ratified-class-container.json L3 binding with substrate_class: "container" Structural finding — ratified class is not kernel-separated
missing-substrate-class.json L2 binding with no substrate_class field Structural finding — required field absent
probe-evidence-ratified-class-mismatch.json L3 binding with substrate_class: "vm-microvm" but transcript records a container Semantic finding — transcript proves a different substrate than ratified

All three fixtures are well-formed in every other dimension (admission policy, escalation routes, merge policy) so only the targeted finding fires.


One minor observation (non-blocking)

In verifyProbeTranscript, the ratified-vs-transcript class mismatch check (transcript.substrate_class !== substrateClass) fires before the recognized-class check (!SUBSTRATE_CLASSES.has(transcript.substrate_class)). If the transcript carries a completely unrecognized class, the returned error says "mismatch" rather than "unrecognized class" — slightly less precise. Both paths return non-null (transcript unverified), so the fail-closed guarantee holds. Worth a note in the comment block for future readers, but no code change needed.


Verdict

The attack vector is fully closed. The fix places the eligibility decision exclusively on the human-ratified governance surface, validates that surface structurally before any transcript is read, and then cross-checks the transcript's capture against the ratified assertion. The three fixture files cover the three distinct failure modes introduced by the new requirement.

LGTM — ready to squash-merge.

@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: 918a8e5337

ℹ️ 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 plugins/autonomy/skills/setup/scripts/check-security-binding.mjs Outdated
…ions

A recognized secret-file basename at arbitrary depth under a credential
anchor let a probe pick a benign existing descendant ($HOME/scratch/
credentials, /etc/example/credentials) whose outer existence check passes
while real host credentials stay exposed. Recognition is now exact-location:
home-anchored relative forms (ssh key family, .netrc, .git-credentials,
.aws/credentials, .kube/config, .docker/config.json, .config/gh/hosts.yml)
under a home env token or /root, plus fixed system secret files
(/etc/ssh host keys, /run/secrets/credentials) matched in full. The vague
.credentials basename is gone (no tool owns it); fixtures probe $HOME/.netrc.
Two new reject fixtures cover anchor-descendant entries. Sweep: 8 pass /
83 reject, zero flips.

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

claude Bot commented Jul 20, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 7m 11s —— View job


PR #377 — Security review: feat(autonomy): guardrails matrix, security binding, setup slice (WP5)

Final review — commit 1f3dacf


Summary

This PR introduces a fail-closed security binding validator, an isolation-ladder reference, a five-class × five-column guardrail matrix, and the setup skill slice. The review ran across 45 commits; 55+ security findings were raised and addressed. The final commit (1f3dacf) closes the last open issue: credential path recognition accepted any anchor-descendant file whose basename matched a recognized name, allowing an adversarial probe to pick a benign existing file ($HOME/scratch/credentials, /etc/example/credentials) whose outer existence check passes and inner read fails — producing a false credential-absence proof while real host credentials stayed exposed.


Commit 1f3dacf — Exact-path credential recognition

What changed:

TERMINAL_SECRET_FILES / TERMINAL_SECRET_PAIRS / TERMINAL_SECRET_TRIPLES (basename-suffix matching) were replaced with two exact-form lists:

isRecognizedCredentialEntry now does full-array comparison via segmentsEqual (length + element-wise) rather than checking only the last 1–3 segments:

if (isHomeEnvToken(segments[0]) || segments[0] === "root") {
  const relativeForm = segments.slice(1);
  return HOME_SECRET_RELATIVE_FORMS.some((form) => segmentsEqual(form, relativeForm));
}
return FIXED_SECRET_PATHS.some((form) => segmentsEqual(form, segments));

Correctness verified for key cases:

Entry Old result New result Correct?
$HOME/scratch/credentials ACCEPTED (basename credentials matched) REJECTED (["scratch","credentials"]HOME_SECRET_RELATIVE_FORMS)
/etc/example/credentials ACCEPTED (etc anchor + credentials basename) REJECTED (["etc","example","credentials"]FIXED_SECRET_PATHS)
$HOME/.ssh/id_rsa ACCEPTED ACCEPTED ([".ssh","id_rsa"] ∈ forms)
$HOME/.aws/credentials ACCEPTED (bare credentials basename) ACCEPTED ([".aws","credentials"] ∈ forms, explicit)
/root/.ssh/id_rsa ACCEPTED ACCEPTED (segments[0]==="root", relativeForm=[".ssh","id_rsa"])
/etc/ssh/ssh_host_rsa_key REJECTED (not in old terminal files) ACCEPTED (new FIXED_SECRET_PATHS) ✅ new
$HOME/.git-credentials REJECTED (not in old terminal files) ACCEPTED (new HOME_SECRET_RELATIVE_FORMS) ✅ new
$HOME/.credentials ACCEPTED REJECTED (removed — not a well-known tool file)
$HOME/id_rsa ACCEPTED (id_rsa basename + home anchor) REJECTED (["id_rsa"] ∉ forms — must be under .ssh/)
/root/$HOME/.ssh/id_rsa ACCEPTED (dot-traversal gate aside) REJECTED (["$home",".ssh","id_rsa"] ∉ relative forms)

Corner cases confirmed correct:

  • UNC-form rejection (normalized.startsWith("//")) at L214 fires before segment matching
  • Dot-segment rejection at L208 fires before segment matching
  • Ephemeral segment rejection at L223 fires before segment matching
  • Bare $HOME (single segment) routes through the env-token branch at L199–L202, not recognized → REJECTED ✅
  • %USERPROFILE%\.ssh\id_rsa — after replaceAll("\\","/")%userprofile%/.ssh/id_rsa, isHomeEnvToken matches → relativeForm=[".ssh","id_rsa"] → ACCEPTED ✅

Fixture integrity verified:

Backfill verified (~40 transcripts): Every transcript that previously used $HOME/.credentials (no longer in the recognized set) was updated to $HOME/.netrc (still recognized as [".netrc"]). Each fixture's intended single defect — egress host classification, exit code validation, etc. — is preserved because the credential path change is neutral to those defects.

Error message updated at L1093 to enumerate exact required forms rather than basenames, eliminating the old confusing "probe genuine host credential locations ending at…" phrasing that implied basename matching was sufficient.


Non-blocking observation

Fixture corpus has no automated CI driver. The 93 binding fixtures in evals/fixtures/security-binding/ are not exercised by CI. scripts/run-plugin-tests.sh runs *.test.sh files; there are none for the autonomy plugin. The fixtures are invaluable as regression documentation and can be run manually against the checker, but a future logic regression in check-security-binding.mjs will not be caught by a CI lane. Suggest adding a check-security-binding.test.sh that runs the checker against every fixture and asserts exit-code 1 for negatives and exit-code 0 for positives (the valid/multi-surface fixtures). Not a blocker for this PR — the current fixture set is correct — but worth tracking.


Overall assessment

The fix is correct and complete. The exact-path matching approach eliminates the entire class of anchor-descendant attacks by construction, the new credential forms are well-chosen (system SSH host keys, .git-credentials, .aws/credentials explicit pairing), the removed forms ($HOME/.credentials, bare basename under /etc) are correctly de-listed, and the 40-transcript backfill preserves each fixture's single intended defect.

All 55+ prior findings from this review cycle remain addressed. The schema (substrate_class as a required binding-side assertion), the validator semantics (fail-closed, probe-before-bind, egress-host classification, credential-path exactness), and the guardrail reference documents are consistent with each other.

Approved.

@kyle-sexton
kyle-sexton merged commit bbea4b1 into main Jul 20, 2026
19 checks passed
@kyle-sexton
kyle-sexton deleted the feat/autonomy-guardrails branch July 20, 2026 02:46
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ice (WP6) (#600)

WP6 standing-routines package (T7), per the merged PLAN. All four phases
complete; the novel-class derivation probe and near-duplicate audit
statement are recorded below.

No linked issue

## Related

- #359 (WP4–WP7 plans, incl. this package's PLAN as merged)
- #242 (standing scheduled routines — the decision node this package
graduates: which maintenance classes, hosted where)
- #239 (WP map: AI adoption ladder, step-3 unlock)
- #372 (WP4 trigger-dispatch — routines are scheduled `temporal`
adapters behind its queue contract)
- PR #377 (WP5 guardrail matrix + security binding — the matrix the
mapping rules derive into; the `admission.classification.temporal` home
the routine slice binds)

## What ships

- **Routine catalog hub** (`reference/routines.md`): routine = scheduled
trigger adapter behind the governed queue, never a private
execution/merge path; two-family repetition language (session-scoped:
loop, goal, batch, dynamic workflow; standing: schedule, routine — with
the corrected batch definition); schedule/event/continuous trigger
taxonomy; output contract (report OR governed-queue work item; direct
change only through the matrix merge policy); the contract-owned
catalog-to-matrix mapping rules including the refined axes (DET → plain
cron, `not-a-routine`; hybrid DET-detect + AGT-judgment split; AGT rules
to C1–C3; structural/config-surface direct change → C4;
attacker-writable external input → C5; compose-to-highest; access axis →
prerequisites); 40-row glance catalog — ten `v1` rows, seven
`not-a-routine` rows, named join triggers on every deferred row.
- **Ten v1 definition leaves** (`reference/routines/`): uniform shape
(purpose, trigger and cadence, access scope, output contract, derived
guardrail row, admission and escalation, precedent). Every guardrail row
is derived step-by-step through the mapping rules, none hand-assigned:
six pure-advisory classes at C1/L2 floor; `doc-freshness-sweep`'s
optional gated docs-change portion C3; `dependency-update-wave` hybrid
with two explicit postures (mechanical-only C2; changelog-reading
posture provenance-composed to C5); `tech-debt-sweep` hybrid with
human-gated prioritization; `ci-health-review`'s optional CI-config
change portion C4 (structural surface). Cadence defaults org-bindable;
governance imported by citation only.
- **Guided-setup routine slice** (`skills/setup/SKILL.md`):
discovery-first surface interview + budget posture; free defaults wired
as reviewable changes across role homes; paid/preview surfaces advisory
with cost surfaced; detect-diff-reconcile of existing schedulers and
bots (a live judgment-bearing bot IS a catalog-class instance — recorded
under its class token, never duplicated; DET stays cron through
reconciliation). Binding-home split by governance sensitivity: the
routine→work-class mapping is admission data and lands only in the
security binding's `admission.classification.temporal` home
(settings-as-code, agent-unwritable, reconciled bots included); the
repo-local `routines` section carries cadence/enablement/surface only,
reusing the envelope checker's `surfaces` map shape.
- **`templates/routine-definitions.md`**: per-surface-class wiring
shapes (CI-cron / local scheduler / self-run / vendor-preview as marked
examples), every shape enqueuing through the trigger contract's
`temporal` adapter — no direct execution path.
- **Protected routine identity (review wave 1)**: routines bind under a
routine identity (`<class-token>` or `<class-token>/<posture-token>`;
multi-posture classes bind per posture, never the bare token);
`admission.classification.temporal` entries are object form `{class,
source_surface}` — the identity↔surface association on the security
binding, one identity per emitting surface; the temporal envelope
carries a required `signal.routine` claim admission validates before
stamping (`check-security-binding.mjs` + `check-signal-envelope.mjs`
enforce; five new fixtures; sweep 9 pass / 89 reject).
- **Evals**: five routine-slice cases (surface interview first,
free-default wiring home split, existing-bot reconciliation
record-not-duplicate, paid-surface advisory refusal-to-default,
selector-is-a-claim protected association). Plugin 0.5.0 → 0.6.0.

## Novel-class derivation probe (Phase 4 acceptance)

Classifying a class the catalog does not contain, using only the shipped
mapping rules — an untrusted-provenance case, exercising the C5 reach:

**Candidate class: community-forum feedback triage** — a standing sweep
that reads public community-forum threads about the product, clusters
recurring complaints, and files candidate work items.

1. **Judgment axis**: semantic clustering, salience ranking, and routing
are agent judgment → `AGT` (not `DET`; no deterministic detection half
worth a hybrid split — a keyword pre-filter could route to plain cron,
leaving the judgment portion as the routine, per the hybrid branch).
2. **Output axis**: report + work items into the governed queue, no
repository mutation → `C1` base (`AGT` + report → `C1`; `AGT` + work
item → `C1`).
3. **Direct-change axes**: no direct change → the `C2`/`C3`
mechanical-checkability rule and the `C4` structural-surface rule do not
lift the class.
4. **Input-provenance axis**: public forum posts are attacker-writable
external content → derives `C5` (untrusted provenance).
5. **Composition**: multiple matches compose to the highest-risk class —
`C5` > `C1` → the class is **C5**.
6. **Access axis → prerequisites**: `ext` access → connector
prerequisite + entitlement resolved in the org binding, join trigger
"named intel need"; the `C5` matrix row then fixes the floor — L3
isolation, full gates + zero secret exposure, human merge always,
escalation always. The read-only class's exfiltration-surface caveat
arrives as the class outcome, not a footnote.

End-to-end: axes → guardrail row → prerequisites, with zero contract
changes. The acceptance criterion holds, including the C5 reach the
plan's stress-test added the provenance axis for.

## Near-duplicate audit

Routines compose the WP4 `temporal` adapter and the WP5 guardrail
matrix; deterministic checks stay plain cron filing work items through
the same trigger adapters; no second scheduling, execution, or merge
path is created by this package.

## Plan deviation (recorded)

The PLAN's Phase-1 row-count gate said 39 (one row per research-taxonomy
class). The research table contains only 9 of the 10 locked v1 classes —
`pr-queue-tending` has no research row (the nearest research row, stale
grooming, is pure-DET and locked `not-a-routine`). The catalog therefore
ships 40 rows = 39 research rows + the fleet-proven `pr-queue-tending`.
The D2 mapping rules, the seven `not-a-routine` rows, and the ten-class
v1 roster are untouched.

## Gates

validate-plugins.sh, validate-plugin-contracts.mjs (33 setup skills /
1777 plugin files), markdownlint-cli2, typos, `claude plugin validate
--strict`, catalog regen in-sync — all green at head. Reference-doc
vendor-name sweep: zero hits (hub + all ten leaves). run-plugin-tests.sh
and lychee validate in CI.

<details>
<summary>WP6 PLAN (full)</summary>

# ai-ladder-wp6-routines

## Brief

### TLDR

Standing-routines package (T7): catalog doc as a progressive-disclosure
hub (class table =
glance layer), contract-tier definition docs for the v1 subset only,
contract-owned
catalog-to-matrix mapping rules, and the routine slice of guided-setup.
A routine stays a
scheduled trigger adapter behind the governed queue — never a private
execution or merge path.

### Goal

Any adopting org can stand up governed background maintenance — the
step-3 unlock:
"maintenance and cleanup that used to wait for someone to find the time
now runs continuously
in the background" — with every routine's guardrail row derived from
catalog axes, free
hosting defaults, and existing org schedulers reconciled rather than
duplicated.

### Locked decisions

| # | Decision |
|---|---|
| D1 | Scope: catalog doc (hub-and-leaves — class table with
judgment/output/access scores = glance layer) + per-class definition
leaf docs for the v1 subset ONLY (non-v1 classes stay table rows with
precedent pointers until their join trigger fires) + catalog-to-matrix
mapping rules + routine slice of guided-setup. All
capability-distribution home. v1 definitions are contract-tier
tool-agnostic docs; vendor-binding capability templates are build-stage
artifacts. Fleet routine stand-up = /work-items backlog. |
| D2 | Catalog-to-matrix mapping rules are contract-owned (orgs classify
new routine classes without republication): judgment DET → not a routine
— plain cron, zero agent tokens, failures file work items through
trigger adapters; AGT + report → C1; AGT + work-item output → C1
(governed-queue write, no repo mutation); AGT + direct change → C2 where
truth is mechanically checkable, C3 otherwise; AGT/HUM → agent prepares,
disposition human-gated always. Access axis → prerequisites: repo-scoped
= L2 unattended floor; prod/product/org/ext = connector prerequisite +
entitlement resolved in the org binding; external-watch classes read
attacker-writable content — the read-only class's exfiltration-surface
caveat applies; unattended GUI actuation = highest isolation tier.
*REFINED 2026-07-18 (WP6/WP7 stress-test F3+F4): (a) hybrid branch — a
DET-detect + AGT-judgment class (catalog rows 7/14/16/28, incl. v1
dependency-update-wave) splits: the detection portion routes to plain
cron, the judgment portion IS the routine and derives through the AGT
rules; the binary DET/AGT reading could not classify hybrids. (b)
Pure-DET classes carry an explicit `not-a-routine` catalog flag (rows
2/12/20/22/25/27/34) instead of silent exclusion. (c) Two axes added so
derivation reaches the full matrix: input provenance — a routine
consuming attacker-writable external content derives C5 (untrusted
provenance), upgrading the external-watch caveat to a class outcome;
structural blast radius — direct change to structural/config surfaces
derives C4. Overlapping matches compose to the highest-risk class (C5 >
C4 > C3 > C2 > C1). Without them derivation topped out at C3 and the
novel-class acceptance criterion was unmeetable for C4/C5 cases.* |
| D3 | v1 definition doc shape (each leaf): purpose (toil addressed),
trigger-taxonomy slot with suggested cadence (org binds values), access
scope, output contract (report shape / work-item filing / gated change),
guardrail row derived via D2, admission + escalation notes imported from
the guardrail contract, precedent pointer. No vendor scheduling detail
baked in — surfaces are preview-stage moving targets; setup researches
them live. |
| D4 | v1 subset (repo-scoped, agent-judgment, proven manual patterns):
issue triage sweep, duplicate-detection sweep, backlog readiness check,
PR-queue tending, doc-freshness sweep, dependency update wave,
advisory/CVE triage, tech-debt sweep, ~~dead-code sweep~~ eng-metrics
digest, CI health review. *CORRECTED 2026-07-18 (review evidence,
RESEARCH-routine-catalog row 25): dead-code sweep is classified DET
detect — per D2's rule it is not a routine; detection routes to plain
cron filing work items through trigger adapters, and any
judgment-bearing triage of its findings belongs to the tech-debt sweep.
The WP6 architect round confirms the final v1 roster.* Deferred classes
keep T7's named join triggers (prod/product/org/ext access, GUI
actuation). |
| D5 | Routine slice of guided-setup is discovery-first: interview the
org for scheduling surfaces (CI cron, dev machine, self-run infra,
vendor-hosted preview) and budget posture; wire free defaults as
reviewable changes (CI-cron handler = CI-orchestration home, enabling
settings = settings-as-code home); advise paid/preview surfaces with
cost surfaced; detect-diff-reconcile EXISTING org schedulers and bots —
a live dependency bot or stale bot IS a catalog-class instance: record
it in the binding, never stand up a second mechanism for the same
concern. |
| D6 | Ubiquitous language imported from the routine contract thread
with one correction: batch = session-scoped parallel fan-out over
decomposed units (vendor command verified against the official commands
doc; research had conflated it with bulk-inference Batch APIs). Contract
carries the two-family distinction: session-scoped (loop, goal, batch,
dynamic workflow) vs standing (schedule, routine). Routine output =
advisory report OR work item into the governed queue; direct change only
through the guardrail matrix's merge policy. |
| D7 | Hosting stance imported unchanged (deployment-owned binding):
contract fixes invariants only — queue contract, per-class isolation
floor, merge-policy caps including hosted-stays-human-gated, cost
surfaced before any paid binding. Profiles are non-normative examples;
no machine/org-size/budget assumption. |

### Constraints

- Any fleet repo or vendor name in normative contract text is a defect;
vendor names appear
  only as marked examples and in binding docs.
- No second execution, merge, or scheduling path: routines enter work
through the trigger
  contract's queue and are governed by the guardrail matrix from there.
- Deterministic checks are never routines — no agent tokens on script
work.
- No new cost by default; paid/preview scheduling surfaces are advisory
+ explicit opt-in.
- Boris-alignment is the standing acceptance criterion (no
step-skipping, trust before scale).

### Acceptance criteria

- Catalog glance layer alone answers "what classes exist, what governs
each" — leaf docs only
  for v1 depth (progressive-disclosure conformance).
- Every v1 leaf derives its guardrail row through the D2 rules; no row
is hand-assigned
  outside them.
- Mapping rules let an org classify a novel routine class end-to-end
(axes → guardrail row →
  prerequisites) without contract changes.
- Setup slice wires only reviewable changes, reconciles existing
schedulers instead of
  duplicating them, and has zero paid dependencies on its default path.
- Research gaps carried visibly: dependency-update-wave precedent detail
rests on official
docs whose page bodies were not all fetched in full (re-verify at
architect); support→bug
conversion class remains emerging/unverified (not in v1); one vendor
goal-primitive claim
  unconfirmed (not load-bearing for v1).
- Boris check: step-3 unlock sentence instantiated by the v1 subset
running as governed
background work; "break up your work into loops and routines" cell
covered with the
session-scoped vs standing distinction; step-3 trap honored
(catalog-now,
instantiate-on-trust — v1 dispatch gated by the admission policy and
promotion discipline);
token-efficiency bottleneck addressed (deterministic-stays-cron + cost
tiers); step-1
  guardrail untouched.

### Captured assumptions

- The v1 classes' manual patterns remain proven in the first adopting
instance (fleet dogfood
  evidence: recurring manual sweeps exist for each).
- Vendor scheduling surfaces remain preview-stage moving targets;
contract-tier docs stay
binding-free and setup researches surfaces live (re-verify at setup
time, not bake-in).
- The guardrail matrix (WP5) and trigger contract (WP4) land before or
with any v1 routine
  instantiation — routines have no governance path without them.

### Out-of-scope (deferred with triggers)

- Non-v1 class definition leaves — trigger: T7's named join triggers per
group (telemetry
connector, analytics/feedback connected, org systems connected, named
intel need, GUI-only
  system + isolation tier).
- Vendor-binding capability templates (routine/workflow files) — build
stage, post-architect.
- Fleet routine stand-up + existing-scheduler reconciliation execution —
/work-items backlog
  post-graduation.
- Interactive escalation UX for routine-filed items — WP7 (inherited
from guardrail package).

### Deferred questions

- Exact class tokens + catalog table serialization — `/architect` (with
plugin naming pass).
- Suggested cadence defaults + per-class output templates —
`/architect`.
- Leaf-doc file split + glance wording — `/architect`.
- Adapter shapes per scheduling-surface class — `/architect`
(live-verified then).

## Plan

Recommendation-locked this round under the user's standing
pre-authorization: catalog hub
`reference/routines.md` with v1 definition leaves under
`reference/routines/` (one file per
v1 class, kebab-case class tokens below); class tokens for the v1 roster
—
`issue-triage-sweep`, `duplicate-detection-sweep`,
`backlog-readiness-check`,
`pr-queue-tending`, `doc-freshness-sweep`, `dependency-update-wave`,
`advisory-cve-triage`,
`tech-debt-sweep`, `eng-metrics-digest`, `ci-health-review` (ten
classes; dead-code sweep
stays off the v1 roster per the D4 correction — the architect-round
confirmation the
correction asked for — and appears in the catalog as a `not-a-routine`
row; hybrid v1
classes, e.g. `dependency-update-wave`, classify through the D2 hybrid
branch: detection
portion cron, judgment portion the routine); catalog table serialization
= one markdown table (class token × judgment × output ×
access × derived guardrail row × v1/deferred + join trigger).

Prerequisites: WP4 implementation merged
(`reference/trigger-dispatch.md` — routines are
scheduled trigger adapters behind its queue contract) and WP5
implementation merged
(`reference/guardrails.md` — the D2 mapping rules derive rows into its
matrix). This package
merges after both.

### Phase 1: Catalog hub + mapping rules [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/routines.md` | Create | The T7 routine
contract as the catalog hub: routine definition (a scheduled trigger
adapter behind the governed queue — never a private execution/merge
path); the two-family ubiquitous language imported per D6
(session-scoped: loop, goal, batch, dynamic workflow; standing:
schedule, routine — with the corrected batch definition); trigger
taxonomy (schedule / event / continuous); output contract (advisory
report OR work item into the governed queue; direct change only through
the matrix merge policy); the D2 catalog-to-matrix mapping rules
verbatim, INCLUDING the refined axes (DET → plain cron, never a routine
— flagged `not-a-routine` in the catalog; hybrid DET-detect +
AGT-judgment → split: detection portion cron, judgment portion IS the
routine, derived through the AGT rules; AGT+report → C1; AGT+work-item →
C1; AGT+direct-change → C2 where mechanically checkable else C3;
structural/config-surface direct change → C4; input provenance:
attacker-writable external content → C5, the untrusted-provenance class,
not a caveat; when multiple rules match, the derivation COMPOSES TO THE
HIGHEST-RISK class (C5 > C4 > C3 > C2 > C1 — a structural change driven
by attacker-writable input derives C5 and its L3 floor, never C4's lower
floor); AGT/HUM → human-gated disposition; access axis → prerequisites
incl. L2 floor for repo-scoped, connector+entitlement for
prod/product/org/ext, highest tier for unattended GUI actuation —
without the C4/C5 axes the Phase 4 untrusted-provenance derivation could
not run from the shipped contract); the glance-layer catalog table: all
39 classes as rows (class token, judgment, output, access, derived row,
and a status flag — `v1`, a named join trigger, or `not-a-routine` for
the seven pure-DET rows incl. dead-code sweep, which stay as rows with
their cron/tooling precedent pointers rather than silent exclusions);
hybrid rows show the D2 split (detection portion cron, judgment portion
the routine); non-v1 rows carry precedent pointers only, no leaves;
hosting stance per D7 (invariants only, profiles as non-normative
examples). Zero vendor/fleet names in normative text. |

**Sanity Check:**

- `grep -c 'DET' plugins/autonomy/reference/routines.md` ≥ 1 and mapping
rules present (`grep -ci 'plain cron' …/routines.md` ≥ 1)
- Catalog rows: table row count = 39 (`grep -cE '^\| [a-z][a-z0-9-]+ \|'
plugins/autonomy/reference/routines.md` = 39)
- v1 rows = 10 (`grep -c '| v1 |'
plugins/autonomy/reference/routines.md` = 10, or the chosen flag token)
- `not-a-routine` rows = 7 (`grep -c 'not-a-routine'
plugins/autonomy/reference/routines.md` ≥ 7); the dead-code-sweep row
carries the flag (`grep 'dead-code'
plugins/autonomy/reference/routines.md | grep -c 'not-a-routine'` = 1 —
flagged, never a silent exclusion, and never a v1 leaf)
- Vendor+fleet deny-list sweep exit 0; lychee lane passes

### Phase 2: v1 definition leaves [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/routines/<class-token>.md` × 10 | Create |
One leaf per v1 class, uniform D3 shape: purpose (toil addressed);
trigger-taxonomy slot + suggested cadence default (issue-triage-sweep,
duplicate-detection-sweep, backlog-readiness-check, pr-queue-tending:
daily; doc-freshness-sweep, tech-debt-sweep, ci-health-review: weekly;
dependency-update-wave, advisory-cve-triage: weekly with event-riding on
advisories; eng-metrics-digest: weekly — all org-bindable values);
access scope (repo); output contract (report vs work-item filing per
class); guardrail row DERIVED via the D2 rules with the derivation shown
(no hand-assigned rows); admission + escalation notes imported by
citation from the guardrail contract; precedent pointer (the proven
manual pattern). No vendor scheduling detail. |

**Sanity Check:**

- `ls plugins/autonomy/reference/routines/*.md | wc -l` = 10
- Every leaf shows its derivation: `grep -lc 'derived'
plugins/autonomy/reference/routines/*.md | wc -l` = 10
- Every leaf names a cadence default: `grep -lc 'cadence'
plugins/autonomy/reference/routines/*.md | wc -l` = 10
- Vendor+fleet deny-list sweep exit 0

### Phase 3: Guided-setup routine slice [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/skills/setup/SKILL.md` | Modify | Routine slice
(D5): interview scheduling surfaces (CI cron, dev machine, self-run
infra, vendor-hosted preview) + budget posture; wire free defaults as
reviewable changes (CI-cron handler shape = CI-orchestration home role,
enabling settings = settings-as-code home role); advise paid/preview
surfaces with cost surfaced; DETECT-DIFF-RECONCILE existing org
schedulers and bots — a live dependency/stale bot IS a catalog-class
instance: record it in the binding (class token + surface), never stand
up a second mechanism. Binding home split by governance sensitivity
(stress-test F1, the WP5 D2 split): the routine→work-class mapping each
temporal signal's `signal.work_class` stamp derives from is ADMISSION
data — it lands in the WP5 security binding's `admission.classification`
(settings-as-code home, reviewable change, agent-unwritable), for
reconciled bots too; only non-security keys (cadence, enablement,
surface choice) land as the additive repo-local `routines` section of
the schema-versioned binding. A repo-local class source would be the
exact agent-writable bypass WP4's classification obligation forbids.
Research scheduling surfaces live at setup time (preview-stage moving
targets), never from this doc. |
| `plugins/autonomy/skills/setup/templates/routine-definitions.md` |
Create | Per-surface-class routine wiring shapes (CI-cron / local
scheduler / self-run / vendor-preview as marked examples), parameterized
by class token + cadence; every shape enqueues through the trigger
contract's temporal adapter — no direct execution path. |
| `plugins/autonomy/skills/setup/evals/evals.json` | Modify |
Routine-slice cases: surface interview, free-default wiring,
existing-bot reconciliation (record-don't-duplicate), paid-surface
advisory refusal-to-default. |
| `plugins/autonomy/README.md` +
`plugins/autonomy/.claude-plugin/plugin.json` | Modify | Capability list
+ description + minor version bump. |

**Sanity Check:**

- `/skill-quality:check` + `validate-evals` pass; `claude plugin
validate --strict` exit 0
- `grep -ci 'detect-diff-reconcile'
plugins/autonomy/skills/setup/SKILL.md` ≥ 2 (guardrail slice + routine
slice each state it)
- `grep -c 'temporal'
plugins/autonomy/skills/setup/templates/routine-definitions.md` ≥ 1
(queue-entry via the trigger contract)
- Fleet-name sweep exit 0

### Phase 4: Derivation demonstration + gates [DONE]

Acceptance probe: classify one NOVEL routine class end-to-end using only
the shipped mapping
rules (axes → guardrail row → prerequisites) — the acceptance criterion
that an org can
classify without contract changes; the chosen novel class MUST be an
untrusted-provenance
case (attacker-writable external input) so the probe exercises the D2
provenance axis and
the C5 reach the stress-test found missing, not just the easy C1–C3
path; record the worked
example in the PR body (not in the contract — it is evidence, not
normative text). Then the full gate roster
(`validate-plugins.sh`, `run-plugin-tests.sh`,
`validate-plugin-contracts.mjs`,
markdown/typos/lychee, `claude plugin validate --strict`, catalog
regen). Near-duplicate
audit statement: routines compose the WP4 temporal adapter + WP5 matrix;
deterministic checks
stay plain cron; no second scheduling or merge path created.

**Sanity Check:**

- Worked novel-class derivation present in the PR body
- All gate scripts exit 0; catalog in-sync
- Near-duplicate audit statement present in the PR body

## Blast radius

MEDIUM — one plugin's files; the catalog constrains WP7 and every
adopting org's background
maintenance, but all governance content is imported from WP4/WP5 by
citation, not redefined.
Fully git-revertible.

## Stress-test summary

Fresh-context plan review (WP6+WP7 batch): 10 findings, verdict
FIX-THEN-SHIP, WP6's share
folded — F1 (HIGH, cross-package): the routine→class mapping had no
security-surface home;
the only binding home named was the repo-local additive-section idiom,
which is exactly the
agent-writable class source WP4's classification obligation forbids →
the mapping lands in
the WP5 security binding's `admission.classification` (reconciled bots
included); only
cadence/enablement/surface stay repo-local. F3 (MED-HIGH): binary
DET/AGT mapping could not
classify hybrid classes (a v1 class among them), and two Phase-1 gates
were mutually
unsatisfiable (39 rows vs dead-code = 0) → D2 hybrid branch + explicit
`not-a-routine`
catalog flag for the seven pure-DET rows; gates reconciled. F4
(MED-HIGH): derivation
reached only C1–C3, silently breaking the novel-class acceptance
criterion for C4/C5 →
provenance and structural-blast axes added; the Phase 4 probe now must
exercise an
untrusted-provenance case. F8 (LOW) routes to WP5's work-classes leaf
(C1 permits
governed-queue writes, no repo mutation).

## Execution shape

Fully sequential 1 → 2 → 3 → 4 — leaves derive through Phase 1's rules;
the slice wires
Phase 2's classes; Phase 4 proves Phase 1's rules on a novel class.
Cross-package: after the
WP4 and WP5 implementation PRs.

| Phase | Surface | Basis |
|---|---|---|
| 1 | main-session | normative catalog + mapping-rule authoring |
| 2 | main-session | ten uniform leaves, derivation judgment per class |
| 3 | main-session | setup-skill judgment |
| 4 | main-session | derivation probe + gate runs |

## Open questions

- Fleet routine stand-up + existing-scheduler reconciliation execution —
/work-items backlog
  post-merge (Brief out-of-scope, trigger recorded).

## Decisions made (gate-passed)

| Decision | What it changes in the plan | Basis (evidence) |
|---|---|---|
| v1 roster confirmed at 10 classes (dead-code sweep stays out) | Phase
1 table + Phase 2 leaf count | D4's correction is evidence-backed
(RESEARCH-routine-catalog row 25: DET detect); D2's rule excludes DET
mechanically |
| Class tokens = kebab-case descriptive names (list in preamble) |
Phases 1–2 | Matches every shipped contract token set; one token per
catalog row |
| Hub-and-leaves under `reference/routines/` | Phase 1–2 paths | Same
layout decision as WP5's guardrail leaves (subdir per hub) |
| Cadence defaults: daily for queue-tending sweeps, weekly for the rest
| Phase 2 leaves | Proven manual patterns' observed frequency;
org-bindable values, not contract |
| Novel-class derivation as the Phase 4 acceptance probe | Phase 4 |
Brief acceptance criterion stated verbatim; cheapest mechanical proof of
the mapping rules |

## Handoff to implementation

### User-approval gates

- Any change to the D2 mapping rules or the v1 roster during
implementation → STOP
  (user-locked content).
- Any scope expansion beyond the four phases re-enters `/architect
review`.

### Execution shape ([EXEC-SHAPE] tagged)

Sequential 1→4, all main-session (table above). PLAN.md phase tags
advance in the same
commit as each phase.

### Mechanical work

Commit per phase on the implementation branch (suggest
`feat/autonomy-routines`); gates
re-run in full at Phase 4; PR body carries the derivation example +
near-duplicate audit
statement + this PLAN in a `<details>` block at close-out.

</details>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
## Summary

WP7 of the AI-adoption-ladder effort: the **autonomous-runner design
pack** — the
architect-ready design contract the T4 charter graduates into, as a
progressive-disclosure
hub (`plugins/autonomy/reference/runner.md`) with four leaves under
`reference/runner/`.
Design only: **zero build artifacts, the runner-execution home stays
unborn**, both T4 build
triggers are restated and neither is assumed fired.

- **Hub + seams (phase 1):** T4 charter imported verbatim in substance
(queue-contract split,
both build triggers, self-run-primary substrate stance with the
vendor-hosted
human-merge-gate cap, inherited constraints, anti-goals) with an
explicit no-build clause;
the two-layer composition spine (normative shape in seam vocabulary;
adopt-first binding
stance with reimplement-the-pattern as the named fallback, no library
pinned); the 8 seams
as the spine's complete interface set, each citing its shipped owning
contract or marked
runner-new; envelope fields `stop_reason` / `outcome` / `evidence` /
`resume_handle`
  resolved.
- **Lifecycle + escalation + topology (phase 2):** linear state model
`leased → executing → verifying → disposing → (escalated | complete)`
with per-transition
telemetry cited from the telemetry contract; thin per-item-PR launch
disposition with the
vendor-hosted cap restated on the disposition path; batched gated-merge
serialization as a
growth stage on its evidence trigger, binding a platform-native
merge-queue facility, never
reimplemented. Terminal-handoff escalation: four terminal outcomes
(`success` never files
escalation work), the two-family stop-criteria taxonomy (`runner-owned`
/ `agent-signaled`),
the deterministic two-step mapping (stop reason → outcome → event
class), two additively
registered runner event classes (`runner-needs-human`,
`runner-cap-exceeded`),
contract-default severities for all eight classes on the
`notice`/`attention`/`urgent` axis,
the urgent stop-reason override, 72h/one-bump acknowledgment knobs,
deferred mid-run
interrupt, and four research gaps carried as UNVERIFIED. Topology: four
ownership homes, the
launch backend set (one free self-run L2 container-class backend; L3
deferred with C5
dispatch blocked until bound, the floor cited from the work-classes
cell; cloud backends
advisory and vendor-hosted-capped), birth-time decisions USER-RESERVED.
- **Escalation-severity join + setup surface (phase 3):** the security
binding gains three
genuinely additive optional keys beside an untouched `escalation_routes`
—
`escalation_severity` (event class → severity),
`escalation_severity_routes` (severity →
route, personal-push legal at any tier, severity-first resolution with
the per-event route
as fallback), `escalation_ack` (staleness window, re-escalation cap).
The event-class
registry grows additively: `escalation_routes` keeps requiring only the
six inherited
guardrail classes (existing bindings validate unchanged) while the two
runner classes are
legal optional route keys. `check-security-binding.mjs` mirrors the
shapes and adds the one
semantic rule the schema cannot express (an unroutable severity-bound
event class fails
closed). SKILL.md gains the short runner note (427/500 lines),
evals.json case 29 covers
  the slice, plugin 0.6.0 → 0.7.0, catalog regenerated.

## Zero-build audit (phase 4)

Branch diff vs base: exactly 17 paths, all inside the four allowed
surfaces (reference/ pack,
skills/setup/, the two READMEs, the plugin manifest) plus the PLAN's own
phase tags. No new
`.sh`/`.mjs`/`.py` files. `grep -rci 'git init\|create the repo'` over
`reference/runner*` = 0.
No sentence in the pack reads as a build commitment; birth-time
decisions carry arbiter
USER-RESERVED.

## Additivity proof (schema)

Full security-binding fixture sweep before/after: 10 pass / 92 reject →
11 pass / 95 reject
with **zero flips** among the 102 pre-WP7 fixtures — the delta is
exactly the four new
escalation fixtures (valid binding passes; bad severity token,
unroutable event class, and
non-positive ack values each reject with their own finding). A runner
class bound through an
optional `escalation_routes` entry alone is routable, matching the
escalation contract.

## Near-duplicate audit

The pack cites the shipped contracts for every inherited obligation —
matrix cells, isolation
ladder levels, queue/lease dispatch, telemetry emission and join
attribute, return-accounting
capture, executor surface classes — and defines only runner-new content
(envelope fields,
state model, stop-criteria taxonomy, severity mapping, ownership seams).
The two deliberate
restatements are the stress-test F6 items: the vendor-hosted human-gate
cap on the lifecycle
disposition path and on the topology cloud-backend path.

## Gates

`validate-plugins.sh`, `generate-catalog --check`,
`validate-plugin-contracts.mjs` (vendor
deny-list over reference/), `run-plugin-tests.sh` (all pass or skip),
`claude plugin validate --strict`, markdownlint, typos, lychee over the
five-file pack
(66 links, 0 errors) — all green locally.

## Related

No linked issue (the T4 charter decision node #244 is already closed;
this package delivers
the design pack it chartered).

- #359 (WP4–WP7 plans, incl. this package's PLAN as merged)
- #244 (T4 autonomous-runner charter — the decision node this package
graduates)
- #239 (WP map: AI adoption ladder)
- PR #600 (WP6 routines — the routine-filed items that drain through
this runner's queue)
- PR #377 (WP5 guardrail matrix + security binding — the
escalation_routes home this PR
  extends additively)

<details>
<summary>PLAN</summary>

# ai-ladder-wp7-runner

## Brief

### TLDR

Runner design pack (T4 charter graduation): architect-ready design
contract for the
autonomous-drain runner — spine shape + 8 seam specs, lifecycle state
model, two-family
stop-criteria taxonomy with terminal-handoff escalation, matrix-derived
backend set, and the
topology seam map. Design only: build stays gated on the T4 triggers;
the runner-execution
home stays unborn.

### Goal

When the T4 build trigger fires, `/architect` and the build start from a
complete,
Boris-aligned design contract instead of a cold charter: every design
decision T4 graduated
(composition spine, isolation backends, lifecycle depth, escalation UX,
topology) is resolved
in contract vocabulary, every inherited constraint is imported
unchanged, and nothing in the
package commits, implies, or starts a build before the trigger earns it.

### Locked decisions

| # | Decision |
|---|---|
| D1 | Scope: architect-ready design contract docs ONLY — charter import
(T4 scope boundary, build triggers, substrate stance, inherited
constraints, anti-goals) + the five design decisions T4 graduated
(composition spine, multi-backend isolation, lifecycle depth, escalation
UX, topology) resolved + seam specifications. Capability-distribution
home. No code, no skeleton repo, no capability templates;
runner-execution home stays unborn until the T4 build trigger fires (WP1
D1); empirical spine validation waits for the trigger. |
| D2 | Composition spine, two-layer: normative contract fixes the spine
SHAPE in seam vocabulary — minimal composable orchestration, pluggable
sandbox-provider seam × pluggable agent-adapter seam,
autonomous/interactive split, the 8 seams as its interface set (absorb
discipline, no vendor names). Binding layer records adopt-first: adopt
the qualifying spine library as a build-stage dependency, re-verified at
trigger time (maintenance, license, seam fit); reimplement-the-pattern
is the named fallback on failed re-verification. |
| D3 | Isolation backends: the sandbox-provider seam is the normative
requirement; the launch backend set derives from matrix coverage of
trigger-admitted work — one free self-run L2 container-class backend
(default-deny egress) at launch; an L3 backend is deferred with trigger
= first C5-class work admitted to autonomous drain (fail-closed blocks
C5 dispatch until an L3 binding exists); paid/cloud backends advisory +
explicit opt-in. |
| D4 | Lifecycle: full state model in the contract — lease-claim →
execute in isolation → verification gates (matrix column) → disposition
per merge policy → escalate or complete, every transition emitting
telemetry per the telemetry contract. Launch disposition is thin:
per-item PR through the platform's native flow. Batched gated merge
serialization is a named growth stage with an evidence trigger (observed
concurrent auto-merge collisions), binding to a platform-native
merge-queue facility where one exists (availability verified at binding
time); never reimplement a built-in. |
| D5 | Escalation UX (WP5 D6 remainder, evidence:
RESEARCH-escalation-observability.md): terminal-handoff at launch —
every stop is a terminal state (success / gate-failed / needs-human /
cap-exceeded); the runner files a human-gated work item carrying the
evidence bundle (failure summary, run-transcript link, cost, trace link,
session-resume handle; human takeover = resume the persisted session).
Contract carries the two-family stop-criteria taxonomy: deterministic
runner-owned (turn/budget/wall-clock caps, execution error after
retries, refusal, verification-gate failure, isolation violation) and
judgment agent-signaled through the structured-output envelope
(ambiguity, design decision needing human judgment,
security/data-integrity event, unresolvable blocker, no-progress);
transient-recoverable retries with backoff, never escalates. Escalation
event classes gain a severity axis mapped to org-bound notification
fan-out (personal-push tier included as an org-bindable route);
acknowledgment on the item + stale-unacked re-escalation (severity bump,
capped) are org-bindable knobs. Mid-run interrupt (pause-for-human)
deferred with trigger: evidence that kill-and-resume loses material
cost/context; first-party pause/resume mechanisms verified available, so
later adoption needs no contract change. Escalation telemetry rides the
telemetry contract's custom namespace (no standard escalation signal
exists; candidate upstream contribution). |
| D6 | Topology: ownership seams pre-committed, shape at birth.
Design-pack docs stay in the capability-distribution home; at trigger
fire the runner-execution home is born owning the runner implementation
+ its build/release toolchain, consuming the contract docs (never
duplicating them). Security-sensitive runner bindings (level→substrate,
merge policy, escalation routes, admission) live in the settings-as-code
home — the runner reads its governance, never writes it (the
agent-writable-binding bypass-channel rationale generalizes to the
runner itself); non-security operational config is deployment-owned per
the hosting stance. Repo count, name, and implementation language
resolve at birth via the naming pass + the re-verified spine choice.
Absent settings-as-code home in an adopting org: layered resolution +
fail-closed on absent security binding applies; guided-setup names the
compliant path. |
| D7 | Imports, all unchanged: T4 charter core (queue-contract split,
both build triggers, self-run-primary substrate stance with hosted
human-gate cap, anti-goals); WP4 D6 executor surface classes — the
runner is one executor behind the invocation-adapter seam, adapters
untouched on swap; WP5 matrix columns + admission decision-table + caps,
promotion human-ratified / demotion automatic, L2+ fail-closed floor;
WP6 routine-filed items drain through the same queue (no routine-special
path); telemetry contract + trace propagation on every lifecycle
transition; return-accounting capture hooks at the runner's task
boundary; queue + lease reused from the work-item capability — no second
claim mechanism; the 8 seams as the spine's interface set. |

### Constraints

- Any fleet repo or vendor name in normative contract text is a defect;
vendor names appear
  only as marked examples and in binding docs.
- Zero build artifacts anywhere in the package: no code, no repo, no
templates; no sentence
  that reads as a build commitment.
- One queue, one lease, one escalation channel (the queue itself); no
second claim, dispatch,
  or escalation mechanism.
- Security-sensitive runner bindings never live where the runner (or the
agents it runs) can
  edit them.
- Fail-closed everywhere: absent L2 substrate, absent security binding,
unbound C5 work —
  block and name the compliant path, never degrade silently.
- No new cost by default; paid backends/surfaces advisory + explicit
opt-in with cost
  surfaced.
- Boris-alignment is the standing acceptance criterion (no
step-skipping, trust before
  scale).

### Acceptance criteria

- Design pack delivers, in contract vocabulary only: spine shape + 8
seam specifications,
lifecycle state model, two-family stop-criteria taxonomy,
severity/ack/re-escalation knob
  set, launch backend derivation, topology seam map.
- Zero build artifacts; runner-execution home unborn; T4 build triggers
restated verbatim;
  no build commitment anywhere in the text.
- WP5 D6's escalation-UX deferral — the last WP5-inherited cross-package
deferral — is
resolved by this package (mid-run interrupt stays deferred with its own
evidence trigger;
this pack resolves the inherited deferral, it does not claim nothing
remains deferred).
- Research gaps carried visibly, not laundered: CI-action-class
failure-reporting UNVERIFIED;
cross-vendor agent-needs-human signaling (agent-protocol /
agent-instruction-file guidance)
UNVERIFIED; managed-agent event names drift between stream and webhook
surfaces — bind
exact names at build from live docs; the deprecated approval-SDK is
never cited as living
  precedent.
- Boris check: step-3 trap honored (charter-not-build, trust before
scale); the 2→3 "let
Claude kick off Claude" cell stays inside the governed-queue audit
trail; the step-4 cell
is chartered, not built — closed loop via self-run substrate,
programmatic-scheduling
surface class, cost controls as caps/budget knobs, model selection as
cost tiers;
monitor-by-exception instantiated as terminal-handoff escalation; the
thread-post return
  metric is fed by return-accounting capture at the runner boundary.

### Captured assumptions

- The qualifying spine library remains maintained and license-compatible
at trigger time —
re-verify at trigger; reimplement-the-pattern is the named fallback
(D2).
- First-party pause/resume mechanisms (SDK callback pause +
defer/resume, managed idled
webhook) remain available — beta/moving surfaces; re-verify at build
before relying on
  them for the mid-run-interrupt growth stage.
- The work-item capability's race-safe lease + autonomous drain mode
remain the dispatch
  entrypoint (live-verified in a prior session).
- Personal-push notification routes presuppose org-bound channel
adapters (WP4) at binding
time; absent adapters, severity fan-out degrades to tracker-item-only —
never silently
  drops the escalation itself.

### Out-of-scope (deferred with triggers)

- Runner build itself — trigger: either T4 build trigger fires.
- L3 backend — trigger: first C5-class work admitted to autonomous
drain.
- Batched gated merge serialization — trigger: observed concurrent
auto-merge collisions.
- Mid-run interrupt escalation — trigger: evidence that kill-and-resume
loses material
  cost/context.
- Fleet materializations (runner stand-up, binding instances) —
/work-items backlog
  post-graduation.

### Deferred questions

- Exact seam interface tokens, envelope field names, and lifecycle state
tokens —
  `/architect` (with the plugin naming pass).
- Severity level names + default staleness window and re-escalation cap
values —
  `/architect`.
- Setup/probe mechanics per backend class (joins the WP5 D4 slice) —
`/architect`.
- Birth-time decisions: repo count/name/implementation language, spine
re-verification
outcome, exact managed-event name bindings — build stage, gated on the
T4 trigger;
  **arbiter: USER-RESERVED** (trigger firing is a user-ratified event).

## Plan

Recommendation-locked this round under the user's standing
pre-authorization. Token sets
(D-deferred, resolved here): lifecycle state tokens `leased` /
`executing` / `verifying` /
`disposing` / `escalated` / `complete` (terminal: `escalated`,
`complete`); terminal-outcome
tokens `success` / `gate-failed` / `needs-human` / `cap-exceeded`;
stop-criteria family
tokens `runner-owned` (deterministic) and `agent-signaled` (judgment,
via the
structured-output envelope field `stop_reason`); severity levels
`notice` / `attention` /
`urgent` (three levels — mapped to org-bound notification fan-out;
tracker item always,
channel notification per org route, personal-push tier org-bindable at
`urgent`); default
staleness window 72h with one re-escalation (severity bump, cap 1) —
org-bindable knobs.
Design-pack docs live under `reference/runner/` (hub
`reference/runner.md`).

Prerequisites: WP4 + WP5 + WP6 implementation PRs merged (this pack
imports their contracts
by citation). Design only — zero build artifacts; the runner-execution
home stays unborn.

### Phase 1: Charter import + spine and seam specs [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/runner.md` | Create | Design-pack hub: T4
charter imported verbatim-in-substance (queue-contract split
interactive-upstream / autonomous-downstream; BOTH build triggers
restated verbatim; self-run-primary substrate stance with the hosted
human-merge-gate cap; anti-goals) with an explicit no-build-commitment
clause; the two-layer composition spine per D2 — normative spine SHAPE
in seam vocabulary (minimal composable orchestration, pluggable
sandbox-provider seam × pluggable agent-adapter seam,
autonomous/interactive split) with the 8 seams as its interface set;
adopt-first binding stance recorded (qualifying spine library
re-verified at trigger time; reimplement-the-pattern the named
fallback); glance rule routing depth to the leaves. |
| `plugins/autonomy/reference/runner/seams.md` | Create | The 8 seam
specifications in contract vocabulary (invocation adapter,
structured-output envelope, queue+lease, isolation policy,
outcome-verification gate, merge-policy toggle, observability+cost,
session/resume+caps): each seam's obligation set, its already-shipped
owning contract where one exists (queue+lease → trigger-dispatch;
isolation policy → guardrails; observability+cost → telemetry; capture
at task boundary → return accounting) cited never restated, and the
runner-side interface tokens. Envelope field names resolved:
`stop_reason`, `outcome`, `evidence` (bundle ref), `resume_handle`. |

**Sanity Check:**

- `grep -ci 'build trigger' plugins/autonomy/reference/runner.md` ≥ 2
(both triggers restated)
- `grep -c 'no build' plugins/autonomy/reference/runner.md` ≥ 1
(commitment clause present)
- Seam count: `grep -cE '^## '
plugins/autonomy/reference/runner/seams.md` = 8
- Vendor+fleet deny-list sweep exit 0; lychee lane passes

### Phase 2: Lifecycle + stop-criteria + escalation leaves [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/runner/lifecycle.md` | Create | Full state
model per D4: `leased → executing → verifying → disposing → (escalated
\| complete)`, every transition emitting telemetry per the telemetry
contract (trace-linked); launch disposition thin (per-item PR through
the platform's native flow, merge disposition governed by the WP5 matrix
row — including the vendor-hosted human-gate cap whenever the executing
backend is vendor-hosted, restated here rather than inherited silently);
batched gated-merge serialization named as a growth stage with its
evidence trigger (observed concurrent auto-merge collisions) binding to
a platform-native merge-queue facility — never reimplemented. |
| `plugins/autonomy/reference/runner/escalation.md` | Create | D5
resolved: terminal-handoff at launch — every stop resolves to a terminal
outcome (`success`/`gate-failed`/`needs-human`/`cap-exceeded`); the
NON-SUCCESS outcomes file the human-gated work item with the evidence
bundle (failure summary, run-transcript link, cost, trace link,
`resume_handle`; human takeover = resume the persisted session), while
`success` completes through the normal disposition + return-accounting
path — no escalation item, no ack/re-escalation obligations, else every
healthy run would generate false escalation work (PR-review finding);
two-family stop-criteria taxonomy verbatim (runner-owned:
turn/budget/wall-clock caps, execution error after retries, refusal,
verification-gate failure, isolation violation; agent-signaled via
`stop_reason`: ambiguity, design decision, security/data-integrity
event, unresolvable blocker, no-progress); transient-recoverable retries
with backoff, never escalates; severity axis
(`notice`/`attention`/`urgent`) → org-bound fan-out with personal-push
as an org-bindable route; an EXPLICIT two-step mapping so severity
resolution is deterministic for every non-success stop (PR-review
findings — the severity join keys on event class, which runner outcomes
alone never named, and stop reasons in turn never named their outcome):
STEP ONE, stop reason → terminal outcome — runner-owned:
verification-gate failure → `gate-failed`; turn/budget/wall-clock cap →
`cap-exceeded`; execution error after retries, refusal, and isolation
violation → `needs-human`; agent-signaled (every `stop_reason` value:
ambiguity, design decision, security/data-integrity event, unresolvable
blocker, no-progress) → `needs-human`; STEP TWO, outcome → event class:
`gate-failed` → the guardrail contract's gate-failure event class;
`needs-human` and `cap-exceeded` → two runner escalation event classes
with the tokens `runner-needs-human` and `runner-cap-exceeded`,
registered ADDITIVELY alongside the guardrail contract's set — the Phase
3 schema edit extends the event-class registry (the enum
`escalation_routes` and `escalation_severity` keys validate against)
with these two tokens, and `check-security-binding.mjs` accepts
route/severity bindings for them like any guardrail class (default
severities `attention` and `notice`, org-bindable), and contract default
severities for EVERY inherited guardrail event class so severity fan-out
is defined with no `escalation_severity` binding at all (gate failure
`attention`, verification divergence `attention`, admission rejection
`notice`, demotion event `attention`, structural-class plan approval
`attention`, untrusted-provenance `urgent` — all org-bindable
overrides); an isolation violation or a `security/data-integrity` stop
reason carries an `urgent` severity override regardless of its event
class's default; the filed escalation item's envelope records the
resolved event class AND the originating stop reason;
acknowledgment-on-item + stale-unacked re-escalation (default 72h
window, one severity-bump re-escalation, both org-bindable); mid-run
interrupt deferred with its evidence trigger; escalation telemetry rides
the telemetry contract's custom-namespace mechanism — the exact
namespace token is read from the shipped telemetry contract at
implementation, not pinned here (candidate upstream contribution noted).
Research gaps carried verbatim (CI-action-class failure-reporting
UNVERIFIED; cross-vendor needs-human signaling UNVERIFIED; managed-agent
event-name drift — bind at build; deprecated approval-SDK never cited as
living precedent). |
| `plugins/autonomy/reference/runner/topology.md` | Create | D6:
ownership seam map — design pack in the capability-distribution home; at
trigger fire the runner-execution home is born owning implementation +
build/release toolchain, consuming contracts never duplicating;
security-sensitive runner bindings in the settings-as-code home (runner
reads its governance, never writes it); non-security operational config
deployment-owned; launch backend set per D3 (one free self-run L2
container-class backend; L3 deferred with the first-C5 trigger,
fail-closed until bound — the C5→L3 floor CITED from the WP5
work-classes matrix cell, never asserted independently here, per
stress-test F5; paid/cloud advisory + opt-in, and any cloud backend IS a
vendor-hosted executor: it forces the security binding's
`executor_class: vendor-hosted`, capping every merge row at human-gated,
per F6); birth-time decisions listed with arbiter USER-RESERVED (repo
count/name/language, spine re-verification, exact managed-event names).
|

**Sanity Check:**

- State tokens present: `grep -c 'disposing'
plugins/autonomy/reference/runner/lifecycle.md` ≥ 1
- `grep -c 'cap-exceeded'
plugins/autonomy/reference/runner/escalation.md` ≥ 1 and `grep -c
'stop_reason' …/escalation.md` ≥ 1
- `grep -ci 'UNVERIFIED'
plugins/autonomy/reference/runner/escalation.md` ≥ 2
- `grep -c 'USER-RESERVED'
plugins/autonomy/reference/runner/topology.md` ≥ 1
- Vendor+fleet deny-list sweep exit 0

### Phase 3: Setup note + WP5 escalation-route join [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/skills/setup/SKILL.md` | Modify | A short runner
note only: the design pack is bindable-when-born; setup records NOTHING
runner-specific until the trigger fires except escalation notification
routes (severity axis + personal-push tier as route options). No probe,
no wiring, no binding section for the unborn home. |
|
`plugins/autonomy/skills/setup/schemas/guardrails-security-binding.schema.json`
| Modify | The severity refinement is GENUINELY additive (stress-test F2
— the schema is `additionalProperties: false`, so an undeclared shape
would fail every severity-tiered binding): `escalation_routes` keeps its
existing event-class→route entries unchanged; three new OPTIONAL
top-level keys land beside it, modeling the event/severity join
explicitly — `escalation_severity` (event class → severity token, the
join: which severity each event class escalates at; contract defaults
per event class, org-bindable), `escalation_severity_routes` (severity
token → route, `notice`/`attention`/`urgent`, personal-push a legal
route value at any tier; route resolution = event class → its severity →
that severity's route, falling back to the event class's own
`escalation_routes` entry when no severity route is bound), and
`escalation_ack` (`staleness_window`, default 72h; `reescalation_cap`,
default 1). Old bindings validate unchanged — no major bump; that
resolution is now in-plan, not asserted. |
| `plugins/autonomy/skills/setup/scripts/check-security-binding.mjs` |
Modify | Semantic checks for the new keys: severity tokens ∈ the
three-level set; `escalation_ack` values positive; routability per event
class matches the resolution rule — an event class keyed in
`escalation_severity` is routable when its severity has an
`escalation_severity_routes` entry OR the event class has its own
`escalation_routes` entry (severity-only fan-out is legal, matching
severity-route-first resolution; a legacy per-event route is the
fallback, never a requirement), and only an event class with NEITHER is
flagged unroutable. |
| `plugins/autonomy/skills/setup/evals/evals.json` | Modify |
Escalation-route slice case (severity + personal-push binding recorded;
unborn-home refusal restated) — stress-test F7: every other WP adds eval
coverage for its SKILL.md change; no exemption here. |
| `plugins/autonomy/README.md` +
`plugins/autonomy/.claude-plugin/plugin.json` | Modify | Capability list
gains the runner design pack; the roadmap row stays trigger-gated
(build), now pointing at the pack; minor version bump. |

**Sanity Check:**

- `grep -ci 'unborn' plugins/autonomy/skills/setup/SKILL.md` ≥ 1
- README roadmap still carries the build trigger row (`grep -c 'build
trigger' plugins/autonomy/README.md` ≥ 1)
- Schema additivity: `grep -c 'escalation_severity_routes' …schema.json`
≥ 1 and a pre-WP7 fixture binding still passes
`check-security-binding.mjs` unchanged (no major bump proven, not
asserted)
- `/skill-quality:check` + `validate-evals` pass; `claude plugin
validate --strict` exit 0

### Phase 4: Zero-build audit + gates [DONE]

Acceptance probe (mechanical): the package introduces no
executable/runtime artifact — the
diff contains no new files outside `reference/`, `skills/setup/`,
README, and plugin
manifest; no new scripts; no repo-creation instruction anywhere. Then
the full gate roster
(`validate-plugins.sh`, `run-plugin-tests.sh`,
`validate-plugin-contracts.mjs`,
markdown/typos/lychee, `claude plugin validate --strict`, catalog
regen). Near-duplicate
audit statement: the pack cites the shipped contracts for every
inherited obligation and
defines only runner-new content.

**Sanity Check:**

- `git diff --name-only <base>` contains no path outside the four
allowed surfaces; no `*.sh`/`*.mjs` additions
- `grep -rci 'git init\|create the repo'
plugins/autonomy/reference/runner*` = 0
- All gate scripts exit 0; catalog in-sync; near-duplicate audit
statement in the PR body

## Blast radius

MEDIUM — docs-only within one plugin, but the pack pre-commits topology
and escalation
contracts the eventual build must honor; WP5's last cross-package
deferral resolves here.
Fully git-revertible; zero runtime surface by design.

## Stress-test summary

Fresh-context plan review (WP6+WP7 batch): 10 findings, verdict
FIX-THEN-SHIP, WP7's share
folded — F2 (HIGH): the escalation-route severity refinement touched the
WP5 security-binding
schema (`additionalProperties: false`) but Phase 3 listed neither the
schema nor the
validator, leaving the flagship severity/ack knob set unbindable, and
"additive, no major
bump" was asserted over what read as a value-shape change → both files
added to Phase 3 and
the refinement modeled as genuinely additive optional keys
(`escalation_severity_routes`,
`escalation_ack`), with a pre-WP7 fixture-passes gate proving no major
bump. F5 (MED): the
C5→L3 fail-closed gate now CITES the WP5 work-classes matrix cell (which
imports T3's C5/L3
row) instead of asserting the value. F6 (LOW-MED): the vendor-hosted
human-gate merge cap is
restated on the cloud-backend and disposition paths instead of silently
inherited. F7 (LOW):
the Phase 3 SKILL.md change gains eval coverage like every other WP. F9
(LOW): the
"last cross-package deferral" claim scoped to the WP5-inherited
deferral. F10 (LOW): the
escalation-telemetry namespace token is read from the shipped telemetry
contract at
implementation, not pinned in the plan. Scope discipline (zero build
artifacts, unborn
runner-execution home, USER-RESERVED birth decisions) audited clean.

## Execution shape

Fully sequential 1 → 2 → 3 → 4 — leaves depend on the hub's imported
charter; the setup note
cites Phase 2's escalation severity axis; Phase 4 audits the authored
tree. Cross-package:
after WP4+WP5+WP6 implementation PRs.

| Phase | Surface | Basis |
|---|---|---|
| 1 | main-session | charter import fidelity + seam-spec judgment |
| 2 | main-session | lifecycle/escalation normative authoring |
| 3 | main-session | minimal setup-skill touch |
| 4 | main-session | mechanical audit + gate runs |

## Open questions

- Birth-time decisions (repo count/name/language, spine re-verification,
managed-event
  names) — USER-RESERVED at trigger fire, restated in topology.md.

## Decisions made (gate-passed)

| Decision | What it changes in the plan | Basis (evidence) |
|---|---|---|
| Lifecycle tokens
`leased/executing/verifying/disposing/escalated/complete` | Phase 2 leaf
| D4's state model named each stage; kebab/lowercase matches shipped
token sets |
| Terminal outcomes `success/gate-failed/needs-human/cap-exceeded` |
Phase 2 leaf | D5 names exactly these four |
| Severity set `notice/attention/urgent`; 72h staleness, one
re-escalation | Phase 2 leaf | D5 requires a severity axis +
ack/re-escalation knobs with suggested defaults; three tiers is the
smallest set covering tracker-only / channel / push fan-out |
| Envelope fields `stop_reason/outcome/evidence/resume_handle` | Phases
1–2 | D5's evidence-bundle + resume requirements; smallest field set
carrying them |
| Hub-and-leaves under `reference/runner/` | Phase 1–2 paths | Same
layout as WP5/WP6 hubs |
| Escalation-route severity refinement = new OPTIONAL schema keys
(`escalation_severity` as the explicit event→severity join,
`escalation_severity_routes`, `escalation_ack`) beside an untouched
`escalation_routes`; schema + validator edits in Phase 3 | Phase 3 | WP5
D6 routes live there; stress-test F2 — only genuinely additive keys
avoid a major bump under `additionalProperties: false`, proven by the
pre-WP7 fixture gate; the join key answers the PR-review finding that
severity→route alone left route selection non-deterministic per event
class |

## Handoff to implementation

### User-approval gates

- Anything that reads as a build commitment or creates a runtime
artifact → STOP (D1
  user-locked; the zero-build audit is the backstop).
- Birth-time decisions stay USER-RESERVED — never resolved by
implementation.
- Any scope expansion beyond the four phases re-enters `/architect
review`.

### Execution shape ([EXEC-SHAPE] tagged)

Sequential 1→4, all main-session (table above). PLAN.md phase tags
advance in the same
commit as each phase.

### Mechanical work

Commit per phase on the implementation branch (suggest
`docs/autonomy-runner-design-pack` —
docs-type: the package is normative text only); gates re-run in full at
Phase 4; PR body
carries the zero-build audit output + near-duplicate audit statement +
this PLAN in a
`<details>` block at close-out.

</details>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
## Summary

Effort-end housekeeping for the AI-adoption-ladder effort: all seven
work packages are merged,
so the per-WP topic slices come out per the close-out convention
(publish the PLAN, then prune
the slice).

- WP4–WP7 PLANs were already spliced into their PR bodies at each
close-out (#372, #377,
  #600, #676).
- WP1–WP3 PLANs are now published the same way into #333, #343, and #356
(each merged PR body
gained a `PLAN (published at effort-end close-out)` details block), so
every slice's content
  has a durable home on its merged PR before the directory is removed.
- The WP map in `docs/topics/ai-adoption-ladder/index.md` now records
delivery PRs instead of
live slugs. The effort hub itself (index, design evidence, research
docs) stays.

No content is lost: every deleted PLAN.md is byte-recoverable from git
history and readable in
its merged PR's details block.

## Related

No linked issue (effort-end housekeeping; the WP map issue #239 is
already closed).

- #239 (WP map: AI adoption ladder — the effort this closes out)
- #333 / #343 / #356 (WP1–WP3 delivery PRs, PLANs published into their
bodies in this
  close-out)
- #372 / #377 / #600 / #676 (WP4–WP7 delivery PRs, PLANs published at
their own close-outs)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ger row (#698)

## Summary

D1 sweep for #353: the WP1–WP7 PLAN out-of-package notes (mined from the
merged PR bodies #333/#343/#356/#372/#377/#600/#676) contained 31
deferred notes; every note that named no tracking issue and had no
contract-recorded trigger now has a durable home:

- **README roadmap** gains four trigger-gated rows: fleet guardrail
materializations (WP5), fleet routine stand-up + existing-scheduler
reconciliation (WP6), vendor-binding capability templates (WP6), cost
enforcement / hard spend caps (WP5, 3→4 trigger).
- **Trigger register** gains the second-binding-consumer cross-repo
drift check (WP1).
- **`reference/return-accounting.md`** records the per-work-class
precision-graduation deferral beside its band-stability rule (WP3).
- **CHANGELOG.md created** for the autonomy plugin (first entry;
0.1.0–0.7.0 history stays in the WP PR bodies) + version bump to 0.7.1 —
starts the CHANGELOG-parity posture #663 gates on.

All other mined notes were already covered: tracked issues (#351, #352,
#694#697), contract-recorded triggers (telemetry immutable-ID,
return-accounting expansion/revisit), delivered sibling WPs, resolved
`/architect` questions, or the user-held dormant triggers (runner build
T4, L3 backend, merge serialization, mid-run interrupt, org-enablement,
cross-team). Full disposition table lands as a comment on #353 at close.

Documentation only — no contract semantics change.

## Related

- #685 (effort-end prune that moved the PLAN records into the merged PR
bodies)
- #663 (CHANGELOG-parity gate this PR's new CHANGELOG.md starts
satisfying)
- WP delivery PRs mined: #333, #343, #356, #372, #377, #600, #676

Closes #353

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 22, 2026
## Summary

The security-binding checker
(`plugins/autonomy/skills/setup/scripts/check-security-binding.mjs`)
recognized a probed host-credential path by **static structural shape**
— exact home-anchored / fixed-system forms. A static checker cannot know
an org's real credential locations, so for any such recognizer an
adversarial reviewer can always craft a plausible-but-invented path (an
invented home user, a mount that need not exist) whose failing read
proves nothing while real host credentials go unrecognized. That is the
non-convergence #549 documents.

This implements the operator-ratified **Option A** (deny-by-default;
issue
[comment](#549 (comment))):
a new `--credential-roots <path,path,...>` flag mirroring the
already-landed `--egress-hosts` seam, with unconfigured default =
deny-all. **Credential side only** — the egress seam (`--egress-hosts`,
`isNonExternalEgressHost`, special-use ranges) is untouched.

## Fix

- **New `--credential-roots` flag**, parsed exactly like
`--egress-hosts` (comma-separated, trimmed; roots are not case-folded at
parse time — `normalizeHostPath` folds case consistently at containment
time, since the checker never touches the probing host's filesystem and
POSIX case-sensitivity is not observable here).
- **Deny-by-default containment replaces the structural recognizer.** A
filesystem credential entry counts as credential-absence evidence only
when its recorded host-side expansion resolves — **lexically, `..`-safe,
filesystem-independent** (`normalizeHostPath` /
`pathUnderConfiguredRoot`) — under one of the configured trusted roots.
With **no roots configured, every filesystem credential entry is
untrusted and the level fails closed.** Membership under a configured
root is the sole test, dissolving the open-ended enumeration.
- **Bounded closed sets kept, needing no allowlist:**
cloud-metadata-endpoint routes (`isMetadataEndpoint`) and single-segment
well-known credential env tokens (`$GITHUB_TOKEN`, `$GH_TOKEN`). The
expansion-coherence guard (`credentialExpansionProblem`: rooted,
non-UNC, ephemeral-free, tail-consistent) is retained and now runs for
every entry kind, so a concrete entry's `host_expanded` must repeat it
verbatim.
- **Segment-boundary containment** (never a bare prefix — `/home/run`
does not accept `/home/runner-x`); UNC / `//`-prefixed forms are refused
so `//etc/credentials` cannot collapse onto a configured `/etc` root.
- **Docs updated** to the enforced behavior (no false guarantees):
`SKILL.md` and `templates/isolation-probe.md` describe credential =
deny-by-default while leaving the egress seam's accurate
allowlist-with-fallback description intact; `CHANGELOG.md` +
`plugin.json` bumped `0.8.0 → 0.9.0`.
- **Fixtures:** pruned the corpus that tested the removed structural
discrimination (which files/depths/anchors count); kept
`invented-home-user`, `host-home-mount`, and `dot-traversal` as
not-under-configured-root regressions; added unconfigured-roots
(fail-closed), UNC-rejection, and metadata-expansion-mismatch
regressions.

An independent security review (fresh-context subagent, rationale
withheld) surfaced a UNC-collapse containment bypass and a
concrete-entry coherence gap; both are fixed in the second commit with
regression fixtures.

## Verification


`plugins/autonomy/skills/setup/scripts/check-security-binding.fixtures.test.sh`
(the repo's graded-fixture harness):

```
All 357 checks passed (100 fixtures graded, 0 quarantined).
```

Baseline before the change was `All 394 checks passed (109 fixtures
graded)`; the delta is the pruned structural-discrimination corpus (−12
fixtures + transcripts) plus 3 new deny-by-default / hardening fixtures.
North-star behavior verified directly against the checker:

- **(a) unconfigured** — real `$HOME/.ssh/id_rsa` →
`/home/runner/.ssh/id_rsa` with no `--credential-roots` ⇒ UNPROVEN,
level fails closed (`no --credential-roots configured`).
- **(b) under root** — same path with `--credential-roots /home/runner`
⇒ `Security binding OK` (C1–C4 eligible).
- **(c) not under root** —
`/home/definitely-not-a-host-user/.ssh/id_rsa` with `--credential-roots
/home/runner` ⇒ `does not resolve under any configured
--credential-roots`.

No egress-side fixture changed behavior.

Closes #549

## Related

- #377 — merged carrier that landed the `--egress-hosts` seam this
mirrors
- #375 — decide-and-defer playbook
- #440 — report-only precedent
- Ratifying decision:
#549 (comment)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants