feat(sync): send and sync projects between environments - #13
Open
BarretoDiego wants to merge 2 commits into
Open
feat(sync): send and sync projects between environments#13BarretoDiego wants to merge 2 commits into
BarretoDiego wants to merge 2 commits into
Conversation
Adds client-orchestrated project sync across environments: the client reads a manifest (path/size/sha256) from the source environment and streams changed files to the destination over signed short-lived HTTP URLs, so it works over every existing connection mode (local, LAN, Tailscale, T3 Connect, SSH) without any server-to-server trails. - contracts: projectSync RPCs, capability flag, tagged errors - shared: binary framing codec for batched file transfer - server: manifest walker, signed export/import routes, path-safety guards, atomic writes, mirror deletions with empty-dir pruning - client-runtime: pure diff/batching plan + runProjectSync controller with progress and cancellation - web: SyncProjectDialog (send/sync modes, delete confirmation, include-.git toggle), command palette entries, Project Settings section; destination folder defaults from the environment's add project base directory - docs: user guide, internals architecture page, glossary terms Implemented by Claude Fable 5 subagents (Sonnet/Opus) via Claude Code.
Review pass over the project sync feature. Security and correctness: - validate framing headers on decode; a negative size decremented the signed byte budget and let a small token write unlimited bytes - enforce the signed fileCount as a record cap (413), closing an inode-exhaustion path via zero-byte records - guard symlinked ancestors on export, which could read outside the workspace root with only read scope - stop transforming paths (no trim, no backslash rewrite) so names that are legal on POSIX round-trip instead of being silently skipped - skip files whose size changed since the manifest instead of blowing the batch budget and aborting the whole sync - release export registrations after use and expire by TTL, so a busy server no longer evicts valid in-flight tokens - tolerate a non-directory ancestor, replacing it as the mirror intends - re-assert source empty directories after the deletion pass - chunk deletions to the contract's per-request cap - check the capability on the origin environment, not just the destination, and reuse the created project when a send is retried Behavior and structure: - the plan the user confirms is the plan that runs: planProjectSync is computed once for review and handed to runProjectSync, so deletions cannot drift between confirmation and execution - stream batch bodies through when the runtime supports it, buffering only as a fallback - carry an HTTP status on transfer errors instead of scraping messages - move the dialog's pure logic to client-runtime so mobile can reuse it - share the signed-token verification and default-folder helpers with their existing counterparts Docs corrected against the implementation. Implemented by Claude Fable 5 subagents (Sonnet/Opus) via Claude Code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
There is no way to move a project between environments. If you start work on your laptop and want it on a remote box (homelab, workstation, cloud server), you re-clone by hand, copy uncommitted work around with
scp, and repeat the whole dance every time the two drift apart. T3 Code already knows how to reach every one of those machines — it just had no way to move a workspace between them.Solution
Send a project to another environment, or sync an existing one, from the command palette or Project Settings → Sync.
.gitis included by default, so the destination is a full working copy. Progress is reported by bytes and files, and the run can be cancelled.How it works
The client orchestrates the transfer — it reads from the source environment and writes to the destination. There is no server-to-server channel in T3 Code, and this does not add one: the client is already the multi-environment surface, so sync works over every connection mode that already exists (local, LAN, Tailscale, T3 Connect, SSH) with no new transport, identity, or credentials between machines.
Metadata travels over four new WS RPCs; file content travels over HTTP with short-lived signed URLs (the same HMAC scheme the attachment uploads use), batched and streamed on both ends so a large project never lands in memory. Writes are atomic, paths are confined to the workspace root, and the
projectSynccapability flag keeps clients from offering the feature against a server that does not support it. Nothing here is event-sourced — sync is a filesystem operation, not a domain event.Review pass
The first implementation was reviewed adversarially, and the second commit fixes what that turned up. Worth calling out:
sizedecremented the signed byte budget — a token authorized for 1 KB could write unbounded data. Headers are now validated structurally.fileCountwas never enforced; a body of zero-byte records could exhaust inodes under a tiny token. It is now a hard cap.Also fixed: file/directory kind conflicts permanently failing import and deletion, deletions above the per-request cap failing to encode, valid in-flight export tokens being evicted under load, a retried send creating a duplicate project, and the capability being checked only on the destination.
Testing
221 tests across 16 files, typecheck clean on contracts, shared, client-runtime, server, and web. Coverage includes a real HTTP round trip (export → import, plus the 400/404/413 paths) and an end-to-end mirror test that runs the client's planner against real manifests and asserts the second pass is a no-op.
Not verified in a browser — happy to do an integrated pass in a real client if you want one before merge.
Known limitations
Documented in
docs/user/project-sync.mdanddocs/internals/project-sync.md: mobile is not supported in v1 (the capability flag gates it), a sync does not survive a tab refresh (re-running reconciles), there is no checkpoint taken on the destination before applying, and syncing while an agent is actively writing to either workspace is not recommended.Model and harness: Claude Fable 5 subagents (Sonnet and Opus) via Claude Code.