Compare commits

...
Author SHA1 Message Date
atlas
5caec9c1a9 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.
2026-06-23 22:37:27 +02:00
atlas
60da6c14b4 fix(forge): reword tracker-tag references in doc comments to prose
Address argus review on the config-repo PR surface: 6 issue-number tags
appeared in Rust doc comments in forge.rs; the tracker-tag lint matches
${'#'}NNN in code comments (markdown docs are exempt, doc comments are not).
Reword to prose, no behavior change.
2026-06-23 22:37:27 +02:00
atlas
558552ec54 docs(forge): s/mirror/config repo/ in dashboard anchor bullet for consistency
Address iris review nit on #1948: the bullet above now reframes the
agent-configs entry as a config repo, so the dashboard-anchor bullet
should match. Pure wording, no behavior change.
2026-06-23 22:37:27 +02:00
atlas
2fe79aaef5 feat(#1787): make agent-configs/<n> the agent-editable, PR-merge config surface
Wires the per-agent config repo as the editable PR surface the #1838
merge handler (run_merge_config_pr) consumes, without yet retiring the
push_config force-mirror (that waits for #1838 P2 — the agent-opens-PR
MCP surface — so config changes keep working through the transition).

ensure_config_repo now, after creating agent-configs/<name>:
- adds the agent as a WRITE collaborator (can push config-change branches
  + open PRs);
- branch-protects main core-only via apply_config_repo_branch_protection:
  push + merge whitelists are core-only (only hive-c0re lands on main, via
  its verify-and-ff-push handler), operator-team approval required, the
  agent can't push main directly or self-merge.

The protection sets enable_force_push=true as a TRANSITIONAL allowance so
push_config's applied->main force-mirror keeps working until P2 retires it
(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. At P2, flip it to false and keep core in the
push-whitelist so ff_push_to_main still lands.

Agent code repos (AGENTS_ORG) are intentionally untouched — their merge
flow has no auto-merge handler yet, so switching them to core-only-merge
would strand those PRs.

All steps idempotent (runs on every spawn + startup sweep). Updated the
CONFIG_ORG doc comment + docs/forge.md to drop the "mirror-only" framing.
2026-06-23 22:37:27 +02:00
2 changed files with 73 additions and 15 deletions

View file

@ -51,12 +51,19 @@ read it without touching c0re's host-side credential store.
Two things live in the `agent-configs` Forgejo organization:
- A mirror repo per agent (`agent-configs/<name>`) — c0re pushes the
agent's applied config repo on each `↻ R3BU1LD`. Agents are
read-only collaborators on `core/meta` (the hive-c0re-owned meta
flake) so they can fetch but never push.
- A config repo per agent (`agent-configs/<name>`). As of #1787 the
agent is a **write collaborator on its own** repo — it can push
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. `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
mirror, so operators can click straight from the SW4RM tab into
config repo, so operators can click straight from the SW4RM tab into
the rendered repo without an extra `git` step.
The `hive-forge` CLI (separate workspace crate, see

View file

@ -30,12 +30,16 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
// helpers in `hive_sh4re::assets`. The `agent-configs.png` is
// rendered from its SVG during the `hyperhive-assets` derivation's
// build.
/// Forgejo org grouping every agent's applied config repo. Core is a
/// site admin and reads + writes every repo here; agents are NOT
/// members and the repos are private, so no agent — not even the one
/// a repo describes — can reach a config repo through the forge. The
/// applied repos stay hive-c0re-owned on disk; this org is just a
/// mirror target core pushes to.
/// Forgejo org grouping every agent's config repo. Core is a site admin
/// and reads + writes every repo here. As of the agent-config-PR flow each
/// agent is a **write collaborator on its own** `agent-configs/<name>` repo —
/// the editable PR surface it pushes config-change branches to — but `main` is
/// 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. `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
@ -688,9 +692,12 @@ pub async fn push_meta(dir: &Path) -> Result<()> {
}
/// Ensure the `agent-configs/<name>` repo exists so the first
/// `push_config` doesn't 404. No-op when the forge isn't running or
/// the core token isn't minted yet. Safe to call on every spawn and
/// on every startup.
/// `push_config` doesn't 404, and wire it as the agent-editable PR surface:
/// the agent is a **write** collaborator (can push feature branches +
/// open config PRs) and `main` is branch-protected core-only (only hive-c0re's
/// merge handler lands on it; operator approval required). No-op when the forge
/// isn't running or the core token isn't minted yet. Safe to call on every
/// spawn and on every startup (all steps idempotent).
pub async fn ensure_config_repo(name: &str) -> Result<()> {
if !is_present().await {
return Ok(());
@ -698,7 +705,12 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> {
let Some(token) = core_token() else {
return Ok(());
};
ensure_org_repo(CONFIG_ORG, name, &token).await
ensure_org_repo(CONFIG_ORG, name, &token).await?;
// 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, fast-forward-only (no auto force-push).
apply_config_repo_branch_protection(name, &token).await
}
/// Ensure the `internal/docs` repo exists. Called once at startup
@ -978,6 +990,45 @@ async fn apply_operator_branch_protection(repo: &str, token: &str) -> Result<()>
}
}
/// Apply branch protection to an `agent-configs/<name>` repo's `main` so it
/// can serve as the agent-editable, PR-merge config surface:
/// - **push + merge whitelists are `core`-only** — the agent (a write
/// collaborator) can push feature branches and open config PRs, but only
/// hive-c0re lands on `main`, via its verify-and-ff-push merge handler
/// (`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 `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":false}}"#
);
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
match status.as_u16() {
201 => {
tracing::info!(%repo, "forge: applied config-repo branch protection");
Ok(())
}
200 | 409 | 422 => {
tracing::debug!(%repo, "forge: config-repo branch protection already present");
Ok(())
}
other => {
anyhow::bail!("POST {CONFIG_ORG}/{repo}/branch_protections returned HTTP {other}")
}
}
}
/// Create a repo for `agent` in the c0re-owned [`AGENTS_ORG`] and wire the
/// perms: the org owns it (perms stay c0re-managed), the agent is added
/// as a **write** collaborator (not owner — can push + open PRs but can't