# `checks.module-eval-agent-forge-bao` — see ./lib.nix for the shared # rationale (why this suite exists, naming convention, "evaluates # not executes"). # # The agent side of the swarm-minted forge token: ../agent-modules/forge-token.nix # fetches it, and ../agent-modules/forge.nix's readers find it. The same fixtures # cover ../agent-modules/egress-proxy.nix, the agent's proxy credential and the # environment built from it. { pkgs, lib, self, nixosSystem, }: let inherit (import ./lib.nix { inherit pkgs lib self nixosSystem ; }) agentWith runGroup ; forgeUrl = "http://forge.t.local"; baoAddr = "https://bao.t.local:8200"; # A forge and a store: the fetch exists and every reader points at it. agentForgeBao = agentWith { services.hyperhive.agent.bao.addr = baoAddr; services.hyperhive.agent.forge.url = forgeUrl; # A second `EnvironmentFile=` on the harness, which the proxy file must join. services.hyperhive.agent.backendEnvironmentFile = "/var/lib/t/backend.env"; services.hyperhive.agent.icon = pkgs.emptyFile; }; # A forge and no store: the absence arm, and what makes the cases above able # to fail. agentForgeNoBao = agentWith { services.hyperhive.agent.forge.url = forgeUrl; services.hyperhive.agent.icon = pkgs.emptyFile; }; fetchUnit = machine: machine.systemd.services.hive-agent-forge-token; proxyUnit = machine: machine.systemd.services.hive-agent-proxy-credential; accountsUnit = machine: machine.systemd.services.hive-agent-forge-accounts; tokenFile = machine: machine.services.hyperhive.agent.forge.tokenFile; proxyEnvEntry = "-/run/hive-agent-proxy-credential/proxy.env"; # The units that make outbound calls and so load the proxy file. This fixture # has an icon, a forge and GitHub on, and no matrix accounts. proxyConsumers = [ "hive-agent" "hive-bash-daemon" "hive-subagent-daemon" "hive-mcp-http" "hive-forge-notify" "hive-github-notify" "forge-avatar-sync" ]; in let cases = [ { # ../agent-modules/hive-ca.nix. Presence arm first (the unit, the # timer, the trust variables, and every consumer ordered after the # unit and requiring it), then the absence arm for an agent with no # store, which has no root to fetch. name = "an agent with a store address fetches the swarm root and its consumers wait for it"; ok = let svc = agentForgeBao.systemd.services; unit = "hive-agent-trust-bundle.service"; bundle = "/run/hive-agent-trust-bundle/bundle.pem"; consumers = [ "hive-agent" "hive-bash-daemon" "hive-subagent-daemon" "hive-mcp-http" "hive-forge-notify" "forge-avatar-sync" ]; in svc ? hive-agent-trust-bundle && agentForgeBao.systemd.timers ? hive-agent-trust-bundle && lib.hasInfix "/v1/pki/ca/pem" svc.hive-agent-trust-bundle.script && agentForgeBao.systemd.globalEnvironment.SSL_CERT_FILE == bundle && agentForgeBao.systemd.globalEnvironment.NODE_EXTRA_CA_CERTS == "/run/hive-agent-trust-bundle/root.pem" && lib.all (c: lib.elem unit svc.${c}.after && lib.elem unit svc.${c}.requires) consumers && !(agentForgeNoBao.systemd.services ? hive-agent-trust-bundle) && !(agentForgeNoBao.systemd.globalEnvironment ? SSL_CERT_FILE) && !(lib.elem unit (agentForgeNoBao.systemd.services.hive-agent.after or [ ])); } { # The whole switch is the store address, as for the queue credential. name = "an agent with a store address fetches its forge token"; ok = agentForgeBao.systemd.services ? hive-agent-forge-token && agentForgeBao.systemd.timers ? hive-agent-forge-token; } { name = "an agent told no store address fetches no forge token"; ok = !(agentForgeNoBao.systemd.services ? hive-agent-forge-token) && !(agentForgeNoBao.systemd.timers ? hive-agent-forge-token) && !(agentForgeNoBao.systemd.globalEnvironment ? HIVE_FORGE_TOKEN_FILE); } { # The nix half of `swarm_secret_client::forge::agent_token_path` plus the # mount. Spelled out: the Rust test pins `swarm/agents/atlas/forge-token`, # and a drift between the two is a fetch that 404s forever and says # "not minted yet". name = "the fetch reads the agent's own forge-token path"; ok = let name = agentForgeBao.services.hyperhive.agent.user.name; in lib.hasInfix "secret/swarm/agents/${name}/forge-token" (fetchUnit agentForgeBao).script; } { # One store identity per agent: the fetch presents the same certificate # the identity check keeps current. Every `BAO_*` value is an address or # a path, never a value. name = "the fetch presents the agent's own store identity, by path"; ok = let u = fetchUnit agentForgeBao; e = u.environment; in e.BAO_ADDR == baoAddr && e.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert" && e.BAO_CLIENT_KEY == "/run/hive-agent-bao-identity/current/key"; } { # The field is the secret. It goes to a file by redirect and is never # echoed. name = "the fetch writes the token by redirect and never echoes it"; ok = let s = (fetchUnit agentForgeBao).script; in lib.hasInfix "-field=value" s && lib.hasInfix "> ${lib.escapeShellArg "/run/hive-agent-forge-token/token.new"}" s && !(lib.hasInfix "echo \"$(bao" s) && !(lib.hasInfix "echo $(bao" s); } { # A timer can only start an inactive unit, so the fetch must not stay # active; the directory, and the token in it, has to outlive each run. name = "the fetch can be re-run by its timer without losing the token"; ok = let c = (fetchUnit agentForgeBao).serviceConfig; in !c.RemainAfterExit && c.RuntimeDirectory == "hive-agent-forge-token" && c.RuntimeDirectoryPreserve == "yes" && c.UMask == "0377"; } { # Every unit and the bash-task runner find the token through this. A # path, never the value. name = "the fetched token's path is published to every unit"; ok = agentForgeBao.systemd.globalEnvironment.HIVE_FORGE_TOKEN_FILE == tokenFile agentForgeBao && tokenFile agentForgeBao == "/run/hive-agent-forge-token/token"; } { # The avatar sync has to re-fire when the swarm's token lands, which is # the file the fetch writes, not the state-dir file nothing writes any # more. name = "the avatar watcher follows the fetched token"; ok = agentForgeBao.systemd.paths.forge-avatar-sync.pathConfig.PathChanged == tokenFile agentForgeBao && builtins.elem "hive-agent-forge-token.service" agentForgeBao.systemd.services.forge-avatar-sync.after; } { # Both readers take the fetched token first and fall back to the state # file, which is still the only copy for an agent with no store identity. name = "the avatar sync reads the fetched token before the state file"; ok = let s = agentForgeBao.systemd.services.forge-avatar-sync.script; name = agentForgeBao.services.hyperhive.agent.user.name; fetched = lib.escapeShellArg (tokenFile agentForgeBao); state = lib.escapeShellArg "/agents/${name}/state/forge-token"; in lib.hasInfix "for f in ${fetched} ${state}; do" s; } { # tea-login copied the token into ~/.config/tea/config.yml, which # docs/swarm/credentials.md forbids for a store secret. Gone, with the # package it configured. name = "no tea-login unit and no tea package"; ok = !(agentForgeBao.systemd.services ? tea-login) && !(agentForgeNoBao.systemd.services ? tea-login) && !(builtins.elem pkgs.tea agentForgeBao.environment.systemPackages); } { name = "an agent with a store address fetches its external forge accounts, and one without does not"; ok = agentForgeBao.systemd.services ? hive-agent-forge-accounts && agentForgeBao.systemd.timers ? hive-agent-forge-accounts && !(agentForgeNoBao.systemd.services ? hive-agent-forge-accounts) && !(agentForgeNoBao.systemd.timers ? hive-agent-forge-accounts); } { # The nix half of `swarm_secret_client::forge::{accounts_dir,account_path}`, # and the two file names `hive-forge -f` reads. name = "the account fetch lists the agent's own accounts and reads each into hive-forge's files"; ok = let name = agentForgeBao.services.hyperhive.agent.user.name; s = (accountsUnit agentForgeBao).script; in lib.hasInfix "bao kv list -format=json secret/swarm/agents/${name}/forge >" s && !(lib.hasInfix "/index/" s) && lib.hasInfix "path=\"secret/swarm/agents/${name}/forge/$label\"" s && lib.hasInfix "/agents/${name}/state" s && lib.hasInfix "/forge-$label-token" s && lib.hasInfix "/forge-$label.json" s && lib.hasInfix "{base_url: (.data.data.url | strings)}" s; } { # The agent user owns its state dir (./user.nix), and the files keep # the `0600` they have always had. name = "the account fetch runs as the agent, with its own store identity"; ok = let u = accountsUnit agentForgeBao; name = agentForgeBao.services.hyperhive.agent.user.name; in u.serviceConfig.User == name && u.serviceConfig.UMask == "0077" && u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"; } { # Files a hive wrote keep working until the operator re-links them. name = "the account fetch never deletes a state-dir file it did not stage"; ok = let s = (accountsUnit agentForgeBao).script; in !(lib.hasInfix "rm -f \"$token\"" s) && !(lib.hasInfix "rm -f \"$sidecar\"" s) && !(lib.hasInfix "forge-*" s); } { name = "the account fetch re-runs every two minutes"; ok = agentForgeBao.systemd.timers.hive-agent-forge-accounts.timerConfig.OnUnitInactiveSec == "2min"; } { # ../agent-modules/egress-proxy.nix. Presence arm: the unit and its timer # exist, and the agent is told nothing until a credential is fetched. name = "an agent with a store address fetches its egress-proxy credential, and one without does not"; ok = agentForgeBao.systemd.services ? hive-agent-proxy-credential && agentForgeBao.systemd.timers ? hive-agent-proxy-credential && !(agentForgeNoBao.systemd.services ? hive-agent-proxy-credential) && !(agentForgeNoBao.systemd.timers ? hive-agent-proxy-credential); } { # The nix half of `swarm_secret_client::proxy::agent_credential_path` # plus the mount. name = "the proxy fetch reads the agent's own proxy-credential path, by redirect, and never echoes it"; ok = let name = agentForgeBao.services.hyperhive.agent.user.name; s = (proxyUnit agentForgeBao).script; in lib.hasInfix "secret/swarm/agents/${name}/proxy-credential" s && lib.hasInfix "-field=value" s && lib.hasInfix "> ${lib.escapeShellArg "/run/hive-agent-proxy-credential/secret.new"}" s && !(lib.hasInfix "echo \"$(bao" s) && !(lib.hasInfix "echo $(bao" s) && !(lib.hasInfix "echo \"$secret" s) && !(lib.hasInfix "printf '%s\\n' \"$secret" s); } { name = "the proxy fetch runs as the agent with its own store identity and keeps its file between runs"; ok = let u = proxyUnit agentForgeBao; c = u.serviceConfig; in c.User == agentForgeBao.services.hyperhive.agent.user.name && c.UMask == "0177" && !c.RemainAfterExit && c.RuntimeDirectory == "hive-agent-proxy-credential" && c.RuntimeDirectoryMode == "0700" && c.RuntimeDirectoryPreserve == "yes" && u.environment.BAO_ADDR == baoAddr && u.environment.BAO_CLIENT_CERT == "/run/hive-agent-bao-identity/current/cert"; } { # Presence: every outbound unit loads the file, and the harness keeps the # entry it already had. Absence: an agent with no store loads none. name = "every outbound unit loads the proxy file optionally, beside its other environment files"; ok = let svc = agentForgeBao.systemd.services; files = c: svc.${c}.serviceConfig.EnvironmentFile or [ ]; in lib.all ( c: lib.elem proxyEnvEntry (files c) && lib.elem "hive-agent-proxy-credential.service" svc.${c}.after && lib.elem "hive-agent-proxy-credential.service" svc.${c}.wants && !(lib.elem "hive-agent-proxy-credential.service" (svc.${c}.requires or [ ])) ) proxyConsumers && lib.elem "-/var/lib/t/backend.env" (files "hive-agent") && !(lib.any ( c: lib.elem proxyEnvEntry (agentForgeNoBao.systemd.services.${c}.serviceConfig.EnvironmentFile or [ ]) ) [ "hive-agent" "hive-bash-daemon" "hive-forge-notify" ] ); } { # The credential is in a file only the fetch writes. Nothing static (a # unit environment, the global one, the login shells) carries a proxy # variable, so an agent without a fetched credential goes direct. name = "no proxy variable is set where the credential is not"; ok = let names = [ "HTTPS_PROXY" "HTTP_PROXY" "https_proxy" "http_proxy" "NO_PROXY" "no_proxy" "NODE_USE_ENV_PROXY" ]; anywhere = m: n: m.systemd.globalEnvironment ? ${n} || m.environment.variables ? ${n} || lib.any (s: s.environment ? ${n}) (lib.attrValues m.systemd.services); in !(lib.any (anywhere agentForgeBao) names) && !(lib.any (anywhere agentForgeNoBao) names); } { # The hive's facts are the only source of the proxy address and of the # hosts that skip it, and they are read before the store is asked: a hive # without them gets no login and no file. The loopback names stay local. name = "the proxy file takes the proxy and the direct hosts from the hive's facts, before it asks the store"; ok = let s = (proxyUnit agentForgeBao).script; at = needle: lib.stringLength (lib.head (lib.splitString needle s)); in lib.hasInfix "HIVE_SQUID_PROXY) proxy=$value" s && lib.hasInfix "HIVE_EGRESS_NO_PROXY) direct=$value" s && lib.hasInfix "no_proxy=localhost,127.0.0.1,::1,$direct" s && lib.hasInfix "/etc/hyperhive-hive-facts" s && at "HIVE_SQUID_PROXY)" < at "bao login" && !(lib.hasInfix "hyperhive-bridge-dns" s); } { name = "the proxy file names every variable the clients read, and the credential only in the URL's userinfo"; ok = let s = (proxyUnit agentForgeBao).script; name = agentForgeBao.services.hyperhive.agent.user.name; in lib.all (v: lib.hasInfix "printf '${v}=%s\\n'" s) [ "HTTPS_PROXY" "HTTP_PROXY" "https_proxy" "http_proxy" "NO_PROXY" "no_proxy" ] && lib.hasInfix "NODE_USE_ENV_PROXY=1" s && lib.hasInfix "url=\"http://${name}:$secret@$proxy\"" s; } ]; in runGroup "agent-forge-bao" cases