diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix index d11a432..b35ea33 100644 --- a/nix/modules/hive-forge.nix +++ b/nix/modules/hive-forge.nix @@ -8,21 +8,24 @@ let cfg = config.services.hive-forge; in { - # Thin wrapper around `services.forgejo` with hyperhive-friendly - # defaults: sqlite (no extra service to manage), built-in SSH on a - # non-22 port so it doesn't fight the host's sshd, registration off - # (agents get accounts seeded out of band), and ports opened in the - # firewall. + # Private Forgejo for hyperhive agents, wrapped in a nixos-container + # so it doesn't fight any `services.forgejo` the operator already + # runs on the host. The container shares the host network namespace + # (`privateNetwork = false`) so agents reach the forge at + # `http://localhost:` 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 - # scope here — containers already share the host network namespace, - # so once this module is enabled an agent can reach the forge at - # http://localhost: without any extra bind mount. The MCP - # tool surface (open PR, list repos, etc.) lives in a separate - # follow-up that the operator opts into per agent. + # Container name is `hive-forge` (not `h-*`), so hive-c0re's + # lifecycle scanner ignores it; the operator manages it via the + # standard `nixos-container` CLI. + # + # State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` + # and survives container restart / host reboot. To wipe, destroy the + # container. 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 { type = lib.types.port; @@ -30,7 +33,8 @@ in description = '' TCP port the forge serves HTTP on. Default 3000 sits outside 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"; example = "forge.internal"; description = '' - Hostname agents and operator dial the forge by. `localhost` is - fine for a single-host setup since containers share the host - netns; set a real hostname when you want clones from outside - the host to look canonical. + Hostname used in repo clone URLs the forge advertises. The + container shares host netns so `localhost` works for any + agent on the same host; set a real hostname when you want + clones from outside the host to look canonical. ''; }; @@ -60,44 +64,58 @@ in type = lib.types.bool; default = true; description = '' - Open `httpPort` + `sshPort` in the host firewall. Off when the - forge should only be reachable from inside the host (the - forge listens on all interfaces either way; this just gates - external access). + Open `httpPort` + `sshPort` in the host firewall. Off when + the forge should only be reachable from inside the host. + (The container shares host netns, so this is the only + firewall layer that matters.) ''; }; }; config = lib.mkIf cfg.enable { - services.forgejo = { - enable = true; - database.type = "sqlite3"; - lfs.enable = true; - settings = { - server = { - DOMAIN = cfg.domain; - ROOT_URL = "http://${cfg.domain}:${toString cfg.httpPort}/"; - HTTP_PORT = cfg.httpPort; - START_SSH_SERVER = true; - SSH_PORT = cfg.sshPort; - SSH_LISTEN_PORT = cfg.sshPort; - BUILTIN_SSH_SERVER_USER = "git"; - DISABLE_SSH = false; + 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 = { + enable = true; + database.type = "sqlite3"; + lfs.enable = true; + settings = { + server = { + DOMAIN = cfg.domain; + ROOT_URL = "http://${cfg.domain}:${toString cfg.httpPort}/"; + HTTP_PORT = cfg.httpPort; + START_SSH_SERVER = true; + SSH_PORT = cfg.sshPort; + SSH_LISTEN_PORT = cfg.sshPort; + BUILTIN_SSH_SERVER_USER = "git"; + DISABLE_SSH = false; + }; + # Registration off — operator seeds agent users via + # `nixos-container run hive-forge -- forgejo admin + # user create …`. + service = { + DISABLE_REGISTRATION = true; + REQUIRE_SIGNIN_VIEW = false; + }; + repository = { + DEFAULT_BRANCH = "main"; + DEFAULT_PRIVATE = "private"; + }; + log.LEVEL = "Warn"; + }; + }; + environment.systemPackages = [ pkgs.forgejo ]; }; - # Registration off — operator creates agent users via - # `forgejo admin user create` (or the API once seeded). - # Agents that need access get a dedicated account + token. - service = { - DISABLE_REGISTRATION = true; - REQUIRE_SIGNIN_VIEW = false; - }; - repository = { - DEFAULT_BRANCH = "main"; - DEFAULT_PRIVATE = "private"; - }; - # Quiet logs at idle; bump when debugging. - log.LEVEL = "Warn"; - }; }; networking.firewall = lib.mkIf cfg.openFirewall { @@ -106,11 +124,5 @@ in 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 ]; }; }