Docs: runner registration endpoints in the API reference, runner tokens with the other token types (#92)
#88 documented registration on the Runners page. The generated API reference still described claim as personal-token only and had no descriptions for the registration-token, register, list and remove endpoints. The token pages and the architecture page's identity step didn't list drg_ and drn_ tokens. Claude-Session: https://claude.ai/code/session_016q5wwBFck8JHq14gHUM2CL Co-authored-by: Claude <noreply@anthropic.com>
Canberk Demirci authored · GitHub committed signed1 parent 5edd6f9commit 0dbc898a962cd3724ccad2ba6775749a88e13cdaBrowse files
4 files changed+37−5
Files (4)
modifiedapps/diffinery/docs/accounts/permissions.md+2−1View file
| @@ -215,8 +215,9 @@ | ||
| 215 | 215 | | --- | --- | --- | --- | |
| 216 | 216 | | Job token | `djt_` | Each workflow job gets one as `GITHUB_TOKEN` (also `DIFFINERY_TOKEN`) | Act as `diffinery-actions[bot]` with what the workflow's `permissions:` grant (see below). Container images follow the job's `permissions: packages`: it pulls its own repository's images, its owner's images not linked to a repository and public images, and pushes only to images linked to its own repository (creating them, linked, on first push) | |
| 217 | 217 | | Sandbox token | `dst_` | Each agent sandbox gets one | Act as the person or agent that started the sandbox, capped at `write`, on that repository | |
| 218 | +| Runner token | `drn_` | A self-hosted runner gets one when it registers with a repository admin's registration token | Claim that repository's queued jobs, with the labels it registered. Nothing else: no reads, pushes or other API calls. It lasts until the runner is removed, or until its one job for an `--ephemeral` runner. See [Manage runners](/docs/actions/runners#manage-runners) | | |
| 218 | 219 | |
| 219 | -Both can also read other public repositories (as anyone can), but no other private repository. They stop working when the job completes or the sandbox closes. See [Secrets and variables](/docs/actions/secrets-and-variables) and [Sandboxes](/docs/agents/sandboxes). | |
| 220 | +Job and sandbox tokens can also read other public repositories (as anyone can), but no other private repository. They stop working when the job completes or the sandbox closes. See [Secrets and variables](/docs/actions/secrets-and-variables) and [Sandboxes](/docs/agents/sandboxes). | |
| 220 | 221 | |
| 221 | 222 | ### What a job token may do |
| 222 | 223 |
modifiedapps/diffinery/docs/accounts/tokens.md+3−1View file
| @@ -239,6 +239,8 @@ | ||
| 239 | 239 | - **Job tokens** (`djt_`) are given to each workflow job as `GITHUB_TOKEN` and `DIFFINERY_TOKEN`. They act as `diffinery-actions[bot]` on the repository the job runs in, with what the workflow's `permissions:` grant, as on GitHub: each API endpoint needs its permission (`contents`, `issues`, `pull-requests`, `statuses`, `checks` and so on), and a request without it gets `403 Resource not accessible by integration`. Without `permissions:` a job gets the repository's default, which is read-only (`contents` and `packages` read) for new repositories; an admin changes it with the repository's workflow permissions setting (`PUT /api/v3/repos/{owner}/{repo}/actions/permissions/workflow`). A job token's role never goes above write, so branch protection applies to it, and it never changes repository settings, secrets, variables, webhooks, runners or branch protection, or creates or changes workflow files. A run whose code came from an agent acts as an agent, so it can't push or merge to the default or protected branches. The token also reads other public repositories (not other private ones), uses the container registry according to `permissions: packages`, and stops working when the job finishes. See [Workflow permissions](/docs/actions/workflow-permissions) and [Secrets and variables](/docs/actions/secrets-and-variables). |
| 240 | 240 | - **Sandbox tokens** (`dst_`) are given to each agent sandbox. They act as whoever started the sandbox, capped at write, on that repository, can also read other public repositories, and stop working when the sandbox closes. See [Sandboxes](/docs/agents/sandboxes). |
| 241 | 241 | |
| 242 | +Self-hosted runners have tokens of their own too. A repository admin creates a **registration token** (`drg_`, valid for an hour), and each runner trades it for a **runner token** (`drn_`) that can claim that repository's jobs and nothing else: it can't read the repository, push or call any other API. Removing the runner ends its token. See [Manage runners](/docs/actions/runners#manage-runners). | |
| 243 | + | |
| 242 | 244 | [Push protection](/docs/security/push-protection) blocks a push or commit that contains a job, sandbox or [agent task](/docs/agents/tasks) model token (`dmt_`), as it does for `dfy_` tokens, so one printed into a file or a commit message by a job or an agent isn't stored in the repository. |
| 243 | 245 | |
| 244 | 246 | ## Security emails about tokens |
| @@ -263,7 +265,7 @@ | ||
| 263 | 265 | | Message | Cause and fix | |
| 264 | 266 | | --- | --- | |
| 265 | 267 | | `401 invalid, expired or revoked token` | The token is unknown, expired or revoked, or your account is disabled or not yet active. Create a new token. | |
| 266 | -| `401 unrecognised token; create one under Settings → Access tokens` | The credential doesn't start with `dfy_` (or `djt_`, `dst_`), for example a GitHub token or a password. Use a Diffinery token from **Settings → Access tokens**. | | |
| 268 | +| `401 unrecognised token; create one under Settings → Access tokens` | The credential doesn't start with `dfy_` (or `djt_`, `dst_`, `drn_`), for example a GitHub token or a password. Use a Diffinery token from **Settings → Access tokens**. | | |
| 267 | 269 | | `401 authentication required` | No token was sent to a private repository. Configure Git credentials or add the `Authorization` header. | |
| 268 | 270 | | `404 repository not found` | You have no access, or the token is restricted to other repositories. A `read` token that tries to change a private repository it can't see also gets this. | |
| 269 | 271 | | `401 malformed basic credentials` | The `Authorization: Basic` value isn't valid base64. Encode `username:token` with base64, or send `Authorization: Bearer dfy_...` instead. | |
modifiedapps/diffinery/docs/self-hosting/architecture.md+1−1View file
| @@ -57,7 +57,7 @@ | ||
| 57 | 57 | |
| 58 | 58 | 1. **Cross-site check.** A request that changes state (anything but `GET`, `HEAD` and `OPTIONS`) and carries no `Authorization` credential is refused with `403 cross-site request refused` if the browser marks it as coming from another site (`Sec-Fetch-Site`, or a foreign `Origin`). Token clients are exempt. The web UI's forms also use a double-submit CSRF token. |
| 59 | 59 | 2. **D1 session.** All D1 queries of the request run in one D1 session (see [D1 and read replication](#d1-and-read-replication)). |
| 60 | -3. **Identity.** `GET` and `HEAD` requests for profile pictures (`/api/avatars/...`) skip this step and run as anonymous, because they are public and appear many times on every page. Requests to the Actions cache and artifact services (`/twirp/...` and `/_runtime/...`) skip it too: they carry their own signed token or URL (see [Actions cache and artifact services](#actions-cache-and-artifact-services)). For everything else, a `dfy_` token in the `Authorization` header (Bearer, `token` or Basic) becomes an account principal with the token's scopes. The push token of a GitHub import is a `dfy_` token too, but it is accepted only on Git smart HTTP, Git LFS and that import's own agent endpoints (`403 this token can only be used by its GitHub import` elsewhere). Job tokens (`djt_`) and sandbox tokens (`dst_`) become a principal for one repository. Otherwise the session cookie from a passkey sign-in is checked. Otherwise the request is anonymous. | |
| 60 | +3. **Identity.** `GET` and `HEAD` requests for profile pictures (`/api/avatars/...`) skip this step and run as anonymous, because they are public and appear many times on every page. Requests to the Actions cache and artifact services (`/twirp/...` and `/_runtime/...`) skip it too: they carry their own signed token or URL (see [Actions cache and artifact services](#actions-cache-and-artifact-services)). For everything else, a `dfy_` token in the `Authorization` header (Bearer, `token` or Basic) becomes an account principal with the token's scopes. The push token of a GitHub import is a `dfy_` token too, but it is accepted only on Git smart HTTP, Git LFS and that import's own agent endpoints (`403 this token can only be used by its GitHub import` elsewhere). Job tokens (`djt_`), sandbox tokens (`dst_`) and self-hosted runner tokens (`drn_`, accepted only for claiming jobs) become a principal for one repository. Otherwise the session cookie from a passkey sign-in is checked. Otherwise the request is anonymous. | |
| 61 | 61 | 4. After the route runs, the response gets security headers and the `x-d1-bookmark` header. |
| 62 | 62 | |
| 63 | 63 | Routes are mounted in this order, so earlier ones win: profile pictures (`/api/avatars/...`), OIDC discovery (`/.well-known/*`), git smart HTTP and LFS (`/<owner>/<repo>.git/...`), the runner API (`/runner/jobs/...`), the email REST API, the container registry (`/v2/`), the packages REST API, the Actions cache and artifact services together with the REST API's artifact and cache routes, the REST API (`/api/v3`), release asset uploads (`/api/uploads/...`), release asset downloads (`/<owner>/<repo>/releases/download/...`) and source archives (`/<owner>/<repo>/archive/...`), the MCP server (`/mcp`), the live-update WebSocket and artifact downloads, a catch-all for `/api/*`, and finally the server-rendered web UI, which also serves the public docs at `/docs`. The `/api/*` catch-all answers any API path that no route matched with GitHub's JSON 404, `{"message": "Not Found", "documentation_url": "https://docs.github.com/rest"}`, so API clients never get the web UI's HTML error page. |
modifiedapps/diffinery/src/docs/api-meta.ts+31−2View file
| @@ -1063,11 +1063,40 @@ | ||
| 1063 | 1063 | status: 201, |
| 1064 | 1064 | body: { ref: ["string", "Branch or tag to run on.", true], inputs: ["object", "Values for `on.workflow_dispatch.inputs`."] }, |
| 1065 | 1065 | }, |
| 1066 | + "POST /repos/{owner}/{repo}/actions/runners/registration-token": { | |
| 1067 | + summary: "Create a runner registration token", | |
| 1068 | + description: "As on GitHub: a `drg_` token that registers any number of self-hosted runners with this repository for an hour. Returns `{ token, expires_at }`. People only: agents, job tokens and sandbox tokens get 403 `only repository admins can add self-hosted runners`. Audited as `runner.registration_token`. See /docs/actions/runners#manage-runners.", | |
| 1069 | + access: "admin", | |
| 1070 | + status: 201, | |
| 1071 | + }, | |
| 1072 | + "POST /repos/{owner}/{repo}/actions/runners/register": { | |
| 1073 | + summary: "Register a self-hosted runner", | |
| 1074 | + description: "Trades a registration token for the runner's own `drn_` token, shown once, which can claim this repository's jobs and nothing else. No `Authorization` header: the registration token is the credential. A wrong or expired token, or a repository that doesn't exist, gets 401 `invalid or expired registration token`. At most 100 runners per repository and 20 labels per runner (429 past the runner limit). Returns `{ id, name, labels, ephemeral, token }`. Audited as `runner.register`, with the admin who created the registration token as the actor.", | |
| 1075 | + access: "public", | |
| 1076 | + status: 201, | |
| 1077 | + body: { | |
| 1078 | + registration_token: ["string", "A `drg_` token from `POST .../actions/runners/registration-token`.", true], | |
| 1079 | + name: ["string", "Runner name (default `runner`)."], | |
| 1080 | + labels: ["string[]", "Labels the runner offers, matched against `runs-on`."], | |
| 1081 | + ephemeral: ["boolean", "`true` for a single job: the runner's token stops working once it takes a job, and the runner is removed when the job finishes."], | |
| 1082 | + }, | |
| 1083 | + }, | |
| 1084 | + "GET /repos/{owner}/{repo}/actions/runners": { | |
| 1085 | + summary: "List self-hosted runners", | |
| 1086 | + description: "GitHub's shape: `{ total_count, runners }`, each with `id`, `name`, `os`, `status` (`online` when it polled in the last 90 seconds, otherwise `offline`), `busy`, `ephemeral` and `labels`.", | |
| 1087 | + access: "admin", | |
| 1088 | + }, | |
| 1089 | + "DELETE /repos/{owner}/{repo}/actions/runners/{id}": { | |
| 1090 | + summary: "Remove a self-hosted runner", | |
| 1091 | + description: "Its runner token stops working at once. A job it is running carries on with its own job token. Audited as `runner.remove`.", | |
| 1092 | + access: "admin", | |
| 1093 | + status: 204, | |
| 1094 | + }, | |
| 1066 | 1095 | "POST /repos/{owner}/{repo}/actions/runners/claim": { |
| 1067 | 1096 | summary: "Claim a job for a self-hosted runner", |
| 1068 | - description: "Used by `diffinery runner`. Returns 204 when no queued job matches the labels.", | |
| 1097 | + description: "Used by `diffinery runner`. Send the runner's `drn_` token and an empty body: the runner offers the name and labels it registered with. A repository admin's personal token with the `admin` scope also works, with `name` and `labels` in the body (how runners worked before registration). Returns 204 when no queued job matches the labels, otherwise the job with a fresh job token.", | |
| 1069 | 1098 | access: "admin", |
| 1070 | - body: { name: ["string", "Runner name."], labels: ["string[]", "Runner labels, matched against `runs-on`."] }, | |
| 1099 | + body: { name: ["string", "Runner name (with a personal token only)."], labels: ["string[]", "Runner labels, matched against `runs-on` (with a personal token only)."] }, | |
| 1071 | 1100 | }, |
| 1072 | 1101 | "GET /repos/{owner}/{repo}/actions/runs": { |
| 1073 | 1102 | summary: "List workflow runs", |