Compare commits

..
2 changed files with 15 additions and 73 deletions

View file

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

View file

@ -30,16 +30,12 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
// helpers in `hive_sh4re::assets`. The `agent-configs.png` is // helpers in `hive_sh4re::assets`. The `agent-configs.png` is
// rendered from its SVG during the `hyperhive-assets` derivation's // rendered from its SVG during the `hyperhive-assets` derivation's
// build. // build.
/// Forgejo org grouping every agent's config repo. Core is a site admin /// Forgejo org grouping every agent's applied config repo. Core is a
/// and reads + writes every repo here. As of the agent-config-PR flow each /// site admin and reads + writes every repo here; agents are NOT
/// agent is a **write collaborator on its own** `agent-configs/<name>` repo — /// members and the repos are private, so no agent — not even the one
/// the editable PR surface it pushes config-change branches to — but `main` is /// a repo describes — can reach a config repo through the forge. The
/// branch-protected core-only, so only hive-c0re's verify-and-ff-push merge /// applied repos stay hive-c0re-owned on disk; this org is just a
/// handler lands on it (operator approval required; the agent can't push /// mirror target core pushes to.
/// `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"; const CONFIG_ORG: &str = "agent-configs";
/// Forgejo org hosting the operator-curated shared docs/skills repo /// Forgejo org hosting the operator-curated shared docs/skills repo
/// that every agent gets read-only access to. Agents use it as a /// that every agent gets read-only access to. Agents use it as a
@ -692,12 +688,9 @@ pub async fn push_meta(dir: &Path) -> Result<()> {
} }
/// Ensure the `agent-configs/<name>` repo exists so the first /// Ensure the `agent-configs/<name>` repo exists so the first
/// `push_config` doesn't 404, and wire it as the agent-editable PR surface: /// `push_config` doesn't 404. No-op when the forge isn't running or
/// the agent is a **write** collaborator (can push feature branches + /// the core token isn't minted yet. Safe to call on every spawn and
/// open config PRs) and `main` is branch-protected core-only (only hive-c0re's /// on every startup.
/// 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<()> { pub async fn ensure_config_repo(name: &str) -> Result<()> {
if !is_present().await { if !is_present().await {
return Ok(()); return Ok(());
@ -705,12 +698,7 @@ pub async fn ensure_config_repo(name: &str) -> Result<()> {
let Some(token) = core_token() else { let Some(token) = core_token() else {
return Ok(()); 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 /// Ensure the `internal/docs` repo exists. Called once at startup
@ -990,45 +978,6 @@ 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 /// 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 /// 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 /// as a **write** collaborator (not owner — can push + open PRs but can't