Skip to content

fix(ci): clear five startup_failures caused by reusable permission escalation - #560

Merged
hyperpolymath merged 7 commits into
mainfrom
fix/reusable-perm-escalation
Jul 28, 2026
Merged

fix(ci): clear five startup_failures caused by reusable permission escalation#560
hyperpolymath merged 7 commits into
mainfrom
fix/reusable-perm-escalation

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

The problem

Five workflows on standards' main have been dead — Rust CI, Elixir CI, Deno CI, CodeQL, Scorecard — each reporting startup_failure with 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.

caller granted reusable requested missing
rust-ci contents:read + actions:read actions:read
elixir-ci contents:read + actions:read actions:read
deno-ci contents:read + actions:read actions:read
codeql contents:read + actions:read, security-events:write both
scorecard actions,contents:read + security-events:write, id-token:write both

Two fixes, chosen per case

rust / elixir / deno → narrow the reusable. The top-level actions: read is 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 (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) and id-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:

OK  rust-ci   -> rust-ci-reusable      OK  codeql    -> codeql-reusable
OK  elixir-ci -> elixir-ci-reusable    OK  scorecard -> scorecard-reusable
OK  deno-ci   -> deno-ci-reusable      ALL SUBSET-SAFE

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 omits contents: read entirely, which is worth a separate look.

🤖 Generated with Claude Code

…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>
@gitar-bot

gitar-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

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.
Learn more

Code Review ✅ Approved

Narrows reusable workflow permissions and widens caller permissions to resolve startup failures caused by permission escalation. No issues found.

Auto-approved and auto-merge armed: No blocking issues found.
Please see Auto-approve Docs for details on setting custom approval criteria. — merges when pipeline and required approvals pass.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@gitar-bot
gitar-bot Bot enabled auto-merge (squash) July 28, 2026 22:37
gitar-bot[bot]
gitar-bot Bot previously approved these changes Jul 28, 2026

@gitar-bot gitar-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gitar has auto-approved this PR and enabled auto-merge (configure)

@gitar-bot gitar-bot Bot added the gitar-approved Added by Gitar label Jul 28, 2026
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>
hyperpolymath and others added 4 commits July 28, 2026 23:43
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>
@hyperpolymath
hyperpolymath disabled auto-merge July 28, 2026 22:54
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>
@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit 38c8f73 into main Jul 28, 2026
34 of 35 checks passed
@hyperpolymath
hyperpolymath deleted the fix/reusable-perm-escalation branch July 28, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gitar-approved Added by Gitar

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant