Continues #718 docs-extraction. Three more blocks moved to `docs/gateway.md` (which already houses the gateway architecture story from #775): 1. **Firewall posture (gateway on vs off)** — was a 22-line block above `networking.firewall = lib.mkIf ...` in hive-c0re.nix. Trimmed to 3-line ref. New `docs/gateway.md::Firewall posture (host-level)` section covers the gateway-on / gateway-off trade-off + why dashboard port stays loopback-only. 2. **`HIVE_FORGE_URL` loopback rationale** — was a 14-line block above the env-var assignment. Trimmed to 5-line ref. New `docs/gateway.md::HIVE_FORGE_URL: loopback for in-cluster, sub-domain for the operator` section covers the in-cluster vs external split + why agent containers can't use the sub-domain. 3. **hive-forge container shape** — was a 15-line top-of-`config` block in hive-forge.nix explaining the nixos-container + host netns choices. Trimmed to 4-line ref. New `docs/gateway.md::hive-forge container shape` section captures the same content with state-dir + wipe-via-destroy notes. Net: hive-c0re.nix -29 lines, hive-forge.nix -11 lines, gateway.md +44 lines. Same pattern as #782 (first pass) per iris's #10114 guidance — substantive WHY moves to docs as named sub-paragraphs, in-code shrinks to `// see docs/<file>::<section>` refs. Verified: `nix eval` on agent-base toplevel still resolves cleanly; firewall posture unchanged (still 0 ports opened in the gateway-on case + the same 8100..8999 range in the gateway-off case). Continues #718. Follow-up batches: remaining harness-base.nix blocks, nix/docs/default.nix, nix/assets.nix, nix/templates/weston-vnc.nix.
386 lines
16 KiB
Nix
386 lines
16 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.forge;
|
|
gatewayCfg = config.services.hyperhive.gateway;
|
|
hyperhiveDomain = config.services.hyperhive.domain;
|
|
|
|
# ROOT_URL forgejo advertises in clone links + outbound URLs. When
|
|
# served behind the gateway (#749 — mara verdict at issue:9609,
|
|
# sub-domain over sub-path), `cfg.domain` doubles as both the
|
|
# forgejo `DOMAIN` setting AND the gateway vhost server-name, so
|
|
# ROOT_URL just uses it directly (drops the port suffix when the
|
|
# gateway is on the canonical port 80). When direct (gateway off
|
|
# or `behindGateway = false`), keep the host:port shape so direct
|
|
# browser access on `:httpPort` still produces correct links.
|
|
# Operators can override via `cfg.rootUrl` for TLS / non-default
|
|
# gateway ports / bespoke shapes.
|
|
defaultRootUrl =
|
|
if cfg.behindGateway then
|
|
let
|
|
portSuffix = if gatewayCfg.port == 80 then "" else ":${toString gatewayCfg.port}";
|
|
in
|
|
"http://${cfg.domain}${portSuffix}/"
|
|
else
|
|
"http://${cfg.domain}:${toString cfg.httpPort}/";
|
|
effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl;
|
|
in
|
|
{
|
|
# Private Forgejo in a `hive-forge` nixos-container, shared host
|
|
# netns so agents reach it on loopback. State at
|
|
# `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` survives
|
|
# restart. See `docs/gateway.md::hive-forge container shape`.
|
|
|
|
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, every agent in
|
|
8100..8999 via FNV-1a hash). 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 = if hyperhiveDomain != null then "forge.${hyperhiveDomain}" else "localhost";
|
|
defaultText = lib.literalExpression ''
|
|
if services.hyperhive.domain != null then
|
|
"forge.''${services.hyperhive.domain}"
|
|
else
|
|
"localhost"
|
|
'';
|
|
example = "git.example.com";
|
|
description = ''
|
|
Public hostname for the forge. Doubles as both the forgejo
|
|
`DOMAIN` setting (clone URLs forgejo advertises) AND the
|
|
gateway vhost server-name when `behindGateway = true`
|
|
(#749, mara verdict at issue:9609 — sub-domain over sub-path).
|
|
|
|
Defaults to `forge.''${services.hyperhive.domain}` when the
|
|
hive-domain is set (idiomatic sub-domain shape — `forge`
|
|
labelled under the hive's bare domain), falling back to
|
|
`localhost` otherwise (pre-#749 direct-on-port behaviour).
|
|
|
|
Set to a full hostname (`git.example.com`,
|
|
`forge.internal.lan`, etc.) for a bespoke vhost shape — the
|
|
full domain goes here, no separate sub-domain-label option
|
|
(mara on #754:9684 — "specify full forge domain in options
|
|
instead").
|
|
'';
|
|
};
|
|
|
|
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.
|
|
'';
|
|
};
|
|
|
|
behindGateway = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = gatewayCfg.enable or false;
|
|
defaultText = lib.literalExpression "config.services.hyperhive.gateway.enable";
|
|
description = ''
|
|
Serve forgejo through the hive-gateway nginx as a sub-domain
|
|
vhost (`server_name = cfg.domain`) instead of directly on
|
|
`httpPort` (#749, mara verdict at issue:9609 — sub-domain
|
|
over sub-path).
|
|
|
|
When `true`:
|
|
- The gateway adds a `server { server_name = ''${cfg.domain}; }`
|
|
block that proxies all `/` → `http://127.0.0.1:''${httpPort}/`.
|
|
- Forgejo's `ROOT_URL` flips to `http(s)://''${cfg.domain}/`
|
|
(sub-domain root, no port suffix when gateway is on 80).
|
|
- `gateway.localHostsEntry = true` extends `/etc/hosts` to
|
|
include `cfg.domain → 127.0.0.1` for local dev.
|
|
|
|
Defaults to `services.hyperhive.gateway.enable` — flipping
|
|
the gateway on/off auto-routes forge through it. Set `false`
|
|
explicitly to keep forge on the direct port even when the
|
|
gateway is running (e.g. an external git client that doesn't
|
|
traverse the gateway).
|
|
|
|
The mara-call on #749:9609 picks sub-domain over sub-path for
|
|
forge + matrix (both are external standard apps with sub-domain-
|
|
native config defaults). Per-agent UIs stay on sub-path
|
|
(`/agent/<name>/`) because they're hyperhive-internal +
|
|
already base-path-aware via #731.
|
|
'';
|
|
};
|
|
|
|
rootUrl = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "https://forge.example.com/";
|
|
description = ''
|
|
Override the auto-derived forgejo `ROOT_URL`. When `null`
|
|
(default), `ROOT_URL` is derived from `cfg.domain` + gateway
|
|
state:
|
|
|
|
- `behindGateway = true` → `http://''${cfg.domain}/` (uses
|
|
`services.hyperhive.gateway.port` when non-80)
|
|
- `behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/`
|
|
|
|
Set this to a fully-qualified URL when running behind TLS
|
|
termination (`https://...`), a non-default gateway port, or
|
|
a bespoke shape. Must end with `/` per forgejo's `ROOT_URL`
|
|
contract.
|
|
'';
|
|
};
|
|
|
|
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 {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.rootUrl == null || lib.hasSuffix "/" cfg.rootUrl;
|
|
message = ''
|
|
services.hyperhive.forge.rootUrl must end with "/". forgejo's
|
|
ROOT_URL contract requires a trailing slash for correct
|
|
relative-link generation; without it forgejo emits URLs like
|
|
`https://forge.example.com.user.id` instead of
|
|
`https://forge.example.com/user.id`. Got: ${toString cfg.rootUrl}
|
|
'';
|
|
}
|
|
{
|
|
# `cfg.domain` can't be empty — would render `.<hive>` shaped
|
|
# garbage as both server_name (nginx wildcard catch-all) and
|
|
# /etc/hosts entry (invalid). Default rejects this case (lands
|
|
# `"localhost"` when hive-domain is unset), but operator-set
|
|
# empty strings should fail loud.
|
|
assertion = cfg.domain != "";
|
|
message = ''
|
|
services.hyperhive.forge.domain = "" is rejected. The
|
|
rendered URLs would be invalid (nginx wildcard catch-all
|
|
for an empty server_name, /etc/hosts rejects empty entries).
|
|
Either leave at default (auto-derives to
|
|
"forge.<services.hyperhive.domain>" when set, else
|
|
"localhost"), or set a non-empty hostname like "forge.example.com"
|
|
or "git.internal".
|
|
'';
|
|
}
|
|
{
|
|
# behindGateway requires the gateway module to actually be on.
|
|
# Otherwise the configured `ROOT_URL` flips to a sub-domain
|
|
# shape that has no nginx vhost backing it → broken on the
|
|
# rebuild.
|
|
assertion = !cfg.behindGateway || (gatewayCfg.enable or false);
|
|
message = ''
|
|
services.hyperhive.forge.behindGateway = true requires
|
|
services.hyperhive.gateway.enable = true (the gateway vhost
|
|
serving forge needs the gateway container to actually be
|
|
running). Either turn the gateway on, or set
|
|
services.hyperhive.forge.behindGateway = false to keep forge
|
|
on its direct port.
|
|
'';
|
|
}
|
|
];
|
|
|
|
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 = effectiveRootUrl;
|
|
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
|
|
];
|
|
};
|
|
};
|
|
}
|