# `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. iris = agentWith { services.hyperhive.agent.user.name = "iris"; }; migrate = iris.system.activationScripts.hive-agent-user-migrate.text; # The tmpfiles rules of the same named agent. rules = iris.systemd.tmpfiles.rules; # The same agent with its nix builds switched off, and one asking for a mode # the option does not have. noBuilds = agentWith { services.hyperhive.agent.user.name = "iris"; services.hyperhive.agent.nixBuilds = "none"; }; unknownBuilds = agentWith { services.hyperhive.agent.user.name = "iris"; services.hyperhive.agent.nixBuilds = "container"; }; # The VM test's agent stand-in: the user and nix-builds modules on a plain # container, under each mode. standIn = mode: (nixosSystem { system = pkgs.stdenv.hostPlatform.system; modules = [ ../agent-modules/user.nix ../agent-modules/nix-builds.nix { fileSystems."/" = { device = "/dev/null"; fsType = "tmpfs"; }; boot.loader.grub.enable = false; system.stateVersion = "25.11"; services.hyperhive.agent.nixBuilds = mode; } ]; }).config; standInHost = standIn "host"; standInNone = standIn "none"; # `privateUsers` with the given pins; the agent name is the one the cases # above use, so only the pins differ. withUsers = user: agentWith { services.hyperhive.agent.user = { name = "iris"; } // user; }; # Whether the module refuses the config over the user-namespace pin. refusesPins = cfg: lib.any (a: !a.assertion && lib.hasInfix "privateUsers" a.message) cfg.assertions; cases = [ { # The control is the matching pair: it passes, so each refusal is the # assertion's own and the default (off, no pins) passes too. name = "privateUsers needs a uid and a gid that are the same number"; ok = !refusesPins (withUsers { }) && !refusesPins (withUsers { privateUsers = true; uid = 1000; gid = 1000; }) && refusesPins (withUsers { privateUsers = true; }) && refusesPins (withUsers { privateUsers = true; uid = 1000; }) && refusesPins (withUsers { privateUsers = true; uid = 1000; gid = 1001; }); } { # 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; } { # The option defaults to host builds, accepts `none`, and refuses a mode # it does not declare. The default and `none` arms are each other's # control; `none` leaves the in-container routing alone, because the # error an agent gets then comes from the missing socket. name = "nixBuilds defaults to host, takes none and refuses a mode it does not declare"; ok = iris.services.hyperhive.agent.nixBuilds == "host" && noBuilds.services.hyperhive.agent.nixBuilds == "none" && noBuilds.systemd.globalEnvironment.NIX_REMOTE == "daemon" && !(builtins.tryEval unknownBuilds.services.hyperhive.agent.nixBuilds).success; } { # The container's own daemon would listen on the tmpfs that hides the # host socket. The `host` agent is the control: its units are on, so a # misspelled unit name cannot pass the `none` arms. name = "nixBuilds none turns the container's own nix-daemon socket and service off, host leaves them on"; ok = iris.systemd.sockets.nix-daemon.enable && iris.systemd.services.nix-daemon.enable && !noBuilds.systemd.sockets.nix-daemon.enable && !noBuilds.systemd.services.nix-daemon.enable # The VM test's agent stand-in imports these two modules and not the # whole agent module set, so the disable has to live in the former. && standInHost.systemd.sockets.nix-daemon.enable && standInHost.systemd.services.nix-daemon.enable && !standInNone.systemd.sockets.nix-daemon.enable && !standInNone.systemd.services.nix-daemon.enable; } ]; in runGroup "agent-user" cases