Skip to content

Fix: honest posture for plan-limited branch protection - #896

Merged
StarshipSuperjam merged 12 commits into
mainfrom
engine-809-696-unsupported-platform-posture
Aug 9, 2026
Merged

Fix: honest posture for plan-limited branch protection#896
StarshipSuperjam merged 12 commits into
mainfrom
engine-809-696-unsupported-platform-posture

Conversation

@StarshipSuperjam

@StarshipSuperjam StarshipSuperjam commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #809
Closes #696

A green mechanical check below shows this change conforms to the engine's rules — not that it is correct. What covers correctness is the behavioural steps in Review and your own read of the change; a green check is never a substitute for that. Your merge is the binding gate.

About those checks: only the one that runs when the change is proposed for merge can stop a risky merge — a check that ran while the change was still being written is early advice. Each check is itself proven against a deliberately broken example it must catch, so a passing check can't be one that quietly did nothing — but that proves the check works, not that this change is right. And a check that could not run leaves its area unverified.

Purpose

Give a repository whose GitHub plan cannot host branch protection an honest, operator-consented posture, so its first pull request stops being permanently red — while keeping the hard failure everywhere protection should work.

  • Engine fault: private-plan setup completes into permanently red CI #809: A private repo on a plan without branch rulesets gets HTTP 403 on the protection-rules read even with an admin token. First-run setup detected this, degraded, completed, and retired — but the standing engine/check/protection hard-failed the same 403 on every pull request forever, so the deployment could never merge green (live case: StarshipSuperjam/synology_nas). Per the operator-approved decision on the issue, the engine now records a deliberate, operator-consented "unsupported-platform" posture; the standing check reads it and reports an honest, non-blocking warning instead of hard-failing — and preserves the hard failure everywhere protection should be available but is missing/unreadable, and for any deployment with no recorded posture.
  • Engine fault: cmd_finalize's transport-error handler would mislabel a (currently unreachable) checkless-misuse invariant #696: ControlPlane.finalize raised BootstrapError (the module's transport-failure type) for a checkless-misuse invariant, so cmd_finalize mislabeled a construction bug as a connectivity failure. A distinct ControlPlaneMisuse exception is now caught separately with an honest internal-error message.

Impact: unblocks deployments on ruleset-less plans (they can merge with an honest, recorded acceptance) without weakening the gate on any repo that can actually be protected.

Scope

One PR carrying #809 (full-coherence: the CI check, the arrival banner, and boot's every-session message) and the small #696 tidy in the same file.

  • New top-level protection_posture block in engine.v1.json (status / reason / operator_login / recorded_on), a sibling of control_plane.
  • A single-homed 403 predicate protection_guard.platform_forbids_rulesets() (+ http_error_forbids_rulesets), shared by the standing check, boot, and bootstrap — softening triggers only on GitHub's genuine plan-limitation 403, excluding rate-limit / incident / permission 403s.
  • The standing check protection_guard.main() softens to a warning only when a posture is recorded and the live read carries that signature; a read that succeeds proves the plan can host rulesets, so a missing floor there stays hard (and nudges to clear a stale posture); malformed bodies fail closed.
  • A new permanent verb bootstrap.py accept-unprotected — the operator's explicit consent act (offered as a spoken phrase, run by the assistant, never a command the operator types), which refuses to record unless it re-verifies the plan-limitation 403 live. It doubles as the repair path for already-retired deployments.
  • Arrival re-classifies the plan-limitation 403 with an honest banner; cmd_status reports it calmly; boot gains a calm "unsupported" gate state handled at every consumer; apply/finalize clear a stale posture on success.
  • ControlPlaneMisuse (Engine fault: cmd_finalize's transport-error handler would mislabel a (currently unreachable) checkless-misuse invariant #696) + its cmd_finalize handler.

Change profile — the shape of this pull request at a glance:

  • Size: 10 files changed, roughly +950 / −40 lines (incl. tests and the regenerated knowledge graph).
  • Kinds of thing touched: 3 engine tools (protection_guard.py, bootstrap.py, boot.py) + their tests + test_module_manager.py, 1 floor-gate schema, 1 operator-copy template, 1 regenerated index (the knowledge graph).
  • Where: .engine/tools, .engine/schemas, .engine/templates, .engine/knowledge.
  • Shape: a standalone change (not a slice of a larger effort).

Impact: the accepted-platform state is recognized consistently across every surface an operator meets — CI, setup, the status verb, and boot — so none of them mislabels or nags about it.

Behaviors

The falsifiable capabilities this change delivers, each with the test that exercises it:

  • On a plan-limitation 403 with a recorded posture, the standing check emits a soft warning citing the accepted-on date — test_protection_guard.py::TestMainPostureSoftening.test_posture_plus_plan_limitation_403_softens.
  • A transient (rate-limit) 403, or a 403 with no posture, or a read-success with a missing floor, all stay hard — same class (..._without_posture_stays_hard, ..._transient_rate_limit_403_..._stays_hard, ..._read_success_floor_missing_..._stays_hard_and_nudges); a malformed list-of-non-dicts 200 fails closed without crashing (test_non_dict_list_200_fails_closed_without_crashing).
  • The accept-unprotected verb records only after re-verifying the plan-limitation 403, states the security consequence + team-mode implication, and refuses on a capable repo — test_bootstrap.py::TestAcceptUnprotected.
  • Arrival classifies the plan-limitation 403 as unsupported-platform with the honest banner — test_plan_limitation_403_routes_to_unsupported_platform_banner.
  • cmd_status reports an accepted posture calmly; cmd_finalize clears a stale posture on success; the verb-write is read back by the guard and validates against the schema — test_cmd_status_reports_an_accepted_posture_calmly, test_cmd_finalize_clears_a_stale_posture_on_success, test_end_to_end_verb_write_is_read_by_the_guard_and_matches_schema.
  • Boot reports the accepted state calmly (never the "gate is off" alarm or the misleading "no GitHub access" line) — test_boot.py::TestGovernanceAlarms.test_gate_unsupported_* + test_protected_branch_signal_unsupported_state.
  • A recorded posture survives an engine upgrade — test_module_manager.py::test_upgrade_preserves_a_recorded_protection_posture.
  • Engine fault: cmd_finalize's transport-error handler would mislabel a (currently unreachable) checkless-misuse invariant #696: cmd_finalize labels the checkless-misuse invariant as an internal error, not connectivity — test_cmd_finalize_labels_misuse_and_transport_failures_differently.

Out of scope

No standalone demo_809 walkthrough file; no change to how protection is applied where it CAN be.

  • The end-to-end regression is covered by tests in the existing files (including one that threads a single manifest through verb-write → guard-read → schema-validate) rather than a new demo_*.py, to avoid introducing a new catalogued surface (retire-manifest mirroring + catalog regens) for the same assurance. (Part of the spec-conformance note below.)
  • The verb still leaves an inert stale posture if an operator later configures protection manually via the GitHub UI instead of running apply/finalize — a housekeeping nit (the check passes clean regardless), not addressed here.

Impact: these are deliberate boundaries, recorded so they're not mistaken for gaps.

Risk

⚠️ This change weakens a safety guardrail: it narrows when the hard "is branch protection on?" check fails — so it requires the operator's guardrail-ack label at merge.

  • On a repo whose GitHub plan genuinely can't host branch rulesets and where the operator recorded acceptance, the standing check warns instead of failing. Everywhere else it fails exactly as before.
  • The safety case (verified by the security-governance review): the softened path is reachable only when the live rules-read returns a genuine plan-limitation 403 — and a repo whose plan can host rulesets returns 200 on that read, never entering the 403/posture branch at all. So a stale, mistaken, or hand-forged posture is provably inert on any protectable repo; it can never silence the gate where protection should work. Transient 403s (rate-limit/incident/permission) are excluded before the positive match, so they can't be ridden to a false soften.
  • Editing protection_guard.py (a hard-guarded floor file) trips the guardrail-weakening check → the PR carries one hard guardrail-ack; the engine.v1.json floor-gate schema edit is a soft disclosure surfaced in the same diff.

Impact: the consequence is bounded by a platform property the manifest cannot override (a capable repo's read returns 200), and gated behind your conscious guardrail-ack.

Validation

All mechanical checks green; the full self-test and CI validation both pass after the post-review fixes.

  • validate.py --suite CI → passed, no hard findings (the protection and hard-check-bite checks are not-applicable locally, as they need the live API / negative fixtures).
  • selftest.py → all self-tests pass (5130 tests).
  • The engine/check/protection not-applicable carve-out (.engine/_fixtures/protection/not-applicable.json) still reads true after the change (the new soft path needs a live 403, not CI-seedable) — re-derived and confirmed by the security-governance review.

Impact: an approver can rely on the suite being green and the carve-out re-derived; the branch-protection check itself only bites in real CI with a token.

Review

A thorough, cold, multi-lens review ran on the built change; no blocking findings, the safety case was verified, and the serious + minor findings it raised were fixed in-line before submission.

  • Depth: thorough — the operator's chosen level. A cold plan-review (four lenses) ran before any code, and a cold pre-submission review (five lenses: spec-conformance, an adversarial divergence hunt, security & governance, technical integrity, and usability) ran after validation went green. Each ran in a fresh context. Some lenses ran the changed code in throwaway copies to judge it (mocked GitHub, no writes to any real project).
  • What they found and how it was handled: no blocking findings. The security & governance lens verified, against an attacker assumed to control engine.json, that no forged/stale posture can soften a protectable repo. Serious findings — a malformed-body crash path in the check, a raw-command instruction in the arrival banner, a cmd_status that reported the accepted state as an unexplained failure, and a finalize path that didn't clear a stale posture — were all fixed in line. Minor findings (a coined-jargon term, a refusal message missing a next step, an inaccurate audit-trail note, small test-coverage gaps) were fixed. One deliberate design choice was flagged for your sign-off (below). No finding was left as tracked-and-proceed.
  • One choice worth your eye: for boot's every-session surfacing, I built a one-time honest completion confirmation and then silence for the accepted state (matching how a healthy gate is also silent after its own confirmation), rather than a persistent every-session line — because a line every session is itself a mild nag, and "stop nagging" was your stated priority. The accepted state stays queryable calmly any time via bootstrap.py status (now fixed to recognize it) and is warned on every pull request by the CI check. If you'd prefer a persistent calm dashboard line instead, say so and I'll add it.
  • Post-review divergence: between the reviewed baseline (9d05220) and submitted (f105c65), about 180 lines were added and 28 deleted or modified — a net change of roughly +152 lines, the large majority of it new tests and reworded operator copy; the actual logic delta is small (a fail-closed hardening in the protection check, the calm cmd_status recognition, and the finalize stale-posture clear). Because that touched logic in the safety-critical file, a scoped adversarial re-audit of just the post-review diff was run; it confirmed the logic fixes introduce no new divergence (the fail-closed hardening only makes the check more conservative; the softening matrix the first review verified is unchanged), and caught one consistency slip — a raw command left in the new cmd_status message — which was then reworded to a spoken phrase (a copy-only change, plus a new copy-parity test).
  • Rebased onto current main: after review, the branch was rebased onto origin/main to pick up the Engine fault: the Commons Clause may not bind an adopter without an .engine/-scoped NOTICE #635 Apache-2.0 relicense (PR Change: relicense the Engine to plain Apache-2.0 #898). The replay was clean with no source conflicts — the only overlap was the regenerated knowledge graph, resolved by regenerating it from the reconciled tree. The relicense is orthogonal to this change (license/comment lines, no interaction with the branch-protection logic), so the reviewed diff content is unchanged and no re-review of the change's logic was warranted; full self-test and CI validation were re-run green on the rebased tree.

Things you can confirm yourself:

  • Nothing here is something you can run yourself — there's no settled description for this project yet. (This is an engine fix, not a product-design work item, so there are no spec-derived operator steps to paste; the behavioral checks above are the engine's own account.)

Close-linkage pre-flight: clean — the pull request will close exactly #809 and #696 as declared (no stray closing keyword, no comma-trap).

Impact: this is the engine's own account of its review — your merge is the binding gate, and this change needs your guardrail-ack to go green.

Files of interest

Start with the check and the predicate; they carry the safety property.

  • .engine/tools/protection_guard.pyrecorded_posture(), the single-homed platform_forbids_rulesets(), and the softened main(). [hard guardrail-ack]
  • .engine/tools/bootstrap.py — the accept-unprotected verb, arrival cause classification, calm cmd_status, apply/finalize stale-posture clear, and ControlPlaneMisuse (Engine fault: cmd_finalize's transport-error handler would mislabel a (currently unreachable) checkless-misuse invariant #696).
  • .engine/schemas/engine.v1.json — the protection_posture block (floor-gate schema, soft disclosure).
  • .engine/tools/boot.py — the calm "unsupported" gate state across every consumer.
  • .engine/templates/control-plane-bootstrap.md — the operator-facing arrival banner.

Impact: the first two most determine whether the softening is safe; the rest determine whether the operator is told the truth on every surface.

AI involvement

Claude Code (Opus 4.8) planned, implemented, and orchestrated the cold reviews; the operator set the direction and the review depth, and holds the merge gate.

Impact: AI judgment is load-bearing on the design and the review synthesis; the guardrail-weakening consent (guardrail-ack) and the merge remain the operator's.

@StarshipSuperjam StarshipSuperjam changed the title Fix: honest posture for plan-limited branch protection (#809, #696) Fix: honest posture for plan-limited branch protection Aug 9, 2026
@StarshipSuperjam
StarshipSuperjam marked this pull request as ready for review August 9, 2026 19:09
StarshipSuperjam and others added 12 commits August 9, 2026 12:11
Draft claim for the build. Work follows as an ordered commit sequence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ControlPlane.finalize's checkless guard raised BootstrapError — the module's
transport-failure type — so cmd_finalize's `except BootstrapError` printed a
"couldn't reach GitHub … back online" message for what is actually a
construction bug. A future checkless finalize path reachable from the CLI would
inherit that misleading connectivity framing.

Introduce a distinct ControlPlaneMisuse (deliberately NOT a BootstrapError
subclass, so the transport handlers cannot swallow it) and catch it separately
in cmd_finalize with an honest internal-error message. Tighten the existing
guard test to the new type and add the first cmd_finalize-level test asserting
the two paths print distinct messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m posture (#809, read side)

On a GitHub plan that cannot host branch rulesets, the branch-rules read returns
403 and the standing engine/check/protection hard-fails every pull request
forever. Add the read side of the operator-consented fix:

- engine.v1.json: a new top-level `protection_posture` block (status,
  reason, operator_login, recorded_on), placed as a sibling of control_plane
  (which requires ruleset_mode — a plan-limitation deployment applied none).
- protection_guard.py: `recorded_posture()` and a single-homed
  `platform_forbids_rulesets()` predicate (shared by the check, boot, and
  bootstrap so the recognition lives in exactly one place). main() now softens
  to an honest, non-blocking WARNING only when BOTH a posture is recorded AND
  the live 403 carries GitHub's genuine plan-limitation signature — excluding
  rate-limit/incident/permission 403s. A read that succeeds proves the plan can
  host rulesets, so a missing floor there stays HARD (and nudges to clear a now
  stale posture); a non-list 200 fails closed. Factor a shared `_load_manifest`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… protection (#809, record side)

- New permanent `bootstrap.py accept-unprotected` verb: the operator's explicit
  consent act. It refuses to record unless it first re-verifies, live, that the
  branch-rules read returns GitHub's genuine plan-limitation 403 (never a
  200-capable repo, never a rate-limit/permission 403), reads the recording
  actor from GET /user (falling back to the manifest handle), stamps the date
  via the sanctioned clock, and states the security consequence — the gate is
  OFF, unreviewed work can merge — plus the team-mode implication. Survives
  retirement, so it is also the repair path for an already-retired deployment.
- Arrival: on a write 403, re-read the rules and, when the plan genuinely
  forbids rulesets, classify the cause as `unsupported-platform` with a new,
  correctly-worded banner (not the misleading "you don't administer this repo").
- `apply` clears a now-stale posture on success, closing the dormant-record
  window structurally.
- The 403 recognition lives once in protection_guard.platform_forbids_rulesets,
  shared by the check, arrival, and the verb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…809)

Boot's protected_branch_signal gains the calm "unsupported" state; the
HTTPError->plan-limitation recognition is single-homed in
protection_guard.http_error_forbids_rulesets (shared by the check and boot).
Render-site handling follows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nsumer (#809, boot coherence)

Completes boot's handling of the "unsupported" signal at all gate consumers so
an accepted plan-limitation deployment is never mislabeled or looped:

- Dashboard: never the "safety gate is off" alarm, never the misleading "no
  GitHub access — don't assume" line it showed every session before; explicit
  calm handling, not a silent fall-through.
- Setup-complete confirmation now also fires for "unsupported" (with honest
  wording — never "your gate is protecting it") and its marker clears the same
  way, so the deployment finishes onboarding once instead of looping.
- Cross-session relay pushes no alarm for the accepted state; present-marker
  stays the calm marker, never a warning.
- Tests: the fourth signal state (posture + genuine plan-limitation 403 only;
  transient 403 and no-posture stay unknown; read-success stays off) and the
  calm render behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…809)

- test_bootstrap: arrival apply() against a plan-limitation transport (the rules
  READ itself 403s with GitHub's upgrade message) degrades with the
  'unsupported-platform' cause and the accept-unprotected banner, persisting no
  control_plane marker.
- test_module_manager: a recorded protection_posture survives a version bump
  (operator config is preserved-not-overlaid), so a retired plan-limited
  deployment doesn't lose its accepted exception on upgrade.

Regression coverage lives in the existing test files rather than a standalone
demo, avoiding a new catalogued surface (retire-manifest mirroring + catalog
regens) for the same end-to-end assurance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Regenerated from the reconciled tree: refreshed tool fingerprints and the one
new dependency edge — bootstrap.py now uses moment (the sanctioned UTC clock)
in the accept-unprotected verb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cold pre-submission review (thorough) surfaced no blocking findings; the safety
case holds. Applying the serious + minor fixes it raised:

- protection_guard.main(): the malformed-body guard now checks elements are
  dicts (`all(isinstance(r, dict) ...)`), so a 200 returning a list of non-dicts
  fails closed instead of crashing missing_floor into an uncaught exception —
  matching the guard's own comment and boot's twin.
- bootstrap.py cmd_status: recognizes a recorded plan-limitation acceptance and
  reports it calmly (matching every other surface), not as an unexplained failure.
- bootstrap.py cmd_finalize: clears a now-stale posture on success too, since the
  check's stale-record nudge names finalize as a way to turn protection on.
- Arrival banner (copy + template): offers plain spoken phrases ("say **accept
  that my plan can't protect this branch**") instead of a raw CLI command, honoring
  the engine's "you never type a command yourself" promise; drops the coined
  "off-by-acceptance" jargon; the accept verb's refusals now name a concrete next
  step (a spoken phrase), not a dead end.
- engine.v1.json: corrects the posture record's audit-trail note — the operator
  fields are advisory "recorded by", not authenticated proof (a plan-limited repo
  has no enforced review on the recording PR).
- Tests: the non-dict-list guard, cmd_status calm recognition, cmd_finalize
  clear, and an end-to-end seam test (verb write -> guard read -> schema-valid).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fingerprint refresh for the edited protection_guard.py and bootstrap.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…py-parity test (#809)

The scoped re-audit of the post-review fixes flagged two items:
- cmd_status's calm message still ended with a raw `apply` command, the exact
  pattern the paired banner fix removes — reworded to the spoken phrase "say
  **turn my safety gate back on**".
- The degraded-unsupported-platform banner had no word-for-word template/fallback
  parity test (only before-you-approve did) — added one, so a future copy fix
  can't drift the two.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@StarshipSuperjam
StarshipSuperjam force-pushed the engine-809-696-unsupported-platform-posture branch from 91de02e to f105c65 Compare August 9, 2026 19:18
@StarshipSuperjam StarshipSuperjam added the guardrail-ack You add this to approve a change the engine flagged as weakening a built-in safety protection. label Aug 9, 2026
@StarshipSuperjam
StarshipSuperjam merged commit 71b9075 into main Aug 9, 2026
10 of 11 checks passed
@StarshipSuperjam
StarshipSuperjam deleted the engine-809-696-unsupported-platform-posture branch August 9, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

guardrail-ack You add this to approve a change the engine flagged as weakening a built-in safety protection.

Projects

None yet

1 participant