> ## Documentation Index
> Fetch the complete documentation index at: https://docs.outerlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub App permissions and build tokens

> What the OuterLayer GitHub App is allowed to do, and the short-lived repository tokens the gateway issues for a build.

The GitHub App reads your repository and posts results to it. For a build, the gateway also issues a short-lived token limited to the item's repository.

In a container build the runner asks for these tokens and holds them, so the build never does. See [the git remote and the build's tokens](/container-builds#the-git-remote-and-the-builds-tokens). A build that runs with hooks of your own fetches and pushes with the GitHub login already on the host. Every host build still opens its pull request as the App: see [Opening a build's pull request](#opening-a-builds-pull-request).

## Permissions the App asks for

If GitHub's list for the App differs from this table, GitHub is right.

| Permission | Access | Why |
| - | - | - |
| Metadata | Read | GitHub requires it of every App. The App uses it to list the repositories an installation includes. |
| Contents | Read and write | Read files, commits and branches, including `.outerlayer/policy.yaml` and the validators. Write is used only by a push token, so a build can push the item's branch. |
| Issues | Read and write | Read issues for **Add work**. Post the evidence comment on a pull request, which GitHub treats as an issue comment. |
| Pull requests | Read and write | Read pull request state and reviews. Write is used to merge from the Work page, and to open or edit a build's pull request through `PUT /v1/work-items/{workItemId}/pull-request`. |
| Checks | Read and write | Post the evidence check run on each pull request. Read the results of CI that reports through checks. |
| Actions | Read | Read the results of GitHub Actions workflow runs. |
| Commit statuses | Read | Read the results of CI that reports through commit statuses. |
| Workflows | Write | Used only by a push token, and only when the repository's policy allows it. It lets a build change files under `.github/workflows/`. |

### 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 with `repository_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

Run `outerlayer 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/access` marks each as `requestedByApp`.
* It warns when the repository's default branch does not require a pull request. See [Protect the default branch](#protect-the-default-branch).
* It passes otherwise.

Builds need contents, pull requests and issues write, and actions, checks, commit statuses and metadata read. A permission held at read does not count where write is needed.

`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](/concepts) hands the build a short-lived [item key](/concepts) (prefixed `olitem_`). `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](/reference/cli-runner#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](#who-can-get-a-token) 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.

Both addresses are GitHub `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.

The release of each attempt records the result as `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](/concepts).** 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 build` or chose **Build** on the Work page. For an amend build, it is the person whose newest comment or `fail` on the item started it.

A request made with an API key counts as a member's only while all of these are true:

* 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.

A build started from a local session gets no token.

`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 with `POST /v1/work-items/{workItemId}/claim/tokens` and a body of `{ "access": "read" }` or `{ "access": "push" }`. The API reference has the full request and response.

| Access | Permissions on the repository |
| - | - |
| `read` | Contents, issues, pull requests, actions, checks and commit statuses, all read only. |
| `push` | Contents write. Workflows write is added when the repository allows it. |

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`:

```yaml theme={"system"}
build:
  workflows: allow
```

The gateway reads the file from the repository's default branch. A change on the item's own branch does not count, so a build cannot grant itself the permission. See [Policy and validators](/policy-and-validators).

## When a token is refused

| Status | Code | Meaning |
| - | - | - |
| 400 | `invalid_request_body` | `access` is missing, or is neither `read` nor `push`. |
| 401 | `runner_key_signature_required`, `runner_key_signature_invalid`, `runner_key_signature_stale`, `runner_key_signature_replayed` | The request was not signed by the host key the runner key is bound to. See [The host key](/run-a-host-as-a-service#the-host-key). |
| 403 | `forbidden` | The key does not hold `work.claim`. |
| 403 | `claim_not_held` | The caller is not the key that holds the item's newest claim. |
| 403 | `build_request_not_from_member` | A member of the organization did not ask for this build. |
| 404 | `work_item_not_found` | No such work item. |
| 409 | `work_item_withdrawn` | The item was withdrawn. |
| 409 | `repository_permissions_pending` | GitHub refused because the installation has not accepted a permission the token needs. An owner has to accept it. |
| 410 | `claim_expired` | The item has no live claim. It was released, or it lapsed. |
| 422 | `work_item_has_no_repository` | The item names no repository. |
| 422 | `repository_not_connected` | The factory has not connected the item's repository. See [Connect a repository](/connect-repository). |
| 422 | `repository_not_in_installation` | The installation does not include the repository. Grant it on GitHub. |
| 503 | `repository_tokens_unavailable` | This gateway has no GitHub App configured. On a self-hosted gateway, set the App's id and private key. |
| 503 | `github_unavailable` | GitHub was rate limiting or failing. Ask again shortly. |

## 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.

## What the gateway records

For every token it issues, the gateway records which claim, repository, access and key it was for, and the permissions it asked GitHub for. It also writes a **Repository Token Issued** event to the organization audit log. It never stores the token.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.