Files
stack/packages/mosaic/framework/tools/quality/scripts/test-upgrade-manifest-guard.sh
Hermes Agent 0a5e703a70
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
test(mosaic): gate #797 ledger upgrade-survival + harden manifest parity (#791)
Fold the Mos-elevated #797 Runtime-Ledger survival sentinel into #791 PR1 and
harden the .txt-format parity test per the accepted-format conditions.

- framework-manifest.txt: annotate the existing fleet/run/** operator carve-out
  to name the #797 ledger (fleet/run/sessions/) so it reads as load-bearing.
  The glob already matches the #797 spec exactly — no location divergence.
- HARD GATE (test-upgrade-manifest-guard.sh): seed a populated ledger
  (events.ndjson journal + ledger.json projection, 0600 under 0700) as an
  operator sentinel; assert byte-identical + mtime-unchanged + dir-perms
  unchanged after a keep-mode upgrade. Relabel the prune check as the explicit
  negative control. 48 -> 58 checks.
- Parity (manifest-parity.spec.ts): add format-edge fixtures driven through
  BOTH resolvers via MANIFEST_FILE — comments/blanks/whitespace, duplicate and
  overlapping globs (deny-wins), section/glob-ordering independence, and an
  explicit UNKNOWN->operator negative probe; add ledger probe paths.
- manifest.spec.ts: isolate the carve-out's load-bearing value with a resolver
  red->green — under a hypothetical fleet/** framework glob, the ledger is
  pruned WITHOUT the fleet/run/** carve-out and protected WITH it (deny-wins).

Gates: typecheck, lint, format:check green; mosaic vitest 1069 passed;
HARD GATE 58/58; migration 21/21. Commits forward on 34e55d4a (no rebase).

Part of #791
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:11:06 -05:00

155 lines
6.8 KiB
Bash

#!/usr/bin/env bash
# test-upgrade-manifest-guard.sh — the #791 HARD GATE.
#
# Proves that a keep-mode framework upgrade (the `mosaic update` path:
# install.sh with MOSAIC_INSTALL_MODE=keep MOSAIC_SYNC_ONLY=1) touches NO path
# outside the framework-owned manifest. Every operator-owned sentinel — including
# a deliberately UNANTICIPATED one the manifest never names — must survive
# byte-identical with an unchanged mtime (not even rewritten). Framework files
# must still update, and a retired framework file inside a shipped subtree must
# still be pruned. No operator secret value may appear in installer output.
#
# Runs the whole matrix twice: once with rsync available, once forcing the
# cp-based fallback (both destructive paths in install.sh must obey the manifest).
#
# Usage: bash test-upgrade-manifest-guard.sh
set -uo pipefail
FW="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" # packages/mosaic/framework
INSTALL="$FW/install.sh"
pass=0; fail=0
chk() { if eval "$2"; then echo "$1"; pass=$((pass + 1)); else echo "$1"; fail=$((fail + 1)); fi; }
SECRET='SUPER-SECRET-TOKEN-do-not-log-3f9a'
# Seed a throwaway MOSAIC_HOME with an operator sentinel per ownership class.
seed_home() {
local H="$1"
mkdir -p "$H/agents" "$H/policy" "$H/memory" "$H/tools/_lib" \
"$H/fleet/agents" "$H/fleet/run/sessions" "$H/harvester" \
"$H/unknown-operator-dir" "$H/guides"
printf '# persona\n' > "$H/SOUL.md" # marks a recognized existing install → keep mode
printf 'MODEL=opus\n' > "$H/agents/coder0.conf"
printf '# operator policy\n' > "$H/policy/custom.md"
printf '# soul overlay\n' > "$H/SOUL.local.md"
printf '# operator memory\n' > "$H/memory/note.md"
printf 'TOKEN=%s\n' "$SECRET" > "$H/tools/_lib/credentials.json"
printf 'MOSAIC_AGENT_NAME=coder0\n' > "$H/fleet/agents/coder0.env"
printf 'version: 2\nagents:\n - name: coder0\n' > "$H/fleet/roster.yaml"
printf '# harvester SOP\n' > "$H/harvester/sop.md"
printf 'operator data the manifest never anticipated\n' > "$H/unknown-operator-dir/x"
printf 'version: 1\nagents:\n - name: mine\n' > "$H/fleet/my-fleet.yaml"
# #797 Runtime Session Ledger (Mos-elevated to a #791 PR1 merge-blocker): a
# populated ledger under fleet/run/sessions/ must survive the upgrade — a
# runtime ledger an upgrade rsync can wipe is worthless. Seed it exactly as
# #797 writes it: a non-empty append journal + a non-empty compacted
# projection, files 0600 under a 0700 dir.
printf '%s\n%s\n%s\n' \
'{"seq":1,"kind":"session.spawn","node":"sess-42","generation":7}' \
'{"seq":2,"kind":"lease.grant","node":"sess-42","lease":"web1"}' \
'{"seq":3,"kind":"dispatch.create","from":"sess-42","to":"disp-9"}' \
> "$H/fleet/run/sessions/events.ndjson"
printf '%s\n' \
'{"generation":7,"nodes":[{"id":"sess-42","kind":"session"}],"edges":[{"from":"sess-42","to":"disp-9","kind":"dispatch"}]}' \
> "$H/fleet/run/sessions/ledger.json"
chmod 0700 "$H/fleet/run" "$H/fleet/run/sessions"
chmod 0600 "$H/fleet/run/sessions/events.ndjson" "$H/fleet/run/sessions/ledger.json"
# A retired framework file inside a shipped subtree (absent from source) — must be pruned.
printf '# retired guide\n' > "$H/guides/RETIRED-OLD-GUIDE.md"
echo 3 > "$H/.framework-version"
}
OPERATOR_SENTINELS=(
"agents/coder0.conf"
"policy/custom.md"
"SOUL.local.md"
"memory/note.md"
"tools/_lib/credentials.json"
"fleet/agents/coder0.env"
"fleet/roster.yaml"
"harvester/sop.md"
"unknown-operator-dir/x"
"fleet/my-fleet.yaml"
# #797 Runtime Session Ledger — populated journal + projection must survive.
"fleet/run/sessions/events.ndjson"
"fleet/run/sessions/ledger.json"
)
run_matrix() {
local label="$1"; shift # extra env / PATH override applied to the run
local H E OUT rel before_hash after_hash before_mt after_mt
H=$(mktemp -d); E=$(mktemp -d); OUT=$(mktemp)
seed_home "$H"
# Snapshot hash + mtime of every operator sentinel before the upgrade.
for rel in "${OPERATOR_SENTINELS[@]}"; do
sha256sum "$H/$rel" | awk '{print $1}' > "$E/$(echo "$rel" | tr / _).hash"
stat -c %Y "$H/$rel" > "$E/$(echo "$rel" | tr / _).mt"
done
# Snapshot the ledger directory permission bits (#797 assert: perms unchanged).
local before_dirperm after_dirperm
before_dirperm=$(stat -c %a "$H/fleet/run/sessions")
# The upgrade under test (keep + sync-only = the `mosaic update` reseed path).
MOSAIC_HOME="$H" MOSAIC_INSTALL_MODE=keep MOSAIC_SYNC_ONLY=1 "$@" bash "$INSTALL" >"$OUT" 2>&1
# HARD GATE: every operator sentinel survives byte-identical AND mtime-unchanged.
for rel in "${OPERATOR_SENTINELS[@]}"; do
before_hash=$(cat "$E/$(echo "$rel" | tr / _).hash")
before_mt=$(cat "$E/$(echo "$rel" | tr / _).mt")
after_hash=$(sha256sum "$H/$rel" 2>/dev/null | awk '{print $1}')
after_mt=$(stat -c %Y "$H/$rel" 2>/dev/null || echo MISSING)
chk "[$label] operator sentinel survives byte-identical: $rel" \
"[ -n '$after_hash' ] && [ '$before_hash' = '$after_hash' ]"
chk "[$label] operator sentinel not rewritten (mtime unchanged): $rel" \
"[ '$before_mt' = '$after_mt' ]"
done
# #797 assert 7: the ledger directory's permission bits are unchanged.
after_dirperm=$(stat -c %a "$H/fleet/run/sessions" 2>/dev/null || echo MISSING)
chk "[$label] ledger dir perms unchanged (#797): $before_dirperm" \
"[ '$before_dirperm' = '$after_dirperm' ]"
# Positive controls / negative controls — prove the test discriminates: the
# upgrade DOES write and prune framework-owned paths, so the operator sentinels
# (incl. the #797 ledger) survive because of the manifest, not because the
# upgrade is a no-op.
chk "[$label] positive control: framework file present after upgrade (guides synced)" \
"[ -f '$H/guides/E2E-DELIVERY.md' ]"
chk "[$label] negative control: retired framework file inside a subtree IS pruned" \
"[ ! -f '$H/guides/RETIRED-OLD-GUIDE.md' ]"
chk "[$label] manifest itself is installed" "[ -f '$H/framework-manifest.txt' ]"
# Secret-safety: the operator secret value never appears in installer output.
chk "[$label] operator secret value absent from installer stdout/stderr" \
"! grep -q '$SECRET' '$OUT'"
rm -rf "$H" "$E" "$OUT"
}
echo "#791 upgrade manifest guard (HARD GATE):"
# 1) rsync path (if available on this host).
if command -v rsync >/dev/null 2>&1; then
run_matrix "rsync"
else
echo " · rsync not installed — skipping rsync-path matrix"
fi
# 2) cp fallback path — hide rsync behind a scratch PATH so install.sh takes the
# find/rm+cp branch. Provide the coreutils the fallback needs.
FBIN=$(mktemp -d)
for t in bash cp find mktemp rm mkdir chmod cmp sed grep cat dirname basename stat sha256sum awk tr; do
p=$(command -v "$t" 2>/dev/null) && ln -s "$p" "$FBIN/$t"
done
run_matrix "cp-fallback" env "PATH=$FBIN"
rm -rf "$FBIN"
echo
echo "RESULT: $pass passed, $fail failed"
[ "$fail" -eq 0 ]