assay · getting started · identities
Create the GitHub Apps
Every desk role posts under its own GitHub App, so a review, a verification, or a board write is attributed to an identity the author cannot post as. Eight Apps: one per desk role, plus a promote identity that holds the fleet’s only workflows grant and moves only on a human dispatch. GitHub has no API for creating an App — the create click is always a human's — but it does accept a pre-filled manifest, which turns eight forms of fourteen toggles each into eight clicks. The manual route is below it, unchanged, for anyone who would rather see every toggle.
Option A — one click per App, from a manifest
Each button below posts a GitHub App manifest to GitHub: name, homepage, and the exact permission set for that role, nothing else. Each opens in a new tab, so this fleet page stays put and you can fire off all eight in turn. GitHub shows you the pre-filled registration page; you review it and press Create GitHub App. Nothing is created until you do, and nothing on this site sees the result — GitHub hands the new App's private key back to you, through a one-time code, on the next page, where each tab collects its role and code so you can exchange all eight with one command.
Before you start, tell the forms where the Apps should live. Both fields are optional and are used only to fill the forms on this page; nothing is sent anywhere until you press a button.
Register under an organization you administer. Empty means your personal account, which can transfer the App to an organization later from its settings page.
App names are unique across all of GitHub, so each is prefixed: myorg-assay-reviewer becomes the bot login myorg-assay-reviewer[bot]. Pick your own handle.
Scripts are off or blocked, so the two fields above do nothing: the buttons register under your personal account with the default prefix. You can edit the name on GitHub's page before creating, and transfer ownership afterwards.
| Role | App name | Permissions in the manifest | Create |
|---|---|---|---|
| reviewer | myorg-assay-reviewer |
pull requests: write · issues: write · contents: read · checks: read · commit statuses: read · actions: read | |
| worker | myorg-assay-worker |
pull requests: write · issues: write · contents: write · checks: read · commit statuses: read · actions: read | |
| verifier | myorg-assay-verifier |
pull requests: read · issues: write · contents: write | |
| desk | myorg-assay-desk |
pull requests: write · issues: write · contents: write · checks: read · commit statuses: read · actions: read | |
| issue-loop | myorg-assay-issue-loop |
pull requests: write · issues: write · contents: write | |
| intake-loop | myorg-assay-intake-loop |
pull requests: write · issues: write · contents: write | |
| board-writer | myorg-assay-board-writer |
contents: write (only needed when main is branch-protected) |
|
| promote | myorg-assay-promote |
contents: write, workflows: write (the one workflows grant in the fleet; used only by the human-dispatched promote job) |
Every manifest also carries metadata: read, which GitHub grants to every App, and no webhook: these Apps are identities that tools mint tokens for, not services that listen. If GitHub's page shows the webhook as active, untick it before creating. On the registration page the name is editable — if GitHub says it is taken, change it there.
After you press Create GitHub App, GitHub sends you to apps-created.html with a one-time code in the address bar. That page shows the two commands that turn the code into the App's private key and ID on your machine. The code is valid for one hour and the key is issued exactly once — finish one App before starting the next.
Option B — by hand, from the permission table
The manifest route fills in exactly this. If you prefer to set each toggle yourself — or want to audit what the manifests asked for — open github.com/settings/apps/new (or github.com/organizations/<org>/settings/apps/new) once per role and enter the following. Leave every permission not listed at No access: a missed toggle is a capability gap that surfaces later as a 403 wearing some other error's clothes; an extra toggle is an over-grant nobody will notice.
- GitHub App name —
<prefix>-assay-<role>. The name becomes the slug and the bot login (<slug>[bot]), and must be unique across GitHub. - Homepage URL — anything;
https://assay.guide/desk-roles.htmlworks. - Callback URL, Setup URL — leave empty. Untick Expire user authorization tokens and Request user authorization (OAuth) during installation; these Apps never act for a user.
- Webhook — untick Active. No URL, no secret.
- Repository permissions — the row for the role, below. Metadata: Read-only is pre-set and mandatory.
- Subscribe to events — none.
- Where can this GitHub App be installed? — Only on this account.
- Press Create GitHub App. On the next page, note the App ID, then under Private keys press Generate a private key; the
.pemdownloads once.
| Role | Pull requests | Issues | Contents | Checks | Commit statuses | Actions | What it does with them |
|---|---|---|---|---|---|---|---|
| reviewer | Read & write | Read & write | Read-only | Read-only | Read-only | Read-only | posts review verdicts, files issues, reads CI rollups and run logs before a ready-flip; never writes code |
| worker | Read & write | Read & write | Read & write | Read-only | Read-only | Read-only | authors branches and draft PRs, reads CI run logs while shepherding a PR; never approves |
| verifier | Read-only | Read & write | Read & write | — | — | — | re-runs Verify tables, commits the evidence; structurally cannot approve a PR |
| desk | Read & write | Read & write | Read & write | Read-only | Read-only | Read-only | coordinator: routes work, reads CI run logs while shepherding, lands coordination writes via PRs |
| issue-loop | Read & write | Read & write | Read & write | — | — | — | inbound issue lane |
| intake-loop | Read & write | Read & write | Read & write | — | — | — | inbound intake lane; writes INTAKE entries |
| board-writer | — | — | Read & write | — | — | — | commits STATUS.md past branch protection; the only identity on the ruleset bypass. Skip it if main is unprotected. |
| promote | — | — | Read & write | — | — | — | additionally holds Workflows: Read & write — the one grant no desk App carries. Pushes reviewed, staged workflow files into .github/workflows/ when a human dispatches the promote job; nothing autonomous ever mints its token. |
Store each downloaded key as ~/.config/assay/<role>-app.pem, mode 0600, and the App ID as a <ROLE>_APP_ID= line in ~/.config/assay/apps.env (hyphens become underscores: ISSUE_LOOP_APP_ID). The desk tools refuse a key with any other mode.
Then install each App — also a click
Creating an App mints an identity; installing it grants that identity access to repositories. There is no API for this either. For each App open https://github.com/apps/<slug>/installations/new, choose the account, and choose Only select repositories — the repositories the fleet works in, no more. An installation is a credential for everything it covers.
When the install completes, the address bar reads …/settings/installations/<ID>. Record that as <ROLE>_INSTALL_ID_<OWNER>=<ID> in apps.env (owner upper-cased, hyphens to underscores: REVIEWER_INSTALL_ID_MY_ORG=123456), one line per account the App is installed on. The token minter picks the installation by the target repository's owner.
Two last human acts, both documented in docs/adopting-assay.md: add the board-writer App to the main ruleset bypass if you protect the branch, and run the Actions-variables step that tells the fleet which identities it trusts. A permission you add to an App later is pending until the account owner accepts it on the installation page — the App does not get it by being edited.
How the fleet authenticates — App tokens only
Once the Apps exist and their keys are in place, the automation authenticates with one thing only: a short-lived installation token, minted from a role's .pem for each action, through the desk tools. It holds no personal login and reaches for no user-level gh or git credential — for nothing. Public downloads use curl against the release, which needs no authentication at all. This is a rule, not a convenience: it is what keeps every attribution on this page honest.
| Capability | In the environment? | Why |
|---|---|---|
| App installation tokens | carried | minted per action from the role's .pem — the only credential the fleet ever writes with |
| The desk tools | carried | mint the token, then do the API and git work on the App's behalf |
curl to the public release | carried | unauthenticated binary downloads; no gh, no login |
A logged-in gh or personal git credential | never | would let an agent author, push, or merge as the human — straight past the reviewer App and every gate on this page |
| Any human token, cookie, or session | never | the person's identity must not be reachable from inside the fleet |
The acts that genuinely need a person — creating an App, granting a permission, changing a workflow, editing a branch ruleset, pressing merge, and giving the approving sign-off — run in a separate human account, in a browser or that person's own shell. They are never wired into the fleet's identity, because the whole point of the separated Apps is that the machine cannot reach them. The promote App is the deliberate, narrow exception that keeps this true for CI: it holds the fleet’s only workflows grant, and its token is minted only inside a job a human has just dispatched against a reviewed diff — the grant exists so that no desk App ever needs it.
Sample icons — one per role
A coherent set, one per role, drawn in the site's ledger-green line — download-and-use starting points, so the fleet reads as a family in a PR's reviewer list. Upload each as its App's avatar on the App settings page; swap them for your own whenever you like. The point is only that each identity looks like itself.