The nixpkgs gitea-actions-runner module uses tokenFile as a systemd
EnvironmentFile, loaded before any ExecStartPre. Our previous
ExecStartPre override was too late — file didn't exist yet.
Fix: tmpfiles pre-creates /run/hive-ci/runner-token with a placeholder
on every boot. A new hive-ci-register.service oneshot (gated on
ConditionPathExists=!/var/lib/gitea-runner/hive/.runner) fetches the
real token and overwrites before gitea-runner-hive.service starts,
wired via After/Wants. On subsequent boots the placeholder is harmless
— nixpkgs register step exits early when .runner already exists.
Closes#918.
Per-instance package option does not exist in the nixpkgs module.
Package is configured at services.gitea-actions-runner.package.
Fixes build failure on main. Closes#916.
nix flake check pulls rust tools hermetically. curl/jq in preStart
use absolute store paths. nix is part of any NixOS system by default.
git is the only package genuinely needed at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the manual runnerTokenFile operator step. On first boot the
container fetches a runner registration token from Forgejo's admin
API using hive-c0re's existing admin token
(/var/lib/hyperhive/forge-core-token). A preStart script writes the
token to /run/hive-ci/runner-token; on subsequent boots it writes a
dummy (registered .runner creds take precedence anyway).
Operator bootstrap is now just `services.hyperhive.ci.enable = true`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `services.hyperhive.ci` NixOS module that spins up a `hive-ci`
nixos-container running `gitea-actions-runner` against the hive-forge
Forgejo instance. Off by default; opt in with `ci.enable = true` after
generating a runner registration token in Forgejo.
Also adds `.forgejo/workflows/ci.yml` with four jobs: nix flake check,
formatting (nix fmt + cargo fmt), cargo test, and cargo clippy. Jobs
target the `hive-ci` runner label.
Container design mirrors hive-forge (shared host netns, non-ephemeral
state, loopback reach to forge). sandbox-fallback = true since nspawn
containers can't create user-namespaces for nix sandbox.
Closes#175.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The #872 path-unit approach was silently broken: IN_MOVED_TO from an
atomic rename on the host does not propagate across the nspawn
mount-namespace boundary into the container, so the watcher inside
the gateway container never fired.
Fix: after each agents.conf write, c0re calls
`systemd-run --machine=hive-gateway -- nginx -s reload` from the host.
The reload is best-effort (logged on failure, not fatal).
Remove the now-unused `hive-gateway-agents-conf.path` +
`hive-gateway-nginx-reload.service` from the gateway container config.
Update docs/gateway.md + comments to reflect the host-side approach.
Dashboard links to the forge were hardcoded as http://<hostname>:3000,
which breaks when the operator accesses the dashboard through
hive-gateway (forge is served at forge.<domain> with no port).
- nix/modules/hive-c0re.nix: inject HIVE_FORGE_PUBLIC_URL when
forge.behindGateway = true (e.g. http://forge.pr1ma.darkest.space)
- hive-c0re/src/dashboard.rs: expose forge_url: Option<String> in
StateSnapshot, reading from HIVE_FORGE_PUBLIC_URL
- frontend/packages/dashboard/src/tabs.js: use state.forge_url when
present; fall back to http://<hostname>:3000 for gateway-off /
local-dev deploys
- gateway_nginx.rs: HOST_CONF_PATH → /var/lib/hyperhive/gateway/agents.conf
- hive-gateway.nix: hostPath = /var/lib/hyperhive/gateway (not whole parent
dir — avoids exposing forge tokens or other credentials to the gateway
container)
- tmpfiles: add /var/lib/hyperhive/gateway/ dir rule + seed agents.conf there
- scrub "(#869)" from hive-gateway-nginx-reload service description
Replace eval-time per-agent nginx location baking with a runtime
include file. c0re writes /var/lib/hyperhive/agents.conf (nginx
location blocks, UDS or TCP per agent) on every topology change and
on the 10s marker poll. The gateway container bind-mounts
/var/lib/hyperhive/ at /run/hive-state/ and nginx includes
/run/hive-state/agents.conf. A systemd path unit inside the container
watches the file for changes and fires `nginx -s reload` on each
atomic rename from c0re — no nixos-rebuild switch needed when agents
start, stop, or flip useUnixSocket.
- new hive-c0re/src/gateway_nginx.rs: write() + render()
- lib.rs + meta.rs + agent_sockets::spawn_poll: hook in write()
- hive-gateway.nix: drop agentPortsTable/agentSocketsTable/
agentUpstreamFor/lib.mapAttrs', add /run/hive-state bind-mount,
include directive, systemd path unit + reload service, tmpfiles
for /var/lib/hyperhive + agents.conf seed
- docs/gateway.md: update vhost table + Per-agent UDS section
nginx-pre-start runs the config test as the nginx user; a 0600
root-owned key fails that check with BIO_new_file Permission denied
even though the master process would later load it as root.
Parent /var/lib/hive-gateway came out 0700, blocking the nginx-user
worker from reaching the 0755 tls/ subdir and surfacing as a generic
cert-load failure at nginx start.
When `services.hyperhive.gateway.enable` is on (default), the c0re
NixOS module now sets `HIVE_GATEWAY_ENABLED=1` on the service env.
`/api/state` exposes the flag as `gateway_enabled`; the SW4RM tab's
container-row renderer flips three link sites (primary agent-name
link, favicon fetch, `container`-kind nav-strip links) from the
legacy `http://<host>:<port>/` direct TCP shape to same-origin
`/agent/<name>/` URLs — the gateway proxies them to the per-agent
harness via `agent-ports.json` or `agent-sockets.json` (#784 / #815).
Gateway-off deploys keep the direct TCP fallback so local-dev /
operator opt-out keeps working.
`forge`-kind nav-strip links still resolve against `:3000` (separate
sub-domain transition, tracked by `forge.behindGateway`);
`external`-kind links are already absolute.
Mirrors the `HIVE_MATRIX_GUI_ENABLED` env→snapshot-flag pattern.
Docs updated: `docs/web-ui.md::Container row` + new
`docs/gateway.md::Dashboard link shape` section.
forge/matrix subdomain address rules are redundant when those are
sub-domains of hyperhive.domain (dnsmasq's /<domain>/ matches
sub-domains). Keep explicit for the cross-domain case (e.g.
forge.domain = "git.example.com") + add a comment explaining
the trade-off.
Stand up the host-side bridge interface + per-agent DNS resolver
ahead of #14 (netns isolation). Mara on #805#11541: "we need it
before #14 so nothing breaks when we switch over". v1 ships the
endpoint live but containers stay on shared host netns — when #14
flips them to private netns the DNS contract is already there.
Shape:
- new `nix/modules/hive-network.nix` with `services.hyperhive.network.*`
options (enable + bridgeName + bridgeIp + bridgePrefixLength +
upstreamDns). Default off. Imported from `hive-c0re.nix`.
- bridge interface via `networking.bridges` (no slave NICs at v1;
per-agent veth pairs attach once #14 lands).
- bridge IP assigned via `networking.interfaces`.
- `networking.firewall.interfaces.<bridge>.allowed{UDP,TCP}Ports =
[ 53 ]` opens the resolver on the bridge interface only —
other interfaces stay closed.
- dnsmasq config added to the existing `hive-gateway` container
(mara on #805:10957: "put the resolver into the gateway container").
Listens only on `bridgeName` + `lo`; authoritative for
`<hive-domain>`, `forge.<hive>`, `matrix.<hive>` answering with
the bridge IP; forwards everything else to upstream.
`resolveLocalQueries = false` keeps the gateway container's own
resolver untouched.
Asserts `services.hyperhive.domain != null` + `gateway.enable =
true` — both required for the resolver to be meaningful.
Docs: new `docs/network.md` covering v1 vs v2 split, container shape
rationale, default addressing, resolver behaviour, firewall posture.
`nix flake check` clean.
Defensive against the fresh-boot-before-any-agent-spawn window where
the bind-mount source wouldn't exist from c0re's per-agent
`set_nspawn_flags` mkdir chain yet. nspawn auto-creates missing
sources, so this is intent-explicit rather than functional fix.
Switch per-agent gateway upstreams from TCP loopback to unix-domain
socket when the agent has opted in via `hyperhive.web.useUnixSocket`
(#822). Coexists with the TCP path during rollout.
Changes:
- New `agentSocketsFile` option (default
`/var/lib/hyperhive/agent-sockets.json`) — c0re writes the map
there via `hive_c0re::agent_sockets::write` (#809).
- `agentSocketsTable = lib.importJSON ...` (graceful empty when
file missing).
- `agentUpstreamFor name port` picks `http://unix:<path>:/` when the
socket has a JSON entry AND the file exists at eval time; else
`http://127.0.0.1:<port>/`. Path-exists gate guards against
c0re's blanket-emit shape during the canary window (agents in
`agent-sockets.json` who haven't actually flipped have no
bound socket on disk → fall back to TCP). Damocles will ship a
`.bound` marker filter on the c0re side (#784 step 2d
follow-up); once that's in, the path-exists check is redundant
but harmless. Step 4 drops it entirely along with the TCP
fallback.
- `containers.hive-gateway.bindMounts."/run/hive-agent"` —
read-only, unconditional. Inert when no agents have opted in.
Required so nginx inside the gateway container can `connect(2)`
to the per-agent sockets damocles's #813 bind-mounts into agent
containers at the same paths.
Docs:
- `docs/gateway.md::Per-agent UDS upstream (#784)` — full rollout
flow, subdir-bind rationale (damocles #813), eval-time gate
explainer, step 4 drop plan.
`nix flake check` clean; `nix fmt` clean.
Canary plan: once #822 (`useUnixSocket` option) lands + this PR
merges, manager flips atlas's agent.nix to `useUnixSocket = true`
via the config-update flow. End-to-end validation against atlas
before broader rollout.
mara on #755: "e.g. /agent/name should show an error page stating
that the agent could not be found if missing in json or that it is
not reachable if we get a connection error. we dont want a fully
generic fallback, only for routes already special cased in the
nginx config."
Adds two static HTML pages built at deploy time via
`pkgs.runCommand "hyperhive-agent-error-pages"`:
- **not-found.html** — served when `/agent/<unknown>/...` hits the
bare `/agent/` catch-all. The catch-all `return 404`s, and
`error_page 404 = /__hive_agent_not_found` rewrites to the static
page.
- **unreachable.html** — served when `/agent/<known>/...` proxy_pass
to the harness returns 502 / 503 / 504. `proxy_intercept_errors
on` + `error_page 502 503 504 = /__hive_agent_unreachable` on each
per-agent location block rewrites to the static page.
Mechanics:
- `agentErrorPagesDir` (in the `let` block) is a `runCommand` that
emits two HTML files using a `<<EOF` heredoc — no template engine
needed.
- Two `internal` nginx locations (`= /__hive_agent_not_found`,
`= /__hive_agent_unreachable`) `alias` the exact files. `internal`
keeps the URIs unreachable from direct operator request — only
nginx's own error-handling can hit them.
- Per-agent location blocks pick up the `error_page` directive
through the existing `lib.mapAttrs'` over `agentPortsTable`. No
per-agent generated content; same static page for all.
- `/agent/` catch-all generates from a tiny optionalAttrs alongside
the per-agent block — both are no-op when the agent table is
empty (matches the pre-#15 shape).
Pages: minimal inline CSS, catppuccin palette matching the
dashboard (`#1e1e2e` bg, `#cdd6f4` text, `#cba6f7` not-found heading,
`#f9e2af` unreachable heading). No frontend-dist dependency — render
even when hive-c0re is down. Both link back to `/`.
Per mara's "only for routes already special cased" — scope stays
narrow. Forge / matrix / fluffychat keep nginx defaults; extending
the custom-error pattern to other vhosts is a separate follow-up
if/when needed.
Verified:
- nginx location attrset has `["/", "/agent/", "= /__hive_agent_not_found", "= /__hive_agent_unreachable"]`
- container toplevel builds clean (`nixos-system-hive-gateway-26.05pre-git`)
- `docs/gateway.md::Per-agent error pages` section captures the
design + rationale + intentional narrowness
Closes#755.
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.
argus on #775 v3: "the `gatewayHost` option description's
server_name-vs-gatewayHost essay + federation SRV note are also
candidates for [docs/gateway.md] section."
Cuts the gatewayHost option's description from ~40 lines (with
inline duplication of the discovery flow, when-to-set-which, and
federation port caveat) down to ~8 lines pointing at
`docs/gateway.md`. The brief `server_name vs gatewayHost` clarifier
stays in code because it disambiguates two SIMILAR-LOOKING options
on the same module — operators reading option docs need the
distinction inline, not behind a doc link.
Also trimmed `matrix.gui.enable` + `matrix.gui.package` descriptions
to similar shapes — point at docs/gateway.md for the architecture,
keep the override-shape hints in code.
Push includes the rebase onto current main (#764 + 0af6ea1 + others
landed since #775 was opened; cherry-picked commits get skipped
cleanly).
Net: matrix.nix loses ~70 lines of inline prose. No behavioral
change (verified gatewayHost still resolves to `matrix.<hive>`).
mara on PR #775: "this is too much docs in code - move bigger picture
stuff to md files and put refs in code"
New `docs/gateway.md` consolidates the gateway architecture story
that was spreading across long inline comments in `hive-gateway.nix`,
`hive-matrix.nix`, and `hive-forge.nix`:
- vhost map (which URL serves what, which upstream, which option)
- matrix discovery flow (.well-known → sub-domain delegation
sequence)
- Accept-header SPA fallback pattern (#686 / #729 design history)
- local-dev `localHostsEntry` story
- sub-domain rationale (mara verdict tracking) + when sub-path is
right (hyperhive-internal apps)
- per-vhost tuning knobs (forge LFS, matrix long-poll, agent SSE)
- sequencing history (which PR added which routing piece)
In-code comments in the two nix modules get trimmed to short refs
into the doc — keeps the *why* in the markdown while the *what*
stays alongside the code:
- hive-gateway.nix: top-of-file comment, `agentPortsTable`,
`appendHttpConfig`, every location block + vhost
- hive-matrix.nix: `fluffychat-web-fixed`, `fluffychat-web-imaging`,
the dart compile postInstall
README.md gets a new row in the docs table pointing at gateway.md.
Verified `nix eval` still resolves the same vhost + location layout
after the comment trim — no behavioral change, just less in-code
prose.
mara on #764:9897: "host the fluffy chat app at / as follow up?"
Moves fluffychat-web from the bare-domain sub-path
(`<hive>/matrix/`) to the matrix sub-domain root
(`matrix.<hive>/`). Follow-up to #764 (matrix vhost itself), per
mara's gateway-architecture verdict (sub-domain for external standard
apps, sub-path for hyperhive-internal). Stacked on
`atlas/747-matrix-behind-gateway` — depends on #764 landing first.
## Mechanics
**hive-matrix.nix:**
- Drop `flutterBuildFlags = [ "--base-href" "/matrix/" ]` from
`fluffychat-web-fixed`. Upstream default `--base-href "/"` is correct
at sub-domain root.
- Update option docs to reflect new mount point.
**hive-gateway.nix:**
- `$matrix_spa_target` map target flips from `/matrix/index.html` →
`/index.html` (sub-domain root now).
- New `<hive>/matrix/*` location: `rewrite ^/matrix/(.*)$
matrix.<hive>/$1 permanent;` — 301 redirect preserves bookmark +
deep-link compatibility for `<hive>/matrix/#/rooms/...` URLs during
the transition.
- `<hive>/matrix/config.json` location removed (moved to `/config.json`
on the matrix vhost).
- Matrix vhost (#764) gains `/` location: serves fluffychat dist as
static files with the Accept-header SPA fallback (`/_matrix/`
proxying to tuwunel keeps working via nginx longer-prefix-wins
precedence). When `gui.enable = false`, `/` returns 404 cleanly.
- Matrix vhost gains `= /config.json` for the FluffyChat boot-config
pre-fill (#736).
## Verified
```
vhosts: ["_", "forge.test.local", "matrix.test.local"]
bare locations: ["/", "/matrix/", "= /.well-known/matrix/client",
"= /.well-known/matrix/server"]
matrix vhost locations: ["/", "/_matrix/", "= /config.json"]
/matrix/ extraConfig: "rewrite ^/matrix/(.*)$ http://matrix.test.local/$1 permanent;"
matrix vhost / alias: /nix/store/...fluffychat-web-2.6.0/
```
Full container toplevel builds clean.
## Risk
Medium. Two breaking changes for operators:
1. **Bookmark migration**: `http://<hive>/matrix/#/rooms/...` 301s
to `http://matrix.<hive>/#/rooms/...`. Browser bookmarks +
shared links keep working via the redirect; can be cleaned up
once it's been in the wild long enough.
2. **fluffychat-web dist hash changes**: dropping the
`--base-href "/matrix/"` flag changes the derivation hash, so
`gui.package` rebuilds even though the source is the same.
Operators on substitute caches will fetch the new dist; building
from source takes the same time as before.
The `.well-known/matrix/{client,server}` delegation (already
advertising `matrix.<hive>` per #764) means matrix clients
auto-discover the new location — no client config change needed.
## Sequencing
**Depends on #764** — needs the matrix vhost to host the new `/`
location. Merge after #764 lands + soaks. If #764 changes shape
during review I'll rebase + force-push.
Closes#772.
Folds both 🟡 notes from argus's #764 review:
1. **Empty-string assertion on `cfg.gatewayHost`**: same footgun as
the forge.domain rejection from #754 — empty would render `.<hive>`
shaped garbage in both nginx server_name (wildcard catch-all,
surprising) and /etc/hosts (invalid entry). Fail loud at toplevel
build with a message pointing at `null` as the right opt-out.
2. **Federation port-8448 caveat in `gatewayHost` docs**: when the
gateway listens on 80, `.well-known/matrix/server` advertises
`${gatewayHost}` with no port suffix → matrix federation spec
falls back to port 8448 → no listener on 8448 → cross-hive
federation requires either `_matrix._tcp.${gatewayHost}` SRV
record OR `services.hyperhive.matrix.openFirewall = true`.
Hyperhive is mostly closed/internal so this rarely bites, but
the option docs now flag it for the federation-curious operator.
Verified: `gatewayHost = ""` triggers the new assertion at toplevel
build with the expected message; default still resolves to
`matrix.<hive>` cleanly.
mara on #747:9722: "this still seems to be an issue in current version"
(after #751 closed without merge). Mirroring the forge sub-domain
pattern just merged as #754 for matrix per mara's #749:9609 verdict
(sub-domain over sub-path for forge + matrix, "not user-visible for
matrix because the .well-known/matrix/{client,server} redirect routes
clients through automatically").
## Mechanics
**New `services.hyperhive.matrix.gatewayHost`** — nullable str, defaults
to `matrix.<services.hyperhive.domain>` when hive-domain set, else
null. Full hostname (`matrix.darkest.space`, `homeserver.internal.lan`)
for bespoke shapes per mara's #754:9684 "specify full domain in
options instead" pattern.
**Gateway:** new `server { server_name = matrixCfg.gatewayHost; }`
block proxying `/_matrix/...` → `http://127.0.0.1:<httpPort>/_matrix/...`
with matrix-spec CORS + tuned for long-poll `/sync` (1h timeout) +
typical media uploads (50M body cap). `/` returns 404 — nothing
else lives at the matrix vhost. Matches the forge vhost shape from #754.
**`.well-known/matrix/{client,server}`** (already served at bare hive-
domain since #660): now points at `matrixCfg.gatewayHost` (no port
suffix when gateway is on the canonical port 80) instead of the
direct `<hive-domain>:<httpPort>` shape. Falls back to direct shape
when `gatewayHost = null` (no hive-domain, or operator nulled it).
**`localHostsEntry` extension**: `/etc/hosts` (when set) now adds the
matrix sub-domain → 127.0.0.1 alongside hive-domain + forge.domain.
`lib.unique` collapses any duplicate (edge case if operator sets
gatewayHost equal to hive-domain).
## Verified via `nix eval`
```
vhosts: ["_", "forge.test.local", "matrix.test.local"]
gatewayHost: "matrix.test.local"
client wellknown: m.homeserver.base_url = "http://matrix.test.local"
server wellknown: m.server = "matrix.test.local"
/etc/hosts: ["test.local", "forge.test.local", "matrix.test.local"]
```
## What this fixes for #747
mara's HAR showed `GET /.well-known/matrix/client` and
`GET /_matrix/client/versions` both failing on `pr1ma.darkest.space`:
1. **`.well-known/matrix/client`** was advertising
`http://pr1ma.darkest.space:8008` — that URL only works if tuwunel's
port 8008 is firewall-open to the operator's browser (it isn't by
default — `services.hyperhive.matrix.openFirewall` defaults to false
since #651). Now advertises `http://matrix.pr1ma.darkest.space/`
which goes through the gateway on the (already-open) port 80.
2. **`/_matrix/client/versions`** was hitting the bare-domain `"_"`
vhost, which has no `/_matrix/` location — fell through to `/` →
c0re's dashboard upstream → 404. Now hits the new `matrix.<hive>`
vhost which proxies the request to tuwunel cleanly.
server_name + serverName unaffected — matrix identifiers (`@alice:<hive>`)
still embed the bare hive-domain per #660; only the wire-level transport
URL moves to the sub-domain.
## Risk
Medium. Existing matrix tokens / sessions stay valid because:
- `serverName` (the identifier domain) doesn't change
- tuwunel's `/_matrix/` endpoints serve the same requests, just reached
via the new sub-domain instead of the direct port
Operators with `services.hyperhive.matrix.openFirewall = true` and
external clients reaching `:8008` directly keep working too — the
sub-domain vhost is additive, doesn't take away the direct port.
## Sequencing
This is a parallel matrix-side mirror of #754 (forge). Both follow
the same mara-verdict pattern; once both have soaked, the gateway-
behind-everything story is done for v0.
Closes#747.
mara on PR #754: "would it be better to specify full forge domain in
options instead?"
Drops the awkward `cfg.subdomain` label option. Now `cfg.domain` is
the single source of truth for both the forgejo `DOMAIN` setting
(existing semantics) AND the gateway vhost server-name (new).
## Before / after
```nix
# before: separate label + cfg.domain juggling
services.hyperhive.forge.subdomain = "forge"; # → forge.<hive>
services.hyperhive.forge.domain = "localhost"; # unused for vhost
# after: full domain, single option
services.hyperhive.forge.domain = "forge.darkest.space"; # ← used for ROOT_URL + vhost
```
## Default
`cfg.domain` default auto-derives:
- `forge.<services.hyperhive.domain>` when hive-domain is set
- `"localhost"` otherwise (pre-#749 direct-on-port shape)
So the common case (hive-domain set) gets `forge.<hive>` for free,
operators with a bespoke shape (`git.example.com`) set the full
hostname directly.
## Assertions
- `cfg.domain != ""` — empty would render `.<hive>` shaped garbage
in both server_name + /etc/hosts.
- `cfg.behindGateway → gateway.enable` — can't route through a
gateway that isn't running.
(The previous "subdomain = empty" assertion is dropped — that
edge case is gone with the rename.)
## Verified
- default with `hyperhive.domain = "test.local"` → `forge.test.local`,
`ROOT_URL = http://forge.test.local/`, vhost present
- `forge.domain = "git.example.com"` → `git.example.com`,
`ROOT_URL = http://git.example.com/`, vhost = `["_", "git.example.com"]`
- `gateway.enable = false` → `forge.domain` falls back to `localhost`,
`ROOT_URL = http://localhost:3000/`, no gateway vhost
(`behindGateway = false`)
- `/etc/hosts` (when `localHostsEntry = true`) → unique entries for
hive-domain + forge.domain (de-duped via `lib.unique` for the
edge case where forge.domain = hive-domain)
- full container toplevel builds clean
## PR title
(Will fix the PR title separately — still says "/forge/" which is
wrong since the rewrite to sub-domain shape.)
argus on PR #754 v2 review:
> `subdomain = ""` edge case: when `cfg.subdomain = ""`, the
> `localHostsEntry` appends ".${domain}" (invalid hostname; bare
> domain is already covered) and the virtualHosts key becomes
> ".${domain}" (nginx treats this as a wildcard catch-all, not a
> bare-domain server block). docs call this "advanced: collides
> with dashboard server block" — the actual nginx behavior is
> more surprising than that.
Fix: reject `""` at assertion time rather than ship the surprising
behaviour. Bare-domain landing is what the dashboard already
serves; there's no use case for `""` that null doesn't already
cover. Updated option description + dropped the now-dead branch
from the `subdomain` let-binding.
Verified: `services.hyperhive.forge.subdomain = ""` triggers the
new assertion at toplevel build with a clear message pointing at
`null` as the right opt-out. Default + `null` paths still build
clean.
mara on #749:9609: "we will go with sub domains for forge and matrix
(redirected in well known in the latter case, not user visible). close /
fix PRs you have open that dont match this."
Reshapes the v1 sub-path (`<host>/forge/`) approach into a sub-domain
vhost (`forge.<host>/`) per the mara verdict. matrix gets the same
treatment in damocles's #751 follow-up.
## Why sub-domain
- forgejo's default `ROOT_URL = http://<host>/` works without any
`X-Forwarded-Prefix` gymnastics — sub-domain hosting is the
canonical Forgejo deploy shape, matches every upstream-doc example.
- Cookie / storage isolation between the dashboard and forge (XSS blast
radius shrinks; a future forge XSS can't reach dashboard session).
- matches the matrix-spec pattern that #751 wires up for the
homeserver.
## Mechanics
**forge options:**
- `services.hyperhive.forge.subdomain` — nullable str, default `"forge"`
→ rendered sub-domain is `forge.<hive-domain>`. Set to `null` to opt
out (forge stays direct on `httpPort`); set to `""` for bare-domain
landing (advanced, collides with dashboard).
- `services.hyperhive.forge.rootUrl` — nullable str override. When
null, auto-derived: `http://<subdomain>.<hive>/` when gateway is on
+ subdomain set, else `http://<domain>:<httpPort>/` (direct).
- **Asserts** rootUrl ends with `/` (argus 🟡 on #754: forgejo's
ROOT_URL contract requires trailing slash, else emits
`https://forge.example.com.user.id` shaped garbage). Asserts
`subdomain != null` requires `hyperhive.domain` set.
**gateway:**
- New `virtualHosts."<subdomain>.<hive-domain>"` server block —
separate from the `"_"` catch-all. Proxies all `/` →
`http://127.0.0.1:<forge.httpPort>/` so forgejo handles requests at
root (no prefix translation needed; matches the upstream-default
ROOT_URL shape).
- Git-tuned: `client_max_body_size 1G`, `proxy_read_timeout 1h`,
`proxy_send_timeout 1h`, `proxy_buffering off`,
`proxyWebsockets = true`. SSH stays direct on `cfg.sshPort`.
- `networking.hosts` (when `localHostsEntry = true`) now also adds
`forge.<hive-domain> -> 127.0.0.1` for the dev loop.
## Verified
- `nix eval ROOT_URL` → `http://forge.test.local/` (default with
gateway on)
- `nix eval ROOT_URL` with `gateway.enable = false` → `http://localhost:3000/`
(current direct shape preserved)
- `nix eval virtualHosts attrs` → `["_", "forge.test.local"]`
- `nix eval networking.hosts` with `localHostsEntry = true` →
`{"127.0.0.1": ["test.local", "forge.test.local"], ...}`
- bad rootUrl (no trailing /) triggers assertion at toplevel build
with the spelled-out forgejo failure mode
- full container toplevel builds clean
(`nixos-system-hive-gateway-26.05pre-git`)
## Migration
ROOT_URL change is a one-way migration on rebuild:
- Existing agent `git remote origin` URLs (`http://localhost:3000/...`)
**keep working** — forgejo accepts any inbound URL; the URL on the
agent side is unchanged.
- New clone-link copy-paste from forge UI uses `forge.<hive>/...` —
operators copying clones after this lands need to go through the
new sub-domain.
- Direct browsing on `:3000` shows pages with `forge.<hive>` links →
works if hosts entry / DNS resolves, broken otherwise. Operators
should switch to `http://forge.<hive>/`.
## Out of scope
- TLS termination (mara explicit on #15: no TLS v0)
- SSH-over-HTTPS / wildcard cert provisioning
- matrix sub-domain (damocles's #751, sibling work)
Closes#749. Addresses argus 🟡 on #754.
mara on PR #740 comment 9295: "we decided to go with the json" (issue #15 comment 9270:
"nginx container lives in system config, so it cannot be just rebuilt
from meta flake. go for the json file the c0re writes").
Drops:
- `cfg.agents` listOf str option
- Replicated FNV-1a hash + char-code table + manager-port special case
- Drift-hazard comment (no more rust↔nix constant sync)
Adds:
- `cfg.agentPortsFile = "/var/lib/hyperhive/agent-ports.json"` (default,
nullable to disable) — path to a JSON map of `{ "<name>": <port> }`
written by hive-c0re on every topology change.
- `agentPortsTable` reads the file at eval time via
`builtins.fromJSON (builtins.readFile path)`, guarded by
`builtins.pathExists` so a missing file gracefully defaults to `{}`.
- Per-agent locations generated via `lib.mapAttrs'` over the table —
one location block per entry; empty table → empty attrset → no
per-agent blocks, pre-#15 shape.
Rust-side dependency: hive-c0re needs to emit the JSON file on every
topology change. Coordinating with damocles via a separate ping — the
nix side ships now with safe defaults (missing file = no routes, no
behavior change vs main).
Verified:
- nix eval with `/tmp/test-agent-ports.json` → 4 per-agent blocks at
correct ports (8178 iris, 8267 argus, 8304 atlas, 8549 damocles)
- nix eval with nonexistent file → only `/` location (graceful default)
- full container toplevel builds clean with matrix on
Empty file case mirrors the previous empty-list default — purely
additive, old `<host>:<port>/` direct reach untouched, no per-agent
blocks until c0re writes the JSON. Operator can also `null` the
option to disable entirely.
Per mara on #14 (comment 9081): focused, purely additive to what's
there, no TLS / no manager special cases, old `<host>:<port>/` path
keeps working. Builds on iris's #731 (agent UI now serves
document-relative URLs so it works under any nginx prefix).
Mechanics:
- New `services.hyperhive.gateway.agents` option (`listOf str`,
default `[]`) lists sub-agent names to expose at
`/agent/<name>/` through the gateway.
- For each name, generate one `location /agent/<name>/` block that
`proxy_pass`es to `http://127.0.0.1:<port>/`, where `<port>`
is computed from the same FNV-1a hash hive-c0re uses internally
(`lifecycle::agent_web_port`).
- Trailing-slash pair on location + proxy_pass strips the
`/agent/<name>` prefix on the upstream side — agent server
receives `GET /`, `GET /api/state`, `GET /screen/ws`, etc. as if
reached directly on its port.
- `X-Forwarded-Prefix` set so the harness can build correct absolute
URLs for cases where document-relative isn't enough.
- `proxyWebsockets = true` + `proxy_buffering off` keeps SSE
+ WS endpoints working transparently.
- Empty `cfg.agents` (default) → no per-agent blocks generated.
- Manager not included — already gets `/` via the c0re upstream.
FNV-1a hash replicated in nix to match `lifecycle::agent_web_port`
line-for-line. Verified against rust output for 8 representative
agent names:
agent | nix | rust | match
iris | 8178 | 8178 | ✓
atlas | 8304 | 8304 | ✓
argus | 8267 | 8267 | ✓
damocles | 8549 | 8549 | ✓
manager | 8000 | 8000 | ✓ (special case)
dmatrix | 8266 | 8266 | ✓
triage | 8737 | 8737 | ✓
bitburner | 8658 | 8658 | ✓
Drift hazard documented in the let-block comment: if the rust
constants change (MANAGER_PORT, WEB_PORT_BASE, WEB_PORT_RANGE, or
the FNV-1a parameters), the nix copy needs a lockstep bump or
gateway will proxy to wrong ports. Tracked in the option's
description as a follow-up to single-source via
`/var/lib/hyperhive/meta/topology.json` lib.importJSON OR runtime
nginx-include written by c0re.
Char-code lookup table covers `[a-z0-9_-]` — the current
`hyperhive.user.name` alphabet. Names with other chars produce an
eval-time error rather than a silent wrong hash.
Verified:
- `nix eval` on the locations attrset for [iris atlas argus damocles]
→ correct ports (matching rust impl) on each `/agent/<name>/` block
- empty `cfg.agents` default → no per-agent blocks (`[ "/" ]` only)
- full container toplevel builds cleanly with 7 agents + matrix on
(`nixos-system-hive-gateway-26.05pre-git`)
Sequencing per mara: this is #15 v0 (gateway-side per-agent routing,
purely additive). #14 netns isolation follows once this soaks.
Out of scope: TLS, manager special-case routing, per-agent unix
sockets (mara: "at some point the agent servers will be domain
sockets"), CORS workaround removal at `POST /answer-question/{id}`,
gateway auth.
Closes#15 v0.
mara on PR #729: "this still feels hacky - is there a proper way to do this?"
damocles: agreed, "Accept-header map is meaningfully better than the
allowlist [...] one map definition that encodes browser semantics directly,
vs ~20 extensions to keep synced with whatever fluffychat (and any future
hyperhive-served SPA) decides to ship".
The previous shape (#684 catch-all regex, then this PR v1's
extension allowlist) leaned on heuristics to distinguish "missing
asset → 404" from "unknown SPA route → fall back to index.html".
Both shapes were fragile against a SPA shipping a new extension,
and the allowlist became dead code the moment a route ended in
`.html-ish-suffix`.
The proper distinction lives at the HTTP layer: top-frame browser
navigations send `Accept: text/html,...` (chrome/firefox/safari are
consistent on this). Asset fetches from script tags / img / fetch() /
XHR send asset-typed Accepts (`image/*`, `application/javascript`,
`*/*`) without `text/html`.
Mechanics: an `nginx http`-context `map` keyed on `$http_accept`
emits either `/matrix/index.html` (navigation) or a sentinel
nonexistent path (`/__matrix_spa_no_html_fallback`); the location's
`try_files $uri $uri/ $matrix_spa_target =404;` does the right thing
for both cases. No extension list, no regex narrowing, no `if` block,
no named-location fallback.
The `map` lives in `services.nginx.appendHttpConfig` (only added
when the matrix GUI is on, otherwise no `map` directive at all).
The location's `extraConfig` is now a single `try_files` line.
Verified via `nix eval` on both the rendered `appendHttpConfig` and
the location's `extraConfig`. Full closure build pending operator
deploy.
Closes#686.
mara's first deploy hit:
Error: Couldn't resolve the package 'matrix' in 'package:matrix/matrix.dart'.
/nix/store/k9j8ns45fz7rpjp6rzk33ydjng67pgm0-source/web/native_executor.dart:1:8:
Error: Not found: 'package:matrix/matrix.dart'
Root cause: `dart compile js` walks up from the source file's dir to
find `.dart_tool/package_config.json`. My previous postInstall passed
`$src/web/native_executor.dart` — pointing dart at the unpacked nix
source, which has no `.dart_tool/` (pub-get wrote it to the build CWD,
not the read-only store path).
Fix: use a relative path `web/native_executor.dart`. nixpkgs's
buildFlutterApplication leaves CWD at the source root in postInstall
(its installPhase is just `cp -r build/web "$out"` with no `cd`
first — see `pkgs/development/compilers/flutter/build-support/
build-flutter-application.nix`), so the relative path walks up from
`web/` to the build CWD where pub-get's package_config lives.
Verified by `nix eval`; full closure build pending operator deploy.
Followup to #697 (the original fix; merged but mara's deploy then
surfaced this regression).
mara on PR #697: "this still puts us in the position of having to update
that dependency in sync with upstream. cant we use the one from the
nixpkgs build directly somehow?"
Drops the parallel `fetchurl` + sha256 pin in `fluffychat-web-imaging`.
Source now comes from
`pkgs.fluffychat-web.passthru.pubspecLock.dependencySources.native_imaging`
— the exact derivation that fluffychat-web's flutter build already pulls
into its pub-cache for the dart-side bindings. Version likewise pulled
from `passthru.pubspecLock.dependencyVersions.native_imaging`.
Result: when nixpkgs bumps `pkgs.fluffychat-web` (and with it the
pubspec.lock-resolved native_imaging version), our build automatically
picks up the matching source. No parallel hash to bump, no risk of drift
between the dart-side bindings and the wasm-side C compile.
Verified the build still works against the pub-cache-sourced derivation
(same Makefile, same emscripten flow):
$ nix-build test-passthru.nix
...
buildPhase completed in 52 seconds
$ ls /nix/store/.../fluffychat-web-imaging-0.4.0/
Imaging.js (9956 bytes)
Imaging.wasm (67363 bytes)
Byte-for-byte identical to the previous v2 output, just sourced from
the same store path fluffychat-web itself uses.
Follow-up to argus's v2 🟢 review of #697. No regression on the prior
review feedback — `make -C js` + explicit installPhase paths still in place.