#!/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 the allowed in-cluster git host" \ PM_GITEA_URL=https://evil.invalid refuses "a real, reachable, WRONG git host" "is not the allowed in-cluster git host" \ PM_GITEA_URL=https://github.com refuses "the right host on a smuggled path" "is not the allowed in-cluster git host" \ PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:3000/../.. # This agent is IN the cluster, so it reaches Gitea by its cluster-local service name. The # EXTERNAL hostname is refused too — it used to be a second accepted value, and as an # accepted value it widened this allowlist to a host reachable over the `world:443` egress # rule, which is the injection shape the allowlist exists to refuse. refuses "this Gitea's EXTERNAL hostname" "is not the allowed in-cluster git host" \ PM_GITEA_URL=https://gitea.home.keithjackson.dev refuses "the cluster-local name over https" "is not the allowed in-cluster git host" \ PM_GITEA_URL=https://gitea.gitea.svc.cluster.local:3000 refuses "the cluster-local name, wrong port" "is not the allowed in-cluster git host" \ PM_GITEA_URL=http://gitea.gitea.svc.cluster.local:8080 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