Skip to content

feat(scripts): fix-actions-policy.sh — repair the estate Actions-policy outage (#362)#434

Merged
hyperpolymath merged 2 commits into
mainfrom
feat/fix-actions-policy
Jul 21, 2026
Merged

feat(scripts): fix-actions-policy.sh — repair the estate Actions-policy outage (#362)#434
hyperpolymath merged 2 commits into
mainfrom
feat/fix-actions-policy

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Implements the "fastest shippable" item of #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. 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 hyperpolymath repos are in this state,
independently corroborating #362's estimate of 91. hyperpolymath is a User account so
there is no org lever; metadatastician is an Organization where one org-level PUT
covers every repo (needs admin:org).

Why this fixer is shaped unlike every other fix-*.sh

The others take a REPO_PATH and edit files — blast radius is a reviewable diff. This one
writes repository settings through the API: no diff, no branch, no PR. #362 calls this
out explicitly, so it is gated:

Guard Behaviour
Default mode --listread-only, never writes
Writes require explicit --apply
Kill switch HYPATIA_AUTOMATION={off,disabled,0} → exit 4
Exclusion registry consulted fail-closed; unreadable → refuse to write, exit 5
Post-write check every PUT is GET-verified; a silent sha_pinning reset aborts (exit 3)

sha_pinning_required is never disabled. Under --mode all the remaining control is
mandatory SHA-pinning — which is the point: a pinned SHA cannot be moved under you, whereas
an empty allowlist protects nothing and blocks everything. --mode selected remains for a
high-sensitivity tier, seeding patterns_allowed from standards.

Dual invocation contract (integration bug caught before it shipped)

dispatch-runner.sh calls fixers as <script> <repo_path> <finding.json>, but a standalone
sweep is <script> <owner> --apply. Registering the script without handling that would have
made the fleet pass a filesystem path where an owner was expected. The script now detects
a directory in $1 and 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 — a
credentialed, diff-less write must not auto-fire merely because a finding routed to it.

Registered under by_category: ActionsPolicyTooRestrictive and
by_recipe: recipe-actions-allow-all.

Verification

shellcheck clean · bash -n clean · registry re-parsed with jq.

Guard rails (exit codes measured directly, not through a masking pipeline):

Invocation Exit Expected
no owner / bad --mode / unknown flag 2
HYPATIA_AUTOMATION=off + --apply 4
HYPATIA_AUTOMATION=off + --list 0 ✅ read-only unaffected

Classification vs an independently collected 306-repo ground truth — 5/5 agreement:

Repo Tool Truth
typefix-zero, stateful-artefacts, trope-checker BROKEN-M1 BROKEN-M1
svalinn, thejeffparadox ok ok

Dispatcher path, invoked exactly as dispatch-runner.sh does:

$ fix-actions-policy.sh /…/hyper-repos/trope-checker /tmp/dispatch-finding-X.json
fix-actions-policy: dispatcher mode — hyperpolymath/trope-checker (action=list)
trope-checker    selected  true  0  BROKEN-M1
scanned=1 broken=1 fixed=0 excluded=0 mode=list/all      # exit 0

⚠️ Not applied anywhere

--apply has not been run. Widening allowed_actions across ~80 repos is a
security-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 actually
start → 3-repo pilot → full sweep → re-run --list to confirm broken=0.

🤖 Generated with Claude Code

hyperpolymath and others added 2 commits July 21, 2026 06:44
…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
hyperpolymath marked this pull request as ready for review July 21, 2026 12:06
@hyperpolymath
hyperpolymath merged commit a3e9d4b into main Jul 21, 2026
11 of 12 checks passed
@hyperpolymath
hyperpolymath deleted the feat/fix-actions-policy branch July 21, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant