Follow-up to #343, which fixed three real problems in the auto-merge job but left one hole that code cannot close.
The hole
For pull_request, GitHub runs the workflow from the PR's merged tree. So a PR that edits .github/workflows/ci.yml runs its own edited version — including the self-modification guard added in #343. A PR can delete the guard and the deleted guard is the one that would have caught it.
Everything else in #343 (base-pinned policy, var-only enable) holds. This is specifically about ci.yml policing itself.
Realistic threat here isn't an attacker. Fork PRs can't reach BOT_APP_PRIVATE_KEY, so agent-setup fails and the job can't act. It's our own agents opening branch PRs — an agent asked to "fix CI" could plausibly rewrite the guard without malice, and the only remaining gate is the agentic reviewer's APPROVED, which is another LLM.
Why CODEOWNERS is the wrong fix here
The obvious answer is CODEOWNERS + "Require review from Code Owners" on .github/**. It does not work for this repo.
agentiknet is the only collaborator, and GitHub does not allow approving your own PR. Requiring code-owner review would mean no .github/** change could ever be merged except by admin bypass — which makes the control theatre, since bypass is exactly what would be used every time.
A two-person control needs two people. Noting this explicitly because it's the first thing anyone (including me) reaches for.
What would actually work: a required status check
Rulesets can require a named status check to pass. A required check that never reports leaves the PR blocked. So:
- Split the guard into its own job with a stable name, e.g.
policy-guard, that always runs on pull_request.
- Mark
policy-guard a required check in the ruleset protecting main.
- A PR that deletes or renames the job → the required check never reports → merge blocked, no human needed.
The ruleset lives in repo settings, out of a PR's reach. This is the same principle as #343's vars.AGENTFLOW_AUTOMERGE: the thing that enforces must live where the PR cannot edit it.
Caveat to verify when implementing: confirm the blocked-on-missing-check behaviour against admin bypass, and decide whether agentiknet should retain bypass (probably yes for emergencies, but then it's advisory for humans and binding for bots — which is precisely the desired split).
Also worth deciding
vars.AGENTFLOW_AUTOMERGE=true is currently set. After #343 it is the only thing that enables auto-merge. Worth a conscious decision on whether LLM-approval-driven auto-merge is wanted at all, rather than leaving it on because it was on. Turning the var off is a one-click, zero-code kill switch.
Context
Follow-up to #343, which fixed three real problems in the auto-merge job but left one hole that code cannot close.
The hole
For
pull_request, GitHub runs the workflow from the PR's merged tree. So a PR that edits.github/workflows/ci.ymlruns its own edited version — including the self-modification guard added in #343. A PR can delete the guard and the deleted guard is the one that would have caught it.Everything else in #343 (base-pinned policy, var-only enable) holds. This is specifically about
ci.ymlpolicing itself.Realistic threat here isn't an attacker. Fork PRs can't reach
BOT_APP_PRIVATE_KEY, soagent-setupfails and the job can't act. It's our own agents opening branch PRs — an agent asked to "fix CI" could plausibly rewrite the guard without malice, and the only remaining gate is the agentic reviewer'sAPPROVED, which is another LLM.Why CODEOWNERS is the wrong fix here
The obvious answer is CODEOWNERS + "Require review from Code Owners" on
.github/**. It does not work for this repo.agentiknetis the only collaborator, and GitHub does not allow approving your own PR. Requiring code-owner review would mean no.github/**change could ever be merged except by admin bypass — which makes the control theatre, since bypass is exactly what would be used every time.A two-person control needs two people. Noting this explicitly because it's the first thing anyone (including me) reaches for.
What would actually work: a required status check
Rulesets can require a named status check to pass. A required check that never reports leaves the PR blocked. So:
policy-guard, that always runs onpull_request.policy-guarda required check in the ruleset protectingmain.The ruleset lives in repo settings, out of a PR's reach. This is the same principle as #343's
vars.AGENTFLOW_AUTOMERGE: the thing that enforces must live where the PR cannot edit it.Caveat to verify when implementing: confirm the blocked-on-missing-check behaviour against admin bypass, and decide whether
agentiknetshould retain bypass (probably yes for emergencies, but then it's advisory for humans and binding for bots — which is precisely the desired split).Also worth deciding
vars.AGENTFLOW_AUTOMERGE=trueis currently set. After #343 it is the only thing that enables auto-merge. Worth a conscious decision on whether LLM-approval-driven auto-merge is wanted at all, rather than leaving it on because it was on. Turning the var off is a one-click, zero-code kill switch.Context