{ pkgs, lib, config, ... }: let cfg = config.services.hive-forge; in { # 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. # # 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 (in a nixos-container) for hyperhive agents"; httpPort = lib.mkOption { type = lib.types.port; default = 3000; description = '' TCP port the forge serves HTTP on. Default 3000 sits outside hyperhive's claimed ranges (dashboard 7000, manager 8000, sub-agents 8100..8999). Change this if you already have another forgejo bound to 3000. ''; }; sshPort = lib.mkOption { type = lib.types.port; default = 2222; description = '' TCP port the forge's built-in SSH server listens on. Kept off 22 so it doesn't clash with the host's openssh. Agents push with `ssh -p git@:/.git`. ''; }; domain = lib.mkOption { type = lib.types.str; default = "localhost"; example = "forge.internal"; description = '' 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. ''; }; openFirewall = lib.mkOption { 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 container shares host netns, so this is the only firewall layer that matters.) ''; }; }; 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 = { 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"; # F3 (federation) computes its data dir relative to the # forgejo binary, which lands in the read-only nix # store and crashes anything that touches the F3 # subsystem — including `forgejo admin user create`, # which init-ses F3 even when ENABLED=false. Pin the # path absolute alongside the disable so the init # resolution succeeds before the flag is checked. "F3" = { ENABLED = false; PATH = "/var/lib/forgejo/data/f3"; }; }; }; environment.systemPackages = [ pkgs.forgejo ]; }; }; networking.firewall = lib.mkIf cfg.openFirewall { allowedTCPPorts = [ cfg.httpPort cfg.sshPort ]; }; }; }