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:
atlas 2026-09-24 15:00:10 +02:00
commit 20419ccd41
18 changed files with 1456 additions and 822 deletions

View file

@ -47,15 +47,6 @@ in
# prompts). `hive_sh4re::assets::*` reads paths underneath.
# `forge/users.rs` reads the core avatar PNG from here on startup.
HIVE_ASSETS_DIR = "${cfg.assets}/share/hyperhive";
# `agent-configs` org avatar PNG — independently overridable via
# `orgAvatarPng` without replacing the whole `assets` package.
# Falls back to the bundled PNG under HIVE_ASSETS_DIR when unset.
# Read by `forge::users::config_org_avatar_png_path`.
HIVE_ORG_AVATAR_PNG =
if cfg.orgAvatarPng != null then
"${cfg.orgAvatarPng}"
else
"${cfg.assets}/share/hyperhive/branding/agent-configs.png";
# Whether this hive runs ruthless — no root/manager agent at all
# (`auto_update::ensure_root_agent`). Default false = root
# auto-managed; true makes the sweep a no-op.

View file

@ -55,19 +55,6 @@
rust derivation.
'';
};
orgAvatarPng = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
PNG uploaded once as the `agent-configs` Forgejo org's avatar
(`forge::users::ensure_config_org_avatar`, one-shot,
marker-guarded — delete `forge/agent-configs-avatar-set` under
the state dir to force a re-upload after changing this).
Defaults (`null`) to the bundled `agent-configs.png` from
`assets`. Set this to override just the org avatar without
replacing the whole `assets` package.
'';
};
xdgIcons = lib.mkOption {
type = lib.types.package;
defaultText = lib.literalExpression "hyperhive.packages.\${system}.xdg-icons";

View file

@ -633,9 +633,9 @@ in
'';
}
{
# Keep mirror orgs out of the hive-c0re-managed namespaces
# Keep mirror orgs out of the hyperhive-managed namespaces
# (config/shared/agents/core) so the seed never races / collides
# with hive-c0re's own startup provisioning of those orgs.
# with the provisioning of those orgs.
assertion = lib.all (
m:
!(lib.elem (builtins.elemAt (lib.splitString "/" m.dest) 0) [
@ -647,8 +647,8 @@ in
) effectiveMirrors;
message = ''
services.hyperhive.deploy.forgejo.mirrors[].dest must not place a mirror
in a hive-c0re-managed org (config / shared / agents / core) —
those are provisioned by hive-c0re and a mirror there would
in a hyperhive-managed org (config / shared / agents / core) —
those are provisioned by hyperhive itself and a mirror there would
collide. Use a dedicated org (e.g. "actions/checkout").
'';
}
@ -1358,12 +1358,22 @@ in
];
};
# Forward the declared pull-mirrors to hive-c0re, which seeds them in
# its forge provisioning sweep (`forge.rs::ensure_mirrors`, alongside
# the SEEDED_ORGS ensure). c0re already holds the core admin token and
# ensures the orgs there, so the seeding lives in one place rather than
# a parallel host-side unit. JSON-encoded list of { upstream, dest };
# `[]` when nothing to seed (c0re no-ops).
systemd.services.hive-c0re.environment.HYPERHIVE_FORGE_MIRRORS = builtins.toJSON effectiveMirrors;
# Forward the declared pull-mirrors to the swarm-controller, which seeds
# them in its swarm-wide forge-objects pass
# (`swarm-controller/src/forge/objects.rs`), beside the orgs they live in.
# JSON-encoded list of { upstream, dest }; `[]` when nothing to seed.
#
# ⚠️ This host's `mirrors` reach a controller on THIS host only. A
# controller elsewhere reads its own host's list, so mirrors declared
# here would never be seeded; the warning below says so at eval rather
# than leaving it to be noticed as a missing repo.
systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS =
lib.mkIf deployCfg.swarm-controller.enable (builtins.toJSON effectiveMirrors);
warnings = lib.optional (effectiveMirrors != [ ] && !deployCfg.swarm-controller.enable) ''
services.hyperhive.deploy.forgejo.mirrors (or the actions/checkout mirror
deploy.forgejo.ci.enable adds) is set on a host that does not run the
swarm-controller. The controller seeds mirrors from its own host's list,
so these are not seeded: declare them on the swarm-controller's host.
'';
};
}

View file

@ -190,6 +190,13 @@ let
# Same `%d` shape as the queue secret above — root reads the plaintext
# at unit start, the daemon's own user sees a 0400 copy.
SWARM_CONTROLLER_FORGE_TOKEN_FILE = "%d/forge-token";
# `agent-configs` org avatar for the forge-objects pass
# (`forge/objects.rs`). Only meaningful with forge access, hence here.
SWARM_CONTROLLER_CONFIG_ORG_AVATAR_PNG =
if deployCfg.swarm-controller.configOrgAvatarPng != null then
"${deployCfg.swarm-controller.configOrgAvatarPng}"
else
"${config.services.hyperhive.c0re.assets}/share/hyperhive/branding/agent-configs.png";
};
# How the forge must address this controller to deliver a swarm-wide
@ -311,6 +318,12 @@ in
hostUnit = true;
enable = deployCfg.swarm-controller.enable;
})
# The avatar moved with the forge-objects pass that uploads it: from
# hive-c0re's boot sweep to this daemon.
(lib.mkRenamedOptionModule
[ "services" "hyperhive" "c0re" "orgAvatarPng" ]
[ "services" "hyperhive" "deploy" "swarm-controller" "configOrgAvatarPng" ]
)
];
options.services.hyperhive.swarm.controller = {
@ -498,6 +511,20 @@ in
'';
};
configOrgAvatarPng = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
PNG uploaded once as the `agent-configs` Forgejo org's avatar by
the controller's swarm-wide forge-objects pass (one-shot,
marker-guarded — delete `forge-config-org-avatar-set` under the
controller's state directory to force a re-upload after changing
this). Defaults (`null`) to the bundled `agent-configs.png` from
{option}`services.hyperhive.c0re.assets`. Set this to override just
the org avatar without replacing the whole `assets` package.
'';
};
forgeTokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
# Forge's own delivery path when the forge runs here, since nothing

View file

@ -60,6 +60,19 @@ let
deploy.allSwarmServices = true;
deploy.bao.enable = false;
};
# The swarm-wide forge objects are the controller's (forge/objects.rs), so
# the mirror list and the org avatar go to its unit, not hive-c0re's.
aMirror = {
upstream = "https://example.invalid/tool";
dest = "mirrors/tool";
};
controllerWithMirrors = hive {
deploy.swarm-controller.enable = true;
deploy.forgejo.mirrors = [ aMirror ];
c0re.orgAvatarPng = "/etc/marker-avatar.png";
};
mirrorsNoController = hive { deploy.forgejo.mirrors = [ aMirror ]; };
cases = [
{
# `lib.all` over an empty set holds vacuously, so the roster is counted
@ -123,6 +136,28 @@ let
name = "an explicit gateway.enable = false wins over the modules asserting it";
ok = !(hive { gateway.enable = false; }).services.nginx.enable;
}
{
name = "declared forge mirrors reach the swarm-controller, not hive-c0re";
ok =
builtins.fromJSON controllerWithMirrors.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_FORGE_MIRRORS
== [ aMirror ]
&& !(controllerWithMirrors.systemd.services.hive-c0re.environment ? HYPERHIVE_FORGE_MIRRORS);
}
{
name = "forge mirrors on a host without the controller warn that nothing seeds them";
ok =
let
warns = cfg: lib.any (lib.hasInfix "declare them on the swarm-controller's host") cfg.warnings;
in
warns mirrorsNoController && !(warns controllerWithMirrors);
}
{
name = "the old c0re org-avatar option lands on the controller's unit";
ok =
controllerWithMirrors.systemd.services.swarm-controller.environment.SWARM_CONTROLLER_CONFIG_ORG_AVATAR_PNG
== "/etc/marker-avatar.png"
&& !(controllerWithMirrors.systemd.services.hive-c0re.environment ? HIVE_ORG_AVATAR_PNG);
}
];
in
runGroup "swarm-services-switch" cases