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.

Form Owner and naming

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.

Table The eight Apps · one manifest each
RoleApp namePermissions in the manifestCreate
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.

  1. GitHub App name<prefix>-assay-<role>. The name becomes the slug and the bot login (<slug>[bot]), and must be unique across GitHub.
  2. Homepage URL — anything; https://assay.guide/desk-roles.html works.
  3. 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.
  4. Webhook — untick Active. No URL, no secret.
  5. Repository permissions — the row for the role, below. Metadata: Read-only is pre-set and mandatory.
  6. Subscribe to events — none.
  7. Where can this GitHub App be installed?Only on this account.
  8. Press Create GitHub App. On the next page, note the App ID, then under Private keys press Generate a private key; the .pem downloads once.
Table Repository permissions by role
RolePull requestsIssuesContentsChecksCommit statusesActionsWhat it does with them
reviewerRead & writeRead & writeRead-onlyRead-onlyRead-onlyRead-onlyposts review verdicts, files issues, reads CI rollups and run logs before a ready-flip; never writes code
workerRead & writeRead & writeRead & writeRead-onlyRead-onlyRead-onlyauthors branches and draft PRs, reads CI run logs while shepherding a PR; never approves
verifierRead-onlyRead & writeRead & writere-runs Verify tables, commits the evidence; structurally cannot approve a PR
deskRead & writeRead & writeRead & writeRead-onlyRead-onlyRead-onlycoordinator: routes work, reads CI run logs while shepherding, lands coordination writes via PRs
issue-loopRead & writeRead & writeRead & writeinbound issue lane
intake-loopRead & writeRead & writeRead & writeinbound intake lane; writes INTAKE entries
board-writerRead & writecommits STATUS.md past branch protection; the only identity on the ruleset bypass. Skip it if main is unprotected.
promoteRead & writeadditionally 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.

Table What is in an agent's environment — and what never is
CapabilityIn the environment?Why
App installation tokenscarriedminted per action from the role's .pem — the only credential the fleet ever writes with
The desk toolscarriedmint the token, then do the API and git work on the App's behalf
curl to the public releasecarriedunauthenticated binary downloads; no gh, no login
A logged-in gh or personal git credentialneverwould 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 sessionneverthe 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.

Set Eight marks · one per App
reviewer
board-writer
worker
verifier
desk
intake-loop
issue-loop
promote