Watch
0
0
Fork
You've already forked hyperhive
0

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:
atlas 2026-10-27 21:14:44 +01:00
commit 75e9e205e3
3 changed files with 933 additions and 31 deletions

View file

@ -0,0 +1,77 @@
#!/usr/bin/env bash
# The stub `bao` for ./agent-backend-env.nix. Leaves are files under
# `$FAKE_BAO_KV`, one per path. Every call is logged, a `kv` call without the
# token `login` printed fails, and the answers are the ones openbao gives:
# `kv get` of a leaf nobody wrote exits 2 with "No value found at <path>", and
# a policy granting `create` and `read` only refuses a second `-cas=0` write
# with a 403.
#
# `FAKE_BAO_LOGIN_RC` makes the login fail, `FAKE_BAO_DENY_READ` and
# `FAKE_BAO_DENY_WRITE` answer 403 as a policy that grants neither does, and
# `FAKE_BAO_RACE` has another writer create the leaf with that text between
# this run's first read and its write.
echo "$*" >>"$FAKE_BAO_LOG"
case "$1 $2" in
"login -method=cert")
[ "${FAKE_BAO_LOGIN_RC-0}" = 0 ] || {
echo 'Error authenticating: Put "https://bao.t.local:8200/v1/auth/cert/login": dial tcp: lookup bao.t.local: no such host' >&2
exit 2
}
printf 's.fake'
;;
"kv get" | "kv put")
[ "${BAO_TOKEN-}" = s.fake ] || {
echo "fake bao: kv called without the login's token" >&2
exit 97
}
;;&
"kv get")
[ "$3" = -field=env ] || {
echo "fake bao: get without -field=env: $*" >&2
exit 95
}
leaf=$FAKE_BAO_KV/$(printf '%s' "$4" | tr / _)
[ -z "${FAKE_BAO_DENY_READ-}" ] || {
printf 'Error reading %s: Error making API request.\n\nURL: GET https://bao.t.local:8200/v1/%s\nCode: 403. Errors:\n\n* 1 error occurred:\n\t* permission denied\n' "$4" "$4" >&2
exit 2
}
if [ -n "${FAKE_BAO_RACE-}" ] && [ ! -e "$FAKE_BAO_KV/.raced" ]; then
: >"$FAKE_BAO_KV/.raced"
printf '%s' "$FAKE_BAO_RACE" >"$leaf"
echo "No value found at ${4/secret\//secret/data/}" >&2
exit 2
fi
if [ ! -e "$leaf" ]; then
echo "No value found at ${4/secret\//secret/data/}" >&2
exit 2
fi
if [ "$(cat "$leaf.field" 2>/dev/null)" = other ]; then
echo 'Field "env" not present in secret' >&2
exit 1
fi
cat "$leaf"
;;
"kv put")
[ "$3" = -cas=0 ] || {
echo "fake bao: put without -cas=0: $*" >&2
exit 96
}
leaf=$FAKE_BAO_KV/$(printf '%s' "$4" | tr / _)
case "$5" in
env=@*) ;;
*)
echo "fake bao: put of something other than env=@file: $*" >&2
exit 94
;;
esac
if [ -n "${FAKE_BAO_DENY_WRITE-}" ] || [ -e "$leaf" ]; then
printf 'Error writing data to %s: Error making API request.\n\nURL: PUT https://bao.t.local:8200/v1/%s\nCode: 403. Errors:\n\n* 1 error occurred:\n\t* permission denied\n' "$4" "$4" >&2
exit 2
fi
cp "${5#env=@}" "$leaf"
;;
*)
echo "fake bao: unexpected call: $*" >&2
exit 98
;;
esac

View file

@ -0,0 +1,56 @@
# `checks.script-test-agent-backend-env` — runs ../agent-modules/backend-env.nix's
# unit against a stub `bao` that keeps its leaves in files. The cases are in
# ./agent-backend-env.sh and the stub is ./agent-backend-env-fake-bao.sh.
#
# What runs is the unit's rendered `ExecStart`, on the unit's own `PATH` with
# the stub in front. The edits are the unit's `/run/<unit>/` prefix and the
# operator's file, both moved under the build directory because the sandbox has
# no writable `/run` and no `/agents`.
{
pkgs,
lib,
self,
nixosSystem,
}:
let
inherit
(import ../module-eval/lib.nix {
inherit
pkgs
lib
self
nixosSystem
;
})
agentWith
;
earlierFile = "/agents/a1/harness/backend.env";
machine = agentWith {
services.hyperhive.agent = {
bao.addr = "https://bao.t.local:8200";
useApiKey = true;
backendEnvironmentFile = earlierFile;
};
};
unit = machine.systemd.services.hive-agent-backend-env;
fakeBao = pkgs.runCommand "fake-bao" { } ''
install -Dm755 ${./agent-backend-env-fake-bao.sh} $out/bin/bao
patchShebangs $out/bin
'';
in
pkgs.runCommand "hyperhive-script-test-agent-backend-env"
{
UNIT_SCRIPT = lib.removeSuffix " " unit.serviceConfig.ExecStart;
UNIT_PATH = unit.environment.PATH;
UNIT_BAO_ADDR = unit.environment.BAO_ADDR;
EARLIER_FILE = earlierFile;
FAKE_BAO_BIN = "${fakeBao}/bin";
}
''
${pkgs.bash}/bin/bash ${./agent-backend-env.sh}
touch "$out"
''

View file

@ -0,0 +1,306 @@
# Cases for ./agent-backend-env.nix. Each case gets a fresh runtime directory,
# credentials directory, operator's file and store, runs the unit's script under
# the unit's `UMask=0077` with a clean environment, and asserts on the exit
# code, the output, the files left behind and the `bao` calls made.
set -uo pipefail
failures=0
count=0
fail() {
echo "FAILED: $case: $*" >&2
failures=$((failures + 1))
}
leaf=secret/swarm/agents/a1/backend-env
login_call="login -method=cert -token-only"
get_call="kv get -field=env $leaf"
# setup <description>
setup() {
case=$1
dir=$(mktemp -d)
rt=$dir/rt
creds=$dir/creds
log=$dir/bao.log
kv=$dir/kv
earlier=$dir/earlier.env
mkdir -m 0700 "$rt"
mkdir "$creds" "$kv"
printf cert >"$creds/hive-agent-bao-cert"
printf key >"$creds/hive-agent-bao-key"
: >"$log"
if ! sed -e "s|/run/hive-agent-backend-env/|$rt/|g" -e "s|$EARLIER_FILE|$earlier|g" \
"$UNIT_SCRIPT" >"$dir/script"; then
echo "cannot read the rendered script" >&2
exit 1
fi
chmod +x "$dir/script"
if grep -q '/run/' "$dir/script"; then fail "the script still names /run after the rewrite"; fi
if ! grep -qF "$earlier" "$dir/script"; then fail "the rewrite did not reach the operator's file"; fi
login_rc=0
deny_read=
deny_write=
race=
stored=$kv/$(printf '%s' "$leaf" | tr / _)
}
go() {
count=$((count + 1))
(
umask 0077
cd "$dir" || exit 99
exec env -i \
PATH="$FAKE_BAO_BIN:$UNIT_PATH" \
BAO_ADDR="$UNIT_BAO_ADDR" \
BAO_CLIENT_CERT="$creds/hive-agent-bao-cert" \
BAO_CLIENT_KEY="$creds/hive-agent-bao-key" \
CREDENTIALS_DIRECTORY="$creds" \
FAKE_BAO_LOG="$log" \
FAKE_BAO_KV="$kv" \
FAKE_BAO_LOGIN_RC="$login_rc" \
FAKE_BAO_DENY_READ="$deny_read" \
FAKE_BAO_DENY_WRITE="$deny_write" \
FAKE_BAO_RACE="$race" \
"$dir/script"
) >"$dir/out" 2>"$dir/err"
rc=$?
}
expect_rc() {
if [ "$rc" != "$1" ]; then fail "exit $rc, expected $1; stderr: $(<"$dir/err")"; fi
}
expect_err() {
if ! grep -qF -- "$1" "$dir/err"; then fail "stderr lacks '$1'; stderr: $(<"$dir/err")"; fi
}
expect_out() {
if ! grep -qF -- "$1" "$dir/out"; then fail "stdout lacks '$1'; stdout: $(<"$dir/out")"; fi
}
# expect_calls <line>... — the exact `bao` argument lines, in order.
expect_calls() {
local want
want=$(printf '%s\n' "$@")
if [ "$(<"$log")" != "${want%$'\n'}" ]; then fail "bao calls were '$(<"$log")', expected '$*'"; fi
}
expect_no_calls() {
if [ -s "$log" ]; then fail "bao was called: $(<"$log")"; fi
}
# expect_same <path> <expected text> — byte for byte, trailing newlines included.
expect_same() {
if [ ! -e "$1" ]; then
fail "$1 missing"
return
fi
if ! printf '%s' "$2" | cmp -s - "$1"; then fail "$1 holds '$(<"$1")', expected '$2'"; fi
}
expect_no_file() {
if [ -e "$1" ]; then fail "$1 left behind"; fi
}
# The operator's file, kept to compare after the run.
put_earlier() {
printf '%s' "$1" >"$earlier"
chmod 0600 "$earlier"
earlier_inode=$(stat -c %i "$earlier")
}
expect_earlier_untouched() {
expect_same "$earlier" "$1"
if [ "$(stat -c %i "$earlier")" != "$earlier_inode" ]; then fail "the operator's file was replaced"; fi
}
# A leaf holding <text> before the run.
put_leaf() {
printf '%s' "$1" >"$stored"
}
expect_leaf() {
expect_same "$stored" "$1"
}
file_text=$'ANTHROPIC_API_KEY=k-file\nANTHROPIC_BASE_URL=http://x\n'
setup "no store identity delivered"
: >"$creds/hive-agent-bao-cert"
put_earlier "$file_text"
go
expect_rc 0
expect_err "has no store identity"
expect_no_calls
expect_no_file "$rt/env"
setup "a store holding nothing is seeded from the file, bytes unchanged, and the store's value is used"
put_earlier "$file_text"
go
expect_rc 0
expect_calls "$login_call" "$get_call" "kv put -cas=0 $leaf env=@$earlier" "$get_call"
expect_leaf "$file_text"
expect_same "$rt/env" "$file_text"
expect_earlier_untouched "$file_text"
if [ "$(stat -c %a "$rt/env")" != 600 ]; then fail "env is mode $(stat -c %a "$rt/env"), expected 600"; fi
expect_no_file "$rt/env.new"
expect_no_file "$rt/bao.err"
setup "a file with no final newline is seeded exactly"
put_earlier 'ANTHROPIC_API_KEY=k-file'
go
expect_rc 0
expect_leaf 'ANTHROPIC_API_KEY=k-file'
expect_same "$rt/env" 'ANTHROPIC_API_KEY=k-file'
expect_earlier_untouched 'ANTHROPIC_API_KEY=k-file'
setup "a value in the store wins over the file and is not written back to it"
put_earlier "$file_text"
put_leaf $'ANTHROPIC_API_KEY=k-bao\n'
go
expect_rc 0
expect_calls "$login_call" "$get_call"
expect_leaf $'ANTHROPIC_API_KEY=k-bao\n'
expect_same "$rt/env" $'ANTHROPIC_API_KEY=k-bao\n'
expect_earlier_untouched "$file_text"
setup "an unchanged store value leaves the file in place"
put_leaf "$file_text"
printf '%s' "$file_text" >"$rt/env"
chmod 0600 "$rt/env"
before=$(stat -c %i "$rt/env")
go
expect_rc 0
expect_out "is unchanged"
expect_same "$rt/env" "$file_text"
if [ "$(stat -c %i "$rt/env")" != "$before" ]; then fail "the unchanged env was replaced"; fi
setup "a changed store value replaces the file in place"
put_leaf $'ANTHROPIC_API_KEY=k-new\n'
printf old >"$rt/env"
chmod 0600 "$rt/env"
go
expect_rc 0
expect_out "fetched this agent's backend environment"
expect_same "$rt/env" $'ANTHROPIC_API_KEY=k-new\n'
expect_no_file "$rt/env.new"
setup "no file and nothing in the store writes nothing"
go
expect_rc 0
expect_out "no file to create it from"
expect_calls "$login_call" "$get_call"
expect_no_file "$rt/env"
expect_no_file "$stored"
setup "an empty file is never stored"
put_earlier ""
go
expect_rc 0
expect_calls "$login_call" "$get_call"
expect_no_file "$rt/env"
expect_no_file "$stored"
setup "nothing in the store leaves an env fetched earlier alone"
printf kept >"$rt/env"
chmod 0600 "$rt/env"
go
expect_rc 0
expect_same "$rt/env" kept
setup "a store that refuses the login with a file in place changes nothing and fails the unit"
put_earlier "$file_text"
login_rc=2
go
expect_rc 1
expect_err "did not accept this agent's certificate login"
expect_err "no such host"
expect_calls "$login_call"
expect_no_file "$rt/env"
expect_no_file "$stored"
expect_earlier_untouched "$file_text"
setup "a lost race reads back the winner's value and keeps the file"
put_earlier "$file_text"
race=$'ANTHROPIC_API_KEY=k-winner\n'
go
expect_rc 0
expect_calls "$login_call" "$get_call" "kv put -cas=0 $leaf env=@$earlier" "$get_call"
expect_err "could not create $leaf"
expect_leaf $'ANTHROPIC_API_KEY=k-winner\n'
expect_same "$rt/env" $'ANTHROPIC_API_KEY=k-winner\n'
expect_earlier_untouched "$file_text"
setup "a policy that refuses the create leaves the file to carry the agent"
put_earlier "$file_text"
deny_write=1
go
expect_rc 0
expect_calls "$login_call" "$get_call" "kv put -cas=0 $leaf env=@$earlier" "$get_call"
expect_err "permission denied"
expect_err "holds no usable env"
expect_no_file "$rt/env"
expect_no_file "$stored"
expect_earlier_untouched "$file_text"
setup "a leaf that exists but holds no env is not overwritten"
put_earlier "$file_text"
put_leaf ""
go
expect_rc 0
expect_calls "$login_call" "$get_call"
expect_err "holds no usable env"
expect_leaf ""
expect_no_file "$rt/env"
setup "a leaf without the env field is not overwritten"
put_earlier "$file_text"
put_leaf "x"
printf other >"$stored.field"
go
expect_rc 0
expect_calls "$login_call" "$get_call"
expect_leaf "x"
expect_no_file "$rt/env"
setup "a refused read fails the unit and writes nothing"
put_earlier "$file_text"
deny_read=1
go
expect_rc 1
expect_err "could not read $leaf"
expect_err "permission denied"
expect_calls "$login_call" "$get_call"
expect_no_file "$rt/env"
expect_earlier_untouched "$file_text"
# `UMask=0077` makes every file the script creates 0600; a stale leftover of a
# stopped run must not block the next.
setup "leftovers of a stopped run do not block the fetch"
put_leaf $'ANTHROPIC_API_KEY=k-bao\n'
printf stale >"$rt/env.new"
printf stale >"$rt/bao.err"
go
expect_rc 0
expect_same "$rt/env" $'ANTHROPIC_API_KEY=k-bao\n'
expect_no_file "$rt/env.new"
expect_no_file "$rt/bao.err"
# The control: the stub's create-only answer is what the lost-race and
# refused-create cases lean on, so show it refuses a second create.
setup "the stub refuses a create over an existing leaf"
put_leaf kept
printf x >"$dir/x"
(
PATH="$FAKE_BAO_BIN:$PATH" BAO_TOKEN=s.fake FAKE_BAO_LOG=$log FAKE_BAO_KV=$kv \
bao kv put -cas=0 "$leaf" env=@"$dir/x"
) >/dev/null 2>"$dir/err"
if [ $? -eq 0 ]; then fail "the stub took a second create"; fi
expect_leaf kept
if [ "$failures" -gt 0 ]; then
echo "script-test-agent-backend-env: $failures failed assertions over $count runs" >&2
exit 1
fi
echo "script-test-agent-backend-env: $count runs, all assertions hold"