Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/module-eval/agent-runtime.nix
atlas 75e9e205e3 fetch the agent's backend env file from the store, seeded from the existing file
the agent's own root unit reads swarm/agents/<agent>/backend-env into /run and
creates it from backendEnvironmentFile with -cas=0 when the store holds nothing.
the shared agent policy gains create (not update) on that one leaf. the harness
and the subagent daemon load the old file first and the fetched one after it.
2026-10-28 01:24:29 +01:00

235 lines
8.5 KiB
Nix

# `checks.module-eval-agent-runtime` — 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
;
backendEnv = "/agents/a1/harness/backend.env";
# Both arms carry a backend file, so the claude arm's lack of one on the
# subagent unit is the gate and not a missing input.
agentOn =
runtime:
agentWith {
services.hyperhive.agent = {
inherit runtime;
backendEnvironmentFile = backendEnv;
acp.command = "/bin/agent";
acp.args = [ "acp" ];
};
};
claude = agentOn "claude";
acp = agentOn "acp";
claudeApiKey = agentWith {
services.hyperhive.agent = {
runtime = "claude";
useApiKey = true;
backendEnvironmentFile = backendEnv;
};
};
# The opencode preset, with and without a secret store.
opencodeWith =
extra:
agentWith {
services.hyperhive.agent = {
runtime = "acp";
acp.preset = "opencode";
acp.opencode.provider.baseUrl = "https://inference.t.local/v1";
acp.opencode.provider.apiKeyEnv = "T_PROVIDER_KEY";
acp.opencode.model = "m";
}
// extra;
};
opencode = opencodeWith { };
opencodeBao = opencodeWith { bao.addr = "https://bao.t.local:8200"; };
claudeApiKeyBao = agentWith {
services.hyperhive.agent = {
runtime = "claude";
useApiKey = true;
backendEnvironmentFile = backendEnv;
bao.addr = "https://bao.t.local:8200";
};
};
claudeBao = agentWith {
services.hyperhive.agent = {
runtime = "claude";
bao.addr = "https://bao.t.local:8200";
};
};
acpBao = agentWith {
services.hyperhive.agent = {
runtime = "acp";
acp.command = "/bin/agent";
bao.addr = "https://bao.t.local:8200";
};
};
subagent = machine: machine.systemd.services.hive-subagent-daemon;
harness = machine: machine.systemd.services.hive-agent;
runtimeVars = [
"HIVE_RUNTIME"
"HIVE_ACP_COMMAND"
"HIVE_ACP_ARGS"
"HIVE_ACP_ENV"
];
cases = [
{
# The daemon reads these with `hive_runtime::RuntimeSpec`, the same
# parser as the harness, so equal values mean the same runtime.
name = "an ACP agent's subagent daemon gets the harness's runtime selection";
ok = lib.all (
var: (subagent acp).environment.${var} or null == (harness acp).environment.${var}
) runtimeVars;
}
{
name = "an opencode agent's harness and subagent daemon are told its provider key variable";
ok =
(harness opencode).environment.HIVE_ACP_API_KEY_ENV == "T_PROVIDER_KEY"
&& (subagent opencode).environment.HIVE_ACP_API_KEY_ENV == "T_PROVIDER_KEY";
}
{
# Only the opencode preset has a provider key variable.
name = "an ACP agent off the opencode preset is told no provider key variable";
ok =
!((harness acpBao).environment ? HIVE_ACP_API_KEY_ENV)
&& (subagent acpBao).environment.HIVE_ACP_API_KEY_ENV or null == null;
}
{
# Every agent with a store, whatever its runtime: the daemon reads the
# agent's queue credential with it to publish subagent terminals.
name = "a subagent daemon gets the agent's store identity whenever the agent has a store";
ok =
lib.all
(
u:
u.serviceConfig.LoadCredential == [ "hive-agent-bao-server-ca" ]
&& u.environment.HIVE_AGENT_NAME == "a1"
&& u.environment.BAO_ADDR == "https://bao.t.local:8200"
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"
)
[
(subagent claudeBao)
(subagent acpBao)
(subagent opencodeBao)
];
}
{
name = "an opencode agent with no store hands its subagent daemon no store identity";
ok =
!((subagent opencode).serviceConfig ? LoadCredential)
&& !((subagent opencode).environment ? BAO_ADDR);
}
{
name = "an ACP agent's subagent daemon loads the backend credentials";
ok = (subagent acp).serviceConfig.EnvironmentFile or null == [ "-${backendEnv}" ];
}
{
# The nested `claude` inherits the daemon's environment, so this file
# is where its `ANTHROPIC_API_KEY` comes from.
name = "an api-key claude agent's subagent daemon loads the backend credentials";
ok = (subagent claudeApiKey).serviceConfig.EnvironmentFile or null == [ "-${backendEnv}" ];
}
{
# Unset is what `RuntimeSpec` reads as claude, and a claude subagent
# of an agent without `useApiKey` is not handed the backend file.
name = "a claude agent's subagent daemon has no runtime selection and no backend file";
ok =
lib.all (var: (subagent claude).environment.${var} or null == null) runtimeVars
&& !((subagent claude).serviceConfig ? EnvironmentFile);
}
{
# Later files override earlier ones, so the operator's file carries the
# agent when the store is down and the store's copy wins when it is up.
name = "an api-key agent with a store loads its backend file, then the store's copy, in the harness and the subagent daemon";
ok =
let
want = [
"-${backendEnv}"
"-/run/hive-agent-backend-env/env"
];
in
(harness claudeApiKeyBao).serviceConfig.EnvironmentFile == want
&& (subagent claudeApiKeyBao).serviceConfig.EnvironmentFile == want
&& (harness acpBao).serviceConfig.EnvironmentFile == [ "-/run/hive-agent-backend-env/env" ]
&& (subagent acpBao).serviceConfig.EnvironmentFile == [ "-/run/hive-agent-backend-env/env" ];
}
{
# The case above is the presence control: the same lookup finds the
# fetched file where it is loaded.
name = "an agent with no store, or a claude agent on OAuth, loads no fetched backend file";
ok =
let
fetched =
machine:
lib.elem "-/run/hive-agent-backend-env/env" (
lib.toList (harness machine).serviceConfig.EnvironmentFile or [ ]
);
in
!(fetched claudeApiKey)
&& !(fetched acp)
&& !(fetched opencode)
&& !(fetched claudeBao)
&& !(claudeBao.systemd.services ? hive-agent-backend-env)
&& !(claudeApiKey.systemd.services ? hive-agent-backend-env)
&& claudeApiKeyBao.systemd.services ? hive-agent-backend-env
&& claudeApiKeyBao.systemd.timers ? hive-agent-backend-env;
}
{
name = "the backend environment fetch runs as root, keeps its file, and the consumers want it without requiring it";
ok =
let
u = claudeApiKeyBao.systemd.services.hive-agent-backend-env;
orders =
unit:
lib.elem "hive-agent-backend-env.service" unit.wants
&& lib.elem "hive-agent-backend-env.service" unit.after
&& !(lib.elem "hive-agent-backend-env.service" unit.requires);
in
!(u.serviceConfig ? User)
&& u.serviceConfig.UMask == "0077"
&& u.serviceConfig.RuntimeDirectoryPreserve == "yes"
&& u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"
&& orders (harness claudeApiKeyBao)
&& orders (subagent claudeApiKeyBao)
&& claudeApiKeyBao.systemd.timers.hive-agent-backend-env.timerConfig.OnUnitInactiveSec == "2min";
}
{
# The nix half of `swarm_secret_client::backend_env::env_path`, and the
# three promises the unit makes about the operator's file.
name = "the backend environment fetch reads the agent's own leaf, seeds it create-only from the operator's file and never writes that file";
ok =
let
s = claudeApiKeyBao.systemd.services.hive-agent-backend-env.script;
in
lib.hasInfix "kv get -field=env secret/swarm/agents/a1/backend-env" s
&& lib.hasInfix "kv put -cas=0 secret/swarm/agents/a1/backend-env env=@\"$earlier\"" s
&& lib.hasInfix "earlier=${backendEnv}" s
&& !(lib.hasInfix "kv put secret" s)
&& !(lib.hasInfix "> \"$earlier\"" s)
&& !(lib.hasInfix ">\"$earlier\"" s)
&& !(lib.hasInfix "rm -f \"$earlier\"" s)
&& !(lib.hasInfix "mv -f \"$staged\" \"$earlier\"" s);
}
];
in
runGroup "agent-runtime" cases