Skip to content

Broker scope from live Team grants: policy v3 drops the repository allowlist (#14) - #17

Merged
immakermatty merged 3 commits into
mainfrom
agent/GH14-live-team-scope
Oct 3, 2026
Merged

immakermatty merged 3 commits into
mainfrom
agent/GH14-live-team-scope

Conversation

@immakermatty

@immakermatty immakermatty commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

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, maintain or admin at 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

  • Policy schema lazurio.github_app_broker.policy.v3: same as v2 minus repositories and minus workspaces[].repository_ids. v2 (and v1) are rejected with the message "migrate to v3 by deleting repositories and every workspaces[].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.
  • Installation gate still checks App identity, Organization target, selection mode and exact permissions. It no longer enumerates installation repositories (one GET instead of probe mint + pages + revoke).
  • POST /v1/token accepts exactly one of {"repository": "Owner/name"} or {"repository_id": n}. The id form keeps deployed v0.9.0 adapters working during rollout. Anything else returns 400 invalid_request. MAX_BODY_BYTES is unchanged.
  • Gates in order: credential (401, no GitHub traffic) → body shape → for the name form, the owner must equal github_owner.login case-insensitively, otherwise 403 repository_denied with no GitHub traffic → installation gate → Team gate. In the id form the Team gate first resolves GET /repositories/{id}; another owner gives repository_denied and 404 gives team_grant_missing. The gate then reads the Team repository grant with the repository-permissions media type. It requires owner.id == github_owner.id and push/maintain/admin, and for the id form the same returned id. The token is minted for the repository id GitHub returned.
  • Response adds repository (the canonical full_name GitHub returned) next to token, expires_at and repository_id. The handler also refuses a mint result for any id other than the verified one.
  • policy check --live verifies 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.
  • Adapter: GITHUB_REPOSITORY_POLICY_JSON, parseRepositoryPolicy, firstPolicyRepository and the client-side deny list are removed. The adapter sends {"repository": ...} and accepts only a response whose repository matches case-insensitively and whose repository_id is a positive integer. brokered-gh resolves the target from --repo/-R, GH_REPO or 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 run gh auth status / gh api user --jq .login from 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 revoked members+metadata probe. It mints no repository token. It returns {workspace_id, organization, github_team_id, github_team_slug} with Cache-Control: no-store. A missing or drifted Team returns 403 team_grant_missing; a GitHub failure returns 502 token_unavailable. It shares the core handler in both the Node and Worker adapters; the Worker repeats the installation gate per request.
  • Discovery proofs in the adapter: inside a checkout (or with GH_REPO), the repository mint proof as before. Anywhere else, requestWorkspaceProof calls /v1/workspace with 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.
  • Worker uses the same core path and keeps its per-request installation gate.
  • Version 0.10.0. The example policy is v3. README, ARCHITECTURE and AGENTS invariants 1, 2, 8 and 9 are updated.

What deliberately does not change

  • Token permissions (tokenPermissions, optional workflows: write) and the exact response-scope check.
  • The Team binding (one immutable Team id per Workspace, optional slug assertion), the probe permissions and the probe revocation.
  • Credential handling, the body size limit, Cache-Control: no-store, and how the Node adapter gates at startup and the Worker on every request.
  • Archived repositories are not special-cased (GitHub refuses writes itself).

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 as repository_denied with only the probe minted, a Team without a grant refused as team_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 with repositories or repository_ids rejected. 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).
  • Live read-only policy check --live against 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

  • Order: run policy check --live on 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.
  • Scope can widen: a Team that holds more write grants than its v2 allowlist gains those repositories. That is the intended semantics, and the migration section says to narrow such Teams on GitHub before switching.
  • selected installations: 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.
  • Request budget: Worker accepted name form 6 requests (was 9), id form 7; Node 5 / 6. /v1/workspace: Node 3, Worker 4.
  • Follow-up: Machines can drop the client-side github_broker.repositories / GITHUB_REPOSITORY_POLICY_JSON after the adapter rollout (the 0.10.0 adapter ignores it). That needs a separate Machines change.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

The PR appears safe to merge, with non-blocking gaps in migration readback and adapter support for names ending in .git.

Findings

  1. P2 Readback overstates usable grants ▶
  2. P2 Literal dot-git names change ▶

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.

  • Both runtimes use the shared Team-grant path before minting a one-repository token.
  • The migration check does not establish that its listed grants are usable by a selected installation.
  • Name normalization leaves a gap for repository names that themselves end in .git.

Diagram

sequenceDiagram
  participant C as Adapter or API client
  participant B as Broker
  participant G as GitHub
  C->>B: Workspace credential + repository name or id
  B->>B: Validate body and name-form owner
  B->>G: Verify installation (Worker per request)
  B->>G: Probe Team identity and repository grant
  G-->>B: Repository id, canonical name, permissions
  B->>G: Mint token for returned repository id
  G-->>B: One-repository token
  B-->>C: Token, repository id and canonical name
Loading

Reviews (1) · Last reviewed commit: "Broker scope from live Team grants: poli..."

…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>
Comment thread src/core.mjs
Comment thread adapter/broker-client.mjs Outdated
immakermatty and others added 2 commits October 3, 2026 13:01
…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 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 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.

@immakermatty
immakermatty merged commit 8e6caed into main Oct 3, 2026
3 checks passed
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.

Broker scope from live Team grants: new repositories should not need a policy redeploy

2 participants