feat(infra): provision the bot account, and move to a fork-based PR model
All checks were successful
deploy / deploy (push) Successful in 5m36s

The design had tireless pushing `tireless/*` branches into the repo it was
working on, with branch protection denying it `main`. It now works the way an
outside contributor does: pushes only to its own fork, reaches the real
repository through a pull request, and has no write access to that repository at
all.

That is a stronger guarantee than the one it replaces. Branch protection is a
rule that can be edited, applies per repo, and is easy to forget on the next repo
added; a bot with no push permission cannot write to `main` whatever anyone
forgets.

The wrinkle is the label protocol. Labels are a write on the issues unit, and
Gitea collaborator permissions are repo-wide, so granting issue write as a
collaborator would hand back the code push the fork exists to avoid. Access is
therefore an org team with unit-level permissions -- lair/tireless-agent, with
Issues=write, PullRequests=write, Code=read, everything else none, scoped to
named repositories rather than the whole org.

Provisioned and verified end to end rather than assumed:

  push to the fork            succeeds
  push to lair/tireless       "User permission denied for writing."
  label an issue              200 / 204
  cross-repo PR from fork     opened as `tireless`
  delete the repository       403

Two credentials with different blast radii: an ssh key on the account for git
transport, and an API token scoped write:issue + write:repository + read:user for
issues and PRs. known_hosts is pre-seeded, because an unattended git must not
prompt and accept-new would trust whatever answered first.

Stage 4 gains the consequence: two remotes, and a fork that has to be checked for
staleness before branching.

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:18:31 +03:00
parent ae285f5597
commit b1e01777ff
2 changed files with 100 additions and 23 deletions

View File

@@ -321,18 +321,41 @@ Remaining one-time steps (operator, on the target host):
ANTHROPIC_API_KEY in /etc/tireless/tireless.env instead. Do not do both:
the API key silently wins, and the subscription goes unused.
2. Gitea bot account.
Create a dedicated `tireless` user on git.lair.cafe (not your own account),
generate a token scoped to issue + PR write, and put it in
/etc/tireless/tireless.env as GITEA_TOKEN (0640 root:tireless).
2. Gitea bot account. (Done for git.lair.cafe on 2026-08-07.)
A dedicated `tireless` user, not your own account, holding two credentials:
Then, for each repo tireless should work on:
- add `tireless` as a collaborator with write access;
- enable branch protection on the default branch, denying `tireless` push;
- confirm it can still push refs matching `tireless/*`.
- ssh key at /var/lib/tireless/.ssh/id_ed25519 (0600 tireless:tireless),
registered on the account, for git transport. The private half is kept at
~/.ssh/id_tireless on the operator workstation.
- API token scoped `write:issue`, `write:repository`, `read:user`, in
/etc/tireless/tireless.env as GITEA_TOKEN (0640 root:tireless), for
issues and pull requests.
The protection rule is what keeps an unattended agent from writing to main.
Verify it rather than assuming it.
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.
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:
- 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
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`.
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