Commit Graph

9 Commits

Author SHA1 Message Date
7f38c9d12e docs: strike propose-work from "Still to build" — it is built
GITEA-SETUP.md listed `propose-work` as unbuilt, and this PR builds it without
touching that list, so merging would leave the doc describing a tool as missing
that exists. Strike it the way `gitea-verify` already is: say what was built,
then say what is still missing — here, an end-to-end run, which only the agent
box can do because only it holds a token that can make the POSTs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 06:14:02 +02:00
MechaCat02
fcf6ec0497 docs: the status block said nothing existed while nine issues were live
Phases 1-4 and 6 are done on the instance. This file still opened with "Nothing
exists on the instance: no agent users, no API tokens, no labels, no milestones,
no branch protection" -- every clause of which was false by the time the merge
that carried it landed.

Replaced with a table of measured state, and each row says what was MEASURED
rather than what was run:

  * protection is verified behaviourally -- a real push to main refused with
    `pre-receive hook declined`, as the repository owner -- not read off a
    settings page. That distinction is the whole subject of this file.
  * the tokens are probed: right identity, 403 on branch_protections for both
    agents, so the Write-not-Admin carve-out is demonstrated and not asserted.
  * the labels are 11 because the instance holds 11.

And a standing note that this block is the part most likely to be wrong, with
what to believe instead: `gitea-protect --verify` and the issue list MEASURE,
this block REMEMBERS. A remembered status is a cache with no invalidation, which
is the same failure as a 1,227-line BLOCKED.md and as the two documents this
runbook was split across an hour ago.
2026-09-04 17:55:28 +02:00
MechaCat02
ce9fc6eea6 Merge branch 'pi/gate-limit' into agents/gitea-mcp 2026-09-04 17:55:00 +02:00
MechaCat02
9aeeb8c574 docs: the tool creates 11 labels, not 12 -- I counted its own definition
Caught by the Pi agent against the live instance after Phase 4 ran. The tool
creates 5 state/*, 2 agent/*, 4 kind/* = 11.

Where the 12 came from is worth a line, because it is a shape that recurs:

    $ grep -c '^mklabel' tools/gitea-setup
    12
    $ grep -n '^mklabel' tools/gitea-setup | grep -v ':mklabel "'
    74:mklabel() { # name colour description

I counted the function DEFINITION as a call. A measurement taken one token away
from the thing being measured -- the same shape as reading protection off a
settings page and reachability off a DNS record, which is now three today. The
version that cannot make this mistake is counting what the instance holds, and
that is what found it.
2026-09-04 17:50:50 +02:00
b67b6243e6 agents: name what branch protection does not gate, and stop the tool contradicting it
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
2026-09-04 17:48:58 +02:00
MechaCat02
184ca0b556 docs: fold the page's revisions into the file, and separate wrong from unchecked
The runbook existed as two documents -- a published page and this file -- with
no mechanism keeping them equal, only an intention to remember. Two versions was
the predicted outcome of that, not an accident on top of it. This is the fold,
and the rule that follows it: THIS FILE IS THE SOURCE, the page is derived from
it. When something is urgent enough to push to the page first, it lands here in
the same turn, not "shortly after".

Four things the file did not carry:

  * YOUR OWN PUSHES TO main STOP. `enable_push: false` compiles to CanUserPush,
    which returns false with no bypass for admins or the owner -- quoted from
    the source. Three commits went in by direct push the day this was written,
    so the first notice would have been mid-task. Now a check step.
  * the token files' MACHINES, which the table had lost.
  * do NOT add `write:repository` to the `fabi` token. That scope IS a push
    credential. Written down because that advice was given, in chat, by the
    author of this file.
  * Gitea 1.25.5 confirmed from the desktop too, not just the Pi.

And one thing deliberately NOT folded in: the page said the desktop's outbound
HTTP was blocked, and that is false. `python3 -c 'urllib...'` returns
200 {"version":"1.25.5"} from this box. What is refused here is `curl`, by a
local permission prompt -- which I read as a network constraint and then
published as one. The Phase 3 locations stand; the reason given for them did not.

The "not verified" section now separates WRONG from UNCHECKED. Four entries are
wrong -- requiring an approval does not close the gate, the check could not have
caught that, the token scope, the reachability -- and the pattern in all four is
identical: a property inferred from something ADJACENT to it (protection from a
settings page, reachability from a DNS record) instead of tested directly. That
is the frozen-splash failure, committed in the document about avoiding it. The
first two were caught by the other agent, which is the argument for the review
gate this file exists to build.
2026-09-04 17:18:46 +02:00
799fa93383 tools: apply and re-check the branch protection rule, rather than clicking it
Phase 2 as a file. Six settings where two are load-bearing and both were missing
from the first draft is the shape of thing that gets mis-clicked at 1am, so it
goes through the API: what was applied is readable in a diff, and `--verify`
can re-check it later instead of it being checked once.

--verify states its expectations INDEPENDENTLY of what the apply path sends.
A check derived from "whatever we posted" cannot fail -- it re-derives the
expectation from the thing under test, which is the same instrument-shaped
failure as a check that passes on an instance with no rule at all.

It also asserts both agents are still Write and not Admin, because an agent
promoted to Admin can edit the rule and then merge, so a green rule proves
nothing on its own. That is the `gitea-verify` card from "Still to build";
what is left of it is only putting it on a timer.

`block_admin_merge_override` stays false on purpose, and the reasoning is in
the file: approvals are whitelisted to `fabi`, and Gitea will not let `fabi`
approve a `fabi` PR -- so with the override blocked, a human-authored PR could
never reach one approval and could never merge at all. The override is not a
hole in the agent gate because the agents are Write, not Admin. Phase 1.2 pays
for that; this is where it is spent.

Reads the repository-scoped credential that already exists on the agent box
(~/.sylph-git-credentials) rather than the issue-only ~/.sylph-gitea-api-token,
which every branch-protection endpoint refuses. That keeps the setup needing no
new credential, and keeps push rights on one machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
2026-09-04 16:49:58 +02:00
eccb789c0b docker: give each agent its own Gitea hands, and close the cross-approval hole
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
2026-09-04 16:39:12 +02:00
MechaCat02
9652a5ad77 agents: the ordered runbook for standing the Gitea surface up
WORKFLOW-gitea.md said what the working surface is and why. It did not say how,
in what order, or how to know a step worked -- so it was a destination with no
route. This is the route.

Seven phases, each with a check, each marked 👤 human or 🤖 me:

  1 identities   two agent users, Write NOT Admin
  2 protection   main behind a PR + 1 approval -- BEFORE tokens exist
  3 tokens       three principals, three tokens, three files
  4 structure    labels and bundles, and deliberately NO Kanban board
  5 MCP          gitea-mcp v1.7.0, per-agent identity, user scope not .mcp.json
  6 items        migrate the live findings only -- not 1,227 historical lines
  7 restart      and verify the three things that must be true

Phase 1 leads because it is not hygiene: Gitea does not let a PR's author
approve it, so while an agent IS `fabi` either the human cannot approve its work
or it can approve its own. The review gate does not exist until the agents are
distinct people. (It also fixes 495 commits of agent work attributed to the
human's email.)

Phase 2's check is a real push and a real PR, not a reading of the settings
page. The reason protection lives in the server rather than in a brief is that
it should not depend on good behaviour -- so verifying it should not either.

Phase 5's install facts are checked, not remembered: gitea-mcp v1.7.0,
`gitea-mcp_Linux_x86_64.tar.gz`, `-t stdio -H <host>`, `GITEA_ACCESS_TOKEN`.
The `--tools` filter is flagged as an EXPERIMENT that might exclude the merge
tool as defence in depth -- explicitly not a substitute for phase 2.

Ends with what is still to build (propose-work, an attachment uploader,
gitea-verify, the wiki landing page) and a "what I have not verified" section:
the approve-your-own-PR behaviour, the --tools names, the Projects API, and the
Gitea version -- the API was unreachable from my sandbox three times running.
2026-09-04 16:25:10 +02:00