hive-forge: wrap forgejo in a nixos-container

avoids fighting an operator-side `services.forgejo` over the
singleton module options. container shares host netns
(`privateNetwork = false`) so agents still dial the forge via
plain `localhost:<httpPort>` and the host firewall is the only
layer that matters. container name is `hive-forge` (no `h-`
prefix) so hive-c0re's lifecycle scanner ignores it — operator
manages it with the standard `nixos-container` CLI. state lives
at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` and
survives restarts.
This commit is contained in:
müde 2026-05-16 20:52:36 +02:00
parent c2d176ed13
commit 6e9c67dd94

View file

@ -8,21 +8,24 @@ let
cfg = config.services.hive-forge; cfg = config.services.hive-forge;
in in
{ {
# Thin wrapper around `services.forgejo` with hyperhive-friendly # Private Forgejo for hyperhive agents, wrapped in a nixos-container
# defaults: sqlite (no extra service to manage), built-in SSH on a # so it doesn't fight any `services.forgejo` the operator already
# non-22 port so it doesn't fight the host's sshd, registration off # runs on the host. The container shares the host network namespace
# (agents get accounts seeded out of band), and ports opened in the # (`privateNetwork = false`) so agents reach the forge at
# firewall. # `http://localhost:<httpPort>` without any extra plumbing —
# nixos-container is just here for state + systemd-unit isolation,
# not network isolation.
# #
# Forge wiring into the agent containers is intentionally out of # Container name is `hive-forge` (not `h-*`), so hive-c0re's
# scope here — containers already share the host network namespace, # lifecycle scanner ignores it; the operator manages it via the
# so once this module is enabled an agent can reach the forge at # standard `nixos-container` CLI.
# http://localhost:<httpPort> without any extra bind mount. The MCP #
# tool surface (open PR, list repos, etc.) lives in a separate # State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/`
# follow-up that the operator opts into per agent. # and survives container restart / host reboot. To wipe, destroy the
# container.
options.services.hive-forge = { options.services.hive-forge = {
enable = lib.mkEnableOption "hive-forge private Forgejo instance for hyperhive agents"; enable = lib.mkEnableOption "hive-forge private Forgejo (in a nixos-container) for hyperhive agents";
httpPort = lib.mkOption { httpPort = lib.mkOption {
type = lib.types.port; type = lib.types.port;
@ -30,7 +33,8 @@ in
description = '' description = ''
TCP port the forge serves HTTP on. Default 3000 sits outside TCP port the forge serves HTTP on. Default 3000 sits outside
hyperhive's claimed ranges (dashboard 7000, manager 8000, hyperhive's claimed ranges (dashboard 7000, manager 8000,
sub-agents 8100..8999) so they don't collide. sub-agents 8100..8999). Change this if you already have
another forgejo bound to 3000.
''; '';
}; };
@ -49,10 +53,10 @@ in
default = "localhost"; default = "localhost";
example = "forge.internal"; example = "forge.internal";
description = '' description = ''
Hostname agents and operator dial the forge by. `localhost` is Hostname used in repo clone URLs the forge advertises. The
fine for a single-host setup since containers share the host container shares host netns so `localhost` works for any
netns; set a real hostname when you want clones from outside agent on the same host; set a real hostname when you want
the host to look canonical. clones from outside the host to look canonical.
''; '';
}; };
@ -60,15 +64,27 @@ in
type = lib.types.bool; type = lib.types.bool;
default = true; default = true;
description = '' description = ''
Open `httpPort` + `sshPort` in the host firewall. Off when the Open `httpPort` + `sshPort` in the host firewall. Off when
forge should only be reachable from inside the host (the the forge should only be reachable from inside the host.
forge listens on all interfaces either way; this just gates (The container shares host netns, so this is the only
external access). firewall layer that matters.)
''; '';
}; };
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
containers.hive-forge = {
autoStart = true;
ephemeral = false;
# Share host netns — forgejo's HTTP / SSH listeners then look
# exactly like a host-side service, no port forwarding dance,
# and agent containers (which also share host netns) reach it
# via plain `localhost`.
privateNetwork = false;
config =
{ pkgs, ... }:
{
system.stateVersion = "25.11";
services.forgejo = { services.forgejo = {
enable = true; enable = true;
database.type = "sqlite3"; database.type = "sqlite3";
@ -84,9 +100,9 @@ in
BUILTIN_SSH_SERVER_USER = "git"; BUILTIN_SSH_SERVER_USER = "git";
DISABLE_SSH = false; DISABLE_SSH = false;
}; };
# Registration off — operator creates agent users via # Registration off — operator seeds agent users via
# `forgejo admin user create` (or the API once seeded). # `nixos-container run hive-forge -- forgejo admin
# Agents that need access get a dedicated account + token. # user create …`.
service = { service = {
DISABLE_REGISTRATION = true; DISABLE_REGISTRATION = true;
REQUIRE_SIGNIN_VIEW = false; REQUIRE_SIGNIN_VIEW = false;
@ -95,10 +111,12 @@ in
DEFAULT_BRANCH = "main"; DEFAULT_BRANCH = "main";
DEFAULT_PRIVATE = "private"; DEFAULT_PRIVATE = "private";
}; };
# Quiet logs at idle; bump when debugging.
log.LEVEL = "Warn"; log.LEVEL = "Warn";
}; };
}; };
environment.systemPackages = [ pkgs.forgejo ];
};
};
networking.firewall = lib.mkIf cfg.openFirewall { networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ allowedTCPPorts = [
@ -106,11 +124,5 @@ in
cfg.sshPort cfg.sshPort
]; ];
}; };
# Convenience: drop the forgejo CLI on the host PATH so the
# operator can `forgejo admin user create …` without hunting for
# the wrapped binary. The forgejo service runs as its own user;
# admin commands need `sudo -u forgejo`.
environment.systemPackages = [ pkgs.forgejo ];
}; };
} }