Two things that read as protection while being none. Phase 2's rule binds everyone who reaches Gitea through the API or the web, and does not bind anyone with `gitea admin` in the container -- which includes the supervising agent that created the agent accounts and minted their tokens. From that shell the rule is editable and an admin token is one command away. That is the boundary of what the phase buys, not a hole to plug there, and the document read as though the gate were universal. Phases 1 and 2 gate the two CONTAINERISED agents, whose design assumption is that policy lives where they cannot reach it; a supervisor with a host shell is not in that set. And `gitea-setup` finished by telling the reader to go and build a Gitea project board by hand, four sections after the doc explains that a board is a second copy of the state to hand-sync and is precisely the failure that produced a 1,227-line BLOCKED.md. A tool instructing you to do the thing its own documentation argues against is the drift this whole surface exists to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
351 lines
16 KiB
Markdown
351 lines
16 KiB
Markdown
# Runbook: standing the Gitea working surface up
|
|
|
|
**For the human. Work top to bottom — later phases depend on earlier ones.**
|
|
[`WORKFLOW-gitea.md`](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
|
|
|
|
* ✅ `main` carries 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` |
|
|
| email | 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`.
|
|
|
|
**Apply it through the API, not the form** — `tools/gitea-protect`. Six settings
|
|
of which two are load-bearing, and both of those were missing from the first
|
|
draft of this phase: that is the shape of thing that gets mis-clicked. An API
|
|
call is reviewable in a diff and repeatable, and the same file re-checks it later.
|
|
|
|
```bash
|
|
tools/gitea-protect --dry-run # the exact rule, no credential read
|
|
tools/gitea-protect # create or update, then verify
|
|
tools/gitea-protect --verify # assert it still holds; exit 1 if not
|
|
```
|
|
|
|
📌 **Run it on the agent box, not the Pi.** Branch protection is a
|
|
repository-scope endpoint, so `~/.sylph-gitea-api-token` cannot do it — that
|
|
token is deliberately issue-only. The credential that can is the one already
|
|
sitting on that machine, `~/.sylph-git-credentials`, which the tool reads. Doing
|
|
it there means no new credential, and no second machine holding push rights just
|
|
to close a one-time setup step.
|
|
|
|
🔴 The tool sets `block_admin_merge_override: false`, deliberately. Turning it on
|
|
would lock **you** out of your own work — approvals are whitelisted to `fabi`,
|
|
Gitea will not let `fabi` approve a `fabi` PR, so a human-authored PR could never
|
|
reach one approval and could never merge. The admin override is what keeps that
|
|
door open, and it is not a hole in the agent gate for exactly one reason: the
|
|
agents are **Write, not Admin**. That is what Phase 1.2 is buying, and this is
|
|
where it gets spent.
|
|
|
|
Or by hand — 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.
|
|
|
|
### 🔴 What this rule does not gate, said plainly
|
|
|
|
It binds everyone who reaches Gitea through the API or the web. **It does not
|
|
bind anyone who can run `gitea admin` inside the container** — and that includes
|
|
the supervising agent on the Pi, the one that created the agent accounts and
|
|
minted their tokens. From that shell you can issue an admin token or edit this
|
|
rule, and nothing here would stop you.
|
|
|
|
That is not a hole to plug here; it is the boundary of what Phase 2 buys, and it
|
|
should be written down rather than discovered. **Phases 1 and 2 gate the two
|
|
containerised agents** — the ones that run unattended on a loop, whose whole
|
|
design assumption is that policy lives somewhere they cannot reach. A supervisor
|
|
with a shell on the host is not in that set, and the protection above should not
|
|
be read as universal.
|
|
|
|
The distinction is exactly the one Phase 1.2 draws with **Write, not Admin**: the
|
|
looping agents get a permission level that cannot edit the rule that binds them.
|
|
`tools/gitea-protect --verify` asserts that level on every run, which is the
|
|
check that keeps this true rather than merely stated.
|
|
|
|
> ### 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:
|
|
>
|
|
> 1. As `sylph-port`, push a throwaway branch and open a PR into `main`.
|
|
> 2. Confirm **no Merge button** is offered to that account.
|
|
> 3. Confirm **you** can approve it, and that *it* cannot approve itself.
|
|
> 4. Approve it yourself, then look at `sylph-port` again: **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.
|
|
> 5. 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` |
|
|
|
|
```bash
|
|
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-run` prints "would create …" rather than a
|
|
> scope error.
|
|
|
|
---
|
|
|
|
## Phase 4 · Labels and bundles 🤖
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. **`Dockerfile`** — fetch the release tarball, verify the checksum, unpack
|
|
`gitea-mcp` into `/usr/local/bin`, and run `--version` at build time so a bad
|
|
pin fails the build rather than the agent.
|
|
2. **`entrypoint.sh`** — register it at user scope for that agent's identity,
|
|
remove-then-add so a restart is idempotent:
|
|
```bash
|
|
claude 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"
|
|
```
|
|
🔴 **By path, not by value.** The earlier draft of this line read
|
|
`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_FILE` is new in the version we pin and
|
|
leaves the secret in its read-only mount.
|
|
3. **`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.
|
|
|
|
```bash
|
|
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 `main` as a
|
|
reviewable commit through git, or it does not reach it.
|
|
* `label_write` / `milestone_write` — agents *apply* labels (that is
|
|
`issue_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 list` shows `gitea` connected, 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 in `logs` rather 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 🤖 + 👤
|
|
|
|
```bash
|
|
docker/decoder/sylph-decoder
|
|
docker/port/sylph-port
|
|
```
|
|
|
|
> ### Check — the three things that must be true
|
|
>
|
|
> 1. 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.**
|
|
> 2. Each opens a **pull request**, not a bare branch push, and labels its issue
|
|
> `state/needs-human` with a one-line "look at this".
|
|
> 3. 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`**, superseding `push-work` — push the branch *and* open the PR
|
|
with `Closes #N` *and* set the label, in one step. Today `push-work` does the
|
|
first third; the other two thirds being manual is how they get skipped. Its
|
|
existing refusals stay: no `main`, no force-push.
|
|
* **an attachment uploader** — the MCP exposes `attachment_read` only, so putting
|
|
a screenshot on an issue needs a direct `POST /repos/{owner}/{repo}/issues/{index}/assets`.
|
|
* ~~**`gitea-verify`**~~ — done, as `tools/gitea-protect --verify`: asserts every
|
|
field of the rule *independently* of what the apply path sends, and that both
|
|
agents are still Write-not-Admin. What is still missing is only the *every
|
|
day* part — nothing runs it on a timer yet.
|
|
* **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 `--tools` filter names~~ — read out of the pinned release, and the
|
|
binary's `--help` run directly. Phase 5 lists the set.
|
|
* ~~the exact Gitea version~~ — **1.25.5**. `enable_merge_whitelist`,
|
|
`enable_approvals_whitelist` and `block_admin_merge_override` are all present
|
|
in this instance's own API schema, so the Phase 2 settings exist under those
|
|
names on the Branches screen.
|