Commit graph hyperhive/nix
Author SHA1 Message Date
müde
40be58b0b9 fix(#702): heal proposed-config ownership in agent-user-migrate
The migration chown stomped agents/<name>/config (the proposed-config repo)
to hive-core like everything else, but hive-agent-user-migrate only re-chowned
state/harness/.claude on boot, leaving config core-owned. config is owned by
the editing parent/manager agent (core only pulls from it), so chown it to
this user too. RO self-mount in an agent's own container fails harmlessly.
2026-06-03 00:37:55 +02:00
müde
d272803d58 fix(#702): spare agent creds from migration chown; give nix a writable HOME
Two privsep follow-ups, both in the hive-c0re service:

1. The migration 'chown -R hive-core /var/lib/hyperhive' (which only
   started running once ExecStartPre stopped failing 203/EXEC) stomped every
   agent's bind-mounted creds — agents/<name>/{claude,state,harness,config}
   are owned by the per-agent/manager users — logging all agents out with no
   way back in. Scope it to everything *except* agents/, plus the agents/
   dir node itself so c0re can still create new per-agent subdirs. Each
   container's hive-agent-user-migrate activation chowns the contents back.

2. nix (prebuild 'nix build', flake-check, meta eval in c0re; nixos-container
   update->nix in priv) writes its cache under $HOME/.cache. Both services
   run as users with no home -> HOME=/var/empty (unwritable) -> Lix cache
   init fails, rebuilds error out. Set HOME to each service's StateDirectory
   (adding one for hive-priv).
2026-06-03 00:33:15 +02:00
müde
e7b6896e4c fix(#702): give hive-priv a PATH; route forge/matrix list through priv
hive-priv had no PATH, so the nixos-container it runs for every container
op (incl. lifecycle::list) failed ENOENT. build_all swallows that into an
empty list — the 'no managed containers' symptom. Give the helper the
minimal set nixos-container shells out to (nixos-container, nix, util-linux,
e2fsprogs) on top of the systemd/coreutils/findutils already in the unit
PATH, rather than the whole system profile.

Also route forge/matrix is_present() through priv_client::list_containers
instead of spawning nixos-container directly from unprivileged hive-c0re.
2026-06-03 00:05:17 +02:00
müde
1e4066b9cb fix(#702): hive-c0re ExecStartPre fails 203/EXEC — coreutils has no sh
The state-migration chown invoked ${pkgs.coreutils}/bin/sh, which does not
exist (coreutils ships chown, not sh), so ExecStartPre exited 203/EXEC on
every boot. hive-c0re hit its start limit and never came up, so the gateway
returned 502 Bad Gateway. Call chown directly with the +- prefix (run as
root, tolerate failure) instead of going through a shell for '|| true'.
2026-06-02 23:49:51 +02:00
damocles
0bec3af933 feat(#702): drop hive-c0re from root to hive-core user (privsep phase 2) 2026-06-02 22:45:12 +02:00
damocles
89665b94de feat(#1014): rename manager agent root→ruth across all crates + frontend 2026-06-02 18:18:06 +02:00
atlas
ed6f40ba40 fix(#1061): suppress tuwunel default displayname suffix
Tuwunel 1.6.1 appends " 💕" to every new user's display name via the
new_user_displayname_suffix config option. Set it to empty string so
agent and operator matrix accounts get clean names.

Existing users (atlas, mara, root) need a one-time display name reset.
Atlas's was already corrected via the client API.
2026-06-02 11:18:38 +02:00
damocles
4ee7967e2a deprecate allowedBashPatterns with warning instead of hard removal 2026-06-02 09:28:14 +02:00
damocles
88f22065b0 feat(#1058): disallow built-in Bash; remove allowedBashPatterns; prompt sweep 2026-06-02 01:14:24 +02:00
damocles
9d11e5b6d6 fix(#1052): use systemctl reload nginx instead of systemd-run 2026-06-02 01:00:48 +02:00
damocles
637b170cc5 fix(#1052): use full nginx path in systemd-run reload (exit 203 = EXEC) 2026-06-02 00:52:56 +02:00
atlas
9d816431dc fix(#981): validate runner credentials on every boot, purge stale .runner
hive-ci-register.service now runs unconditionally on every boot (not
just when .runner is absent). Before fetching a registration token it
validates existing .runner credentials via the forge admin API:
- 200: runner still registered, write dummy token and exit
- 404: runner deleted from forge, purge .runner and re-register
- 000: forge unreachable, keep credentials (runner surfaces the error)
- other non-200 or malformed .runner: purge and re-register

Removes ConditionPathExists so stale credentials from a wiped forge
no longer block the runner indefinitely. Updates docs/ci.md to match.
2026-06-02 00:27:47 +02:00
atlas
167b4fa1f3 refactor(gateway): fixed htpasswd path, drop htpasswdFile option
Remove the custom htpasswdFile option and bind-mount. The htpasswd file
now lives at the fixed path /var/lib/hyperhive/gateway/gateway.htpasswd
on the host, which is already exposed inside the container at
/run/hive-state/gateway.htpasswd via the existing gateway state
bind-mount — no extra bind-mount needed.

A tmpfiles rule pre-creates the file so nginx can open it even before
any users exist (empty file → all requests return 401, which is correct).

hivectl gateway commands default --file to the standard path so
`hivectl gateway create-user alice` just works without any flags.
2026-06-02 00:26:10 +02:00
atlas
ba1d096391 fix(gateway): interpolate actual htpasswdFile path in 401 page
Per argus review: the hardcoded /etc/hyperhive/gateway.htpasswd
example was wrong for operators with a custom htpasswdFile path.

Move the unauthorized.html from the static agentErrorPagesDir derivation
into a pkgs.writeText inside the lib.optionalAttrs guard where
cfg.auth.htpasswdFile is in scope and statically known non-null.
The rendered page now shows the operator's actual configured path.
2026-06-02 00:26:10 +02:00
atlas
a248c6bee1 feat(gateway): custom 401 page explaining how to add users
When HTTP Basic auth is enabled and credentials are absent or rejected,
nginx serves a Catppuccin-styled 401 page that tells the operator which
hivectl command to run to create a user. Uses error_page 401 =401 so
the browser still receives a 401 status (login dialog fires on first
visit) while getting a human-readable body when the dialog is dismissed.

The exact-match location (= /__hive_auth_unauthorized) beats location /
in nginx's prefix ordering so the internal subrequest does not loop back
through auth_basic.
2026-06-02 00:26:10 +02:00
atlas
db50da570a refactor(#1003): nixpkgs + nixpkgs-unstable as top-level meta inputs
Per mara's direction: both nixpkgs and nixpkgs-unstable are now
top-level meta flake inputs with explicit store-path URLs.  Hyperhive
follows them rather than the other way around:

  inputs.nixpkgs.url            = "path:${pkgs.path}";
  inputs.nixpkgs-unstable.url   = "path:${nixpkgs-unstable}";
  inputs.hyperhive.url          = "...";
  inputs.hyperhive.inputs.nixpkgs.follows            = "nixpkgs";
  inputs.hyperhive.inputs.nixpkgs-unstable.follows   = "nixpkgs-unstable";

New NixOS host options (auto-set at build time, overridable):
  services.hyperhive.c0re.nixpkgsFlake
    default: "path:${pkgs.path}" — host's evaluated nixpkgs.
  services.hyperhive.c0re.nixpkgsUnstableFlake
    default: "path:${nixpkgs-unstable}" from hyperhive's flake.nix —
    the channel that carries claude-code.  Operators can override to
    track a different unstable snapshot.

Legacy fallback (both args empty) preserved for backward compat.
Two new Rust tests cover the full-URL and fallback paths.
2026-06-01 23:30:04 +02:00
atlas
fe5a41288d feat(#1003): inject pkgs.path into meta flake as explicit nixpkgs.url
meta flake was using `nixpkgs.follows = "hyperhive/nixpkgs"` but
`hyperhive` is a store-path input, so nix resolves hyperhive's own
pinned lock rather than the host's follows-substituted version.
When an operator sets `inputs.hyperhive.inputs.nixpkgs.follows =
"nixpkgs"` in their host flake, the meta flake was silently ignoring
it and using hyperhive's pinned nixpkgs instead.

Fix: hive-c0re.nix injects `--nixpkgs-flake path:${pkgs.path}` into
the daemon's ExecStart. `pkgs` IS the host's nixpkgs when follows is
set; otherwise it's hyperhive's own pin — so the meta flake gets the
right nixpkgs in both cases. render_flake emits `nixpkgs.url = "..."`
(explicit) when nixpkgs_flake is non-empty, falling back to the old
`follows` form when empty for backward compat.
2026-06-01 23:29:54 +02:00
atlas
6b6289c191 fix(gateway): point option descriptions at hivectl instead of raw htpasswd 2026-06-01 23:25:28 +02:00
atlas
4bff450343 feat(gateway): hivectl gateway user management + fix htpasswdFile assertion
Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.

Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.

Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
2026-06-01 23:25:28 +02:00
atlas
25d2951d1e feat(gateway): htpasswd Basic auth — close #1010
Replaces the earlier PAM+binary approach with nginx's built-in
`auth_basic` module. No new binary, no new systemd service, no PAM.

New option `services.hyperhive.gateway.auth`:
- `enable` — off by default
- `htpasswdFile` — host path to an htpasswd file (required when enable)
- `realm` — WWW-Authenticate realm string (default "hyperhive");
  restricted to `strMatching "[^\"$]*"` to prevent nginx config injection

When enabled:
- the parent directory of `htpasswdFile` is bind-mounted read-only
  into the gateway container at `/run/gateway-auth/`
- the `"/"` proxy location gets `auth_basic` + `auth_basic_user_file`

Create credentials: `htpasswd -Bc /path/to/file alice` (BCrypt).
See `docs/gateway.md` ("HTTP Basic auth") for the full setup guide.
2026-06-01 23:24:47 +02:00
atlas
d4409b27a3 feat(gateway): PAM auth against host — close #1010
Adds opt-in HTTP Basic auth to the hive-gateway backed by the host PAM
stack + group membership check.

New binary `hive-gateway-auth` (hive-c0re workspace):
- Axum HTTP service on 127.0.0.1:7002 (host loopback)
- Decodes Basic credentials, authenticates via pam_unix.so
- Checks membership in `hyperhive-operator` group (or custom)
- Returns 200 / 401 / 403; nginx `auth_request` consumes these

New options under `services.hyperhive.gateway.auth`:
- `enable`      — off by default
- `port`        — auth service port (default 7002)
- `realm`       — WWW-Authenticate realm string (default "hyperhive")
- `group`       — required host group (default "hyperhive-operator")
- `pamService`  — PAM service name (default "hive-gateway")

Host-side NixOS wiring:
- `users.groups.hyperhive-operator` declared when default group used
- `/etc/pam.d/hive-gateway` emitted via `security.pam.services`
- `systemd.services.hive-gateway-auth` runs the auth binary as root
  (needs /etc/shadow access for pam_unix.so)

Gateway container nginx wiring:
- `location = /__hive_gateway_auth` — internal proxy to auth service
- `auth_request /__hive_gateway_auth` on the `"/"` proxy location
- `@hive_auth_required` named location adds WWW-Authenticate: Basic
  header on 401 so browsers display a login prompt

Workspace deps: pam = "0.8"; flake.nix: linux-pam added to
nativeBuildInputs so pkg-config can find libpam at build time.
2026-06-01 23:24:47 +02:00
atlas
4f684dc7c3 fix(#1012): add path trigger for forge-avatar-sync so it fires on token arrival
On first agent deployment, the container boots before hive-c0re has
provisioned the forge-token. forge-avatar-sync was exiting early with
"no forge-token found", and RemainAfterExit=true prevented systemd
from ever re-running it — avatar never got uploaded until the next
container reboot.

Add a systemd.paths.forge-avatar-sync unit (PathExistsGlob on the
forge-token file) to re-fire the service once the token arrives, and
set RemainAfterExit=false to allow the re-fire. Mirrors the existing
matrix-avatar-sync pattern exactly.
2026-06-01 23:23:54 +02:00
damocles
9450cd8a7e fix(#1034): chown harness dir to agent user on activation 2026-06-01 22:59:40 +02:00
atlas
fe62db8917 fix(#981): enable Forgejo Actions so runner registration token API works
Without [actions] ENABLED = true in forgejo's config, the
GET /api/v1/admin/runners/registration-token endpoint returns
"runner registration token not found" regardless of token scopes,
causing hive-ci-register.service to fail on first boot.
2026-06-01 18:38:44 +02:00
damocles
b83edc40c6 fix(#970,#973): retry gateway reload on failure; always enable HIVE_WEB_SOCKET 2026-06-01 18:18:45 +02:00
damocles
008609d0e7 rename(162): drop HIVE_PORT fallback; sweep remaining hm1nd in docs 2026-06-01 17:40:47 +02:00
iris
e162c1a1fa feat(#955): split agent page serving — statics from nix store, API proxied
gateway_nginx.rs reads HIVE_AGENT_FRONTEND_DIR (injected by hive-c0re.nix
as ${cfg.frontend}/agent). When set, agents.conf emits per-agent split
blocks instead of the old single proxy_pass:

  # Compiled assets — immutable nix store path, cache 1y
  location ^~ /agent/<name>/static/ {
      alias <frontend>/static/;
      expires 1y; add_header Cache-Control "public, immutable, ...";
  }
  # Static dist + proxy fallback
  location /agent/<name>/ {
      alias <frontend>/;
      try_files $uri $uri.html $uri/index.html @<name>_dynamic;
  }
  location @<name>_dynamic {
      proxy_pass <upstream>;   # api, events, icon, login, …
      …proxy headers unchanged…
  }

try_files path resolution (nginx applies alias mapping first):
  $uri           — exact file (/static/app.js → static/app.js)
  $uri.html      — bare-path fallback (/stats → stats.html)
  $uri/index.html — directory index (/ → index.html)
  @<name>_dynamic — proxy catchall for anything not in the dist

Adding pages to the frontend dist works automatically — no generator
change needed. Per-agent extraFiles (in mergedDist, not in the base
nix-store path) continue to proxy to the agent daemon.

frontend is a nix store path injected at build time — only [a-z0-9/._-],
no shell metacharacters — safe to interpolate without sanitization;
comment added documenting this assumption.

Without HIVE_AGENT_FRONTEND_DIR the existing single-proxy block is
emitted unchanged — backward-compatible for deployments without the env.

render() takes frontend_dir as a parameter so tests exercise both code
paths safely in parallel. 13 tests: 7 legacy, 6 split-mode. No clippy
warnings in changed files.

nix/modules/hive-c0re.nix: inject HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent".
2026-06-01 17:35:28 +02:00
atlas
84f5c2722e nix(#702): add hive-priv systemd socket + service units
Nix side of the hive-priv phase 1 work. hive-priv is socket-activated
exclusively: systemd holds /run/hive/priv.sock (mode 0660) and starts
the service on first connection. LISTEN_FDS + LISTEN_PID are passed;
hive-priv reads them in socket_listener() to accept the pre-bound fd.

Phase 2 note (comment in file): when hive-c0re drops to a non-root
user, add SocketGroup = hive-core to the socket unit so the
unprivileged caller can still connect. No code change needed in
hive-priv itself.
2026-06-01 17:33:32 +02:00
damocles
e7e0675d22 rename: role badge stays 'manager'; unify harness unit name to hive-ag3nt 2026-06-01 17:33:09 +02:00
damocles
1d0d52b85f rename(162): update manager.nix comments for root 2026-06-01 17:33:09 +02:00
damocles
7772e29ec2 rename(162): hive-m1nd -> hive-root, HIVE_LABEL/PORT for root 2026-06-01 17:33:09 +02:00
damocles
2f25131403 fix(947): strip prose/issue-tags from harness-base.nix 2026-06-01 16:49:41 +02:00
damocles
0ac05f0638 fix(#947): set HIVE_WEB_SOCKET for manager unconditionally 2026-06-01 16:49:41 +02:00
atlas
f8c0f64fd4 feat: socket-activate the hive-c0re admin socket
Add a systemd.sockets.hive-c0re unit that holds /run/hyperhive/host.sock
before hive-c0re starts. hive-c0re serve() detects LISTEN_FDS via the
listenfd crate and accepts the systemd-handed fd instead of calling bind().
Falls back to the existing bind path when LISTEN_FDS is absent so direct
invocation and CI are unaffected.

Benefits: hivectl can connect the moment the socket unit activates (no
racy window), and a hive-c0re restart never drops the socket inode.
2026-06-01 16:29:41 +02:00
iris
188f3ea5ec chore: scrub #NNN issue references from code comments and nix descriptions
Part of the docs-migration chore (issue #708). Remove GitHub issue
numbers from inline comments, option descriptions, and rustdoc —
these are contextless noise for anyone reading the code without
access to the original discussions. Replace with prose that captures
the same rationale directly.

No functional change. Build still clean (cargo check passes).
2026-06-01 13:30:52 +02:00
damocles
89030ff1e1 docs: clarify per-agent user comment in harness-base.nix 2026-06-01 11:21:47 +02:00
atlas
2fd6de7bab fix: address argus review nits — tmpfiles dir comment, drop redundant chmod 2026-06-01 00:18:19 +02:00
atlas
f0bd572b35 fix: correct runner registration ordering — oneshot + tmpfiles
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.
2026-06-01 00:13:00 +02:00
atlas
4df65deb20 fix: move gitea-actions-runner package to top-level option
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.
2026-05-31 23:55:49 +02:00
damocles
52cfc3ea1c reshape swarm peers: domain-as-key, certFingerprint field 2026-05-31 23:55:34 +02:00
atlas
a8e8c50a30 refactor: move ci options to services.hyperhive.forge.ci 2026-05-31 23:52:57 +02:00
atlas
45cbf2eded fixup: trim systemPackages to git only
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>
2026-05-31 23:44:12 +02:00
atlas
61565e3c3a fixup: drop stale docs/ci.md forward-ref in module comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:44:12 +02:00
atlas
0fa0e821d9 fixup: stateVersion 26.05, drop ubuntu labels, simplify CI workflow
- stateVersion: 25.11 → 26.05 (match hive-gateway + hive-matrix)
- default labels: drop ubuntu-latest/ubuntu-22.04 aliases — misleading
  since this isn't Ubuntu; hive-ci:host is the correct label
- ci.yml: collapse to single `nix flake check` job — flake checks.* already
  covers formatting (treefmt+rustfmt), cargo test, and clippy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:44:12 +02:00
atlas
bf350df442 fixup: hive-ci auto-registers runner via forge admin API
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>
2026-05-31 23:44:12 +02:00
atlas
aa7ebbcd3b feat: add hive-ci module — Forgejo Actions runner for CI
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>
2026-05-31 23:44:12 +02:00
atlas
d45b2f0293 chore: scrub issue/PR cookies from gateway + network nix modules and gateway_nginx.rs 2026-05-31 23:09:46 +02:00
damocles
4093f4fdb4 fixup: remove issue tags from code, use plain prose 2026-05-31 23:06:38 +02:00
damocles
348fb3792a feat(#589): swarm peers option + HYPERHIVE_PEERS env wire v0
nix: services.hyperhive.peers attrset-of-submodules option; serialises
to HYPERHIVE_PEERS JSON ([{label,domain}]); forwarded to containers via
FORWARDED_VARS. dashboard.rs: peer_hives: Vec<PeerHiveView> in
StateSnapshot, derived as {name:label, url:"http://domain/"}.
identity.rs: PeerHive struct + peers() accessor for agent-side use.
2026-05-31 23:06:38 +02:00
iris
cce85a6c1b fix(#540): use systemd.globalEnvironment instead of environment.variables
environment.variables writes to /etc/environment (PAM sessions only)
and is not visible to systemd service units. The correct path for
env vars that need to reach all systemd services is
systemd.globalEnvironment (sets DefaultEnvironment in systemd.conf),
which is the pattern established by #608 for HYPERHIVE_STATE_DIR.

Also update the inline comment reference from environment.variables
to systemd.globalEnvironment.
2026-05-31 22:59:46 +02:00