Skip to content

Read-only Workspaces: an optional per-Workspace read permission ceiling (0.12.0) - #22

Merged
immakermatty merged 2 commits into
mainfrom
agent/GH21-workspace-permission-ceiling
Oct 16, 2026
Merged

immakermatty merged 2 commits into
mainfrom
agent/GH21-workspace-permission-ceiling

Conversation

@immakermatty

Copy link
Copy Markdown
Contributor

Fixes #21.

Why

An Organization whose installation accepted Issues: Read and write has to declare issues: write in its policy, because the installation gate compares the accepted permissions exactly. 0.11.0 then adds issues: write to both tiers of every Workspace, so a Workspace whose Team holds only pull still gets a token that can open, edit, label, close, reopen and comment on issues. A Workspace that must write nothing on GitHub (the consumer in #21 is a shared visitor Environment of a public reference Organization) had no way to be read-only while the Organization keeps Issues for its other Workspaces.

What changes

Policy v3 gains one optional key per Workspace, permission_ceiling, whose only value is "read":

Without the key (every existing policy) permission_ceiling: "read"
push / maintain / admin grant write tier, unchanged read tier
pull / triage grant read tier, unchanged read tier
read tier permissions (Issues accepted) actions/checks/contents/members/pull_requests: read, issues: write the same with issues: read
read tier permissions (Issues not accepted) unchanged unchanged (plain read tier)
no grant, drifted Team, foreign owner refused refused exactly the same

Fail-closed rules, all enforced before GitHub is asked:

  • mintToken(policy, workspace, repositoryId, access) and tokenPermissions(policy, access, workspace) now require a Workspace object of the parsed policy; a missing or foreign Workspace is refused (no default, like the tier).
  • A write tier for a read-ceiling Workspace is refused, not silently lowered, so a gate that lost the ceiling cannot mint write.
  • Any requested set that asks for a permission or level the installation has not accepted is refused. Parse rules already make this unreachable today; it is the explicit guard the ceiling relies on.
  • Any other ceiling value, null included, rejects the policy (the Worker answers 503 configuration_unavailable).

Unchanged on purpose: the installation gate (still an exact comparison), the token response shape, the adapter, the probe, the request budget, and everything a policy without the key mints.

policy check prints the ceiling's set and a CEILING column only when a Workspace declares one, so other policies print exactly as before; --live rows show the live ROLE next to the capped ACCESS.

Version 0.12.0 (minor: additive optional policy key).

Proof that other Organizations behave identically

  • All 88 existing tests pass with only the mint call signature threaded through; none of their expected permission sets, policy parses or policy check outputs changed.
  • New: a Workspace without the key parses to exactly its previous shape; in a mixed policy the uncapped Workspace mints byte-identical write and read sets (issues: write, workflows: write included) while the capped one mints reads only.
  • New end-to-end tests through the Node server and through the Worker with the real core client: a push grant mints access: read with issues: read for the capped Workspace and access: write for the other; cross-Workspace, cross-repository and foreign-owner refusals are unchanged.
  • Mutation check: removing any one of the four rules (gate cap, read lowering, write refusal, installation guard) fails 1–3 of the new tests.

npm test (96 pass) and npm run check (license check, syntax, Wrangler dry run) pass locally.

Risks and rollout

  • Older brokers ignore the key. A broker before 0.12.0 accepts a policy with a ceiling and mints the uncapped tiers. README states the order: deploy 0.12.0 with the unchanged policy, then add the ceiling; never roll such a Workspace's broker back below 0.12.0 while its credential is in use. A schema bump would make old brokers reject the policy, but it would force a policy migration on every Organization for one optional key, so this PR documents the order instead.
  • issues: read is requested only where the Organization accepted Issues, so it is always within the installation's grant.
  • The changelog entry is this squash commit's subject; the release workflow generates CHANGELOG.md from commit subjects.

Follow-ups

  • Release v0.12.0 after merge, then roll out only to the first-ring broker that needs the ceiling. Other Organizations' brokers need no change and can take 0.12.0 in the next ring.

🤖 Generated with Claude Code

immakermatty and others added 2 commits October 17, 2026 01:03
…ng (0.12.0)

An Organization whose installation accepted `Issues: Read and write` must
declare `issues: write` in its policy (the installation gate compares
permissions exactly), and `tokenPermissions()` then adds `issues: write` to
both tiers of every Workspace. A Workspace that must write nothing on GitHub
had no way to stay read-only (#21).

Policy v3 gains an optional `workspaces[].permission_ceiling`, whose only
value is "read". For such a Workspace the Team gate selects the read tier for
every usable grant (a push/maintain/admin grant included), and the read tier
asks GitHub for each permission at `read`, so an accepted `issues: write`
becomes `issues: read`. A Workspace without the key parses, gates and mints
exactly as before; the existing tests pass unchanged apart from the mint
signature.

Fail closed:
- `mintToken(policy, workspace, repositoryId, access)` and
  `tokenPermissions(policy, access, workspace)` require a Workspace of the
  policy; a write tier for a read-ceiling Workspace is refused before GitHub;
- any requested set that asks for a permission or level the installation has
  not accepted is refused before the request;
- any other ceiling value (null included) rejects the policy.

The installation gate is unchanged. Offline `policy check` adds the ceiling's
set and a CEILING column only when a Workspace declares one; `--live` rows
show the live role next to the capped tier.

Version 0.12.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t order

README, ARCHITECTURE and the agent invariants describe
`workspaces[].permission_ceiling: "read"`: what its tokens can and cannot do,
that it never creates access or changes which repositories a Workspace
reaches, that the installation gate stays exact, and that a broker before
0.12.0 would accept such a policy but mint the uncapped tiers, so the broker
must be deployed first and never rolled back below 0.12.0 while the
Workspace's credential is in use. The offline `policy check` sample now shows
the `members=read` both tiers already ask for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent exact-head QA on 6f8b010: APPROVED. The parsed ceiling is restricted to "read"; live Team grant still controls repository reachability, while the capped access tier and requested permission set both remain read-only. Mint rejects a write tier for the capped Workspace, foreign Workspace identity, and permission escalation before the scoped token request; the response is checked against the exact repository and permission set. Uncapped policies retain their prior requested sets and summary output. The README and architecture describe the old-broker rollback hazard and the deploy-before-policy rollout order. Independently ran npm test (96/96) and npm run check (license, syntax, Wrangler dry run), each under the host heavy-command lock; CI test and OCI image are green. Local Node was v22.22.3 (repository declares >=24), so CI remains the Node-24 validation source. No blocking findings. Review only; no merge.

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.

A Workspace cannot be strictly read-only once the Organization accepted the Issues permission

2 participants