fix(infra): grant the runner's account nothing; split labelling off
All checks were successful
deploy / deploy (push) Successful in 5m34s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user