Files
home-network-k3s-impl/infra/agent/probe-validation.sh
T
jakman85andClaude Opus 5.5 e539109830 feat(images): build, verify and publish the three images in CI; Renovate carries the digests
Operator rule 2026-10-29: image creation, hosting and lifecycle belong to CI/CD, never to a
workstation build/push. Nothing had ever been pushed to the Gitea registry.

- .gitea/workflows/images.yaml + scripts/images.sh: PRs build+verify (no credential); main
  builds, verifies and pushes toolbox, agent-runner (FROM toolbox) and conformance-runner as
  sha-<commit> + main, reading the digest back from the registry. Only changed images build.
- Each image's own build-image.sh does the build and in-image verification, now engine-neutral
  (DinD: no podman-only calls, probe over stdin not a bind mount). New toolbox build-image.sh.
- Digest references carry `# renovate-digest:` markers; renovate.json5 gains a custom.regex
  manager for them. The worker Job template is deliberately untracked (it would loop).
- renovate.yaml pins renovate-version 44.167.0: the action's default (39.264.0) rejects the
  current config's managerFilePatterns, so every run would have died at CONFIG_VALIDATION.
- Push credential: REGISTRY_PUSH_TOKEN secret; the Actions token has no package scope in
  Gitea 1.24.3 (container.go Authenticate). Retention: infra/gitea/registry-retention.sh.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 23:59:55 -05:00

133 lines
6.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
# `:-$0` because build-image.sh feeds this script over STDIN (`bash -s`), where BASH_SOURCE is
# empty and `set -u` would abort on it; PM_WORKER_PATH is always set in that case.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-$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