Permissions the App asks for
If GitHub’s list for the App differs from this table, GitHub is right.Accepting a permission the App asks for
An installation keeps the permissions it accepted. When the App asks for more, GitHub does not widen the installation on its own. An owner of the installation must accept the request. Until then, a token that needs a missing permission is refused withrepository_permissions_pending. Everything else keeps working.
Opening or editing a build’s pull request needs pull requests write (pull_requests: write on GitHub). A new installation gets it when it is installed. An older installation that never accepted it must accept it first. Until then, the route answers repository_permissions_pending and opens nothing.
To accept, open the installation’s settings link that outerlayer doctor prints. On GitHub the request shows as Review request. One acceptance covers every repository in the installation.
Check what each repository is missing
Runouterlayer doctor on any signed-in machine, with a factory key or a login. It adds one GitHub App: owner/name check per repository connected to the factory:
- It warns when the installation has not accepted a permission builds need, and names each one. The fix says who acts. A permission the App’s own settings request is accepted by an owner of the installation, on the installation’s settings page. A permission the App’s settings do not request cannot be accepted yet: the App’s settings must add it first. The response of
GET /v1/repositories/accessmarks each asrequestedByApp. - It warns when the repository’s default branch does not require a pull request. See Protect the default branch.
- It passes otherwise.
doctor only reads. It changes nothing on GitHub or in your factory. It asks the gateway with GET /v1/repositories/access, which any key holding git.read can call; the API reference has the response.
Opening a build’s pull request
A host build opens its pull request as the App, not with the GitHub login on the host. A claim hands the build a short-lived item key (prefixedolitem_). outerlayer work open-pr sends PUT /v1/work-items/{workItemId}/pull-request with that key. Only an item key may call this route. In your own session, work open-pr runs gh under your login instead.
The route asks GitHub for a token limited to the item’s repository with pull requests write and contents read, opens or edits the pull request on the branch the claim named, and revokes the token when the call ends. The token is never returned or stored. See the branch a claim names for the branch rule and the refusals.
Who a build’s commits are authored by
Every commit an unattended build makes is authored and committed by the App’s bot account,<app-slug>[bot]. GitHub links it to the App, so a reviewer can tell a factory commit from a person’s. The runner sets the author for every process the build starts, and git prefers it to any identity configured on the host. The host owner’s own git identity never appears on a factory commit.
The member who asked for the build is credited as co-author. Each commit message ends with a Co-authored-by: <name> <email> line for them, so the commit counts on their GitHub profile. The requester is the same person the token rules use: for an implement build, whoever ran outerlayer work build or chose Build on the Work page, and for an amend build, the author of the comment that started it.
The co-author comes from the GitHub account the member signed in with, not from their profile:
- The email is
<github-user-id>+<login>@users.noreply.github.com, built from that sign-in. A member cannot change it by editing their profile. - The name is the member’s profile name, or their GitHub login when the profile has none. The login is also used when the profile name is over 200 characters or holds a line break or an angle bracket, which a commit trailer cannot carry.
- A member who signed in without GitHub gets no co-author line. The commit is still authored by the bot.
noreply addresses. No personal email address is written into a commit.
This needs two things: a gateway that holds the App’s keys, and a runner at protocol 8 or newer.
- A gateway without the App’s keys sends no identity. Commits keep the host’s own git identity.
- For an older runner, the gateway skips the bot lookup, because that runner ignores the identity. The claim succeeds even when GitHub cannot be reached.
- If GitHub rate limits the bot lookup, the claim answers 503 at once and the runner claims again.
environment.commitIdentity: author is app or host, and coAuthor is credited or none. GET /v1/work-items/{workItemId} returns it on each claim.
The runner adds the co-author line with git hooks of its own. Your repository’s own git hooks still run first, and a hook that refuses a commit still refuses it.
A build that a host’s own provision hook runs through an exec gets the bot as author and no co-author line. Its commands may run inside a virtual machine, where the runner’s hooks directory does not exist. Commits from your own local session are not affected.
Who can get a token
A token is issued only when both of these hold at the moment it is asked for:- The caller holds the item’s live claim. Error codes call a claim a lease. The API key that claimed the item is the only key that can ask. A released or lapsed claim gets nothing, and neither does a different key.
- A member of your organization asked for the build. For an implement build, that is whoever ran
outerlayer work buildor chose Build on the Work page. For an amend build, it is the person whose newest comment orfailon the item started it.
- The key still exists and has not expired.
- The key is bound to a member.
- That member is still active in the organization.
- The key does not hold
work.claim. A key that holds it is a runner’s key, so a build requested with one is a machine’s.
POST /v1/work-items/build still accepts a request from a key that fails the member test, with no member bound or holding work.claim, because a host that runs builds with its own hooks needs no tokens. The response then carries repositoryTokens with earnable: false and the reason, and outerlayer work build prints a warning that a container host will fail the build.
These checks run against the current records every time, not against what was true when the claim began. A member removed from the organization stops earning tokens at once.
The two kinds of token
Ask withPOST /v1/work-items/{workItemId}/claim/tokens and a body of { "access": "read" } or { "access": "push" }. The API reference has the full request and response.
Either token names only the item’s repository. It lasts as long as GitHub says in
expiresAt, about an hour. The response carries Cache-Control: no-store.
Letting a build change workflows
A build cannot change files under.github/workflows/ unless the repository allows it. Set this in .outerlayer/policy.yaml:
When a token is refused
Protect the default branch
A push token can write to any branch of the item’s repository, and a push runs that repository’s CI. Two settings keep a build from reaching further than its own branch:- Require a pull request before merging on the default branch. Use a ruleset with the Require a pull request before merging rule, or classic branch protection with required reviews. A build then cannot push to the default branch directly; its change lands only through a pull request someone approves.
- Keep CI secrets in protected environments. Put deployment keys and other secrets in a GitHub environment whose deployment branches are limited to the default branch, and reference that environment from the workflow jobs that need them. A workflow run on a build’s branch then gets no secret it could print or send.
outerlayer doctor warns about a default branch that requires no pull request. It reads rulesets with the installation’s own access. Reading classic branch protection needs the Administration permission, which the App does not ask for. So for a branch protected the classic way, doctor cannot tell whether reviews are required, and says so instead of warning. A ruleset is readable either way.