04 — SBX propagation + P1 lifecycle + plan exit matrix
04 — SBX propagation + P1 lifecycle + plan exit matrix (rows 3-11; A3-A6, A18)
SBX validation + propagation (A1/A2/A7)
STG_SANDBOX_RE='^c1_staging_[0-9]{8}_[0-9]{4}$' (_common.sh:26). stg_assert_sandbox_name
(_common.sh:64-71) enforces the regex AND an off-limits denylist (directus, incomex_metadata,
workflow, postgres, template0/1, directus_gov_test_20260602). Every script asserts the name BEFORE
any DB op: create:37, drop:14, vocab/verify/harness/readback:10. The plan passes --sandbox-id "$SBX"
to P3/P4/P5/P6 and to P2 in cleanup.
sandbox_db == sandbox_id (A2 second sense)
create:87 printf 'SANDBOX_JSON: {"sandbox_id":"%s","sandbox_db":"%s",…}' "$SBX" "$SBX" — both
fields are the same $SBX; p1a:4 creates DATABASE :"sbx" (= $SBX). Plan verifies BOTH
PARSED==CAND and PARSED_DB==CAND (exits 71/72 on mismatch).
P1 partial-create lifecycle (A4, A5)
create:42 P1_CREATED_DB=0; P1_DONE=0. create:43-58 EXIT trap p1_on_exit: if rc!=0 && P1_CREATED_DB==1 && P1_DONE!=1 → compensating stg_drop_db "$SBX"; on drop failure → exit 70
(named sandbox + manual hint). create:68 arms P1_CREATED_DB=1 immediately after CREATE; create:88
sets P1_DONE=1 only after orphan-check + ledger + SANDBOX_JSON. So a created-but-unreported sandbox
is always compensating-dropped, and created=true is authoritative only at full success. (Residue:
SIGKILL between CREATE and arming can't run any trap — recoverable: orphan is a c1_staging_% DB the
next create's reuse-block catches; not a false-PASS.)
--force disabled (A8)
create:29 --force) stg_die "FORCE_DISABLED…" 4. No name-prefix-only blind drop exists in the
create path; a pre-existing sandbox fails closed (create:60-64 REUSE_BLOCK exit 4) and routes the
operator to the governed P2 drop (which requires an active sbx_meta registry row).
Plan hard gate (A3)
plan:45 if [ "${C1_STAGING_DRY_RUN_CONFIRM:-}" != "CODEX_R3_PASS" ]; then … exit 64. The EXIT
trap is installed AFTER the gate, so a gate refusal cleans up nothing (SBX empty, no sandbox made).
Plan exit-code matrix (rows 9-10, A6, A18)
cleanup() captures rc=$? (primary) and a separate cleanup_rc. If $SBX set it runs P2 drop;
on drop failure cleanup_rc=86; on a residual c1_staging_% count != 0 (incl. unreadable → '' != '0')
cleanup_rc=87. Matrix:
rc!=0 && cleanup_rc!=0 → exit rc (both reported)
rc!=0 → exit rc (primary FAIL, cleanup ok)
cleanup_rc!=0 → exit cleanup_rc (primary ok, cleanup FAIL)
else (PRIMITIVES_OK=1) → DRY_RUN_OK ; exit 0
DRY_RUN_OK is the ONLY success marker and is emitted by the trap after P2 + count=0. set -o pipefail (plan:30) means a stage failure behind | tee still aborts the chain → A18 refuted.
R3-SELF-1 fix applied here (see file 08)
SBX (the cleanup/drop target) now stays empty until P1 returns success, and a pre-existing
candidate is refused up-front (exit 74). The plan therefore never drops a sandbox it did not create.