diff --git a/docker/decoder/Dockerfile b/docker/decoder/Dockerfile index b3f85389..571d4ced 100644 --- a/docker/decoder/Dockerfile +++ b/docker/decoder/Dockerfile @@ -82,6 +82,24 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && npm cache clean --force \ && rm -rf /var/lib/apt/lists/* +# ── gitea-mcp ──────────────────────────────────────────────────────────────── +# The agent's hands on issues, pull requests and notifications — Gitea's own MCP +# server, so there is no second store of truth to drift out of sync with the +# first. +# +# PINNED AND CHECKSUMMED, not "whatever is at that URL today": this binary is +# handed a token that can write to the repository. The checksum is the one +# published in `gitea-mcp_1.7.0_checksums.txt` for the Linux x86_64 asset. +ARG GITEA_MCP_VERSION=1.7.0 +ARG GITEA_MCP_SHA256=bbc9a7b462facd3c56b1558ee6054e91f2fca27a2878b5599afddcf57d446b8d +RUN curl -fsSL -o /tmp/gitea-mcp.tar.gz \ + "https://gitea.com/gitea/gitea-mcp/releases/download/v${GITEA_MCP_VERSION}/gitea-mcp_Linux_x86_64.tar.gz" \ + && echo "${GITEA_MCP_SHA256} /tmp/gitea-mcp.tar.gz" | sha256sum -c - \ + && tar -xzf /tmp/gitea-mcp.tar.gz -C /usr/local/bin gitea-mcp \ + && chmod +x /usr/local/bin/gitea-mcp \ + && rm -f /tmp/gitea-mcp.tar.gz \ + && gitea-mcp --version + # ── The agent user ─────────────────────────────────────────────────────────── # NOT root, and not negotiable: Claude Code refuses --dangerously-skip-permissions # when it has root privileges. uid/gid 1000 matches the host account so files diff --git a/docker/decoder/entrypoint.sh b/docker/decoder/entrypoint.sh index f9d4e39b..525de7f7 100755 --- a/docker/decoder/entrypoint.sh +++ b/docker/decoder/entrypoint.sh @@ -208,6 +208,54 @@ python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" "$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true chmod 600 "$HOME/.claude.json" 2>/dev/null || true +# ── The Gitea MCP server ───────────────────────────────────────────────────── +# Registered at USER scope rather than from a committed `.mcp.json`: the token +# differs per agent and none of it belongs in git. +# +# 🔴 THE TOKEN IS PASSED AS A PATH, NOT A VALUE. `-e GITEA_ACCESS_TOKEN=$(cat +# …)` would write the secret in cleartext into ~/.claude.json, where it is read +# by every session in this container and lands in any copy of that file. +# `GITEA_ACCESS_TOKEN_FILE` (gitea-mcp ≥ 1.7.0) leaves the token in its +# read-only mount and lets the server read it itself. +# +# Re-registered on every start, remove-then-add: `claude mcp add` refuses a name +# that already exists, and ~/.claude.json is re-seeded above — neither ordering +# survives alone. +GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.sylph-gitea-token}" +GITEA_HOST_URL="${SYLPH_GITEA_HOST:-https://git.mc02.dev}" +# Which tools this agent gets. Deliberately not all of them: +# +# * `pull_request_review_write` IS ABSENT, and that is the load-bearing one. +# Gitea will not let an author approve its own pull request — but the moment +# the two agents are separate people, nothing stops them approving each +# OTHER's and satisfying `required_approvals` between themselves with no +# human involved. Separate identities open that hole; withholding the tool +# closes it here, and the approvals whitelist on `main` closes it there. +# * the file / branch / repo WRITE tools are absent: a change reaches `main` +# as a reviewable commit through git, or it does not reach it. +# +# `pull_request_write` bundles `merge` into one tool and cannot be split, so +# merging stays blocked where the agent cannot reach it — the merge whitelist in +# branch protection. This list is defence in depth BEHIND that, never instead. +GITEA_MCP_TOOLS="${SYLPH_GITEA_TOOLS:-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}" +if [ ! -s "$GITEA_TOKEN_FILE" ]; then + echo "[entrypoint] no Gitea token at $GITEA_TOKEN_FILE — MCP not registered." + echo "[entrypoint] This agent cannot read its notifications or open a pull" + echo "[entrypoint] request, which is most of what its brief asks of it." +elif ! command -v gitea-mcp >/dev/null 2>&1; then + echo "[entrypoint] gitea-mcp is not in this image — rebuild it." >&2 +else + claude mcp remove gitea -s user >/dev/null 2>&1 || true + if claude mcp add -s user gitea \ + -e "GITEA_ACCESS_TOKEN_FILE=$GITEA_TOKEN_FILE" \ + -- gitea-mcp -t stdio -H "$GITEA_HOST_URL" -O "$GITEA_MCP_TOOLS" >/dev/null 2>&1; then + echo "[entrypoint] gitea MCP registered against $GITEA_HOST_URL" + else + echo "[entrypoint] gitea MCP registration FAILED — the agent has no issues," >&2 + echo "[entrypoint] no pull requests and no notifications." >&2 + fi +fi + # ── Claude Code ────────────────────────────────────────────────────────────── if [ "${SYLPH_AUTONOMOUS:-0}" = "1" ]; then # Drop the image's default CMD first, or `claude` is handed the literal string diff --git a/docker/decoder/sylph-decoder b/docker/decoder/sylph-decoder index 29689f65..7c49e9f2 100755 --- a/docker/decoder/sylph-decoder +++ b/docker/decoder/sylph-decoder @@ -23,6 +23,8 @@ # SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent) # SYLPH_GIT_CREDENTIALS file with `https://:@host` for push-work # (default: $HOME/.sylph-git-credentials) +# SYLPH_GITEA_TOKEN this agent's own Gitea token file +# (default: $HOME/.sylph-gitea-token-decoder) # SYLPH_LOOP_INTERVAL fixed loop cadence, e.g. 30m (default: 45m) # SYLPH_CPUS / SYLPH_MEM_GB override the computed half set -euo pipefail @@ -229,6 +231,29 @@ docker_args() { echo " or point SYLPH_GIT_CREDENTIALS elsewhere." >&2 fi + # ── Gitea ── + # This agent's OWN token, for its OWN Gitea account — not the push credential + # and not the human's. Three reasons it is separate: `~/.sylph-git-credentials` + # is scoped `write:repository` and every issue endpoint REFUSES it; a pull + # request the agent authored is one a human can approve, which is the entire + # review gate; and revoking one agent then touches neither the other nor you. + # + # Mounted read-only and passed to the MCP server BY PATH — see the entrypoint + # for why the value must not go through the environment. + # Inert until the file exists: the container still runs, with no issues. + GITEATOK="${SYLPH_GITEA_TOKEN:-$HOME/.sylph-gitea-token-decoder}" + if [ -f "$GITEATOK" ]; then + _out+=( + -v "$GITEATOK:/sylph-home/re/.sylph-gitea-token:ro" + -e "GITEA_TOKEN_FILE=/sylph-home/re/.sylph-gitea-token" + ) + else + echo "==> NOTE: no Gitea token at $GITEATOK — this agent cannot read its" >&2 + echo " notifications, open an issue or open a pull request. Generate one" >&2 + echo " while logged in AS sylph-decoder: Settings -> Applications, scopes" >&2 + echo " write:repository, write:issue, write:notification, read:user." >&2 + fi + [ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY") [ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN") [ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE") diff --git a/docker/port/Dockerfile b/docker/port/Dockerfile index e0893494..e89e945d 100644 --- a/docker/port/Dockerfile +++ b/docker/port/Dockerfile @@ -62,6 +62,24 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && npm cache clean --force \ && rm -rf /var/lib/apt/lists/* +# ── gitea-mcp ──────────────────────────────────────────────────────────────── +# The agent's hands on issues, pull requests and notifications — Gitea's own MCP +# server, so there is no second store of truth to drift out of sync with the +# first. +# +# PINNED AND CHECKSUMMED, not "whatever is at that URL today": this binary is +# handed a token that can write to the repository. The checksum is the one +# published in `gitea-mcp_1.7.0_checksums.txt` for the Linux x86_64 asset. +ARG GITEA_MCP_VERSION=1.7.0 +ARG GITEA_MCP_SHA256=bbc9a7b462facd3c56b1558ee6054e91f2fca27a2878b5599afddcf57d446b8d +RUN curl -fsSL -o /tmp/gitea-mcp.tar.gz \ + "https://gitea.com/gitea/gitea-mcp/releases/download/v${GITEA_MCP_VERSION}/gitea-mcp_Linux_x86_64.tar.gz" \ + && echo "${GITEA_MCP_SHA256} /tmp/gitea-mcp.tar.gz" | sha256sum -c - \ + && tar -xzf /tmp/gitea-mcp.tar.gz -C /usr/local/bin gitea-mcp \ + && chmod +x /usr/local/bin/gitea-mcp \ + && rm -f /tmp/gitea-mcp.tar.gz \ + && gitea-mcp --version + # ── The agent user ─────────────────────────────────────────────────────────── # NOT root: Claude Code refuses --dangerously-skip-permissions with root # privileges. Ubuntu 24.04 ships its own `ubuntu` account at uid 1000, so the diff --git a/docker/port/entrypoint.sh b/docker/port/entrypoint.sh index c8c90ba0..ffebced2 100755 --- a/docker/port/entrypoint.sh +++ b/docker/port/entrypoint.sh @@ -87,6 +87,54 @@ python3 /usr/local/bin/seed-claude-config.py "$HOME/.claude.json" "$CLAUDE_VER" "$PWD" "${PROJECT_DIR:-/work}" "$HOME" || true chmod 600 "$HOME/.claude.json" 2>/dev/null || true +# ── The Gitea MCP server ───────────────────────────────────────────────────── +# Registered at USER scope rather than from a committed `.mcp.json`: the token +# differs per agent and none of it belongs in git. +# +# 🔴 THE TOKEN IS PASSED AS A PATH, NOT A VALUE. `-e GITEA_ACCESS_TOKEN=$(cat +# …)` would write the secret in cleartext into ~/.claude.json, where it is read +# by every session in this container and lands in any copy of that file. +# `GITEA_ACCESS_TOKEN_FILE` (gitea-mcp ≥ 1.7.0) leaves the token in its +# read-only mount and lets the server read it itself. +# +# Re-registered on every start, remove-then-add: `claude mcp add` refuses a name +# that already exists, and ~/.claude.json is re-seeded above — neither ordering +# survives alone. +GITEA_TOKEN_FILE="${GITEA_TOKEN_FILE:-$HOME/.sylph-gitea-token}" +GITEA_HOST_URL="${SYLPH_GITEA_HOST:-https://git.mc02.dev}" +# Which tools this agent gets. Deliberately not all of them: +# +# * `pull_request_review_write` IS ABSENT, and that is the load-bearing one. +# Gitea will not let an author approve its own pull request — but the moment +# the two agents are separate people, nothing stops them approving each +# OTHER's and satisfying `required_approvals` between themselves with no +# human involved. Separate identities open that hole; withholding the tool +# closes it here, and the approvals whitelist on `main` closes it there. +# * the file / branch / repo WRITE tools are absent: a change reaches `main` +# as a reviewable commit through git, or it does not reach it. +# +# `pull_request_write` bundles `merge` into one tool and cannot be split, so +# merging stays blocked where the agent cannot reach it — the merge whitelist in +# branch protection. This list is defence in depth BEHIND that, never instead. +GITEA_MCP_TOOLS="${SYLPH_GITEA_TOOLS:-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}" +if [ ! -s "$GITEA_TOKEN_FILE" ]; then + echo "[entrypoint] no Gitea token at $GITEA_TOKEN_FILE — MCP not registered." + echo "[entrypoint] This agent cannot read its notifications or open a pull" + echo "[entrypoint] request, which is most of what its brief asks of it." +elif ! command -v gitea-mcp >/dev/null 2>&1; then + echo "[entrypoint] gitea-mcp is not in this image — rebuild it." >&2 +else + claude mcp remove gitea -s user >/dev/null 2>&1 || true + if claude mcp add -s user gitea \ + -e "GITEA_ACCESS_TOKEN_FILE=$GITEA_TOKEN_FILE" \ + -- gitea-mcp -t stdio -H "$GITEA_HOST_URL" -O "$GITEA_MCP_TOOLS" >/dev/null 2>&1; then + echo "[entrypoint] gitea MCP registered against $GITEA_HOST_URL" + else + echo "[entrypoint] gitea MCP registration FAILED — the agent has no issues," >&2 + echo "[entrypoint] no pull requests and no notifications." >&2 + fi +fi + # ── The repository, cloned into THIS AGENT'S OWN volume ───────────────────── # Not a bind mount of a human's working tree. That arrangement bit this project # three times: an agent's `git config --local` captured a human's commits, a diff --git a/docker/port/sylph-port b/docker/port/sylph-port index 246c5101..f1f49065 100755 --- a/docker/port/sylph-port +++ b/docker/port/sylph-port @@ -14,6 +14,8 @@ # SYLPH_PORT_REPO repo to mount at /work (default: this script's parent) # SYLPH_DISC extracted disc root # SYLPH_GIT_CREDENTIALS file with `https://:@host` for push-work +# SYLPH_GITEA_TOKEN this agent's own Gitea token file +# (default: $HOME/.sylph-gitea-token-port) # SYLPH_LOOP_INTERVAL fixed loop cadence (default 45m) # # ── Two hard-won constraints ──────────────────────────────────────────────── @@ -133,6 +135,29 @@ docker_args() { echo " so its work dies with the container." >&2 fi + # ── Gitea ── + # This agent's OWN token, for its OWN Gitea account — not the push credential + # and not the human's. Three reasons it is separate: `~/.sylph-git-credentials` + # is scoped `write:repository` and every issue endpoint REFUSES it; a pull + # request the agent authored is one a human can approve, which is the entire + # review gate; and revoking one agent then touches neither the other nor you. + # + # Mounted read-only and passed to the MCP server BY PATH — see the entrypoint + # for why the value must not go through the environment. + # Inert until the file exists: the container still runs, with no issues. + local giteatok="${SYLPH_GITEA_TOKEN:-$HOME/.sylph-gitea-token-port}" + if [ -f "$giteatok" ]; then + _out+=( + -v "$giteatok:/sylph-home/port/.sylph-gitea-token:ro" + -e "GITEA_TOKEN_FILE=/sylph-home/port/.sylph-gitea-token" + ) + else + echo "==> NOTE: no Gitea token at $giteatok — this agent cannot read its" >&2 + echo " notifications, open an issue or open a pull request. Generate one" >&2 + echo " while logged in AS sylph-port: Settings -> Applications, scopes" >&2 + echo " write:repository, write:issue, write:notification, read:user." >&2 + fi + [ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY") # ── GPU ── diff --git a/docs/agents/GITEA-SETUP.md b/docs/agents/GITEA-SETUP.md index d28a4c5c..3b88696b 100644 --- a/docs/agents/GITEA-SETUP.md +++ b/docs/agents/GITEA-SETUP.md @@ -13,8 +13,10 @@ Steps are marked **👤 you** (a decision or a credential only you can make) or * ✅ 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, no API tokens, no labels, no - branch protection, no MCP. +* ❌ 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. @@ -65,6 +67,26 @@ Repo → Settings → Branches → *Protected Branches* → add rule for `main`: | 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 > @@ -74,10 +96,14 @@ Repo → Settings → Branches → *Protected Branches* → add rule for `main`: > 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. Close the PR, delete the branch. +> 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 offers a Merge button, stop — the rest of this runbook assumes it -> does not. +> If step 2 or step 4 offers a Merge button, stop — the rest of this runbook +> assumes neither does. --- @@ -132,34 +158,72 @@ filter turns out to be insufficient. ## Phase 5 · The MCP server 🤖 -`gitea-mcp` **v1.7.0** — `gitea-mcp_Linux_x86_64.tar.gz` from -`https://gitea.com/gitea/gitea-mcp/releases`. Confirmed flags: `-t stdio`, -`-H `, token via `GITEA_ACCESS_TOKEN`. +**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 `, `-O/--tools`, `-S/--scope`, +`-r/--read-only` and `GITEA_ACCESS_TOKEN_FILE` are confirmed, not assumed. -Three edits per image, which I make: +Three edits per image, made: -1. **`Dockerfile`** — fetch and unpack the release binary to `/usr/local/bin`, - pinned to v1.7.0 with a checksum. -2. **`entrypoint.sh`** — register it for that agent's own identity: +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=$(cat "$GITEA_TOKEN_FILE")" \ - -- gitea-mcp -t stdio -H https://git.mc02.dev + 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" ``` - User scope, not a committed `.mcp.json` — the token differs per agent and none - of it belongs in git. -3. **`sylph-decoder` / `sylph-port`** — mount the matching token file read-only. + 🔴 **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. -Then rebuild both images. ⚠️ `CARGO_BUILD_JOBS=4` and limited `-j`; a -full-parallel build has OOM-crashed this box. +**👤 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. -📌 **Worth trying, unverified:** the server takes `--tools` / `GITEA_TOOLS` to -filter which tools it exposes. If a merge tool can be excluded by name, that is -defence in depth behind Phase 2 — belt *and* braces. I have not confirmed the -tool names, so this is an experiment at install time, **not** a substitute for -branch protection. +```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. +> 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. --- @@ -222,9 +286,16 @@ 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. -* **the `--tools` filter names** (Phase 5) — an experiment, not a control. + 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. -* **the exact Gitea version** — the API was unreachable from my sandbox on the - last three attempts. Every screen named here has been stable for many - releases, but if a menu is not where I say it is, that is why. + +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. diff --git a/docs/agents/WORKFLOW-gitea.md b/docs/agents/WORKFLOW-gitea.md index e0ae0911..0234eaf8 100644 --- a/docs/agents/WORKFLOW-gitea.md +++ b/docs/agents/WORKFLOW-gitea.md @@ -67,10 +67,18 @@ One PR per item, closing its issue: too big to be an item. The discipline stops depending on an agent's judgement. 🔴 **Agents must not merge their own pull requests.** The MCP's -`pull_request_write` includes `merge`, so this cannot be left to instruction — -it goes in **branch protection on `main`**, requiring review. Same principle that -fixed the build-jobs cap: policy belongs where the agent cannot reach it, not in -a document asking it not to. +`pull_request_write` includes `merge` and the tool cannot be split, so this +cannot be left to instruction — it goes in **branch protection on `main`**. Same +principle that fixed the build-jobs cap: policy belongs where the agent cannot +reach it, not in a document asking it not to. + +⚠️ **"Requiring review" is not the rule that does it.** Gitea stops an author +approving their own pull request; it does not stop *the other agent* approving +it, and it never blocked merging in the first place — `Enable Push: off` blocks +pushes. The rule that holds is the pair of whitelists: **approvals whitelisted to +the human**, so an agent's approval does not count, and **merges whitelisted to +the human**, so an approved PR is still merged by a person. See +[`GITEA-SETUP.md`](GITEA-SETUP.md) Phase 2. ### 🔴 The wiki is NOT for the RE corpus