feat(infra): provision the bot account, and move to a fork-based PR model
All checks were successful
deploy / deploy (push) Successful in 5m36s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user