fix(ci): clear five startup_failures caused by reusable permission escalation - #560
Conversation
…calation standards' main has had five workflows dead — Rust CI, Elixir CI, Deno CI, CodeQL and Scorecard — all reporting `startup_failure` with ZERO jobs: no logs, no red step, just an empty run. Single root cause: a reusable workflow may NARROW its caller's permissions but never WIDEN them. Requesting a permission the caller has not granted aborts the whole run before any job starts. Each pair was mismatched: caller granted reusable requested rust-ci contents:read + actions:read elixir-ci contents:read + actions:read deno-ci contents:read + actions:read codeql contents:read + actions:read, security-events:write scorecard actions,contents + security-events:write, id-token:write Two different fixes, chosen per case rather than uniformly: * rust/elixir/deno — the top-level `actions: read` was used by NO job (all 8 jobs declare their own `contents: read`, and nothing in these files downloads cross-workflow artifacts or calls the Actions API). Removed it from the reusable. This is least-privilege AND fixes every consumer at once without touching ~150 wrapper workflows, since a reusable that requests less is compatible with strictly more callers. * codeql/scorecard — security-events:write (SARIF upload) and id-token:write (Scorecard OIDC) are genuinely required, so the CALLER must grant them. Widened standards' own two wrappers only. Verified: every caller/reusable pair now satisfies the subset rule, and all five files re-parse with their job counts intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Note Automatic reviews are paused because your trial's included automatic processing has been used for this period. Upgrade now, or comment "Gitar review" to run a review anytime. Code Review ✅ ApprovedNarrows reusable workflow permissions and widens caller permissions to resolve startup failures caused by permission escalation. No issues found.
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
With no deno.json, the deno-ci reusable falls back to scanning raw *.js and
swept in four bsc-generated axel-protocol/src/*.res.js artifacts, failing
the lint on prefer-const violations in code no human wrote.
The exclude list deliberately MIRRORS this repo's own no-js-scan.yml, which
already classifies *.res.js, *.res.mjs, lib/{js,es6,bs}, out, dist, .deno
and generated/ as generated/compiled. Two gates in one repo disagreeing
about what counts as source was the actual defect.
Does NOT make Deno CI green: 133 genuine lint problems remain in 17
hand-authored files (82 prefer-const, 28 no-import-prefix, 15
no-unused-vars, 6 no-case-declarations, 2 require-await), mostly in
0-ai-gatekeeper-protocol/mcp-repo-guardian and k9-coordination-protocol.
That debt was previously invisible behind startup_failure. `deno lint --fix`
does not fix these rules in Deno 2.9.2, so it needs hand edits and is left
for a separate, reviewable change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-actions-policy.sh ends by exec'ing its SIBLING script:
exec bash "${0%/*}/check-allowed-actions.sh" "$CANON" "$WF_DIR"
but the workflow copied only check-actions-policy.sh into $RUNNER_TEMP and
then `rm -rf`'d the standards checkout. `${0%/*}` therefore resolved to
$RUNNER_TEMP, where the sibling did not exist — so the step died with exit
127 (command not found) rather than ever running the check.
Also staged the canonical allowlist and taught the wrapper to honour
$ALLOWLIST_JSON. The old hard-coded path was relative to the CALLER's
working directory, so a consumer repo — which has no
rhodium-standard-repositories/ of its own — would take the `exit 2`
"allowlist not found" branch even once the 127 was fixed.
Verified by reproducing the CI staging exactly (both scripts copied out,
checkout deleted, allowlist passed by env): "checked 34 `uses:` refs across
.github/workflows — 0 not covered by the allowlist", exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Language/package anti-pattern gate has been failing on standards' own
main with:
❌ Python files detected — Python is fully banned
scripts/bulk-fix-workflows.py
The repository that DEFINES the estate language policy was the one breaking
it. Python is banned with no exceptions (SaltStack carve-out removed
2026-01-03), so an exemption pragma would be the wrong instrument here.
scripts/bulk-fix-workflows.py is a 355-line one-off migration tool that
replaced duplicate workflow files with calls to the root reusables. That
migration has landed (d8f8975), nothing in the repo references the script
— no workflow, justfile, doc or other script — and it is fully recoverable
from git history if ever needed. Porting dead code to Deno would be effort
spent on something with no callers, so it is removed rather than rewritten.
This was the only *.py file in the repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hypatia's baseline gate reported exactly two NEW (unfiltered) findings on
standards' main. One was scripts/bulk-fix-workflows.py, removed in the
previous commit. This is the other:
missing_timeout_minutes signed-push-smoke.yml (medium)
An un-bounded job can occupy a runner for the full 6-hour default on hang,
against a shared account-wide Actions pool.
Deliberately applied ONLY to `smoke`, which is a steps-job. `timeout-minutes`
is an ILLEGAL key on a job that `uses:` a reusable workflow, where it makes
GitHub refuse to parse the entire file — killing every job in it, with no
check run to show why. Swept all of .github/workflows/ distinguishing the
two job shapes: this was the last steps-job in the repo without a bound,
and there are now zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Registry Verify has been red on main. Its first step is the drift check,
and under `set -euo pipefail` it aborts the job before the Mustfile and
scorecard steps ever run:
DRIFT: .machine_readable/REGISTRY.a2ml is stale — run 'just registry'
Regenerated with scripts/build-registry.sh, exactly as the failure message
and the job summary instruct. The diff is only what the generator derives:
· three refreshed source_hash values (a2ml, adoption-readiness-grades,
rhodium-standard-repositories) — their spec homes changed without the
derived registry being rebuilt;
· publication-pre-flight's canonical_doc re-pointed from the retired
ESTATE-AUDIT-BASELINE-2026-03-30.adoc to HOL-SUITABILITY-CHECKLIST.adoc.
No hand edits. `build-registry.sh --check` now passes, and
check-mustfile-structure.sh passes. The scorecard --verify step could not
be exercised locally (xmllint absent — the script correctly declines to
judge pass-rows rather than call them false on missing tooling); CI installs
ripgrep + libxml2-utils for exactly that reason, so it is verified there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clearing the REGISTRY drift let Registry Verify reach its --verify step for
the first time, which RUNS every claimed pass-row's check. Five claims did
not hold:
82 grounded pass · 5 broken pass · 0 unrunnable
Investigated each; they split cleanly into two causes.
STALE CHECKS (2) — the compliance is real, the check text was not:
· a2ml/S4 greps for an inline 'gitleaks/gitleaks-action', but
secret-scanner.yml now `uses:` secret-scanner-reusable.
· axel-protocol/S4 greps for 'ossf/scorecard-action', but scorecard.yml
now `uses:` scorecard-reusable.
Both repos ARE scanning; the checks tested a pre-migration implementation
detail. Widened each to accept the inline action OR the reusable, so the
check survives the migration instead of silently going false.
GENUINELY UNMET (3) — the artifact does not exist, so the claim was false:
· avow-protocol/M5 codeql.yml absent (avow-protocol has no
.github/workflows/ at all)
· avow-protocol/S1 AVOW-THREAT-MODEL.adoc absent (only BINDING.adoc)
· rhodium-.../M2 wave0-false-green-test.sh fails 1/13 — it references
$ROOT/audit-contractiles.sh, which exists nowhere in
the repo
Corrected status pass -> fail. This does NOT fabricate compliance and does
not paper over the gap: `fail` is already a normal state on these
scorecards, and an honest red is worth more than a green the repo's own
verifier calls "not real". Restoring the three artifacts is real work and
is left to a deliberate follow-up.
No other pass rows were touched (4 files, 5 lines). Dashboard regenerated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|



The problem
Five workflows on standards'
mainhave been dead — Rust CI, Elixir CI, Deno CI, CodeQL, Scorecard — each reportingstartup_failurewith zero jobs. No logs, no red step, just an empty run, which is why they persisted.Single root cause
A reusable workflow may narrow its caller's permissions but never widen them. Requesting a permission the caller hasn't granted aborts the run before any job starts.
rust-cicontents:read+ actions:readactions:readelixir-cicontents:read+ actions:readactions:readdeno-cicontents:read+ actions:readactions:readcodeqlcontents:read+ actions:read, security-events:writescorecardactions,contents:read+ security-events:write, id-token:writeTwo fixes, chosen per case
rust / elixir / deno → narrow the reusable. The top-level
actions: readis used by no job: all 8 jobs declare their owncontents: read, and nothing in these files downloads cross-workflow artifacts or calls the Actions API (verified by grep). Removing it is least-privilege and fixes every consumer at once without touching ~150 wrapper workflows — a reusable that requests less is compatible with strictly more callers.codeql / scorecard → widen the caller.
security-events: write(SARIF upload, which branch protection depends on) andid-token: write(Scorecard OIDC) are genuinely required. Only standards' own two wrappers were widened.Verification
Machine-checked that every caller/reusable pair now satisfies the subset rule, comparing the caller's grant against the union of the reusable's top-level and every job block:
Confirmed every job in the three narrowed reusables declares its own
permissions:(5/5, 2/2, 1/1), so nothing was relying on top-level inheritance. All five files re-parse with job counts intact.Noted, not changed
Several other reusables (
mirror,readme-derive,hypatia-scan) have top-level permissions their jobs don't re-declare — but there the jobs inherit the top level, so it is load-bearing, not dead weight. Left alone deliberately.scorecard-reusable's job block omitscontents: readentirely, which is worth a separate look.🤖 Generated with Claude Code