Skip to content

feat: add governed local runner routing - #74

Merged
kyle-sexton merged 27 commits into
mainfrom
codex/local-runner-selector
Jul 11, 2026
Merged

feat: add governed local runner routing#74
kyle-sexton merged 27 commits into
mainfrom
codex/local-runner-selector

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add the centralized hosted/self-hosted selector with fail-hosted security and API-error behavior
  • allowlist the reviewed ubuntu-24.04 hosted fallback and enforce the governed Linux/JIT inventory namespace
  • add the immutable private canary and cache/parity contracts
  • add a read-only production HA proof for exact runner-group access, host-specific acquisition, pre-acquisition failover, and laptop power evidence
  • require first-attempt execution on every routed/evidence job, preserve normal cancellation, downscope observer tokens to ci-runner-canary, and require protected main
  • keep Windows, Docker-dependent, privileged, broad-write, Dependabot, and public workloads hosted
  • include the reviewed cache-epoch caller contract and checksum-pinned ShellCheck/Actionlint parity stack

Why

This is the shared routing contract for the ephemeral local fleet. Eligible private Linux jobs can use idle local capacity while reruns, unsafe events, missing capacity, missing credentials, namespace conflicts, malformed inventory, and every selector/API failure route hosted.

Immutable proof chain

  • final reviewed stacked head: 3fa4a44c1883f72be8050995109b0969dd4a627c
  • corrected HA implementation and private caller pin: fdf0e6e5905e50be8e332f9bc522e24947a61b13
  • cache-epoch stack merge: 68b9f40aa65978c180e6a51642fa7f56ad8a6f45
  • Actionlint/ShellCheck parity stack merge: 3fa4a44c1883f72be8050995109b0969dd4a627c
  • the caller is authorized only from protected main in the private melodic-software/ci-runner-canary repository

Validation

  • independent selector/security review: PASS
  • independent HA proof review and finding recheck: PASS with no findings
  • final independent GitHub gate audit: PASS with zero review threads or pending findings
  • full selector/canary/HA/tooling suite: 156/156 PASS
  • focused HA suite: 29/29 PASS
  • Actionlint, ShellCheck, strict Zizmor, Biome, Markdown, generated-workflow drift, and git diff --check: PASS
  • all author commits have valid signatures; GitHub-merged stack commits are verified by GitHub

All 33 hosted checks pass on this exact head. A post-push online Zizmor audit of the two HA-proof workflows reports no findings.

Rollout

Production routing remains hosted-only through controller publication, App/IaC bootstrap, physical canary, and two-host proof. Downstream consumers will pin the immutable squash-merge commit from main; they will not pin this feature-branch head.


Note

High Risk
Changes authentication/OIDC policy matching, production runner routing, and multiple security-critical CI gates across a very large surface; misconfiguration could block deploys or route untrusted work locally.

Overview
Adds the shared CI routing and proof stack for ephemeral self-hosted runners: a reusable select-runner workflow (prefer-self-hosted / hosted-only) that fail-closes to reviewed ubuntu-24.04, validates managed runner namespaces, and refuses local routing for public repos, forks, Dependabot, reruns, and API/auth failures.

Immutable private canary (local-runner-canary) delegates selection to the pinned selector, asserts managed runner identity on every job, and runs a shared parity script (LFS, toolchains, sudo/cert trust, Native AOT) plus bidirectional artifact/cache proofs and optional long-run/cancellation modes. Production HA proof is a separate reusable workflow with read-only GitHub inventory, hosted failover hold after desktop drain, and laptop execution assertions—backed by production-ha-proof.cjs embedded via render scripts.

Pulumi deployment guard composite action compares live OIDC personal allow policies to bundled v2 contracts (immutable subjects, exact claims, no wildcards) and inventories operational stack URNs before apply. Pulumi CLI drift reusable workflow opens/ages maintenance issues and hard-fails after 14 days.

OSV scanning moves to a digest-pinned container contract with osv-scan-guard.sh governing empty scans and SARIF upload safety. Actionlint now installs checksum-pinned ShellCheck (lockstep with the standalone action). PSScriptAnalyzer runs once per file with exact version import and no #1708 retry subprocesses.

Workflow policy tests enforce fixed GA runner labels (no *-latest), ban secrets: inherit on reusables, and add broad contract tests for pins, templates, and generated workflow drift.

Reviewed by Cursor Bugbot for commit 3fa4a44. Bugbot is set up for automated code reviews on this repo. Configure here.

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 50s —— View job


Code Review — feat: add governed local runner routing

This is a security-critical PR introducing the central routing contract for an ephemeral local GitHub Actions fleet. I've reviewed all 49 changed files, focusing on the security model, correctness, and CLAUDE.md compliance.


Overall Assessment

The design is well-considered and the threat model is clearly articulated. The fail-closed pattern is consistently applied: every error path (auth failure, API timeout, invalid response, missing config, public repo, fork PR, Dependabot, rerun) routes to a hosted runner. The dual-guard pattern (YAML if: + JS preflight validation) is deliberate defense-in-depth, not redundancy. CLAUDE.md rules are satisfied: no pull_request_target or workflow_run triggers, SHA pins with version comments throughout, observer credential is narrowly scoped.

The findings below are ordered by severity.


Findings

1. ubuntu-slim runner label in the selector job is undocumented

File: .github/workflows/select-runner.yml:64

runs-on: ubuntu-slim

ubuntu-slim is not a standard GitHub-hosted runner label (standard names are ubuntu-24.04, ubuntu-22.04, ubuntu-latest). Every other workflow in this repo uses ubuntu-24.04. This label only makes sense as an org-level custom runner or a GitHub-internal alias.

Why it matters: If ubuntu-slim is a private org runner, the selector job — which mints the observer App token and makes the routing API call — runs on a custom runner rather than a fully hosted one. That changes the trust envelope: a compromised org runner could intercept the token mint step and observe the observer private key before it reaches actions/create-github-app-token. The job itself has continue-on-error: true only on the steps, not the job — if the runner label simply doesn't exist and the job never schedules, consumers of the select-runner workflow would see their needs.select-runner dependency fail with no routing output.

Suggestion: Either replace with ubuntu-24.04 (consistent with every other job in this repo) and document any cost rationale, or add a comment explaining what ubuntu-slim resolves to and in which environment.

Fix this →


2. configuredCandidateLabels silently drops selfHostedLabelsJSON parse errors

File: .github/scripts/select-runner.cjs:55–63

configuredCandidateLabels (used only for the hosted-runner collision check) catches JSON parse errors silently, while parseCandidateLabels returns {error: "invalid-response"} on the same malformed input. The end result is correct — malformed JSON always routes to hosted — but the two functions behave differently on the same bad input, which creates an implicit coupling that can surprise a future maintainer.

Additionally, configuredCandidateLabels does not filter labels through validManagedLabel, so it would include reserved labels (e.g. "self-hosted", "linux") in the collision set. This is correct for the collision check (you want the hosted runner to collide with anything suspicious), but it means the two functions have divergent interpretations of the same selfHostedLabelsJSON value. A comment explaining the intentional asymmetry would aid future maintenance.


3. runAttempt !== 1 appears in the config-validation block after the rerun-detection block

File: .github/scripts/select-runner.cjs:144–176

The early guard (line 144) routes to hosted when runAttempt > 1 (reruns). The config check (line 172) then also requires input.runAttempt !== 1. This means runAttempt === 0 or NaN reach the config check and route to hosted with reason missing-config, not rerun. That is the correct behavior, but the code reads as if the two checks are independent — a future refactor that removes the early check would silently change the behavior for runAttempt > 1 if the config check remains. The redundancy is intentional, but a note on the config-side check explaining it handles the 0/NaN cases the early guard doesn't catch would make the invariant explicit.


4. API version string "2026-03-10" is valid but unverified

File: .github/scripts/select-runner.cjs:4

const GITHUB_API_VERSION = "2026-03-10";

This is a date-versioned GitHub API version from March 2026. As of today (July 2026) this is in the past and GitHub still honours pinned versions for a deprecation window, so this is fine. Just worth noting that if this version is eventually retired, the runner inventory call would fall back to the previous version or fail — nothing in the codebase currently tracks this for drift. The tool-version-drift-check.yml watches binary tools but not the GitHub API version string. Consider adding a note or tracking issue.


5. Canary select-runner SHA pin lacks a version comment

File: .github/workflows/local-runner-canary.yml:128

uses: melodic-software/ci-workflows/.github/workflows/select-runner.yml@4943b1c4ff6ae9624736ac95622d7ab748132c8d # governed selector review

The comment says governed selector review rather than a version (e.g. # v1.0.0 or # <commit date>). This is consistent with CLAUDE.md's SHA-pin requirement, but the tool-version-drift-check.yml and the rest of the codebase also include the tag in the comment so Dependabot can parse them. An internal first-party SHA pin without a version comment won't be bumped by Dependabot — that may be intentional here (the canary tests a specific reviewed revision), but it should be called out explicitly, since every other uses: in this repo follows the @<sha> # vX.Y.Z pattern.


6. safe_path in canary preflight allows // (minor)

File: .github/workflows/local-runner-canary.yml:99–103

safe_path() {
  [[ $1 =~ ^[A-Za-z0-9._/-]+$ ]] &&
  [[ $1 != /* ]] &&
  [[ "/$1/" != *"/../"* ]] &&
  [[ "/$1/" != *"/./"* ]]
}

This allows fixtures//lfs/canary.txt (double slash). On POSIX, // collapses to /, so it's not a meaningful bypass — but a downstream sha256sum check and LFS validation still gate on the actual file content, so there's no real exploit path. Cosmetic only.


What Works Well

Security model:

  • Every error path (auth-error, api-timeout, api-error, invalid-response, missing-config, missing-secret, no-idle-runner) returns an immutable hosted-runner result via Object.freeze(). There is no error path that can produce a self-hosted label.
  • canonicalHostedRunner validates the hosted fallback against the reserved label set AND the configured candidate labels, preventing a misconfiguration where the fallback string is itself a self-hosted label.
  • managedRunnerPrefix + ephemeral === true filters in selectIdleCandidate mean only known, disposable fleet runners are ever selected.
  • isDependabot uses github.actor == 'dependabot[bot]' || github.secret_source == 'Dependabot' — dual check prevents actor-name spoofing.
  • Public-repo guard, fork-PR guard, and rerun guard are all applied before any API token is used.
  • The observer App token has the minimum-viable scope (permission-organization-self-hosted-runners: read or permission-administration: read) and never receives a host/controller credential.
  • No pull_request_target or workflow_run events on any new or modified workflow — ✅ CLAUDE.md compliant.
  • All uses: references are SHA-pinned with version comments — ✅ CLAUDE.md compliant.

API pagination:

The declaredTotal consistency check across pages is a strong guard: if the runner inventory changes during pagination, the code fails to hosted rather than selecting from a partial snapshot. The loop termination conditions correctly handle edge cases including 0-runner inventories, exactly full final pages, and early truncation.

Canary ephemeral isolation proof:

Sentinel files in $HOME, work_root, $RUNNER_TEMP, $RUNNER_TOOL_CACHE, /tmp, and /usr/local/share written by the seed worker and verified absent by the freshness worker is a concrete, auditable proof of ephemeral isolation. The container-ID cross-check (/etc/hostname) adds a second independent identity proof.

Cache bridge anti-replay:

The hosted-to-self and self-to-hosted cache keys embed GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT, and the payload is validated verbatim (test "$(<"$CACHE_PATH/payload")" = "hosted-to-self:$GITHUB_RUN_ID:$GITHUB_RUN_ATTEMPT"). A replay from a different run cannot produce the correct payload.

Immutable workflow checkout in parity:

local-runner-parity.sh is invoked from .ci-workflows-contract/ (checked out at steps.contract.outputs.sha), which is itself validated against WORKFLOW_SHA with git rev-parse HEAD. This prevents a tampered caller repo from substituting a different parity script.

Pulumi deploy guard:

The OIDC policy contract validation is exact: [*?.] wildcard prohibition in rule fields, sub claim format reconstruction and comparison, unique-repository enforcement within a single contract, and byte-exact policy comparison via cmp -s. The symlink check on the contract file (! -L "$contract") and on the results files in osv-scan-guard.sh (realpath -e + parent-directory equality) are good mitigations against path-substitution in composite-action file handling.

PowerShell refactor:

The NUL-delimited git ls-files -z discovery is correct for paths with non-ASCII or special bytes. The Test-PathExcluded helper normalizing to forward slashes before -like matching is cross-platform safe. The hard-fail on analyzer engine errors (exit 2) rather than swallowing them is the right policy.

Generated selector pattern:

The IIFE-wrapped bundle in select-runner.yml with render-select-runner-workflow.cjs (check mode run in CI) keeps the source-of-truth in select-runner.cjs and the inlined copy always synchronized. The test for generation parity in selector-conformance.yml closes the drift gap.


Summary

Four of the six findings are documentation/clarity issues. The two substantive ones are the ubuntu-slim runner label (finding #1 — worth resolving before production routing goes live, because the selector job's trust envelope depends on it) and the configuredCandidateLabels / parseCandidateLabels asymmetry (finding #2 — code comment is the fix, not a behavior change). The rest are cosmetic or carry no exploit path.

The core security properties — fail-closed routing, read-only observer credential, ephemeral-only fleet selection, public/fork/Dependabot/rerun guards, reusable-workflow event isolation, and SHA pinning — all hold.

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 45s —— View job


Code Review — feat: add governed local runner routing (updated at 93c12f3)

  • Identify what changed since the previous review (at 764a372)
  • Review the incremental diff (fix: make selector gates reproducible in CI)
  • Post updated findings

Incremental Review — fix: make selector gates reproducible in CI

Four files changed since the prior review (764a372). Here is a detailed assessment of each change.


fetch-depth: 0 added to ci.yml and selector-conformance.yml

Files:

Why: local-runner-canary.test.cjs runs git show bb762391c41e9d12975fae25a06ac930050baba9:.github/workflows/local-runner-canary.yml (line 424–428) to verify the pinned reusable contract at a specific reviewed SHA. GitHub Actions defaults to fetch-depth: 1 (shallow clone), so this commit object is not present in CI unless the full history is fetched. This was the source of non-reproducibility.

lfs: true was already present and was needed for the fs.readFileSync(templateLfsPath) content check (line 456). Without it, that read returns the LFS pointer rather than the canonical content. The fetch-depth: 0 + lfs: true combination is the minimum necessary to make the test deterministic in both local and CI environments.

Assessment: Correct and well-scoped. The full history is genuinely required here — not a blanket policy.


Contract test added to local-runner-canary.test.cjs

File: local-runner-canary.test.cjs:393–395

for (const source of [rootCi, selectorConformance]) {
  assert.match(source, /fetch-depth: 0[\s\S]*?lfs: true/u);
}

This enforces the fetch-depth: 0 addition as a tested invariant, preventing accidental removal in a future refactor. The regex correctly spans newlines via [\s\S]*? and uses the u flag. It's coarser than the existing job-anchored assertions at lines 463–469 (those anchor to the specific job name + checkout action), but the combination is sufficient: the existing assertions already verify lfs: true is in the correct checkout step; the new one adds ordering with respect to fetch-depth: 0.

Assessment: Correct. The non-greedy cross-line match is the right tool here.


cache: false removed from two setup-node steps in local-runner-canary.yml

File: local-runner-canary.yml:202–205 and the second occurrence at the freshness-worker step.

In actions/setup-node@v6, package-manager-cache: false is the correct parameter for disabling package manager caching. The cache: false parameter was the v3.x/v4.x name and is either ignored or a no-op in v6. Both were already set to false, so this is a clean-up of a harmless redundancy, not a behavior change.

Note: setup-dotnet (line 200) retains cache: false — that is correct for the setup-dotnet action, which uses the older parameter name.

Assessment: Correct clean-up.


SC2317 added to ShellCheck disable in local-runner-canary.yml

File: local-runner-canary.yml:690

# shellcheck disable=SC2317,SC2329
on_cancel() { ... }
trap on_cancel INT TERM

SC2317 is "Command appears to be unreachable" — ShellCheck fires this for functions invoked only via trap because it cannot trace execution through the signal handler registration. SC2329 ("function is never called") was already suppressed for the same reason. Adding SC2317 is correct and makes the suppression set complete for what newer ShellCheck versions emit on this pattern.

Assessment: Correct.


Prior Findings — Status

None of the six prior findings were addressed by this commit; they remain open as documented in the previous review:

# Finding Status
1 ubuntu-slim runner label in select-runner.yml Open — worth resolving before production routing goes live
2 configuredCandidateLabels / parseCandidateLabels asymmetry needs a comment Open
3 runAttempt !== 1 in config-check block after early rerun guard Open
4 GitHub API version string "2026-03-10" untracked for drift Open
5 Canary select-runner SHA pin comment says governed selector review instead of # vX.Y.Z Open
6 safe_path allows // (cosmetic, no exploit path) Open

Summary

The fix: make selector gates reproducible in CI commit is focused and correct. The fetch-depth: 0 addition is the right fix for the root cause (a git show <old-sha> call that fails on GitHub Actions' default shallow clone), the contract test enforces it as an invariant, the cache: parameter clean-up is accurate, and the ShellCheck suppress extension is precise. No new issues were introduced.

@claude

claude Bot commented Jul 10, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 13s —— View job


Code Review — feat: add governed local runner routing (updated at 9596686)

  • Identify what changed since the previous review (at 93c12f3)
  • Review the incremental diff (fix: constrain hosted fallback labels)
  • Post updated findings

Incremental Review — fix: constrain hosted fallback labels

Four files changed since the prior review (93c12f3). Here is a detailed assessment of each.


APPROVED_HOSTED_RUNNERS allowlist added to canonicalHostedRunner

Files:

const APPROVED_HOSTED_RUNNERS = new Set([DEFAULT_HOSTED_RUNNER]);
// …
const unsafe =
  !APPROVED_HOSTED_RUNNERS.has(configured) ||
  RESERVED_SELF_HOSTED_LABELS.has(configuredLower) ||
  configuredCandidateLabels(input).some();

Why this matters: The previous implementation accepted any hostedRunner value that wasn't a reserved self-hosted label or a configured local candidate. That means ubuntu-26.04, ubuntu-22.04, windows-2022, or any arbitrary opaque string could pass through as the fallback runner — a governance gap, since only ubuntu-24.04 has been reviewed for this V1 contract.

Correctness assessment:

  • Short-circuit logic is correct. !APPROVED_HOSTED_RUNNERS.has(configured) || … short-circuits to unsafe = true for unapproved labels without evaluating the candidate-labels check. But critically, when configured IS approved (negation is false), evaluation continues through the reserved-label and candidate-collision checks. So an approved label that is also (mis)configured as a local candidate is still rejected — the allowlist is not a bypass of the downstream guards.

  • Case sensitivity is intentional and safe. APPROVED_HOSTED_RUNNERS.has(configured) is case-sensitive, while configuredLower comparisons below use .toLowerCase(). A caller passing "Ubuntu-24.04" will fail the allowlist check and be canonicalized to DEFAULT_HOSTED_RUNNER — the correct behavior; the fallback is still safe. GitHub runner labels are lowercase by convention, so no legitimate consumer is affected.

  • Empty/missing input path is safe. Lines 70–72 already normalize input.hostedRunner to DEFAULT_HOSTED_RUNNER when absent or non-string. DEFAULT_HOSTED_RUNNER ("ubuntu-24.04") is in APPROVED_HOSTED_RUNNERS, so the fallback path is always safe.

  • Prior finding feat!: convert reusable workflows to composite actions #2 (configuredCandidateLabels / parseCandidateLabels asymmetry) is now partially mitigated. Since the allowlist check runs first, a silent JSON parse error in configuredCandidateLabels that produces an empty label set can no longer allow an unapproved label to slip through — the allowlist rejects it regardless. The code-comment gap still exists for a future reader, but the exploit surface is closed.

Assessment: Correct and well-placed.


New test cases in select-runner.test.cjs

File: select-runner.test.cjs:102–103

["unapproved label", { hostedRunner: "some-other-self-hosted-label" }],
["unreviewed hosted generation", { hostedRunner: "ubuntu-26.04" }],

Two new fallback variants are added to the existing parameterized test loop (which runs over hosted-only, rerun, and public guard routes). This produces 6 new test cases covering:

  1. An opaque/arbitrary string that is clearly not a standard GitHub label — exercises the allowlist against labels that weren't self-hosted reserved names either.
  2. A label that looks like a legitimate future GitHub-hosted runner generation (ubuntu-26.04) but hasn't been approved — exactly the governance gap the commit is closing.

The naming distinction ("unapproved label" vs "unreviewed hosted generation") makes the intent of each case immediately legible in the test output.

Assessment: Correct and well-targeted.


select-runner.yml — inlined bundle and input description

File: select-runner.yml

The IIFE bundle inside the run: step is updated to match the source — as expected given the render-select-runner-workflow.cjs generation mechanism. The hosted-runner input description changes from:

Explicit GitHub-hosted runner returned on every fallback.

to:

Approved GitHub-hosted runner returned on every fallback; V1 permits ubuntu-24.04.

This surfaces the governance constraint at the workflow interface, so a consumer reading the inputs: block understands why arbitrary labels are not accepted, without needing to trace into the script.

Assessment: Correct and useful.


README.md — updated prose

File: README.md:198–207

The new paragraph is clearer than the previous version: it explicitly states "allowlists only the reviewed V1 value ubuntu-24.04" and enumerates all the input classes that get canonicalized (missing, malformed, unapproved, generic self-hosted, or configured local-candidate). This matches the actual code behavior precisely.

Assessment: Accurate and improved.


Prior Findings — Status

None of the six prior findings were addressed by this commit; they remain open:

# Finding Status
1 ubuntu-slim runner label in select-runner.yml:64 Open — resolve before production routing goes live; changes the trust envelope of the selector job
2 configuredCandidateLabels / parseCandidateLabels asymmetry needs a comment Open — the allowlist now closes the downstream gap, but the code-comment explaining the intentional divergence is still missing
3 runAttempt !== 1 in config-check block after early rerun guard — redundancy note Open
4 GitHub API version string "2026-03-10" untracked for drift Open
5 Canary select-runner SHA pin comment says governed selector review instead of # vX.Y.Z Open
6 safe_path allows // (cosmetic, no exploit path) Open

Summary

The fix: constrain hosted fallback labels commit closes a real governance gap — without it, any string could be passed as the hosted fallback runner, bypassing the implicit assumption that only reviewed labels are ever used. The implementation is correct: the allowlist is the first guard, it doesn't short-circuit the downstream safety checks (reserved-label collision, candidate-label collision), case sensitivity follows runner label conventions, and the empty/missing input path was already safe. Two targeted test cases cover the newly enforced constraint. The README and input description updates are accurate and improve clarity for consumers.

No new issues are introduced by this change.

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 16s —— View job


Code Review — feat: add governed local runner routing (updated at b99eb6a)

  • Identify what changed since the previous review (at 9596686)
  • Review the incremental diff
  • Post updated findings

Incremental Review — chore: sync standards components + merge from main

Two commits since the prior review (9596686):

  • f96ba7b chore: sync standards components (#75) — 5 files changed
  • b99eb6a Merge branch 'main' into codex/local-runner-selector — merge commit, no new diffs

Five files changed, all standards/config components. Here is a detailed assessment of each.


comment-hygiene-patterns.sh — XXX split into case-sensitive rule

File: .github/actions/comment-hygiene/comment-hygiene-patterns.sh:54–63

The most substantive functional change in this commit. Previously XXX was bundled with FIXME and HACK under nocasematch, which would match xxx, Xxx, XXX identically. Now XXX is a separate case-sensitive check:

shopt -u nocasematch
if [[ "$line" =~ (^|[^[:alnum:]_])XXX([^[:alnum:]_]|$) ]]; then
  shopt -s nocasematch
  printf '%s:warning-marker:XXX\n' "$lineno"
  violations=$((violations + 1))
  continue
fi
shopt -s nocasematch

Correctness assessment:

  • shopt state is correctly preserved across both branches. On a match, nocasematch is restored before the continue (line 58). On no-match, it is restored at line 63. Every subsequent rule in the same iteration and every future iteration starts with nocasematch enabled — the invariant holds. ✅
  • shopt state when FIXME/HACK fires. If FIXME or HACK matches at line 49 and the function continues, nocasematch is still ON (the XXX block was never reached). The next iteration also starts with nocasematch ON. On that next iteration, line 56 correctly disables it for the XXX check. ✅
  • Removal of [[ -z "$entry" ]] && continue. Safe — the awk filter on line 124 (/^[[:space:]]*(\/\/|#|\/\*|\*|<!--)/) only emits lines that start with a comment marker, so empty entries cannot appear in practice. Even if one somehow did, no pattern matches an empty string. ✅
  • The rationale is accurate. xxx-large is a CSS font-size keyword and xxx appears in placeholder text; case-insensitive matching would produce false positives. The change is motivated and correct.

Assessment: Correct and well-handled.


.shellcheckrc — comment clarification for require-double-brackets

File: .shellcheckrc:43

The comment now specifies SC2292 as the code to use in per-file disable directives, noting that ShellCheck disable directives take SC codes, not the option name. This is accurate — # shellcheck disable=require-double-brackets is not a valid disable directive; # shellcheck disable=SC2292 is. The prior comment was subtly wrong.

Assessment: Accurate correction.


PSScriptAnalyzerSettings.psd1 — broken sentence fixed

File: PSScriptAnalyzerSettings.psd1:6

The prior comment had a line-break mid-sentence: "Severity below is the\n# single source of truth…". Now a single coherent sentence, with a note that the docs call this "profile values taking precedence" rather than citing "Example 8" (which could rot as the docs are reorganized).

Assessment: Accurate and more maintainable reference.


_typos.toml — UUID regex comment improved

File: _typos.toml:20

The old comment said "typos skips… opaque hex segments trip the word splitter (e.g. ba54 -> 'bash')". The new comment correctly notes that typos skips uniform-case UUIDs natively (lowercase or uppercase only), but a mixed-case UUID trips the word splitter because the case-change signals a word boundary. The example is updated to Ba54 → Ba → "By", which accurately reflects the splitter behavior.

Assessment: More accurate explanation of what the regex actually guards against.


lychee.toml — HTTPS clone URL comment clarified

File: lychee.toml:35

Minor comment reword explaining why the .git arm is needed (the trailing dot after the repo name would otherwise not match the [/#?] boundary check). No behavioral change.

Assessment: Correct, minor.


Prior Findings — Status

No prior findings were addressed by either commit since 9596686. All six remain open:

# Finding Status
1 ubuntu-slim runner label in select-runner.yml:64 Open — resolve before production routing goes live; the selector job trust envelope depends on it
2 configuredCandidateLabels / parseCandidateLabels asymmetry needs a comment Open — exploit surface is closed by the allowlist, but the intentional divergence is still undocumented
3 runAttempt !== 1 in config-check block after early rerun guard Open — redundancy note for future maintainers
4 GitHub API version string "2026-03-10" untracked for drift Open
5 Canary select-runner SHA pin comment says governed selector review instead of # vX.Y.Z Open
6 safe_path allows // (cosmetic, no exploit path) Open

Summary

The two commits since the last review are a standards sync and its merge. The substantive change is the XXX case-sensitivity split in comment-hygiene-patterns.sh — the shopt sandwiching is correct on both branches, the rationale is sound, and no new issues are introduced. Everything else is comment accuracy improvements across config files.

The six prior findings remain open and unchanged. Finding #1 (ubuntu-slim) is the only one with a security relevance before production routing goes live.

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 47s —— View job


Code Review — feat: add governed local runner routing (updated at c34f0aa)

  • Identify what changed since the previous review (at b99eb6a)
  • Review the incremental diff (10 commits, 685 net additions across 7 files)
  • Post updated findings

Incremental Review — fix: enforce Linux runner OS namespacedocs: complete selector contamination contract

Ten commits since the prior review (b99eb6a). Seven files changed. Here is a detailed assessment of each change cluster.


1. normalizedLabel() centralization — select-runner.cjs

File: select-runner.cjs:47–51

function normalizedLabel(value) {
  return value.toLowerCase();
}

All previously inline .toLowerCase() calls are replaced with this helper. The dedup check in parseCandidateLabels is upgraded to new Set(labels.map(normalizedLabel)).size !== labels.length, which now rejects case-only duplicates (["duplicate","DUPLICATE"]). A corresponding test case is added. This is a clean, correct refactor.

Assessment: Correct.


2. validateRunner tightening — select-runner.cjs

File: select-runner.cjs:222–245

Three additions:

  • id required as integer: !Number.isInteger(runner.id) rejects undefined, "1", and 1.5. This enables the cross-page duplicate-ID check below.
  • os required as non-empty string: !exactNonEmptyString(runner.os) rejects undefined, "", and " linux ".
  • ephemeral now optional but type-guarded: The previous typeof runner.ephemeral !== "boolean" required the field. The new guard Object.hasOwn(runner, "ephemeral") && typeof runner.ephemeral !== "boolean" allows omission but rejects a present non-boolean (e.g., null, "true", 1). This matches the 2026-03-10 OpenAPI schema where ephemeral is declared optional.
  • label.name tightened to exactNonEmptyString: Previously typeof label.name !== "string" permitted empty strings.

The two-phase structure is also improved — null/non-object check is separated into its own throw block before the field-level validations, which produces a clearer error message.

Assessment: Correct. The ephemeral relaxation is the right fix for live API responses; the null/non-object early exit is cleaner.


3. Duplicate runner ID check in pagination — select-runner.cjs

File: select-runner.cjs:256

A runnerIDs Set accumulates IDs across pages; a duplicate throws InvalidResponseError. This defends against a race-reconstructed inventory where the same runner appears on multiple pages (e.g., registered on page 2 after being on page 1, or an adversarially crafted response). Test coverage: a cross-pagination duplicate correctly routes invalid-response. ✓


4. Label namespace contamination model — select-runner.cjs

File: select-runner.cjs:330–399

This is the largest and most security-relevant change in this batch. The key invariant: since downstream runs-on contains only the selected label, GitHub can route to any online runner bearing that label — not specifically the idle one observed by the selector. A contaminated label must never be returned.

Contamination trigger: A runner taints every configured label it carries when any of these holds:

  • Name doesn't start with managedRunnerPrefix
  • ephemeral === false (explicitly declared as persistent)
  • os.toLowerCase() is not in {"linux", "unknown"} (non-Linux platform)

Scope: Contamination is per-label. A distinct configured label that has no contaminating bearers remains safe. Ordered candidates skip the contaminated label and return the next clean one.

Correctness assessment:

  • Offline runners are checked for contamination too — an offline unmanaged runner bearing a configured label taints it. This is correct because "online" state is not stable.
  • A managed-prefix runner with omitted ephemeral and valid OS is not contaminating. The JIT trust assumption applies: the governed prefix+label contract says these are controller-created one-job workers.
  • Explicit ephemeral: false IS contaminating (line 358), even if the runner is in the managed namespace. An operator-declared persistent runner must not be selected.
  • idleRunnerCount in the returned object counts only eligible runners on clean labels, not contaminated-label runners. This means the consumer sees 0 idle runners on a fully contaminated namespace — the invalid-response path propagates that.
  • labelNamespaceInvalid: contaminatedLabelKeys.size > 0 && safeLabels.length === 0 is the right discriminant: it fires only when contamination exists and it consumed all configured labels. When partial contamination leaves some labels safe, the flag is false and selection continues on clean labels.
  • selectedLabel?.label preserves the configured spelling in the output (not the normalized key), so the caller's runs-on gets back the casing they configured. ✓

One observation worth calling out: When a contaminated label has an eligible idle runner (managed prefix, omitted ephemeral, valid OS, online, not busy) but the label is still contaminated by a sibling that violates the contract — that eligible runner is excluded from idleRunners because safeLabelKeys doesn't include the contaminated key. idleRunnerCount will be 0 for runners exclusively on contaminated labels. This is the correct behavior (you cannot safely route to the label), but operators may see a confusing invalid-response reason when there are technically idle runners. The README now explains this.


5. V1_MANAGED_RUNNER_OSES set — select-runner.cjs

File: select-runner.cjs:7

const V1_MANAGED_RUNNER_OSES = new Set(["linux", "unknown"]);

unknown is accepted because the GitHub JIT configuration endpoint (/jitconfig) reports os: unknown in live responses, confirmed in the 2026-03-10 OpenAPI. The comparison in contamination check uses runner.os.toLowerCase(), so "LINUX", "Linux", "UNKNOWN" all pass. Tests cover unknown, UNKNOWN, and LiNuX — and verify that windows and macOS contaminate.

The acceptance of unknown is explicitly documented in the README as a trust assumption rather than an OS attestation, which is the right framing.

Assessment: Correct.


6. preflight comment for runAttempt !== 1select-runner.cjs

File: select-runner.cjs:189–192

Prior finding #3 is now addressed with a code comment explaining that runAttempt !== 1 in the config check catches 0, fractional, NaN, and undefined values without misclassifying them as reruns. A new parameterized test covers undefined, NaN, 0, 1.5, -1 — all route missing-config. ✓


7. configuredCandidateLabels comment — select-runner.cjs

File: select-runner.cjs:55–63

Prior finding #2 is now addressed. The comment explicitly documents the intentional asymmetry: this function is a safety-only superset for the hosted-label collision check and retains the legacy path for malformed JSON, while parseCandidateLabels is the authoritative routing parser that rejects malformed JSON, reserved labels, and duplicates before selection. ✓


8. Runtime OS/arch assertions in local-runner-canary.yml

File: local-runner-canary.yml:279–284 (and four other assertion blocks)

RUNNER_OS: ${{ runner.os }}
RUNNER_ARCH: ${{ runner.arch }}
...
[[ "$RUNNER_OS" = Linux ]] || fail "runner.os is not Linux"
[[ "$RUNNER_ARCH" = X64 ]] || fail "runner.arch is not X64"

All 5 selected-runner assertion blocks now check runner.os and runner.arch against the official runner context values before doing any substantive work. This is the second layer of the defense: the selector checks what the inventory API reports; the canary checks what GitHub's own context values report at runtime.

The test verifies: exactly 5 occurrences of each env var and each assertion, the two checks appear in assertion scripts, and both wrong-OS (Windows/X64) and wrong-arch (Linux/ARM64) are rejected by the assertion script. ✓


9. SHA chain updates — local-runner-canary.yml and templates/

The canary pins the selector at 66e3e97 (the commit that enforces the Linux OS namespace). The template pins the canary at ca3c19c (the commit that adds RUNNER_OS/RUNNER_ARCH runtime checks). Superseded SHAs (4943b1c, d948779, 257f584 for selector; bb762391, 2dfd2c9 for canary) are explicitly tracked in local-runner-canary.test.cjs and asserted absent from all pinned references. This prevents accidental downgrade and makes the intent of each chain link auditable. ✓


10. select-runner.test.cjs — new test coverage

File: select-runner.test.cjs

308 net additions. Key new coverage:

  • Invalid runAttempt values (undefined, NaN, 0, 1.5, -1) → missing-config
  • Case-only duplicate candidate labels → invalid-response
  • Omitted ephemeral on unrelated runner — not contaminating, ineligible
  • Omitted ephemeral on matching managed runner — eligible and selected
  • Explicit ephemeral: false on same-label runner — label contaminated → invalid-response
  • Present non-boolean ephemeral — inventory validation fails
  • Wrong-prefix sibling on same label — label contaminated → invalid-response, idle count 0
  • windows/macOS OS sibling — label contaminated
  • unknown/UNKNOWN/LiNuX OS — accepted under V1 contract
  • Wrong-OS/wrong-prefix on unrelated label — does not poison a good sibling label
  • Ordered candidate skip: contaminated first label, clean second selected
  • Configured spelling preserved when inventory uses different casing
  • Duplicate runner ID across pagination → invalid-response
  • Missing/malformed id and os fields → invalid-response

All tests maintain a global monotonic nextRunnerID counter to ensure IDs are unique by default, which is necessary now that validateRunner requires integer IDs and listRunners rejects duplicates.


Prior Findings — Status

# Finding Status
1 ubuntu-slim runner label in select-runner.yml:64 Open — this is the only remaining item with a trust-envelope implication before production routing goes live
2 configuredCandidateLabels / parseCandidateLabels asymmetry Resolved — comment added in this batch
3 runAttempt !== 1 redundancy note after early rerun guard Resolved — comment added in this batch
4 GitHub API version string "2026-03-10" untracked for drift Open — cosmetic / no exploit path
5 Canary select-runner SHA pin comment says governed selector review instead of # vX.Y.Z Open — cosmetic, intent is clear
6 safe_path allows // Open — cosmetic, no exploit path

What Works Well

Contamination model correctness: The label contamination pass runs over the complete paginated inventory, not just the idle subset. An offline unmanaged runner that happens to carry a configured label is caught. A managed runner with ephemeral: false is caught. A managed runner with os: windows is caught. And critically, contamination is per-configured-label: a clean lower-priority label remains selectable when its own bearers all pass the contract, because GitHub cannot route a runs-on: <clean-label> job to a bearer of only the contaminated label.

Ephemeral field relaxation is correct: The 2026-03-10 OpenAPI declares ephemeral optional on runners. The new code path — !Object.hasOwn(runner, "ephemeral") || runner.ephemeral === true — correctly allows omission while keeping explicit false as authoritative exclusion and keeping present non-boolean as a validation failure.

Dual-layer OS verification: The selector rejects inventory-reported OS violations (API layer), and the canary rejects runner.os != Linux or runner.arch != X64 at runtime (context layer). These are independent evidence sources — the REST inventory and the runner context are populated by different GitHub subsystems.

os: unknown acceptance is documented and bounded: unknown is accepted explicitly under the JIT trust assumption, not as a generic wildcard. The README's wording ("accepted solely under the same governed prefix-and-label/JIT trust assumption") makes the distinction clear. Any bearer of a configured label reporting a different OS string (e.g., windows, macOS) still contaminates.

Test coverage is comprehensive: The new suite covers every contamination trigger independently and in combination, including cross-label independence (clean sibling survives contaminated first candidate) and runner label case variants.


Summary

This batch closes the main behavioral gap: a non-Linux or non-ephemeral runner bearing a managed label could previously be selected (if online and idle). The new contamination model prevents that by failing the entire label rather than just excluding that runner. Prior findings #2 and #3 are resolved with targeted comments. The remaining open items (#1, #4, #5, #6) are unchanged from the prior review — finding #1 (ubuntu-slim) is the only one that warrants resolution before production routing goes live, as it determines whether the selector job's observer token mint runs on fully hosted or org-level infrastructure.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Revalidated the complete review history against final signed head c34f0aa and current official GitHub contracts:

Finding Classification Evidence / disposition
ubuntu-slim trust envelope Incorrect GitHub documents ubuntu-slim as a standard GitHub-hosted runner for public and private repositories. GitHub provisions a new unprivileged container per job with hypervisor level-2 isolation; its 15-minute platform cap applies only to the selector job, not downstream jobs. https://docs.github.com/en/actions/reference/runners/github-hosted-runners
Candidate-parser asymmetry Resolved The safety-superset parser and authoritative routing parser now have an explicit invariant comment and adversarial coverage.
runAttempt guard Resolved The malformed-attempt vs rerun distinction is documented and parameterized tests cover undefined, NaN, zero, fractional, and negative values.
API version lifecycle Valid deferred maintenance 2026-03-10 is GitHub's current/latest supported version. Retirement still fails hosted; the release/drift lifecycle tracks future review rather than mutating the production pin automatically. https://docs.github.com/en/rest/about-the-rest-api/api-versions
Canary SHA comment / Dependabot Incorrect as a blocker The canary intentionally pins a reviewed immutable first-party commit. GitHub/Dependabot supports repository-syntax SHA references; a tag-style comment is explanatory metadata, not the security boundary.
safe_path interior // Valid cosmetic defer POSIX treats interior repeated slashes as one; absolute/traversal checks plus content/hash validation remain intact. https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap03.html

The live API issue discovered after the earlier reviews is fixed: official OpenAPI makes ephemeral optional, labels are case-insensitive, JIT inventory may report os: unknown, and downstream label routing requires whole-namespace contamination checks. The final chain is template → canary ca3c19c → selector 66e3e97; consumers pin production head c34f0aa. Independent implementation and official-contract reviews both passed, 126/126 Node tests passed (93 selector and 16 canary), and all 33 hosted PR checks are green.

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 6m 34s —— View job


Code Review — feat: add governed local runner routing (updated at da4bec8)

  • Identify what changed since the previous review (at c34f0aa)
  • Evaluate kyle-sexton's rebuttal of prior findings
  • Review the incremental diff (4 commits: production HA proof + reruns hardening)
  • Post updated findings

Prior Findings — Rebuttal Assessment

kyle-sexton's 2026-07-11T05:00:24Z comment disputes or defers four of the six prior findings. Assessment of each:

# Finding Rebuttal verdict
1 ubuntu-slim runner label changes selector trust envelope Rebuttal accepted — finding was incorrect. GitHub documents ubuntu-slim as a standard GitHub-hosted runner with level-2 hypervisor isolation, available for both public and private repos. The prior review incorrectly assumed it was an org-level custom runner. The 15-minute cap is satisfied by the 5-minute timeout-minutes on the selector job.
2 configuredCandidateLabels / parseCandidateLabels asymmetry Resolved in c34f0aa batch.
3 runAttempt !== 1 redundancy note Resolved in c34f0aa batch.
4 API version string "2026-03-10" untracked for drift Deferred correctly. 2026-03-10 is the current supported version; retirement still fails hosted; the drift lifecycle tracks future review. Valid deferred maintenance.
5 Canary SHA pin comment format Rebuttal accepted — finding was incorrect. The canary intentionally pins a specific reviewed commit that must not be auto-bumped. The comment # governed selector review is explanatory metadata, not a version tag for Dependabot. The CLAUDE.md SHA-pin requirement is fully met.
6 safe_path allows // Deferred correctly. POSIX collapses interior repeated slashes; content/hash validation remains intact. Cosmetic with no exploit path.

All six prior findings are now resolved, closed, or correctly deferred.


Incremental Review — feat: add production runner HA prooffix: repin production HA proof caller

Four commits since the prior review (c34f0aa). Eight files changed. Here is a detailed assessment of each commit cluster.


1. feat: add production runner HA proofproduction-ha-proof.cjs + production-ha-proof.yml

JS module (production-ha-proof.cjs)

The module mirrors the pagination, validation, and evidence patterns from select-runner.cjs with several additions appropriate to a production acceptance proof:

  • assertPositiveInteger / assertSafeString / validateGroup / validateRepository / validateRunner form a strict chain. validateRunner requires: integer ID, SAFE_NAME-matching name, name within host.runnerPrefix, OS in ACCEPTED_RUNNER_OSES ({"linux","unknown"}), status in {"online","offline"}, boolean busy, optional-but-type-guarded ephemeral, non-empty labels, and the production routing label present. This mirrors the V1 contract from the selector and is correct.
  • Cross-group runner deduplication: seenRunnerIds spans both host groups, ensuring no runner appears in both ci-local-melo-desk-001 and ci-local-melo-lap-001. This is the correct guard against the inventory misreporting a runner as belonging to two independent groups.
  • assertUniqueIds on runner groups: Ensures the two group IDs are distinct before validateGroup is called on each.
  • Repository set parity: repositorySetKey(evidenceGroups[0].selectedRepositories) === repositorySetKey(evidenceGroups[1].selectedRepositories) — both production groups must have exactly the same private repository access. Sorted by ID before comparison so order differences don't produce false mismatches.
  • PROOF_REPOSITORY presence check: The proof repo itself (melodic-software/ci-runner-canary) must be in the shared repository set, ensuring the caller has access to the inventory it is authorized to read.
  • waitForDesktopDrain validates maxWaitMinutes is 5–25 before entering the polling loop, with a stable-observation counter (REQUIRED_STABLE_DRAIN_OBSERVATIONS = 2) that resets on any non-zero desktop observation. The deadline is checked after each observation, preventing an infinite loop. Correct.
  • commonEvidence requires RUN_ATTEMPT to match /^1$/u — a JS-layer rerun guard independent of the YAML if: guard.
  • Evidence files written with mode: 0o600 — owner-read/write only. Correct.
  • Failure path writes structured evidence: The catch block calls safeFailure(error) to produce a clean {code, message} and then writeEvidence(evidencePath, {...base, status: "failed", failure, ...}). Since evidencePath is bound before the try, a failure in the try block always has a valid path to write to. The only scenario where this fails is if requiredEnvironment(env, "EVIDENCE_PATH") itself throws — but EVIDENCE_PATH is set unconditionally to ${{ runner.temp }}/... in the YAML env block, so that can't happen.

Workflow (production-ha-proof.yml)

  • workflow_call only, permissions: {} at workflow level, permissions: {} on every job — CLAUDE.md compliant. No pull_request, pull_request_target, or workflow_run triggers.
  • preflight job uses CALLER_WORKFLOW_REF / EXPECTED_CALLER_WORKFLOW_REF to hard-fail on any caller other than melodic-software/ci-runner-canary/.github/workflows/production-ha-proof.yml@refs/heads/main. This is a branch ref (not a SHA), which is correct: the reviewed SHA-pinned template caller runs from that branch. The reusable workflow can only be invoked via its @<sha> pin in the template, so the branch ref check validates the caller's identity without needing to predict which commit will be on main.
  • All substantive jobs run ubuntu-24.04 except production-execution, which runs on ${{ needs.select-production-runner.outputs.runner }} — the selector's direct output. The selector already guarantees this is a governed label or ubuntu-24.04 fallback; the validate-selection job hard-fails if it's not the local production label.
  • select-production-runner pins selector at 66e3e974e9c0132150cc982cdd76aca284df19de — the reviewed selector SHA from the prior batch. This is the same SHA used by local-runner-canary.yml. Consistent.
  • EXPECTED_HOST ternary ${{ inputs.mode == 'desktop-only' && 'melo-desk-001' || 'melo-lap-001' }} is correct: only desktop-only acquires the desktop; laptop-only, failover, and laptop-power all acquire the laptop. In failover mode, failover-hold ensures the desktop drains before production-execution runs, so the laptop is the only available host at acquisition time.
  • container_id from /etc/hostname is guarded by [[ "$container_id" =~ ^[A-Za-z0-9._-]{1,128}$ ]] before printf-interpolation into JSON. No JSON-special characters can appear. Correct.
  • Laptop-power heartbeat loop caps sleep_for to remaining before the final sleep to avoid overshooting the minimum. The post-loop test "$final_elapsed" -ge "$minimum" is a hard assertion that the loop ran long enough regardless of timing variation. Correct.
  • Evidence sanitization: the artifacts include REST group IDs, runner names, observation timestamps, and run correlation. They explicitly exclude App tokens, private keys, JIT configuration, and raw API responses. The README documents this limitation set precisely.
  • No secrets: inherit, no untrusted checkout, no raw gh run or gh api calls in the workflow. ✓

2. fix: harden production HA proof reruns — four targeted hardening additions

This commit adds four independent rerun guards and two token-scope tightenings that were missing from the initial implementation.

REF_PROTECTED check (production-ha-proof.yml:69):

[[ "$REF_PROTECTED" = true ]] || fail "production proof ref is not protected"

github.ref_protected is true only when the triggering ref is protected in the repository settings. Without this check, a branch named main that is not actually protected (e.g., protection rules were temporarily disabled) would pass the SOURCE_REF check. Defense-in-depth is correct here.

if: github.run_attempt == 1 on inventory, select-production-runner, validate-selection, failover-hold: These were missing from the initial implementation. Without them, on a rerun, preflight would fail (routing to hosted), but the YAML scheduler might still attempt to evaluate conditions on downstream jobs. Adding the guard to each job ensures no production API call, token mint, selector invocation, or observer hold can execute on attempt ≥ 2, regardless of how preflight fails.

always()!cancelled() + github.run_attempt == 1 on production-execution: always() would have run the job even when all upstreams were cancelled (e.g., if the workflow itself was cancelled while failover-hold was sleeping). !cancelled() allows the job to run when upstreams complete, fail, or skip — but not when the workflow is cancelled. Combined with github.run_attempt == 1, no production-execution can occur on a rerun or a cancelled-then-restarted run.

The condition also correctly handles the non-failover case:

(inputs.mode != 'failover' || needs.failover-hold.result == 'success')

When mode != 'failover', failover-hold is skipped (result == 'skipped'); inputs.mode != 'failover' short-circuits the check to true. When mode == 'failover', only a successful hold allows execution. ✓

repositories: ci-runner-canary on both observer token mints: The create-github-app-token action's repositories parameter scopes repo-level permissions to the named repository. For org-level permissions (organization-self-hosted-runners: read), the scope is already org-wide by the permission grant; the repositories parameter narrows what the token can do for repo-level operations. This is a correct tightening that limits the token's blast radius. ✓


3. feat: pin production HA proof caller + fix: repin production HA proof caller — template and contract tests

Template caller (templates/ci-runner-canary/.github/workflows/production-ha-proof.yml):

  • Pins at fdf0e6e5905e50be8e332f9bc522e24947a61b13 (the hardened commit) — not the initial 4e2fe88. Correct: the published template must pin to the hardened implementation, not the pre-hardening version.
  • No runner-label or prefix inputs in the template — the reusable workflow owns fleet details. ✓
  • fromJSON(inputs.drain-wait-minutes) correctly casts the choice string input to number before passing it to the number-typed reusable input. ✓
  • No secrets: inherit — explicit observer-private-key only. ✓

Contract tests (production-ha-proof.test.cjs):

  • implementationSha is correctly set to fdf0e6e (the hardened commit) — the test verifies the template pins this exact SHA and that the SHA's workflow content includes the rerun guards, REF_PROTECTED check, and repositories: ci-runner-canary scoping.
  • "every routed or evidence-producing job independently rejects reruns" — asserts github.run_attempt == 1 appears in the if: header of all 5 substantive jobs.
  • "production execution is cancellation-aware while allowing a skipped hold" — asserts !cancelled() and not always(), and verifies the correct failover-hold exception clause.
  • "observer secret is explicit and every API contract remains read-only" — asserts repositories: ci-runner-canary appears exactly twice, no write permissions, no POST/PUT/PATCH/DELETE patterns, no cancel or rerun paths, no gh api/gh run calls.
  • "pinned reusable commit contains the exact caller and selector contracts" — runs git show fdf0e6e:.github/workflows/production-ha-proof.yml and asserts the caller workflow ref, selector SHA, REF_PROTECTED env binding, !cancelled() condition, and repositories: ci-runner-canary count are all present in the pinned content.

What Works Well

Failure-closed evidence path: Every failure branch writes a structured {status: "failed", failure: {code, message}} evidence artifact. An operator reviewing evidence always sees why the proof failed, not just a missing artifact.

Cross-group runner ID deduplication (seenRunnerIds): A runner that registers itself in both production groups (e.g., after a controller misconfiguration) is caught before the proof can report passing topology. This is the correct guard for the invariant that ci-local-melo-desk-001 and ci-local-melo-lap-001 must be genuinely independent capacity.

Three-layer rerun rejection: (1) YAML if: github.run_attempt == 1 on every substantive job; (2) bash [[ "$RUN_ATTEMPT" = 1 ]] in preflight (before any token is minted); (3) JS requiredEnvironment(env, "RUN_ATTEMPT", /^1$/u) in commonEvidence. Each layer independently prevents a rerun from producing evidence that could be mistaken for a first-attempt production proof.

Limitations explicitly recorded in evidence: The limitations field documents restGroupIdIsScaleSetId: false, scaleSetIdObserved: false, controllerCapacityObserved: false, and batteryOrLogonStateObserved: false. An operator reading the artifact cannot mistake REST group IDs for controller scale-set IDs, and cannot treat the workflow artifact as a substitute for physical host evidence.

Immutable canary chain consistency: local-runner-canary.yml pins the selector at 66e3e97; production-ha-proof.yml pins the same selector SHA; the test for production-ha-proof.yml verifies the pinned content contains that exact SHA. Both acceptance paths use the same reviewed selector revision.


Summary

All six prior findings are closed: findings #2 and #3 were resolved by code changes in the previous batch; findings #1 and #5 were correctly disputed by kyle-sexton (the earlier review was wrong about ubuntu-slim being a non-standard label); findings #4 and #6 are validly deferred. No new findings from this batch.

The production HA proof is the correct capstone for the local runner rollout: it adds a separate two-host acceptance proof that is failure-closed, observer-read-only, rerun-hardened, and evidence-emitting. The fdf0e6e rerun hardening commit is the correct implementation SHA to publish — it closes gaps in the initial implementation before any external caller pins the workflow. The template correctly pins to the hardened commit, not the initial one. Test coverage of the new invariants (rerun guards, cancellation awareness, observer scope, immutable content at the published SHA) is comprehensive.

Pass the governed cache epoch explicitly through the private canary seed and fail closed when it is missing.
@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 46s —— View job


Code Review — feat: add governed local runner routing (updated at 68b9f40)

  • Identify what changed since the previous review (at da4bec8)
  • Review the incremental diff (fix: wire governed canary cache epoch (#76))
  • Post updated findings

Incremental Review — fix: wire governed canary cache epoch

One commit since the prior review (da4bec8). Three files changed.


Template caller wires CI_CACHE_EPOCH variable

File: templates/ci-runner-canary/.github/workflows/local-runner-canary.yml:27

cache-epoch: ${{ vars.CI_CACHE_EPOCH }}

The cache-epoch input on the reusable workflow has type: string with a hardcoded default of v1 (local-runner-canary.yml:34). Before this commit, the template caller passed no explicit value — so every canary run used v1 regardless of what the production IaC had provisioned. That breaks cross-epoch cache isolation when IaC changes the epoch.

Correctness:

  • CI_CACHE_EPOCH is a repository variable (not a secret), which is the right credential class for a cache epoch string that appears in log output and cache key metadata.
  • If CI_CACHE_EPOCH is unset, GitHub resolves vars.* to an empty string. The preflight validates the value against ^[A-Za-z0-9._-]{1,32}$ (local-runner-canary.yml:115), which requires at least one character. An unset variable fails the regex — producing "invalid cache epoch" — before any local work runs. The README's statement that "the reusable preflight rejects a missing or unsafe epoch before local work" is accurate.
  • The epoch is embedded in all four cache keys (lines 246, 462, 516, 552). Driving it from the IaC-owned variable means IaC can invalidate the entire cache bridge by rotating CI_CACHE_EPOCH, consistent with the intent of the "governed cache epoch" contract.

Assessment: Correct. The fix closes a real gap — without it, the epoch never changed and IaC couldn't govern cache invalidation through its standard variable.


Contract test enforces the new wiring

File: .github/scripts/local-runner-canary.test.cjs:479–482

assert.match(
  templateWorkflow,
  /cache-epoch: \$\{\{ vars\.CI_CACHE_EPOCH \}\}/u,
);

This assertion is added to the "canonical private seed pins the corrected reusable contract" test, between the existing observer-client-id and observer-private-key assertions — matching the order in the template workflow. The regex escapes the ${{/}} delimiters and the . in vars.CI_CACHE_EPOCH correctly. The u flag is consistent with every other assertion in this test.

Assessment: Correct. The wiring is now an enforced invariant — a future diff that drops or changes the cache-epoch line without updating the contract test will be caught by the suite.


README documents the new variable requirement

File: templates/ci-runner-canary/README.md:12–14

- repository variable `CI_CACHE_EPOCH`, provisioned by IaC and matching the
  production cache epoch;

The list of required repository configuration items now includes CI_CACHE_EPOCH, with the key operational note that it is IaC-provisioned. The updated prose ("The caller passes the governed epoch and that one secret explicitly") correctly distinguishes the epoch (variable, explicit) from the observer key (secret, explicit).

Assessment: Accurate and useful for operators bootstrapping the canary repository.


Prior Findings — Status

All six prior findings were fully resolved or correctly deferred in the previous review (da4bec8). This commit does not re-open any of them.

# Finding Status
1 ubuntu-slim runner label in select-runner.yml Closed — rebuttal accepted; ubuntu-slim is a standard GitHub-hosted runner
2 configuredCandidateLabels / parseCandidateLabels asymmetry Resolved
3 runAttempt !== 1 redundancy note Resolved
4 GitHub API version "2026-03-10" untracked for drift Deferred correctly
5 Canary SHA pin comment format Closed — rebuttal accepted; first-party reviewed pin intentionally not tagged
6 safe_path allows // Deferred correctly

Summary

fix: wire governed canary cache epoch is a small, correct fix that closes a real governance gap: without it, the template caller always used the hardcoded default v1 epoch, making IaC's CI_CACHE_EPOCH variable a no-op and preventing governed cache invalidation. The fix is three lines, all three files are consistent with each other (template wires the variable; test enforces the wiring; README documents the requirement), and the fail-closed behavior on an unset variable is already guaranteed by the existing preflight regex. No new issues are introduced.

Install and checksum-pin the same ShellCheck release for shared Actionlint execution on hosted and local workers, with runtime parity proof.
@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 3s —— View job


Code Review — feat: add governed local runner routing (updated at 3fa4a44)

  • Identify what changed since the previous review (at 68b9f40)
  • Review the incremental diff (fix: pin ShellCheck for Actionlint parity)
  • Post updated findings

Incremental Review — fix: pin ShellCheck for Actionlint parity

One commit since the prior review (68b9f40). Six files changed.


Motivation

actionlint uses whatever ShellCheck binary is on PATH at runtime. Ubuntu-hosted runners preinstall ShellCheck, but the version varies by image generation and is absent on a generic self-hosted worker. The previous setup silently depended on the image's ambient ShellCheck for embedded run: script checks, creating a divergence risk: a self-hosted runner would receive no ShellCheck linting, and a future hosted image update could silently bump or remove the binary. This commit eliminates that dependency.


1. actionlint/action.yml — explicit ShellCheck install step

File: .github/actions/actionlint/action.yml:26–56

Two new inputs (shellcheck-version, shellcheck-sha256) and a new "Install ShellCheck for embedded scripts" step that uses the shared install-release.sh with a checksum-verified URL — the same installation pattern used by every other tool in this repo.

Correctness:

  • Version and hash match the canonical ShellCheck action exactly (0.11.0 / b7af85e4...). Reading both files confirms the strings are identical.
  • Step ordering is correct. ShellCheck is installed before "Lint workflows", so actionlint finds it on PATH during the lint step.
  • The install step persists the binary to PATH via $GITHUB_PATH (the shared install-release.sh uses this mechanism — confirmed by the fact that the existing "Install actionlint" step is immediately followed by a step that calls actionlint directly). The ShellCheck binary is therefore visible to all subsequent steps in the job, including the new proof step.
  • ARCHIVE_MEMBER: shellcheck-v${{ inputs.shellcheck-version }}/shellcheck is the correct tarball path — the koalaman/shellcheck release archive nests the binary under a shellcheck-v<version>/ directory.

Comment accuracy: The updated comment ("ShellCheck is installed above instead of relying on a runner image") correctly describes the new invariant and explains the pyflakes situation accurately — neither ubuntu-24.04 nor the self-hosted contract preinstalls pyflakes, so actionlint's pyflakes integration remains optional/PATH-dependent in both environments.

Assessment: Correct.


2. fixtures/shellcheck.yml — intentional SC2086 fixture

File: .github/actions/actionlint/fixtures/shellcheck.yml

      - name: Intentional unquoted expansion
        env:
          FIXTURE_VALUE: two words
        run: printf '%s\n' $FIXTURE_VALUE

A minimal valid workflow file with a deliberate SC2086 violation ($FIXTURE_VALUE unquoted in a printf call). The env: block is essential — it shows FIXTURE_VALUE is used as an expansion target, which is exactly the pattern ShellCheck flags. The two words value makes the split-on-whitespace consequence visible in the name.

File placement is correct: .github/actions/actionlint/fixtures/ is not under .github/workflows/, so actionlint does not auto-discover it. It is only linted when explicitly passed as an argument in the CI proof step. It is not subject to normal CI actionlint checking, which is the right behavior for an intentionally-bad fixture.

Assessment: Correct.


3. ci.yml — "Prove embedded scripts are checked by ShellCheck" step

File: .github/workflows/ci.yml:199–219

The proof step has three distinct assertions:

Version parity check (lines 202–211):

expected_version="$(awk '
  $0 == "  shellcheck-version:" { in_input=1; next }
  in_input && /^  [^ ]/ { exit }
  in_input && /^    default: / { sub(/^    default: /, ""); print; exit }
' .github/actions/actionlint/action.yml)"
actual_version="$(shellcheck --version | awk '$1 == "version:" { print $2 }')"

The awk YAML parser is correct for this specific structure: $0 == " shellcheck-version:" is an exact string match (not a regex), so it won't false-match shellcheck-sha256: or any other input. The in_input && /^ [^ ]/ exit condition correctly fires on the next sibling input ( shellcheck-sha256:) before the default value of a different input could be returned. The description block ( description: >- and its continuation) is skipped because it doesn't match ^ default:. Result: 0.11.0.

The shellcheck --version | awk '$1 == "version:" { print $2 }' extraction is the conventional way to get the ShellCheck version string and handles any leading whitespace variance.

Fixture proof (lines 212–219):

output="$RUNNER_TEMP/actionlint-shellcheck.txt"
if actionlint .github/actions/actionlint/fixtures/shellcheck.yml >"$output" 2>&1; then
  echo '::error::The intentional ShellCheck fixture unexpectedly passed actionlint.'
  exit 1
fi
grep -F '[shellcheck]' "$output"
grep -F 'SC2086' "$output"

The control flow is the correct inversion: if actionlint ... ; then fail. When actionlint finds ShellCheck violations it exits non-zero, so the if body (unexpected-pass guard) only triggers if actionlint somehow passes — which would indicate ShellCheck is not active. The grep -F '[shellcheck]' and grep -F 'SC2086' then verify which error was found, not just that some error occurred, ensuring the failure isn't due to an unrelated lint error.

One observation: cat "$output" before the greps is good CI hygiene — it echoes the full actionlint output to the job log before the greps run, so if a grep fails the log already shows what actionlint actually found.

Assessment: Correct.


4. actionlint-shellcheck-pin.test.cjs — version parity contract test

File: .github/scripts/actionlint-shellcheck-pin.test.cjs

The inputDefault helper correctly navigates the YAML structure: it finds the input block by exact line prefix match, then scans forward within the same indentation level for a default: line. The loop break condition /^ {2}\S/u (exactly two spaces then non-space) correctly detects the next sibling input and stops — this is the same logic as the awk parser in the CI step.

The test covers:

  • Version equality between the two action files (prevents silent drift when Dependabot bumps one but not the other)
  • Exact URL template, SHA256 binding, BIN, ARCHIVE_MEMBER, and run-script path in the install step (prevents the install step from being rewritten in a way that changes the binary without updating the hash check)
  • Presence of all key patterns in the CI proof step (prevents the proof step from being silently deleted or its assertions weakened)

The ciWorkflow assertions use cross-line patterns (/su flag) for actual_version.*shellcheck --version and expected_version.*shellcheck-version, which correctly match the multi-line awk block regardless of exact whitespace. ✓

Assessment: Correct and comprehensive.


5. tool-version-drift-check.yml — path triggers and comment

File: .github/workflows/tool-version-drift-check.yml:21–62

Three new paths: entries trigger the drift check when either the actionlint action, the shellcheck action, or the parity test changes. The inline comment explains why there's no second drift-manifest entry for actionlint's ShellCheck inputs: the parity test already enforces equality with the canonical shellcheck action entry, which IS in the manifest (shellcheck:version:github:koalaman/shellcheck). The enforcement chain is: Dependabot bumps shellcheck/action.yml → parity test fails → actionlint/action.yml must be updated too. This is the correct design — one drift-check entry, one parity-test enforcement, no double-registration.

Assessment: Correct.


6. README.md — updated actionlint description

The description change ("with the canonical checksum-pinned ShellCheck release installed explicitly so embedded shell validation is identical on hosted and self-hosted workers") accurately describes the new behavior. The phrase "canonical checksum-pinned" is correct: the SHA256 is validated by install-release.sh during install, and "canonical" refers to the parity with the standalone ShellCheck action.

Assessment: Accurate.


Prior Findings — Status

All six prior findings were resolved, closed, or correctly deferred in the da4bec8 review. This commit does not re-open any of them.

# Finding Status
1 ubuntu-slim runner label in select-runner.yml Closed — rebuttal accepted; standard GitHub-hosted runner
2 configuredCandidateLabels / parseCandidateLabels asymmetry Resolved
3 runAttempt !== 1 redundancy note Resolved
4 GitHub API version "2026-03-10" untracked for drift Deferred correctly
5 Canary SHA pin comment format Closed — rebuttal accepted; first-party reviewed pin
6 safe_path allows // Deferred correctly

No new findings from this commit.


What Works Well

Three-layer enforcement chain: (1) actionlint-shellcheck-pin.test.cjs asserts version and checksum parity between the two action files at the source level; (2) the CI proof step asserts the running binary matches the pinned version; (3) the proof step asserts actionlint actually invokes ShellCheck by running it against a known-bad fixture and verifying the expected SC2086 finding appears. Each layer independently catches a different failure mode: source drift, install drift, and integration drift.

Drift check is not double-registered: The manifest comment is explicit about the choice to rely on the parity test rather than adding a second drift entry. An operator reading the drift manifest knows to look at the test, not search for a missing second entry.

Fixture is placement-safe: The intentional-bad fixture in .github/actions/actionlint/fixtures/ is not auto-discovered by actionlint's normal CI run, so it cannot pollute normal lint results. Only the proof step explicitly passes it as an argument.


Summary

fix: pin ShellCheck for Actionlint parity closes a real gap: the previous setup silently depended on the runner image's ambient ShellCheck, which is absent on a self-hosted worker. The fix installs a checksum-pinned ShellCheck at the same version as the standalone ShellCheck action, making embedded run: script linting deterministic across both runner types. The three-layer enforcement chain (parity test, version check, fixture proof) is comprehensive and correctly structured. No new issues are introduced.

@kyle-sexton
kyle-sexton marked this pull request as ready for review July 11, 2026 11:54
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_413ef6b8-6abb-48b9-ad20-6a941e3b3cbb)

@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 45s —— View job


Code Review — feat: add governed local runner routing (final review at 3fa4a44)

  • Review the complete diff at final head 3fa4a44
  • Assess all prior findings and their resolution status
  • Check for any gaps not covered in the incremental review history
  • Post final assessment

Final Assessment

This PR has been reviewed incrementally across 27 commits with extensive coverage in prior rounds. All six original findings have been resolved, rebutted, or correctly deferred. This pass confirms the final state and checks the changes not individually called out in prior incremental reviews.


Changes Reviewed in this Pass

The prior incremental reviews covered select-runner.cjs, select-runner.yml, local-runner-canary.yml, production-ha-proof.yml/cjs, actionlint/action.yml, and their test suites. The following files changed in this PR were not individually reviewed in those passes:

claude-review.ymlsecrets: inherit removed from consumer example

The consumer example comment was changed from secrets: inherit to an explicit named-secret pass-through (CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}). The runs-on job label was also updated from ubuntu-latest to ubuntu-24.04. Both are correct improvements — explicit secret passing is more secure than secrets: inherit, and the hosted-runner-pins test now enforces no ubuntu-latest/windows-latest labels across all workflows.

semantic-pr.yml — runner input added

runner:
  description: Runner label selected by the caller.
  type: string
  default: ubuntu-24.04

The pr-title job now runs on ${{ inputs.runner }} rather than ubuntu-latest. The default is ubuntu-24.04 (the reviewed hosted label), so existing consumers get no behavior change. The new input allows a caller that has already passed through the select-runner workflow to route this job locally. The default ensures unconfigured callers remain on a known hosted runner. Correct.

pester.ymlrunner-os input removed, hardcoded to windows-2025

The runner-os input is removed and the job is hardcoded to windows-2025. The change correctly documents the rationale: Pester is Windows-only and the pinned runner generation avoids windows-latest drift. The hosted-runner-pins.test.cjs enforces both the fixed label and the absence of runner-os. Correct.

hosted-runner-pins.test.cjs — new contract test

Three assertions:

  1. No ubuntu-latest or windows-latest anywhere under .github/ (prevents moving-image label drift)
  2. Pester hardcoded to windows-2025 with no runner-os input (prevents consumer confusion)
  3. No secrets: inherit in any reusable workflow (prevents over-broad secret exposure)

All three are correctly scoped and enforce governance invariants that previously depended on convention. The movingLabels regex (/\b(?:ubuntu|windows)-latest\b/u) is precise — the word boundary prevents it from matching a hypothetical ubuntu-latest-something variant. Correct.

zizmor.ymlubuntu-latestubuntu-24.04, runner-policy exception annotation added

The runner-policy-exception-template comment documents the Docker-socket dependency for the controller or operator reviewing local-routing exclusions. The comment format matches osv-scanner.yml. Correct.


Prior Findings — Final Status

# Finding Status
1 ubuntu-slim runner label in select-runner.yml:64 Closed — rebuttal accepted; prior review confirmed this is a standard GitHub-hosted runner with hypervisor isolation
2 configuredCandidateLabels / parseCandidateLabels asymmetry Resolved — explicit invariant comment added
3 runAttempt !== 1 redundancy note Resolved — comment added, parameterized tests cover edge values
4 GitHub API version "2026-03-10" untracked for drift Deferred correctly — current supported version; retirement still fails hosted
5 Canary SHA pin comment format Closed — rebuttal accepted; intentional first-party reviewed pin, not Dependabot-managed
6 safe_path allows // Deferred correctly — POSIX collapse plus hash validation means no exploit path

CLAUDE.md Compliance Check

  • pull_request only; never pull_request_target or workflow_run with secrets. ✅ All five new workflows (select-runner.yml, local-runner-canary.yml, production-ha-proof.yml, selector-conformance.yml, pulumi-version-drift-check.yml) use only workflow_call or pull_request. The claude-review.yml tripwire step for privileged triggers is unchanged.
  • SHA-pin both layers. ✅ All uses: references carry a 40-character SHA with a version comment. First-party reusable workflow pins carry a descriptive comment explaining which reviewed revision is pinned.
  • Least privilege, owned by the caller.permissions: {} at the workflow level with job-level minimums. Observer token scoped to ci-runner-canary only. No secrets: inherit.
  • No untrusted checkout before the action. ✅ No actions/checkout steps precede the App token mint step in any new workflow.
  • Public-repo log hygiene.display_report and show_full_output not modified. Evidence artifacts explicitly exclude tokens and raw API responses.

Overall Assessment

The PR is well-structured, security-conscious, and consistent throughout. The fail-closed model is uniformly applied: every error path (auth failure, API timeout, invalid response, missing config, public repo, fork, Dependabot, rerun) produces a hosted-runner result. The label contamination model, ephemeral field handling, and OS namespace enforcement close the gaps that existed in earlier iterations. The production HA proof provides a concrete, evidence-emitting acceptance gate before any production traffic is routed locally. Test coverage across the selector, canary, and HA proof is comprehensive.

The two deferred items (API version drift tracking, safe_path double-slash) carry no security relevance and are correctly flagged for future maintenance rather than blocking merge.

@cursor cursor 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.

Risk: high. Cursor Bugbot did not complete successfully (skipped: usage limit reached), so automated review is unavailable for this security-critical CI change. Human review is required before merge; no reviewers were assigned because no non-author maintainers were identifiable in this repository.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton
kyle-sexton merged commit 99ac2f8 into main Jul 11, 2026
40 checks passed
@kyle-sexton
kyle-sexton deleted the codex/local-runner-selector branch July 11, 2026 12:00

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fa4a44c18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

[[ "$EVENT_NAME" = workflow_dispatch ]] || fail "caller must use workflow_dispatch"
[[ "$REPOSITORY_PRIVATE" = true ]] || fail "canary caller must be private"
[[ "$REPOSITORY" = "$EXPECTED_REPOSITORY" ]] || fail "unexpected canary repository"
[[ "$SOURCE_REF" = refs/heads/main ]] || fail "canary must run from protected main"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require branch protection for canary dispatch

When melodic-software/ci-runner-canary dispatches from an unprotected or accidentally unprotected main, this guard still passes because it only checks the ref string and never verifies github.ref_protected (unlike the production proof preflight). That lets the workflow mint the observer token and generate acceptance evidence from a branch that no longer has the protected-main control the workflow and docs require; add a REF_PROTECTED: ${{ github.ref_protected }} check here before secrets are used.

Useful? React with 👍 / 👎.

kyle-sexton added a commit to melodic-software/standards that referenced this pull request Jul 11, 2026
## Summary

- add a centralized GitHub Actions runner policy that enforces approved
selector routing, public/hosted boundaries, explicit read-only
permissions, cancellation-safe literal hosted fallback, and
machine-readable hosted exceptions
- derive the required literal fallback from the governed policy default,
so changing an approved hosted image is a policy/configuration change
rather than parser-code surgery
- route Standards' 28 eligible private Linux workloads through the
governed selector while retaining exact hosted exceptions for policy and
control-plane boundaries
- recursively validate repository-local reusable workflows and
caller/callee permission narrowing without allowing arbitrary secrets,
tokens, inputs, labels, or runner expressions
- distribute the locked policy runtime and Node version to six enrolled
private consumers from one deterministic manifest
- harden staged .NET formatting and PSScriptAnalyzer adapters with
deterministic cross-platform path semantics and per-target no-profile
PowerShell isolation
- keep the complete .NET-format named job managed by Standards while
each consumer owns only strict data in `.lefthook/dotnet-format.json`
- preserve executable source and consumer index modes through
distribution
- pin every production selector/reusable and Actionlint parity reference
to merged `ci-workflows/main` commit
`99ac2f8c5b09dbb785d4eaf18465cbd96c30290c`

## Dependencies

The final routing contract is the immutable squash merge from
melodic-software/ci-workflows#74 (including stacked #76/#77):

- `99ac2f8c5b09dbb785d4eaf18465cbd96c30290c`

## Reviewed head

`0795d22c89cb8fae11642ede9757e7b43fd5d546`

## Validation

Independent author, reviewer, recheck, and integration-review gates all
PASS with no findings.

- runner-policy adversarial suite: 83/83, including alternate configured
hosted-default proof
- Standards private self-audit: PASS
- .NET/Lefthook adapter: 12/12
- pinned Lefthook 2.1.9 validate, dump, and actual job execution: PASS
- independent argv probe: spaces, semicolons, and `$()` remain inert
data with `shell:false`
- production distribution suite: 114/114 under checksum-pinned yq 4.53.3
in author native Linux and hosted Linux
- independent reviewer inspected the exact-head hosted log and confirmed
assertions 1 through 114
- exact executable-bit gate: PASS; both source CLIs are index mode
`100755`
- routing graph: 28 selectors, 28 workloads, 31 actual `ci-status`
gates, zero selector gates
- final pin proof: exactly 46 merged-main references, zero stale
full/short feature-stack references, and 25 preserved transitional
compatibility references
- all eight changed files reconstruct byte-for-byte from only the two
intended SHA/comment substitutions
- Actionlint 1.7.12 plus hosted checksum-verified ShellCheck 0.11.0:
PASS
- all 10 uniquely referenced workflow/action paths exist at the
immutable ci-workflows commit
- six workflow schemas, Zizmor medium/high, Biome, Markdown, ShellCheck,
Gitleaks, full Lefthook, and diff checks: PASS
- signed final pin commit: `0795d22c89cb8fae11642ede9757e7b43fd5d546`

All 63 hosted checks pass on this exact head.

## Authoritative basis

- GitHub Actions workflow syntax and runner routing:
https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
- Node cross-platform path semantics:
https://nodejs.org/api/path.html#pathwin32
- Lefthook v2.1.9 named-job merge contract:
https://github.com/evilmartians/lefthook/blob/v2.1.9/docs/configuration/jobs.md
- Lefthook v2.1.9 job templates:
https://github.com/evilmartians/lefthook/blob/v2.1.9/docs/configuration/templates.md

## Rollout safety

This PR does not change GitHub variables, secrets, runners, repository
settings, or live infrastructure. Production routing remains hosted
until the IaC and physical canary gates are applied later.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **High Risk**
> Large CI workflow refactor with secrets/vars for runner selection and
a new security gate; misconfiguration could break merges or route jobs
incorrectly until fleet is live.
> 
> **Overview**
> Introduces a **YAML-aware runner policy** (`components/runner-policy`,
`.github/runner-policy.json`, `policy.json`) and a hosted **Runner
policy** CI lane that tests and enforces it against workflow inventory
and repository visibility.
> 
> **CI routing** shifts eligible lint/contract jobs from fixed
`ubuntu-latest` to paired `select-runner` + workload jobs using
`needs.select-*.outputs.runner || 'ubuntu-24.04'`, `if: ${{ !cancelled()
}}`, and `merge_group` support. Control-plane jobs (runner-policy gate,
ci-status, zizmor, osv-scanner) stay on explicit hosted runners with
documented exceptions. `ci-status` now requires `runner-policy`, treats
only `success` as pass (not `skipped`), and pins several workflows to
`ci-workflows@99ac2f8`.
> 
> **Local hooks:** Lefthook .NET formatting moves to a consumer-owned
`.lefthook/dotnet-format.json` and `dotnet-format-staged.mjs`
(shell-less `dotnet format whitespace`). PSScriptAnalyzer staged checks
run **one target per fresh `pwsh` worker**; `PSUseCorrectCasing` is
removed from settings. Dependabot gains an npm root for
`components/runner-policy`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
0795d22. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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