178 lines
7.1 KiB
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
|