fix(forge): config-repo main is fast-forward-only (no auto force-push)

Per operator directive: a silent automatic force-push is a bug. The
config-repo merge path already never force-pushes (run_merge_config_pr
lands via ff_push_to_main, a non-force git push). So set the branch
protection's enable_force_push to false — main only ever advances by
fast-forward.

The legacy push_config mirror does force-push (it re-points the status
tags and rewinds main on a failed-build rollback); the protection now
rejects those non-ff updates, so the mirror runs best-effort until the
agent-opened PR-merge flow retires it. Docs + comments updated to match.
This commit is contained in:
atlas 2026-06-23 22:24:23 +02:00 committed by mara
commit 5caec9c1a9
2 changed files with 18 additions and 16 deletions

View file

@ -56,10 +56,11 @@ Two things live in the `agent-configs` Forgejo organization:
config-change branches and (once #1838 P2 lands) open config PRs — but
`main` is branch-protected core-only: only hive-c0re's verify-and-ff-push
merge handler lands on `main`, an operator-team approval is required, and
the agent can neither push `main` directly nor self-merge. During the
transition c0re still force-mirrors the agent's applied config repo here
on each `↻ R3BU1LD` (the `enable_force_push` allowance), until the PR
flow replaces that. Repos stay private, so an agent can't read another
the agent can neither push `main` directly nor self-merge. `main` is
fast-forward-only — no auto force-push (the merge handler's ff push lands
fine; the legacy `push_config` mirror, which force-pushes to re-point status
tags and rewind on rollback, runs best-effort until the PR flow retires it).
Repos stay private, so an agent can't read another
agent's config. (Agents remain read-only collaborators on `core/meta`.)
- The dashboard links each container's "config" anchor to this
config repo, so operators can click straight from the SW4RM tab into

View file

@ -37,8 +37,9 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
/// branch-protected core-only, so only hive-c0re's verify-and-ff-push merge
/// handler lands on it (operator approval required; the agent can't push
/// `main` or self-merge). The repos remain private, so an agent still can't
/// reach *another* agent's config. During the transition `push_config` also
/// force-mirrors `applied → main` here until the PR-merge flow replaces it.
/// reach *another* agent's config. `main` is fast-forward-only (no auto
/// force-push); the legacy `push_config` mirror runs best-effort until the
/// PR-merge flow retires it.
const CONFIG_ORG: &str = "agent-configs";
/// Forgejo org hosting the operator-curated shared docs/skills repo
/// that every agent gets read-only access to. Agents use it as a
@ -708,7 +709,7 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> {
// Agent = write collaborator: it can push config-PR branches + open PRs,
// but the branch protection below keeps it off `main` directly.
add_collaborator(CONFIG_ORG, name, name, "write", &token).await?;
// Protect `main` core-only (+ transitional force-push for push_config).
// Protect `main` core-only, fast-forward-only (no auto force-push).
apply_config_repo_branch_protection(name, &token).await
}
@ -997,20 +998,20 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()>
/// (`run_merge_config_pr`). The agent can never push `main` directly.
/// - **operator-team approval is required** to merge, and the author (not in
/// the team) cannot self-approve.
/// - **`enable_force_push` is a TRANSITIONAL allowance**: `push_config` still
/// force-mirrors `applied → main` until the agent-opened PR-merge flow
/// replaces it, and a protected branch otherwise rejects force-push.
/// Forgejo's force-push allowlist is deploy-keys-only (no per-user list), so
/// this is a plain `enable_force_push` toggle — only `core` is in the
/// push-whitelist, so only `core` can force-push anyway. When `push_config`
/// is retired, flip this to `false`; keep `core` in the push-whitelist so
/// `ff_push_to_main` can still land fast-forwards.
/// - **`enable_force_push` is `false`** — `main` only ever advances by
/// fast-forward. The merge handler's `ff_push_to_main` is already a
/// non-force push, so it lands fine. The legacy `push_config` mirror DOES
/// force-push (it re-points status tags and rewinds `main` on a failed-build
/// rollback), so the protection now rejects those non-ff updates — that
/// mirror runs best-effort until the agent-opened PR-merge flow retires it.
/// (Auto force-push is intentionally not allowed: per operator directive a
/// silent force-push is a bug, not a feature.)
///
/// Idempotent: an existing rule for the branch (200/409/422) is success.
async fn apply_config_repo_branch_protection(repo: &str, token: &str) -> Result<()> {
let url = format!("{FORGE_HTTP}/api/v1/repos/{CONFIG_ORG}/{repo}/branch_protections");
let body = format!(
r#"{{"branch_name":"main","enable_push_whitelist":true,"push_whitelist_usernames":["core"],"enable_merge_whitelist":true,"merge_whitelist_usernames":["core"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true,"allow_manual_merge":true,"enable_force_push":true}}"#
r#"{{"branch_name":"main","enable_push_whitelist":true,"push_whitelist_usernames":["core"],"enable_merge_whitelist":true,"merge_whitelist_usernames":["core"],"enable_approvals_whitelist":true,"approvals_whitelist_teams":["{OPERATORS_TEAM}"],"required_approvals":1,"block_on_official_review_requests":true,"allow_manual_merge":true,"enable_force_push":false}}"#
);
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
match status.as_u16() {