#!/usr/bin/env bash # Verify mission delivery end-to-end against the deployed stack. # # Every verification of the delivery chain so far has been a throwaway bash # script, written fresh per run and discarded. One of them printed # # --- host .git owner (should be one uid) --- # UNKNOWN # # and that `UNKNOWN` is the whole reason this file exists: the probe could not # read its subject, and said so in a way that looked like output rather than # like failure. It would have printed `UNKNOWN` just as happily if the uid # split had come back. That is seam 4 — absence encoded as a legitimate value — # reappearing inside the tool built to detect seam 1. # # So the rules here are structural, not stylistic: # # 1. A probe returns a value or exits non-zero. There is no third outcome, # no placeholder, no empty string that a caller might read as "fine". # 2. The uid probe is self-tested against a mission KNOWN to have the split, # before any result from it is believed. A probe that cannot see the # known-bad case has not verified the good one — it has only failed to # look. `--selftest-only` runs that check alone. # 3. A scenario that never ran is FAIL-NORUN, never PASS. An absent branch # is indistinguishable from a dead container, and once scored PASS. # # Usage: # scripts/verify-mission-delivery.sh selftest # probe self-test alone # scripts/verify-mission-delivery.sh uids # uid probe, one mission # scripts/verify-mission-delivery.sh chain # phase continuity # scripts/verify-mission-delivery.sh multirole # 3 roles + real tests # scripts/verify-mission-delivery.sh noop # empty phase must FAIL # scripts/verify-mission-delivery.sh microvm # runs in a guest kernel + fans out # scripts/verify-mission-delivery.sh capacity # a burst > the fleet must QUEUE # scripts/verify-mission-delivery.sh drain-midmission # a drained node hands the mission on # scripts/verify-mission-delivery.sh all # everything # # Environment: # CLAWMATES_HOST ssh host running the stack (default gw-04) # CLAWMATES_OWNER_EMAIL account to mint a session for (default om.sobh@…) # CLAWMATES_UID_CONTROL mission id/prefix known to have a uid split; # auto-discovered when unset # CLAWMATES_REPO_ID scratch repo for the delivery scenarios # CLAWMATES_TEAM_TEMPLATE team template for the delivery scenarios # MISSION_TIMEOUT seconds to wait for a mission (default 1800) # CAPACITY_BURST missions to launch in the capacity burst # (default: fleet slots + 2 — smaller is reported # NORUN, since it never exercises the queue) set -uo pipefail HOST="${CLAWMATES_HOST:-gw-04}" OWNER="${CLAWMATES_OWNER_EMAIL:-om.sobh@gmail.com}" MISSIONS_ROOT="${CLAWMATES_MISSIONS_ROOT:-/var/lib/clawmates-missions}" # Real host kernels, read at start-up. The microvm scenario asserts the agent's # kernel differs from BOTH, which proves it ran in a guest without pinning a # vmlinux version that an upgrade would invalidate. GW_KERNEL=$(ssh "$HOST" 'uname -r' 2>/dev/null | tr -d '[:space:]') NODE_KERNEL=$(ssh "${FLEET_NODE:-osobh@tank}" 'uname -r' 2>/dev/null | tr -d '[:space:]') REPO_ID="${CLAWMATES_REPO_ID:-f8bbe4d7-2878-40c8-b657-7a7f6031def1}" TEAM_TEMPLATE="${CLAWMATES_TEAM_TEMPLATE:-7e453826-41c4-4425-bab5-8f11fd0a14d7}" MISSION_TIMEOUT="${MISSION_TIMEOUT:-1800}" FAILURES=0 CHECKS=0 # ── Reporting ──────────────────────────────────────────────────── # # Deliberately only three verdicts, and NORUN is one of them. "The scenario # did not execute" must not be able to borrow PASS's vocabulary. pass() { printf 'PASS %s\n' "$*"; CHECKS=$((CHECKS + 1)); } fail() { printf 'FAIL %s\n' "$*"; CHECKS=$((CHECKS + 1)); FAILURES=$((FAILURES + 1)); } norun() { printf 'FAIL-NORUN %s\n' "$*"; CHECKS=$((CHECKS + 1)); FAILURES=$((FAILURES + 1)); } info() { printf ' %s\n' "$*"; } die() { printf 'ABORT %s\n' "$*" >&2; exit 2; } # ── Session + API ──────────────────────────────────────────────── mint_session() { local secret hash secret="verify-$(openssl rand -hex 16)" hash=$(printf '%s' "$secret" | openssl dgst -sha256 -binary \ | openssl base64 -A | tr '+/' '-_' | tr -d '=') local rows rows=$(ssh "$HOST" "docker exec clawmates_postgres_1 psql -U postgres -d clawmates -tAc \ \"insert into auth_sessions (user_id, token_hash, expires_at) \ select id, '$hash', now() + interval '90 minutes' \ from users where email='$OWNER' limit 1 returning 1;\"" 2>/dev/null \ | head -1 | tr -d '[:space:]') # `head -1`: psql emits the returned row AND its `INSERT 0 1` command tag, # and collapsing both gave `1INSERT01`, which failed this check on a mint # that had in fact worked. # # No row inserted means no such user — without this check the script would # carry on and report every API call as a delivery failure. [ "$rows" = "1" ] || { echo "could not mint a session for $OWNER (no such user?)" >&2; return 1; } printf '%s' "$secret" } api() { # api [json] local t="$1" m="$2" p="$3" b="${4:-}" # The JSON body travels on STDIN (`curl -d @-`), not embedded in the command. # # It used to be interpolated into a single-quoted `-d '...'` inside a # double-quoted ssh command, which works only for bodies containing neither # apostrophes nor anything else the two shells rewrite. A task description # saying "the crate's test suite" ended the quoting and the remote shell died # with "unexpected EOF"; two attempts at escaping it were themselves wrong, # because the backslashes have to survive bash AND sed AND sh. Removing the # interpolation removes the whole class. if [ -n "$b" ]; then printf '%s' "$b" | ssh "$HOST" "docker run --rm -i --network clawmates_core \ curlimages/curl:latest -s -X $m \ -H 'Authorization: Bearer $t' -H 'Content-Type: application/json' -d @- \ http://clawmates_server_1:8080$p" else ssh "$HOST" "docker run --rm --network clawmates_core curlimages/curl:latest -s -X $m \ -H 'Authorization: Bearer $t' http://clawmates_server_1:8080$p" fi } # ── The uid probe ──────────────────────────────────────────────── # # The structural claim copy mode makes is that the host checkout has exactly # one writer. This is what tests it. It prints a sorted, comma-separated uid # list on stdout and exits 0, or prints nothing and exits 1. probe_uids() { # probe_uids local mission="$1" dir uids dir=$(ssh "$HOST" "ls -d $MISSIONS_ROOT/$mission* 2>/dev/null | head -1" | tr -d '\r') [ -n "$dir" ] || { echo "no mission dir under $MISSIONS_ROOT for $mission" >&2; return 1; } ssh "$HOST" "sudo test -d '$dir/repo'" 2>/dev/null \ || { echo "$dir/repo is not a directory" >&2; return 1; } # -printf '%U' over stat: one process for the whole tree, and it reports the # numeric uid even when the host has no passwd entry for it. uids=$(ssh "$HOST" "sudo find '$dir/repo' -xdev -printf '%U\n' 2>/dev/null | sort -un | paste -sd, -" | tr -d '\r') # An empty result is not "no uids", it is a failed read. A checkout always # contains files; if find returned nothing, find did not work. [ -n "$uids" ] || { echo "uid read produced no output for $dir/repo" >&2; return 1; } printf '%s' "$uids" } # Find a mission whose checkout still shows the pre-copy-mode split. Used as # the probe's negative control. discover_split_control() { ssh "$HOST" "for d in $MISSIONS_ROOT/*/repo; do [ -d \"\$d\" ] || continue n=\$(sudo find \"\$d\" -xdev -printf '%U\n' 2>/dev/null | sort -un | wc -l) if [ \"\$n\" -gt 1 ]; then basename \$(dirname \"\$d\"); break; fi done" 2>/dev/null | tr -d '\r' | head -1 } # ── Probe self-test ────────────────────────────────────────────── # # Run BEFORE trusting any uid result. A green uid report from a probe that # cannot detect the split is not evidence of anything. selftest_uid_probe() { local control uids control="${CLAWMATES_UID_CONTROL:-$(discover_split_control)}" if [ -z "$control" ]; then # Not a pass. Every checkout on the host is single-uid, which is the # desired end state but leaves the probe unexercised — so say exactly # that rather than implying the probe was validated. info "selftest: no split-uid mission remains on $HOST to use as a control" info "selftest: uid results below are UNVALIDATED (set CLAWMATES_UID_CONTROL)" return 0 fi uids=$(probe_uids "$control") || { fail "selftest: probe failed on control $control"; return 1; } case "$uids" in *,*) pass "selftest: probe reports the split on control $control (uids=$uids)" ;; *) fail "selftest: control $control reports a single uid ($uids) — the probe cannot detect the split it exists to find" ;; esac } check_single_uid() { # check_single_uid