Phase 5 of docs/agents/GITEA-SETUP.md, plus a correction to Phase 2 that the runbook could not have known it needed. gitea-mcp v1.7.0 goes into both images, pinned by the sha256 the release publishes and smoke-tested with `--version` at build time, so a bad pin fails the build instead of the agent. Each entrypoint registers it at user scope for that container's own identity, remove-then-add so a restart is idempotent. The token is passed BY PATH. `-e GITEA_ACCESS_TOKEN=$(cat …)` would write it in cleartext into ~/.claude.json, which every session in the container reads; GITEA_ACCESS_TOKEN_FILE is new in the pinned version and leaves the secret in its read-only mount. Verified against the binary's own --help, not assumed. The tool filter stops being an experiment. The names are in the release README: each agent gets issues, notifications, labels, milestones and pull requests, and NOT `pull_request_review_write`. That one matters because separate identities open a hole the runbook did not name: Gitea refuses to let an author approve their own pull request, and does nothing about sylph-decoder approving sylph-port's. Two agents could satisfy `required_approvals = 1` between themselves and then merge, since branch protection blocks pushes to main and never blocked merges. Withholding the tool is defence in depth; the controls are in branch protection, and both docs now say so: approvals whitelisted to the human so an agent's approval does not count, merges whitelisted to the human so an approved PR is still merged by a person. Phase 2's check gains the step that actually tests it -- approve the throwaway PR yourself, then confirm the agent STILL has no merge button. Without that step, the check passes on an instance where the agents can merge each other's work. Also settles two entries on the runbook's own "not verified" list: the tool filter names, and the Gitea version (1.25.5, whose API schema carries enable_merge_whitelist and enable_approvals_whitelist under those names). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
13 KiB
Runbook: standing the Gitea working surface up
For the human. Work top to bottom — later phases depend on earlier ones.
WORKFLOW-gitea.md says what this is and why; this says
how, in order, with a check after each phase.
Steps are marked 👤 you (a decision or a credential only you can make) or 🤖 me (I do it once you have unblocked it).
Where things stand
- ✅
maincarries the play-tested port work and the rewritten briefs (cd3a81af). - ✅ The briefs already describe this workflow. They describe tooling that does not exist yet — which is why the agents are stopped and must stay stopped until Phase 7.
- ❌ Nothing exists on the instance: no agent users (only
fabi), no API tokens, no labels, no milestones, no branch protection. - ⏳ The MCP wiring (Phase 5) is written and in the tree; the images have not been rebuilt, and no token exists for it to use yet.
⚠️ Do not start an agent before Phase 7. Its brief will tell it to read notifications and open issues, and it will have no tool that can.
Phase 1 · Identities 👤
The agents currently push as fabi, using your credential. That is the defect
this phase fixes, and it is not cosmetic: Gitea does not let the author of a
pull request approve it. While an agent is you, either you cannot approve its
PR or it can approve its own — and there is no third possibility. The review gate
does not exist until the agents are distinct people.
Two more reasons, once you are there anyway: 495 commits of decoder work are currently attributed to your email, so blame is wrong; and separate identities mean revoking one agent does not touch the other or you.
1.1 — Create two users. Site Administration → Identity & Access → User Accounts → Create User Account.
| usernames | sylph-decoder, sylph-port |
anything you control and can tell apart — you+decoder@…, you+port@… |
|
| "require password change on first login" | off — they never log in interactively |
1.2 — Add both to fabi/Sylpheed as collaborators. Repo → Settings →
Collaborators → add each → permission Write.
🔴 Write, not Admin. Admin can edit branch protection, which would let an agent remove the rule that stops it merging.
Check: the repo's Collaborators list shows both, each reading
Write.
Phase 2 · Branch protection 👤
Do this before the agents hold tokens, so there is no window in which they
can push to main.
Repo → Settings → Branches → Protected Branches → add rule for main:
| setting | value | why |
|---|---|---|
| Enable Push | off | nothing reaches main except through a PR |
| Require approvals | 1 | the human gate, made native |
| Dismiss stale approvals | on | an approval must describe the code that merges |
| Block merge on rejected reviews | on | "changes requested" has to mean something |
| Enable Merge Whitelist | on → whitelist fabi only |
approvals are not the last gate. Merging is |
| Enable Approvals Whitelist | on → whitelist fabi only |
only a human's approval counts toward the 1 |
🔴 The hole that separate identities open, and why the last two rows close it
Phase 1 makes the agents distinct people so that a human can approve their
work. The same change makes something else possible for the first time: Gitea
refuses to let an author approve their own pull request — it does not stop
sylph-decoder approving sylph-port's. With required_approvals = 1 and
nothing else, the two agents satisfy the human gate between themselves, and the
author can then press Merge, because branch protection blocks pushes to main
and never blocked merges.
Neither whitelist is decoration, and neither replaces the other:
- approvals whitelist — an agent's approval stops counting toward the 1.
- merge whitelist — even a legitimately approved PR is merged by you.
Withholding the review tool from the agents (Phase 5) is defence in depth behind these, not a substitute: an agent still has a browser-shaped API token.
Check — and actually run it, do not assume it
The whole point of putting this in protection rather than in a document is that it does not depend on anyone's good behaviour. So verify it the same way:
- As
sylph-port, push a throwaway branch and open a PR intomain.- Confirm no Merge button is offered to that account.
- Confirm you can approve it, and that it cannot approve itself.
- Approve it yourself, then look at
sylph-portagain: still no Merge button, now that an approval exists. This is the step that tests the merge whitelist rather than the absence of an approval — without it, steps 2 and 3 pass on an instance where the agents can merge each other's work.- Close the PR, delete the branch.
If step 2 or step 4 offers a Merge button, stop — the rest of this runbook assumes neither does.
Phase 3 · Tokens 👤
Three principals, three tokens. Settings → Applications → Generate New Token while logged in as that user.
| whose | scopes | goes in |
|---|---|---|
you (fabi) |
write:issue, read:repository |
~/.sylph-gitea-api-token |
sylph-decoder |
write:repository, write:issue, write:notification, read:user |
~/.sylph-gitea-token-decoder |
sylph-port |
same four | ~/.sylph-gitea-token-port |
printf '%s\n' '<token>' > ~/.sylph-gitea-api-token && chmod 600 ~/.sylph-gitea-api-token
⚠️ Never paste a token into chat. The files are mounted read-only into the
containers, exactly like ~/.sylph-claude-token.
📌 The existing ~/.sylph-git-credentials is scoped write:repository and is
refused by every issue endpoint — verified, not assumed:
required=[read:issue], token scope=write:repository. It stays as it is; these
are additional.
Check:
tools/gitea-setup --dry-runprints "would create …" rather than a scope error.
Phase 4 · Labels and bundles 🤖
tools/gitea-setup --dry-run # read it first
tools/gitea-setup # idempotent; safe to re-run
Creates 12 labels (the state/* set, agent/*, kind/*) and 4 milestones
(Menus, Title screen, Graphics pipeline, Infrastructure).
No Kanban board yet, on purpose. Gitea's board does not follow labels, so it
would be a second copy of the state to keep in sync by hand — which is the exact
failure that produced a 1,227-line BLOCKED.md. Labels are the truth; a
saved issue filter gives the same view for nothing. Add a board later if the
filter turns out to be insufficient.
Check: the Issues page offers the
state/*labels, and Milestones lists the four bundles.
Phase 5 · The MCP server 🤖
Done — in the tree, not yet in an image. gitea-mcp v1.7.0, Linux
x86_64, sha256 bbc9a7b4…d446b8d from the release's own checksums.txt. The
flags are no longer taken on trust: the arm64 build of the same release was run
and its --help read, so -t stdio, -H <url>, -O/--tools, -S/--scope,
-r/--read-only and GITEA_ACCESS_TOKEN_FILE are confirmed, not assumed.
Three edits per image, made:
Dockerfile— fetch the release tarball, verify the checksum, unpackgitea-mcpinto/usr/local/bin, and run--versionat build time so a bad pin fails the build rather than the agent.entrypoint.sh— register it at user scope for that agent's identity, remove-then-add so a restart is idempotent:🔴 By path, not by value. The earlier draft of this line readclaude mcp add -s user gitea -e "GITEA_ACCESS_TOKEN_FILE=$GITEA_TOKEN_FILE" \ -- gitea-mcp -t stdio -H https://git.mc02.dev -O "$GITEA_MCP_TOOLS"GITEA_ACCESS_TOKEN=$(cat …), which writes the token in cleartext into~/.claude.json— read by every session in the container and carried into any copy of that file.GITEA_ACCESS_TOKEN_FILEis new in the version we pin and leaves the secret in its read-only mount.sylph-decoder/sylph-port— mount~/.sylph-gitea-token-{decoder,port}read-only and pass its path. Inert until the file exists: without a token the container still starts, says plainly that the agent has no issues and no pull requests, and carries on.
👤 Yours: rebuild both images on the agent box, where the containers run.
⚠️ CARGO_BUILD_JOBS=4 and a limited -j; a full-parallel build has OOM-crashed
that machine.
docker/decoder/sylph-decoder build
docker/port/sylph-port build
The tool filter is a control now, not an experiment
The tool names were unknown when this was written; they are in the release's
README, and the set each agent gets is pinned in the entrypoint
(SYLPH_GITEA_TOOLS overrides it):
get_me, notification_read, notification_write, list_issues, issue_read,
issue_write, attachment_read, search_issues, label_read, milestone_read,
list_pull_requests, pull_request_read, pull_request_write
What is absent is the point:
pull_request_review_write— the tool that approves, dismisses and resolves reviews. Without it an agent cannot approve the other agent's pull request through the MCP. Pair it with the approvals whitelist in Phase 2; the whitelist is the control, this is the layer in front of it.- the file, branch, tag and repo write tools — a change reaches
mainas a reviewable commit through git, or it does not reach it. label_write/milestone_write— agents apply labels (that isissue_write); they do not get to redefine the state machine.
pull_request_write bundles merge into one action-based tool and cannot be
split, which is exactly why merging is blocked by the merge whitelist instead.
Check: in each container,
claude mcp listshowsgiteaconnected, and a read call returns this repo's labels. The entrypoint also says which of the two it did on every start, so a missing token is visible inlogsrather than as an agent quietly improvising.
Phase 6 · Seed the first items 🤖 + 👤
I migrate the live findings into issues — not the 1,227 historical lines, only what is actually open:
| bundle | items |
|---|---|
| Title screen | F5 (does Ⓐ snap or accelerate?), F6 (ptloop01/02 sweep onset), re-propose the F5/F6 work left off main |
| Menus | F1 (held-direction repeat rate — Decoder measures, Port implements), F2 (SFX mix too loud), F3 (missing title audio), re-propose the OPTIONS menu work |
Each gets a bundle, an owner label, a dependency edge where one waits on the other, and — for anything already written on the port branch — a note that the code exists and needs re-proposing as a reviewable PR, not rewriting.
👤 Your part: approve the shape of each (state/proposed →
state/approved). This is the cheap gate — before effort, not after.
Phase 7 · Restart, and verify the loop 🤖 + 👤
docker/decoder/sylph-decoder
docker/port/sylph-port
Check — the three things that must be true
- Each agent's first iteration reads its notifications. If it does not, nothing addressed to it will ever arrive: notifications are polled, and nothing pushes.
- Each opens a pull request, not a bare branch push, and labels its issue
state/needs-humanwith a one-line "look at this".- Neither can merge. (Already proven in Phase 2; confirm it holds for a real PR.)
Still to build 🤖
Not blockers for Phase 7, but the workflow is not finished without them:
propose-work, supersedingpush-work— push the branch and open the PR withCloses #Nand set the label, in one step. Todaypush-workdoes the first third; the other two thirds being manual is how they get skipped. Its existing refusals stay: nomain, no force-push.- an attachment uploader — the MCP exposes
attachment_readonly, so putting a screenshot on an issue needs a directPOST /repos/{owner}/{repo}/issues/{index}/assets. gitea-verify— asserts protection is still on and the agents are still Write-not-Admin. Phase 2 is checked once; this checks it every day.- wiki landing page — bundles in flight and what each agent is on. There is currently no view of what is happening except container logs.
What I have not verified
Said plainly, because a runbook that hides its soft spots is worse than one that does not:
- that Gitea hides Approve from a PR's own author. Widely true; Phase 2's check tests it directly rather than trusting me. What I no longer assume is that it is enough — it says nothing about one agent approving the other, which is what the approvals whitelist is for.
- Gitea's Projects API, which is why Phase 4 creates no board.
Two things on this list have since been settled rather than assumed:
the— read out of the pinned release, and the binary's--toolsfilter names--helprun directly. Phase 5 lists the set.the exact Gitea version— 1.25.5.enable_merge_whitelist,enable_approvals_whitelistandblock_admin_merge_overrideare all present in this instance's own API schema, so the Phase 2 settings exist under those names on the Branches screen.