Repository navigation
Read-only Team grants: pull and triage mint a read-tier token (0.11.0) - #18
Conversation
A Team that holds only a `pull` or `triage` grant on a repository was
refused with `team_grant_missing`, so an Organization that lowered a Team
to read cut its Team Environment off the repository completely: no clone,
no fetch, no issues.
The live Team grant now selects one of two access tiers at mint time:
- push/maintain/admin -> write tier, unchanged: actions: write,
checks: read, contents: write, pull_requests: write (+ workflows: write
when the policy declares it);
- pull/triage -> read tier: contents: read (+ GitHub's automatic
metadata: read), plus issues: write only when the policy declares that
the installation accepted it. No contents, pull request, Actions,
Checks or workflow write.
No grant, a missing or drifted Team, or a foreign owner still refuse
exactly as before. `mintToken` requires an explicit tier, so a lost tier
fails before GitHub instead of becoming the write set, and the handler
checks the minted tier matches the verified one.
The token response adds `access` ("read" | "write"); `policy check`
prints the permission set each tier asks for and lists read grants with an
ACCESS column. The adapter returns the tier (a missing value from an older
broker means write), still refuses no command up front, and after GitHub
refuses a read-tier gh command adds one stderr line naming the read access
instead of leaving it to look like an outage.
Version 0.11.0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… order README, ARCHITECTURE and the AGENTS invariants describe the two access tiers, why triage maps to the read tier, the honest scope of `issues: write` (GitHub has no create-only issue permission), the App permission an Organization must accept before read-only Environments can open issues, and the rollout order: release, deploy the broker per Organization, optionally pin the adapter, accept issues where wanted, and only then lower Team grants. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…both tiers Two decisions folded into the read-only grants change: - The read tier now asks for `checks: read`, `contents: read` and `pull_requests: read`, so people in a read-only Environment can list and view pull requests and check runs. It still asks for no write other than the optional `issues: write`, and for no Actions access. - When the policy declares the accepted `issues: write`, the write tier asks for it too, so write-capable Team Environments can file escalation issues. Without the declaration the write tier is exactly its previous set. Tests pin both tier sets with and without each opt-in and refuse any under- or over-scoped mint. README, ARCHITECTURE and the AGENTS invariants describe the new sets. They also note that GitHub CLI check views ask for the workflow run behind an Actions check, which the read tier cannot read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`gh pr checks` and `--json statusCheckRollup`, which T3's pull request list uses, read the workflow run behind each GitHub Actions check. That is Actions data, so with only `checks: read` and `pull_requests: read` those views could fail in a read-only Environment. The read tier now also asks for `actions: read`, still with no write of any kind except the optional `issues: write`. Tests pin the read set and refuse a mint that leaves out any of its four reads or adds Actions write. The docs and the AGENTS invariant state that `actions: read` also reads the repository's workflow logs and artifacts, as a person with read access can. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
Review of exact head 9d32c67.
Changes requested — one finding (also raised in the open review thread):
- adapter/brokered-gh.mjs:260-271: Any nonzero exit from official gh with a read-tier token produces the definitive notice that GitHub refuses pushes, pull requests and other changes, and recommends
gh issue create. A failed read (bad arguments, nonexistent resource, network/API outage) receives the same misleading diagnosis; without the optionalissues: writedeclaration, the proposed command is itself unavailable. Please distinguish a permission refusal from other failures, or make the notice explicitly conditional and mention that creating issues requires the installation's accepted Issues permission. Cover both a permission failure and an unrelated failed read / missing Issues permission in tests. Keep the official gh error and exit code and do not pre-block commands.
The other requested boundaries checked out on this head: grant-to-tier mapping, optional Issues permission, exact returned permission set, additive access, no grant refusal, and rollout order. Local npm test passed 88/88, npm run check and git diff --check origin/main...HEAD passed. No live installation token was minted or deployment performed.
Both tiers now ask for members: read, which the installation already holds for the Team gate. Without it GitHub answers the Owner and collaborator lists with an empty list, so an Environment could not find the Owners to mention in an issue (verified with a live brokered token). A failed read-tier command keeps official gh's error and exit code and adds a conditional notice: it names read access as the reason only if GitHub refused a change and says that issues need the accepted Issues permission. Tests cover a refused write and an unrelated failed read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
Exact-head re-review of 0ba2f2a: APPROVED. The prior notice finding is addressed: after official gh fails, the read-tier notice is conditional and states the Issues-permission prerequisite; official gh still runs and its exit code/error are retained. I checked the members: read addition in both requested tier sets and exact minted permission validation, grant-to-tier routing and fail-closed tests, documentation and rollout order. Locally on this detached SHA: npm test 88/88, npm run check, git diff --check all pass; CI test and image checks pass on this SHA. The prior thread is resolved. No live installation mint, release or deployment was performed.
… paths; a lookup that finds nobody is said, never guessed The Owners fallback now also applies when CODEOWNERS names nobody but bots for the affected paths, and when even the Owners lookup returns nothing or fails, the agent files the issue without a mention and tells the Operator to pass the link on. On a Team Environment the lookup needs the brokered token's members: read (Lazurio/github-app#18); until then GitHub answers 200 [] and this handoff applies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ts a proposal issue (F9 and F14 addenda, base-instructions-28) (#198) * Reader role for installing an Organization; a read-only repository gets a proposal issue (F9 and F14 addenda, base-instructions-28) Roles come from GitHub rights (Matěj, 2026-10-05): in an Organization, regular people and the Team Environment only read the knowledge base, the planning and the Organization's root repository; only the Owners write. A person with only read on the root resolved to role-unverified, so nobody but Owners, Stewards and Builders could install or re-install their Organization. Content installation (F9 addendum): - A fourth role, reader: read or triage on the root repository and an active membership (a public root answers read to everyone). Same scope as a Builder, restricted slots excluded without a provider call. - Live resolution: admin > steward > builder > reader (the Launchpad's first run and "Obsah Environmentu"). - The CLI accepts --role reader; the bare form stays the Admin installation and its failure now names the --role forms. - Nothing in install, update or doctor asks for write; clones only read. Generated Folder (F14 addendum): - AGENTS.md: before proposing a change, check the live push right (gh api .../permissions.push; on the Team preset a dry-run push, as an installation token answers false). With push, worktree and PR; without it, an issue in that repository mentioning CODEOWNERS or the Organization's Owners (no bots); never a fork. - The missing-rights escalation says a read-only repository is not a missing right; the manual gains its own section before it, and the knowledge and checkout-alignment texts point to the proposal issue. - Template revision base-instructions-28, digest re-pinned. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Folder: CODEOWNERS counts only when it names someone for the affected paths; a lookup that finds nobody is said, never guessed The Owners fallback now also applies when CODEOWNERS names nobody but bots for the affected paths, and when even the Owners lookup returns nothing or fails, the agent files the issue without a mention and tells the Operator to pass the link on. On a Team Environment the lookup needs the brokered token's members: read (Lazurio/github-app#18); until then GitHub answers 200 [] and this handoff applies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * F14 addendum: the CODEOWNERS condition, members: read on the Team and the fail-closed handoff Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Why
An Organization wants roles by GitHub rights. Regular people and the Team Environment get read-only access to some repositories, for example the knowledge base, planning data or the Organization root. Only owners write, from their own Environments under their own GitHub accounts. Agents without write propose changes as issues instead of pull requests.
Until now the broker minted a token only when the Workspace's Team held a
push,maintainoradmingrant on the repository. Apullortriagegrant was refused as403 team_grant_missing. So the moment an Organization lowered a Team to read, that Team Environment lost the repository completely: no clone, no pull, no issues. This PR makes read grants work. It also lets write-capable Environments open issues once the Organization accepts the permission, for the escalation issues the Lazurio Folder asks for.Target behaviour
On every mint, the live Team grant selects the tier. Policy schema v3 does not change and names no tier.
metadata: read)issues: writedeclaredpush,maintain,adminactions: write,checks: read,contents: write,members: read(new),pull_requests: write(+workflows: writeif declared)issues: writepull,triageactions: read,checks: read,contents: read,members: read,pull_requests: readissues: write403 team_grant_missing/repository_deniedissues: write. There is no contents, pull request, Checks, Actions or workflow write. People in a read-only Environment can list and view pull requests, check runs and GitHub Actions workflow runs.actions: read.gh pr checksandgh pr list/view --json statusCheckRollup(which T3's pull request panel uses) read the workflow run behind each Actions check. That field is Actions data, so withoutactions: readthose views could fail on repositories whose CI runs in GitHub Actions. This repository originally addedactions: readto tokens (commit8454be9) for the same reason. Honest scope:actions: readalso lets the token read that repository's workflow run logs and artifacts, as a person with read access can.members: read). The Lazurio Folder (LazurioPlatform#198) tells an agent without write to mention the Organization Owners in its proposal issue, found withgh api "orgs/<org>/members?role=admin". A live check from a Team Environment with today's write-tier token (nomembers) got200 []for that list and forrepos/<repo>/collaborators(GitHub's accepted-permission headers:members=readandmetadata=read; the collaborator list is filtered empty), so the agent silently found nobody. The installation already holdsmembers: read(the Team gate requires it), so this needs no new App permission or acceptance. It reads the Organization's members and Teams, as any member can; it changes nothing.issues: write. Then it also asks forissues: write, so write-capable Environments can open escalation issues.workflows: writestays write-tier only.triagemaps to the same read tier aspull. Pull request triage would needpull_requests: write, which also lets a token open and edit pull requests. An Environment without write access proposes through issues, so the tier stays minimal. Issue triage comes withissues: writeanyway (next point).issues: write. It is GitHub's smallest installation permission that can open an issue, and GitHub has no create-only variant. The same permission also lets the token edit, label, close, reopen and comment on existing issues of that repository. The docs and the AGENTS invariant say so explicitly, as they already do foractions: write.POST /v1/tokenadds"access": "read" | "write". The change is additive.policy check(offline) prints the exact permission set each tier asks for under the policy.policy check --livelists read and triage grants too, with anACCESScolumn and a(W write, R read)summary.mintToken(policy, repositoryId, access)requires the tier and fails before any GitHub call without it, so a lost tier can never become the write set. The handler also checks that the minted tier equals the verified one.Adapter (
brokered-gh/ credential helper)auth,alias,config,extension,search,--show-tokenand host overrides are denied, as before. Officialghruns with whatever token the broker minted, and GitHub decides.git clone/fetch/pullgo through the credential helper. The helper lives in the Workspace image and only usesrequestGitHubAppToken().token, so it needs no change.gh repo view,gh apireads,gh pr list,gh pr view,gh pr checksandgh run viewwork, and so doesgh issue createwhere declared.git pushfails with GitHub's own403.ghcommands get a conditional explanation. Any failedghcommand under the read tier keeps gh's own error and exit code; nothing is pre-blocked. The adapter cannot tell a refusal from a bad argument or an outage, so it adds one conditional stderr line: "Lazurio for GitHub: this Team Environment has read access to OWNER/REPO. If GitHub refused a change above (a push, pull request or other write), that is the reason; any other error above is GitHub's own. A change can be proposed in an issue (gh issue create) once the Organization has accepted the app's Issues permission, or by someone with write access." Tests cover a refused write and an unrelated failed read.requestGitHubAppTokenreturnsaccess. A missing value meanswrite, because older brokers mint only that tier. Any other value is an invalid response.What does not change
members: readand, when declared,issues: write. The rest of the broker is also unchanged: theworkflows: writeopt-in, the probe (members: read+metadata: read), the request budget, the error codes, andPOST /v1/workspace.installation_permissions.issues: writeis just another accepted-permission declaration, likeworkflows: write.404for a Team without a grant even on a public repository.App-permission prerequisite (only for issues, either tier)
Yes, an App change is needed for issues. The Lazurio for GitHub App currently requests no
issuespermission (publicGET /apps/lazurio-for-github: actions, administration, checks, contents, emails, members, metadata, pull_requests, workflows). Without it, both tiers work as described minus issue creation, and the write tier mints exactly what it minted before.installation_permissions.issues: write. The installation gate compares permissions exactly, so until this deploy the Worker returns502 token_unavailableand the Node adapter refuses to start and fails--verify-only. This is the same procedure asworkflows: writein 0.9.0. From then on, tokens of both tiers carryissues: write.Rollout (order matters)
v0.11.0.members: read, which every installation already holds. Usepolicy check --liveas the deploy gate.policy check --livefirst to see every Team's current grants and tiers. A lowered repository mints the read tier on the next request. A write-tier token minted earlier stays valid until it expires (at most one hour). If a grant is lowered before step 2, the Environment is cut off the repository.Rolling the broker back to 0.10.0 refuses read-only Teams again with
team_grant_missing. It never widens access.Verification
npm test: 88/88 pass (77 before). New and updated tests cover:read/triage→ read andwrite/maintain/admin→ write, in both body forms, through one revoked probe.actions: read,checks: read,contents: read,members: read,pull_requests: read, plusissues: writeonly when declared (not forissues: read, neverworkflows). It refuses any over- or under-scoped answer: any write (including Actions write), an undeclared issues grant, or a missing Actions, checks, members or PR read (and amembers: writeanswer).members: readand refuses an answer without it. It addsissues: writeonly when declared, alone or together withworkflows: write. It refuses an undeclared issues grant and a declared-but-missing one.pull/triagemint the read set,pushmints write, no grant mints nothing, and cross-Workspace is stillteam_grant_missing.team_grant_missingunchanged for 404, a payload without any permission, a missing permissions object, id mismatch (both tiers), a missing full name and 204.access. The allow-list refuses no write command up front. With a read token,issue create,api,pr view,pr checksandrun viewrun silently, whilepr createruns official gh, keeps its exit code and gets the conditional notice; an unrelated failed read (api …/contents/missing) also keeps gh's exit code and gets the same conditional notice, which never claims a refusal and names the Issues-permission prerequisite. No token appears in any output. A read-tier discovery proof succeeds.npm run check: pass. This covers the license check, syntax checks and the Wrangler dry-run.policy check. I rannode src/broker.mjs policy checkon the example policy, with and withoutissues: write; it prints the tier lines as documented.members: read. From the Team Environment of a pilot Organization, with today's brokered write-tier token:gh api orgs/<org>/members?role=admin→200 [](accepted permissionmembers=read) andgh api repos/<org>/<repo>/collaborators→200 [](acceptedmetadata=read), although the Organization has five Owners.🤖 Generated with Claude Code