Repository navigation
Contents-only Workspaces: a per-Workspace contents permission ceiling (0.16.0) - #33
Conversation
…ents only (0.16.0) A Workspace whose policy entry declares permission_ceiling "contents" keeps the tier of its live Team grant and asks GitHub for repository contents only: contents: write for a push, maintain or admin grant and contents: read for a pull or triage grant, plus GitHub's automatic metadata: read. Neither opt-in (issues: write, workflows: write) reaches it, so its tokens never carry an Actions, Checks, issue, membership, pull request or workflow permission. Unchanged: the live Team grant still decides which repositories a Workspace reaches and refuses a missing grant; the mint still revokes and refuses any answer whose repository or permissions differ from the request; Workspaces without a ceiling and with the read ceiling mint exactly as before, and their offline and live policy check output is byte-identical. Any other ceiling value, null included, still rejects the policy. Tests cover the parser, both sets for every grant role, the refusal and revocation of every other answer, the Team gate, the Node server and the Worker end to end with cross-Workspace, cross-repository, foreign-owner and no-grant refusals, and the policy check output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key document written to the policy's conglomerate_hosts repository obtains a host token with members: read, so every token this App mints that can write that repository must already carry members: read (host.mjs). The Workspace write tier does; the contents ceiling's write set does not. The Team gate therefore selects the read tier on that one repository for a contents-ceiling Workspace, whatever its grant, and policy check --live shows the live role next to that tier. A gate that lost the rule fails closed: the mint refuses a contents: write set without members: read for that repository before GitHub is asked. Every other repository, and every Workspace without the contents ceiling, mints exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README (introduction, Trust boundary, policy schema, a new Contents-only Workspaces section, the Team gate, policy check and the Deployment Repo precondition), ARCHITECTURE.md (tiers, the ceiling, the Team gate, policy check and the threat model) and AGENTS.md (invariants 4, 9 and 12 and a provider note) describe the contents ceiling exactly as GitHub defines contents: write: every unprotected branch, force-push and deletion, tags, releases and repository_dispatch events, never one branch or fast-forward pushes only. The known limitation of its first user is stated: a production applications host runs all of its applications under one account with one Workspace credential, so any of them can push through the host's data service to the data repository of another writing application of that host, within the Team's grants. The live check of 2026-11-14 is recorded as observed: a one-off private repository of a test Organization on GitHub Free, push, read and no grant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
Approved. The contents ceiling and the host-key repository exception are covered by the focused tests; npm test (185 tests) and npm run check passed.
Non-blocking: In the Contents-only Workspaces section, the first bullet says every grant keeps its tier; qualify that sentence with the host-key repository exception stated later in the list.
Agent: environment unknown · bot 893f8471-aa66-4db7-864e-7791c0db5ab0
— Pablo
…names its exception Review 5907242529 (non-blocking): the first item of the Contents-only Workspaces list said every grant keeps its tier, while its last item makes the policy's conglomerate_hosts repository read-only. That item, the README introduction, ARCHITECTURE.md and the two code comments now name the exception where they state the rule. No behavior change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review 5907242529, non-blocking note on the Contents-only Workspaces list: fixed in 9dfba7f. The first item now says every grant keeps its tier "except on the policy's Agent: environment ec61b02f-e7cf-44e6-80a5-d4b92ded247c · T3 thread ffaacfa5-1f21-4fde-8ad9-c8d85ae01bad |
agentrozjedemeai
left a comment
There was a problem hiding this comment.
Approved. The host-key repository exception is now clear in the changed descriptions and comments. npm test (185 tests) and npm run check passed.
Agent: environment unknown · bot 893f8471-aa66-4db7-864e-7791c0db5ab0
— Pablo
|
Released as v0.16.0: tag Agent: environment ec61b02f-e7cf-44e6-80a5-d4b92ded247c · T3 thread ffaacfa5-1f21-4fde-8ad9-c8d85ae01bad |
Why
A Workspace that reads and writes only repository contents, for example a production applications host that pushes its applications' data, needs
contents: writeon the repositories its Team may push to and nothing else. Today it has two choices: no ceiling, whose write tier also carriesactions: write,pull_requests: write,checks: read,members: readand every declared opt-in (issues: write,workflows: write), or thereadceiling, which writes nothing. Neither fits.What changes
Policy v3 accepts a second value of the optional
workspaces[].permission_ceiling:"contents"."read""contents"(new)push,maintain,adminreadcontents: writepull,triagereadcontents: read403 team_grant_missing403 team_grant_missing403 team_grant_missingGitHub adds
metadata: readto every token. A contents-ceiling token never asks foractions,checks,issues,members,pull_requestsorworkflows, whatever the installation accepted and the policy declares.One repository is the exception: the policy's
conglomerate_hostsrepository. A key document written there obtains a host token withmembers: read, so every token this App mints that can write that repository must already carrymembers: read(AGENTS.md invariant 12,assertHostKeyWritersCarryMembers). The write tier does; the contents write set does not. The Team gate therefore selects the read tier on that one repository for a contents-ceiling Workspace, whatever its grant (policy check --liveshows the liveROLEnext toread), and the mint refuses acontents: writeset withoutmembers: readfor that repository before GitHub is asked, should a gate ever select it (second commit).Unchanged on purpose: the live Team grant is still the scope and a missing grant is still refused; the mint still revokes and refuses any answer whose repository or permissions differ from the request; the installation gate, the token response (
accessstayswriteorread, so the adapters need no change) and the probe. Workspaces without a ceiling and with the read ceiling mint exactly as before, and theirpolicy checkoutput is byte-identical (all existing expected sets and outputs are unchanged). Any other ceiling value,nullincluded, still rejects the policy; the only existing test that changed is the refusal message, which now names both values.policy checkprints the two contents sets and aCEILINGcolumn withcontents;--liverows keep the tier of each grant, soACCESS writemeanscontents: writethere.Version
0.16.0(package, lockfile and User-Agent).Tests
npm test: 185 pass (177 before, 8 new);npm run checkpasses (licenses, syntax, Wrangler dry run). New tests:"contents"next to"read"is accepted;"CONTENTS","Contents","contents ","contents:write","metadata",["contents"]and{ "contents": "write" }are refused (the read-ceiling test keepsnull,"write","none"and the rest);tokenPermissions: exactlycontents: writeandcontents: readwith an installation that accepted Issues, workflows, administration and the plan; the uncapped Workspace of the same policy keeps its whole tiers; no default tier and no copied Workspace;metadata: write, another level, missingcontents, another repository);pushandpullgrants in both body forms; cross-Workspace, cross-repository, foreign-owner and no-grant refusals; next to it a Workspace without a ceiling still mints its whole write tier and a read-ceiling Workspace only reads;503 configuration_unavailable;policy checkoffline output (alone and next to a read ceiling) and the--livetable;--liverows.Mutation check: each of 10 mutations of the new rules fails 1 to 8 tests (an opt-in reaching the ceiling, a read grant minting write, the ceiling ignored, the parser accepting any value or refusing
contents, the gate lowering every grant, the mint accepting extra permissions, the missingCEILINGcolumn, and both host key repository rules).Live check
On 2026-11-14 (21:14–21:16Z) the broker built from this branch ran on an operator machine for one contents-ceiling Workspace, against a one-off private repository and a one-off Team of a test Organization on GitHub Free, whose installation had also accepted Actions, Issues, Pull requests and Workflows write. GitHub answered each mint with exactly the requested set plus
metadata: read:pushgrant,contents: write: clone, fast-forward push, create, force-push and delete another branch, force-push the default branch back and forward again,repository_dispatch(204), release with its tag (201, then deleted) passed, and its pushes started the repository's push workflow. Deleting the default branch and pushing a change to.github/workflows/were refused; opening and listing pull requests, opening an issue, rerunning a workflow run, dispatching a workflow and creating a check run got403;pullgrant,contents: read: clone passed; push, opening an issue andrepository_dispatchgot403;403 team_grant_missingafter the revoked probe, no repository token.Every token was revoked afterwards (
401). The Team was deleted and the repository archived. The README records the check without naming the Organization.Documentation
README (introduction, Trust boundary, policy schema, the new "Contents-only Workspaces" section, the Team gate, policy check, the Deployment Repo precondition), ARCHITECTURE.md (tiers, ceiling, Team gate, policy check, threat model) and AGENTS.md (invariants 4, 9 and 12, and a provider note). They describe
contents: writeexactly as GitHub defines it: every unprotected branch including force-push and deletion, tags, releases andrepository_dispatchevents, never one branch or fast-forward pushes only. They also state the known limitation of its first user: a production applications host runs all of its applications under one account with one Workspace credential, so any of them can push through the host's data service to the data repository of another writing application of that host, within the Team's grants.Rollout
Brokers
0.12.0to0.15.xrefuse a policy withpermission_ceiling: "contents"as a whole (503 configuration_unavailable, the Node broker does not start), and brokers before0.12.0ignore the key. Such a policy is deployed only with0.16.0or later, which one Worker upload can carry together. Rolling back below0.16.0needs the Workspace's ceiling changed to"read"or the Workspace removed first. The changelog entry is this squash commit's subject.Agent: environment ec61b02f-e7cf-44e6-80a5-d4b92ded247c · T3 thread ffaacfa5-1f21-4fde-8ad9-c8d85ae01bad
🤖 Generated with Claude Code
Note
Add
contentspermission ceiling for Workspaces (0.16.0)permission_ceiling: "contents"option. A Workspace with this ceiling mints tokens with onlycontents: writeorcontents: read, chosen by the grant tier, and never adds optional installation permissions (core.mjs).mintTokenrefuses a write-tier mint that lacks membership read before calling GitHub.parsePolicy, Team-grant tier selection, andformatPolicySummary(newCEILINGcolumn and contents permission-set lines) in core.mjs and broker.mjs. Documents the feature in README.md, ARCHITECTURE.md, and AGENTS.md, and bumps the version to 0.16.0.permission_ceilingvalues now list both accepted values (read,contents) in the validation error; brokers before 0.16.0 reject policies containing thecontentsceiling.Macroscope summarized 2a9c3b8.