KB-28B1

04 — SBX propagation + P1 lifecycle + plan exit matrix

4 min read Revision 1
c1-stagingclaude-r3-self-gateplan-exit-matrixp1-lifecycle

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.

Back to Knowledge Hub knowledge/dev/laws-new/reports/c1-staging-claude-r3-hard-self-gate/04-sbx-and-plan-exit-review.md