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:
parent
5486fc0333
commit
75e9e205e3
1 changed files with 933 additions and 31 deletions
306
nix/script-tests/agent-backend-env.sh
Normal file
306
nix/script-tests/agent-backend-env.sh
Normal 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"
|
||||
Loading…
Reference in a new issue