From 32dcbe4b0d3c3e5558ae8b2f9d627d56fcc7c22b Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:45:07 -0700 Subject: [PATCH 01/12] Claim: honest posture for plan-limited branch protection (#809, #696) Draft claim for the build. Work follows as an ordered commit sequence. Co-Authored-By: Claude Opus 4.8 From 27558713a674e44eef0f3bfc9c1dd8f197281f2c Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:12:06 -0700 Subject: [PATCH 02/12] Fix: cmd_finalize labels the checkless-misuse invariant honestly (#696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .engine/tools/bootstrap.py | 19 +++++++++++++-- .engine/tools/test_bootstrap.py | 42 +++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/.engine/tools/bootstrap.py b/.engine/tools/bootstrap.py index b07e68f3..f6f46c37 100644 --- a/.engine/tools/bootstrap.py +++ b/.engine/tools/bootstrap.py @@ -116,6 +116,14 @@ class BootstrapError(Exception): """A GitHub read/transport failure during bootstrap — surfaced and degraded, never swallowed.""" +class ControlPlaneMisuse(Exception): + """A programmer-misuse invariant in the control plane — e.g. finalize called on a checkless instance, + whose whole job is to BIND the required checks a checkless instance would silently no-op. This is a + CONSTRUCTION bug, not a GitHub transport failure, so it is deliberately NOT a BootstrapError subclass: + the transport-error handlers that degrade a genuine network failure ("couldn't reach GitHub … back + online") must never swallow it and mislabel a bug as connectivity (#696).""" + + # ---- the protection-floor payload -------------------------------------------------------------- SOLO, TEAM = protection_guard.SOLO, protection_guard.TEAM # re-export the tier vocabulary (single home: protection_guard) @@ -873,8 +881,8 @@ def finalize(self, branch: str | None = None, announce=None) -> Result: already-bound branch reads 'already'. On success it re-emits the Actions-enablement reminder, because finalize is the moment the checks become load-bearing and they only run if Actions is enabled.""" if self.checkless: # finalize's whole job is to BIND the checks; a checkless instance would no-op them - raise BootstrapError("finalize must run on a non-checkless ControlPlane — it binds the checks a " - "checkless arrival deferred.") + raise ControlPlaneMisuse("finalize must run on a non-checkless ControlPlane — it binds the checks a " + "checkless arrival deferred.") branch = branch or boot.PROTECTED_BRANCH say = announce if announce is not None else (lambda text: print(text)) if not self.workflows_present_on(branch): @@ -1208,6 +1216,13 @@ def cmd_finalize(args) -> int: cp = ControlPlane(repo, token) try: result = cp.finalize(branch=args.branch) + except ControlPlaneMisuse as e: + # A construction bug, not a network failure — say so honestly rather than mislabeling it as + # connectivity (the transport handler below). Unreachable from this call site today (cmd_finalize + # always constructs a non-checkless ControlPlane); this keeps a future checkless finalize path honest. + print(f"Couldn't finalize branch protection — an internal error, not a connectivity problem: {e} " + "Please report this; nothing was changed.") + return 1 except BootstrapError as e: print(f"Couldn't reach GitHub to finalize branch protection ({e}). Nothing changed — try again " "when you're back online.") diff --git a/.engine/tools/test_bootstrap.py b/.engine/tools/test_bootstrap.py index 5ae004c0..94b623b6 100644 --- a/.engine/tools/test_bootstrap.py +++ b/.engine/tools/test_bootstrap.py @@ -921,13 +921,51 @@ def test_finalize_returns_degraded_when_the_bind_write_is_denied(self): def test_finalize_refuses_on_a_checkless_instance(self): # finalize's whole job is to BIND checks; on a checkless instance it would silently no-op them, so it - # raises loudly against that documented invariant rather than falsely reading 'already'. + # raises loudly against that documented invariant rather than falsely reading 'already'. It raises the + # distinct ControlPlaneMisuse (a construction bug), NOT BootstrapError (a transport failure) — so + # cmd_finalize's connectivity handler never swallows it and mislabels it as a network problem (#696). gh = AugmentGitHub(products=[_engine_ruleset(checkless=True)]) cp = bootstrap.ControlPlane(REPO, "tok", transport=_with_workflows(gh.transport, present=True), refresh_fn=lambda s: True, issues=FakeIssues(), tier=bootstrap.SOLO, checkless=True) - with self.assertRaises(bootstrap.BootstrapError): + with self.assertRaises(bootstrap.ControlPlaneMisuse): cp.finalize(branch="main") + # And it is NOT a BootstrapError, so a bare `except BootstrapError` cannot catch it. + self.assertNotIsInstance(bootstrap.ControlPlaneMisuse("x"), bootstrap.BootstrapError) + + def test_cmd_finalize_labels_misuse_and_transport_failures_differently(self): + # The CLI wrapper must tell a construction bug (ControlPlaneMisuse) apart from a genuine network + # failure (BootstrapError): the first is an honest "internal error", never the "back online" + # connectivity message the second gets. This is the #696 fix at the surface the operator sees. + import argparse + import contextlib + import io + from unittest import mock + + def _run(exc): + args = argparse.Namespace(repo=REPO, branch="main") + + class _CP: + def finalize(self, branch=None): + raise exc + + buf = io.StringIO() + with mock.patch.object(bootstrap, "boot") as mb, \ + mock.patch.object(bootstrap, "ControlPlane", lambda repo, token: _CP()), \ + contextlib.redirect_stdout(buf): + mb.gh_token.return_value = "tok" + rc = bootstrap.cmd_finalize(args) + return rc, buf.getvalue() + + misuse_rc, misuse_msg = _run(bootstrap.ControlPlaneMisuse("checkless instance")) + transport_rc, transport_msg = _run(bootstrap.BootstrapError("GitHub is unreachable")) + + self.assertEqual(misuse_rc, 1) + self.assertEqual(transport_rc, 1) + self.assertIn("internal error", misuse_msg.lower()) + self.assertNotIn("back online", misuse_msg) # never the connectivity framing + self.assertIn("back online", transport_msg) # the genuine transport message is preserved + self.assertNotEqual(misuse_msg, transport_msg) def test_checkless_augment_then_finalize_then_debootstrap_restores_the_product(self): # The reversal-integrity round trip: without the union marker, de_bootstrap would leave the From 3ed73b6e71e47059e263c7f4810baaf25ece87fd Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:16:53 -0700 Subject: [PATCH 03/12] Fix: standing protection check honors an accepted unsupported-platform posture (#809, read side) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .engine/schemas/engine.v1.json | 27 +++++ .engine/tools/protection_guard.py | 134 ++++++++++++++++++++-- .engine/tools/test_protection_guard.py | 149 +++++++++++++++++++++++++ 3 files changed, 301 insertions(+), 9 deletions(-) diff --git a/.engine/schemas/engine.v1.json b/.engine/schemas/engine.v1.json index 750a852a..43fa2ec9 100644 --- a/.engine/schemas/engine.v1.json +++ b/.engine/schemas/engine.v1.json @@ -96,6 +96,33 @@ } } }, + "protection_posture": { + "description": "A deliberate, operator-consented record that this repository's GitHub plan cannot host branch-protection rulesets at all, so the standing protection check reports it as an honest, non-blocking warning instead of hard-failing every pull request. Written ONLY by `bootstrap.py accept-unprotected`, which first re-verifies that the branch-rules API genuinely returns GitHub's plan-limitation 403 for this repo — never inferred, never written silently. Its mere presence never softens the gate: the standing check ALSO demands a live plan-limitation 403 at evaluation time, so on any repo whose plan can host protection (the read succeeds there) this record is inert and the check stays hard. Absent on every deployment whose platform can host protection (the default) — and that absence is exactly what keeps the check hard-failing when protection SHOULD be available but is missing or unreadable. The `operator_login`/`recorded_on` fields are an advisory audit breadcrumb of who ran the accept and when (the tamper-evident consent record is the reviewed pull request that adds this block), never authenticated proof of consent.", + "type": "object", + "additionalProperties": false, + "required": ["status", "reason", "operator_login", "recorded_on"], + "properties": { + "status": { + "description": "The recorded posture. unsupported-platform: the repository's GitHub plan cannot host branch rulesets (the branch-rules API returns GitHub's plan-limitation 403). An enum-of-one deliberately, leaving room for a future posture without reshaping the block.", + "enum": ["unsupported-platform"] + }, + "reason": { + "description": "A short plain-language reason recorded with the acceptance, for the operator reading engine.json later (for example 'This private repository's plan does not expose branch rulesets; the owner accepted running without the protection floor.'). Never empty.", + "type": "string", + "minLength": 1 + }, + "operator_login": { + "description": "The GitHub login whose token recorded this decision, read from GET /user at record time — an advisory 'recorded by', the who of the acceptance. It is the token owner, NOT authenticated proof of the operator's consent (the reviewed pull request carrying this block is that); downstream copy presents it as 'recorded by', never as governance evidence.", + "type": "string", + "minLength": 1 + }, + "recorded_on": { + "description": "The UTC date the operator accepted the posture (YYYY-MM-DD), which the standing warning and boot's calm line cite. Recorded via the engine's sanctioned clock, not a hand-rolled time idiom.", + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + } + } + }, "engine_identity": { "description": "The distinct, non-admin GitHub identity that authors the engine's commits and pull requests in TEAM mode, so the operator becomes the enforced code-owner reviewer — a change cannot merge without a second sign-off, and this identity (holding no admin) cannot itself weaken the ruleset. Recorded when the operator switches solo->team via the team-switch operation; absent in solo (the default) and in the engine's own home repository (which runs no first-run setup), so a manifest without it stays valid. Records only the PUBLIC identity — never a token or any secret, which stays in the operator's own credential store, so switching back to solo simply drops this record.", "type": "object", diff --git a/.engine/tools/protection_guard.py b/.engine/tools/protection_guard.py index 2a6f2d18..a19c7eb2 100644 --- a/.engine/tools/protection_guard.py +++ b/.engine/tools/protection_guard.py @@ -22,6 +22,7 @@ import json import os import sys +import urllib.error import urllib.parse sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # the sibling tools dir, for github_client @@ -41,6 +42,19 @@ _ENGINE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # .engine, two dirs up from tools/ +def _load_manifest(engine_dir: str | None = None) -> dict | None: + """The engine manifest (engine.json) as a dict, or None when it is absent/unreadable/not-an-object — the + single committed-manifest reader this module shares (resolve_tier and recorded_posture both call it, so + neither opens the file independently). Deliberately robust; never raises.""" + engine_dir = engine_dir if engine_dir is not None else _ENGINE_DIR + try: + with open(os.path.join(engine_dir, "engine.json"), encoding="utf-8") as fh: + manifest = json.load(fh) + except (OSError, ValueError): + return None + return manifest if isinstance(manifest, dict) else None # a list/string/number honors the never-raises contract + + def resolve_tier(engine_dir: str | None = None) -> str: """Resolve the repo's identity tier from its committed manifest — the SINGLE place the tier is read, so no ruleset/verify call site defaults it independently (a defaulted tier spread across sites is fail-open: an @@ -48,14 +62,9 @@ def resolve_tier(engine_dir: str | None = None) -> str: or an absent/unknown `identity` (the documented default; a malformed manifest is caught loudly by the engine.v1 schema check, an intentional team->solo downgrade by the weakening guard's identity detector — neither is this read's job). Returns TEAM only when the manifest explicitly records it. Deliberately robust; never raises.""" - engine_dir = engine_dir if engine_dir is not None else _ENGINE_DIR - try: - with open(os.path.join(engine_dir, "engine.json"), encoding="utf-8") as fh: - manifest = json.load(fh) - except (OSError, ValueError): + manifest = _load_manifest(engine_dir) + if manifest is None: return SOLO - if not isinstance(manifest, dict): - return SOLO # valid JSON that isn't an object (a list/string/number) -> honor the never-raises contract # TEAM is real only when the distinct identity that makes it real is ALSO recorded. This is the deadlock # guard: the team floor (1 required approval) is unsatisfiable without a distinct identity to author the PRs # (a sole owner cannot approve their own PR), so any team-WITHOUT-identity state — a first-run tier preference @@ -67,6 +76,79 @@ def resolve_tier(engine_dir: str | None = None) -> str: return SOLO +def recorded_posture(engine_dir: str | None = None) -> dict | None: + """The operator-consented protection posture recorded in engine.json, or None. Returns the posture dict + ONLY when it is well-formed and records the unsupported-platform status; anything else reads as no posture + (fail toward the HARD check, never toward a false soften). Written solely by `bootstrap.py + accept-unprotected` after it re-verifies the platform limitation; its mere presence never softens the gate — + the standing check also demands a live plan-limitation 403 (platform_forbids_rulesets), so a stale or + hand-forged posture is inert on any repo whose plan can host protection. Deliberately robust; never raises.""" + manifest = _load_manifest(engine_dir) + if manifest is None: + return None + posture = manifest.get("protection_posture") + if isinstance(posture, dict) and posture.get("status") == "unsupported-platform": + return posture + return None + + +def _forbidden_body(err: urllib.error.HTTPError) -> dict: + """Best-effort parse of an HTTPError's JSON body (GitHub returns an object with a `message`). Returns a + dict, or {} when the body is absent/unreadable/not-JSON. Never raises — a body we cannot read simply + can't match the plan-limitation signature, so the gate stays HARD.""" + try: + raw = err.read() + except Exception: # noqa: BLE001 — an unreadable error body must not crash the gate + return {} + if not raw: + return {} + try: + data = json.loads(raw.decode("utf-8", "replace") if isinstance(raw, (bytes, bytearray)) else raw) + except (ValueError, AttributeError, TypeError): + return {} + return data if isinstance(data, dict) else {} + + +def platform_forbids_rulesets(status: int, body, headers=None) -> bool: + """The SINGLE definition of "this repository's GitHub PLAN cannot host branch rulesets at all" — the one + 403 that is a permanent platform limitation rather than a transient or a permission failure. It is the + load-bearing gate on the whole unsupported-platform posture: the standing check softens to a warning, boot + reports it calmly, and the accept-unprotected verb records it, ONLY when this returns True. Every other 403 + — a rate-limit/secondary-limit throttle, a service incident, an ordinary not-admin or org-policy block — + stays a HARD, unresolved failure, because treating any of those as an accepted limitation would silence the + safety gate on a repo that genuinely CAN be protected (a repo whose plan hosts rulesets returns 200 for any + token; only writes 403 there). Shared by the standing guard, boot's signal, and bootstrap's arrival/verb so + the recognition lives in exactly one place and cannot drift between them. + + Grounded in GitHub's real response: on a plan that cannot host rulesets the rules read returns 403 with an + upgrade-oriented message ('Upgrade to GitHub Team/Enterprise to enable this feature', 'rulesets won't be + enforced on this private repository until you upgrade …'). A transient rate-limit 403 instead carries + rate-limit headers/message, excluded FIRST so an induced or coincidental throttle can never masquerade as a + plan limit. When the wording is unrecognizable we return False — the safe direction is a red gate the + operator can re-file, never a silently softened one.""" + if status != 403: + return False + msg = "" + if isinstance(body, dict): + msg = (body.get("message") or "").lower() + elif isinstance(body, str): + msg = body.lower() + hdrs = {} + try: + hdrs = {str(k).lower(): str(v).lower() for k, v in dict(headers or {}).items()} + except (TypeError, ValueError): + hdrs = {} + # Exclude the transient/abuse 403s first — these are NOT plan limitations, and are the inducible cases a + # forged posture would try to ride to a false soften. + if "retry-after" in hdrs or hdrs.get("x-ratelimit-remaining") == "0": + return False + if "rate limit" in msg or "secondary rate" in msg or "abuse" in msg: + return False + # Positive plan-limitation signature: an upgrade-to-a-paid-tier message about rulesets / this feature. + return "upgrade" in msg and any( + token in msg for token in ("ruleset", "team", "enterprise", "feature", "private repositor")) + + def missing_floor(rules: list, required_checks: list, *, tier: str = SOLO) -> list: """Pure evaluation of the protection floor against the EVALUATED per-branch rules (which already omit rules in evaluate/disabled mode), for the given identity `tier`. Returns the list of floor pieces not in force — empty @@ -156,22 +238,56 @@ def main() -> int: "message": "Branch protection was not checked here — no repository " "access token is available, which is normal on your own machine. The " "check that can actually block a bad merge runs in CI."}]) + posture = recorded_posture() # an operator-consented 'this plan can't host protection' acceptance, or None try: rules = get_json(f"/repos/{repo}/rules/branches/{urllib.parse.quote(branch, safe='')}", token, user_agent=UA) - except Exception as e: # token present but the API could not be read -> fail closed in CI + except urllib.error.HTTPError as e: + # The read failed with an HTTP status. Softens to an honest WARNING ONLY when BOTH the operator + # recorded an unsupported-platform posture AND this 403 genuinely carries GitHub's plan-limitation + # signature (platform_forbids_rulesets excludes rate-limit/incident/permission 403s). Any other + # failure — no posture, or a 403 that isn't a plan limit — stays HARD, exactly as before. + if posture and platform_forbids_rulesets(e.code, _forbidden_body(e), e.headers): + when = posture.get("recorded_on") or "an earlier date" + who = posture.get("operator_login") or "the operator" + return emit([{"severity": "soft", "location": None, + "message": f"Branch protection isn't available on this repository's GitHub plan, so " + f"the safety gate can't be enforced on '{branch}'. Running without it was accepted " + f"on {when} (recorded by {who}) — a known, accepted limitation, not a failure to " + f"fix. If your plan later supports branch rulesets, run `python " + f".engine/tools/bootstrap.py apply` and this note stops applying."}]) + return emit([{"severity": tier, "location": None, + "message": f"Branch protection could not be verified for '{branch}' " + f"({e}); treating it as not in force until confirmed."}]) + except Exception as e: # token present but the API could not be read (network, etc.) -> fail closed in CI return emit([{"severity": tier, "location": None, "message": f"Branch protection could not be verified for '{branch}' " f"({e}); treating it as not in force until confirmed."}]) + if not isinstance(rules, list): + # A 200 with an unexpected body is NOT a confirmation that protection is in force — fail CLOSED + # (mirrors boot's twin guard). Never let a garbage/partial 200 crash missing_floor into an unhandled + # exception and an ambiguous disposition. + return emit([{"severity": tier, "location": None, + "message": f"Branch protection could not be verified for '{branch}' (the rules " + "response was not in the expected form); treating it as not in force until confirmed."}]) missing = missing_floor(rules, REQUIRED_CHECKS, tier=identity_tier) if missing: + # The read SUCCEEDED, which proves this plan CAN host rulesets — so a posture recorded here is now + # stale (e.g. the plan was upgraded) and must NOT soften anything: this stays a HARD finding, and we + # nudge the operator to clear the stale record. This is the "should be available but missing" case the + # design preserves as red. + stale = "" + if posture: + stale = (" (This repository also carries a recorded 'protection unavailable on this plan' " + "acceptance, but its plan now supports branch protection — that record is stale; turning " + "protection on with the command above clears it.)") return emit([{"severity": tier, "location": None, "message": f"The protected-branch safety gate on '{branch}' is not fully " "in force: " + "; ".join(missing) + ". Until this is on, an unreviewed " "change could reach the protected branch. If the engine was just added to " "this project, run `python .engine/tools/bootstrap.py finalize` to turn its " "required checks on now that their workflows are on the branch; otherwise " - "complete the branch-protection setup you were handed, then re-run."}]) + "complete the branch-protection setup you were handed, then re-run." + stale}]) return emit([]) # protection is fully in force diff --git a/.engine/tools/test_protection_guard.py b/.engine/tools/test_protection_guard.py index ceb16771..345c72fe 100644 --- a/.engine/tools/test_protection_guard.py +++ b/.engine/tools/test_protection_guard.py @@ -9,6 +9,7 @@ import subprocess import sys import unittest +import urllib.error from unittest import mock HERE = os.path.dirname(os.path.abspath(__file__)) @@ -59,5 +60,153 @@ def test_url_quotes_a_slash_containing_branch(self): self.assertEqual(path, "/repos/o/r/rules/branches/release%2F1.0") +class TestPlatformForbidsRulesets(unittest.TestCase): + """The single load-bearing predicate: only GitHub's genuine plan-limitation 403 counts — every transient + or permission 403 stays a hard failure, so a stale/forged posture cannot ride a rate-limit blip to a soft.""" + + def test_plan_limitation_message_matches(self): + for msg in ("Upgrade to GitHub Team to enable this feature.", + "Upgrade to GitHub Enterprise to enable this feature.", + "Your rulesets won't be enforced on this private repository until you upgrade this " + "organization account to GitHub Team."): + self.assertTrue(protection_guard.platform_forbids_rulesets(403, {"message": msg}), msg) + + def test_rate_limit_403_is_excluded_by_message(self): + self.assertFalse(protection_guard.platform_forbids_rulesets( + 403, {"message": "You have exceeded a secondary rate limit. Please wait a few minutes."})) + self.assertFalse(protection_guard.platform_forbids_rulesets( + 403, {"message": "API rate limit exceeded for user."})) + + def test_rate_limit_403_is_excluded_by_header(self): + # A throttle whose body somehow said 'upgrade' is still excluded by its rate-limit headers. + self.assertFalse(protection_guard.platform_forbids_rulesets( + 403, {"message": "Upgrade to GitHub Team feature."}, {"Retry-After": "60"})) + self.assertFalse(protection_guard.platform_forbids_rulesets( + 403, {"message": "Upgrade to GitHub Team feature."}, {"X-RateLimit-Remaining": "0"})) + + def test_ordinary_not_admin_403_does_not_match(self): + self.assertFalse(protection_guard.platform_forbids_rulesets( + 403, {"message": "Resource not accessible by personal access token"})) + + def test_non_403_never_matches(self): + self.assertFalse(protection_guard.platform_forbids_rulesets(404, {"message": "Upgrade to GitHub Team"})) + self.assertFalse(protection_guard.platform_forbids_rulesets(500, {"message": "Upgrade to GitHub Team"})) + + def test_unreadable_body_does_not_match(self): + # An unrecognizable/empty body fails toward HARD (the safe direction), never a false soften. + self.assertFalse(protection_guard.platform_forbids_rulesets(403, {})) + self.assertFalse(protection_guard.platform_forbids_rulesets(403, None)) + + +class TestRecordedPosture(unittest.TestCase): + """The posture reader honors only a well-formed unsupported-platform record; anything else reads as None + (fail toward the hard check).""" + + def _write(self, tmp, manifest): + with open(os.path.join(tmp, "engine.json"), "w", encoding="utf-8") as fh: + json.dump(manifest, fh) + + def test_well_formed_posture_is_returned(self): + import tempfile + with tempfile.TemporaryDirectory() as tmp: + self._write(tmp, {"protection_posture": {"status": "unsupported-platform", + "reason": "x", "operator_login": "me", + "recorded_on": "2026-08-08"}}) + posture = protection_guard.recorded_posture(engine_dir=tmp) + self.assertIsNotNone(posture) + self.assertEqual(posture["operator_login"], "me") + + def test_absent_or_wrong_status_reads_as_none(self): + import tempfile + with tempfile.TemporaryDirectory() as tmp: + self._write(tmp, {"identity": "solo"}) + self.assertIsNone(protection_guard.recorded_posture(engine_dir=tmp)) + self._write(tmp, {"protection_posture": {"status": "something-else"}}) + self.assertIsNone(protection_guard.recorded_posture(engine_dir=tmp)) + + def test_missing_manifest_reads_as_none(self): + import tempfile + with tempfile.TemporaryDirectory() as tmp: + self.assertIsNone(protection_guard.recorded_posture(engine_dir=tmp)) + + +class TestMainPostureSoftening(unittest.TestCase): + """main()'s soft/hard decision: soften ONLY on (recorded posture AND a live plan-limitation 403); every + other outcome stays hard, and a read-success never softens regardless of a recorded posture.""" + + def _http_error(self, code, message="", headers=None): + import email.message + import io + hdrs = email.message.Message() + for k, v in (headers or {}).items(): + hdrs[k] = v + return urllib.error.HTTPError("https://api.github.com/x", code, message, hdrs, + io.BytesIO(json.dumps({"message": message}).encode())) + + def _run(self, *, posture, get_json_side_effect, missing=None): + captured = [] + with mock.patch.dict(os.environ, {"GITHUB_REPOSITORY": "o/r", "GITHUB_TOKEN": "t"}, clear=False), \ + mock.patch.object(repo_identity, "resolve_default_branch", return_value="main"), \ + mock.patch.object(protection_guard, "resolve_tier", return_value="solo"), \ + mock.patch.object(protection_guard, "recorded_posture", return_value=posture), \ + mock.patch.object(protection_guard, "missing_floor", return_value=(missing or [])), \ + mock.patch.object(protection_guard, "get_json", side_effect=get_json_side_effect), \ + mock.patch.object(protection_guard, "emit", side_effect=lambda f: captured.append(f) or 0): + protection_guard.main() + return captured[0] + + _POSTURE = {"status": "unsupported-platform", "reason": "plan can't host rulesets", + "operator_login": "owner", "recorded_on": "2026-08-08"} + + def _raise(self, err): + def _side(path, token, **kw): + raise err + return _side + + def test_posture_plus_plan_limitation_403_softens(self): + findings = self._run( + posture=self._POSTURE, + get_json_side_effect=self._raise(self._http_error(403, "Upgrade to GitHub Team to enable this feature."))) + self.assertEqual(len(findings), 1) + self.assertEqual(findings[0]["severity"], "soft") + self.assertIn("isn't available on this repository's GitHub plan", findings[0]["message"]) + self.assertIn("2026-08-08", findings[0]["message"]) + + def test_plan_limitation_403_without_posture_stays_hard(self): + findings = self._run( + posture=None, + get_json_side_effect=self._raise(self._http_error(403, "Upgrade to GitHub Team to enable this feature."))) + self.assertEqual(findings[0]["severity"], "hard") + self.assertIn("could not be verified", findings[0]["message"]) + + def test_transient_rate_limit_403_with_posture_stays_hard(self): + findings = self._run( + posture=self._POSTURE, + get_json_side_effect=self._raise(self._http_error(403, "You have exceeded a secondary rate limit."))) + self.assertEqual(findings[0]["severity"], "hard") + + def test_read_success_floor_missing_with_posture_stays_hard_and_nudges(self): + findings = self._run( + posture=self._POSTURE, + get_json_side_effect=lambda path, token, **kw: [], + missing=["a pull request is not required before merging"]) + self.assertEqual(findings[0]["severity"], "hard") + self.assertIn("stale", findings[0]["message"]) + + def test_non_list_200_fails_closed_hard(self): + findings = self._run( + posture=self._POSTURE, + get_json_side_effect=lambda path, token, **kw: {"unexpected": "object"}) + self.assertEqual(findings[0]["severity"], "hard") + self.assertIn("not in the expected form", findings[0]["message"]) + + def test_read_success_floor_present_passes_clean(self): + findings = self._run( + posture=self._POSTURE, + get_json_side_effect=lambda path, token, **kw: [{"type": "pull_request"}], + missing=[]) + self.assertEqual(findings, []) + + if __name__ == "__main__": unittest.main() From 1c9826b1a5c114518ef6f5adbcd7a5daf58383c8 Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:23:53 -0700 Subject: [PATCH 04/12] Fix: accept-unprotected verb + honest arrival banner for plan-limited protection (#809, record side) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .engine/templates/control-plane-bootstrap.md | 11 ++ .engine/tools/bootstrap.py | 175 ++++++++++++++++++- .engine/tools/test_bootstrap.py | 80 +++++++++ 3 files changed, 265 insertions(+), 1 deletion(-) diff --git a/.engine/templates/control-plane-bootstrap.md b/.engine/templates/control-plane-bootstrap.md index 7dc61b69..edc15a66 100644 --- a/.engine/templates/control-plane-bootstrap.md +++ b/.engine/templates/control-plane-bootstrap.md @@ -54,6 +54,17 @@ The authorization screen completed but the permission didn't save (some sign-in Protection is still off, so work can merge unreviewed. Let's try once more, or sign in again first. I'll keep reminding you until it's on. +## If it couldn't turn on — this plan can't host branch protection + +I couldn't turn on branch protection — this repository's GitHub plan doesn't offer the branch-protection +rules the safety gate needs (private repositories need GitHub Pro, Team, or Enterprise; public repositories +have them for free). This isn't a permission problem — your account administers the repository fine. +Protection is not active, so work can merge unreviewed. Two ways forward: upgrade this repository's plan (or +make it public) and run this again — or, if you're deliberately running without the gate, record that with +`python .engine/tools/bootstrap.py accept-unprotected`, which tells the engine to stop failing every pull +request over a limitation it can't fix and instead report the gate as off-by-acceptance. Until one of those, +I'll keep reminding you the gate is off. + ## Removing the engine — keep or remove your safety rule I set up a safety rule on your main branch that requires checks to pass and a pull request before anything diff --git a/.engine/tools/bootstrap.py b/.engine/tools/bootstrap.py index f6f46c37..9abf07a0 100644 --- a/.engine/tools/bootstrap.py +++ b/.engine/tools/bootstrap.py @@ -435,6 +435,17 @@ def _product_preserved(pre: dict, post: dict, added: dict) -> bool: "this). Protection is still off, so work can merge unreviewed. Let's try once more, or sign in " "again first. I'll keep reminding you until it's on." ), + "degraded-unsupported-platform": ( + "I couldn't turn on branch protection — this repository's GitHub plan doesn't offer the " + "branch-protection rules the safety gate needs (private repositories need GitHub Pro, Team, or " + "Enterprise; public repositories have them for free). This isn't a permission problem — your " + "account administers the repository fine. Protection is not active, so work can merge unreviewed. " + "Two ways forward: upgrade this repository's plan (or make it public) and run this again — or, if " + "you're deliberately running without the gate, record that with `python " + ".engine/tools/bootstrap.py accept-unprotected`, which tells the engine to stop failing every pull " + "request over a limitation it can't fix and instead report the gate as off-by-acceptance. Until one " + "of those, I'll keep reminding you the gate is off." + ), "applied": ( "Your safety gate is on. The main branch now requires a pull request, passing checks, and resolved " "review comments before anything merges — and it can't be force-pushed or deleted." @@ -505,6 +516,7 @@ def _product_preserved(pre: dict, post: dict, added: dict) -> bool: "degraded-not-admin": "If it couldn't turn on — you don't administer this repository", "degraded-org-policy": "If it couldn't turn on — your organization blocks the permission", "degraded-didnt-save": "If it couldn't turn on — the approval didn't save", + "degraded-unsupported-platform": "If it couldn't turn on — this plan can't host branch protection", "applied": "When it's on", "already": "When it was already on", "unverified": "When it couldn't be confirmed", @@ -677,6 +689,28 @@ def floor_missing(self, branch: str) -> list: # branch reads as fully in force and re-runs are idempotent), the frozen set in steady state. return protection_guard.missing_floor(data, self.required_checks, tier=self.tier) + def _plan_forbids_rulesets(self, branch: str) -> bool: + """True when a live read of the evaluated branch rules returns GitHub's genuine plan-limitation 403 — + the platform, not the operator, forbids rulesets on this repo. The recognition itself lives once in + protection_guard.platform_forbids_rulesets (shared with the standing check and boot); this only + performs the read and hands it the result, so arrival and the accept-unprotected verb classify the 403 + identically. A transport failure propagates as BootstrapError; any readable non-plan-limit response + (including a 200 — the plan can host rulesets — and an ordinary not-admin 403) is False.""" + status, body, headers = self._transport( + "GET", f"/repos/{self.repo}/rules/branches/{urllib.parse.quote(branch, safe='')}", None) + return protection_guard.platform_forbids_rulesets(status, body, headers) + + def user_login(self) -> str | None: + """The login of the token's owner (GET /user) — the advisory 'recorded by' actor for an accepted + unsupported-platform posture. None when unreadable; the caller falls back to the recorded handle.""" + try: + status, body, _ = self._transport("GET", "/user", None) + except BootstrapError: + return None + if status == 200 and isinstance(body, dict) and body.get("login"): + return body["login"] + return None + def engine_ruleset(self) -> dict | None: """The engine's own ruleset, if it already exists (matched by ENGINE_RULESET_NAME). Returns None when absent. Raises BootstrapError if the admin rulesets endpoint cannot be listed.""" @@ -836,7 +870,17 @@ def apply(self, branch: str | None = None, announce=None) -> Result: status, body = self._write_floor(own) if status >= 400: labels_ok = self.ensure_labels() - cause = self._forbidden_cause(body) if status in (401, 403) else "verify-failed" + if status in (401, 403): + # Distinguish "this plan can't host rulesets at all" (a platform limitation, not the + # operator's fault) from an ordinary not-admin/org-policy block, by re-reading the evaluated + # rules: on a plan that forbids rulesets the READ itself returns GitHub's plan-limitation 403. + try: + plan_limited = self._plan_forbids_rulesets(branch) + except BootstrapError: + plan_limited = False + cause = "unsupported-platform" if plan_limited else self._forbidden_cause(body) + else: + cause = "verify-failed" return Result("degraded", branch, missing or [], cause, labels_ok, mode=mode) # 4. Verify the floor is now actually in force (never assume the write took). An UNREADABLE @@ -1101,6 +1145,7 @@ def render(result: Result, copy: dict | None = None) -> str: "not-admin": "degraded-not-admin", "org-policy": "degraded-org-policy", "didnt-save": "degraded-didnt-save", + "unsupported-platform": "degraded-unsupported-platform", }.get(result.cause or "", "degraded-not-admin") msg = copy[key] if not result.labels_ok: @@ -1152,6 +1197,11 @@ def cmd_apply(args) -> int: "when you're back online.") return 1 print(render(result)) + if result.is_protected(): + # Protection is now in force, which proves this plan CAN host rulesets — so any recorded + # unsupported-platform posture is obsolete. Clear it (best-effort) rather than leave a stale record + # that a later transient could ride toward a softened gate. + _clear_protection_posture() return 0 if result.is_protected() else 1 @@ -1203,6 +1253,125 @@ def _persist_finalize_marker(marker) -> None: return +def _manifest_handle() -> str | None: + """The operator's own account handle recorded at first-run setup (engine.json `handle`), or None.""" + try: + with open(_engine_json_path(), encoding="utf-8") as fh: + data = json.load(fh) + except (OSError, ValueError): + return None + return data.get("handle") if isinstance(data, dict) else None + + +def _write_manifest(mutate) -> bool: + """Read engine.json, apply `mutate(data)` (which returns the new dict or None to abort), and write it back + atomically (temp-file + os.replace, so a crashed write never truncates the manifest). Returns True on a + completed write. Best-effort: any read/write failure returns False without raising.""" + path = _engine_json_path() + try: + with open(path, encoding="utf-8") as fh: + data = json.load(fh) + except (OSError, ValueError): + return False + if not isinstance(data, dict): + return False + new_data = mutate(data) + if new_data is None: + return False + try: + tmp = path + ".tmp" + with open(tmp, "w", encoding="utf-8") as fh: + json.dump(new_data, fh, indent=2) + fh.write("\n") + os.replace(tmp, path) + except OSError: + return False + return True + + +def _persist_protection_posture(posture: dict) -> bool: + """Record the operator-consented unsupported-platform posture into engine.json. Returns True on success.""" + def _set(data): + data["protection_posture"] = posture + return data + return _write_manifest(_set) + + +def _clear_protection_posture() -> None: + """Remove a now-stale unsupported-platform posture from engine.json (best-effort, no-op when absent).""" + def _drop(data): + if "protection_posture" not in data: + return None # nothing to do — abort the write + del data["protection_posture"] + return data + _write_manifest(_drop) + + +def cmd_accept_unprotected(args) -> int: + """Record the operator's DELIBERATE acceptance that this repository's GitHub plan cannot host branch + protection, so the standing check reports the gate as off-by-acceptance (an honest warning) instead of + hard-failing every pull request over a limitation the engine cannot fix. This is the operator's explicit + consent act — the engine offers it (arrival/boot banners), the operator asks for it, Claude runs it — and + it REFUSES to record unless it first re-verifies, live, that the branch-rules read genuinely returns + GitHub's plan-limitation 403. So it can never mint an exception on a repo whose plan can host protection. + Doubles as the repair path for an already-retired deployment (bootstrap.py survives retirement).""" + repo = _resolve_repo(args.repo) + token = boot.gh_token() + branch = args.branch + if not repo or not token: + print("Can't record this from here — no repository access is available. Run this where you're " + "logged in to GitHub (`gh auth login`).") + return 1 + cp = ControlPlane(repo, token) + # The load-bearing belt: re-read the evaluated branch rules and confirm the platform genuinely forbids + # rulesets before recording anything. + try: + status, body, headers = cp._transport( + "GET", f"/repos/{repo}/rules/branches/{urllib.parse.quote(branch, safe='')}", None) + except BootstrapError as e: + print(f"Couldn't reach GitHub to check this repository's branch protection ({e}). Nothing was " + "recorded — try again when you're back online.") + return 1 + if status == 200: + print("This repository's plan CAN host branch protection, so I won't record an exception. Turn the " + "safety gate on instead: `python .engine/tools/bootstrap.py apply`.") + return 1 + if not protection_guard.platform_forbids_rulesets(status, body, headers): + print("I couldn't confirm that this repository's PLAN is why branch protection is unavailable — the " + "branch-rules check didn't return GitHub's plan-limitation response (it may be a permission " + "problem, a rate limit, or a temporary error). I won't record an exception on a guess; nothing " + "was recorded. If protection should work here, complete the branch-protection setup instead.") + return 1 + # Genuine plan limitation confirmed. Record the operator-consented posture. + import moment # lazily — the arrival-critical module import stays minimal and 3.9-safe + login = cp.user_login() or _manifest_handle() or "unknown" + posture = { + "status": "unsupported-platform", + "reason": ("This repository's GitHub plan does not expose branch rulesets, so the protected-branch " + "safety gate cannot be enforced; the owner accepted running without it."), + "operator_login": login, + "recorded_on": moment.today_utc().isoformat(), + } + if not _persist_protection_posture(posture): + print("I confirmed the plan limitation but couldn't write the record to .engine/engine.json — nothing " + "was changed. Check the file is present and writable, then try again.") + return 1 + is_team = protection_guard.resolve_tier() == protection_guard.TEAM + print("Recorded: branch protection isn't available on this repository's GitHub plan, and you've accepted " + f"running without it (recorded {posture['recorded_on']}, by {login}). What this means: the " + "protected-branch safety gate is OFF — work can reach '" + branch + "' without a pull request, " + "passing checks, or your review, and nothing here technically prevents that. The standing check will " + "now report this as an accepted limitation (an honest warning) rather than failing every pull " + "request. If your plan later supports branch rulesets (upgrade it, or make the repository public), " + "run `python .engine/tools/bootstrap.py apply` to turn the gate on — that clears this record.") + if is_team: + print("Because this engine runs in TEAM mode, this is worth weighing carefully: the team floor exists " + "so a teammate's change can't merge without a distinct code-owner's review. Without branch " + "protection, that review is not technically enforced — teammates' unreviewed commits can reach " + "the protected branch.") + return 0 + + def cmd_finalize(args) -> int: """Post-merge: bind the engine's required checks that a brownfield arrival deliberately left off (#673) — now that the arrival pull request has merged and the engine's workflows are on the branch. Idempotent; @@ -1244,6 +1413,8 @@ def main(argv: list | None = None) -> int: sub.add_parser("status", help="report whether the safety gate is on (read-only)") sub.add_parser("apply", help="turn the safety gate on (idempotent)") sub.add_parser("finalize", help="after a brownfield arrival merges, turn on the engine's required checks") + sub.add_parser("accept-unprotected", + help="record that this plan can't host branch protection and you accept running without it") args = parser.parse_args(argv) # Resolve the default once for whichever verb runs (env -> recorded -> origin/HEAD -> "main"), so a repo # whose default is not `main` is reported and repaired on its real branch. @@ -1254,6 +1425,8 @@ def main(argv: list | None = None) -> int: return cmd_apply(args) if args.cmd == "finalize": return cmd_finalize(args) + if args.cmd == "accept-unprotected": + return cmd_accept_unprotected(args) parser.print_help() return 0 diff --git a/.engine/tools/test_bootstrap.py b/.engine/tools/test_bootstrap.py index 94b623b6..f5df7cb3 100644 --- a/.engine/tools/test_bootstrap.py +++ b/.engine/tools/test_bootstrap.py @@ -1000,5 +1000,85 @@ def test_union_added_carries_arrival_rules_and_finalize_checks(self): self.assertEqual(bootstrap._union_added(arrival, created), created) +class TestAcceptUnprotected(unittest.TestCase): + """The accept-unprotected verb records an operator-consented unsupported-platform posture — but ONLY after + it re-verifies live that the branch-rules read genuinely returns GitHub's plan-limitation 403, so it can + never mint an exception on a repo whose plan can host protection.""" + + def _run(self, *, rules_response, login="octocat", tier="solo"): + import argparse + import contextlib + import io + from unittest import mock + recorded = {} + + def transport(method, path, body=None): + if path == "/user": + return 200, {"login": login}, {} + return rules_response # (status, body, headers) for the /rules/branches read + + cp = bootstrap.ControlPlane("o/r", "tok", transport=transport, refresh_fn=lambda s: True, + issues=FakeIssues()) + args = argparse.Namespace(repo="o/r", branch="main") + buf = io.StringIO() + with mock.patch.object(bootstrap, "boot") as mb, \ + mock.patch.object(bootstrap, "ControlPlane", lambda repo, token: cp), \ + mock.patch.object(bootstrap, "_persist_protection_posture", + side_effect=lambda p: recorded.update(p) or True), \ + mock.patch.object(protection_guard, "resolve_tier", return_value=tier), \ + contextlib.redirect_stdout(buf): + mb.gh_token.return_value = "tok" + rc = bootstrap.cmd_accept_unprotected(args) + return rc, recorded, buf.getvalue() + + def test_records_on_genuine_plan_limitation_403(self): + rc, recorded, out = self._run( + rules_response=(403, {"message": "Upgrade to GitHub Team to enable this feature."}, {})) + self.assertEqual(rc, 0) + self.assertEqual(recorded.get("status"), "unsupported-platform") + self.assertEqual(recorded.get("operator_login"), "octocat") + self.assertRegex(recorded.get("recorded_on", ""), r"^\d{4}-\d{2}-\d{2}$") + self.assertIn("safety gate is OFF", out) # the security consequence is stated at consent time + + def test_refuses_when_the_read_succeeds(self): + # A repo whose plan CAN host rulesets returns 200 on the read — the belt refuses to record. + rc, recorded, out = self._run(rules_response=(200, [], {})) + self.assertEqual(rc, 1) + self.assertEqual(recorded, {}) # nothing recorded + self.assertIn("CAN host", out) + + def test_refuses_on_a_non_plan_limitation_403(self): + # An ordinary not-admin 403 is NOT a plan limitation — refuse rather than record on a guess. + rc, recorded, out = self._run( + rules_response=(403, {"message": "Resource not accessible by personal access token"}, {})) + self.assertEqual(rc, 1) + self.assertEqual(recorded, {}) + + def test_team_mode_states_the_team_implication(self): + rc, recorded, out = self._run( + rules_response=(403, {"message": "Upgrade to GitHub Team to enable this feature."}, {}), tier="team") + self.assertEqual(rc, 0) + self.assertIn("TEAM mode", out) + + def test_posture_persist_and_clear_roundtrip(self): + import json + import tempfile + from unittest import mock + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "engine.json") + with open(path, "w", encoding="utf-8") as fh: + json.dump({"engine_release": "1.0.0", "packages": {}, "identity": "solo"}, fh) + with mock.patch.object(bootstrap, "_engine_json_path", return_value=path): + ok = bootstrap._persist_protection_posture( + {"status": "unsupported-platform", "reason": "x", "operator_login": "me", + "recorded_on": "2026-08-08"}) + self.assertTrue(ok) + with open(path, encoding="utf-8") as fh: + self.assertEqual(json.load(fh)["protection_posture"]["operator_login"], "me") + bootstrap._clear_protection_posture() # apply-on-success clears the now-stale record + with open(path, encoding="utf-8") as fh: + self.assertNotIn("protection_posture", json.load(fh)) + + if __name__ == "__main__": unittest.main() From baa17994cc79cc50e5307affd8aba0311e7d77d1 Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:28:00 -0700 Subject: [PATCH 05/12] WIP: boot unsupported-platform signal + shared http_error predicate (#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 --- .engine/tools/boot.py | 36 ++++++++++++++++++++++--------- .engine/tools/protection_guard.py | 9 +++++++- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/.engine/tools/boot.py b/.engine/tools/boot.py index 2bf99b3b..734734dd 100644 --- a/.engine/tools/boot.py +++ b/.engine/tools/boot.py @@ -68,6 +68,7 @@ import subprocess import sys import unicodedata +import urllib.error import urllib.parse sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -288,19 +289,27 @@ def emit_refused_cursor_finding(*, spool_path: str | None = None) -> bool: def protected_branch_signal(repo: str | None, token: str | None, branch: str | None = None) -> tuple[str, str | None]: """The protected-branch governance signal, RELAYED from protection_guard (the control-plane's own - evaluation), in three honest states: - ("off", reason) -> the gate is NOT in force: a pinned governance alarm that OFFERS the fix. - boot stays read-only and only offers; the assistant runs the already-built, - idempotent one-click `bootstrap.py finalize` (bootstrap.ControlPlane.finalize — - apply plus the workflows-present guard, so it can't re-deadlock a freshly-arrived - repo) on the operator's consent — the shared repair-offer contract - (boot-session-start.md). - ("on", None) -> the gate fully bites: no alarm. - ("unknown", None) -> boot could not verify it (no token/repo/unreachable): a clear degraded line - that must NEVER read as a green all-clear. + evaluation), in four honest states: + ("off", reason) -> the gate is NOT in force: a pinned governance alarm that OFFERS the fix. + boot stays read-only and only offers; the assistant runs the already-built, + idempotent one-click `bootstrap.py finalize` (bootstrap.ControlPlane.finalize — + apply plus the workflows-present guard, so it can't re-deadlock a freshly-arrived + repo) on the operator's consent — the shared repair-offer contract + (boot-session-start.md). + ("on", None) -> the gate fully bites: no alarm. + ("unsupported", date) -> this repository's GitHub plan cannot host branch rulesets AND the operator + recorded a deliberate acceptance of that (protection_posture): a CALM, + non-alarm steady state, never "your gate is off (broken)" — the platform, + not a fault, is why the gate is off, and the operator already accepted it. + The second slot carries the accepted-on date. Requires BOTH the recorded + posture AND a live plan-limitation 403, so a stale/forged posture never + quiets the alarm on a repo whose plan can host protection. + ("unknown", None) -> boot could not verify it (no token/repo/unreachable/an unrecognized failure): + a clear degraded line that must NEVER read as a green all-clear. """ if not repo or not token: return "unknown", None + posture = protection_guard.recorded_posture() # an operator-consented plan-limitation acceptance, or None # The branch to probe is the AUTHORITATIVE default (env -> recorded -> origin/HEAD -> "main"), resolved at # call time so it self-heals a pre-recorded-key deployment; quoted so a malformed name can never redirect # this token-bearing request off its `/rules/branches/` path. @@ -316,6 +325,13 @@ def protected_branch_signal(repo: str | None, token: str | None, # check enforces. missing = protection_guard.missing_floor( rules, protection_guard.REQUIRED_CHECKS, tier=protection_guard.resolve_tier()) + except urllib.error.HTTPError as e: + # A recorded acceptance PLUS a live plan-limitation 403 is the calm off-by-acceptance state. Any other + # failure — no posture, or a 403 that isn't a genuine plan limit — stays the honest "unknown" degraded + # line, never a false all-clear. + if posture and protection_guard.http_error_forbids_rulesets(e): + return "unsupported", posture.get("recorded_on") + return "unknown", None except Exception: # noqa: BLE001 — unreachable / auth / malformed body -> unknown, never a false "on" return "unknown", None if missing: diff --git a/.engine/tools/protection_guard.py b/.engine/tools/protection_guard.py index a19c7eb2..0127c4a3 100644 --- a/.engine/tools/protection_guard.py +++ b/.engine/tools/protection_guard.py @@ -149,6 +149,13 @@ def platform_forbids_rulesets(status: int, body, headers=None) -> bool: token in msg for token in ("ruleset", "team", "enterprise", "feature", "private repositor")) +def http_error_forbids_rulesets(err: urllib.error.HTTPError) -> bool: + """platform_forbids_rulesets for the raising read model (get_json raises HTTPError unwrapped) — used by + the standing check's main() and by boot's protected_branch_signal so both branch on the genuine + plan-limitation 403 through exactly one recognition. Never raises.""" + return platform_forbids_rulesets(err.code, _forbidden_body(err), err.headers) + + def missing_floor(rules: list, required_checks: list, *, tier: str = SOLO) -> list: """Pure evaluation of the protection floor against the EVALUATED per-branch rules (which already omit rules in evaluate/disabled mode), for the given identity `tier`. Returns the list of floor pieces not in force — empty @@ -247,7 +254,7 @@ def main() -> int: # recorded an unsupported-platform posture AND this 403 genuinely carries GitHub's plan-limitation # signature (platform_forbids_rulesets excludes rate-limit/incident/permission 403s). Any other # failure — no posture, or a 403 that isn't a plan limit — stays HARD, exactly as before. - if posture and platform_forbids_rulesets(e.code, _forbidden_body(e), e.headers): + if posture and http_error_forbids_rulesets(e): when = posture.get("recorded_on") or "an earlier date" who = posture.get("operator_login") or "the operator" return emit([{"severity": "soft", "location": None, From e483094b812fe3064a0f8d50e72b1589ef38580a Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:39:13 -0700 Subject: [PATCH 06/12] Fix: boot reports plan-limited protection calmly across every gate consumer (#809, boot coherence) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .engine/tools/boot.py | 42 +++++++++++++++++++++++---- .engine/tools/test_boot.py | 58 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/.engine/tools/boot.py b/.engine/tools/boot.py index 734734dd..ab53f8e2 100644 --- a/.engine/tools/boot.py +++ b/.engine/tools/boot.py @@ -1773,12 +1773,26 @@ def render_dashboard(s: dict) -> str: # Gated on the safety gate being ON (#810 usability): "complete" must never appear beside a "your gate is off" # alarm — an un-gated repo has NOT finished setup. When the gate is off the confirmation is held back (and the # marker is NOT cleared, in _relay_lines), so it fires on a later start once the gate is on. + # "unsupported" (this plan can't host protection, accepted by the operator) is ALSO a completed-setup state: + # setup landed, the gate simply couldn't be turned on for a reason the operator accepted. It gets the + # one-time confirmation too — with HONEST wording (never "your gate is protecting it") — so an + # unsupported deployment isn't stuck showing setup-incomplete forever, and its marker clears below the same + # way (avoiding the every-session loop). After this one-time line it stays calm and silent — no alarm. setup_landed = s.get("setup_landed") - if setup_landed and setup_landed.get("present") and s.get("gate") == "on": - pinned.append( - "✅ **Setup is now complete.** Your setup changes have landed on your main branch, your safety gate " - "is protecting it, and your project is ready — that was the last onboarding step. From here it's " - "ordinary work.") + if setup_landed and setup_landed.get("present") and s.get("gate") in ("on", "unsupported"): + if s.get("gate") == "unsupported": + branch = s.get("protected_branch") or PROTECTED_BRANCH + pinned.append( + "✅ **Setup is now complete.** Your setup changes have landed on your main branch. Branch " + "protection isn't available on this repository's GitHub plan, and you accepted running without " + f"the safety gate on `{branch}` — so there was no gate to turn on, and that was the last " + "onboarding step. If you later upgrade the plan (or make the repository public), say **turn my " + "safety gate back on** and I'll enable it.") + else: + pinned.append( + "✅ **Setup is now complete.** Your setup changes have landed on your main branch, your safety " + "gate is protecting it, and your project is ready — that was the last onboarding step. From here " + "it's ordinary work.") # The engine-MECHANIC setup OFFER (eADR-0026, Slice 3): this engine builds a separate OWNED product checkout, # but this machine's path to that checkout is missing (the portable fork case — the committed slug travelled, @@ -1846,6 +1860,13 @@ def render_dashboard(s: dict) -> str: f"I couldn't verify your safety gate from here (no GitHub access), so **don't assume " f"`{s.get('protected_branch') or PROTECTED_BRANCH}` is protected** — confirm it before merging " f"anything important.") + elif s["gate"] == "unsupported": + # An accepted plan-limitation: the operator recorded that this repo's GitHub plan can't host branch + # protection. Deliberately NEITHER an alarm (the "off" branch) NOR the misleading "no GitHub access" + # degraded line (the "unknown" branch above) — it is a calm, accepted steady state, acknowledged once + # by the setup-complete confirmation above and otherwise silent here. Explicit so the state is handled, + # not left to fall through by accident. + pass # Engine findings NO LONGER pin a ⚠ here. A routine finding count is the engine's own housekeeping (the # operator's lowest priority in a deployed repo), so it renders only as a quiet facts line below and is @@ -2437,6 +2458,8 @@ def present_marker_line(s: dict) -> str: return "⚠ Your safety gate is off" # same noun as the dashboard + the unknown-gate marker below if s["gate"] == "unknown": return f"⚠ {PRESENT_MARKER}: couldn't verify the safety gate" + # "unsupported" is intentionally NOT a ⚠ here: an accepted plan-limitation is a calm steady state, so it + # falls through to the calm `▸ Project status` marker below rather than reading as a governance alarm. if s["refused"]: return f"⚠ {PRESENT_MARKER}: couldn't read where the project stands" if s["strand"]: # ranked after the governance alarms; a governance alarm still wins the marker @@ -2541,6 +2564,10 @@ def _pushed_alarms(s: dict) -> list: f"{RELAY_MARKER} the safety gate couldn't be verified (no GitHub access), so they shouldn't " f"assume `{s.get('protected_branch') or PROTECTED_BRANCH}` is protected — confirm before merging " f"anything important.")}) + elif s["gate"] == "unsupported": + # An accepted plan-limitation steady state — NOT a governance alarm, so nothing is pushed across + # sessions. Explicit (not a silent fall-through) so the intent is on the record. + pass if s["refused"]: alarms.append({"key": "refused", "value": True, "collapsible": False, "full": ( f"{RELAY_MARKER} the engine couldn't read where the project stands, so project status is " @@ -2722,7 +2749,10 @@ def _relay_lines(s: dict) -> list: # Clear only when the confirmation actually SHOWS (same gate-on condition render_dashboard uses), so a gate-off # session holds the marker rather than burning the one-time confirmation before the operator ever sees it. sl = s.get("setup_landed") - if sl and sl.get("present") and sl.get("main") and s.get("gate") == "on": + if sl and sl.get("present") and sl.get("main") and s.get("gate") in ("on", "unsupported"): + # "unsupported" clears the marker too: its one-time completion confirmation renders in the dashboard on + # this same condition (render_dashboard), so an accepted plan-limitation deployment finishes onboarding + # once and never loops the "setup landed, awaiting the gate" state. first_run_health.clear_first_run_marker(sl["main"]) # The broken-hooksPath offer rides this SAME single decide() call (#707/#708), like off_main/foreign_license — # it is NOT a pushed governance alarm (it renders only in the dashboard, at the top of the offer tier). It is diff --git a/.engine/tools/test_boot.py b/.engine/tools/test_boot.py index 61b912fc..2de2b3c5 100644 --- a/.engine/tools/test_boot.py +++ b/.engine/tools/test_boot.py @@ -1580,6 +1580,29 @@ def test_gate_on_is_silent(self): pack = self._pack_with(("on", None), (0, "u")) self.assertNotIn("safety gate", pack.lower()) + def test_gate_unsupported_is_calm_never_an_alarm_or_the_no_access_line(self): + # The accepted plan-limitation is a CALM steady state: never the "safety gate is off" alarm, and never + # the misleading "no GitHub access / don't assume" degraded line the old code showed every session. + dash = boot.render_dashboard(_signals(gate="unsupported", reason="2026-08-08")).lower() + self.assertNotIn("safety gate is off", dash) + self.assertNotIn("don't assume", dash) # the misleading unknown-line must NOT appear + self.assertNotIn("⚠", dash) # not a degraded/alarm framing + + def test_gate_unsupported_present_marker_is_calm(self): + # The first-line status marker stays the calm ▸, never a ⚠ — an accepted limitation is not an alarm. + self.assertNotIn("⚠", boot.present_marker_line(_signals(gate="unsupported", reason="2026-08-08"))) + + def test_gate_unsupported_setup_complete_line_is_honest(self): + # The one-time completion confirmation for an unsupported deployment must NOT claim the gate is + # protecting the branch; it states the plan can't host protection and the operator accepted running + # without it — and it fires (so the deployment isn't stuck showing setup-incomplete forever). + dash = boot.render_dashboard(_signals( + gate="unsupported", reason="2026-08-08", + setup_landed={"present": True, "main": "/tmp/marker"})) + self.assertIn("Setup is now complete", dash) + self.assertIn("isn't available on this repository's GitHub plan", dash) + self.assertNotIn("your safety gate is protecting it", dash) + def test_routine_findings_do_not_pin_or_relay_only_a_quiet_fact(self): # A routine (unmarked) finding count is the engine's own housekeeping: no ⚠ pin, no must-push relay — # it appears only as the quiet "Engine findings" facts line, folded into the whole-backlog total. @@ -1663,6 +1686,41 @@ def test_protected_branch_signal_three_states(self): self.assertEqual(boot.protected_branch_signal("o/r", "t"), ("unknown", None), f"a non-list body ({body!r}) must read unknown, never on") + def test_protected_branch_signal_unsupported_state(self): + # The fourth state: a recorded acceptance PLUS a live plan-limitation 403 is the calm "unsupported" + # steady state, carrying the accepted-on date — never a false all-clear, never the misleading "unknown" + # (no-GitHub-access) line, and never softened by the posture ALONE. + import email.message + import io + import json + import urllib.error + + def _http_error(code, message): + hdrs = email.message.Message() + return urllib.error.HTTPError("https://api.github.com/x", code, message, hdrs, + io.BytesIO(json.dumps({"message": message}).encode())) + + posture = {"status": "unsupported-platform", "recorded_on": "2026-08-08", "operator_login": "me"} + plan_msg = "Upgrade to GitHub Team to enable this feature." + # posture + genuine plan-limitation 403 -> ("unsupported", accepted-on date) + with mock.patch.object(boot.protection_guard, "recorded_posture", return_value=posture), \ + mock.patch.object(boot.protection_guard, "get_json", side_effect=_http_error(403, plan_msg)): + self.assertEqual(boot.protected_branch_signal("o/r", "t"), ("unsupported", "2026-08-08")) + # a transient rate-limit 403, even WITH a posture, stays "unknown" — never a false calm all-clear + with mock.patch.object(boot.protection_guard, "recorded_posture", return_value=posture), \ + mock.patch.object(boot.protection_guard, "get_json", + side_effect=_http_error(403, "You have exceeded a secondary rate limit.")): + self.assertEqual(boot.protected_branch_signal("o/r", "t"), ("unknown", None)) + # a genuine plan-limitation 403 with NO posture recorded stays "unknown" (never silently calm) + with mock.patch.object(boot.protection_guard, "recorded_posture", return_value=None), \ + mock.patch.object(boot.protection_guard, "get_json", side_effect=_http_error(403, plan_msg)): + self.assertEqual(boot.protected_branch_signal("o/r", "t"), ("unknown", None)) + # read succeeds but the floor is missing, even with a posture -> "off" (the plan clearly hosts rulesets) + with mock.patch.object(boot.protection_guard, "recorded_posture", return_value=posture), \ + mock.patch.object(boot.protection_guard, "get_json", return_value=[]), \ + mock.patch.object(boot.protection_guard, "missing_floor", return_value=["no pull request"]): + self.assertEqual(boot.protected_branch_signal("o/r", "t")[0], "off") + class TestTriagePressureRender(unittest.TestCase): """The render-only triage-pressure line (#403.2): boot renders it read-only from the COMPLETE open From e982ad6442f8d66ac8b2ee6759f4d21cda0bb99a Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:42:30 -0700 Subject: [PATCH 07/12] Test: arrival classification + upgrade preservation for the posture (#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 --- .engine/tools/test_bootstrap.py | 28 ++++++++++++++++++++++++++++ .engine/tools/test_module_manager.py | 21 +++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/.engine/tools/test_bootstrap.py b/.engine/tools/test_bootstrap.py index f5df7cb3..69fc99b5 100644 --- a/.engine/tools/test_bootstrap.py +++ b/.engine/tools/test_bootstrap.py @@ -245,6 +245,34 @@ def test_plain_403_routes_to_not_admin_banner(self): self.assertEqual(result.cause, "not-admin") self.assertIn("administer", bootstrap.render(result)) + def test_plan_limitation_403_routes_to_unsupported_platform_banner(self): + # A repo whose PLAN can't host rulesets: the evaluated-rules READ itself returns GitHub's plan-limitation + # 403 (not just the write). apply() degrades with the 'unsupported-platform' cause and a banner that + # points at accept-unprotected — never the misleading 'you don't administer this repository'. + upgrade = {"message": "Upgrade to GitHub Team to enable this feature."} + + def transport(method, path, body=None): + headers = {"X-OAuth-Scopes": "repo"} + if method == "GET" and path == f"/repos/{REPO}": + return 200, {"full_name": REPO}, headers + if path == f"/repos/{REPO}/rules/branches/main": + return 403, upgrade, headers # the READ 403s -> the plan can't host rulesets at all + if path == f"/repos/{REPO}/rulesets": + return 403, upgrade, headers + if method in ("POST", "PUT"): + return 403, upgrade, headers + return 404, None, headers + + result = bootstrap.ControlPlane( + REPO, "tok", transport=transport, refresh_fn=lambda s: True, + issues=FakeIssues()).apply(branch="main", announce=quiet) + self.assertEqual(result.status, "degraded") + self.assertEqual(result.cause, "unsupported-platform") + self.assertIsNone(result.marker) # a degraded arrival persists no control_plane marker + rendered = bootstrap.render(result) + self.assertIn("plan", rendered.lower()) + self.assertIn("accept-unprotected", rendered) # points at the verb, not "you don't administer this" + def test_fine_grained_403_then_refresh_retries_and_applies(self): # A fine-grained token (no scope header): the first write 403s, the refresh "grants" admin, the # retry succeeds -> applied, with exactly one engine ruleset created (no duplicate). diff --git a/.engine/tools/test_module_manager.py b/.engine/tools/test_module_manager.py index 950b604e..5f2ceda4 100644 --- a/.engine/tools/test_module_manager.py +++ b/.engine/tools/test_module_manager.py @@ -1195,6 +1195,27 @@ def test_upgrade_preserves_the_recorded_home_across_the_version_bump(self): self.assertEqual((engine or {}).get("home_repository"), "acme/engine-home") # preserved self.assertEqual((engine or {}).get("packages", {}).get("base"), "0.2.0") # but versions bumped + def test_upgrade_preserves_a_recorded_protection_posture(self): + # #809: an unsupported-platform posture is operator config (a top-level manifest key), so a version bump + # must carry it across unchanged — otherwise a retired plan-limited deployment (e.g. a private repo on a + # ruleset-less plan) would lose its accepted exception on upgrade and go permanently red again. + posture = {"status": "unsupported-platform", "reason": "plan can't host rulesets", + "operator_login": "owner", "recorded_on": "2026-08-08"} + with tempfile.TemporaryDirectory() as d: + live = os.path.join(d, "live") + os.makedirs(live) + release = module_manager._build_upgrade_release(os.path.join(d, "release")) + with module_manager._redirect_root(live): + module_manager._build_upgrade_fixture(live) + data = module_manager.module_coherence.load_engine_manifest() + data["protection_posture"] = posture # operator-recorded exception + module_manager._write_json(module_manager._engine_manifest_path(), data) + module_manager.upgrade(ref="v0.2.0", release_tree=release, + opener=lambda **k: {"number": 1}, backup=lambda *a, **k: {"ok": 1}) + engine = module_manager.module_coherence.load_engine_manifest() + self.assertEqual((engine or {}).get("protection_posture"), posture) # preserved verbatim + self.assertEqual((engine or {}).get("packages", {}).get("base"), "0.2.0") # versions still bumped + def test_upgrade_reasserts_the_foundation_gitignore_fence_and_keeps_operator_lines(self): # #409: the foundation fence is release-evolvable — an upgrade re-applies it (like the CODEOWNERS # re-render / CLAUDE.md floor merge), so a repo provisioned before/without it converges, and an From 9d05220ecda674581d5af588a8a033ee4df66cca Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sat, 8 Aug 2026 23:43:26 -0700 Subject: [PATCH 08/12] Chore: regenerate the knowledge graph (bootstrap -> moment edge) (#809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .engine/knowledge/graph.json | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.engine/knowledge/graph.json b/.engine/knowledge/graph.json index 4447603a..55700943 100644 --- a/.engine/knowledge/graph.json +++ b/.engine/knowledge/graph.json @@ -5413,7 +5413,7 @@ }, "slug": "engine.v1", "source": { - "fingerprint": "sha256:de28e10e8b3bf43c5ed4b48fb1503f94aceb6a27405a513491b3d911475e8371", + "fingerprint": "sha256:929eeae237ab8cae8482452891b77c9ba6ef390d708ea75506697a3edd4faeb2", "path": ".engine/schemas/engine.v1.json" }, "status": "active", @@ -6442,7 +6442,7 @@ }, "slug": "boot", "source": { - "fingerprint": "sha256:c4c3adcb7aac76e7fc31eb429b93af71f597a2e741bda3b4ad30243f86060c08", + "fingerprint": "sha256:6a754ca0e3c0cd1fe487ea38af1398127c09b5b3af2ea603d4fe181dcd2976f4", "path": ".engine/tools/boot.py" }, "status": "active", @@ -6507,6 +6507,7 @@ "imports": [ "tool:boot", "tool:github_client", + "tool:moment", "tool:protection_guard", "tool:repo_identity", "tool:telemetry", @@ -6519,7 +6520,7 @@ }, "slug": "bootstrap", "source": { - "fingerprint": "sha256:ff8d1d43e9a1b8b3cb9311f27c731c9ba45e655d31d5aa9d64f5d12a36fb0018", + "fingerprint": "sha256:420619f6800fdaae981d5739d8240f70d2df2736ee854081fb031ad1395bbfb0", "path": ".engine/tools/bootstrap.py" }, "status": "active", @@ -9876,7 +9877,7 @@ }, "slug": "protection_guard", "source": { - "fingerprint": "sha256:6a295eeb90182549d278f571c819beab648884d851a0da544a6c38f2e703e046", + "fingerprint": "sha256:c42392cb49f6de8386e237f61176cae20f1f4542be09da048b04e0cce8c51159", "path": ".engine/tools/protection_guard.py" }, "status": "active", @@ -11048,7 +11049,7 @@ }, "slug": "test_boot", "source": { - "fingerprint": "sha256:bce831aab02cb0e08ff2c99ea1292bf883543fef902b153f7ce13fddd7e7d447", + "fingerprint": "sha256:75f7a44224bbe899146dff7c7d7bd4ca82336608c052234254015486fff98b78", "path": ".engine/tools/test_boot.py" }, "status": "active", @@ -11129,7 +11130,7 @@ }, "slug": "test_bootstrap", "source": { - "fingerprint": "sha256:0a66d453ecbb331ad47bb2977ca5748b8944746e768ee78d193bb474bb529dcc", + "fingerprint": "sha256:4ca36e76de33725b9a343f13d0f66b219831c23053c95ace28b8b41beb66a188", "path": ".engine/tools/test_bootstrap.py" }, "status": "active", @@ -12709,7 +12710,7 @@ }, "slug": "test_module_manager", "source": { - "fingerprint": "sha256:61b29b5b0ffb5554a4365523140a20c2a47ccc314468801d864dac6c2b2f11d5", + "fingerprint": "sha256:f525e7902646db4a3f924a77ebeed91ad172488c8cf8a3104762814e30b2ad8e", "path": ".engine/tools/test_module_manager.py" }, "status": "active", @@ -13128,7 +13129,7 @@ }, "slug": "test_protection_guard", "source": { - "fingerprint": "sha256:78a61b46043bb12c98da57fa104f77ba209e8367f909d7fc1ca6f0f790a6577b", + "fingerprint": "sha256:4a26b23c09edc804d27b12b68dcf1f66f0a963e608552c013d357d0de46c51e6", "path": ".engine/tools/test_protection_guard.py" }, "status": "active", From d8d710f7669a29bb7883ff92ac817f3dd528c35e Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:55:48 -0700 Subject: [PATCH 09/12] Fix: address pre-submission review findings (#809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .engine/schemas/engine.v1.json | 2 +- .engine/templates/control-plane-bootstrap.md | 9 +- .engine/tools/bootstrap.py | 51 ++++++--- .engine/tools/protection_guard.py | 7 +- .engine/tools/test_bootstrap.py | 108 ++++++++++++++++++- .engine/tools/test_protection_guard.py | 15 +++ 6 files changed, 169 insertions(+), 23 deletions(-) diff --git a/.engine/schemas/engine.v1.json b/.engine/schemas/engine.v1.json index 43fa2ec9..9bf5990f 100644 --- a/.engine/schemas/engine.v1.json +++ b/.engine/schemas/engine.v1.json @@ -97,7 +97,7 @@ } }, "protection_posture": { - "description": "A deliberate, operator-consented record that this repository's GitHub plan cannot host branch-protection rulesets at all, so the standing protection check reports it as an honest, non-blocking warning instead of hard-failing every pull request. Written ONLY by `bootstrap.py accept-unprotected`, which first re-verifies that the branch-rules API genuinely returns GitHub's plan-limitation 403 for this repo — never inferred, never written silently. Its mere presence never softens the gate: the standing check ALSO demands a live plan-limitation 403 at evaluation time, so on any repo whose plan can host protection (the read succeeds there) this record is inert and the check stays hard. Absent on every deployment whose platform can host protection (the default) — and that absence is exactly what keeps the check hard-failing when protection SHOULD be available but is missing or unreadable. The `operator_login`/`recorded_on` fields are an advisory audit breadcrumb of who ran the accept and when (the tamper-evident consent record is the reviewed pull request that adds this block), never authenticated proof of consent.", + "description": "A deliberate, operator-consented record that this repository's GitHub plan cannot host branch-protection rulesets at all, so the standing protection check reports it as an honest, non-blocking warning instead of hard-failing every pull request. Written ONLY by `bootstrap.py accept-unprotected`, which first re-verifies that the branch-rules API genuinely returns GitHub's plan-limitation 403 for this repo — never inferred, never written silently. Its mere presence never softens the gate: the standing check ALSO demands a live plan-limitation 403 at evaluation time, so on any repo whose plan can host protection (the read succeeds there) this record is inert and the check stays hard. Absent on every deployment whose platform can host protection (the default) — and that absence is exactly what keeps the check hard-failing when protection SHOULD be available but is missing or unreadable. The `operator_login`/`recorded_on` fields are an advisory audit breadcrumb of who ran the accept and when — never authenticated proof of consent (a repository whose plan cannot host branch rulesets has no enforced review on the pull request that records this, so the field is a recorded 'by', not a verified one).", "type": "object", "additionalProperties": false, "required": ["status", "reason", "operator_login", "recorded_on"], diff --git a/.engine/templates/control-plane-bootstrap.md b/.engine/templates/control-plane-bootstrap.md index edc15a66..d1f360e3 100644 --- a/.engine/templates/control-plane-bootstrap.md +++ b/.engine/templates/control-plane-bootstrap.md @@ -60,10 +60,11 @@ I couldn't turn on branch protection — this repository's GitHub plan doesn't o rules the safety gate needs (private repositories need GitHub Pro, Team, or Enterprise; public repositories have them for free). This isn't a permission problem — your account administers the repository fine. Protection is not active, so work can merge unreviewed. Two ways forward: upgrade this repository's plan (or -make it public) and run this again — or, if you're deliberately running without the gate, record that with -`python .engine/tools/bootstrap.py accept-unprotected`, which tells the engine to stop failing every pull -request over a limitation it can't fix and instead report the gate as off-by-acceptance. Until one of those, -I'll keep reminding you the gate is off. +make it public), then say **turn my safety gate back on** — or, if you're deliberately running without the +gate, say **accept that my plan can't protect this branch** and I'll record that, so the engine stops failing +every pull request over a limitation it can't fix and instead reports the gate as off by your informed choice. +Either way I do it for you — you never type a command yourself. Until then, I'll keep reminding you the gate +is off. ## Removing the engine — keep or remove your safety rule diff --git a/.engine/tools/bootstrap.py b/.engine/tools/bootstrap.py index 9abf07a0..315e6e5c 100644 --- a/.engine/tools/bootstrap.py +++ b/.engine/tools/bootstrap.py @@ -440,11 +440,11 @@ def _product_preserved(pre: dict, post: dict, added: dict) -> bool: "branch-protection rules the safety gate needs (private repositories need GitHub Pro, Team, or " "Enterprise; public repositories have them for free). This isn't a permission problem — your " "account administers the repository fine. Protection is not active, so work can merge unreviewed. " - "Two ways forward: upgrade this repository's plan (or make it public) and run this again — or, if " - "you're deliberately running without the gate, record that with `python " - ".engine/tools/bootstrap.py accept-unprotected`, which tells the engine to stop failing every pull " - "request over a limitation it can't fix and instead report the gate as off-by-acceptance. Until one " - "of those, I'll keep reminding you the gate is off." + "Two ways forward: upgrade this repository's plan (or make it public), then say **turn my safety gate " + "back on** — or, if you're deliberately running without the gate, say **accept that my plan can't " + "protect this branch** and I'll record that, so the engine stops failing every pull request over a " + "limitation it can't fix and instead reports the gate as off by your informed choice. Either way I do " + "it for you — you never type a command yourself. Until then, I'll keep reminding you the gate is off." ), "applied": ( "Your safety gate is on. The main branch now requires a pull request, passing checks, and resolved " @@ -1172,6 +1172,22 @@ def cmd_status(args) -> int: try: missing = cp.floor_missing(args.branch) except BootstrapError as e: + # The read failed. If this repository's PLAN can't host rulesets AND the operator recorded a + # deliberate acceptance of that, say so calmly — matching every other surface — rather than as an + # unexplained technical failure. + posture = protection_guard.recorded_posture() + try: + plan_limited = cp._plan_forbids_rulesets(args.branch) + except BootstrapError: + plan_limited = False + if posture and plan_limited: + when = posture.get("recorded_on") or "an earlier date" + print(f"Branch protection isn't available on this repository's GitHub plan, and you accepted " + f"running without it on {when}. The safety gate is OFF for '{args.branch}' — a known, " + "accepted limitation, not a failure. If your plan later supports branch rulesets (upgrade it, " + "or make the repository public), turn the gate on with `python .engine/tools/bootstrap.py " + "apply`.") + return 0 print(f"Couldn't read branch protection for '{args.branch}' ({e}); treating it as not on.") return 0 if not missing: @@ -1309,12 +1325,13 @@ def _drop(data): def cmd_accept_unprotected(args) -> int: """Record the operator's DELIBERATE acceptance that this repository's GitHub plan cannot host branch - protection, so the standing check reports the gate as off-by-acceptance (an honest warning) instead of - hard-failing every pull request over a limitation the engine cannot fix. This is the operator's explicit - consent act — the engine offers it (arrival/boot banners), the operator asks for it, Claude runs it — and - it REFUSES to record unless it first re-verifies, live, that the branch-rules read genuinely returns - GitHub's plan-limitation 403. So it can never mint an exception on a repo whose plan can host protection. - Doubles as the repair path for an already-retired deployment (bootstrap.py survives retirement).""" + protection, so the standing check reports the gate as off by the operator's informed choice (an honest + warning) instead of hard-failing every pull request over a limitation the engine cannot fix. This is the + operator's explicit consent act — the engine offers it (arrival/boot banners), the operator asks for it in + plain words, the assistant runs it — and it REFUSES to record unless it first re-verifies, live, that the + branch-rules read genuinely returns GitHub's plan-limitation 403. So it can never mint an exception on a + repo whose plan can host protection. Doubles as the repair path for an already-retired deployment + (bootstrap.py survives retirement).""" repo = _resolve_repo(args.repo) token = boot.gh_token() branch = args.branch @@ -1333,14 +1350,16 @@ def cmd_accept_unprotected(args) -> int: "recorded — try again when you're back online.") return 1 if status == 200: - print("This repository's plan CAN host branch protection, so I won't record an exception. Turn the " - "safety gate on instead: `python .engine/tools/bootstrap.py apply`.") + print("Good news — this repository's plan CAN host branch protection, so I won't record an exception. " + "If the safety gate isn't on yet, say **turn my safety gate back on** and I'll turn it on for " + "you.") return 1 if not protection_guard.platform_forbids_rulesets(status, body, headers): print("I couldn't confirm that this repository's PLAN is why branch protection is unavailable — the " "branch-rules check didn't return GitHub's plan-limitation response (it may be a permission " "problem, a rate limit, or a temporary error). I won't record an exception on a guess; nothing " - "was recorded. If protection should work here, complete the branch-protection setup instead.") + "was recorded. If branch protection should work here, say **turn my safety gate back on** and " + "I'll try to turn it on, or check your repository's branch settings for what's blocking it.") return 1 # Genuine plan limitation confirmed. Record the operator-consented posture. import moment # lazily — the arrival-critical module import stays minimal and 3.9-safe @@ -1399,6 +1418,10 @@ def cmd_finalize(args) -> int: print(render(result)) if result.is_protected(): _persist_finalize_marker(result.marker) + # Protection is now in force, which proves this plan CAN host rulesets — so any recorded + # unsupported-platform posture is obsolete. Clear it too (like cmd_apply), because the standing check's + # stale-record nudge names `finalize` as a way to turn protection on; both commands must honour it. + _clear_protection_posture() return 0 if result.is_protected() else 1 diff --git a/.engine/tools/protection_guard.py b/.engine/tools/protection_guard.py index 0127c4a3..e235dfe6 100644 --- a/.engine/tools/protection_guard.py +++ b/.engine/tools/protection_guard.py @@ -270,10 +270,11 @@ def main() -> int: return emit([{"severity": tier, "location": None, "message": f"Branch protection could not be verified for '{branch}' " f"({e}); treating it as not in force until confirmed."}]) - if not isinstance(rules, list): + if not isinstance(rules, list) or not all(isinstance(r, dict) for r in rules): # A 200 with an unexpected body is NOT a confirmation that protection is in force — fail CLOSED - # (mirrors boot's twin guard). Never let a garbage/partial 200 crash missing_floor into an unhandled - # exception and an ambiguous disposition. + # (mirrors boot's twin guard). This checks BOTH the outer container AND the elements: a list of + # non-dicts (e.g. [1, 2, 3]) would otherwise crash missing_floor's `r.get("type")` into an uncaught + # exception (missing_floor runs below, outside the read's try) and an ambiguous disposition. return emit([{"severity": tier, "location": None, "message": f"Branch protection could not be verified for '{branch}' (the rules " "response was not in the expected form); treating it as not in force until confirmed."}]) diff --git a/.engine/tools/test_bootstrap.py b/.engine/tools/test_bootstrap.py index 69fc99b5..d5f75b0a 100644 --- a/.engine/tools/test_bootstrap.py +++ b/.engine/tools/test_bootstrap.py @@ -271,7 +271,11 @@ def transport(method, path, body=None): self.assertIsNone(result.marker) # a degraded arrival persists no control_plane marker rendered = bootstrap.render(result) self.assertIn("plan", rendered.lower()) - self.assertIn("accept-unprotected", rendered) # points at the verb, not "you don't administer this" + # Offers a plain spoken phrase (the engine never asks the operator to type a command), not a raw CLI + # invocation, and does not misblame the operator ("you don't administer this repository"). + self.assertIn("accept that my plan can't protect this branch", rendered) + self.assertNotIn("python .engine/tools/bootstrap.py accept-unprotected", rendered) + self.assertNotIn("you don't administer", rendered) def test_fine_grained_403_then_refresh_retries_and_applies(self): # A fine-grained token (no scope header): the first write 403s, the refresh "grants" admin, the @@ -1107,6 +1111,108 @@ def test_posture_persist_and_clear_roundtrip(self): with open(path, encoding="utf-8") as fh: self.assertNotIn("protection_posture", json.load(fh)) + def test_cmd_status_reports_an_accepted_posture_calmly(self): + # cmd_status must recognize a recorded plan-limitation acceptance and report it calmly, not as an + # unexplained "Couldn't read... treating it as not on" technical failure. + import argparse + import contextlib + import io + from unittest import mock + posture = {"status": "unsupported-platform", "recorded_on": "2026-08-08", "operator_login": "me"} + + class _CP: + def floor_missing(self, branch): + raise bootstrap.BootstrapError("could not read evaluated branch rules (status 403)") + + def _plan_forbids_rulesets(self, branch): + return True + + buf = io.StringIO() + args = argparse.Namespace(repo="o/r", branch="main") + with mock.patch.object(bootstrap, "boot") as mb, \ + mock.patch.object(bootstrap, "ControlPlane", lambda repo, token: _CP()), \ + mock.patch.object(bootstrap.protection_guard, "recorded_posture", return_value=posture), \ + contextlib.redirect_stdout(buf): + mb.gh_token.return_value = "tok" + rc = bootstrap.cmd_status(args) + out = buf.getvalue() + self.assertEqual(rc, 0) + self.assertIn("isn't available on this repository's GitHub plan", out) + self.assertIn("2026-08-08", out) + self.assertNotIn("Couldn't read", out) # never the unexplained-failure message + + def test_cmd_finalize_clears_a_stale_posture_on_success(self): + # The standing check's stale-record nudge names `finalize` as a way to turn protection on, so finalize + # must clear the stale posture on success too (not only cmd_apply). + import argparse + import contextlib + import io + from unittest import mock + cleared = [] + protected = bootstrap.Result("applied", "main", [], None, + marker={"ruleset_mode": "created", "augmented_ruleset_id": None, + "added": None}) + + class _CP: + def finalize(self, branch=None): + return protected + + args = argparse.Namespace(repo="o/r", branch="main") + with mock.patch.object(bootstrap, "boot") as mb, \ + mock.patch.object(bootstrap, "ControlPlane", lambda repo, token: _CP()), \ + mock.patch.object(bootstrap, "_persist_finalize_marker", lambda m: None), \ + mock.patch.object(bootstrap, "_clear_protection_posture", + side_effect=lambda: cleared.append(True)), \ + contextlib.redirect_stdout(io.StringIO()): + mb.gh_token.return_value = "tok" + rc = bootstrap.cmd_finalize(args) + self.assertEqual(rc, 0) + self.assertEqual(cleared, [True]) # stale posture cleared on a protected finalize + + def test_end_to_end_verb_write_is_read_by_the_guard_and_matches_schema(self): + # The SEAM, proven with ONE real manifest carried through: the exact dict the verb persists is what + # protection_guard.recorded_posture() reads back AND what the committed schema accepts. + import argparse + import contextlib + import io + import json + import tempfile + from unittest import mock + + def transport(method, path, body=None): + if path == "/user": + return 200, {"login": "octocat"}, {} + return 403, {"message": "Upgrade to GitHub Team to enable this feature."}, {} + + with tempfile.TemporaryDirectory() as tmp: + path = os.path.join(tmp, "engine.json") + with open(path, "w", encoding="utf-8") as fh: + json.dump({"engine_release": "1.0.0", "packages": {}, "identity": "solo"}, fh) + cp = bootstrap.ControlPlane("o/r", "tok", transport=transport, refresh_fn=lambda s: True, + issues=FakeIssues()) + args = argparse.Namespace(repo="o/r", branch="main") + with mock.patch.object(bootstrap, "boot") as mb, \ + mock.patch.object(bootstrap, "ControlPlane", lambda repo, token: cp), \ + mock.patch.object(bootstrap, "_engine_json_path", return_value=path), \ + mock.patch.object(bootstrap.protection_guard, "resolve_tier", return_value="solo"), \ + contextlib.redirect_stdout(io.StringIO()): + mb.gh_token.return_value = "tok" + self.assertEqual(bootstrap.cmd_accept_unprotected(args), 0) + # the guard reads back the SAME record the verb wrote + posture = protection_guard.recorded_posture(engine_dir=tmp) + self.assertIsNotNone(posture) + self.assertEqual(posture["status"], "unsupported-platform") + self.assertEqual(posture["operator_login"], "octocat") + # and the written manifest validates against the committed schema (drift between write and schema fails) + import jsonschema + with open(path, encoding="utf-8") as fh: + manifest = json.load(fh) + schema_path = os.path.join(os.path.dirname(os.path.abspath(bootstrap.__file__)), + "..", "schemas", "engine.v1.json") + with open(schema_path, encoding="utf-8") as fh: + schema = json.load(fh) + jsonschema.validate(manifest, schema) + if __name__ == "__main__": unittest.main() diff --git a/.engine/tools/test_protection_guard.py b/.engine/tools/test_protection_guard.py index 345c72fe..4c757322 100644 --- a/.engine/tools/test_protection_guard.py +++ b/.engine/tools/test_protection_guard.py @@ -207,6 +207,21 @@ def test_read_success_floor_present_passes_clean(self): missing=[]) self.assertEqual(findings, []) + def test_non_dict_list_200_fails_closed_without_crashing(self): + # A 200 whose body is a list of NON-dict elements must NOT crash missing_floor's r.get("type") into an + # uncaught exception (missing_floor runs outside the read's try) — the element guard fails it closed to + # a hard finding. missing_floor is deliberately NOT mocked here, so a regression would raise, not pass. + captured = [] + with mock.patch.dict(os.environ, {"GITHUB_REPOSITORY": "o/r", "GITHUB_TOKEN": "t"}, clear=False), \ + mock.patch.object(repo_identity, "resolve_default_branch", return_value="main"), \ + mock.patch.object(protection_guard, "resolve_tier", return_value="solo"), \ + mock.patch.object(protection_guard, "recorded_posture", return_value=None), \ + mock.patch.object(protection_guard, "get_json", return_value=[1, 2, "x"]), \ + mock.patch.object(protection_guard, "emit", side_effect=lambda f: captured.append(f) or 0): + protection_guard.main() # must not raise + self.assertEqual(captured[0][0]["severity"], "hard") + self.assertIn("not in the expected form", captured[0][0]["message"]) + if __name__ == "__main__": unittest.main() From 47335c13a9497bad1fa6ebbece616b773ffcbd29 Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:56:17 -0700 Subject: [PATCH 10/12] Chore: regenerate the knowledge graph after review fixes (#809) Fingerprint refresh for the edited protection_guard.py and bootstrap.py. Co-Authored-By: Claude Opus 4.8 --- .engine/knowledge/graph.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.engine/knowledge/graph.json b/.engine/knowledge/graph.json index 55700943..8db49bca 100644 --- a/.engine/knowledge/graph.json +++ b/.engine/knowledge/graph.json @@ -5413,7 +5413,7 @@ }, "slug": "engine.v1", "source": { - "fingerprint": "sha256:929eeae237ab8cae8482452891b77c9ba6ef390d708ea75506697a3edd4faeb2", + "fingerprint": "sha256:dd84e6ea48f2945f68fe88bbf014c9313412831c7b5e2fe4c83e74f9747ecd3f", "path": ".engine/schemas/engine.v1.json" }, "status": "active", @@ -6520,7 +6520,7 @@ }, "slug": "bootstrap", "source": { - "fingerprint": "sha256:420619f6800fdaae981d5739d8240f70d2df2736ee854081fb031ad1395bbfb0", + "fingerprint": "sha256:5c400874e8ec468e27d6de054b93203bff99cdfd387be2c510aa7a709cc631ab", "path": ".engine/tools/bootstrap.py" }, "status": "active", @@ -9877,7 +9877,7 @@ }, "slug": "protection_guard", "source": { - "fingerprint": "sha256:c42392cb49f6de8386e237f61176cae20f1f4542be09da048b04e0cce8c51159", + "fingerprint": "sha256:af0bb70842fa9761110d2dc49a58b7213c6db700fd1200cfe53eeb73d994f2da", "path": ".engine/tools/protection_guard.py" }, "status": "active", @@ -11130,7 +11130,7 @@ }, "slug": "test_bootstrap", "source": { - "fingerprint": "sha256:4ca36e76de33725b9a343f13d0f66b219831c23053c95ace28b8b41beb66a188", + "fingerprint": "sha256:7b9df4a4107977aecdb607533811a19c1b1ebe3fea8176ce041a7a61fcef508f", "path": ".engine/tools/test_bootstrap.py" }, "status": "active", @@ -13129,7 +13129,7 @@ }, "slug": "test_protection_guard", "source": { - "fingerprint": "sha256:4a26b23c09edc804d27b12b68dcf1f66f0a963e608552c013d357d0de46c51e6", + "fingerprint": "sha256:215ef9949af2f0a0036e1384af2bb94c5599f261330342355244f47a41feecdc", "path": ".engine/tools/test_protection_guard.py" }, "status": "active", From dc1797e4987ff83d0b64a74c4be18d76721af1d8 Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:03:35 -0700 Subject: [PATCH 11/12] =?UTF-8?q?Fix:=20address=20scoped=20re-audit=20find?= =?UTF-8?q?ings=20=E2=80=94=20cmd=5Fstatus=20spoken=20phrase=20+=20copy-pa?= =?UTF-8?q?rity=20test=20(#809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .engine/tools/bootstrap.py | 3 +-- .engine/tools/test_bootstrap.py | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.engine/tools/bootstrap.py b/.engine/tools/bootstrap.py index 315e6e5c..74eca4fd 100644 --- a/.engine/tools/bootstrap.py +++ b/.engine/tools/bootstrap.py @@ -1185,8 +1185,7 @@ def cmd_status(args) -> int: print(f"Branch protection isn't available on this repository's GitHub plan, and you accepted " f"running without it on {when}. The safety gate is OFF for '{args.branch}' — a known, " "accepted limitation, not a failure. If your plan later supports branch rulesets (upgrade it, " - "or make the repository public), turn the gate on with `python .engine/tools/bootstrap.py " - "apply`.") + "or make the repository public), say **turn my safety gate back on** and I'll enable it.") return 0 print(f"Couldn't read branch protection for '{args.branch}' ({e}); treating it as not on.") return 0 diff --git a/.engine/tools/test_bootstrap.py b/.engine/tools/test_bootstrap.py index d5f75b0a..b412aa1e 100644 --- a/.engine/tools/test_bootstrap.py +++ b/.engine/tools/test_bootstrap.py @@ -387,6 +387,13 @@ def test_template_and_fallback_before_you_approve_do_not_drift(self): self.assertEqual(self._norm(bootstrap.load_copy(bootstrap.TEMPLATE_PATH)["before-you-approve"]), self._norm(bootstrap.FALLBACK_COPY["before-you-approve"])) + def test_template_and_fallback_unsupported_platform_do_not_drift(self): + # The same word-for-word guard for the plan-limitation banner (#809), so a future copy fix can't land + # in the template and not the built-in fallback (or vice versa). Tolerates only the template's wrapping. + self.assertEqual( + self._norm(bootstrap.load_copy(bootstrap.TEMPLATE_PATH)["degraded-unsupported-platform"]), + self._norm(bootstrap.FALLBACK_COPY["degraded-unsupported-platform"])) + def test_missing_template_falls_back_not_crashes(self): copy = bootstrap.load_copy("/no/such/template.md") self.assertEqual(copy["before-you-approve"], bootstrap.FALLBACK_COPY["before-you-approve"]) From f105c657aa6195520d130398ed8b505126d18def Mon Sep 17 00:00:00 2001 From: Shane Kidd <33380501+StarshipSuperjam@users.noreply.github.com> Date: Sun, 9 Aug 2026 12:03:36 -0700 Subject: [PATCH 12/12] Chore: regenerate the knowledge graph after re-audit copy fix (#809) Co-Authored-By: Claude Opus 4.8 --- .engine/knowledge/graph.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.engine/knowledge/graph.json b/.engine/knowledge/graph.json index 8db49bca..4e2789db 100644 --- a/.engine/knowledge/graph.json +++ b/.engine/knowledge/graph.json @@ -6520,7 +6520,7 @@ }, "slug": "bootstrap", "source": { - "fingerprint": "sha256:5c400874e8ec468e27d6de054b93203bff99cdfd387be2c510aa7a709cc631ab", + "fingerprint": "sha256:a84ed2e2ba23c6031ffab48169ee778f9a11efcf43fe2060de9144622b9dce4e", "path": ".engine/tools/bootstrap.py" }, "status": "active", @@ -11130,7 +11130,7 @@ }, "slug": "test_bootstrap", "source": { - "fingerprint": "sha256:7b9df4a4107977aecdb607533811a19c1b1ebe3fea8176ce041a7a61fcef508f", + "fingerprint": "sha256:bcd26ffc51c7690174c75972de868a6a278490724383b8aabfe688557dbea7c0", "path": ".engine/tools/test_bootstrap.py" }, "status": "active",