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.
261 lines
10 KiB
Nix
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
|
|
];
|
|
};
|
|
};
|
|
}
|