pm-worker's command is pinned by admission with no args, so everything a caller could influence arrives in the environment — and the guards at the top of that script are what stand between an injected variable and a clone from the wrong host or a push to the wrong ref. Nothing exercised them, which makes them a claim rather than a control. probe-validation.sh asserts 17 refusals, each on BOTH a non-zero exit and the specific message, so a guard that stopped firing cannot pass by failing somewhere else. It covers a redirected git host, a real and reachable WRONG one (the egress policy permits tcp/443, so that case is not hypothetical), the right host with a smuggled path, 'main' and five other bad branch shapes, a malformed owner/repo, a bad mode, a bad issue number, and each missing credential. build-image.sh runs it inside the built image too. Writing it found three things: - the PM_BRANCH != main guard is unreachable while the pattern check above it holds, so the comment claiming it as the defence was wrong about which line does the work. Kept as an assertion independent of the regex; the comment now says so. - the EXIT trap ended in a failing test, which under set -e can replace the script's real exit code with that failure. - a provenance LOG LINE read a file through a redirect that set -o pipefail would have turned into a dead worker if the file were absent. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
121 lines
5.5 KiB
Bash
Executable File
121 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
###############################################################################
|
|
# infra/agent/probe-validation.sh — prove pm-worker REFUSES hostile input.
|
|
#
|
|
# ./probe-validation.sh # from anywhere; needs only bash
|
|
#
|
|
# WHY THIS EXISTS. pm-worker's `command` is pinned by admission with no args, so everything a
|
|
# caller could influence arrives in the ENVIRONMENT — and the guards at the top of that
|
|
# script are what stand between an injected variable and a clone from the wrong host, a push
|
|
# to the wrong ref, or a session with the wrong credential. Guards that nothing exercises are
|
|
# a claim, not a control. Each case below asserts BOTH a non-zero exit AND the specific
|
|
# message, because a check that only grepped stderr would pass a build where the guard had
|
|
# stopped rejecting and something else had started failing instead.
|
|
#
|
|
# Every case must die BEFORE the script reaches any network call: the probe runs with a
|
|
# scrubbed environment and fake credentials, and if a guard ever stopped firing, the case
|
|
# would fail here rather than by really cloning something.
|
|
#
|
|
# Run offline, no cluster, no Gitea, no model. infra/agent/build-image.sh runs it inside the
|
|
# built image as well, where the interpreter and the script are the ones that will actually
|
|
# run in the cluster.
|
|
###############################################################################
|
|
set -uo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
WORKER="${PM_WORKER_PATH:-${HERE}/pm-worker}"
|
|
[ -r "$WORKER" ] || { echo "cannot read ${WORKER}" >&2; exit 2; }
|
|
|
|
# A complete, VALID assignment. Each case below overrides exactly one thing, so a failure
|
|
# names the guard rather than a pile of missing variables.
|
|
BASE=(
|
|
PM_TASK_MODE=implement
|
|
PM_ISSUE_NUMBER=42
|
|
PM_BRANCH=feat/some-real-slug
|
|
PM_BASE_BRANCH=main
|
|
PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:3000
|
|
PM_GITEA_OWNER=Home-Network
|
|
PM_GITEA_REPO=home-network-k3s-impl
|
|
PM_GITEA_USER=pm-agent
|
|
GITEA_TOKEN=not-a-real-token
|
|
CLAUDE_CODE_OAUTH_TOKEN=not-a-real-token
|
|
)
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
# `env -i` so nothing from the invoking shell leaks in and accidentally satisfies a guard.
|
|
refuses() {
|
|
local label="$1" want="$2"; shift 2
|
|
local out rc=0
|
|
out="$(env -i PATH="$PATH" HOME=/tmp "${BASE[@]}" "$@" bash "$WORKER" 2>&1)" || rc=$?
|
|
if [ "$rc" -eq 0 ]; then
|
|
printf ' FAIL %s — exited 0; it should have been refused\n' "$label"
|
|
FAIL=$((FAIL + 1))
|
|
return
|
|
fi
|
|
if printf '%s' "$out" | grep -qF "$want"; then
|
|
printf ' ok %s (exit %s)\n' "$label" "$rc"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
printf ' FAIL %s — refused (exit %s) but not for the expected reason %q:\n' "$label" "$rc" "$want"
|
|
printf '%s\n' "$out" | tail -3 | sed 's/^/ /'
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
echo "pm-worker input-validation probe ($WORKER)"
|
|
|
|
# An argument on a pinned command is caller-supplied code. There is no argument surface.
|
|
out="$(env -i PATH="$PATH" HOME=/tmp "${BASE[@]}" bash "$WORKER" --layer l2 2>&1)" || rc=$?
|
|
if printf '%s' "$out" | grep -qF "takes no arguments"; then
|
|
echo " ok arguments are refused (exit ${rc:-?})"; PASS=$((PASS + 1))
|
|
else
|
|
echo " FAIL arguments are refused — got: $(printf '%s' "$out" | tail -1)"; FAIL=$((FAIL + 1))
|
|
fi
|
|
|
|
# --- the ref it may create and push ---------------------------------------
|
|
refuses "branch 'main' (CLAUDE.md §1)" "does not match" PM_BRANCH=main
|
|
refuses "branch with a wrong prefix" "does not match" PM_BRANCH=feature/x
|
|
refuses "branch with a shell metacharacter" "does not match" 'PM_BRANCH=feat/x;id'
|
|
refuses "branch with path traversal" "does not match" 'PM_BRANCH=../../etc/passwd'
|
|
refuses "branch too short for the slug rule" "does not match" PM_BRANCH=fix/ab
|
|
refuses "branch equal to the base branch" "must not be the base branch" \
|
|
PM_BRANCH=feat/same-slug PM_BASE_BRANCH=feat/same-slug
|
|
|
|
# --- where code comes FROM and goes TO -------------------------------------
|
|
# The namespace's egress policy permits tcp/443 to the world (the model needs it), so an
|
|
# injected host would be REACHABLE. These are the cases that matter most.
|
|
refuses "a redirected git host" "is not an allowed git host" \
|
|
PM_GITEA_URL=https://evil.invalid
|
|
refuses "a real, reachable, WRONG git host" "is not an allowed git host" \
|
|
PM_GITEA_URL=https://github.com
|
|
refuses "the right host on a smuggled path" "is not an allowed git host" \
|
|
PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:3000/../..
|
|
refuses "a malformed owner" "PM_GITEA_OWNER is malformed" \
|
|
'PM_GITEA_OWNER=../other-org'
|
|
refuses "a malformed repository" "PM_GITEA_REPO is malformed" \
|
|
'PM_GITEA_REPO=repo;id'
|
|
|
|
# --- the assignment itself -------------------------------------------------
|
|
refuses "an unknown task mode" "must be 'implement' or 'revise'" \
|
|
PM_TASK_MODE=merge
|
|
refuses "a non-numeric issue number" "must be a positive integer" \
|
|
PM_ISSUE_NUMBER=42x
|
|
refuses "issue number zero" "must be a positive integer" \
|
|
PM_ISSUE_NUMBER=0
|
|
|
|
# --- the credentials ------------------------------------------------------
|
|
refuses "a missing model credential" "CLAUDE_CODE_OAUTH_TOKEN is not set" \
|
|
CLAUDE_CODE_OAUTH_TOKEN=
|
|
refuses "a missing git credential" "GITEA_TOKEN is not set" \
|
|
GITEA_TOKEN=
|
|
|
|
echo
|
|
if [ "$FAIL" -eq 0 ]; then
|
|
echo "probe OK — ${PASS} refusal(s), 0 failure(s)"
|
|
exit 0
|
|
fi
|
|
echo "probe FAILED — ${FAIL} of $((PASS + FAIL)) case(s) did not refuse as expected"
|
|
exit 1
|