# `checks.module-eval-agent-user` — see ./lib.nix for the shared # rationale (why this suite exists, naming convention, "evaluates # not executes"). { pkgs, lib, self, nixosSystem, }: let inherit (import ./lib.nix { inherit pkgs lib self nixosSystem ; }) agentWith runGroup ; # A named agent, so the rendered script carries a name no default supplies. migrate = (agentWith { services.hyperhive.agent.user.name = "iris"; }) .system.activationScripts.hive-agent-user-migrate.text; # The tmpfiles rules of the same named agent. rules = (agentWith { services.hyperhive.agent.user.name = "iris"; }).systemd.tmpfiles.rules; cases = [ { # hive-c0re creates the host work dir as hive-core; this activation is # the only thing that lets the agent write to it. name = "the agent's activation hands its own work dir to the agent user"; ok = lib.hasInfix ''if [ "$(stat -c %U "/agents/$userName/work")" != "$userName" ]; then'' migrate && lib.hasInfix ''heal_own_dir "/agents/$userName/work"'' migrate; } { # The helper is the only recursive chown of these dirs; it must hand the # tree to the agent user and log a failure rather than discard it. name = "the agent's activation hands state, harness and .claude to the agent user, logging failures"; ok = lib.hasInfix ''chown -hR "$userName:$userName" "$1" 2>&1'' migrate && lib.hasInfix "could not chown $1 to $userName" migrate && lib.hasInfix ''heal_own_dir "/agents/$userName/state"'' migrate && lib.hasInfix ''heal_own_dir "/agents/$userName/harness"'' migrate && lib.hasInfix ''heal_own_dir "$homeDir/.claude"'' migrate; } { # hive-priv creates the host socket dir `0751 root` and never chowns it, # so this activation is the only thing that lets the harness bind there. name = "the agent's activation hands its socket dir to the agent user at 0751"; ok = lib.hasInfix "userName=${lib.escapeShellArg "iris"}" migrate && lib.hasInfix ''socketDir="/run/hive-agent/$userName"'' migrate && lib.hasInfix ''chown -h "$userName:$userName" "$socketDir"'' migrate && lib.hasInfix ''chmod 0751 "$socketDir"'' migrate; } { # `test -d` and `chmod` follow symlinks, so without this refusal ahead # of the chown branch a link at the socket dir is chmodded at its target. name = "the agent's activation refuses a symlinked or non-directory socket dir"; ok = lib.hasInfix ''if [ -L "$socketDir" ] || { [ -e "$socketDir" ] && [ ! -d "$socketDir" ]; }; then'' migrate && lib.hasInfix ''elif [ -d "$socketDir" ]; then'' migrate; } { # The container root is rebuilt at every start; nix keeps a non-root # user's profile in the home, so these rules are the only thing that puts # it in the persistent per-container profiles bind. The `d` and `L` rules # are each other's control: a misspelled path in one still finds the other. name = "the agent's tmpfiles rules keep nix's per-user state in the persistent profiles bind"; ok = lib.elem "d /nix/var/nix/profiles/per-user/iris 0755 iris iris - -" rules && lib.elem "L /home/iris/.local/state/nix - - - - /nix/var/nix/profiles/per-user/iris" rules && lib.elem "L /home/iris/.nix-profile - - - - /home/iris/.local/state/nix/profiles/profile" rules; } ]; in runGroup "agent-user" cases