Commit graph hyperhive/nix
Author SHA1 Message Date
atlas
09bcf9fd7a chore(ci): drop issue tag from hive-ci-prefetch 401 log string
The runner's prefetch 401/403 diagnostic baked a literal issue tag into
its runtime journal output. Per the hive-wide rule (no issue/PR #NNN tags
in code), strip it — the message already explains the stale-core-token
condition and the operator remedy in full prose, so nothing is lost.
2026-06-06 12:34:51 +02:00
atlas
d9c7b7fcc1 fix(ci): hive-ci prefetch — correct partOf unit name + 401-harden token fetch
Two bugs that together kept the runner registration token from
refreshing (#1475):

1. Unit name: the prefetch's before/wantedBy/partOf targeted
   nixos-container@hive-ci.service, but a declarative containers.<n> is
   the host unit container@<n>.service (confirmed against the live
   container@hive-matrix.service during the #1465 incident). The wrong
   name made all three silent no-ops, so the partOf never bound — the
   RemainAfterExit oneshot stayed 'active (exited)' and never re-ran on
   nixos-container restart, leaving the stale token in place. Corrected
   to container@hive-ci.service.

2. 401-hardening: the registration-token fetch used a bare curl -sf | jq,
   so a forge-core-token that is stale/invalid for the current forge
   (e.g. after a forge rebuild) 401s and fails silently every attempt for
   the full 60s loop, then exits with a misleading 'core token absent or
   forge unreachable'. Now capture the HTTP status and fail fast + loudly
   on 401/403 with a clear message pointing at re-minting the core token.
2026-06-06 11:58:13 +02:00
atlas
c5e23fd3a3 fix(matrix): correct container ordering unit name to container@<n>.service
The host systemd unit for a declarative nixos-container is
container@<name>.service (confirmed from the live
container@hive-matrix.service status), not nixos-container@. The prior
commit's nixos-container@ ordering targeted a non-existent unit (a
silent no-op). Use container@hive-{matrix,gateway}.service so the
after-ordering actually takes effect.
2026-06-06 11:51:37 +02:00
atlas
43776afbfd fix(matrix): order hive-matrix container start after the gateway (resolver)
Per operator review on the PR: when the network module is on, the
matrix container's resolver is the dnsmasq in the gateway container, so
order the matrix container start after the gateway container. This is
robustness for tuwunel's lazy federation lookups, not a boot
requirement — the boot fix is the resolv.conf nameserver line (the
failure was a parse error on an empty resolv.conf, not connectivity).
Soft 'after' (not 'requires') keeps lifecycles decoupled; network.enable
asserts gateway.enable so the gateway container unit always exists.
2026-06-06 11:02:25 +02:00
atlas
38f2435767 fix(matrix): give hive-matrix container a DNS resolver so tuwunel can boot
tuwunel hard-fails to start when /etc/resolv.conf has no nameserver
line (Failed to configure DNS resolver: no nameservers found in
config -> exit 1 -> systemd start-limit). The declarative
containers.hive-matrix generates its own resolv.conf via resolvconf
and, unlike agent containers whose resolv.conf is written by
hive-c0re's lifecycle, has no nameserver source -> it comes up empty
(just 'options edns0'). Defaulting network.enable on surfaced this:
the host DNS moved to the bridge dnsmasq but the container was never
pointed at it, so the homeserver could not boot, taking down matrix
for all agents.

Point the container at the hive resolver (the dnsmasq the network
module runs at bridgeIp) when the network module is enabled; the
container always shares the host netns (privateNetwork = false) so it
reaches bridgeIp whether or not isolateContainers is set. With the
network module off, inherit the host resolv.conf.
2026-06-06 10:50:55 +02:00
atlas
48b7c24265 fix(nix): emit explicit port in .well-known/matrix/server delegation
The matrix server-server spec resolves a portless m.server delegated
host to the federation default port 8448 (not 443 — the URL convention
that https implies 443 does not apply to the m.server field). The
gateway reused publicPortSuffix, which intentionally drops :443 for the
client base_url, so .well-known/matrix/server advertised the bare
gateway host. Peers then federated to <gatewayHost>:8448, which nothing
listens on, while the federation endpoint actually lives on the gateway
443 vhost (/_matrix/ proxied to tuwunel). Always emit the explicit port
in the federation delegation so peers reach the live 443 endpoint.
2026-06-06 00:37:07 +02:00
damocles
e029944cb3 docs: specify certFingerprint format and generation recipe 2026-06-05 23:38:54 +02:00
damocles
c5c47b29b4 fix: own /run/hive-agent as hive-core so the unprivileged daemon can create per-agent socket dirs on first launch 2026-06-05 23:30:50 +02:00
damocles
4c8e916ae6 fix: add /run/lock to hive-priv ReadWritePaths so nixos-container can lock on first launch 2026-06-05 23:20:35 +02:00
iris
60042d877e docs(options): emit markdown only, render HTML in website
Per #1407 review: the options reference should be markdown here; the
HTML + CSS belongs in the website repo where the theme lives and the
stylesheet can be shared with /docs.

- nix/docs/default.nix: drop the cmark-gfm HTML rendering, the inline
  <style>, and the HTML index. Emit index.md + host.md + agent.md only
  (CommonMark from nixosOptionsDoc).
- Remove nix/docs/style.css.
- Update docs/gotchas.md + CLAUDE.md to describe the md-only output and
  point at the website for HTML rendering.

The website (hyperhive/website#26) renders these .md to themed HTML for
/options/, sharing one docs.css with the prose /docs/ tree.
2026-06-05 19:36:54 +02:00
atlas
e5da9654f1 feat: add hyperhive.availableModels option for configurable model picker
Adds a per-agent hyperhive.availableModels list option (default
[ haiku sonnet opus ]) rendered into the HIVE_AVAILABLE_MODELS env var
(comma-separated) so the per-agent web UI model quick-picker lists
exactly the configured models instead of a hardcoded set. Operators set
a shared default hive-wide or narrow it per-agent.

An assertion guards that hyperhive.model is present in the list so the
picker can always offer the model the agent is actually running.
2026-06-05 13:47:33 +02:00
atlas
4f446269ae fix: re-run hive-ci-prefetch on every container restart via partOf
The hive-ci-prefetch oneshot has RemainAfterExit=true and is wired to the
container only via wantedBy + before. Once it runs successfully it stays
'active (exited)' indefinitely, so systemd skips it on subsequent container
restarts. The runner-token file it wrote is never refreshed.

This breaks the runner after its first registration: the token written on
the first successful boot is either a placeholder (forge-core-token wasn't
ready yet) or a registration token that has since been consumed/rotated.
On the next container restart prefetch does not re-run, the stale token
persists, and the in-container register service fails with
'invalid_argument: runner registration token not found' — exactly the
symptom in the field (worked briefly, two orphan runners registered, then
permanently offline).

Add partOf = [ nixos-container@hive-ci.service ] so a container stop/restart
propagates to the prefetch unit, forcing it to re-run and fetch a fresh
registration token before the container comes back up. before= still orders
it ahead of the container start within the same transaction.
2026-06-05 00:39:13 +02:00
damocles
6c16d25039 fix(#1304) scope state/harness chowns to own agent dir in hive-agent-user-migrate 2026-06-04 21:04:12 +02:00
atlas
60adb5aac9 fix: chmod .claude dir 0755 in hive-agent-user-migrate activation script
ensure_claude_dir creates the dir as 0755 but cannot re-chmod after
hive-agent-user-migrate chowns it to the agent user (EPERM — non-owner).
The activation script runs as root and can always chmod it. Add an explicit
'chmod 755 $homeDir/.claude' after the existing chown so existing 0700
dirs from pre-fix containers are corrected on the next container boot.

Without this, all agents with pre-existing .claude dirs show 'needs login'
in the dashboard even with working sessions, because hive-core cannot list
the 0700 dir owned by a different user.
2026-06-04 20:48:39 +02:00
atlas
0a3a956d28 fix: add /var/lib/hyperhive to hive-priv ReadWritePaths
WriteAgentForgeToken and WriteAgentMatrixToken write agent state files
under /var/lib/hyperhive/agents/<name>/state/. The hive-priv service
has ProtectSystem=strict which makes all of /var read-only by default,
but /var/lib/hyperhive was missing from ReadWritePaths, causing EROFS
on every token write.

Closes #1274.
2026-06-04 15:35:46 +02:00
damocles
c2d023ba86 fix(#1269): add requires+after hive-c0re.socket to service unit 2026-06-04 15:16:39 +02:00
damocles
911dc5227b fix(#1263): repeat security headers in locations with their own add_header 2026-06-04 14:35:18 +02:00
damocles
41eb3f806c refactor: remove hyperhive.role option — there is only one role: agent 2026-06-04 14:31:44 +02:00
atlas
5ec1306c7b feat(gateway): assert hsts.enable requires TLS
Addresses mara's review: add a NixOS assertion that rejects
hsts.enable = true when no TLS mode is configured. HSTS over
plain HTTP is silently ignored by browsers; the assertion turns
a silent misconfiguration into a build-time error.
2026-06-04 13:50:22 +02:00
atlas
5264ef7d5e feat(gateway): add gateway.hsts.enable option, disabled by default
HSTS was unconditionally tied to hasTls. This is risky: enabling it on a
deployment that later loses TLS locks browsers out until max-age expires.

Add three options under services.hyperhive.gateway.hsts:
  enable          — bool, default false
  maxAge          — seconds, default 31536000 (1 year)
  includeSubDomains — bool, default true

HSTS header is now only emitted when hsts.enable = true.
2026-06-04 13:50:22 +02:00
atlas
18941848dc feat(gateway): add security headers to all vhosts
X-Frame-Options, X-Content-Type-Options, Referrer-Policy at server
scope on _, forge, and matrix vhosts. HSTS added when TLS is active.

nginx inheritance rule: locations with their own add_header (CORS API
endpoints like /.well-known/matrix/client, /_matrix/) are unaffected —
they already carry the headers they need. HTML-serving and proxy
locations pick the security headers up automatically.
2026-06-04 13:50:22 +02:00
atlas
644519f358 fix: skip matrix avatar upload when icon unchanged
matrix-avatar-sync ran on every boot (RemainAfterExit=false + path
trigger), uploading a fresh PNG each time. Every upload mints a new
mxc:// URI, which triggers a profile state event in every joined room
— resulting in timeline spam even when the avatar hasn't changed.

Fix: before uploading, compute sha256sum of /etc/hyperhive/icon.svg
and compare against the last-synced hash stored in
$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash. Skip the upload if the
hash matches. Write the hash after a successful avatar_url PUT so
subsequent boots are no-ops until the icon file changes.

Hash file lives in the agent's state dir — survives restart, cleared
on purge (so purge + re-provision gets a fresh upload). Delete to force
re-upload manually.

Closes #1231
2026-06-03 23:24:21 +02:00
atlas
dacd83f278 fix: wait for forge-core-token in prefetch instead of bailing early
On first boot (or after a wipe) hive-c0re writes forge-core-token only
after the forge container starts and the admin is provisioned. This
lags hive-c0re.service becoming active. The previous code bailed
immediately with TOKEN=placeholder if the token file was absent,
causing the runner to fail registration with 'token not found'.

Fix: merge both waits (core-token file appearance + forge API ready)
into a single 60s retry loop. The early-bail path is removed; the
script only exits cleanly if .runner is valid (writes placeholder)
or a fresh registration token is obtained.

When .runner exists but core-token is absent after 60s, we keep the
existing credentials (safe — the runner holds valid creds; next boot
will validate properly).

Closes #1224 (which tracks #1221).
2026-06-03 22:50:01 +02:00
atlas
9209094397 fix: move core-token out of hive-ci container (host-side prefetch service) 2026-06-03 22:18:53 +02:00
atlas
4d1ce6f313 fix: use path= (ExecSearchPath) to add nix to hive-ci runner PATH 2026-06-03 21:39:47 +02:00
damocles
d1fbb4aef8 feat(#1184): make agent CPU quota and memory limit configurable 2026-06-03 21:39:14 +02:00
atlas
4f80253101 fix: use lib.mkForce on hive-ci runner PATH to resolve conflicting definition 2026-06-03 21:37:27 +02:00
atlas
181535650b fix(#1199): add nix to hive-ci PATH for runner job execution
The gitea-actions-runner's host-scheme job processes use the
service's environment PATH, not the NixOS login-shell PATH.
Without this, 'nix flake check' and similar steps fail with
'nix: command not found'.

- add pkgs.nix to environment.systemPackages (ensures the binary
  is in /run/current-system/sw/bin)
- set explicit PATH on gitea-runner-hive service covering both
  /run/current-system/sw/bin and /nix/var/nix/profiles/default/bin
2026-06-03 21:25:05 +02:00
damocles
18e0e8fc2b rename bash tools: bash_run/bash_status → run/status (#1186) 2026-06-03 21:20:49 +02:00
müde
3bb45acd87 fix: give hive-bash-daemon PATH so it can spawn sh + agent tooling 2026-06-03 20:17:09 +02:00
müde
872d605b4e fix: RuntimeDirectoryPreserve=yes on bash+matrix daemons so they survive rebuild restarts 2026-06-03 19:53:08 +02:00
atlas
453ee9c44c fix(nix): merge duplicate hyperhive.extraMcpServers assignments in harness-base
bash and matrix MCP entries were both assigned to the same attribute in the
same attrset literal, causing nix evaluation to fail with "attribute already
defined". Merged both into a single lib.mkMerge list under one assignment.
2026-06-03 19:00:12 +02:00
damocles
f87a52f28f fix: address argus review yellows on #1158
- restore count in get_loose_ends: 'N local task(s):' instead of bare 'local task(s):'
- add cross-crate coupling comment to both mcp_loose_ends_dir() copies
- add comment in hive-bash-daemon service env explaining HYPERHIVE_HARNESS_DIR
  is already injected via systemd.globalEnvironment by the meta flake
2026-06-03 18:06:59 +02:00
damocles
e86160820a feat(#1106): split bash mcp into hive-bash-daemon + hive-bash-mcp bridge
- new hive-bash-mcp crate: daemon (subprocess runner, wake signals) +
  stdio bridge (mcp tools). mirrors hive-matrix-mcp architecture
- hive-ag3nt: remove bash_runner.rs and bash_run/bash_status mcp tools;
  get_loose_ends uses hive_bash_mcp:🏃:active_tasks() via crate dep
- harness-base.nix: add hive-bash-daemon systemd service + auto-inject
  bash extraMcpServer into every agent (socket: /run/hive-bash/socket)
2026-06-03 18:06:59 +02:00
damocles
9aa624d310 fix(#1159): set WorkingDirectory=/var/lib/hive-priv for hive-priv service
With ProtectSystem=strict (added in the previous commit), / is read-only
inside hive-priv. nix creates a temp result symlink in its cwd; without
WorkingDirectory that cwd is / (systemd default), causing:

  error: creating symlink /.tmp.tmp-...: Read-only file system

Point WorkingDirectory at the StateDirectory (/var/lib/hive-priv) which is
always writable under strict. Nix drops its temp symlink there instead.
2026-06-03 17:44:37 +02:00
atlas
0e3df82504 feat(#972): tighten hive-priv systemd sandbox — ProtectSystem=strict + ReadWritePaths
Replace ProtectSystem=false with ProtectSystem=strict now that privsep
is complete (issue #702 closed) and hive-c0re runs as the non-root
hive-core user.

ReadWritePaths carves out the six paths hive-priv must write to at
runtime; everything else is read-only:

  /etc/nixos-containers   — writes <container>.conf (bind-mounts,
                            network isolation, nspawn flags)
  /run/hive               — fallback socket bind when LISTEN_FDS absent
  /run/hive-agent         — chown/chmod per-agent socket directories
  /run/systemd            — container@ drop-ins (resource limits) and
                            machinectl/systemd-machined machine state
  /var/lib/nixos-containers — container rootfs (nixos-container script)
  /nix                    — nix store + profile updates during
                            container create/update

PrivateTmp=true (already set) keeps /tmp private.
StateDirectory=hive-priv (already set) keeps /var/lib/hive-priv writable
for nix's fetch/eval cache (HOME points there).

nix flake check --no-build passes.
2026-06-03 16:57:01 +02:00
atlas
d2298e183b fix(#1155): enable recommendedTlsSettings + recommendedGzipSettings in gateway nginx 2026-06-03 16:48:33 +02:00
atlas
fb93cbf5c2 feat: add tls.acme mode — nginx inside container manages Let's Encrypt
per mara's suggestion: instead of bind-mounting operator certs, let
nginx handle ACME directly inside the gateway container.

- tls.acme.enable: lets nginx obtain + renew via HTTP-01 challenge
- tls.acme.email: ACME account contact (required when enable=true)
- security.acme in container config when acme.enable
- hasTls includes acme.enable → https, httpsPort listen, firewall
- mutual exclusion assertions: acme vs selfSignedTls vs certDir
- docs/gateway.md: four-mode TLS table + ACME section

typical setup:
  selfSignedTls = false; openFirewall = true;
  tls.acme = { enable = true; email = "admin@example.com"; };
2026-06-03 16:46:52 +02:00
atlas
44122c66de feat(#594): gateway operator-cert TLS mode (tls.certDir)
add services.hyperhive.gateway.tls.certDir option: operators with a
CA-signed cert (Let's Encrypt, corporate CA) point at the ACME output
dir instead of using the auto-generated self-signed cert.

- tls.certDir: host path bind-mounted r/o at /run/hive-tls/ in gateway
- tls.certName / tls.keyName: filenames within certDir (default: cert.pem / key.pem, matches nixpkgs security.acme layout)
- hasTls = selfSignedTls || certDir != null: publicScheme=https in both cases
- assertion: selfSignedTls=true + certDir set together is an error
- openFirewall: httpsPort opened in both TLS modes
- docs/gateway.md: TLS modes table + operator-cert section
- docs updated in swarm.md peer config reference in the cert TLS section

when using operator cert, swarm peers can omit certFingerprint —
standard CA bundle handles trust automatically.
2026-06-03 16:46:52 +02:00
atlas
a2a96490d3 fix: assert gateway.enable when isolateContainers + forge.enable
isolated agents reach forge via http://forge.<domain> → nginx.
without the gateway there is nothing on port 80 to serve that
hostname. assert early rather than fail silently at runtime.

addresses argus yellow note on PR #1150.
2026-06-03 16:33:10 +02:00
atlas
806d0e4a61 fix: use forge domain URL + open 80/443 for isolated agents
when isolateContainers=true, isolated agents have dnsmasq as their
resolver — forge.<domain> resolves to bridgeIp. route HIVE_FORGE_URL
through nginx on port 80 instead of exposing the raw forge port.

- HIVE_FORGE_URL: http://<forge.domain> when isolated (nginx proxies)
- bridge firewall: open 80+443 for agents to reach nginx (gateway)
- remove forge-specific httpPort rule (no longer needed)
- update docs/gateway.md + docs/network.md

per mara's review comment on PR #1150.
2026-06-03 16:33:10 +02:00
atlas
c97120f016 fix: forge URL + firewall for isolateContainers=true
When containers run in private netns (isolateContainers=true), host
loopback is unreachable so HIVE_FORGE_URL=http://127.0.0.1:3000 breaks.

- nix/modules/hive-network.nix: when isolateContainers is on + forge
  is enabled, open forge.httpPort on the bridge interface so agents
  can reach forgejo at bridgeIp:httpPort (forgejo binds 0.0.0.0)
- nix/modules/hive-c0re.nix: HIVE_FORGE_URL switches to bridge IP
  when network.enable && isolateContainers; loopback path retained
  when isolateContainers=false
- docs/network.md: add Forge access + Forge URL rows to effects table
- docs/gateway.md: rewrite HIVE_FORGE_URL section for both modes
2026-06-03 16:33:10 +02:00
atlas
9e63c9c1d0 fix: wireguard address option default + clarify peer mesh inclusion doc 2026-06-03 15:19:16 +02:00
atlas
89609aaa6a feat(#569): wireguard inter-hive mesh option
Add opt-in WireGuard mesh support to services.hyperhive.swarm:

- swarm.peers.<domain>.wireguardPublicKey — peer's wg public key
- swarm.peers.<domain>.wireguardEndpoint  — peer's UDP endpoint (optional)
- swarm.peers.<domain>.wireguardAddress   — peer's mesh IP with prefix

- swarm.wireguard.enable           — bring up wg-hive interface
- swarm.wireguard.privateKeyFile   — path to host's wg private key
- swarm.wireguard.address          — this host's mesh IP/prefix
- swarm.wireguard.listenPort       — UDP listen port (default 51820)
- swarm.wireguard.persistentKeepalive — keepalive seconds (default 25)

When enabled, generates networking.wireguard.interfaces.wg-hive with
one peer entry per mesh-enabled swarm.peers entry. Opens listenPort
UDP on the host firewall. Adds wireguard_address to HYPERHIVE_PEERS
JSON so hive-c0re can use mesh IPs for intra-swarm routing.

Assertions guard against enable=true without privateKeyFile or address.

Also refactors networking.firewall.allowedTCPPortRanges from the
nested attrset form (which conflicted with the new allowedUDPPorts
line) to the per-attribute form.

docs/swarm.md: adds WireGuard setup section with key generation
commands, two-hive config example, NAT/keepalive notes.
2026-06-03 15:19:16 +02:00
atlas
b89c5f5334 network: fix stale assertion messages; move prose to docs/network.md 2026-06-03 11:19:29 +02:00
atlas
3db51deace network: default network.enable to services.hyperhive.enable 2026-06-03 11:19:29 +02:00
atlas
8e50ddf016 network: revert isolateContainers default to false 2026-06-03 11:19:29 +02:00
atlas
a141d157ba network: default isolateContainers to true 2026-06-03 11:19:29 +02:00
atlas
c1b40ef819 network: fix dead assertion + document HIVE_NETWORK_SUBNET host-address
Two argus review fixups:

1. Move isolateContainers assertion to an unconditional `lib.mkIf
   cfg.isolateContainers` arm. The prior placement inside
   `mkIf (enable && isolateContainers)` was dead code — the assertion
   could never fire because both flags were already true by the time the
   block activated. Now `isolateContainers=true; enable=false;` raises
   a NixOS assertion error at eval time.

2. Add comment on HIVE_NETWORK_SUBNET noting it carries the host-side
   bridge IP (e.g. "10.42.0.1/24"), not the canonical network address
   ("10.42.0.0/24"). Rust consumer must normalize via bitwise AND before
   subnet membership checks or address arithmetic.
2026-06-03 11:19:29 +02:00
atlas
39b4c65922 network: add isolateContainers option for #14 netns isolation
Adds `services.hyperhive.network.isolateContainers` (bool, default
false). When enabled alongside `network.enable`, activates:

- IP forwarding + NAT masquerade so isolated agents reach the internet
- nftables DROP rule blocking bridge-subnet → loopback (defence-in-depth
  against compromised agent reaching the c0re dashboard)
- `HIVE_NETWORK_ISOLATION`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET`
  injected into the hive-c0re service env; the Rust lifecycle reads these
  to set `PRIVATE_NETWORK`, `LOCAL_ADDRESS`, and `HOST_BRIDGE` in each
  agent container's conf

Config block rewritten as `lib.mkMerge [...]` — the prior `lib.mkIf //
lib.mkIf` pattern was invalid nix (mkIf returns a tagged value, not an
attrset; // on it is a type error). See docs/network.md for full design.
2026-06-03 11:19:29 +02:00