Fix: honest posture for plan-limited branch protection - #896
Merged
StarshipSuperjam merged 12 commits intoAug 9, 2026
Conversation
StarshipSuperjam
marked this pull request as ready for review
August 9, 2026 19:09
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
force-pushed
the
engine-809-696-unsupported-platform-posture
branch
from
August 9, 2026 19:18
91de02e to
f105c65
Compare
StarshipSuperjam
deleted the
engine-809-696-unsupported-platform-posture
branch
August 9, 2026 19:26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #809
Closes #696
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/check/protectionhard-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.ControlPlane.finalizeraisedBootstrapError(the module's transport-failure type) for a checkless-misuse invariant, socmd_finalizemislabeled a construction bug as a connectivity failure. A distinctControlPlaneMisuseexception 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.
protection_postureblock inengine.v1.json(status / reason / operator_login / recorded_on), a sibling ofcontrol_plane.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.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.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.cmd_statusreports it calmly; boot gains a calm"unsupported"gate state handled at every consumer;apply/finalizeclear a stale posture on success.ControlPlaneMisuse(Engine fault: cmd_finalize's transport-error handler would mislabel a (currently unreachable) checkless-misuse invariant #696) + itscmd_finalizehandler.Change profile — the shape of this pull request at a glance:
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)..engine/tools,.engine/schemas,.engine/templates,.engine/knowledge.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:
test_protection_guard.py::TestMainPostureSoftening.test_posture_plus_plan_limitation_403_softens...._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).accept-unprotectedverb 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.unsupported-platformwith the honest banner —test_plan_limitation_403_routes_to_unsupported_platform_banner.cmd_statusreports an accepted posture calmly;cmd_finalizeclears 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.test_boot.py::TestGovernanceAlarms.test_gate_unsupported_*+test_protected_branch_signal_unsupported_state.test_module_manager.py::test_upgrade_preserves_a_recorded_protection_posture.cmd_finalizelabels 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_809walkthrough file; no change to how protection is applied where it CAN be.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.)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
guardrail-acklabel at merge.protection_guard.py(a hard-guarded floor file) trips the guardrail-weakening check → the PR carries one hardguardrail-ack; theengine.v1.jsonfloor-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 (theprotectionandhard-check-bitechecks are not-applicable locally, as they need the live API / negative fixtures).selftest.py→ all self-tests pass (5130 tests).engine/check/protectionnot-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.
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, acmd_statusthat reported the accepted state as an unexplained failure, and afinalizepath 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.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.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 calmcmd_statusrecognition, and thefinalizestale-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 newcmd_statusmessage — which was then reworded to a spoken phrase (a copy-only change, plus a new copy-parity test).main: after review, the branch was rebased ontoorigin/mainto 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:
Close-linkage pre-flight: clean — the pull request will close exactly
#809and#696as 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-ackto go green.Files of interest
Start with the check and the predicate; they carry the safety property.
.engine/tools/protection_guard.py—recorded_posture(), the single-homedplatform_forbids_rulesets(), and the softenedmain(). [hard guardrail-ack].engine/tools/bootstrap.py— theaccept-unprotectedverb, arrival cause classification, calmcmd_status,apply/finalizestale-posture clear, andControlPlaneMisuse(Engine fault: cmd_finalize's transport-error handler would mislabel a (currently unreachable) checkless-misuse invariant #696)..engine/schemas/engine.v1.json— theprotection_postureblock (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.
github_client.get_json, not assumed.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.