forge: expose forge-side branch protection and check the local mirror against it #122

Open
opened 2026-07-28 01:04:23 +01:00 by allod-agent · 0 comments
Member

Make the forge-side branch protection rules readable from the agent side, and check the local protected-branches file against them, so the git rails derive from the authoritative fact instead of duplicating it. Today the forge rule is the wall that actually rejects a push, protected-branches is a hand-maintained mirror of it, and nothing detects when the two disagree.

Primary goals:

  • Read effective protection — one forge command reports whether a repo's branch is protected and whether the current token may push it, using the endpoint a non-admin token can actually read.
  • Detect mirror drift — compare every protected-branches entry against the forge and fail loud on disagreement in either direction: a locally-protected branch the forge does not protect, and a forge-protected branch the local file omits.
  • Name the authority — the forge rule is the source of truth and the local file is a derived cache, stated in the tool's own output rather than only in memory prose.
  • Stay read-only — no command changes protection. Enumerating or editing rules needs repo admin, which agents do not have, and changing them stays a human act at the forge.

Current state

forge has no branch-protection command. The obvious endpoint is closed to agents: GET /api/v1/repos/{owner}/{repo}/branch_protections returns 403 with user should be an owner or a collaborator with admin write of a repository, because the agent token carries push but not admin.

GET /api/v1/repos/{owner}/{repo}/branches/{branch} is readable with that same token and carries what the rails need — protected and user_can_push. Verified across the org: the eight repos listed in ~/.config/git/protected-branches all return protected: true, user_can_push: false, and allod/memory and allod/strategy return false and true. That is an exact match with the local file today, which is precisely why drift would be invisible until a push failed or a rail silently stopped firing.

The plumbing already exists. forge:54 resolves FORGE_URL, and the shared request helper at forge:244 attaches the token through curl --config so it never reaches child argv, per the token-handling rule in allod/memory security-practices.md.

Suggested shape

forge repo protection [<owner/repo>] prints the effective status for the repo's default branch, defaulting to the repo containing $PWD like the other forge commands. forge repo protection --check walks every entry in protected-branches, compares each against the forge, and exits non-zero listing any disagreement.

The alternative siting for the check half is a workspace/ script alongside the other cross-repo tools, since protected-branches is consumed by allod and git-hooks/protected-refs-policy rather than by forge. Default to the forge subcommand because the comparison needs the API client and the authority being checked is the forge's; move it only if the check grows workspace-wide concerns.

Relationship to the rails

This does not replace the path-vs-remote-identity work in allod/tools#112. That issue fixes how the rails look a repo up; this one checks that what they look up still matches the forge. They compose: allod/tools#112's remote-identity key is what makes a per-repo comparison well-defined, since it is the same owner/repo derivation the check needs.

Scope

In: a new read-only forge subcommand, its help text, and tests against a stubbed API. Out: any command that changes protection rules; the branch_protections admin endpoint; the remote-identity lookup change in allod/tools#112; and changing the contents of protected-branches, which stays a human edit.

Make the forge-side branch protection rules readable from the agent side, and check the local `protected-branches` file against them, so the git rails derive from the authoritative fact instead of duplicating it. Today the forge rule is the wall that actually rejects a push, `protected-branches` is a hand-maintained mirror of it, and nothing detects when the two disagree. Primary goals: - **Read effective protection** — one `forge` command reports whether a repo's branch is protected and whether the current token may push it, using the endpoint a non-admin token can actually read. - **Detect mirror drift** — compare every `protected-branches` entry against the forge and fail loud on disagreement in either direction: a locally-protected branch the forge does not protect, and a forge-protected branch the local file omits. - **Name the authority** — the forge rule is the source of truth and the local file is a derived cache, stated in the tool's own output rather than only in memory prose. - **Stay read-only** — no command changes protection. Enumerating or editing rules needs repo admin, which agents do not have, and changing them stays a human act at the forge. ### Current state `forge` has no branch-protection command. The obvious endpoint is closed to agents: `GET /api/v1/repos/{owner}/{repo}/branch_protections` returns 403 with `user should be an owner or a collaborator with admin write of a repository`, because the agent token carries `push` but not `admin`. `GET /api/v1/repos/{owner}/{repo}/branches/{branch}` is readable with that same token and carries what the rails need — `protected` and `user_can_push`. Verified across the org: the eight repos listed in `~/.config/git/protected-branches` all return `protected: true, user_can_push: false`, and `allod/memory` and `allod/strategy` return `false` and `true`. That is an exact match with the local file today, which is precisely why drift would be invisible until a push failed or a rail silently stopped firing. The plumbing already exists. `forge:54` resolves `FORGE_URL`, and the shared request helper at `forge:244` attaches the token through `curl --config` so it never reaches child argv, per the token-handling rule in `allod/memory` `security-practices.md`. ### Suggested shape `forge repo protection [<owner/repo>]` prints the effective status for the repo's default branch, defaulting to the repo containing `$PWD` like the other `forge` commands. `forge repo protection --check` walks every entry in `protected-branches`, compares each against the forge, and exits non-zero listing any disagreement. The alternative siting for the check half is a `workspace/` script alongside the other cross-repo tools, since `protected-branches` is consumed by `allod` and `git-hooks/protected-refs-policy` rather than by `forge`. Default to the `forge` subcommand because the comparison needs the API client and the authority being checked is the forge's; move it only if the check grows workspace-wide concerns. ### Relationship to the rails This does not replace the path-vs-remote-identity work in allod/tools#112. That issue fixes how the rails look a repo up; this one checks that what they look up still matches the forge. They compose: allod/tools#112's remote-identity key is what makes a per-repo comparison well-defined, since it is the same `owner/repo` derivation the check needs. ### Scope In: a new read-only `forge` subcommand, its help text, and tests against a stubbed API. Out: any command that changes protection rules; the `branch_protections` admin endpoint; the remote-identity lookup change in allod/tools#112; and changing the contents of `protected-branches`, which stays a human edit.
Sign in to join this conversation.
No description provided.