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).
This commit is contained in:
müde 2026-06-03 00:33:15 +02:00
commit d272803d58

View file

@ -346,6 +346,12 @@ in
"/run/current-system/sw"
];
environment = {
# nix (the prebuild `nix build`, flake-check, and meta eval) writes
# its fetcher/eval cache under $HOME/.cache. As a system user
# hive-core has no home, so HOME defaults to the unwritable
# /var/empty and Lix fails to initialise its cache. Point HOME at
# the writable StateDirectory.
HOME = "/var/lib/hyperhive";
HYPERHIVE_GIT = "${pkgs.git}/bin/git";
# Path to the dashboard static dist. The hive-c0re axum router
# serves this via `tower_http::ServeDir` for any path it doesn't
@ -423,15 +429,26 @@ in
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --hyperhive-flake ${cfg.hyperhiveFlake} --nixpkgs-flake ${cfg.nixpkgsFlake} --nixpkgs-unstable-flake ${cfg.nixpkgsUnstableFlake} --dashboard-port ${toString cfg.dashboardPort} --operator-pronouns ${lib.escapeShellArg cfg.operatorPronouns} --context-window-tokens ${lib.escapeShellArg (builtins.toJSON cfg.contextWindowTokens)}";
# One-time migration: chown existing state tree to the service
# user after upgrading from a root-run install. The `+` prefix
# runs this step as root even though User = hive-core; the `-`
# prefix tolerates failure (e.g. an empty tree) without blocking
# startup. coreutils ships `chown` but no `sh`, so invoke chown
# directly rather than through a shell. systemd's StateDirectory
# chowns the top-level dir at every start, but pre-existing files
# inside may still be root-owned.
ExecStartPre = "+-${pkgs.coreutils}/bin/chown -R hive-core:hive-core /var/lib/hyperhive";
# Migrate hive-c0re's *own* state to the service user after an
# upgrade from a root-run install (systemd's StateDirectory only
# chowns the top-level dir, not pre-existing files inside it). The
# `+` prefix runs as root despite User = hive-core; `-` tolerates
# failure. coreutils ships `chown` but no `sh`, so invoke the
# binaries directly rather than through a shell.
#
# CRITICAL: exclude the per-agent `agents/` subtree. Its contents
# (each agent's `claude/` OAuth creds, `state/`, `harness/`,
# `config/`) are owned by the per-agent / manager users, and each
# container's `hive-agent-user-migrate` activation script chowns
# them back to that user on boot. Blanket-chowning them to hive-core
# makes every agent's `~/.claude` unreadable — logging them all out
# with no way to log back in. So chown everything *except* agents/,
# plus the `agents/` dir node itself (not its contents) so c0re can
# still create new per-agent subdirs.
ExecStartPre = [
"+-${pkgs.findutils}/bin/find /var/lib/hyperhive -mindepth 1 -maxdepth 1 -not -name agents -exec ${pkgs.coreutils}/bin/chown -R hive-core:hive-core {} +"
"+-${pkgs.coreutils}/bin/chown hive-core:hive-core /var/lib/hyperhive/agents"
];
Restart = "on-failure";
RestartSec = 2;
User = "hive-core";
@ -521,6 +538,14 @@ in
pkgs.util-linux # umount (nsenter is hardcoded in the script)
pkgs.e2fsprogs # chattr
];
environment = {
# `nixos-container update/create` runs `nix`, which writes its
# fetcher/eval cache under $HOME/.cache. With ProtectHome and no
# explicit HOME this lands on the unwritable /var/empty and Lix
# errors out. Point HOME at the StateDirectory below (persistent,
# so the cache survives across rebuilds).
HOME = "/var/lib/hive-priv";
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/hive-priv";
Type = "simple";
@ -530,6 +555,8 @@ in
# hive-priv needs to write to /etc/nixos-containers/ and
# /run/systemd/system/ — "strict" would block both.
ProtectSystem = "false";
# Writable HOME for nix's caches (see environment.HOME above).
StateDirectory = "hive-priv";
};
};
};