diff --git a/flake.nix b/flake.nix index ff7f31a..aefc28c 100644 --- a/flake.nix +++ b/flake.nix @@ -98,6 +98,7 @@ hyperhivePackage = system: self.packages.${system}.default; hyperhiveFlake = "${self}"; }; + hive-forge = ./nix/modules/hive-forge.nix; }; nixosConfigurations = diff --git a/nix/modules/hive-forge.nix b/nix/modules/hive-forge.nix new file mode 100644 index 0000000..d11a432 --- /dev/null +++ b/nix/modules/hive-forge.nix @@ -0,0 +1,116 @@ +{ + pkgs, + lib, + config, + ... +}: +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. + # + # 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. + + options.services.hive-forge = { + enable = lib.mkEnableOption "hive-forge — private Forgejo instance 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) so they don't collide. + ''; + }; + + 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 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. + ''; + }; + + 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 + forge listens on all interfaces either way; this just gates + external access). + ''; + }; + }; + + 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; + }; + # 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 { + allowedTCPPorts = [ + cfg.httpPort + 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 ]; + }; +}