/sync-gbrain's memory-ingest resume path adopted gbrain's import-checkpoint.json
`dir` field as the staging directory without proving it was one. A poisoned
checkpoint — `dir` = the repo root, written when an autopilot `gbrain import`
was SIGTERM'd while CWD was the repo — was adopted on the next run and then
recursively deleted by cleanupStagingDir(), destroying the user's working tree.
Root cause is a TRUST failure, not path math: code deleted a path it never
proved it owned.
Changes:
- NEW lib/staging-guard.ts: checkOwnedStagingDir() — single fail-closed proof of
"safe to recurse-delete / resume into". Requires ALL of: realpath resolves;
target is a directory; it is a direct child of $GSTACK_HOME named
.staging-ingest-*; contains no .git (tripwire); and carries a .gstack-staging
marker that is a REGULAR FILE (lstat, so a dir/symlink can't impersonate it).
- makeStagingDir(): writes the .gstack-staging marker at creation.
- cleanupStagingDir(): refuses rm -rf unless the guard passes (single deletion
chokepoint — covers the finally block AND the SIGTERM handler).
- decideResume(gstackHome = GSTACK_HOME): pure decision; returns the rejection
reason so the caller logs "refused: <why>" instead of the misleading "gone"
(the poisoned path often still exists on disk).
- gstack-memory-ingest second entry point: the binary is runnable directly, so
it independently guards GSTACK_INGEST_RESUME_DIR with checkOwnedStagingDir
rather than trusting the env it receives.
- test/regression-1611: marker minted in the resume test; added the #1802 poison
matrix + a checkOwnedStagingDir unit matrix (9→32 assertions; +23).
Compatibility: decideResume() gains an optional gstackHome param (default
GSTACK_HOME); the sole caller is unchanged. 55 tests green across the two
affected files.
Deliberate non-change (documented): pre-marker staging dirs from an interrupted
run made BEFORE this upgrade are refused (restage from scratch — seconds, no
data loss) rather than accepted via a legacy name-based path, which would
reintroduce the exact name-trust vulnerability this fix removes.
Reviewed by a 4-model design steelman (Gemini, Codex, gpt-4o, qwen3.5-27b) plus
an in-repo eng pass (Codex + an independent code-review agent on the actual
diff). All converged that the durable fix is upstream in gbrain
(staging-first checkpointing) — filed as a companion issue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>