The orchestrator shelled out to gbrain's destructive subcommands as if they were
safe. gbrain can rm-rf a user's working tree during an autopilot race (its own
bug, upstream gbrain #1526); gstack now defends itself. New lib/gbrain-guards.ts
gates the two destructive reach points, all checked immediately before the op:
- Autopilot refuse (multi-signal, affirmative-only): refuse a destructive op when
a live 'gbrain autopilot' process (primary) or a known autopilot lock file
(secondary; checked under both GBRAIN_HOME and ~/.gbrain since gbrain #1226
ignores GBRAIN_HOME) is present. No signal → proceed; inability to introspect
never bricks a normal sync.
- sources remove: routed through safeSourcesRemove → decideSourceRemove. Fail
CLOSED — refuse to remove a user-managed source (remote_url set, local_path
outside gbrain's clones) when gbrain has no --keep-storage to protect the files
(it doesn't in 0.41.x). Also fail closed when the source list can't be read.
Path containment uses realpath so a symlink can't smuggle a delete out of clones.
- sync --strategy code: decideCodeSync refuses URL-managed sources (remote_url
set) unless --allow-reclone is passed, since the walk can auto-reclone (rm-rf).
Capability detection memoizes per process keyed to gbrain's identity (no stale
persistent cache); --keep-storage can't be probed (generic help) so it defaults
unsupported → fail closed. Every guard surfaces a visible reason; autopilot/reclone
refusals fail the code stage (verdict ERR) rather than silently skipping protection.
test/gbrain-guards.test.ts covers all branches hermetically (injected rows + probe
overrides): autopilot signals, fail-closed remove, keep-storage path, reclone gate,
realpath/symlink containment. Supersedes #1736 (which guarded a nonexistent path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>