Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/agent-user.nix

178 lines
7.1 KiB
Nix

# `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