# The swarm's forge as every hive sees it: the names and ports it answers on, # the URLs it advertises, and the OIDC client it is registered as. What the # host running it decides, and the container itself, are in ./default.nix. { lib, config, ... }: let cfg = config.services.hyperhive.swarm.forge; gatewayCfg = config.services.hyperhive.gateway; swarmDomain = config.services.hyperhive.swarm.domain; deployCfg = config.services.hyperhive.deploy; # Forgejo's name for the login source. Duplicated in ./default.nix, which # registers the source under it. ssoSourceName = "authelia"; # Forgejo's OAuth2 callback shape. `ssoSourceName` and `effectiveRootUrl` # must equal their copies in ./default.nix: a mismatch is a rejected login # with no error text worth reading. ssoRedirectUri = "${effectiveRootUrl}user/oauth2/${ssoSourceName}/callback"; # Forgejo's `ROOT_URL`, duplicated from ./default.nix, which documents its # shape. defaultRootUrl = if deployCfg.forgejo.behindGateway then let portSuffix = if gatewayCfg.httpsPort == 443 then "" else ":${toString gatewayCfg.httpsPort}"; in "https://${cfg.domain}${portSuffix}/" else "http://${cfg.domain}:${toString cfg.httpPort}/"; effectiveRootUrl = if cfg.rootUrl != null then cfg.rootUrl else defaultRootUrl; in { # Forge moved under `swarm` when the swarm-global services were # consolidated. One rename for the namespace: the subtree comes with it, # so existing hives keep evaluating and get one warning naming both paths. imports = [ (lib.mkRenamedOptionModule [ "services" "hyperhive" "forge" ] [ "services" "hyperhive" "swarm" "forge" ] ) (lib.mkRemovedOptionModule [ "services" "hyperhive" "swarm" "forge" "sso" "enable" ] '' SSO is no longer optional: the forge always registers the swarm's authelia as a login source. Removed rather than defaulted to true so a config that turned it OFF fails here, where the line is, instead of silently gaining a login provider on the next rebuild. Drop the line; if it was false, set services.hyperhive.deploy.forgejo.sso.clientSecretFile and services.hyperhive.swarm.authelia.url as the assertions describe. '') ]; # Every swarm needs this forge — it's the canonical store for the meta # flake + every agent's config repo (and the `internal/*` repos) — but # only one host runs it: `deploy.forgejo.enable`, in ../deploy.nix. What # follows is what every hive needs to reach it, wherever it runs. options.services.hyperhive.swarm.forge = { 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 git@:/.git`. ''; }; domain = lib.mkOption { type = lib.types.str; # Under the SWARM domain, not this hive's: a swarm runs one forge # and every hive in it reaches the same host, so the name belongs # to the swarm rather than to whichever hive happens to run it. # # Total on a null swarm domain so the required-domain assertion in # hive-network.nix is the thing that fires; see the comment there. default = if swarmDomain == null then "forge.invalid" else "forge.${swarmDomain}"; defaultText = lib.literalExpression ''"forge.''${services.hyperhive.swarm.domain}"''; 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 `deploy.forgejo.behindGateway = true` (sub-domain routing — see `docs/networking/gateway.md`). Defaults to `forge.''${services.hyperhive.swarm.domain}` — the swarm's domain, not this hive's, because a swarm runs **one** forge that every hive in it talks to. ⚠️ A deployment that was running before this moved keeps its current name by pinning it here: `forge.''${services.hyperhive.domain}`, which is exactly what the old default rendered. Certificates follow either way: the swarm-services sub-CA is name-constrained to the configured names (see `./swarm-ca.nix`), not to a fixed tree. 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. ''; }; publicUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = if deployCfg.forgejo.behindGateway then "https://${cfg.domain}" else null; defaultText = lib.literalExpression '' if behindGateway then "https://''${domain}" else null ''; example = "https://forge.example.com"; description = '' Browser-facing forge URL the dashboard uses to build clickable forge links (the H0M3 Forge tile, per-agent-row forge links, the approval-queue's "review PR on forge" link) — sourced into every agent container + hive-c0re as `HIVE_FORGE_PUBLIC_URL`. Defaults to `https://''${cfg.domain}` when `deploy.forgejo.behindGateway = true` (the gateway vhost is genuinely reachable at that URL) and `null` otherwise. When `null`, the dashboard **hides** forge links rather than guessing one — see `docs/web-ui/dashboard.md::H0M3 page` for the rationale (a link built from the operator's own browser hostname + a container port is only an accident away from wrong on any deployment that isn't plain localhost). **Set this explicitly if `deploy.forgejo.behindGateway = false`** and the forge is still reachable at a stable URL you want linked from the dashboard (e.g. `http://:''${toString cfg.httpPort}` for an all-LAN deployment) — leaving it unset there means the dashboard's forge links are simply absent, not broken. ''; }; 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, including the scheme: - `deploy.forgejo.behindGateway = true` → `https://''${cfg.domain}/`. The gateway always terminates TLS (self-signed is the implicit floor when no `gateway.tls.certDir` / ACME is set), so the forge is always advertised over https. A non-canonical `gateway.httpsPort` is appended as `:`. - `deploy.forgejo.behindGateway = false` → `http://''${cfg.domain}:''${cfg.httpPort}/` The TLS scheme is derived automatically now, so you only need to set this for a genuinely bespoke shape (e.g. an external reverse proxy on a different host/path). Must end with `/` per forgejo's `ROOT_URL` contract. ''; }; # The swarm's authelia is always registered as an OpenID Connect # login source here — there is no toggle: a forge without it has no # way to log a person in through the swarm's SSO. # # **Additive, never exclusive.** Forgejo keeps its local password # database and gains an extra "sign in with" button; this does not # disable local login. Deliberate: an identity provider that can take # the forge offline when it hiccups is a worse forge than one with # two ways in — which is also what makes always-on safe. sso = { clientId = lib.mkOption { type = lib.types.str; default = "forgejo"; description = '' OAuth2 client id this forge identifies itself with. Must match the `id` of the corresponding entry in `services.hyperhive.swarm.authelia.oidc.clients`. ''; }; redirectUri = lib.mkOption { type = lib.types.str; readOnly = true; default = ssoRedirectUri; defaultText = lib.literalExpression ''"''${ROOT_URL}user/oauth2/authelia/callback"''; description = '' OAuth2 callback authelia sends the browser back to, and the URI it matches **exactly**. Read-only: forgejo derives it from its own `ROOT_URL` and the login source's name, so it is a fact other modules read rather than a knob. The glue that registers this client wherever authelia runs reads it from here instead of restating the format. ⚠️ With {option}`services.hyperhive.swarm.forge.rootUrl` unset, `ROOT_URL` follows {option}`services.hyperhive.deploy.forgejo.behindGateway` and the gateway's `httpsPort`, which are per-host. An authelia host that is not the forge's host renders the forge's callback only if the two agree on them; set `rootUrl` if they do not. ''; }; # The secret half is a path on the host that runs the forge, so it # lives under `deploy.forgejo.sso`, in ./default.nix. }; }; }