hyperhive/nix/modules/hive-forge.nix
atlas 85790b0cca nix: add breaking-change note to each openFirewall description (argus #653)
argus picked option (a) on #653: put the upgrade note in each option's
`description` so it shows up in `nix flake show` + the rendered
options docs, right next to the option itself. cheapest option, no
eval-time noise (a `warnings` block would fire on every new
deployment that wants false — the normal case now).

Appended a `**Breaking change as of #651**` paragraph to each of the
three `openFirewall` descriptions, naming the exact option string the
operator needs to set to restore the old behaviour.

Gateway's note specifically calls out that external reach is the
common case (operator's primary entry point), so the upgrade hint
is most likely needed there.
2026-05-30 19:30:11 +02:00

261 lines
10 KiB
Nix

{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.hyperhive.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:<httpPort>` 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.hyperhive.forge = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Run hive-forge a private Forgejo (in a nixos-container) for
hyperhive agents. On by default: hive-c0re mirrors every
agent's applied config repo into the forge's `agent-configs`
org, so the forge is part of the standard install. Set
`services.hyperhive.forge.enable = false` to opt out.
'';
};
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 <sshPort> git@<domain>:<owner>/<repo>.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.
'';
};
package = lib.mkOption {
type = lib.types.package;
default = pkgs.forgejo;
defaultText = lib.literalExpression "pkgs.forgejo";
description = ''
Forgejo package to run inside the container. Defaults to
`pkgs.forgejo` (the latest release line) rather than the
nixpkgs-module default of `pkgs.forgejo-lts`, because LTS
lags far behind on schema and the DB easily ends up "newer
than the binary" if the operator ever ran a non-LTS forgejo
against the same state dir. Override to `pkgs.forgejo-lts`
if you actively want the slower release train.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Open `httpPort` + `sshPort` in the host firewall. Off by
default (#651, secure-by-default): the forge is reachable
from the host + every agent container via `localhost` either
way (shared netns), so the firewall opens only matter for
access from outside the host. Flip to `true` when you want
the operator's browser / external git clients to hit the
forge directly. (The container shares host netns, so this
is the only firewall layer that matters.)
**Breaking change as of #651**: this used to default to
`true`. If you relied on the old default for external
reach, add `services.hyperhive.forge.openFirewall = true;`
to your host config before rebuilding.
'';
};
};
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, ... }:
let
# Build a custom static-root that is the standard forgejo data
# output with our theme CSS added. Using STATIC_ROOT_PATH instead
# of tmpfiles / bind-mounts means the theme is always present in
# the nix store — no separate hive-forge container rebuild needed,
# and no persistent-state directory involved.
staticRootWithTheme = pkgs.runCommand "forgejo-static-with-theme" { } ''
cp -r --no-preserve=mode,ownership ${cfg.package.data}/. $out/
mkdir -p $out/public/assets/css
cp ${../forge-theme/theme-catppuccin-vibec0re.css} \
$out/public/assets/css/theme-catppuccin-vibec0re.css
# Replace the default Forgejo logo + favicon with the hyperhive
# mark. Files in public/assets/img/ are served before built-ins.
mkdir -p $out/public/assets/img
cp ${../../branding/hyperhive.svg} $out/public/assets/img/logo.svg
cp ${../../branding/hyperhive.svg} $out/public/assets/img/favicon.svg
cp ${../../branding/hyperhive.png} $out/public/assets/img/logo.png
cp ${../../branding/hyperhive.png} $out/public/assets/img/favicon.png
cp ${../../branding/hyperhive.png} $out/public/assets/img/avatar_default.png
'';
in
{
system.stateVersion = "25.11";
services.forgejo = {
enable = true;
package = cfg.package;
database.type = "sqlite3";
lfs.enable = true;
settings = {
DEFAULT.APP_NAME = "HyperHive";
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;
# Point forgejo at our extended static root that includes
# the custom theme CSS baked straight into the nix store.
STATIC_ROOT_PATH = staticRootWithTheme;
};
# 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";
};
# Repo migrations / pull-mirrors fetch from the source
# URL *inside* Forgejo. hyperhive code is synced from
# `localhost` (and the host LAN), which Forgejo's
# migration guard blocks by default ("cannot import from
# disallowed hosts"). Allow loopback + RFC-1918 sources
# so an in-hive mirror of the hyperhive repo works.
migrations.ALLOW_LOCALNETWORKS = true;
log.LEVEL = "Warn";
ui = {
DEFAULT_THEME = "catppuccin-vibec0re";
THEMES = "catppuccin-vibec0re,forgejo-auto,forgejo-light,forgejo-dark,gitea-auto,gitea-light,gitea-dark";
};
# Point forgejo at the GPG key generated by the
# forgejo-gpg-init oneshot below. "default" resolves to
# the first secret key found in GNUPGHOME. GNUPGHOME
# must be absolute and writeable by the forgejo user.
"repository.signing" = {
SIGNING_KEY = "default";
GNUPGHOME = "/var/lib/forgejo/.gnupg";
};
# 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
pkgs.gnupg
];
# Generate a GPG signing key for Forgejo on first boot so UI
# merges produce signed commits instead of erroring "no key to
# sign with". The key lives in forgejo's persistent state dir
# (/var/lib/forgejo/.gnupg) and survives container restarts.
# The stamp file prevents re-generation on subsequent boots.
# Service runs as the forgejo user so file ownership is correct.
systemd.services.forgejo-gpg-init = {
description = "generate GPG signing key for Forgejo (once)";
# Start before forgejo so the key is ready when forgejo reads
# repository.signing config on startup.
wantedBy = [ "forgejo.service" ];
before = [ "forgejo.service" ];
unitConfig.ConditionPathExists = "!/var/lib/forgejo/.gnupg/hive-key-init.stamp";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "forgejo";
Group = "forgejo";
};
environment.GNUPGHOME = "/var/lib/forgejo/.gnupg";
path = [
pkgs.gnupg
pkgs.coreutils
];
script = ''
mkdir -p "$GNUPGHOME"
chmod 700 "$GNUPGHOME"
gpg --batch --gen-key <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Name-Real: HyperHive Forge
Name-Email: forgejo@hive
Expire-Date: 0
EOF
touch "$GNUPGHOME/hive-key-init.stamp"
'';
};
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
cfg.httpPort
cfg.sshPort
];
};
};
}