feat(scripts): fix-actions-policy.sh — repair the estate Actions-policy outage (#362)#434
Merged
Merged
Conversation
…cy outage Implements the "fastest shippable" item of gitbot-fleet#362. ## The fault An onboarding stamp set `allowed_actions=selected` + `sha_pinning_required=true` but never populated `patterns_allowed`, leaving it `[]`. An empty selected-list rejects every non-github-owned `uses:` at workflow-parse time, so runs die as `startup_failure` with ZERO jobs — looks like "CI is broken", is actually a settings fault, and no amount of workflow editing fixes it. MEASURED 2026-07-21: 80 of 306 scanned hyperpolymath repos are in this state. ## Why this fixer is shaped unlike the others Every other fix-*.sh takes a REPO_PATH and edits files, so its blast radius is a reviewable diff. This one writes repository *settings* via the API: no diff, no branch, no PR. Per #362 that demands gating, so: * default mode is --list (READ ONLY; never writes) * --apply is required to write * HYPATIA_AUTOMATION={off,disabled,0} halts writes (exit 4) * the estate exclusion registry is consulted FAIL-CLOSED before any write (unreadable registry => refuse to write, exit 5) * every PUT is GET-verified and the sweep ABORTS (exit 3) if sha_pinning was silently reset — never trade pinning away for permissiveness ## Dual invocation contract dispatch-runner.sh calls fixers as `<script> <repo_path> <finding.json>`, but a standalone sweep is `<script> <owner> --apply`. The script detects a directory in $1 and derives owner/repo from the git origin, so one tool serves both paths instead of silently misreading a filesystem path as an owner. In the dispatcher path it stays report-only unless FIX_ACTIONS_POLICY_APPLY=1 — a credentialed diff-less write must not auto-fire just because a finding routed to it. Registered under by_category ActionsPolicyTooRestrictive and by_recipe recipe-actions-allow-all so dispatch-runner auto-routes hypatia findings. ## Verification shellcheck clean; bash -n clean; registry re-parsed with jq. Guard rails, exit codes measured directly (not through a masking pipeline): no owner / bad --mode / unknown flag -> 2 HYPATIA_AUTOMATION=off with --apply -> 4 HYPATIA_AUTOMATION=off with --list -> 0 (read-only unaffected) Classification, against an independently-collected 306-repo ground truth: typefix-zero, stateful-artefacts, trope-checker -> BROKEN-M1 (correct) svalinn, thejeffparadox -> ok (correct) 5/5 agreement. Dispatcher path exercised exactly as dispatch-runner invokes it: fix-actions-policy.sh <path-to-trope-checker> <finding.json> -> "dispatcher mode — hyperpolymath/trope-checker (action=list)" -> BROKEN-M1, exit 0. NOT run with --apply anywhere: widening allowed_actions is a security-posture change across ~80 repos and needs the owner's explicit sign-off first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… allowlist) Converges this fixer with a parallel remediation effort (llm-coding-configs/claude-code/notification-storm-remediation/FINDINGS.md) rather than letting two incompatible fixes race on the same 98 repos. #362's decision of record was `allowed_actions=all`. That works, but it is a security-posture change: it removes the owner allowlist and leaves mandatory SHA-pinning as the only control. The parallel effort established a strictly safer route reaching the same outcome: keep allowed_actions=selected, keep sha_pinning_required=true, and simply POPULATE the empty patterns_allowed. Its natural experiment over 426 repos: allowed_actions=all 286 repos -> wrappers run selected + `hyperpolymath/*` present 42 repos -> wrappers run selected + patterns_allowed=[] 98 repos -> ALL startup_failure So the breakage is the list being EMPTY, not the list existing. Populating it restores CI *and* keeps the allowlist as a real control. `--mode all` remains available as an explicit fallback; it is no longer the default. Changes: * MODE now defaults to `selected` (was `all`) * new `--allowlist FILE` to supply a curated patterns_allowed document * refuse an allowlist whose patterns_allowed is EMPTY — that is precisely the fault being repaired, and applying it would be a silent no-op * `[ x = y ] && rm` replaced with a total `if` — under `set -e` the bare `&&` form returns 1 when the test is false and would abort the sweep Verified: shellcheck clean; bash -n clean; empty-allowlist guard exercised; `--list` still classifies correctly (trope-checker, now populated with 87 patterns, reports `selected true 87 ok` — it was `selected true 0 BROKEN-M1`). Target lists reconcile exactly: this tool's 95 (--source, non-archived) is a subset of the parallel effort's 98; the 3 extra are forks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hyperpolymath
marked this pull request as ready for review
July 21, 2026 12:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the "fastest shippable" item of #362.
The fault
An onboarding stamp set
allowed_actions=selected+sha_pinning_required=truebut neverpopulated
patterns_allowed, leaving it[]. An empty selected-list rejects everynon-github-owned
uses:at workflow-parse time, so runs die asstartup_failurewithzero jobs. It presents as "CI is broken" but no workflow edit can fix it — it is a settings
fault.
Measured 2026-07-21: 80 of 306 scanned
hyperpolymathrepos are in this state,independently corroborating #362's estimate of 91.
hyperpolymathis a User account sothere is no org lever;
metadatasticianis an Organization where one org-level PUTcovers every repo (needs
admin:org).Why this fixer is shaped unlike every other
fix-*.shThe others take a
REPO_PATHand edit files — blast radius is a reviewable diff. This onewrites repository settings through the API: no diff, no branch, no PR. #362 calls this
out explicitly, so it is gated:
--list— read-only, never writes--applyHYPATIA_AUTOMATION={off,disabled,0}→ exit 4sha_pinningreset aborts (exit 3)sha_pinning_requiredis never disabled. Under--mode allthe remaining control ismandatory SHA-pinning — which is the point: a pinned SHA cannot be moved under you, whereas
an empty allowlist protects nothing and blocks everything.
--mode selectedremains for ahigh-sensitivity tier, seeding
patterns_allowedfromstandards.Dual invocation contract (integration bug caught before it shipped)
dispatch-runner.shcalls fixers as<script> <repo_path> <finding.json>, but a standalonesweep is
<script> <owner> --apply. Registering the script without handling that would havemade the fleet pass a filesystem path where an owner was expected. The script now detects
a directory in
$1and derives owner/repo from the git origin, so one tool serves both.In the dispatcher path it stays report-only unless
FIX_ACTIONS_POLICY_APPLY=1— acredentialed, diff-less write must not auto-fire merely because a finding routed to it.
Registered under
by_category: ActionsPolicyTooRestrictiveandby_recipe: recipe-actions-allow-all.Verification
shellcheckclean ·bash -nclean · registry re-parsed withjq.Guard rails (exit codes measured directly, not through a masking pipeline):
--mode/ unknown flagHYPATIA_AUTOMATION=off+--applyHYPATIA_AUTOMATION=off+--listClassification vs an independently collected 306-repo ground truth — 5/5 agreement:
BROKEN-M1BROKEN-M1✅okok✅Dispatcher path, invoked exactly as
dispatch-runner.shdoes:--applyhas not been run. Wideningallowed_actionsacross ~80 repos is asecurity-posture change and needs explicit owner sign-off. This PR ships the tool and the
evidence; the sweep is a separate, deliberate decision.
Suggested rollout once signed off:
--apply --repo <one>→ confirm its workflows actuallystart → 3-repo pilot → full sweep → re-run
--listto confirmbroken=0.🤖 Generated with Claude Code