Repository navigation
Broker scope from live Team grants: policy v3 drops the repository allowlist (#14) - #17
Conversation
…lowlist (#14) The live GitHub Team grant is now the whole scope of a Workspace. Policy schema v3 removes `repositories` and `workspaces[].repository_ids`; v2 and v1 are rejected with a migration message, and a half-migrated v3 policy that still carries either key fails closed. - The installation gate keeps identity, target, selection mode and exact permissions and no longer enumerates installation repositories. - `POST /v1/token` accepts exactly one of `{"repository": "Owner/name"}` or `{"repository_id": n}` (kept for deployed 0.9.0 adapters); anything else is `400 invalid_request`. A name of another owner is `403 repository_denied` without GitHub traffic; the id form resolves the live coordinate first and refuses another owner the same way. - The Team gate mints for the repository id GitHub returns from the live grant and requires the Organization owner id and a push/maintain/admin grant. The response adds the canonical `repository` name. - `policy check --live` verifies each Team and lists its live write-capable repositories as information; it fails only on Team or installation drift. - The adapter drops the client-side repository policy: it sends the name, accepts only a response for the same repository and has no default repository outside a checkout. - Version 0.10.0; README, ARCHITECTURE and AGENTS invariants updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ted mode Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…a checkout Launchpad and T3 run `gh auth status` and `gh api user --jq .login` from the Folder root, which is not a repository. Without a policy default those proofs had no repository and would report GitHub as disconnected. - Broker: `POST /v1/workspace` runs the credential gate, the installation gate and the Team identity read through the revoked probe, mints no repository token and returns the Workspace, Organization and Team. Team missing or drifted is 403 team_grant_missing; GitHub failure is 502. It shares the core handler in the Node and Worker adapters. - Adapter: discovery mints its proof for the checkout or GH_REPO when there is one and otherwise calls `requestWorkspaceProof`; any failure stays fail closed and a failed repository proof never falls back. 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 security review of fdf45e2: APPROVED. The v3 policy rejects legacy and residual repository lists; name and id forms enforce policy Organization identity, a live immutable Team grant with write capability, and the returned repository id before minting an exact one-repository token. Credential failures make no GitHub calls. The repository-independent Workspace proof reads only installation/Team identity through a revoked probe, returns no token, and fails closed. The adapter checks the returned repository name and positive id. README and architecture make GitHub Team write grants the scope and document selected-installation and migration limits. Locally: npm test 77/77 and npm run check pass under the host heavy lock; CI test and OCI checks green. Both earlier review threads are resolved: selected-installation readback is documented as informational, and literal .git names were already unsupported by the v0.9 adapter.
Why
GitHub is the only access authority. Today, when an Organization grants a Team access to a new repository on GitHub, the operator still has to edit the broker policy (
repositories,workspaces[].repository_ids) and redeploy the Worker. A Machines change is also needed for the client-side list. On 2026-10-02 a real Organization Admin could not make a new module available to her Team Environments on her own. v0.9.0 already checks the Team grant live on every mint (the Team gate). The static allowlist only gets in the way of adding access; it no longer protects against anything GitHub does not already enforce.Closes #14.
Target state
The live GitHub Team grant is a Workspace's whole scope. A Workspace gets a token for exactly those repositories of the policy's Organization on which its immutable Team holds
push,maintainoradminat the moment of the request. The policy no longer narrows the scope below what GitHub grants. An Organization that wants a narrower scope for a Team Environment sets it with GitHub Team grants: it grants that Team only what the Environment should reach, or binds the Environment to a dedicated, narrower Team.What changes
lazurio.github_app_broker.policy.v3: same as v2 minusrepositoriesand minusworkspaces[].repository_ids. v2 (and v1) are rejected with the message "migrate to v3 by deletingrepositoriesand everyworkspaces[].repository_ids; the live Team grant is the scope". A v3 policy that still has either key is rejected, so a half-migrated policy fails closed.POST /v1/tokenaccepts exactly one of{"repository": "Owner/name"}or{"repository_id": n}. The id form keeps deployed v0.9.0 adapters working during rollout. Anything else returns400 invalid_request.MAX_BODY_BYTESis unchanged.github_owner.logincase-insensitively, otherwise403 repository_deniedwith no GitHub traffic → installation gate → Team gate. In the id form the Team gate first resolvesGET /repositories/{id}; another owner givesrepository_deniedand 404 givesteam_grant_missing. The gate then reads the Team repository grant with the repository-permissions media type. It requiresowner.id == github_owner.idand push/maintain/admin, and for the id form the same returned id. The token is minted for the repository id GitHub returned.repository(the canonicalfull_nameGitHub returned) next totoken,expires_atandrepository_id. The handler also refuses a mint result for any id other than the verified one.policy check --liveverifies each Team and lists the Team's live write-capable repositories (paginated) as information. It exits non-zero only on a missing or drifted Team or on installation drift.GITHUB_REPOSITORY_POLICY_JSON,parseRepositoryPolicy,firstPolicyRepositoryand the client-side deny list are removed. The adapter sends{"repository": ...}and accepts only a response whoserepositorymatches case-insensitively and whoserepository_idis a positive integer.brokered-ghresolves the target from--repo/-R,GH_REPOor the checkout's brokered HTTPS origin. If there is no target, it fails closed and says so.POST /v1/workspace(new): a repository-independent connection proof. Launchpad and T3 rungh auth status/gh api user --jq .loginfrom the Folder root, which is not a repository. The endpoint runs the same credential gate (401, no GitHub traffic), then the installation gate and the Team identity read through the existing revokedmembers+metadataprobe. It mints no repository token. It returns{workspace_id, organization, github_team_id, github_team_slug}withCache-Control: no-store. A missing or drifted Team returns403 team_grant_missing; a GitHub failure returns502 token_unavailable. It shares the core handler in both the Node and Worker adapters; the Worker repeats the installation gate per request.GH_REPO), the repository mint proof as before. Anywhere else,requestWorkspaceProofcalls/v1/workspacewith the same origin pin, Workspace id, credential path and timeout validation. A successful proof means connected. Any failure fails closed, and a failed repository proof never falls back to the Workspace proof.What deliberately does not change
tokenPermissions, optionalworkflows: write) and the exact response-scope check.Cache-Control: no-store, and how the Node adapter gates at startup and the Worker on every request.Verification
npm test: 77 tests, 77 pass (42 broker, 15 Worker, 20 adapter). New or rewritten tests cover both body forms, the 400 shapes (both keys, neither, extra key, string/zero/negative/fractional/unsafe ids,../., URL forms, extra path segments), a foreign-owner name refused with no GitHub traffic (Node and Worker), an id form resolving to another owner refused asrepository_deniedwith only the probe minted, a Team without a grant refused asteam_grant_missing, pull-only and triage-only grants refused, a cross-Workspace request (the other Team has no grant) refused, a mint result for another id refused, the adapter rejecting a response for a different repository, a v2 policy rejected with the migration message, and v3 policies withrepositoriesorrepository_idsrejected. Also covered: pagination and the readback for a missing Team. For/v1/workspace: a proof from the Folder root succeeds through the endpoint with no repository token minted (probe mint, Team read, revoke only). It fails closed on 401 (no GitHub traffic), 403 (missing or drifted Team) and 502 (outage), both in the broker and end to end through the adapter. The Worker gives the same results and verifies the installation first. The adapter rejects a proof for another Workspace or one carrying a token. A failed repository proof inside a checkout does not fall back.npm run check: green (license check, syntax, Wrangler dry-run).policy check --liveagainst a v3 conversion of one real deployment policy, kept in a temporary directory and never committed: exit 0, 3 Workspaces checked, 3 Teams ok, 0 refused, 35 write-capable repository grants. Each Team's live grants equal its former v2 allowlist exactly (5/5, 15/15, 15/15), so for that deployment the switch neither widens nor narrows access.Rollout, risks and follow-ups
policy check --liveon the converted policy, then deploy broker 0.10.0 with the v3 policy, then roll out the 0.10.0 adapter. The 0.10.0 broker accepts both body forms; the 0.10.0 adapter needs a 0.10.0 broker. Rolling back to 0.9.0 needs the v2 policy back.selectedinstallations: a new repository must also be added to the App installation on GitHub before it can be minted. This is still a GitHub-side action for the Organization Admin, not the broker operator./v1/workspace: Node 3, Worker 4.github_broker.repositories/GITHUB_REPOSITORY_POLICY_JSONafter the adapter rollout (the 0.10.0 adapter ignores it). That needs a separate Machines change.🤖 Generated with Claude Code
The PR appears safe to merge, with non-blocking gaps in migration readback and adapter support for names ending in
.git.Findings
Summary
The PR replaces policy repository allowlists with live GitHub Team grants, adds name-based token requests while retaining the id form, and updates the adapter and operator readback.
.git.Diagram
Reviews (1) · Last reviewed commit: "Broker scope from live Team grants: poli..."