Iris has one GitHub identity: a personal access token you set once, held by the server, used for every session. Callers never present a GitHub credential — there is no sign-in, no OAuth app and no device flow. The README says why it works this way. This file is what you need to deploy it.
Iris refuses to start without the token, so this is not optional setup.
- Go to github.com/settings/personal-access-tokens/new (Fine-grained tokens → Generate new token).
- Repository access: Only select repositories → your
upstream_repo. - Permissions → Repository permissions → Issues: Read and write. Nothing else.
- Set an expiry you will remember. Nothing renews it.
- Put the value in the environment as
IRIS_GITHUB_TOKEN; the example config reads it from there.
github:
token: ${IRIS_GITHUB_TOKEN}
upstream_repo: https://github.com/your-org/your-agent-libraryThe token is never sent to a browser and never written to the database. Issues get filed as whatever account made it — so make it an account you are willing to see on those issues, and consider a service account rather than your own.
When it expires, conversions keep working. Filing is the only thing that stops, with a 403 or 404
whose log line names github.token. Nothing else breaks, which is why an expired token is easy to
miss — put the expiry date in a calendar.
Iris warns about this at every boot, so it is not a surprise later. All four follow from having no per-caller identity, and none of them is a bug:
| What | Why |
|---|---|
| Contributors get no attribution | Every issue is filed as your token's account. The issue says what a session found, not who found it. |
GET /v1/sessions lists the deployment's sessions |
Ownership is one account, so there is no such thing as "the caller's sessions". Anyone who can call it sees every session id, and an id is all /output needs. |
| Uploads are limited per address | The only credential a caller can present is shared, so keying a budget on it would put the whole internet in one bucket. Behind NAT, callers share a budget — and server.trust_proxy has to be right or they all look like the proxy. |
| A visitor can read another visitor's document | Given the session id. Sessions are not isolated from each other, because there is nobody to isolate. |
The last two are the ones to think about before you deploy publicly. Gating (below) is the answer.
server.api_token is a separate shared secret that decides who may call the API at all. It is
not a GitHub token and it does not make anyone anybody: every caller who presents it reaches the same
deployment account.
Blank (the default) means open. Anyone who can reach the port can convert documents, spend your model budget and read any session whose id they have. That is what makes the bundled browser app work with no setup, and it is the right default for a laptop or a private network — not for a public URL.
Set it and callers must present it:
server:
api_token: ${IRIS_API_TOKEN} # openssl rand -hex 32curl -H "Authorization: Bearer $IRIS_API_TOKEN" "$BASE/me"Absent, malformed, wrong scheme and wrong secret all get the same 401 saying only that the
deployment is gated. Nothing tells a caller anything about the secret.
Gating turns off the bundled demo page, which holds no credential. That is the trade: a public deployment either hands the secret to the people who should use it, or accepts strangers.
The gate is attached to /v1/me and /v1/sessions, and to nothing else. Four endpoints are outside
it and answer on a gated deployment. None touches a document or an identity:
| Endpoint | Why |
|---|---|
GET /v1/health |
A load balancer's probe cannot hold a secret. |
GET /v1/limits |
Someone deciding whether their scan is small enough should not need the key to find out. |
GET /v1/stats |
A deployment-wide tally, no per-session detail. |
GET /v1/quality |
Has its own token (server.quality_token) and 404s unless you set it. |
If any of those must be private too, put it behind your reverse proxy. Iris will not do it for you.
401 This deployment could not authenticate to GitHub — your token is wrong, revoked or expired,
or GitHub is down. Iris asks GitHub once, caches the answer for the life of the process, and after a
failure waits 30 seconds before asking again — so a transient outage clears itself without a restart
and without one GET /user per request. Fix the token and restart.
500 github.token is not configured — only reachable from a config that never went through
validation. A deployment that booted has the key.
Filing failures are separate and never fail a run: they are logged as agent_issue_failed /
agent_update_issue_failed with a hint. The likely cause is always the same one now — that PAT's
access to upstream_repo. Expect 404 more often than 403: GitHub does not reveal repositories a
credential cannot see, so no access reads as "no such repo". A misspelled upstream_repo is identical
on the wire, and the hint says so rather than blaming one.
- Delete
data/iris.sqlite. An early build stored a token per user in agithub_tokencolumn. There is no migration and the service refuses to start against such a file rather than adopting it: the oldgithub_token TEXT NOT NULLsurvivesCREATE TABLE IF NOT EXISTS, so Iris could not write the one row it owns and every request would fail with a500naming a SQLite constraint on a column no current build writes. The file also still holds live plaintext tokens, so delete it rather than archiving it. You lose session history and nothing else. - Delete these keys.
github.client_id,github.client_secret,github.oauth_scope,github.oauth_base_url,github.anonymous_token,github.issue_token, andserver.rate_limits.auth_per_minute. They are ignored, not errors — but leaving them in a config file describes a deployment you do not have. POST /v1/auth/github/deviceand its poll endpoint are gone (404). Any client running the device flow needs updating: it now sends either nothing orserver.api_token.GET /v1/meno longer describes the caller. It describes the deployment, and it has noanonymousfield to check — 200 means open, 401 means gated.
Yours is in your config and your environment; treat it like any other server secret. Nothing else
about it is stored: there is no github_token column and no token file. A stolen copy of
data/iris.sqlite is not GitHub access. What it does hold is a GitHub user id and login for each
account this deployment has run as — one, unless you have pointed it at a different account, since
nothing removes the old row or its sessions — plus the session history, which is the part worth
protecting.
There is no per-user token to rotate, cache or purge, and no user-facing revocation story — because no user ever authorized anything. Revoke at github.com and restart.
Rotating the token for the same account changes nothing in the database. Pointing it at a
different account strands the old account's sessions. Every per-session route checks the owner, so
a session id you still hold answers 404 session_not_found — all of it, not just the listing:
| What you lose | Why it matters |
|---|---|
| Status, document, logs, diagnostics | All four reads 404, so you cannot even check what a session's state was, let alone fetch the HTML it produced. |
POST /{id}/feedback |
A session waiting at ready_for_review cannot be iterated on — the review loop stops. |
POST /{id}/close |
No fixture capture, and the session's temporary files stay on disk. close is the only thing that removes them. |
Nothing is deleted. The rows are still in data/iris.sqlite, and pointing the config back at the
first account makes them reachable again — but only reachable: the tmp trees go when someone actually
closes those sessions, not when you switch back.
So: close anything you have finished with before switching accounts, and finish anything mid-review first. Exporting the documents does neither.