fix(infra): grant the runner's account nothing; split labelling off
All checks were successful
deploy / deploy (push) Successful in 5m34s

Reverses the org team added an hour ago. It was solving the wrong problem.

Measured on the bare account, with no team and no collaborator role: pushing to
its own fork works, opening a cross-repo pull request works, commenting works,
creating an issue works. Pushing to a lair repo is denied and so is deleting it.
Exactly one §2.2 capability is out of reach — applying a label — and that is the
one that must not be bought by granting code write.

So the runner gets nothing at all, which is a better property than a carefully
scoped grant: there is no permission to review, no team to audit, and no rule to
forget on the next repo added. Adding a repo to its remit is now one step, a
fork, rather than three.

Labelling moves to a separate identity, which lines up with the split the two
units already have. The runner records state in Postgres — already the authority
per §2.2 — and the poller reconciles labels onto the forge under its own account
with issue write and no code access. The token that can touch issues is held by
the process that never runs an agent; the token held by the process running
unattended agents can only push to a repository nobody depends on.

That also removes a silent failure I found while testing this. Creating an issue
*with* labels as an unprivileged user returns 201 and drops the labels — no
error, an issue that never gets picked up. A Plan job stamping inherited
admission on its children would have failed precisely that way and looked fine.
Children are now created bare, with their intended labels recorded in Postgres
for the poller to apply.

The second identity is provisioned with stage 2, when the first label is
written. Nothing before then needs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TxK1CWPkFXqdcXMJ4hVe6
This commit is contained in:
rob thijssen
2026-08-07 18:26:01 +03:00
parent b1e01777ff
commit 8586da2cb9
2 changed files with 70 additions and 41 deletions

View File

@@ -334,28 +334,34 @@ Remaining one-time steps (operator, on the target host):
Seed known_hosts for the forge at the same time. An unattended git must not
prompt, and StrictHostKeyChecking=accept-new would trust whatever answered.
Access is a Gitea ORG TEAM with unit-level permissions -- `lair/tireless-agent`:
Issues=write, PullRequests=write, Code=read, everything else none, scoped to
named repositories rather than the whole org.
GRANT THIS ACCOUNT NOTHING. No collaborator role, no team membership, no
permission on any repo beyond public read. An ordinary authenticated user can
already fork, push to its own fork, open a cross-repo pull request, comment
on an issue and create an issue -- which is the whole of what the runner does.
Do NOT add `tireless` as a collaborator with write access. Collaborator
permissions in Gitea are repo-wide, so "write" to run the label protocol also
grants code push, which is exactly what the fork model exists to avoid. The
unit-level team grants issue write WITHOUT code write.
For each repo tireless should work on, the only step is:
- fork it to the `tireless` user.
For each repo tireless should work on:
- add the repo to the `tireless-agent` team;
- fork it to the `tireless` user (the runner pushes only to its fork);
- confirm, rather than assume:
push to the fork succeeds
push to the upstream repo "User permission denied for writing."
labelling an issue succeeds
deleting the repo refused
Confirm, rather than assume:
push to the fork succeeds
cross-repo PR from the fork succeeds
push to the upstream repo "User permission denied for writing."
deleting the repo refused
Note what is NOT needed: branch protection for the bot. It has no push
permission at all, so there is no rule to forget on the next repo you add.
Keep branch protection for your own workflow if you want it; it is no longer
what stands between an unattended agent and `main`.
What is NOT needed: branch protection for the bot. It has no push permission
at all, so there is no rule to forget on the next repo you add. Keep branch
protection for your own workflow if you want it; it is no longer what stands
between an unattended agent and `main`.
LABELLING IS A SEPARATE IDENTITY, and does not exist yet -- provision it with
stage 2, which is when the first label is written. Applying a label is the one
§2.2 capability an unprivileged account cannot do, and it must not be bought
by granting the runner code write. The poller reconciles labels from Postgres
under its own account; the runner never writes one.
Beware when you build it: creating an issue WITH labels as an unprivileged
user returns 201 and silently drops the labels. No error, no warning, an issue
that never gets picked up.
3. Confirm the proxy is serving the cert that is on disk.
The vhost, its cert and the split-horizon DNS were installed above, but