Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,43 @@ dependency-light and customer-neutral.
request and never cached; a name never selects a different object than the
id GitHub returns for it.
3. The App private key never leaves the broker workload.
4. A token is limited to one repository and exactly `actions: write`,
`checks: read`, `contents: write` and `pull_requests: write` (plus GitHub's
automatic `metadata: read`), plus `workflows: write` only when the reviewed
policy declares that the installation accepted it, and expires on GitHub's
installation-token schedule.
4. A token is limited to one repository and to exactly one of two permission
sets (plus GitHub's automatic `metadata: read`), and expires on GitHub's
installation-token schedule. The write tier is `actions: write`,
`checks: read`, `contents: write`, `members: read` and
`pull_requests: write`. The read tier is `actions: read`, `checks: read`,
`contents: read`, `members: read` and `pull_requests: read`. Each tier
adds `issues: write`, and the write tier `workflows: write`, only when the
reviewed policy declares that the installation accepted that permission. A
mint without an explicit tier fails.
5. Workspace credentials and installation tokens are never logged, committed,
stored in Git configuration or persisted by this service.
6. The broker authorizes a Workspace and repository, never a browser user.
7. No customer-specific policy, ids or credentials belong in this public repo.
8. Every behavior change requires focused tests for the positive path and
cross-Workspace, cross-repository and foreign-owner denial.
9. Every Workspace is bound to exactly one immutable GitHub Team id, and every
token issuance verifies live that the Team exists and holds a write-capable
grant on the exact repository, failing closed with `team_grant_missing`.
The live Team grant is the scope: the policy holds no repository list and
never narrows or widens it. Nothing from that readback is cached; the
policy, the Dashboard and any derived mapping are reviewed inputs, never a
second ACL.
token issuance verifies live that the Team exists and holds a grant on the
exact repository, failing closed with `team_grant_missing`. The grant's
role selects the tier: `push`, `maintain` or `admin` mints the write tier,
`pull` or `triage` the read tier. The live Team grant is the scope: the
policy holds no repository list or tier and never narrows or widens it.
Nothing from that readback is cached; the policy, the Dashboard and any
derived mapping are reviewed inputs, never a second ACL.

`actions: write` is GitHub's smallest installation-token permission that can
rerun a workflow. GitHub does not provide a rerun-only token capability, so
this permission also authorizes other Actions mutations inside the same exact
repository. Keep that provider limitation explicit in architecture and
operator documentation; never describe the token as rerun-only.

`issues: write` is likewise GitHub's smallest installation-token permission
that can open an issue. It also authorizes editing, labelling, closing,
reopening and commenting on existing issues of the same repository. Never
describe a read-tier token with `issues: write` as read-only or as able only
to create issues. The read tier's `actions: read` likewise reads more than
check status: it also reads the repository's workflow run logs and artifacts.

Use an isolated worktree and pull request for every change. Code, comments,
commits and pull-request descriptions are English. Run `npm test` and
`npm run check` before handoff.
122 changes: 86 additions & 36 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@ predeclared Team Workspaces. Its deployment-owned policy binds:
- each immutable Workspace id to exactly one immutable GitHub Team id and a
separate deployment credential reference.

The policy names no repositories. The scope of a Workspace is the live GitHub
Team grant: the repositories of the policy's Organization on which the
Workspace's Team holds a `push`, `maintain` or `admin` grant at the moment of
a request. The policy no longer narrows below GitHub. An Organization that
wants a narrower scope for a Team Environment does it with GitHub Team grants:
it grants that Team only the repositories the Environment should reach, or
binds the Environment to a dedicated, narrower Team. GitHub stays the one
place where access is granted, read and revoked, and an Organization Admin can
make a new repository available to a Team Environment herself, without a
The policy names no repositories and no access tiers. The scope of a
Workspace is the live GitHub Team grant: the repositories of the policy's
Organization on which the Workspace's Team holds any repository grant at the
moment of a request, and the grant's role selects the tier. A `push`,
`maintain` or `admin` grant mints the write tier; a `pull` or `triage` grant
mints the read tier. The policy no longer narrows below GitHub. An
Organization that wants a narrower scope for a Team Environment does it with
GitHub Team grants: it grants that Team only the repositories the Environment
should reach, at the role it should have, or binds the Environment to a
dedicated, narrower Team. GitHub stays the one place where access is granted,
read and revoked, and an Organization Admin can make a new repository
available to a Team Environment, or lower it to read, herself, without a
broker policy change or redeploy.

Credential references and credential values must be unique across all
Expand All @@ -34,13 +37,23 @@ owner, target type, repository selection mode and exact permissions. It does
not compare a repository set; there is none in the policy. The Node adapter
performs this gate before listening; the Worker performs it after local
authorization on every valid token request. Every token request then asks
GitHub to mint a new one-repository token with exactly `actions: write`,
`checks: read`, `contents: write` and `pull_requests: write`, plus
`workflows: write` when the policy declares that the installation accepted it,
and refuses a response with any other scope or any other repository.
Between the installation gate and the mint, the runtime performs the Team gate
described below; it is the step that turns a live GitHub Team grant into the
authority for the token.
GitHub to mint a new one-repository token with exactly the permission set of
its tier, and refuses a response with any other scope or any other repository:

- write tier: `actions: write`, `checks: read`, `contents: write`,
`members: read` and `pull_requests: write`, plus `workflows: write` and `issues: write` each
when the policy declares that the installation accepted it;
- read tier: `actions: read`, `checks: read`, `contents: read`,
`members: read` and `pull_requests: read`, plus `issues: write` when the
policy declares that the installation accepted it.

GitHub adds `metadata: read` to both. `members: read` lets an Environment
list the Organization Owners it mentions in an issue; GitHub answers that list
with an empty list, not an error, to a token without it. There is no default tier: a mint without
an explicit tier fails before reaching GitHub, so a lost tier can never become
the write set. Between the installation gate and the mint, the runtime
performs the Team gate described below; it is the step that turns a live
GitHub Team grant into the authority and the tier for the token.
Read-only Checks access and Actions write access let the standard GitHub CLI
render check runs and their workflow-run context and explicitly rerun a failed
workflow without a synthetic commit or human credential. GitHub exposes no
Expand All @@ -49,6 +62,30 @@ Actions mutations in the same repository. The remaining boundaries therefore
stay material: one immutable repository per short-lived token, a live Team
grant on exactly that repository and no Checks write access.

The read tier exists so an Organization can keep some repositories read-only
for a Team without cutting that Team's Environment off them. A read-tier token
clones, fetches and reads one repository and lists and views its pull requests,
check runs and GitHub Actions workflow runs, so people in a read-only
Environment see pull requests and CI. `actions: read` is there because GitHub
CLI check views and the `statusCheckRollup` behind T3's pull request list read
the workflow run of each Actions check; it also lets the token read that
repository's workflow run logs and artifacts, as a person with read access
can. The token receives no contents, pull request, Checks, Actions or workflow
write. `issues: write` is the
only write it can carry, so that an Agent without write access can propose a
change as an issue the way a person with read access does. GitHub has no
create-only issue permission: the same permission lets the token edit, label,
close, reopen and comment on existing issues of that repository, which is
closer to the `triage` role for issues than to read access. `triage` maps to
the same read tier as `pull`, because the permission that would add pull
request triage, `pull_requests: write`, also opens and edits pull requests.

`issues: write` is one opt-in for both tiers. An Organization opts in by
accepting the App permission and declaring it in its reviewed policy; then a
write-tier token also carries it, so a write-capable Environment can file the
escalation issues its work asks for. Without the declaration neither tier asks
for `issues` and the write tier is exactly its previous set.

`workflows: write` lets a Workspace push commits that change
`.github/workflows/`. Editing a workflow does not by itself run it or hand it
secrets, but a workflow it writes can, in an eligible run, reference and
Expand All @@ -59,7 +96,7 @@ approval). That is the same capability a Team member with a write grant
already has from GitHub, and an Organization opts into it by accepting the
App permission and declaring it in its reviewed policy. A policy without it
mints the base set, so the release can roll out before any Organization
accepts.
accepts. The same holds for `issues: write` in both tiers.

## Team binding and live grant verification

Expand All @@ -83,17 +120,20 @@ the id form resolves the repository's current name and owner by
`/repositories/{id}` (another owner is `repository_denied`), checks the Team's
grant with `/organizations/{org_id}/team/{team_id}/repos/{owner}/{repo}` and
the repository-permissions media type, revokes the probe, and only then mints
the one-repository token for the repository id GitHub returned. The returned
`owner.id` must equal the policy's Organization id. A missing Team, an
identity that differs from the policy assertions, a missing grant, a
`pull`/`triage`-only grant, a repository the installation cannot see or, for
the id form, a returned id that differs from the requested id is refused with
`403 team_grant_missing`; a GitHub outage, rate-limit or quota response is
`502 token_unavailable`. No refusal issues a repository token — the only
token minted before the decision is the short-lived probe, already revoked —
and nothing from the readback is retained between requests, so a revoked
grant can still act only through a token already issued, and a new grant
works on the next request.
the one-repository token of the grant's tier for the repository id GitHub
returned. The returned `owner.id` must equal the policy's Organization id.
`admin`, `maintain` or `push` selects the write tier, `triage` or `pull` the
read tier. A missing Team, an identity that differs from the policy
assertions, a missing grant or one without any of these roles, a repository
the installation cannot see or, for the id form, a returned id that differs
from the requested id is refused with `403 team_grant_missing`; a GitHub
outage, rate-limit or quota response is `502 token_unavailable`. No refusal
issues a repository token — the only token minted before the decision is the
short-lived probe, already revoked — and nothing from the readback is
retained between requests, so a revoked or
lowered grant can still act only through a token already issued, and a new or
raised grant works on the next request. The token response names the minted
tier as `access`, so the adapter and operators see which one was issued.

Names are inputs, never keys. The authorization keys are the immutable
Workspace id, Team id, Organization id and the repository id GitHub returns
Expand All @@ -115,11 +155,13 @@ path. A Workspace whose Team temporarily lacks a grant does not stop the
service for other Workspaces; it is refused per request.

`policy check --live` runs the installation gate and then, per Workspace,
verifies the Team identity and lists the Team's live write-capable
repositories. It is the migration and readback gate an operator runs before
deploying a policy. The repository rows are information; the command exits
non-zero only on a missing or drifted Team or installation drift, and its rows
never contain a token or credential.
verifies the Team identity and lists the Team's live repository grants with
the tier each one mints. Without `--live` it prints the exact permission set
each tier asks for under the policy. It is the migration and readback gate an
operator runs before deploying a policy or lowering Team grants. The
repository rows are information; the command exits non-zero only on a missing
or drifted Team or installation drift, and its rows never contain a token or
credential.

The Lazurio Dashboard is a read-only lens over this contract. It may show the
desired Team-to-repository mapping and derive a reviewed policy input, but
Expand Down Expand Up @@ -147,8 +189,9 @@ continue after the browser disconnects.
Workspace, then replace the client-side copy in its custody boundary.
- Remove the GitHub Team's grant on a repository, or delete the Team, to
refuse that Workspace's next token for the repository without touching the
policy or restarting the broker. Granting a Team a repository likewise takes
effect on the next request.
policy or restarting the broker. Lower the grant to `pull` or `triage` to
make the next token read-tier. Granting or raising a Team's grant likewise
takes effect on the next request.
- Remove a Workspace from the policy, or rotate its credential, to stop all of
its issuance.
- Rotate the App private key in deployment custody and restart the Node broker
Expand Down Expand Up @@ -203,7 +246,14 @@ GitHub API proxy. A repository command must resolve one HTTPS GitHub checkout
or one matching `--repo` selector, obtain a fresh token through `/v1/token`,
and then run the real CLI. The adapter holds no repository list: it sends the
repository name and accepts only a response for that same repository; the
broker's live Team gate decides. Installation tokens exist only in the child
broker's live Team gate decides. It also refuses nothing by tier: with a
read-tier token official `gh` runs and GitHub refuses what the token cannot
do, with GitHub's own error and exit code. After any failed read-tier command
the adapter adds one conditional stderr line: it names the read access as the
reason if GitHub refused a change, leaves every other error as GitHub's own and
says that proposing through an issue needs the accepted Issues permission. The
adapter cannot tell a refusal from a bad argument or an outage, so it never
claims which one happened. Installation tokens exist only in the child
environment and are never cached, rendered or written to GitHub CLI config.
The official CLI remains responsible for pull requests, REST and GraphQL.

Expand Down
Loading
Loading