feat(runner-policy): classify packages-only write jobs as publication - #184
Conversation
A hosted job whose only write scope is packages carries registry-publication authority, not repository or organization state. Requiring privileged-control-plane for it left write-token publications homeless after the Wave 4/5 enum tighten (github-iac#157): the only admissible path would have moved packages:write and the registry token onto fleet containers, trading away hosted artifact provenance for no security gain. permissionHostedRequirement now maps a packages-only write map to the durable publication category; any additional write scope keeps the privileged requirement. Consumes the new category in the same change: publish-packages#publish re-reasons from privileged-control-plane to publication and stays hosted. sync.yml#sync remains the one privileged-control-plane exception, converting in the standards-sync runner-input lane. ## Verification - npm run test:runner-policy: 210/210 (category pass/fail/mixed-write cases added). - GITHUB_REPOSITORY=melodic-software/standards npm run lint:runner-policy passes with the re-reasoned exception; the old reason now fails hosted-exception-category, confirming enforcement. - npm run lint:md: 0 errors. ## Related - melodic-software/github-iac#157 (deferred-lane conversion plan) - melodic-software/github-iac#78 (epic) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 298f1a696a
ℹ️ 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".
|
Claude finished @kyle-sexton's task in 2m 25s —— View job PR Review
SummaryThe semantic claim is sound — 🔴 Important — Early return bypasses
|
The packages-only branch returned the publication requirement from the permission check, before the deployment-environment, credential- expression, and credential-minting-action checks ran. Adding packages: write to a job carrying any of those surfaces therefore downgraded its required exception category from privileged-control-plane to publication (codex review finding on #184). privilegedHostedRequirement now holds the publication requirement and returns it only after every later privileged check passes; any other privileged surface keeps the job privileged. The one admission is the exact GitHub-provided token expression: with a packages-only map it carries only the already-categorized publication authority, mirroring the local-routing grant's reasoning, so publish jobs can still authenticate to the registry with GITHUB_TOKEN. ## Verification - npm run test:runner-policy: 214/214 (environment, credential-minting, GitHub-token-admission, and named-secret cases added). - GITHUB_REPOSITORY=melodic-software/standards npm run lint:runner-policy passes: publish-packages#publish keeps publication with its GITHUB_TOKEN step env. - npm run lint:md: 0 errors. ## Related - melodic-software/github-iac#157 (deferred-lane conversion plan) - melodic-software/github-iac#78 (epic) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 123d5d513d
ℹ️ 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".
…on rule Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96ba6ee289
ℹ️ 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".
…ion carve-out Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 616fc7df33
ℹ️ 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".
|
Claude finished @kyle-sexton's task in 3m 57s —— View job PR Review
Prior findings resolvedThe three findings from the earlier review pass on the current HEAD:
🔴 Important —
|
|
Claude finished @kyle-sexton's task in 3m 39s —— View job PR Review
SummaryAll three prior review findings (early-return bypass, missing GitHub-token README carve-out, inconsistent later workload rules) were correctly addressed in commits 123d5d5, 96ba6ee, and 616fc7d. The implementation and tests are sound for the covered paths. The latest Codex P2 finding about 🟡 Nit — Local-reusable section does not explicitly scope out the packages-only carve-outFile: The general workload-job rule (lines 590–596) says a write permission map can use A single clarifying phrase — e.g. "A fixed hosted called job may inherit write only with its own On the Codex P2 finding (
|
|
Claude finished @kyle-sexton's task in 2m 11s —— View job PR Review
Prior 🔴 findings — resolvedThe early-return bypass (round 1) and the README carve-out gaps (rounds 2–3) are correctly addressed in commits 123d5d5, 96ba6ee, and 616fc7d. The implementation is sound for the direct-job path. 🟡 Nit — README local-reusable section still missing the carve-out scopeFile: This finding was identified in the previous review round and remains open at HEAD. Lines 590–596 now correctly say
It does not say that the packages-only A single appended clause is sufficient:
On the Codex P2 finding (
|
…permission flow auditLocalPermissionFlow hard-coded privileged-control-plane for every fixed-hosted called job with write-capable inherited permissions, so a packages-only publisher factored through a repository-local reusable hit two contradictory category checks on one exception key (codex review finding on #184). The flow pass now derives the required reason from the called job's own full requirement: only a genuine publication classification — a declared packages-only map with no other privileged surface — downgrades; broader maps, extra surfaces, and undeclared inherited maps stay privileged. ## Verification - npm run test:runner-policy: 216/216 (local-reusable publication round-trip and undeclared-map privileged guard added). - Dogfood run against this repository passes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dcd56bb538
ℹ️ 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".
|
Claude finished @kyle-sexton's task in 5m 18s —— View job PR Review
Prior findings — resolvedAll findings from rounds 1–4 are correctly addressed:
On the Codex P2 finding — hosted-reusable credential scan (runner-policy.mjs:2796)The finding correctly observes that 🟡 Nit — README local-reusable paragraph now contradicts the codeFile: After dcd56bb,
A consumer following this sentence would declare
I prepared this one-line fix as a local commit (98ef1ca) but could not push due to write-access restrictions. What's working well
|
…lication downgrade A packages-only caller of an approved hosted-only reusable returned publication at the contract boundary before the credential scan ran, so a credential expression smuggled through an allowlisted with input value could ride the weaker category (codex review finding on #184). The boundary early-return now scans the caller outside its contract-governed secrets mapping first — the exact GitHub-provided token stays admitted — and only then returns the held publication requirement. The README's local-reusable paragraph also carries the publication carve-out so the normative guidance matches the analyzer. ## Verification - npm run test:runner-policy: 217/217 (with-value smuggle stays privileged; clean literal input keeps publication). - Dogfood run against this repository passes; lint:md 0 errors. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe09647c06
ℹ️ 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".
|
Claude finished @kyle-sexton's task in 4m 23s —— View job PR Review
Prior findings — resolvedAll findings from rounds 1–5 are correctly addressed at HEAD (fe09647):
🟡 Nit — Runner-input paragraph still missing the packages-only qualifierFile: The runner-input section says:
In A consumer following this sentence would declare Suggested fix — append the qualifier to the sentence:
What's working well
|
…r-input paragraph Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 168294fa4d
ℹ️ 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".
|
Claude finished @kyle-sexton's task in 3m 53s —— View job PR Review
Prior findings — resolvedAll findings from rounds 1–6 are correctly addressed at HEAD (168294f):
🔴 Important — Container and service execution bypass the publication escalationFile:
const hostedRequirement = privilegedHosted ?? structuralHostedRequirement(job);Because Before this PR, every write-capable job returned Concrete exploit path — this job passes with only a jobs:
publish:
permissions:
packages: write
container:
image: ghcr.io/attacker/builder:latest # arbitrary container code
runs-on: ubuntu-24.04
steps:
- run: npm publishThe Fix: Before if (structuralHostedRequirement(job)) {
return {
reason: "privileged-control-plane",
description: "containerized execution with packages-only write permissions",
rule: "privileged-hosted-only",
};
}
return publicationRequirement;This preserves the pre-PR behavior: write-capable + container was always 🔴 Important — Missing tests for container + packages-only combinationFile: The publication test block covers environment, credential expressions, credential-minting actions, named secrets, GITHUB_TOKEN admission, local-reusable round-trip, and hosted-reusable credential smuggling. None of the new tests combine 🟡 Nit — README publication rule omits container/services as escalating surfacesFile:
The sentence lists three escalating surfaces. Once the code fix above lands, a On the Codex P2 findingThe finding correctly identifies the structural-hosted bypass. My analysis confirms it as a regression introduced by this PR (pre-PR: write + container always What's working well
|
…lication A held publication downgrade made privilegedHostedRequirement non-null, so the structural job-container/service-container requirement was never evaluated for a containerized packages-only job and a plain publication exception satisfied it (codex review finding on #184). Structural requirements now take precedence over a publication classification; the ordinary privileged-over-structural precedence is unchanged. ## Verification - npm run test:runner-policy: 218/218 (containerized packages-only job now demands job-container, not publication). - Dogfood run against this repository passes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 51s —— View job PR Review
Prior findings — all resolvedAll findings from rounds 1–7 are correctly addressed at HEAD (f2e375a):
On the round 7 🟡 Nit (README missing container/services as escalating surfaces)The round 7 review assumed the container fix would escalate to 🟡 Nit — Services-only path missing a test in the publication test blockFile: The container fix test at line 2179 covers What's working well
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f2e375a635
ℹ️ 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".
…ion flow The flow pass derived its required reason from privilegedHostedRequirement alone, so a containerized packages-only job inside a repository-local reusable demanded publication here while the direct audit demanded job-container — an unsatisfiable pair on one exception key (codex review finding on #184). The flow's publication downgrade now defers to structuralHostedRequirement the same way the direct audit does. ## Verification - npm run test:runner-policy: 219/219 (containerized local-reusable publisher passes both audits with a job-container exception). - Dogfood run against this repository passes. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 48s —— View job PR Review
Prior findings — all resolvedAll findings from rounds 1–8 are correctly addressed at HEAD (429a4fc):
🟡 Nit — Services-only path still untested for both the direct and local-reusable casesFile: The round 8 nit identified that the direct-audit services-only case has no test. Commit 429a4fc added the container local-reusable round-trip (line 2207) but did not add the services-only counterparts: neither the direct-audit case ( The code handles these paths correctly — What's working well
|
No linked issue. Standards-sync caller lane from the melodic-software/github-iac#157 plan — the last privileged-control-plane exception in this repository converts. - **Contract** (secret-capable runner-input, selector-lockstep): `standards-sync.yml@ec91c343…` registered in `components/runner-policy/policy.json` with `routing: runner-input`, the full input surface (`runner`, `manifest`, `standards-ref`, `dry-run`, `targets`), and the exact reviewed `allowedSecrets` mapping (`app-client-id`/`app-private-key` → the two `STANDARDS_SYNC_*` secret expressions). No `allowedCallerPermissions` — the caller stays statically read-only, so the contract alone owns the secret boundary. Secret-capable contracts decline Dependabot auto-approval unconditionally, so every future SHA of this reusable is human-reviewed. - **Caller**: `sync.yml` adopts the governed selector (success-guard shape per epic decision 4) with the routed runner input and the `|| 'ubuntu-24.04'` fallback, and bumps the reusable pin `35f2684 → ec91c343` (the v0.6.0 revision that added the `runner` input — no ci-workflows change needed). - **Exception removal**: `sync.yml#sync`'s `privileged-control-plane` entry is deleted; `publish-packages#publish` (publication, #184) is now this repository's only hosted exception. ## Verification - `GITHUB_REPOSITORY=melodic-software/standards node components/runner-policy/runner-policy.mjs` passes with the exception removed; removing the contract instead fails closed on the caller. - `npm run test:runner-policy`: 219/219. - lint:md 0 errors; full pre-commit suite green. ## Related - melodic-software/github-iac#157 (conversion plan), melodic-software/github-iac#78 (epic) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
No linked issue.
The Wave 4/5 enum tighten (melodic-software/github-iac#157) removes
privileged-control-plane, but the validator's category rule required exactly that reason for ANY write-scoped token — leaving a hostedpackages: writepublication job homeless: its only admissible post-tighten path would move the registry token onto fleet containers, trading hosted artifact provenance for no security gain.permissionHostedRequirementnow maps a write map whose only writable scope ispackagesto the durablepublicationcategory. Any additional write scope keepsprivileged-control-plane. README documents the boundary.publish-packages.yml#publishre-reasonsprivileged-control-plane→publicationand stays hosted.sync.yml#syncremains the sole privileged exception here, converting later in the standards-sync runner-input lane.Verification
npm run test:runner-policy— 210/210 (added: publication-category pass, wrong-reason fail with the new message, mixed packages+issues stays privileged)GITHUB_REPOSITORY=melodic-software/standards npm run lint:runner-policy— passes re-reasoned; the old reason now failshosted-exception-categorynpm run lint:md— 0 errorsRelated
🤖 Generated with Claude Code