Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
atlas 2026-10-27 21:14:44 +01:00
commit 75e9e205e3

View file

@ -61,6 +61,14 @@ let
};
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";
@ -133,13 +141,13 @@ let
}
{
name = "an ACP agent's subagent daemon loads the backend credentials";
ok = (subagent acp).serviceConfig.EnvironmentFile or null == "-${backendEnv}";
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}";
ok = (subagent claudeApiKey).serviceConfig.EnvironmentFile or null == [ "-${backendEnv}" ];
}
{
# Unset is what `RuntimeSpec` reads as claude, and a claude subagent
@ -149,6 +157,79 @@ let
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