swarm-controller re-issues an agent's store leaf at half its validity, but the running container kept presenting the copy its hive delivered at start, so a re-issued leaf reached the agent only on its next restart. hive-agent-bao-identity now seeds a live copy of the pair in its own runtime directory from the delivered credentials, and re-runs every 10 minutes: it logs in with the live leaf, reads its own swarm/agents/<agent>/bao-mtls (a read render_agent already grants), and when the leaf changed, logs in with the new pair and swaps it in only if the store accepts it. Every unit in the container that logs in to the store presents the live pair: hive-agent (queue-identity.nix), hive-matrix-daemon, hive-subagent-daemon, hive-agent-forge-token, hive-agent-forge-accounts and hive-agent-github-token. They keep loading the server CA as a credential. The long-running daemons are ordered after the refresher so the live pair exists when they start. It renews only while the held leaf is still valid: the login that reads the new leaf is made with the current one. Refs #5017
166 lines
5.4 KiB
Nix
166 lines
5.4 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"; };
|
|
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/cert"
|
|
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key"
|
|
)
|
|
[
|
|
(subagent claudeBao)
|
|
(subagent acpBao)
|
|
(subagent opencodeBao)
|
|
];
|
|
}
|
|
{
|
|
name = "an opencode agent's subagent daemon gets the agent's store identity";
|
|
ok =
|
|
let
|
|
u = subagent opencodeBao;
|
|
in
|
|
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/cert"
|
|
&& u.environment.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/key";
|
|
}
|
|
{
|
|
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);
|
|
}
|
|
];
|
|
in
|
|
runGroup "agent-runtime" cases
|