swarm-controller: own the swarm-wide forge objects; hive-c0re stops creating them
The orgs agent-configs/internal/agents (plus mirror owners), the operators team in agents and agent-configs, the pull-mirrors, internal/docs, internal/knowledge (public, README-seeded) and the agent-configs org avatar are one set per forge. hive-c0re ensured them in its boot sweep, as the core admin, and only on the hive co-located with the forge container. swarm-controller now reconciles them at start and every 5 minutes (forge/objects.rs: observe -> pure plan -> apply). A failed object logs a warn line plus a pass summary and is retried next tick. create_repo ensures the agent-configs org and its operators team first, so a config repo's merge gate never depends on the periodic pass having run. hive-c0re drops ensure_org, SEEDED_ORGS, ensure_mirrors/ensure_mirror_repo, ensure_operators_team, ensure_shared_docs_repo, ensure_knowledge_repo/ set_repo_public, seed_readme, ensure_config_org_avatar and the one-shot knowledge::remove_webhook cleanup, with their now-unused helpers. nix: the mirror list moves from the hive-c0re unit (HYPERHIVE_FORGE_MIRRORS) to the swarm-controller unit (SWARM_CONTROLLER_FORGE_MIRRORS), with an eval warning when mirrors are declared on a host that runs no controller. c0re.orgAvatarPng is renamed to deploy.swarm-controller.configOrgAvatarPng. Refs #3782
This commit is contained in:
parent
85ba45b2de
commit
20419ccd41
18 changed files with 1456 additions and 822 deletions
|
|
@ -1,7 +1,7 @@
|
|||
//! Optional Forgejo wiring — per-agent account alignment,
|
||||
//! config-repo mirroring, meta read-access grants. Also seeds
|
||||
//! `internal/docs` — a private repo every agent gets read-only
|
||||
//! collaborator access to for operator-curated shared content.
|
||||
//! config-repo mirroring, meta read-access grants, and each agent's
|
||||
//! read-only grant on `internal/docs` (the swarm-controller creates that
|
||||
//! repo, with the other swarm-wide orgs, teams and repos).
|
||||
//! No-op when `hive-forge` isn't running. Full design: `docs/integrations/forge.md`.
|
||||
|
||||
mod ci_runner;
|
||||
|
|
@ -17,9 +17,9 @@ pub use pr_merge::{
|
|||
};
|
||||
pub use reconcile::{reconcile_config_apply, reconcile_config_status};
|
||||
pub use repos::{
|
||||
clone_config_into_proposed, create_agent_repo, ensure_config_repo, ensure_knowledge_repo,
|
||||
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main,
|
||||
fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access,
|
||||
clone_config_into_proposed, create_agent_repo, ensure_config_repo, ensure_meta_remote,
|
||||
ensure_repo, fast_forward_applied_main, fetch_config_main_into_applied, meta_read_access,
|
||||
push_config, push_meta, shared_docs_access,
|
||||
};
|
||||
pub use users::core_token;
|
||||
|
||||
|
|
@ -30,10 +30,9 @@ use anyhow::{Context, Result};
|
|||
use forgejo_api::{Auth, Forgejo};
|
||||
use url::Url;
|
||||
|
||||
use repos::{ensure_mirrors, ensure_operators_team, ensure_org};
|
||||
use users::{
|
||||
ensure_config_org_avatar, ensure_core_avatar, ensure_core_user_and_token,
|
||||
ensure_repo_creation_disabled, ensure_user_email,
|
||||
ensure_core_avatar, ensure_core_user_and_token, ensure_repo_creation_disabled,
|
||||
ensure_user_email,
|
||||
};
|
||||
|
||||
const FORGE_CONTAINER: &str = "hive-forge";
|
||||
|
|
@ -107,36 +106,30 @@ pub(crate) const CONFIG_ORG: &str = "agent-configs";
|
|||
/// that every agent gets read-only access to. Agents use it as a
|
||||
/// common reference without the operator having to bake content into
|
||||
/// the system prompt or rely on `/shared`. Only the manager + operator
|
||||
/// (i.e. `core` user) can push.
|
||||
/// (i.e. `core` user) can push. The swarm-controller creates the org
|
||||
/// and the repo; this hive only grants its agents read access.
|
||||
const SHARED_ORG: &str = "internal";
|
||||
/// The shared docs repo inside `SHARED_ORG`. Cloneable by every agent
|
||||
/// at `{forge_http_base()}/internal/docs.git`.
|
||||
const SHARED_DOCS_REPO: &str = "docs";
|
||||
/// The hive-wide knowledge repo inside `SHARED_ORG`. Public — agents
|
||||
/// can fork it and open PRs without explicit collaborator grants.
|
||||
/// Bind-mounted read-only into every container at `/knowledge`.
|
||||
/// See `hive-c0re/src/workers/knowledge.rs`.
|
||||
const KNOWLEDGE_REPO: &str = crate::knowledge::REPO;
|
||||
/// Forgejo org that owns agent-created repos. Agents can't create
|
||||
/// repos with their own token (`max_repo_creation = 0`); instead hive-c0re
|
||||
/// creates them here and adds the requesting agent as a **write** member
|
||||
/// (not owner/admin). Because the org — not the agent — owns the repo,
|
||||
/// perms stay c0re-managed and branch protection (referencing
|
||||
/// [`OPERATORS_TEAM`]) can block the author from merging their own PR. This
|
||||
/// is the "agents namespace" repos land in by default.
|
||||
/// is the "agents namespace" repos land in by default. The swarm-controller
|
||||
/// ensures the org itself.
|
||||
const AGENTS_ORG: &str = "agents";
|
||||
/// Operator merge-gate team inside [`AGENTS_ORG`]. Provisioned **empty** by
|
||||
/// hive-c0re (so perms can be set before anyone joins); the operator adds
|
||||
/// herself via the forge UI / hivectl. Branch protection on agents-org repos
|
||||
/// references this team by name for the merge/approval whitelist, so the
|
||||
/// rule never hardcodes a specific reviewer agent (which may not exist).
|
||||
/// the swarm-controller (so perms can be set before anyone joins); the
|
||||
/// operator adds herself via the forge UI. Branch protection on agents-org
|
||||
/// repos references this team by name for the merge/approval whitelist, so
|
||||
/// the rule never hardcodes a specific reviewer agent (which may not exist).
|
||||
const OPERATORS_TEAM: &str = "operators";
|
||||
/// Forgejo orgs hive-c0re ensures on startup. The meta repo lives at
|
||||
/// `core/meta` (the `core` user's own namespace — no org needed).
|
||||
const SEEDED_ORGS: &[&str] = &[CONFIG_ORG, SHARED_ORG, AGENTS_ORG];
|
||||
|
||||
/// Leak `s` to get a `&'static str` warning `kind` for the small, bounded
|
||||
/// set of per-org boot warnings in [`ensure_all`] (one per seeded org, at
|
||||
/// set of boot warnings in [`ensure_all`] keyed by a runtime name (at
|
||||
/// most a handful per process). [`crate::warnings::set_boot_warning`]
|
||||
/// requires a `'static` kind so distinct orgs/repos don't clobber each
|
||||
/// other's banner entry; leaking a few short strings once per boot is
|
||||
|
|
@ -277,44 +270,15 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
|
|||
ok
|
||||
}
|
||||
|
||||
/// The `core_token.is_some()` half of [`ensure_all`]: orgs, teams, the meta
|
||||
/// repo, shared/knowledge repos, avatars, and CI runner registration — every
|
||||
/// step that needs an authenticated forge client. Split out purely to keep
|
||||
/// The `core_token.is_some()` half of [`ensure_all`]: the meta repo, the
|
||||
/// local knowledge clone, the core avatar, and CI runner registration —
|
||||
/// every step that needs an authenticated forge client. The swarm-wide
|
||||
/// objects (orgs, the `operators` team, mirrors, `internal/docs`,
|
||||
/// `internal/knowledge`, the `agent-configs` avatar) are the
|
||||
/// swarm-controller's, not this hive's. Split out purely to keep
|
||||
/// `ensure_all` under clippy's function-length limit; not meant to be called
|
||||
/// from anywhere else.
|
||||
async fn ensure_all_orgs_and_repos(token: &str) {
|
||||
for org in SEEDED_ORGS {
|
||||
if let Err(e) = ensure_org(org, token).await {
|
||||
tracing::warn!(%org, error = ?e, "forge: ensure_org failed");
|
||||
crate::warnings::set_boot_warning(
|
||||
static_kind(format!("forge_ensure_org_{org}")),
|
||||
"crit",
|
||||
format!("forge: org {org} provisioning failed: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Seed the operator-declared pull-mirrors (nix `forge.mirrors` +
|
||||
// the CI-auto `actions/checkout`, forwarded via the
|
||||
// `HYPERHIVE_FORGE_MIRRORS` env). Each ensures its own dest org, so
|
||||
// this is independent of the SEEDED_ORGS loop above.
|
||||
ensure_mirrors(token).await;
|
||||
// Provision the operator merge-gate team (empty) inside BOTH the
|
||||
// agents org and the agent-configs org so branch protection in each
|
||||
// can reference it before anyone joins. Gitea teams are org-scoped —
|
||||
// missing the agent-configs copy 422'd every config-repo protection
|
||||
// apply, leaving those repos unprotected and letting operator-merged
|
||||
// config PRs bypass the deploy pipeline. The operator adds herself as
|
||||
// a member out-of-band.
|
||||
for org in [AGENTS_ORG, CONFIG_ORG] {
|
||||
if let Err(e) = ensure_operators_team(org, token).await {
|
||||
tracing::warn!(%org, error = ?e, "forge: ensure_operators_team failed");
|
||||
crate::warnings::set_boot_warning(
|
||||
static_kind(format!("forge_ensure_operators_team_{org}")),
|
||||
"crit",
|
||||
format!("forge: operators team in {org} provisioning failed: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Meta repo lives at core/meta — pushed from git_commit in
|
||||
// meta.rs on every deploy/lock-update. Make sure it exists
|
||||
// before the first push hits a 404.
|
||||
|
|
@ -326,26 +290,8 @@ async fn ensure_all_orgs_and_repos(token: &str) {
|
|||
format!("forge: core/meta repo provisioning failed: {e}"),
|
||||
);
|
||||
}
|
||||
// Seed the shared docs repo. internal is already in
|
||||
// SEEDED_ORGS above so the org exists; ensure the repo itself.
|
||||
if let Err(e) = ensure_shared_docs_repo(token).await {
|
||||
tracing::warn!(error = ?e, "forge: ensure_shared_docs_repo failed");
|
||||
crate::warnings::set_boot_warning(
|
||||
"forge_ensure_shared_docs_repo",
|
||||
"warn",
|
||||
format!("forge: shared docs repo provisioning failed: {e}"),
|
||||
);
|
||||
}
|
||||
// Seed the hive-wide knowledge repo.
|
||||
if let Err(e) = ensure_knowledge_repo(token).await {
|
||||
tracing::warn!(error = ?e, "forge: ensure_knowledge_repo failed");
|
||||
crate::warnings::set_boot_warning(
|
||||
"forge_ensure_knowledge_repo",
|
||||
"crit",
|
||||
format!("forge: knowledge repo provisioning failed: {e}"),
|
||||
);
|
||||
}
|
||||
// Clone knowledge repo locally so it can be bind-mounted into agents.
|
||||
// The swarm-controller creates and seeds the repo itself.
|
||||
if let Err(e) = crate::knowledge::ensure_local_clone(token).await {
|
||||
tracing::warn!(error = ?e, "knowledge: ensure_local_clone failed");
|
||||
crate::warnings::set_boot_warning(
|
||||
|
|
@ -362,14 +308,6 @@ async fn ensure_all_orgs_and_repos(token: &str) {
|
|||
format!("forge: core avatar upload failed: {e}"),
|
||||
);
|
||||
}
|
||||
if let Err(e) = ensure_config_org_avatar(token).await {
|
||||
tracing::warn!(error = ?e, "forge: ensure_config_org_avatar failed");
|
||||
crate::warnings::set_boot_warning(
|
||||
"forge_ensure_config_org_avatar",
|
||||
"warn",
|
||||
format!("forge: agent-configs org avatar upload failed: {e}"),
|
||||
);
|
||||
}
|
||||
// Register the hive-ci Actions runner (off the container's boot path;
|
||||
// no-op when CI is disabled or the runner already holds valid creds).
|
||||
ci_runner::ensure_ci_runner_registered(token).await;
|
||||
|
|
@ -419,7 +357,8 @@ async fn wait_until_ready() -> bool {
|
|||
/// each has a forgejo user + token, plus an `agent-configs/<name>`
|
||||
/// repo mirroring its applied config. Also seeds the `core` admin
|
||||
/// user (hive-c0re's own identity for pushing the meta repo + driving
|
||||
/// the API), the `agent-configs` org, and the `core/meta` repo.
|
||||
/// the API) and the `core/meta` repo. The `agent-configs` org itself is
|
||||
/// the swarm-controller's to ensure.
|
||||
/// Called once at hive-c0re startup. Per-step failures are logged
|
||||
/// but don't abort the sweep.
|
||||
pub async fn ensure_all() {
|
||||
|
|
@ -495,9 +434,9 @@ pub async fn ensure_all() {
|
|||
/// An org-level hook covers every repo in `agent-configs` automatically,
|
||||
/// so no per-repo setup is needed as new agents are provisioned.
|
||||
///
|
||||
/// Called at startup beside `knowledge::remove_webhook`, its opposite: that
|
||||
/// repo's one hook is the controller's now, this one has not moved yet. No-op
|
||||
/// when the core token is absent (forge not yet provisioned).
|
||||
/// Called at startup. The knowledge repo's one hook is the controller's
|
||||
/// now; this one has not moved yet. No-op when the core token is absent
|
||||
/// (forge not yet provisioned).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
|
|
|||
Loading…
Reference in a new issue