diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 9ff67e4c..be2a4574 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -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"; }; }; };