# This agent's backend environment (`ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, # an ACP provider key), fetched from the swarm secret store by the agent # itself into the file the harness and the subagent daemon load as # `EnvironmentFile=`. # # The store holds the file's text at `swarm/agents//backend-env`, field # `env` (`swarm_secret_client::backend_env`). Each run does one of three things: # # - the store has a value: it is written to `/run/hive-agent-backend-env/env`; # - the store has none and `backendEnvironmentFile` is a non-empty file: the # file's exact bytes are created at the leaf with `-cas=0`, then read back # and written the same way. A run that loses the race to another writer, or # whose create the store's policy refuses, uses whatever the read-back finds; # - neither: nothing is written, which is an agent with no backend file. # # It never rotates a value and never touches `backendEnvironmentFile`. That # file stays an `EnvironmentFile=` of its own, listed before the fetched one: # systemd lets a later file override an earlier one, so the store's value wins # when there is one, and the file carries the agent when the store is down. # # The leaf's create grant cannot constrain content, so while the leaf is empty # the agent's own process can also create it with any text, and the store's # value then wins over the file. That is no wider than the file the agent # already owns: `./user.nix`'s `heal_own_dir` chowns the harness dir to the # agent at every activation, a gap the issue tracker has an open bug for. # # The unit runs as root because the file is root-owned `0600` where an operator # placed it. Root can still read the agent's `0400` store identity. { pkgs, lib, config, ... }: let agentCfg = config.services.hyperhive.agent; cfg = agentCfg.bao; agentName = agentCfg.user.name; # The server CA id ./bao.nix loads. serverCaCredential = "hive-agent-bao-server-ca"; unitName = "hive-agent-backend-env"; # The nix half of `swarm_secret_client::backend_env::env_path` plus # `path::MOUNT`. leaf = "secret/swarm/agents/${agentName}/backend-env"; runtimeDir = unitName; envFile = "/run/${runtimeDir}/env"; stagedFile = "/run/${runtimeDir}/env.new"; errFile = "/run/${runtimeDir}/bao.err"; # An OAuth claude agent has no backend credentials to fetch. fetches = cfg.addr != null && (agentCfg.useApiKey || agentCfg.runtime == "acp"); earlier = agentCfg.backendEnvironmentFile; storeRetry = import ../host-modules/lib/store-retry.nix { }; in { options.services.hyperhive.agent.backendEnvironment.environmentFiles = lib.mkOption { type = lib.types.listOf lib.types.str; readOnly = true; default = lib.optional (earlier != null) "-${earlier}" ++ lib.optional fetches "-${envFile}"; description = '' The `EnvironmentFile=` entries that carry this agent's backend environment, in load order: `backendEnvironmentFile` when set, then the file `${unitName}.service` keeps when the agent has a store and uses an API key. Read-only: the harness and the subagent daemon take it as it is. ''; }; config = lib.mkIf fetches { systemd.services.${unitName} = { description = "fetch this agent's backend environment from the secret store"; after = [ "network.target" "hive-agent-bao-identity.service" ]; wantedBy = [ "multi-user.target" ]; path = [ pkgs.openbao pkgs.coreutils pkgs.diffutils pkgs.gnugrep ]; # ../host-modules/lib/store-retry.nix. inherit (storeRetry) startLimitBurst startLimitIntervalSec; serviceConfig = storeRetry.serviceConfig // { Type = "oneshot"; # Not `RemainAfterExit`, so the timer below can start it again. # `RuntimeDirectoryPreserve` keeps the file between runs and after the # last one: the consumers read it at their own start. RemainAfterExit = false; TimeoutStartSec = 60; RuntimeDirectory = runtimeDir; RuntimeDirectoryMode = "0700"; RuntimeDirectoryPreserve = "yes"; # `0600`, the mode of the file an operator placed. UMask = "0077"; LoadCredential = [ serverCaCredential ]; }; environment = { BAO_ADDR = cfg.addr; BAO_CLIENT_CERT = cfg.clientCertFile; BAO_CLIENT_KEY = cfg.clientKeyFile; }; script = '' set -euo pipefail # No identity delivered: ./bao.nix's check reports that. for f in "$BAO_CLIENT_CERT" "$BAO_CLIENT_KEY"; do if [ ! -s "$f" ]; then echo "this agent has no store identity, so it cannot fetch its backend environment." >&2 exit 0 fi done if [ -s "$CREDENTIALS_DIRECTORY/${serverCaCredential}" ]; then export BAO_CACERT="$CREDENTIALS_DIRECTORY/${serverCaCredential}" fi err=${lib.escapeShellArg errFile} staged=${lib.escapeShellArg stagedFile} live=${lib.escapeShellArg envFile} earlier=${lib.escapeShellArg (if earlier == null then "" else earlier)} trap 'rm -f "$err" "$staged"' EXIT # Bounded, so a store that drops packets cannot hold the unit past its # start timeout. store() { timeout 15 bao "$@"; } if ! BAO_TOKEN="$(store login -method=cert -token-only 2>"$err")"; then echo "the swarm secret store at $BAO_ADDR did not accept this agent's certificate login:" >&2 if [ -s "$err" ]; then cat "$err" >&2; fi exit 1 fi export BAO_TOKEN # Sets READ to have, missing or unusable, and leaves the value in # $staged. `kv get` exits 2 with "No value found at " for a leaf # nobody wrote, and 2 again for a refused read, so the message tells # them apart. The value goes from the store's answer into a file and # is never in a variable or an argument. read_leaf() { rm -f "$staged" if store kv get -field=env ${lib.escapeShellArg leaf} >"$staged" 2>"$err"; then if [ -s "$staged" ]; then READ=have; else READ=unusable; fi elif grep -q '^No value found at ' "$err"; then READ=missing elif grep -q '^Field "env" not present' "$err"; then READ=unusable else echo "could not read ${leaf}:" >&2 cat "$err" >&2 exit 1 fi } read_leaf if [ "$READ" = missing ]; then if [ -n "$earlier" ] && [ -s "$earlier" ]; then echo "${leaf} holds nothing; creating it from $earlier." if ! store kv put -cas=0 ${lib.escapeShellArg leaf} env=@"$earlier" >/dev/null 2>"$err"; then echo "could not create ${leaf}; reading it back in case another run did:" >&2 cat "$err" >&2 fi read_leaf else echo "${leaf} holds nothing and there is no file to create it from." exit 0 fi fi if [ "$READ" != have ]; then echo "${leaf} holds no usable env; the file in place, if any, is used." >&2 exit 0 fi if cmp -s "$staged" "$live"; then echo "this agent's backend environment at ${leaf} is unchanged." exit 0 fi mv -f "$staged" "$live" echo "fetched this agent's backend environment from ${leaf}." ''; }; # The same cadence as ./github-token.nix. systemd.timers.${unitName} = { description = "re-fetch this agent's backend environment from the secret store"; wantedBy = [ "timers.target" ]; timerConfig = { OnUnitInactiveSec = "2min"; RandomizedDelaySec = "20s"; }; }; # `wants`, not `requires`: a store outage must not stop the harness, whose # `backendEnvironmentFile` still loads. `after` waits for one attempt. systemd.services.hive-agent = { wants = [ "${unitName}.service" ]; after = [ "${unitName}.service" ]; }; systemd.services.hive-subagent-daemon = { wants = [ "${unitName}.service" ]; after = [ "${unitName}.service" ]; }; }; }