Skip to content

feat(runner-policy): accept a list of exact caller permission maps for a migration - #950

Merged
kyle-sexton merged 1 commit into
mainfrom
feat/runner-policy-permission-map-list
Nov 6, 2026
Merged

kyle-sexton merged 1 commit into
mainfrom
feat/runner-policy-permission-map-list

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Refs: melodic-software/claude-code-plugins#13586

Summary

The runner-policy contract matches a caller's permissions against allowedCallerPermissions exactly. standards#949 added actions: read to the intake-triage contract, so every caller must now grant it. The claude-code-plugins caller cannot add it before the sync, because the old synced contract rejects it, and the sync PR cannot be hand-edited (check-managed-files). Under a required ci-status that is a deadlock.

This PR lets a contract list several exact maps, so a permission change lands expand/contract:

  1. This PR: accept a list of exact maps, and list the old and new intake-triage maps.
  2. The sync PR, plus the caller PR that grants actions: read.
  3. A follow-up that drops the old intake-triage map.
  4. ci(intake-triage): skip a credit-stopped key with the check-key-credit gate ci-workflows#996.

Fix

  • runner-policy.mjs: allowedCallerPermissions is one exact map or a non-empty list of exact maps. A caller passes when its effective permissions equal any one listed map exactly; each map is still matched exactly, so the ceiling stays exact. With more than one map, the failure names every allowed map (must be exactly one of {...}, {...}). The single-map form keeps its existing per-scope wording. The config checks (at least one write scope, satisfies minimumCallerPermissions) run per map and name the failing index.
  • policy.schema.json: allowedCallerPermissions is oneOf a permission map or a minItems: 1, uniqueItems array of permission maps.
  • policy.json: intake-triage lists {contents: read, issues: write} and {actions: read, contents: read, issues: write}. No other contract changes.
  • README.md: "Contract revision lockstep" item 3 describes the expand/contract order for a caller permission change; the contract description admits the list form.

Verification

  • npm run test:runner-policy: 232 pass, 0 fail. New cases: a caller matching the first map passes; matching the second passes; matching neither fails with a message listing both; a superset, subset or access drift of either map fails; the single-map form passes on an exact match and keeps its old messages; the schema rejects an empty list; every listed map must carry a write scope.
  • A mutant that checks only the first listed map fails the "matching either map passes" case.
  • npm run lint:runner-policy: passed.
  • biome 2.6.3 ci on components/runner-policy: clean. markdownlint on the README: clean.

Related

🤖 Generated with Claude Code

…r a migration

A contract's allowedCallerPermissions is one exact map or a non-empty list
of exact maps; a caller passes when its effective permissions equal any one
listed map exactly. The single-map form and its error wording are unchanged.

intake-triage.yml lists the old map {contents: read, issues: write} and the
new one with actions: read, so callers can move to the new map after the
sync instead of deadlocking on it. The README's contract revision lockstep
item 3 records the expand/contract order.

Refs: melodic-software/claude-code-plugins#13586

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton marked this pull request as ready for review November 6, 2026 01:22
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T01:24:06.909941Z 2c453c6 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

claude-review reviewed 2c453c6dc965ceeeeb8b2099ab13f3d5d6395f9f: 0 finding(s), 0 as inline comments.

No findings. I read all 5 changed files and traced every consumer of allowedCallerPermissions in runner-policy.mjs, the schema, the README and the threat model.

  • Scope: allowedCallerPermissions can now be one exact map or a non-empty list of exact maps. The schema uses oneOf with minItems: 1 and uniqueItems. callerPermissionMapEntries normalizes both forms, and the write-scope and minimumCallerPermissions checks now run per map and name the failing index.
  • Match logic: exactCallerPermissionsError passes a caller that equals any one listed map exactly. With a single map it returns the old per-scope message. With several maps it lists every allowed map.
  • Stale consumers: The only remaining reference to the removed allowedCallerPermissionNames field is gone with it. The other uses of allowedCallerPermissions (contract.allowedCallerPermissions !== undefined and the secrets check) only test for presence, so the switch to an array breaks nothing.
  • policy.json: The intake-triage contract lists the old map {contents: read, issues: write} and the new map with actions: read. Both carry a write scope.
  • Docs: The README's contract description and "Contract revision lockstep" item 3 match the new behavior. The threat model only mentions the contract in passing and still reads correctly.
  • Closing issue: None. The PR uses Refs:.

I could not run npm run test:runner-policy here because the sandbox denied it. The PR body's "232 pass, 0 fail" and mutant-check claims are the author's and I did not confirm them. This review is static only.

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

claude-security-review reviewed 2c453c6dc965ceeeeb8b2099ab13f3d5d6395f9f: 0 finding(s), 0 as inline comments.

No security findings in this pull request.

What I reviewed. All five changed files in components/runner-policy/: runner-policy.mjs, policy.json, policy.schema.json, the tests and the README. I also read the code that consumes allowedCallerPermissions in runner-policy.mjs: the validation at lines 165–260, reusableWorkflowStatus, privilegedHostedRequirement, and exactCanonicalMap.

Why nothing was flagged.

  • Matching stays exact. A caller passes only when its effective permissions equal one listed map exactly. exactCallerPermissionsError runs exactCanonicalMap against each map and passes only if one returns undefined. Extra scopes, missing scopes, write-all, non-mapping values and access drift still fail closed.
  • Empty list. The schema requires minItems: 1, so an empty list is rejected. A test covers this.
  • Single-map form. It keeps its exact match and wording. A test covers this too.
  • Per-map checks. Each listed map must still contain a write permission and clear minimumCallerPermissions. The code now loops over every map. A test covers the second-map failure.
  • Fail-closed path. The map array is frozen, and contract.allowedCallerPermissions is truthy only when a map exists. privilegedHostedRequirement only checks !== undefined, so the waiver gate behaves as before.
  • Policy data change. policy.json adds {contents: read, issues: write} beside the existing {actions: read, contents: read, issues: write}. The new map is a strict subset of the old one, so it grants no new token scope. It only lets callers drop actions: read during the expand/contract migration.
  • Secret handling. The check that secret expressions stay literal (${{ secrets.NAME }}) when allowedCallerPermissions is present still runs through Object.hasOwn, so it still applies.

Trust controls. None of the changed files is in the listed trust-control set: the trusted-trigger check, token request, kill switch, token broker, merge holds or the other .github/actions/ controls. No manifests changed, and no instruction surface (AGENTS.md or a rules file) was deleted or softened.

Not read. docs/contracts/instruction-exception-register/README.md was not consulted, because nothing here removes an instruction.

critical-findings: 0

@kyle-sexton kyle-sexton added do-not-merge Hard merge gate: do not merge while applied. and removed do-not-merge Hard merge gate: do not merge while applied. labels Nov 6, 2026
@kyle-sexton
kyle-sexton merged commit 9663302 into main Nov 6, 2026
72 of 75 checks passed
@kyle-sexton
kyle-sexton deleted the feat/runner-policy-permission-map-list branch November 6, 2026 04:47
kyle-sexton added a commit that referenced this pull request Nov 6, 2026
Refs: melodic-software/claude-code-plugins#13586

## Summary

Contract step of the intake-triage permission expand/contract. #950
listed the old and new caller maps;
melodic-software/claude-code-plugins#13660 moves the only caller to the
new map (`actions: read`). This PR drops the old map, so the contract is
again one exact map.

Draft until melodic-software/claude-code-plugins#13660 merges: its sync
would otherwise reject the caller still on main.

## Fix

- `components/runner-policy/policy.json`: intake-triage's
`allowedCallerPermissions` is the single map `{actions: read, contents:
read, issues: write}`.

## Verification

- `node --test components/runner-policy/runner-policy.test.mjs`: 232
pass, 0 fail.

## Related

- #950, melodic-software/claude-code-plugins#13630,
melodic-software/claude-code-plugins#13660, then
melodic-software/ci-workflows#996.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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