From 6d0c30ade2461ee379f0cc3926d17839ca6c7881 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 24 Sep 2026 16:40:38 +0200 Subject: [PATCH] module-eval: pin the agent forge-token fetch and tea-login's removal Refs #3782 --- nix/checks.nix | 4 + nix/module-eval/agent-forge-bao.nix | 169 ++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 nix/module-eval/agent-forge-bao.nix diff --git a/nix/checks.nix b/nix/checks.nix index c5238637..bbb0110b 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -108,6 +108,10 @@ in inherit pkgs self nixosSystem; inherit (pkgs) lib; }; + module-eval-agent-forge-bao = import ./module-eval/agent-forge-bao.nix { + inherit pkgs self nixosSystem; + inherit (pkgs) lib; + }; module-eval-agent-memory = import ./module-eval/agent-memory.nix { inherit pkgs self nixosSystem; inherit (pkgs) lib; diff --git a/nix/module-eval/agent-forge-bao.nix b/nix/module-eval/agent-forge-bao.nix new file mode 100644 index 00000000..ce7ef5c9 --- /dev/null +++ b/nix/module-eval/agent-forge-bao.nix @@ -0,0 +1,169 @@ +# `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. +{ + 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; + 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; + tokenFile = machine: machine.services.hyperhive.agent.forge.tokenFile; +in +let + cases = [ + { + # 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 proves. Every `BAO_*` value is an address or a + # `%d/` 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 + builtins.elem "hive-agent-bao-cert" u.serviceConfig.LoadCredential + && builtins.elem "hive-agent-bao-key" u.serviceConfig.LoadCredential + && e.BAO_ADDR == baoAddr + && e.BAO_CLIENT_CERT == "%d/hive-agent-bao-cert" + && e.BAO_CLIENT_KEY == "%d/hive-agent-bao-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"; + } + { + # Rename in only on a change: a reader never sees half a token, and the + # avatar watcher does not fire on every timer tick. + name = "the fetch swaps the token in by rename, only when it changed"; + ok = + let + s = (fetchUnit agentForgeBao).script; + in + lib.hasInfix "cmp -s" s && lib.hasInfix "mv -f" s; + } + { + # 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); + } + ]; +in +runGroup "agent-forge-bao" cases