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:
parent
5486fc0333
commit
75e9e205e3
1 changed files with 933 additions and 31 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue