nix: move the agent option namespace under services.hyperhive.agent

Every per-agent harness option lived at the top-level `hyperhive.*` while
the host tier has always been `services.hyperhive.*`. Move all 52 agent-tier
option leaves (33 top-level names across 16 modules) to
`services.hyperhive.agent.*`, repoint every read, and keep existing agent
configs evaluating through one `mkRenamedOptionModule` per old leaf path in
the new nix/agent-modules/renamed-options.nix.

The shims are per leaf rather than per namespace: `user`, `mcp`, `otel`,
`queue`, `docs`, `forge`, `frontend`, `github`, `gui`, `logs`, `matrix` and
`cargo` are plain attrsets of declarations, not submodule-typed options, so
a parent-path rename would not reach their children. Three read-only
options (`frontend.mergedDist`, `queue.clientIdFile`,
`queue.clientSecretFile`) deliberately get no shim — a rename contributes a
definition, which a read-only option refuses; the exclusions are commented
in place.

Refs #4473
This commit is contained in:
atlas 2026-09-17 19:29:17 +02:00
commit 3662eda440
21 changed files with 531 additions and 290 deletions

View file

@ -1,4 +1,4 @@
# Per-agent unix user: the `hyperhive.user.*` options, the user/group
# Per-agent unix user: the `services.hyperhive.agent.user.*` options, the user/group
# declarations, passwordless sudo, and the first-boot migration that
# chowns the bind-mounted state dirs to the agent user.
{
@ -8,7 +8,7 @@
...
}:
let
userName = config.hyperhive.user.name;
userName = config.services.hyperhive.agent.user.name;
homeDir = "/home/${userName}";
in
{
@ -20,7 +20,7 @@ in
# uniquely-named user matching its agent label. UID auto-assigned
# by NixOS (the auto-allocation range for normal users); no hard-
# coded UID.
options.hyperhive.user.name = lib.mkOption {
options.services.hyperhive.agent.user.name = lib.mkOption {
type = lib.types.strMatching "^[a-z_][a-z0-9_-]{0,30}$";
default = "agent";
example = "iris";
@ -33,11 +33,11 @@ in
Constraints match `useradd`'s NAME_REGEX: lowercase / `_` start,
total length 31, no special characters. UID is auto-assigned
by NixOS unless `hyperhive.user.uid` is explicitly set.
by NixOS unless `services.hyperhive.agent.user.uid` is explicitly set.
'';
};
options.hyperhive.user.uid = lib.mkOption {
options.services.hyperhive.agent.user.uid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
example = 1100;
@ -59,24 +59,24 @@ in
'';
};
options.hyperhive.user.gid = lib.mkOption {
options.services.hyperhive.agent.user.gid = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
example = 1100;
description = ''
Optional fixed GID for the per-agent unix group. `null` (default)
lets NixOS auto-assign. Usually set alongside `hyperhive.user.uid`
lets NixOS auto-assign. Usually set alongside `services.hyperhive.agent.user.uid`
to the same value (the conventional Unix pattern for per-user
groups where uid == gid), but can be set independently.
'';
};
options.hyperhive.user.passwordlessSudo = lib.mkOption {
options.services.hyperhive.agent.user.passwordlessSudo = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = ''
Grant `${config.hyperhive.user.name}` passwordless sudo
Grant `${config.services.hyperhive.agent.user.name}` passwordless sudo
(`NOPASSWD: ALL`). True by default so claude's `Bash` tool
keeps working for tools that expect root inside the container
(`systemctl`, package managers in dev shells, etc.) the
@ -94,10 +94,12 @@ in
assertions = [
{
assertion =
config.hyperhive.user.uid == null
|| (config.hyperhive.user.uid >= 1000 && config.hyperhive.user.uid < 60000);
config.services.hyperhive.agent.user.uid == null
|| (
config.services.hyperhive.agent.user.uid >= 1000 && config.services.hyperhive.agent.user.uid < 60000
);
message = ''
hyperhive.user.uid must be in [1000, 60000) values below
services.hyperhive.agent.user.uid must be in [1000, 60000) values below
1000 clash with system accounts; values 60000 are reserved
by NixOS for dynamic allocation. Leave unset (null) to let
NixOS auto-assign.
@ -105,11 +107,13 @@ in
}
{
assertion =
config.hyperhive.user.gid == null
|| (config.hyperhive.user.gid >= 1000 && config.hyperhive.user.gid < 60000);
config.services.hyperhive.agent.user.gid == null
|| (
config.services.hyperhive.agent.user.gid >= 1000 && config.services.hyperhive.agent.user.gid < 60000
);
message = ''
hyperhive.user.gid must be in [1000, 60000) same range
constraint as hyperhive.user.uid.
services.hyperhive.agent.user.gid must be in [1000, 60000) same range
constraint as services.hyperhive.agent.user.uid.
'';
}
];
@ -123,29 +127,29 @@ in
home = homeDir;
createHome = true;
group = userName;
extraGroups = lib.optional config.hyperhive.user.passwordlessSudo "wheel";
extraGroups = lib.optional config.services.hyperhive.agent.user.passwordlessSudo "wheel";
# Matches /bin/bash on NixOS — the harness's claude shell-outs
# expect a POSIX shell at $SHELL; bashInteractive is already
# the system default for the root user too.
shell = pkgs.bashInteractive;
}
// lib.optionalAttrs (config.hyperhive.user.uid != null) {
uid = config.hyperhive.user.uid;
// lib.optionalAttrs (config.services.hyperhive.agent.user.uid != null) {
uid = config.services.hyperhive.agent.user.uid;
};
users.groups.${userName} =
{ }
// lib.optionalAttrs (config.hyperhive.user.gid != null) {
gid = config.hyperhive.user.gid;
// lib.optionalAttrs (config.services.hyperhive.agent.user.gid != null) {
gid = config.services.hyperhive.agent.user.gid;
};
# `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool
# keep working with anything that expected root (systemctl,
# nix-env, etc.) without prompting. Flip
# `hyperhive.user.passwordlessSudo = false` to drop both
# `services.hyperhive.agent.user.passwordlessSudo = false` to drop both
# the wheel-group membership and this sudoers entry; anything
# that needs root then fails loudly instead of silently
# succeeding.
security.sudo.extraRules = lib.mkIf config.hyperhive.user.passwordlessSudo [
security.sudo.extraRules = lib.mkIf config.services.hyperhive.agent.user.passwordlessSudo [
{
users = [ userName ];
commands = [