Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

374 lines · 49.5 KBRaw History
1---
2title: Configuration
3description: Every setting of a self-hosted Diffinery: Worker variables and secrets, bindings, the dev environment, the deploy workflows' secrets, and rotating MASTER_KEY with MASTER_KEY_PREV.
4---
5
6A Diffinery instance is configured in four places: Worker variables in `apps/diffinery/wrangler.jsonc`, Worker secrets set with `wrangler secret put`, the Cloudflare bindings in the same `wrangler.jsonc`, and the secrets and variables of the repository whose workflows deploy it. This page lists every setting, its default and what it changes. To deploy for the first time, follow [Deploy your own Diffinery](/docs/self-hosting/deploy).
7
8## Where configuration lives
9
10| File or place | What it holds | Edit it? |
11| --- | --- | --- |
12| `apps/diffinery/wrangler.jsonc` | Worker name, route, variables, bindings, container settings, cron, and the `dev` environment. | Yes. This is the source of truth. |
13| `apps/diffinery/wrangler.deploy.jsonc` | `wrangler.jsonc` without the `dev` environment, plus the real D1 database ID, `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD`, `TURNSTILE_SITE_KEY`, and the route, `PUBLIC_URL` and `MAIL_FROM` for your hostname. Written by `scripts/render-config.mjs` on every deploy and ignored by git. | No. It is regenerated on each deploy. |
14| Worker secrets | `MASTER_KEY` and `OIDC_PRIVATE_KEY`, created by `scripts/ensure-secrets.sh`; `TURNSTILE_SECRET`, set by the deploy from its Turnstile widget; and optionally `GITHUB_OAUTH_CLIENT_SECRET`, which you set yourself. | Only to restore them, or to add the optional one. Never change `MASTER_KEY`. |
15| Repository secrets and variables | Cloudflare credentials and options for the Deploy workflow, and a token for the live test. | Yes, in your forge's repository settings. |
16| `apps/diffinery/.dev.vars` | Local secrets for `wrangler dev`, such as `MASTER_KEY`. Ignored by git. | Yes, for local development only. |
17
18## Worker variables
19
20Set under `vars` in `wrangler.jsonc`. All values are strings.
21
22| Variable | Production value in the repository | Required | What it does |
23| --- | --- | --- | --- |
24| `PUBLIC_URL` | `https://diffinery.com` | Yes | The canonical origin of the instance. See [PUBLIC_URL](#public_url). On deploy, `scripts/bootstrap.sh` replaces it with `https://$DIFFINERY_DOMAIN` (default `https://diffinery.com`), whatever `wrangler.jsonc` says. |
25| `MAIL_FROM` | `notifications@diffinery.com` | No | Sender address of every email. The display name is "Diffinery", or `<actor> (Diffinery)` for emails about someone's action. On deploy, `scripts/bootstrap.sh` replaces it with `notifications@$MAIL_DOMAIN` (default `notifications@$DIFFINERY_DOMAIN`). When empty, Diffinery sends from `notifications@<hostname of PUBLIC_URL>`. The domain must be onboarded to Email Sending. |
26| `BOOTSTRAP_ADMINS` | A comma-separated list of emails | For the first sign-in | Emails (case-insensitive) that become site administrators. See [BOOTSTRAP_ADMINS](#bootstrap_admins). |
27| `ACCESS_TEAM_DOMAIN` | Empty (filled in on deploy) | For email sign-in | Your Cloudflare Zero Trust team domain, like `example.cloudflareaccess.com`. Used to fetch the Access signing keys and to check the token issuer. |
28| `ACCESS_AUD` | Empty (filled in on deploy) | For email sign-in | The audience tag of the Access application that guards `/auth/access`. |
29| `ARTIFACT_QUOTA_BYTES` | Not set | No | Workflow artifact storage per repository, in bytes: artifacts that haven't expired plus uploads in progress. Default `10737418240` (10 GB). Anything that isn't a positive whole number counts as not set. Add it under `vars` in `wrangler.jsonc`; the deploy keeps it. When a repository reaches it, uploads fail with `413 the repository's artifact storage quota (N bytes) is used up; delete artifacts or wait for them to expire`. See [Caches and artifacts](/docs/reference/limits#caches-and-artifacts). |
30| `DEV_AUTH` | Not set in production; `dev@diffinery.local` in the `dev` environment | Never in production | Local development only. See [DEV_AUTH](#dev_auth). |
31| `SIGNUP` | Not set | No | `open` lets anyone create an account at `/signup`. See [SIGNUP](#signup). Any other value, or none, keeps the instance invite-only. |
32| `TURNSTILE_SITE_KEY` | Set by the deploy | With `SIGNUP=open`; for anonymous security reports and [early access requests](/docs/accounts/early-access) | The site key of the Cloudflare Turnstile widget on the sign-up page, the [security report form](#security-contact) and the early access form. The deploy creates the widget and sets this; see [Turnstile](#turnstile). Setting it doesn't open sign-up. |
33| `IMPORT_MAX_REPO_KB` | Not set | No | The largest GitHub repository a one-repository import accepts, in KB as GitHub reports a repository's `size`. Default `10485760` (10 GB). An empty value, `0`, a negative number or text that isn't a number counts as not set. A larger source is refused before the import starts with `422 the repository is about N GB, over the M GB import limit; ask an administrator to raise IMPORT_MAX_REPO_KB or split the repository`. See [GitHub imports](#github-imports). |
34| `QUOTA_HOSTED_MINUTES`, `QUOTA_HOSTED_JOBS`, `QUOTA_SANDBOXES`, `QUOTA_REPOSITORIES`, `QUOTA_STORAGE_BYTES` | Not set | No | The default per-account quotas: hosted minutes per month (default `3000`), hosted Actions jobs at once (`20`), agent sandboxes open at once (`5`), repositories (`500`) and storage in bytes (`10737418240`, 10 GB). `-1` is unlimited; text that isn't a whole number counts as not set. Site administrators change them for one account with `PATCH /api/v3/admin/accounts/{login}/quotas`, and accounts run by a site administrator have none. See [Limits](/docs/reference/limits#account-quotas). |
35| `HOSTED_JOBS_LIMIT` | Not set | No | Hosted Actions jobs the whole instance runs at once (default `80`); one account runs at most half of them, accounts run by a site administrator included. Keep it under the runner container classes' `max_instances`. Past it, hosted jobs wait as queued until one finishes. See [Limits](/docs/reference/limits#actions). |
36| `IMPORT_GHE_HOST` | Not set | No | The host name of a GitHub Enterprise Server (like `ghe.example.com`) that imports may download release assets from. Without it, imports download release assets only from github.com and its asset hosts, and assets of an Enterprise import are skipped with a warning. |
37| `GITHUB_OAUTH_CLIENT_ID` | Not set | No | The client ID of a GitHub OAuth app, for linking GitHub accounts with a GitHub sign-in instead of a gist. Needs `GITHUB_OAUTH_CLIENT_SECRET` too. See [GitHub imports](#github-imports). |
38| `IMPORT_GIT_AGENT` | Not set in production; `external` in the `dev` environment | Never in production | With `external`, an import's Git transfer doesn't start a container: it waits for an import agent started by hand (see [The dev environment](#the-dev-environment)). |
39| `SCHEDULE_TICK_MINUTES` | Not set | No | Minutes between the workflow scheduler's checks, a whole number from 1 to 60. Default `5`, GitHub's shortest schedule interval. Anything else counts as not set. The end-to-end tests use `1`. |
40| `WAIT_TIMER_MINUTE_MS` | Not set | Never in production | Local development and the end-to-end tests only: how long one minute of a deployment environment's wait timer lasts, in milliseconds, from 1 to `60000` (the default). Anything else counts as not set. It counts only when `PUBLIC_URL` is on this machine (`localhost`, `127.x` or `[::1]`) and `ACCESS_AUD` is empty, as for `DEV_AUTH`; a deployed Worker ignores it. The end-to-end tests use `10000` so a wait timer passes in seconds. |
41| `AGENT_POLL_MS` | Not set | Never in production | The end-to-end tests only: how often a running agent task is checked, in milliseconds, from `200` to `3000` (the default). Like `WAIT_TIMER_MINUTE_MS`, it counts only on a Worker on this machine; a deployed Worker ignores it. |
42| `TERMINAL_POLL_MS` | Not set | Never in production | The end-to-end tests only: how often open web terminals are checked for their limits and their agent's access, in milliseconds, from `2000` to `60000` (the default). It counts only on a Worker on this machine; a deployed Worker ignores it. |
43| `EGRESS` | Not set | No | `web` limits hosted Actions jobs and agent sandboxes (including tasks, reviews and terminals) to HTTP and HTTPS (ports 80 and 443) and DNS. Any other value, or none, leaves them open to the whole internet. Accounts run by a site administrator stay open. See [EGRESS](#egress). |
44| `RUNNER_IMAGE` | Set by the deploy | No | The runner image's name and tag (for example `diffinery-runner:96dbbe31790f`), as jobs report it in `runner.image`. `scripts/render-config.mjs` sets it when the deploy uses a prebuilt runner image, and `GET /api/v3/meta` returns it as `runner_image` (`null` when not set). Don't set it by hand. |
45| `SECURITY_CONTACT` | Not set | No | More places people can report a vulnerability in your instance, listed in `/.well-known/security.txt` after the instance's own report form (`/security/report`): an email address or an `https://` URL, several separated by commas. See [Security contact](#security-contact). |
46| `TERMINALS` | Not set | No | `off` switches [terminals](/docs/agents/terminal) off: nobody can open one, and open ones end at their next check, within a minute. Anything else, or not set, leaves them on. |
47
48When either `ACCESS_TEAM_DOMAIN` or `ACCESS_AUD` is empty, email sign-in at `/auth/access` is off: the page answers "Account recovery by email is not set up on this instance. Ask an administrator for a sign-in link.", and the sign-in page doesn't offer **Recover your account by email**. Passkeys, invitations and admin sign-in links keep working, but the first administrator can't sign in, because email sign-in is how an account in `BOOTSTRAP_ADMINS` is created.
49
50### PUBLIC_URL
51
52`PUBLIC_URL` must be the exact origin people and tools use, with `https://` and no path. You don't set it by hand for a deploy: `scripts/render-config.mjs` sets it from `--public-url`, which bootstrap fills in as `https://<DIFFINERY_DOMAIN>`, and refuses a URL that isn't `https://` or has a path (`--public-url must be an https origin, like https://git.example.com`). The same flag points the Worker's route at that hostname, so the route and `PUBLIC_URL` always agree. Diffinery uses it for:
53
54- **Passkeys.** The WebAuthn relying party ID is the hostname of `PUBLIC_URL`, and the expected origin is its origin. Passkeys only work on that hostname.
55- **Every generated link:** emails, invitation and sign-in links, `html_url` and `clone_url` in API responses, webhook payloads, check run details links, the sitemap and canonical tags.
56- **Workflows:** `github.server_url` is `PUBLIC_URL`, `github.api_url` is `PUBLIC_URL/api/v3`, and the runner agent and sandboxes call back to it.
57- **OIDC:** the issuer (`iss`) of workflow ID tokens, and their default audience.
58- **The container registry host name**, the hostname of `PUBLIC_URL` (images are `<hostname>/<owner>/<name>`).
59- **The cross-site check:** a cookie-authenticated write with an `Origin` other than this origin is refused.
60
61> **Important:** Changing `PUBLIC_URL` on a running instance (by deploying with another `DIFFINERY_DOMAIN`) breaks every existing passkey (they are bound to the old hostname), changes the OIDC issuer that cloud providers trust, and changes every image name. Choose the hostname before your first sign-in.
62
63### BOOTSTRAP_ADMINS
64
65A comma-separated list of email addresses; spaces around entries are ignored and matching is case-insensitive. Logins in the list are ignored, because anyone could pick a free login, while an email address has to be proven (by email sign-in or by opening the link in an invitation email).
66
67An account becomes a site administrator from this list only when it is created (through email sign-in at `/auth/access`, by joining from an invitation or through open sign-up, or through `DEV_AUTH` locally), or when an account that is still waiting for an invite signs in through `/auth/access`. Accounts that already exist are not promoted or demoted when you change the list later, and there is no page for granting or removing site administration. See [Site administration](/docs/admin/site-administration).
68
69### SIGNUP
70
71With `SIGNUP=open`, the sign-in page links to **Create an account** (`/signup`). Someone enters their email address, gets a 6-digit code by email, enters it, picks a username and creates a passkey; their account is active at once (see [Open sign-up](/docs/accounts/passkeys-and-sign-in#open-sign-up)). Unset, or any other value, the instance is invite-only as before: `/signup` answers `404` and the sign-in page says so.
72
73Open sign-up needs Cloudflare Turnstile, so scripts can't use it to send email. The deploy sets Turnstile up (see [Turnstile](#turnstile)), so to open sign-up add `"SIGNUP": "open"` under `vars` in `wrangler.jsonc`; the deploy keeps it. The Turnstile keys alone never open sign-up: only `SIGNUP=open` does.
74
75Each code request is checked with Turnstile's `siteverify` API, with the visitor's IP address. Without both keys, `/signup` answers `503` with "Sign-up needs Turnstile configured", except when `PUBLIC_URL` is on the local machine (`localhost`, `127.x.x.x` or `[::1]`): local development and the end-to-end tests skip the check. Only the sign-up page's and the security report form's content security policy allows scripts and frames from `https://challenges.cloudflare.com`.
76
77The limits on codes (5 a day per address, 20 a day per IP address, 10 minutes and 5 tries each) are in [Limits](/docs/reference/limits#sign-in-and-sessions). Sign-up emails always send: they need the `EMAIL` binding and the `MAIL` queue like every other email.
78
79### EGRESS
80
81**Experimental:** `web` hasn't been tried on a live instance yet. The setting covers the whole instance, so try it on a test instance first, or check it with an account no site administrator runs, as in [Check it](#check-it) below. diffinery.com doesn't set it.
82
83With `EGRESS=web`, hosted job and sandbox containers start without direct internet access. Cloudflare then lets only ports 80 and 443, and DNS to Cloudflare's resolvers, leave the container, and Diffinery forwards HTTP and HTTPS requests to any host through the Worker. Unset, or any other value, hosted compute reaches the whole internet as before. The setting is read when a container starts: jobs and sandboxes already running keep the access they started with.
84
85Under `web`:
86
87- **Works:** `git clone https://...`, `curl`, `apt-get`, `npm`, `pnpm` and `pip` installs, `docker pull`, and calls to the Diffinery and GitHub APIs.
88- **Refused:** every other port. That includes SSH (`git@...` remotes, `ssh`, `scp`), SMTP (25, 465 and 587), IRC, databases and HTTP on other ports.
89- **HTTPS is intercepted.** Requests leave through the instance's own Worker, which sees them decrypted, as the operator already can for the container itself. Cloudflare re-signs them with a CA it puts in the container at `/etc/cloudflare/certs/cloudflare-containers-ca.crt`. The runner agent adds that CA to the system store and sets `NODE_EXTRA_CA_CERTS` and `REQUESTS_CA_BUNDLE`, so tools that use the system store, Node.js and Python's `requests` trust it. Tools with their own fixed CA list or certificate pinning, client certificates (mutual TLS), and HTTPS from inside Docker containers a job starts (`docker run`, `services:`, `RUN` steps of a build) fail their certificate checks.
90
91The account that owns the repository decides, as for [quotas](/docs/reference/limits#account-quotas). Accounts run by a site administrator (their own account, and organizations with a site administrator as an owner) stay open even with `EGRESS=web`, so the instance's own Deploy and live test workflows keep their access. Self-hosted runners and GitHub imports are not affected. Under `wrangler dev` sandboxes run in a local process, not containers, so the setting changes nothing there.
92
93To turn it on, add `"EGRESS": "web"` under `vars` in `wrangler.jsonc`; the deploy keeps it. To turn it off, remove it (or set any other value) and deploy again; containers started after that are open.
94
95#### How a blocked connection looks
96
97Diffinery doesn't add a line to the job or terminal output when it limits a connection. The tool that made it reports an ordinary network error:
98
99- **A port other than 80 or 443** fails to connect, as behind a firewall: for example `ssh: connect to host github.com port 22: Connection refused` (or `Connection timed out`), or a mail or database client that can't reach its server.
100- **HTTPS from a tool that doesn't trust the Cloudflare CA** fails its certificate check: for example `SSL certificate problem: unable to get local issuer certificate` (curl), `x509: certificate signed by unknown authority` (Go), `PKIX path building failed` (Java), or `self-signed certificate in certificate chain` (Node.js with its own CA list). Inside a Docker container a job starts, add the CA from `/etc/cloudflare/certs/cloudflare-containers-ca.crt` to the image, or run that step on the host.
101
102If the CA never appears in the container, the runner agent writes `web-only egress, but /etc/cloudflare/certs/cloudflare-containers-ca.crt did not appear: HTTPS will fail` to the container's own log (not the job log), and every HTTPS request from the job fails its certificate check.
103
104#### Check it
105
106With `EGRESS=web` deployed, run a workflow in a repository owned by an account that no site administrator runs (a site administrator's own repositories stay open):
107
108```yaml
109on: workflow_dispatch
110jobs:
111 egress:
112 runs-on: ubuntu-latest
113 steps:
114 - run: curl -fsSI https://example.com # works
115 - run: git ls-remote https://github.com/git/git HEAD # works
116 - run: npm view left-pad version # works
117 - shell: bash
118 run: "! timeout 10 bash -c '</dev/tcp/github.com/22'" # SSH port: must not connect
119```
120
121All four steps pass. The last one fails if port 22 is reachable, which means the account is open (a site administrator runs it) or the setting isn't deployed.
122
123### Security contact
124
125`/.well-known/security.txt` ([RFC 9116](https://www.rfc-editor.org/rfc/rfc9116)) tells security researchers where to report a problem. Diffinery always serves it, with:
126
127| Field | Value |
128| --- | --- |
129| `Contact` | First `<PUBLIC_URL>/security/report`, the instance's private report form, then one line per entry of `SECURITY_CONTACT`. An email address becomes a `mailto:` URI; `https://`, `mailto:` and `tel:` URIs are kept as they are; anything else is left out. |
130| `Expires` | Midnight UTC 360 days after the request, so the file never goes stale. |
131| `Policy` | `<PUBLIC_URL>/docs/security/disclosure`, the [disclosure policy](/docs/security/disclosure). |
132| `Preferred-Languages` | `en` |
133| `Canonical` | `<PUBLIC_URL>/.well-known/security.txt` |
134
135The report form needs no setup: reports go to the instance's site administrators, who each get an email with the report's summary and read it under **Settings → Security reports** (see [Site administration](/docs/admin/site-administration#security-reports)). People who are signed in can always send one. People who aren't pass Cloudflare Turnstile first, with the same `TURNSTILE_SITE_KEY` and `TURNSTILE_SECRET` as [open sign-up](#signup), which the deploy sets up (see [Turnstile](#turnstile); you don't need `SIGNUP=open` for this). Without both keys, for example when the deploy's API token lacks **Account → Turnstile → Edit**, the form asks visitors to sign in, except on the local machine, where the check is skipped as for sign-up.
136
137`SECURITY_CONTACT` is optional. Use an address someone reads. The repository leaves it unset on purpose, so an instance never advertises an address its operator didn't choose. To add one, add `"SECURITY_CONTACT": "security@example.com"` under `vars` in `wrangler.jsonc`; the deploy keeps it.
138
139### Turnstile
140
141The security report form, and open sign-up when it's on, check with [Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) that a visitor without an account is a person. Every deploy sets it up (`scripts/turnstile.sh`, run by `scripts/bootstrap.sh`):
142
1431. It looks for a Turnstile widget named `diffinery <DIFFINERY_DOMAIN>` in the account, and creates one when there is none: managed mode, for your hostname only.
1442. The widget's site key becomes the `TURNSTILE_SITE_KEY` variable of the deployed Worker.
1453. Its secret key becomes the `TURNSTILE_SECRET` secret: `scripts/ensure-secrets.sh` sets it when it's missing, and bootstrap replaces it before the deploy when it goes with another widget (the deployed `TURNSTILE_SITE_KEY` is not this widget's, for example because the widget was deleted and created again). The key goes from the Cloudflare API straight to `wrangler secret put` and is never printed.
146
147The deploy's API token needs **Account → Turnstile → Edit** for this (the [API reference](https://developers.cloudflare.com/api/resources/turnstile/subresources/widgets/methods/create/) calls it "Turnstile Sites Write"). Without it the deploy warns, naming the permission, and goes on; people who aren't signed in then can't send security reports until you add it and deploy again.
148
149To use a widget of your own instead, add `"TURNSTILE_SITE_KEY": "<site key>"` under `vars` in `wrangler.jsonc` and store its secret key with `printf '%s' "$TURNSTILE_SECRET" | pnpm exec wrangler secret put TURNSTILE_SECRET -c wrangler.deploy.jsonc`. The deploy then creates no widget and never replaces your secret; it only fills in a missing `TURNSTILE_SECRET` when your widget is in the same Cloudflare account. If you rotate the secret key of the deploy's widget in the dashboard, run `pnpm exec wrangler secret delete TURNSTILE_SECRET -c wrangler.deploy.jsonc` and deploy again, so the deploy stores the new one.
150
151### DEV_AUTH
152
153When `DEV_AUTH` is set, `ACCESS_AUD` is empty and both `PUBLIC_URL` and the request's own address are on the local machine (`localhost`, `127.x.x.x` or `[::1]`), every request without credentials (no token, no session) acts as the account with the email in `DEV_AUTH`, with full access. That account is created on first use and is a site administrator if the email is also in `BOOTSTRAP_ADMINS`. While `DEV_AUTH` is set, the email log also keeps each whole message, and `GET /api/v3/diffinery/emails` returns it, so tests can read emails without delivering them.
154
155> **Important:** Never set `DEV_AUTH` on a deployed instance. As a safeguard it does nothing there: a request that reaches the Worker under any other host name, such as `workers.dev` or your domain, or a `PUBLIC_URL` that isn't on the local machine, turns it off, and so does Cloudflare Access.
156
157## Worker secrets
158
159Set with `wrangler secret put` (the deploy does this for you with `scripts/ensure-secrets.sh`), never in `wrangler.jsonc`.
160
161| Secret | Format | Created by | What it does | If it is missing |
162| --- | --- | --- | --- | --- |
163| `MASTER_KEY` | 32 random bytes, base64 | `ensure-secrets.sh`, once | Root key for envelope encryption of Actions secrets: each repository, organization and deployment environment gets its own AES-GCM key derived from it with HKDF. The GitHub token of a running import and each webhook's signing secret are encrypted the same way. Model provider keys are encrypted the same way. It also signs the unsubscribe links in emails, the links in invitation and email verification emails, the codes and OAuth state for linking GitHub accounts, keys the stored hashes of sign-up codes, and a key derived from it (HKDF) signs each job's `ACTIONS_RUNTIME_TOKEN` and the upload and download URLs of the Actions cache and artifact services. | Repository, organization and environment secrets can't be stored or read (`could not store the secret: ... (is MASTER_KEY configured?)`). Notification emails that carry unsubscribe links can't be built, so they end up in the email log as failed; security and account emails still send. Jobs get no `ACTIONS_RUNTIME_TOKEN`: `actions/cache` and the built-in cache step skip caching (`The cache service is not available to this job; skipping.`), and uploading or downloading artifacts fails (`The artifact service is not available to this job.`). Imports that come with a GitHub token can't be started. Webhooks can't be created or delivered. Open sign-up can't send codes. |
164| `MASTER_KEY_PREV` | The `MASTER_KEY` you replaced | You, only while [rotating `MASTER_KEY`](#rotating-master_key) | Everything sealed or signed with the old key keeps working: secrets encrypted with it still decrypt, and links and tokens signed with it still verify. Nothing new uses it. | The normal state. Right after a rotation without it, secrets stored under the old key can't be read and links signed with it stop working, as if the old key had been lost. |
165| `OIDC_PRIVATE_KEY` | An ECDSA P-256 private key as a JWK (JSON), with a `kid` | `ensure-secrets.sh`, once | Signs the ID tokens that workflow jobs request with `permissions: id-token: write` (ES256). Its public half is served at `/.well-known/jwks`. | Jobs that request an ID token fail with `OIDC is not configured on this Diffinery instance`, `/.well-known/jwks` returns no keys, and `GET /api/v3/meta` returns `"oidc": false`. |
166| `GITHUB_OAUTH_CLIENT_SECRET` | The client secret of a GitHub OAuth app | You, optionally | With `GITHUB_OAUTH_CLIENT_ID`, turns on the **Link with GitHub** option on **Settings → Linked accounts**. | People link GitHub accounts with the gist code only. |
167| `MAIL_EVENTS_SECRET` | A random string, at least 32 characters | You, optionally | Turns on `POST /-/email/events`, the signed intake for bounces and complaints from anything other than Cloudflare's own event subscriptions. See [Bounces, complaints and the suppression list](/docs/admin/site-administration#bounces-complaints-and-the-suppression-list). | The endpoint answers 404. Bounces and complaints still arrive through the `diffinery-mail-events` queue. |
168| `TURNSTILE_SECRET` | The secret key of a Cloudflare Turnstile widget | The deploy, from its widget (see [Turnstile](#turnstile)) | With `TURNSTILE_SITE_KEY`, checks that whoever asks for a sign-up code (with `SIGNUP=open`), or sends a security report without signing in, passed the Turnstile widget. See [SIGNUP](#signup) and [Security contact](#security-contact). | With `SIGNUP=open`, sign-up answers `503` "Sign-up needs Turnstile configured", and the security report form takes reports only from people who are signed in (local development skips the check). The next deploy sets it when the token has **Account → Turnstile → Edit**. |
169
170None of these secrets is rotated automatically, and `ensure-secrets.sh` never replaces one that exists. Keep a copy of `MASTER_KEY` somewhere safe: replacing it without keeping the old one as `MASTER_KEY_PREV` makes every stored secret undecryptable and invalidates unsubscribe links already sent, along with the runtime tokens of running jobs and cache and artifact links already handed out. To rotate it, follow [Rotating MASTER_KEY](#rotating-master_key). To rotate the OIDC key, delete `OIDC_PRIVATE_KEY` and deploy: a new key with a new `kid` (`diffinery-<date>`) is generated, and relying parties pick it up from `/.well-known/jwks`.
171
172To set a secret by hand (for example to restore a backup):
173
174```sh
175cd apps/diffinery
176printf '%s' "$MASTER_KEY" | pnpm exec wrangler secret put MASTER_KEY -c wrangler.deploy.jsonc
177```
178
179### Rotating MASTER_KEY
180
181Rotate `MASTER_KEY` when it may have leaked, or on your own schedule. Diffinery accepts two keys while a rotation is under way: `MASTER_KEY`, which encrypts and signs everything new, and `MASTER_KEY_PREV`, the key it replaced, which is only used to decrypt and verify what was made before.
182
1831. Make a new key and keep it with your copy of the current one:
184
185 ```sh
186 NEW_KEY="$(openssl rand -base64 32)"
187 ```
188
1892. Set the current key as `MASTER_KEY_PREV` and the new one as `MASTER_KEY` in one step, so no version of the Worker runs with the new key alone. `OLD_KEY` is the current value from your safe copy (Cloudflare doesn't show a secret's value):
190
191 ```sh
192 cd apps/diffinery
193 jq -n --arg old "$OLD_KEY" --arg new "$NEW_KEY" '{MASTER_KEY_PREV: $old, MASTER_KEY: $new}' | pnpm exec wrangler secret bulk -c wrangler.deploy.jsonc
194 ```
195
196 To use `wrangler secret put` instead, put `MASTER_KEY_PREV` first and `MASTER_KEY` second: each command deploys a new version of the Worker, and a version with the new `MASTER_KEY` but no `MASTER_KEY_PREV` can't read the secrets stored so far.
197
1983. Save the new key where you kept the old one. Nothing else needs to change: no deploy, migration or restart.
199
200From then on:
201
202- New and replaced secrets (repository, organization and environment secrets, model provider keys, webhook secrets, import tokens) are encrypted with the new key. Each stored value names the key it was encrypted with (`v2.<key id>.…`, where the key id is the first 8 hex characters of the key's SHA-256: `printf %s "$NEW_KEY" | base64 -d | sha256sum | cut -c1-8`). Values stored before key ids existed (`v1.…`) are tried with both keys.
203- Stored values move to the new key by themselves as they are used: repository, organization and environment secrets the next time a workflow job uses them, model provider keys the next time an agent task does, webhook secrets the next time the webhook delivers, and the GitHub token of an import when the import is retried. A value nothing uses stays encrypted with the old key until someone saves it again.
204- Unsubscribe links, invitation and verification emails, sign-up codes, GitHub linking codes and OAuth state, runtime tokens, cache and artifact URLs, and image proxy URLs in pages already rendered signed with the old key keep working; new ones are signed with the new key.
205
206Remove `MASTER_KEY_PREV` (`pnpm exec wrangler secret delete MASTER_KEY_PREV -c wrangler.deploy.jsonc`) only when nothing you still need depends on the old key:
207
208- Every repository, organization and environment secret and every model provider key you still need has been used or saved again since the rotation. Secrets your workflows use every day move over by themselves; one that only a rare workflow uses (a release, a yearly job) may not have, and its value can't be read back, so whoever manages it re-enters it. One left behind can't be decrypted once `MASTER_KEY_PREV` is gone, and runs that use it fail.
209- Links already sent have expired, or you accept that they stop working: runtime tokens and cache and artifact URLs last about as long as their job, sign-up codes 10 minutes, image proxy URLs until pages and browsers cache them no more (at most 7 days), verification links 24 hours, invitation emails until the invitation expires, and unsubscribe links up to a year. Someone with an old unsubscribe link can still change their email settings after signing in.
210
211If the old key leaked, treat the secrets as exposed: replace their values at the source (new API tokens, new passwords) and save the new values, which are sealed with the new key. Then remove `MASTER_KEY_PREV` right away: until then, anyone holding the old key can still forge what it signs.
212
213## Bindings
214
215Defined in `wrangler.jsonc`. The deploy creates the underlying resources if they don't exist.
216
217| Binding | Kind | Resource (production) | What Diffinery uses it for |
218| --- | --- | --- | --- |
219| `DB` | D1 database | `diffinery` (migrations in `migrations/`) | Accounts (including each account's appearance setting and profile picture version), organizations, tokens, repository list, topics and access, invitations, sessions and passkeys, notifications, audit log, schedules, organization secrets and variables, the registry index, email settings and log, GitHub imports and linked GitHub accounts. |
220| `BUCKET` | R2 bucket | `diffinery-data` | Large git objects, LFS objects, archived job logs, caches, artifacts, container image layers and manifests, the event archive, release assets, cached source archives (zipball and tarball downloads) and profile pictures. |
221| `EVENTS` | Queue producer, with a consumer on the same Worker | `diffinery-events`, dead-letter queue `diffinery-events-dlq` | Repository events for webhooks, notifications and the audit log. Consumer: batches of up to 50, waiting up to 2 seconds, 8 retries. |
222| `MAIL` | Queue producer, with a consumer on the same Worker | `diffinery-mail`, dead-letter queue `diffinery-mail-dlq` | Email jobs. Consumer: batches of up to 20, waiting up to 2 seconds, 8 retries. Optional: without it emails are dropped with a warning in the logs. |
223| `EMAIL` | `send_email` (Email Sending) | Your onboarded domain | Sending email. Optional: without it every email is logged as `skipped`. |
224| (none) | Queue consumer | `diffinery-mail-events` | Bounces and complaints from Email Sending's event subscriptions; the deploy subscribes this queue to your sending domain's events (see [Site administration](/docs/admin/site-administration#where-bounces-and-complaints-come-from)). Consumer: batches of up to 50, waiting up to 5 seconds, 8 retries. |
225| `RL_AUTH`, `RL_ANON`, `RL_ACCOUNT`, `RL_CREATE`, `RL_MODEL` | Rate limiting (`ratelimits`) | Namespaces `1001` to `1005`; 20, 300, 2,000, 60 and 600 requests per 60 seconds | [Request rate limits](/docs/reference/rest-api#rate-limits): sign-in and security reports per IP address, anonymous requests per IP address, signed-in requests per account, new content per account, and agents' model requests per paying person (`MODEL_RATE_LIMIT` in `src/lib/rate-limit.ts`). Optional: a missing binding (the `dev` environment declares none) or one that fails lets requests through. If you change a limit, change `RATE_LIMITS` (or `MODEL_RATE_LIMIT`) in `src/lib/rate-limit.ts` to match, since the 429 messages quote it. Namespace IDs are shared across your Cloudflare account, so pick others if another Worker already uses these. |
226| `REPO` | Durable Object namespace | Class `RepoDO` (SQLite) | One object per repository. |
227| `SCHEDULER` | Durable Object namespace | Class `SchedulerDO` (SQLite) | One object that starts scheduled workflow runs on its own alarm. |
228| `COMPOSE` | Durable Object namespace | Class `ComposeDO` (SQLite) | Joins the blocks of large artifact and cache uploads, one short-lived object per step, so the copying doesn't slow the repository's object. Without it, `RepoDO` does the joining. |
229| `RUNNER` | Durable Object namespace with a container | Class `RunnerContainer` (SQLite) | One container per hosted workflow job. |
230| `SANDBOX` | Durable Object namespace with a container | Class `SandboxContainer` (SQLite) | One container per agent sandbox. |
231| `RUN_WORKFLOW` | Workflow | `diffinery-run`, class `RunWorkflow` | One durable instance per workflow run. |
232| `IMPORTER` | Durable Object namespace with a container | Class `ImportContainer` (SQLite) | One container per Git transfer of a GitHub import. |
233| `IMPORT_WORKFLOW` | Workflow | `diffinery-import`, class `ImportWorkflow` | One durable instance per attempt of a GitHub import. |
234
235The Durable Object classes are declared in six migrations (`new_sqlite_classes`): tag `v1` for `RepoDO`, `RunnerContainer` and `SandboxContainer`, tag `v2` for `ImportContainer`, tag `v3` for `ProjectDO`, tag `v4` for `SchedulerDO`, tag `v5` for `RunnerContainerMedium`, `RunnerContainerSmall` and `RunnerContainerTiny`, and tag `v6` for `ComposeDO`. `wrangler deploy` applies a migration tag the account doesn't have yet.
236
237## Other Worker settings
238
239| Setting | Value | Notes |
240| --- | --- | --- |
241| `name` | `diffinery` | The Worker's name in your account. |
242| `main` | `src/index.ts` | |
243| `compatibility_date` | `2026-10-01` | |
244| `compatibility_flags` | `nodejs_compat` | |
245| `workers_dev` | `false` | The instance is only served on your route. |
246| `routes` | `[{ "pattern": "diffinery.com/*", "zone_name": "diffinery.com" }]` | A zone route, not a Custom Domain. The hostname's DNS record must be proxied. On deploy, `scripts/render-config.mjs` replaces it with one route, `<DIFFINERY_DOMAIN>/*` in the zone `DIFFINERY_ZONE` (by default the hostname's last two labels), so you don't edit it by hand. |
247| `observability` | Enabled, `head_sampling_rate: 1` | Every request is logged to Workers Logs. |
248| `upload_source_maps` | `true` | Readable stack traces in the logs. |
249| `limits.cpu_ms` | `300000` | Up to 5 minutes of CPU time per request (large pushes, diffs and merges). |
250| `containers` | `RunnerContainer`: `standard-4`, `max_instances: 100`; `RunnerContainerMedium`, `RunnerContainerSmall`, `RunnerContainerTiny`: `standard-3`, `standard-1`, `basic`, `max_instances: 100` each (smaller hosted runners; remove them and their `RUNNER_*` bindings to run every job on `standard-4`); `SandboxContainer`: `standard-1`, `max_instances: 50`; `ImportContainer`: `standard-4`, `max_instances: 20`; `rollout_active_grace_period`: 23400 for runners and 25200 for imports (longer than a job or transfer can run, so a deploy never replaces a running one), 3600 for sandboxes; `rollout_step_percentage: 100`, so containers started after a deploy get the new image | All three build `containers/runner/Dockerfile` with `containers/runner` as context. Raise `max_instances` for more concurrent jobs, sandboxes or import transfers. |
251| `triggers.crons` | `["*/5 * * * *"]` | Token expiry reminders, email log pruning, registry cleanup, cancelling draft imports older than 7 days, and setting the workflow scheduler's alarm if it isn't set. Scheduled workflows run from that alarm, not from the cron trigger. |
252
253## The dev environment
254
255`wrangler.jsonc` has an `env.dev` section that `pnpm dev` (`wrangler dev -e dev`) and the end-to-end tests use. It is removed from the deployed config.
256
257| Setting | `dev` value |
258| --- | --- |
259| `routes`, `workers_dev` | No routes; `workers_dev: true` |
260| `PUBLIC_URL` | `http://localhost:8787` |
261| `BOOTSTRAP_ADMINS`, `DEV_AUTH` | `dev@diffinery.local` |
262| `ACCESS_TEAM_DOMAIN`, `ACCESS_AUD` | Empty |
263| `DB` | `diffinery-dev` |
264| `BUCKET` | `diffinery-data-dev` |
265| Queues | `diffinery-events-dev` and `diffinery-mail-dev`, batches waiting up to 1 second, no dead-letter queues |
266| `EMAIL` | Captured locally by Miniflare, not delivered |
267| `RUN_WORKFLOW` | `diffinery-run-dev` |
268| `IMPORT_WORKFLOW` | `diffinery-import-dev` |
269| `IMPORT_GIT_AGENT` | `external` |
270| `containers` | None: hosted runners, sandboxes and import containers are unavailable locally; use a self-hosted runner, and run an import's Git transfer by hand |
271| `triggers.crons` | None |
272
273To run an import locally, start it as usual, wait until its `git.phase` is `waiting`, claim the transfer, and run the runner agent's import mode against your instance (build it first with `pnpm -r build`):
274
275```sh
276TOKEN=$(curl -sS -X POST http://localhost:8787/api/v3/diffinery/imports/<id>/claim | jq -r .token)
277DIFFINERY_API_URL=http://localhost:8787 DIFFINERY_IMPORT_ID=<id> DIFFINERY_IMPORT_TOKEN=$TOKEN \
278 DIFFINERY_WORK_ROOT=/tmp/diffinery-import node containers/runner/dist/agent.mjs
279```
280
281Only the account that started the import can claim it. In local development (`DEV_AUTH` in effect, as described in [DEV_AUTH](#dev_auth)), imports also accept plain `http://` and loopback GitHub API URLs, for test doubles.
282
283`MASTER_KEY` is not set in `dev`. Add it to `apps/diffinery/.dev.vars` (`MASTER_KEY=<base64 of 32 random bytes>`) or pass `--var MASTER_KEY:<value>` to `wrangler dev` to store Actions secrets locally. The end-to-end tests pass a random one with `--var`, together with `PUBLIC_URL`.
284
285## GitHub imports
286
287[Imports from GitHub](/docs/migrate-from-github) need no setup beyond a normal deploy: the `IMPORTER` container, the `IMPORT_WORKFLOW` Workflow and the D1 tables of migration `0010_imports.sql` are created by the deploy. What you can tune:
288
289| Setting | Where | Effect |
290| --- | --- | --- |
291| `IMPORT_MAX_REPO_KB` | `vars` in `wrangler.jsonc` | The size ceiling for one-repository imports (default 10 GB). The Git transfer makes a full mirror clone (`git clone --mirror`) inside the import container (`standard-4`), so a higher ceiling needs room for that clone. |
292| `max_instances` of `ImportContainer` | `containers` in `wrangler.jsonc` | How many Git transfers run at once across the instance (default 20). |
293| `IMPORT_GHE_HOST` | `vars` in `wrangler.jsonc` | Lets imports download release assets from your GitHub Enterprise Server host. |
294| `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET` | `vars` (or a secret) and a Worker secret | Adds **Link with GitHub** to **Settings → Linked accounts**. |
295
296To offer linking through GitHub sign-in, create an OAuth app on github.com (**Settings → Developer settings → OAuth Apps**) with the authorization callback URL `https://git.example.com/settings/linked/github/callback` (your `PUBLIC_URL` plus `/settings/linked/github/callback`). Then add the client ID under `vars` in `wrangler.jsonc` and store the secret:
297
298```sh
299cd apps/diffinery
300printf '%s' "$CLIENT_SECRET" | pnpm exec wrangler secret put GITHUB_OAUTH_CLIENT_SECRET -c wrangler.deploy.jsonc
301```
302
303The OAuth flow asks GitHub for the `read:user` scope only, uses github.com (not an Enterprise host), and binds its state to the signed-in account. Without the two settings, people link accounts with the gist code, which needs no configuration.
304
305Imports reach GitHub only on HTTPS hosts with public names; a GitHub API URL on a private address, `localhost`, `.local` or `.internal` is refused with `422 github_api_url must be a public host`.
306
307## Deploy script settings
308
309Environment variables the scripts in `scripts/` read:
310
311| Variable | Used by | Default | Meaning |
312| --- | --- | --- | --- |
313| `CLOUDFLARE_API_TOKEN` | `bootstrap.sh`, `email-sending.sh`, `ensure-secrets.sh` (through wrangler), `wrangler deploy` | Required | The API token. |
314| `CLOUDFLARE_ACCOUNT_ID` | `bootstrap.sh`, wrangler | Required | The account to deploy to. |
315| `DIFFINERY_DOMAIN` | `bootstrap.sh`, `email-sending.sh` | `diffinery.com` | Your instance's hostname, without a scheme or path: the Worker's route becomes `<domain>/*`, `PUBLIC_URL` becomes `https://<domain>`, email is sent from `notifications@<domain>` (unless `MAIL_DOMAIN` is set), the Access application covers `<domain>/auth/access`, and email checks look at this domain. |
316| `DIFFINERY_ZONE` | `bootstrap.sh` | The hostname's last two labels | The Cloudflare zone that holds the hostname, for the Worker's route. Set it when the last two labels aren't the zone, for example `example.co.uk` for `git.example.co.uk`. The API token needs Workers Routes Edit on this zone. |
317| `ACCESS_TEAM_DOMAIN` | `bootstrap.sh` | Discovered | Your Zero Trust team domain, when the token can't read it. |
318| `MAIL_DOMAIN` | `bootstrap.sh`, `email-sending.sh` | `DIFFINERY_DOMAIN` | The domain email is sent from: `MAIL_FROM` becomes `notifications@<MAIL_DOMAIN>`, and the Email Sending check looks at this domain. |
319| `GITHUB_OUTPUT` | `bootstrap.sh` | Set by Actions | Where bootstrap writes `d1_id`, `access_team` and `access_aud_set`. |
320
321`scripts/render-config.mjs` takes flags instead:
322
323| Flag | Effect |
324| --- | --- |
325| `--d1 <database id>` | Required. The D1 `database_id`. |
326| `--team <team domain>` | Sets `ACCESS_TEAM_DOMAIN`. |
327| `--aud <audience tag>` | Sets `ACCESS_AUD`. |
328| `--public-url <origin>` | Sets `PUBLIC_URL` to the origin, `routes` to `[{ "pattern": "<hostname>/*", "zone_name": "<zone>" }]` and `MAIL_FROM` to `notifications@<hostname>`. Must be an `https://` URL without a path. |
329| `--zone <zone>` | The route's zone, with `--public-url`. Default: the hostname's last two labels. |
330| `--mail-from <address>` | Sets `MAIL_FROM`, overriding the one `--public-url` sets. |
331
332Bootstrap calls it with `--public-url https://$DIFFINERY_DOMAIN`, `--zone $DIFFINERY_ZONE` when that is set, and `--mail-from notifications@${MAIL_DOMAIN:-$DIFFINERY_DOMAIN}`. See [Deploy your own Diffinery](/docs/self-hosting/deploy#scriptsrender-configmjs).
333
334## Repository secrets and variables for the workflows
335
336The workflows in `.github/workflows/` read these from the repository (or its `production` environment):
337
338| Name | Kind | Used by | Value |
339| --- | --- | --- | --- |
340| `CLOUDFLARE_API_TOKEN` | Secret | Deploy | Cloudflare API token with the permissions listed in [Deploy your own Diffinery](/docs/self-hosting/deploy). |
341| `CLOUDFLARE_ACCOUNT_ID` | Secret | Deploy | Your Cloudflare account ID. |
342| `DIFFINERY_DOMAIN` | Variable | Deploy, Sync to Diffinery | Optional. Your instance's hostname, default `diffinery.com`. Deploy passes it to bootstrap (see [Deploy script settings](#deploy-script-settings)), uses it as the `production` environment's URL and runs its checks against `https://<DIFFINERY_DOMAIN>`. |
343| `DIFFINERY_ZONE` | Variable | Deploy, Sync to Diffinery | Optional. The Cloudflare zone for the route, when it isn't the hostname's last two labels. |
344| `MAIL_DOMAIN` | Variable | Deploy, Sync to Diffinery | Optional. The domain email is sent from, when it isn't `DIFFINERY_DOMAIN`. |
345| `ACCESS_TEAM_DOMAIN` | Variable | Deploy, Sync to Diffinery | Optional. Your Zero Trust team domain, for example `example.cloudflareaccess.com`, when the API token can't read it. |
346| `DIFFINERY_LIVE_TOKEN` | Secret | Live test, Sync to Diffinery | A Diffinery token with the `admin` scope and access to all repositories, created under **Settings → Access tokens** on the instance. The live test creates and deletes throwaway repositories with it, and uploads a test profile picture to the token's account (putting the previous one back afterwards). When the secret is not set, those steps are skipped with a warning; the check that `/docs`, `/llms.txt` and the CLI download are served without signing in still runs. The sync workflow uses it to create the organization and repository, copy secrets and variables, and push. |
347| `IMPORT_GITHUB_TOKEN` | Secret | Import campaign | Optional. A read-only GitHub token for public repositories. When it is set on GitHub, the campaign copies it to the Diffinery repository's secrets before it starts; without it, imports use GitHub's anonymous rate limit, and large repositories take hours. The main sync doesn't copy it. |
348
349Deploy reads `DIFFINERY_DOMAIN`, `DIFFINERY_ZONE` and `MAIL_DOMAIN` from the repository's variables and passes them to bootstrap, so you point it at your own hostname without editing the workflow (see [Deploy your own Diffinery](/docs/self-hosting/deploy#step-1-point-the-code-at-your-domain)). Deploy runs on pushes to `main` on any Diffinery instance, and when started by hand on GitHub.
350
351The Deploy job runs in an environment named `production` (created by its first run, with no branch policy). The Live test targets `https://diffinery.com`; adapt or delete it for your own instance.
352
353**When agents land changes on `main`.** A merge that Diffinery lands for an agent through auto-merge is the agent's push, so the Deploy and Live test runs after it are [runs of agent code](/docs/actions/workflow-permissions#runs-whose-code-came-from-an-agent). Those get no repository or organization secrets, and Deploy stops with `CLOUDFLARE_ACCOUNT_ID is required`. Either keep the secrets in the `production` environment, limited to protected branches, with `main` protected (add `environment: production` to the Live test job as well), or have an admin turn on **Give runs of agent code the repository's and organization's secrets** under **Settings → Actions**. The environment is the tighter choice: the setting gives the secrets to every agent run, branches included. See [Which runs receive secrets](/docs/actions/secrets-and-variables#which-runs-receive-secrets).
354
355The **Import campaign** workflow is a manual test of the importer. Start it on Diffinery (**Run workflow** on its page under **Actions**). Started on a GitHub copy, it relays: it starts the same workflow on Diffinery and prints its report. On Diffinery it imports a list of public GitHub repositories (the `repos` input) as private repositories named `<owner>/campaign-<github owner>-<github repo>`, replacing copies from earlier campaigns. It can then run their push workflows (`run_workflows`, off by default, because that is third-party code) and writes a report of what came across and what ran.
356
357- **Queueing.** Diffinery runs at most 3 repository imports per person at a time and answers `429` beyond that, so the campaign queues the rest and starts each one as an earlier one finishes.
358- **Time limit.** Imports still waiting when `minutes` runs out are reported as `not started`.
359- **Failures.** When a run fails before writing its report, the GitHub side prints the last 150 lines of the job's log instead.
360- **Following a run.** Set `follow_run` to a Diffinery run id to print the report of a run that already started.
361
362It needs `DIFFINERY_LIVE_TOKEN` and, optionally, `IMPORT_GITHUB_TOKEN`.
363
364Sync to Diffinery is how this repository moved from GitHub to Diffinery, and it is kept as a record. It runs only when started by hand on GitHub (`workflow_dispatch`). Since the cut-off, the repository's home is on Diffinery, and pushes to GitHub are no longer copied. Don't start it on the frozen GitHub copy: it still names the `codician-team` organization, which no longer exists on Diffinery, so a run would create that organization and a new `codician-team/diffinery` repository, which ends the old URLs' redirect to `codician/diffinery`. A run copies the four variables above to the repository on Diffinery (only those that are set), together with the secrets `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` and `DIFFINERY_LIVE_TOKEN`, so the copy deploys to the same place. It then copies every branch and tag, moving each ref only if it still points where it did when the run read it, so it never puts back an older commit that changed on Diffinery in the meantime. Branch deletions aren't copied. A run on `main` checks that `main` on Diffinery matches and waits for its Actions runs. Tick **Only copy the secrets and variables** to skip the branches, tags and the wait, for a repository on Diffinery that already has the code.
365
366`scripts/diffinery-wait-runs.sh`, which the sync workflow uses to wait for Actions runs on a commit, reads `DIFFINERY` (the instance origin) and `TOKEN` (a Diffinery token) from the environment:
367
368```sh
369DIFFINERY=https://diffinery.com TOKEN=dfy_... scripts/diffinery-wait-runs.sh acme/website <sha> 60
370```
371
372## Settings that are not configuration
373
374Some values are fixed in code and can't be changed without editing it, for example the job timeout ceiling, matrix size, the 10 GB cache budget per repository and session lifetime. They are listed in [Limits](/docs/reference/limits).