Skip to content

Read-only Team grants: pull and triage mint a read-tier token (0.11.0) - #18

Merged
immakermatty merged 6 commits into
mainfrom
claude/read-only-team-grants
Oct 5, 2026
Merged

immakermatty merged 6 commits into
mainfrom
claude/read-only-team-grants

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

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, maintain or admin grant on the repository. A pull or triage grant was refused as 403 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.

Live Team grant Tier Token asks for (GitHub adds metadata: read) With the policy's issues: write declared
push, maintain, admin write actions: write, checks: read, contents: write, members: read (new), pull_requests: write (+ workflows: write if declared) + issues: write
pull, triage read (new) actions: read, checks: read, contents: read, members: read, pull_requests: read + issues: write
none, Team missing or drifted, foreign owner refused: unchanged nothing: 403 team_grant_missing / repository_denied nothing
  • Read tier: no write except the optional issues: 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.
  • Why actions: read. gh pr checks and gh 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 without actions: read those views could fail on repositories whose CI runs in GitHub Actions. This repository originally added actions: read to tokens (commit 8454be9) for the same reason. Honest scope: actions: read also lets the token read that repository's workflow run logs and artifacts, as a person with read access can.
  • Both tiers read Organization membership (members: read). The Lazurio Folder (LazurioPlatform#198) tells an agent without write to mention the Organization Owners in its proposal issue, found with gh api "orgs/<org>/members?role=admin". A live check from a Team Environment with today's write-tier token (no members) got 200 [] for that list and for repos/<repo>/collaborators (GitHub's accepted-permission headers: members=read and metadata=read; the collaborator list is filtered empty), so the agent silently found nobody. The installation already holds members: 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.
  • Write tier otherwise unchanged unless the policy declares issues: write. Then it also asks for issues: write, so write-capable Environments can open escalation issues. workflows: write stays write-tier only.
  • triage maps to the same read tier as pull. Pull request triage would need pull_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 with issues: write anyway (next point).
  • Honest scope of 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 for actions: write.
  • Response. POST /v1/token adds "access": "read" | "write". The change is additive.
  • Readback. policy check (offline) prints the exact permission set each tier asks for under the policy. policy check --live lists read and triage grants too, with an ACCESS column and a (W write, R read) summary.
  • No default tier. 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)

  • No tier-based refusals. The allow-list refuses nothing up front by tier. Only auth, alias, config, extension, search, --show-token and host overrides are denied, as before. Official gh runs with whatever token the broker minted, and GitHub decides.
  • Clone, fetch and pull work. With a read-tier token, git clone/fetch/pull go through the credential helper. The helper lives in the Workspace image and only uses requestGitHubAppToken().token, so it needs no change.
  • Reads work. gh repo view, gh api reads, gh pr list, gh pr view, gh pr checks and gh run view work, and so does gh issue create where declared.
  • Pushes fail with GitHub's error. A git push fails with GitHub's own 403.
  • Failed gh commands get a conditional explanation. Any failed gh command 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.
  • Discovery inside a checkout now succeeds for a read-only repository.
  • requestGitHubAppToken returns access. A missing value means write, because older brokers mint only that tier. Any other value is an invalid response.

What does not change

  • Write tier, apart from members: read and, when declared, issues: write. The rest of the broker is also unchanged: the workflows: write opt-in, the probe (members: read + metadata: read), the request budget, the error codes, and POST /v1/workspace.
  • Policy schema v3. installation_permissions.issues: write is just another accepted-permission declaration, like workflows: write.
  • Public repositories without a Team grant are still refused. I checked that GitHub's Team-repository endpoint answers 404 for 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 issues permission (public GET /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.

  1. Once: an App owner adds Repository permissions → Issues: Read and write. GitHub then asks every installation to accept. Installations that have not accepted keep their current permissions.
  2. Per Organization: an owner reviews and accepts the request. Accepting also accepts any other permission the App requests at that moment.
  3. Per Organization, in the same short window: the policy declares the whole newly accepted set, including installation_permissions.issues: write. The installation gate compares permissions exactly, so until this deploy the Worker returns 502 token_unavailable and the Node adapter refuses to start and fails --verify-only. This is the same procedure as workflows: write in 0.9.0. From then on, tokens of both tiers carry issues: write.

Rollout (order matters)

  1. Release v0.11.0.
  2. Deploy broker 0.11.0 for each Organization's broker. No policy change is needed; write grants mint what they did before plus members: read, which every installation already holds. Use policy check --live as the deploy gate.
  3. (Optional) Pin the 0.11.0 adapter in the Workspace image, so a refused write is explained. The 0.10.0 adapter already works with read-tier tokens.
  4. (Where the Organization wants issues: proposals from read-only Environments, escalations from write-capable ones) do the App-permission prerequisite above.
  5. Only then does the Organization lower Team grants on GitHub. Run policy check --live first 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:
    • Grant readback per tier. read/triage → read and write/maintain/admin → write, in both body forms, through one revoked probe.
    • Read tier mint. It asks for exactly actions: read, checks: read, contents: read, members: read, pull_requests: read, plus issues: write only when declared (not for issues: read, never workflows). 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 a members: write answer).
    • Write tier mint. It asks for members: read and refuses an answer without it. It adds issues: write only when declared, alone or together with workflows: write. It refuses an undeclared issues grant and a declared-but-missing one.
    • No default tier. A missing or unknown tier never reaches GitHub.
    • End to end through the real core client. pull/triage mint the read set, push mints write, no grant mints nothing, and cross-Workspace is still team_grant_missing.
    • team_grant_missing unchanged for 404, a payload without any permission, a missing permissions object, id mismatch (both tiers), a missing full name and 204.
    • Lost tier fails closed. The handler fails closed when the gate or the mint loses or changes the tier. The Worker passes the read tier through.
    • Adapter. It parses access. The allow-list refuses no write command up front. With a read token, issue create, api, pr view, pr checks and run view run silently, while pr create runs 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.
  • Offline policy check. I ran node src/broker.mjs policy check on the example policy, with and without issues: write; it prints the tier lines as documented.
  • Live evidence for 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 permission members=read) and gh api repos/<org>/<repo>/collaborators → 200 [] (accepted metadata=read), although the Organization has five Owners.
  • Not verified live. I did not mint against a real installation, because that needs the App key, which stays in the deployment's custody. The per-Organization smoke after deploy (step 2) covers it.

🤖 Generated with Claude Code

immakermatty and others added 2 commits October 5, 2026 17:54
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>
@immakermatty
immakermatty marked this pull request as ready for review October 5, 2026 15:58
Comment thread adapter/brokered-gh.mjs
immakermatty and others added 2 commits October 5, 2026 18:04
…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 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.

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 optional issues: write declaration, 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.

immakermatty and others added 2 commits October 5, 2026 18:37
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 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.

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.

immakermatty added a commit to Lazurio/LazurioPlatform that referenced this pull request Oct 5, 2026
… 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>
@immakermatty
immakermatty merged commit aca0525 into main Oct 5, 2026
3 checks passed
@immakermatty
immakermatty deleted the claude/read-only-team-grants branch October 5, 2026 17:03
immakermatty added a commit to Lazurio/LazurioPlatform that referenced this pull request Oct 5, 2026
…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>
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.

2 participants