Skip to content

feat(hub): local-folder / cloud-drive-folder carrier completes AD-1#106

Merged
Reederey87 merged 1 commit into
mainfrom
feat/folder-carrier
Jul 5, 2026
Merged

feat(hub): local-folder / cloud-drive-folder carrier completes AD-1#106
Reederey87 merged 1 commit into
mainfrom
feat/folder-carrier

Conversation

@Reederey87

@Reederey87 Reederey87 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • New folder:<abs-path> hub carrier (internal/hub/folder.go): FolderHub composes the proven R2Hub semantics over the existing fsObjectStore rooted directly in a shared directory — a cloud-drive folder or network mount is the replication transport, so every dssync.Hub op is lock → delegate → unlock (no fetch/commit/push loop). This is the last AD-1 slice: the zero-infrastructure hub backend direction is now COMPLETE.
  • Extracted the cross-process lock (in-process mutex + O_EXCL lock file + heartbeat + stale-TTL break) from GitCarrierHub into a shared fsLock helper used by both carriers — behavior-preserving refactor, verified by the existing gitcarrier suite and an independent Codex line-level check.
  • Lock file + observation floor live in the local home cache (~/.devstrap/hub-folder/<hash>/), never in the replicated folder — replicating lock churn would cause false contention and conflicted copies. Cross-device CAS is best-effort by design (no cross-writer linearization point on a cloud drive); documented in spec/15 alongside the existing sweep-lock residuals.
  • CLI: folder: case in selectBackendHub/hubConfigured, parseFolderURI validation (absolute path, no ? params), doctor workspace-id-mismatch warning parity.

Review round (dual review)

  • Codex P2 accepted: guard() now re-resolves the root under the lock (use-time root revalidation) and refuses when it no longer denotes the construction-time directory — safePath only Lstats components below the root, so a shared root later swapped for a symlink would otherwise redirect every read/write outside the registered folder. Pinned by TestFolderHubRefusesReplacedRoot.
  • Codex P3 accepted: TestFolderHubWorkspacesAreIsolated pins two workspace ids sharing one folder (isolation rides the workspaces/<ws>/ key prefix).
  • Codex verified clean: fsLock extraction fidelity, full 21-method guard coverage, cache-vs-shared placement, CLI dispatch ordering.

Test plan

  • TestFolderHubConformance reuses the full shared hub contract (round-trip, ack plane, retention/snapshot, sweep lock) the git carrier is proven against.
  • Cross-process-lock CAS one-winner, sweep-lock one-holder, two-device convergence + compaction, symlinked-root resolution, constructor rejections.
  • sync_folder_hub.txtar e2e: two device homes share one folder hub — founder seeds, joiner enrolls, converge + materialize.
  • Gates: gofmt, golangci-lint (0 issues), spec-drift, go test -race ./... all green post-rebase.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for a new shared-folder sync hub option.
    • Enabled end-to-end syncing between two devices through a common local folder.
  • Bug Fixes

    • Improved hub detection so folder-based setups are recognized correctly.
    • Added safeguards for invalid folder paths and configuration issues.
  • Tests

    • Expanded automated coverage for folder-based syncing, locking, and workspace isolation.
    • Added an end-to-end test that verifies project materialization on a second device.

Add FolderHub, the `folder:<abs-path>` hub carrier: a plain shared
directory (a Dropbox/iCloud/Drive folder or network mount) carries the
same zero-knowledge object set as R2 and the git carrier. It composes the
proven R2Hub semantics over the existing fsObjectStore rooted directly in
the shared folder, so there is no fetch/commit/push loop — the drive is
the replication transport and each Hub method is lock -> delegate ->
unlock.

The cross-process lock (in-process mutex + O_EXCL lock file + heartbeat +
stale-TTL break) is extracted from GitCarrierHub into a shared package-
local `fsLock` helper used by both carriers (pure refactor; the git side
is behaviorally unchanged). The lock file and per-clone observation floor
live in the local home cache under ~/.devstrap/hub-folder/<hash>/, never
in the replicated folder; only ciphertext objects and their timestamp
sidecars live in the shared folder. Cross-device conditional writes are
best-effort by nature (a cloud drive has no cross-writer linearization
point), documented as an advisory-cooperation residual.

CLI: selectBackendHub/hubConfigured gain a `folder:` case (hub id
`folder:<workspace_id>`), doctor's isRemoteHubID classifies it as remote,
and `hub init` stays git-only. Full conformance + constructor-rejection +
cross-process-CAS + two-device tests, a folder:-scheme CLI test, and a
sync_folder_hub.txtar e2e. Specs 01/03/13/14/15/16/18 updated; AD-1 is
now complete. docs/audits/README.md unchanged (AD-1 is a spec/14
direction item; ledger counts unaffected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Reederey87 Reederey87 enabled auto-merge (squash) July 5, 2026 10:40
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new folder: hub carrier backend (FolderHub) implementing dssync.Hub over a shared filesystem directory, refactors cross-process locking into a shared fsLock reused by the existing git carrier, wires CLI hub selection/validation and doctor detection for the new scheme, and adds corresponding tests and spec documentation.

Changes

Folder hub carrier

Layer / File(s) Summary
FolderHub implementation and shared fsLock
internal/hub/folder.go
Adds FolderHub implementing dssync.Hub, with constructor, guarded delegation for event/blob/retention/snapshot/ack/sweep-lock/compaction operations, and the shared fsLock cross-process lock primitive.
GitCarrierHub lock refactor
internal/hub/gitcarrier.go, internal/hub/gitcarrier_adversarial_test.go
Renames lock timing constants to fsLock* and replaces GitCarrierHub's manual lock logic with the shared fsLock.acquire().
CLI hub selection, validation, and doctor detection
internal/cli/hub.go, internal/cli/hub_folder_test.go, internal/cli/doctor.go, internal/cli/doctor_test.go
Adds folder: URI parsing/validation, hub resolution via NewFolderHub, updated error messages, and extends isRemoteHubID to recognize folder:-prefixed hub IDs.
FolderHub conformance and regression tests
internal/hub/folder_test.go
Adds tests for conformance, constructor validation, symlink resolution, CAS conflicts, sweep-lock exclusivity, two-device convergence, root-swap safety, and workspace isolation.
End-to-end script and spec docs
cmd/devstrap/testdata/script/sync_folder_hub.txtar, spec/*
Adds a two-device convergence integration test and updates architecture, security, roadmap, test plan, and work log documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Reederey87/DevStrap#96: Both PRs extend internal/cli/hub.go with new hub URI schemes and converge on the shared cross-process fsLock abstraction.
  • Reederey87/DevStrap#100: Both PRs extend isRemoteHubID/shouldWarnWorkspaceIDMismatch in internal/cli/doctor.go with new hub-id prefixes and matching tests.
  • Reederey87/DevStrap#75: The new FolderHub implements the same ack-plane APIs (PutAck, ListAcks, DeleteAck, DeleteDeviceStream) introduced there.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the new folder-based hub carrier completing AD-1.
Description check ✅ Passed Detailed and on-topic, with summary and test coverage, though it doesn't use the template's exact Tests and Safety Checklist sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/folder-carrier

Comment @coderabbitai help to get the list of available commands.

@Reederey87 Reederey87 merged commit 0ae78ac into main Jul 5, 2026
6 of 7 checks passed
@Reederey87 Reederey87 deleted the feat/folder-carrier branch July 5, 2026 10:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/cli/hub.go (1)

168-185: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

No guard against the shared folder: path overlapping the local cache root.

cacheRoot (Home/hub-folder) is where NewFolderHub stores the per-device lock and observed.json — deliberately kept out of the shared folder per the design rationale (replicating lock churn would cause false contention/conflicted copies). Nothing here (or in the NewFolderHub constructor shown in context) rejects a user-supplied folder:<path> that is the same as, or an ancestor/descendant of, cacheRoot/opts.paths().Home. Misconfiguring the shared path to point inside the local devstrap home would violate that invariant and could leak per-device lock/observation state into the shared carrier.

Consider validating that the resolved folder: directory does not overlap with cacheRoot (or the devstrap home) before constructing the hub.

Suggested guard
 		cacheRoot := filepath.Join(filepath.Dir(opts.paths().KeyDir()), "hub-folder")
+		if rel, err := filepath.Rel(cacheRoot, path); err == nil && !strings.HasPrefix(rel, "..") {
+			return nil, "", fmt.Errorf("folder hub path %q must not be inside the devstrap cache directory %q", path, cacheRoot)
+		}
 		folderHub, err := hub.NewFolderHub(path, ws, cacheRoot)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/hub.go` around lines 168 - 185, The folder hub setup in the
folder: branch of hub.go does not prevent the shared path from overlapping the
local cache area used by NewFolderHub. Add a validation step after
parseFolderURI and before hub.NewFolderHub that resolves the requested folder
path and rejects it if it is the same as, or nested with, cacheRoot or
opts.paths().Home, returning a clear error. Use the existing folder: handling
branch, cacheRoot computation, and NewFolderHub call as the key locations to
update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spec/03_SYSTEM_ARCHITECTURE.md`:
- Around line 372-374: Clarify the root-validation behavior in FolderHub: the
current wording implies the folder root is only resolved once, but the
implementation re-validates the root path under the cross-process lock on each
operation. Update the AD-1 folder-carrier description to explicitly state that
the resolved root is checked again during each Hub method call, while keeping
the one-time EvalSymlinks resolution and constructor validation contract tied to
FolderHub and fsLock.

---

Nitpick comments:
In `@internal/cli/hub.go`:
- Around line 168-185: The folder hub setup in the folder: branch of hub.go does
not prevent the shared path from overlapping the local cache area used by
NewFolderHub. Add a validation step after parseFolderURI and before
hub.NewFolderHub that resolves the requested folder path and rejects it if it is
the same as, or nested with, cacheRoot or opts.paths().Home, returning a clear
error. Use the existing folder: handling branch, cacheRoot computation, and
NewFolderHub call as the key locations to update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d564d3be-a55c-461b-b984-fc014f7e22d0

📥 Commits

Reviewing files that changed from the base of the PR and between 5b5728d and 1062cee.

📒 Files selected for processing (16)
  • cmd/devstrap/testdata/script/sync_folder_hub.txtar
  • internal/cli/doctor.go
  • internal/cli/doctor_test.go
  • internal/cli/hub.go
  • internal/cli/hub_folder_test.go
  • internal/hub/folder.go
  • internal/hub/folder_test.go
  • internal/hub/gitcarrier.go
  • internal/hub/gitcarrier_adversarial_test.go
  • spec/01_ARCHITECTURE_DECISION.md
  • spec/03_SYSTEM_ARCHITECTURE.md
  • spec/13_CLI_DAEMON_API.md
  • spec/14_MVP_ROADMAP_AND_BACKLOG.md
  • spec/15_SECURITY_THREAT_MODEL.md
  • spec/16_TEST_PLAN.md
  • spec/18_WORK_LOG.md

Comment thread spec/03_SYSTEM_ARCHITECTURE.md
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.

1 participant