#!/usr/bin/env bash # Tool-inventory parity sweep (NAROS.md §10.3): nothing agents rely on may silently # vanish when the default sandbox image moves from nucleic-sandbox:v7 to naros-agent. # # parity-sweep.sh [--allow FILE] # # Compares (a) the set of executables on PATH and (b) the npm global package set, in # each image. Anything present in the REFERENCE but missing from the CANDIDATE fails # the sweep unless listed in the allowlist (deliberate, understood removals — one name # per line, `#` comments). Additions are reported informationally. Python module parity # is not swept: neither image bakes site-packages beyond pip's own. # # Exit: 0 parity holds, 1 unexplained removals, 2 usage/docker errors. set -euo pipefail CAND="${1:?usage: parity-sweep.sh [--allow FILE]}" REF="${2:?missing reference image}" shift 2 ALLOW="$(cd "$(dirname "$0")" && pwd)/parity-allowlist.txt" while [ $# -gt 0 ]; do case "$1" in --allow) ALLOW="$2"; shift 2 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done # Every executable name reachable on the image's default PATH. --entrypoint /bin/sh is # nash in naros images and dash in the v7 base — the script is portable to both. path_inventory() { docker run --rm --entrypoint /bin/sh "$1" -c \ 'for d in $(echo "$PATH" | tr : " "); do ls -1 "$d" 2>/dev/null; done | sort -u' } npm_inventory() { docker run --rm --entrypoint /bin/sh "$1" -c \ 'npm ls -g --depth=0 --parseable 2>/dev/null | tail -n +2' \ | awk -F/ 'NF { if ($(NF-1) ~ /^@/) print $(NF-1)"/"$NF; else print $NF }' | sort -u } tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT echo "inventorying candidate $CAND …" path_inventory "$CAND" > "$tmp/cand.path" npm_inventory "$CAND" > "$tmp/cand.npm" echo "inventorying reference $REF …" path_inventory "$REF" > "$tmp/ref.path" npm_inventory "$REF" > "$tmp/ref.npm" allow_filter() { if [ -f "$ALLOW" ]; then grep -vE '^\s*(#|$)' "$ALLOW" | grep -vxF -f /dev/stdin "$1" || true else cat "$1" fi } comm -23 "$tmp/ref.path" "$tmp/cand.path" > "$tmp/removed.path" comm -23 "$tmp/ref.npm" "$tmp/cand.npm" > "$tmp/removed.npm" added_path=$(comm -13 "$tmp/ref.path" "$tmp/cand.path" | wc -l) cat "$tmp/removed.path" "$tmp/removed.npm" | sort -u > "$tmp/removed.all" allow_filter "$tmp/removed.all" > "$tmp/unexplained" || true echo "PATH: $(wc -l < "$tmp/ref.path") reference, $(wc -l < "$tmp/cand.path") candidate," \ "$(wc -l < "$tmp/removed.path") removed, $added_path added" echo "npm globals removed: $(wc -l < "$tmp/removed.npm")" if [ -s "$tmp/unexplained" ]; then echo "PARITY FAILURE — present in $REF, missing from $CAND, not allowlisted:" >&2 cat "$tmp/unexplained" >&2 echo "(fix the image, or add to $ALLOW with a comment explaining the removal)" >&2 exit 1 fi if [ -s "$tmp/removed.all" ]; then echo "--- deliberate removals (allowlisted) ---" cat "$tmp/removed.all" fi echo "parity OK"