#!/usr/bin/env bash # What berth has settled, and what it has withdrawn, written down as assertions. # # Two halves: # - decisions that hold. Failing one means "you are about to undo this". # - every entry in ../STALE.md. Failing one means a withdrawn assumption came # back. That is the half that makes STALE.md an audit surface and not an # archive — a retraction nobody re-reads is a retraction that decays. # # Scope: no cloud, no ssh, no sudo, no network. Cheap enough to actually run. # `make check` reports on the world and never fails; this exits 1, like rig's. # # Usage: make selftest (or: bash ctrl/selftest.sh) set -uo pipefail # NOT -e: one failing check must not abort the rest cd "$(dirname "$0")" source ./lib/config.sh rc=0 passed=0 check() { # name, expected, actual if [ "$2" = "$3" ]; then printf ' ok %s\n' "$1" passed=$((passed + 1)) else printf ' FAIL %s\n expected: %s\n got: %s\n' "$1" "$2" "$3" rc=1 fi } note() { printf '\n%s\n' "$1"; } skip() { printf ' skip %s (%s)\n' "$1" "$2"; } # An absence check must not match the files that RECORD the absence. STALE.md # names every withdrawn thing by definition, and this file names them again to # assert them — so both are excluded, or every check fails on itself. rig hits # the same wall and assembles its pattern from fragments for the same reason. NOSELF="--exclude=selftest.sh --exclude=STALE.md" absent() { grep -rIl $NOSELF "$@" 2>/dev/null | wc -l; } # A throwaway estate, for the checks that have to run berth rather than read it. TMP_ESTATE=_selftest cleanup() { rm -f "../estate/${TMP_ESTATE}.json"; } trap cleanup EXIT note "the withdrawn assumptions — ../STALE.md, one check each" # B1 — Pulumi. The two surviving mentions are historical fact about ppl/infra # and live in README.md and the estate, not in anything that runs. check "B1 no pulumi in the code" "0" "$(absent -i pulumi . ../Makefile)" # B2 — the ctlptl precedent. Withdrawn; the argument stands on its own now. check "B2 the withdrawn precedent is cited nowhere" "0" "$(absent -i ctlptl ..)" # B3 — `wg show dump` leaks the private key in field 1. Stating only # show-vs-showconf makes the dump form read as safe. check "B3 all three wg forms are named" "yes" \ "$(grep -q 'dump' vpn.sh && grep -q 'showconf' vpn.sh && echo yes || echo no)" check "B3 capture refuses showconf-shaped input" "1" \ "$(printf '[Interface]\nPrivateKey = x\n' | bash vpn.sh capture >/dev/null 2>&1; echo $?)" check "B3 capture refuses dump-shaped input" "1" \ "$(printf 'priv\tpub\t51820\toff\n' | bash vpn.sh capture >/dev/null 2>&1; echo $?)" # B4 — keepalive belongs to the peer that DIALS, not the one that roams. The # first version warned on a correctly configured overlay, so the check is run # against one: hub carries the keepalive, nrft roams. python3 - <<'PY' import json, collections d = json.load(open("../estate/mcrn.json"), object_pairs_hook=collections.OrderedDict) d["vpn"]["overlays"]["estate"]["peers"]["box"]["keepalive"] = 25 json.dump(d, open("../estate/_selftest.json", "w"), indent=2, ensure_ascii=False) PY check "B4 a correct overlay raises no keepalive warning" "0" \ "$(ESTATE=$TMP_ESTATE bash vpn.sh check 2>/dev/null | grep -ci 'no peer entry carries')" # B6 — public keys are 44-char base64 too, so shape alone would flag correct # data. Same fixture, with a real-shaped public key on a peer. python3 - <<'PY' import base64, collections, json, os d = json.load(open("../estate/_selftest.json"), object_pairs_hook=collections.OrderedDict) d["vpn"]["overlays"]["estate"]["peers"]["box"]["public_key"] = base64.b64encode(os.urandom(32)).decode() json.dump(d, open("../estate/_selftest.json", "w"), indent=2, ensure_ascii=False) PY check "B6 a public key does not trip the secret check" "0" \ "$(ESTATE=$TMP_ESTATE bash vpn.sh check 2>/dev/null | grep -c 'FAIL.*key')" cleanup # the fixture is done with; two estate files would make load_config # refuse to guess below, which is right but reads as a config failure # B5 — the pass-through block must be LAST, or a subcommand that names a real # target runs that target too. Checked through make, not by reading the file. note "B5 a subcommand that names a target dispatches once" for combo in "host ports" "host services" "vpn check" "vpn show estate" "estate show"; do check " make $combo" "1" \ "$(cd .. && make -n $combo 2>/dev/null | grep -c 'bash ctrl/')" done # B7 — the overlay moved out of network.wireguard into a top-level vpn block. check "B7 nothing reads network.wireguard" "0" "$(absent 'network\.wireguard' .)" # B8 — peers, not relatives. berth sources nothing from rig. check "B8 berth sources nothing from rig" "0" "$(absent -E 'rig/ctrl|\.\./rig' .)" # B9 — langfuse was filed as an exception a template could not express. It was # the general case. The proof is a live route: render it and diff against the # hand-written file, normalised for comments and whitespace. LIVE=/home/mariano/wdir/semester/ppl/gateway/nginx/conf.d/langfuse.conf if [ -f "$LIVE" ]; then norm() { sed -e 's/#.*//' -e 's/[[:space:]]\+/ /g' -e 's/^ //' -e 's/ $//' -e '/^$/d' "$1"; } bash services.sh render aws >/dev/null 2>&1 check "B9 the generated vhost reproduces the live one" "same" \ "$(diff -q <(norm ./render/out/aws/langfuse.conf) <(norm "$LIVE") >/dev/null 2>&1 \ && echo same || echo different)" else skip "B9 generated vhost matches the live one" "ppl not on this machine" fi note "the safety contract — berth's verbs are not all safe" check "estate defaults to show" "show" "$(cd .. && make -n estate 2>/dev/null | grep -oE 'estate\.sh [a-z]+' | awk '{print $2}')" check "certs defaults to status" "status" "$(cd .. && make -n certs 2>/dev/null | grep -oE 'certs\.sh [a-z]+' | awk '{print $2}')" check "dns defaults to list" "list" "$(cd .. && make -n dns 2>/dev/null | grep -oE 'dns\.sh [a-z]+' | awk '{print $2}')" check "vpn defaults to list" "list" "$(cd .. && make -n vpn 2>/dev/null | grep -oE 'vpn\.sh [a-z]+' | awk '{print $2}')" for verb in apply destroy; do check "estate $verb refuses without --yes" "1" \ "$(bash estate.sh "$verb" >/dev/null 2>&1; echo $?)" done for verb in renew push; do check "certs $verb refuses" "1" \ "$(bash certs.sh "$verb" >/dev/null 2>&1; echo $?)" done check "dns add refuses to change live DNS" "1" \ "$(bash dns.sh add selftest >/dev/null 2>&1; echo $?)" check "vpn up refuses without --yes" "1" \ "$(bash vpn.sh up >/dev/null 2>&1; echo $?)" note "config — the caller's env beats the files" # Generated from CONFIG_OVERRIDABLE, so a new key enrols itself. test_value() { case "$1" in TARGET) echo "gcp" ;; ESTATE) echo "mcrn" ;; *) echo "selftest-sentinel" ;; esac } for key in $CONFIG_OVERRIDABLE; do want="$(test_value "$key")" got="$(export "$key=$want"; load_config >/dev/null 2>&1; echo "${!key}")" check " caller's $key wins" "$want" "$got" done note "rig agreement — recomputed, never imported" # rig pins these same constants in its own selftest. Both arrive at them from # the same formula with no shared code, which is the coupling rule made testable. check "derive_port_base rig" "20310" "$(derive_port_base rig)" check "derive_port_base foo" "21690" "$(derive_port_base foo)" check "derive_port_base my-proj" "21030" "$(derive_port_base my-proj)" note "containment — berth writes nothing outside berth/" check "no tracked change outside berth/" "0" \ "$(cd ../.. && git status --porcelain 2>/dev/null | grep -vc '^.. berth/')" check "generated output is ignored" "yes" \ "$(cd .. && git check-ignore -q ctrl/render/out && echo yes || echo no)" # A trailing-slash pattern matches directories only, so ask about a path # inside it rather than the (not-yet-existing) directory itself. check "key material is ignored" "yes" \ "$(cd .. && git check-ignore -q ctrl/.secrets/vpn/any.key && echo yes || echo no)" note "capture and the checks that read it — three bugs found by running, 2026-09-14" # 1. A placed service's upstream is DERIVED (✖ B9). Anything reading the raw # `upstream` field sees "" and skips it — which is how vpn.sh's bindings # invariant, the "my configurations broke" detector, went quiet the day # placement landed while still printing OK. Vacuous passes are the failure # mode this whole file exists to catch. check "a placed service resolves to a real upstream" "10.8.0.2:3000" \ "$(bash -c 'source ./lib/config.sh; source ./lib/estate.sh; load_config >/dev/null; service_upstream "" local nrft 3000')" check "bindings actually inspects a service" "1" \ "$(bash ./vpn.sh check 2>/dev/null | grep -c 'no service currently has an overlay address' \ | awk '{print 1-$1}')" # 2. A listen port belongs to a PEER. The roaming peer's is an ephemeral source # port; writing it to the overlay renames the port the firewall rule is # checked against — silently, since both are plausible integers. check "a roaming peer's port is not the overlay's port" "51820" \ "$(python3 -c 'import json;print(json.load(open("../estate/mcrn.json"))["vpn"]["overlays"]["estate"]["listen_port"])')" # 3. _status is always non-empty — capture rewrites it rather than clearing it — # so a warning gated on "is it set" can never turn off, including after the # capture it asks for. Gate on the structure instead. check "the capture warning clears once keys are in" "0" \ "$(bash ./check.sh 2>/dev/null | grep -c 'public keys not captured')" note "every STALE entry has a check here" # Not "$0": line 15 cd's into this script's directory, so a relative $0 no # longer resolves. After the cd the file is simply selftest.sh. # Ids are counted wherever they appear — B5's sits in a note(), not a check name. entries="$(grep -c '^\*\*✖ B' ../STALE.md)" checked="$(grep -oE '\bB[1-9][0-9]?\b' selftest.sh | sort -u | wc -l)" check "STALE.md entries are all covered" "$entries" "$checked" printf '\n%d passed' "$passed" [ "$rc" -ne 0 ] && printf ', SOME FAILED' printf '\n' exit "$rc"