Skip to content

v0.42.33.0 fix(sources): confine sync re-clone to gbrain-owned clones; never delete a user working tree (#1881)#1960

Merged
garrytan merged 4 commits into
masterfrom
garrytan/sync-code-rmrf-fix
Jun 8, 2026
Merged

v0.42.33.0 fix(sources): confine sync re-clone to gbrain-owned clones; never delete a user working tree (#1881)#1960
garrytan merged 4 commits into
masterfrom
garrytan/sync-code-rmrf-fix

Conversation

@garrytan

@garrytan garrytan commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1881gbrain sync --strategy code deleted a user's live git working tree.

The bug: recloneIfMissing (src/core/sources-ops.ts) deleted local_path whenever a source had a remote_url and a non-healthy on-disk state, with no check that gbrain actually created the clone. A source whose local_path pointed at a user's working tree (the federated shape a remote_url + external path produces) could have its directory removed and re-cloned over.

The fix — ownership, not path-containment:

  • isOwnedClone() — gbrain re-clones/deletes only a clone it created, proven by a config.managed_clone marker (written by addSource --url) or exact normalized-path equality with defaultCloneDir(id) (back-compat for pre-marker default-location clones). Everything else is fail-closed (refused).
  • recloneIfMissing — ownership guard aborts before any filesystem op (new unmanaged_path SourceOpError); EXDEV-safe sibling-temp clone + atomic swap (old aside → new in → drop old) with best-effort restore and an error that names where the original is preserved; symlink-leaf reject before the destructive rename.
  • sync.ts validate_repo_state guards the reclone on isOwnedClone (no per-sync warning — surfacing lives in a follow-up doctor check).
  • sources restore degrades to a warning for an unowned source instead of the misleading "missing clone, try sync" hint.

Reported by @zaqwery. The ownership-invariant correction came out of a cross-model plan + diff review (Codex outside-voice).

Test Coverage

isOwnedClone(5 branches) · unownedHint(healthy/degraded) · recloneIfMissing
(entry guard, sibling-temp clone, symlink-leaf reject, aside swap, residue-free
cleanup) · addSource marker · --clone-dir owned-via-marker · sync-level refusal ·
restore CV3 — all tested with side-effect assertions (working-tree + sentinel
survival, symlink-target survival, no swap residue).

AI-assessed coverage of the changed paths: 85% (gate PASS). The 2 untested branches are a deliberately-defensive unreachable TOCTOU re-check and a hard-to-provoke rename-failed restore catch.

Pre-Landing Review

Pre-landing review: ship as-is (no issues). Claude + Codex adversarial both flagged one item (lexical resolvePath ownership + symlinked clones-ancestor). Investigated: the proposed realpath-chain fix false-positives on system symlinks (macOS /var/private/var) and doesn't address the real residual, which is DB-trust (a forged managed_clone marker), not path confusion — for an owned clone gbrain created the dir there (cloneRepo refuses a non-empty dest). Kept the leaf-symlink TOCTOU guard, improved the failed-swap message to name the preserved original, and filed the residual as a P2 TODO (CI guard that only addSource writes the marker + an on-disk ownership stamp).

Design Review

No frontend files changed — design review skipped.

Eval Results

No prompt-related files changed — evals skipped.

Plan Completion

All plan items DONE (eng-review plan + all accepted CV findings). Plan + GSTACK REVIEW REPORT at ~/.claude/plans/.

Verification Results

No dev server / web UI — CLI tool. Verified via the unit suite and the #1881 repro encoded as a regression test (federated row → throws unmanaged_path, working tree + sentinel survive).

TODOS

Filed 5 follow-ups (gbrain#1881 section): doctor misconfig check (P2), harden managed_clone against forgery + on-disk stamp (P2), .gbrain-reclone-* orphan sweep (P3), --clone-dir policy / dormant clone_dir_outside_gbrain (P3), CLI sources remove clone-storage leak (P3).

Documentation

  • docs/architecture/KEY_FILES.md: added the missing src/core/sources-ops.ts entry capturing the v0.42.33.0 (gbrain sync --strategy code deleted the working directory (federated source with local_path, no --url) #1881) reclone-ownership invariant — gbrain only deletes/re-clones a clone it created (isOwnedClone: config.managed_clone marker or defaultCloneDir exact-match), never a user working tree. Covers the fail-closed unmanaged_path SourceOpError, EXDEV-safe sibling-temp swap, TOCTOU + symlink-leaf guards, shared unownedHint, and the read-only gbrain sources restore path.

Test plan

  • Affected suites: 72 pass / 0 fail (sources-ops, sources-resync-recovery, sources-mcp)
  • Full unit suite: 10073 pass; typecheck clean; jsonb guard pass
  • 1 pre-existing failure (think-gateway-adapter / conversation-parser/llm-*) — not in this branch's files; environment-dependent (a local AI-gateway credential makes the adapter build a client with ANTHROPIC_API_KEY unset); passes in clean CI.

🤖 Generated with Claude Code

garrytan and others added 4 commits June 7, 2026 18:30
…ete a user working tree (#1881)

recloneIfMissing deleted local_path whenever a source had a remote_url and a
non-healthy on-disk state, with no check that gbrain actually created the clone.
A source whose local_path was a user's live working tree (remote_url set, no
gbrain-created clone) could have its directory removed and re-cloned over.

- isOwnedClone(): ownership, not path-containment. True only for a config
  .managed_clone marker (written by addSource --url) or exact normalized-path
  equality with defaultCloneDir(id) (back-compat for pre-marker default clones).
- recloneIfMissing: ownership guard aborts before ANY filesystem op; EXDEV-safe
  sibling-temp clone + atomic swap (old aside -> new in -> drop old) with
  best-effort restore + a message naming where the original is preserved;
  symlink-leaf reject before the destructive rename.
- sync.ts validate_repo_state guards reclone on isOwnedClone (no per-sync warn).
- sources restore degrades to a warning for an unowned source instead of the
  misleading "missing clone, try sync" hint.

Tests: #1881 regression (tree survives), isOwnedClone matrix, symlink reject,
EXDEV swap residue-free, --clone-dir owned-via-marker, restore CV3, unownedHint
healthy/degraded, sync-level refusal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1881)

Add the missing src/core/sources-ops.ts entry to KEY_FILES.md capturing the
must-never-violate reclone-ownership guarantee: gbrain only deletes/re-clones a
clone it created (isOwnedClone), never a user working tree. Covers managed_clone
marker, defaultCloneDir back-compat, EXDEV-safe swap, TOCTOU + symlink-leaf
guards, unmanaged_path SourceOpError, and the read-only sources restore path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rmrf-fix

# Conflicts:
#	CHANGELOG.md
#	VERSION
#	package.json
@garrytan garrytan merged commit b31de66 into master Jun 8, 2026
21 checks passed
mgunnin added a commit to mgunnin/gbrain that referenced this pull request Jun 9, 2026
* upstream/master:
  v0.42.37.0 fix(security,ingest): source-isolation grant enforcement + non-string frontmatter guard + papercuts (garrytan#1999)
  v0.42.36.0 fix(sync): resumable, durable, single-flight sync — converges under pool exhaustion + repeated kills (garrytan#1794) (garrytan#1980)
  v0.42.35.0 fix(sync): recover from unreachable last_commit instead of full-walking forever (garrytan#1970) (garrytan#1975)
  v0.42.34.0 feat(search): typed-edge relational retrieval — relationship questions get relationship answers (garrytan#1959)
  docs(designs): add COMMUNITY_IDEAS ledger from open-PR backlog triage (garrytan#1969)
  v0.42.33.0 fix(sources): confine sync re-clone to gbrain-owned clones; never delete a user working tree (garrytan#1881) (garrytan#1960)

# Conflicts:
#	src/core/operations.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gbrain sync --strategy code deleted the working directory (federated source with local_path, no --url)

1 participant