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
136 lines
6.1 KiB
Nix
136 lines
6.1 KiB
Nix
# Swarm-queue coordinates for this agent's harness.
|
|
#
|
|
# Three of the four the harness needs are addresses (this file's two options
|
|
# plus the secret's path); the fourth, the client id, arrives as a file beside
|
|
# the secret so a reader never spells `hive-<name>-agent` a second time.
|
|
#
|
|
# ⚠️ The credential arrives as a systemd credential and NOT as a bind mount,
|
|
# and the mode is why: the host file is `root:0600` and this unit runs as the
|
|
# unprivileged agent user. nspawn's `--load-credential` (written by
|
|
# `hive_c0re::lifecycle::host_config`) is read by the container manager as
|
|
# root and re-exposed under this unit's own `User=`; a bind would deliver a
|
|
# file the harness cannot open.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.agent.queue;
|
|
configured = cfg.natsUrl != null && cfg.tokenEndpoint != null;
|
|
|
|
# The two ids `hive_c0re::lifecycle::host_config` forwards under. Neither
|
|
# side can discover the other's spelling, so a rename is a rename there too.
|
|
secretCredential = "hive-queue-agent-secret";
|
|
clientIdCredential = "hive-queue-agent-client-id";
|
|
|
|
# Where systemd materialises this unit's credentials. `%d` above is the
|
|
# same directory, but `%d` only expands *inside* a unit — a consumer that
|
|
# is not one (the `swarm-logs` wrapper, see ./logs.nix) needs the literal,
|
|
# and deriving it from the unit name here is what keeps the two from
|
|
# disagreeing about which unit's credentials they mean.
|
|
credentialsDir = "/run/credentials/hive-agent.service";
|
|
in
|
|
{
|
|
options.services.hyperhive.agent.queue = {
|
|
natsUrl = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "nats://10.42.0.1:4222";
|
|
description = ''
|
|
Where the swarm queue listens, as this container reaches it.
|
|
|
|
Set by the generated meta flake from the host's
|
|
{option}`services.hyperhive.deploy.hive-controller.queue.agentNatsUrl`,
|
|
which is the bridge address rather than a loopback one — inside this
|
|
container `127.0.0.1` is the agent itself.
|
|
|
|
`null` means this hive has not been given the queue's address for its
|
|
agents, and the harness then declares no credential and logs that it
|
|
has none. It is deliberately not defaulted to anything: a guessed
|
|
address builds fine and talks to the wrong machine.
|
|
'';
|
|
};
|
|
|
|
tokenEndpoint = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "https://auth.example.com/api/oidc/token";
|
|
description = ''
|
|
The swarm IdP's OAuth2 token endpoint. The harness mints a
|
|
`client_credentials` token there and presents it to the queue, which
|
|
authenticates it as the client named in the delivered credential.
|
|
|
|
Set together with {option}`services.hyperhive.agent.queue.natsUrl` or not at all —
|
|
the harness treats a half-set pair as a deployment bug rather than as
|
|
"no queue coordinates".
|
|
'';
|
|
};
|
|
|
|
# One declaration, two readers. The credential ids are this module's —
|
|
# `hive_c0re::lifecycle::host_config` and the `LoadCredential=` below are
|
|
# the pair that has to agree on them — and a second consumer that spelled
|
|
# them again would be a second source of truth for a string whose
|
|
# mismatch is a file that is simply not there.
|
|
clientIdFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
readOnly = true;
|
|
default = "${credentialsDir}/${clientIdCredential}";
|
|
description = ''
|
|
Path the agent's OIDC client id is delivered at, for a consumer
|
|
outside the harness unit. Read-only: it is a fact about where the
|
|
credential lands, not a knob — see {option}`services.hyperhive.agent.logs.queryUrl`
|
|
for the consumer this exists for.
|
|
'';
|
|
};
|
|
|
|
clientSecretFile = lib.mkOption {
|
|
type = lib.types.str;
|
|
readOnly = true;
|
|
default = "${credentialsDir}/${secretCredential}";
|
|
description = ''
|
|
Path the agent's OIDC client secret is delivered at. Read-only for
|
|
the same reason as {option}`services.hyperhive.agent.queue.clientIdFile`.
|
|
|
|
🩸 A PATH and never a value. The file is `0400` to the agent user and
|
|
is read at the moment of a token request; nothing in this tree puts
|
|
its contents in an environment variable, where `/proc/<pid>/environ`
|
|
would publish them to every process in the container.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf configured {
|
|
systemd.services.hive-agent = {
|
|
# Bare ids, no paths: this is the terse `LoadCredential=` form that
|
|
# inherits a credential the service *manager* received, which is what
|
|
# the container manager passed in. `man systemd.exec` also makes that
|
|
# form non-fatal when the credential is absent, which is exactly the
|
|
# behaviour a hive whose publisher has not run yet needs — the unit
|
|
# starts, finds no id, and says so.
|
|
serviceConfig.LoadCredential = [
|
|
secretCredential
|
|
clientIdCredential
|
|
];
|
|
environment = {
|
|
# `%d` is `$CREDENTIALS_DIRECTORY`, per-unit and owned by `User=`.
|
|
# Same shape hive-c0re's own queue client is handed its secret in
|
|
# (`nix/host-modules/hive-c0re/environment.nix`) — the harness reads
|
|
# a path and never a value.
|
|
HIVE_AGENT_OIDC_CLIENT_SECRET_FILE = "%d/${secretCredential}";
|
|
# The id is public (it is sent to the token endpoint on every
|
|
# connection) but still arrives as a path, because it arrives *with*
|
|
# the secret. `QueueConfig::from_env` wants it as a value, so the
|
|
# harness reads this file itself — see `hive-agent`'s `swarm_queue`.
|
|
HIVE_AGENT_OIDC_CLIENT_ID_FILE = "%d/${clientIdCredential}";
|
|
};
|
|
# No `HIVE_AGENT_OIDC_CA_FILE`. The hive's own client needs one because
|
|
# the host does not trust the swarm's CA, but an agent does: the meta
|
|
# flake embeds the hive CA and the swarm root it is issued under into
|
|
# this container's `security.pki.certificateFiles` at build time, and
|
|
# reqwest's rustls backend verifies against the system store. A path
|
|
# here would need the bundle delivered as a third credential to say
|
|
# nothing new.
|
|
};
|
|
};
|
|
}
|