git_seed: git.seed.export bundles a seed's changes back (git-seed-export-v1)

TE
16h ago
+1099-162

Piece 6 (machine side) of the Cloud project seed: bring a seeded Cloud folder's changes back to the Mac as a diff, not a folder copy.

Leo's words:

Your project folder is copied up.
we'll want to deal with the edge cases for this too

Spec: leo-workspace design/cloud.md, "Getting the project onto the machine", rule 6 ("Bringing changes back is a diff too").

What it adds

  • A new op, git.seed.export {folder, since_work, since_head, bundle}, under a new capability, git-seed-export-v1. It is not added to git-seed-v1 because a machine advertising only git-seed-v1 would deny the new op at its remote gate. The Mac needs a capability it can ask for first (machine_gate::outdated). The link's FsGate admits the op through git_seed::frame_command. build_capabilities leaves the new capability out, as it already does for git-seed-v1, because whether a host serves it depends on the host.
  • A job, not synchronous. Making the INDEX and WORK commits runs add -A over the whole working tree, which can take a while on a large repository. So the op answers {job} at once, like git.seed.start, and git.seed.status reads it. Once the job is done its status adds {head_ref, head, index, work, stash, unchanged, bundle_bytes, left_out}.
  • The machine makes the same INDEX and WORK commits the Mac's seed plan makes: the same trees, the fixed author cmux seed <seed@cmux.invalid>, the fixed date 946684800 +0000, and the same messages. The shared code lives in git_seed/snapshot.rs, which delta seeds' check_delta now uses too. The "untracked secret" rule moved to cmux_tui_core::git_seed::secrets, and the Mac's link/seed/secrets.rs re-exports it. Both sides leave out the same .env* files, keys and nested repositories, so the same state gives the same commit, and an unchanged machine answers unchanged with no bundle written.
  • The bundle carries refs/cmux-back/{head,index,work,stash?} with the prerequisites ^since_work and ^since_head. The seed's trees are excluded too, so it holds only the files that changed. It is written under the fs roots through fs-v1's atomic PendingWrite (create-new, 0600, free space checked), outside the seed's folder. The commits are also recorded in the folder as refs/cmux-back/*, which keeps them out of gc, and a later delta seed may build on an export: check_delta accepts since equal to refs/cmux-seed/work or to refs/cmux-back/work.
  • It keeps the same sandbox, path rules and hooks-off git settings as git.seed.start. Ignored files (node_modules, build output) never go into the bundle.
  • CMUX_TEST_GIT_SEED_REMOTES is a test seam for debug builds only: it reads https://github.com/O/N.git as file://<dir>/O/N.git so a journey test can seed a real daemon without the network. Release builds never read it.

The Mac side, cmux link seed pull, is the follow-up PR stacked on this one.

Tests

cargo test -p cmux-tui-core --lib git_seed on the fleet (cmux-ci test). New tests in git_seed/tests/export.rs:

  • An export of an unchanged seed is the Mac's exact WORK commit, and no bundle is written.
  • A commit, an edit, an add, a delete and a staged change all come back. Ignored files, .env and a nested repository stay out. The new commits form a WORK, INDEX, HEAD chain, the same state exports to the same commits, and the bundle is 0600.
  • A delta seed builds on an export.
  • Refused cases: a bundle path inside the folder, a folder that is not a seed, a bad sha, and a since the folder lacks.

Clippy with -D warnings is clean in the files this PR touches. The remaining clippy errors on the base are in server/agent_session_folders.rs and cli/cloud_repo.rs and are not from this PR.

Agent session: agent session

🤖 Generated with Claude Code


Summary by cubic

Adds the machine side of the Cloud project seed's rule 6: a seeded folder's changes can now be bundled back to the Mac as a diff (git.seed.export), not a folder copy.

  • Runs under a new capability, git-seed-export-v1, kept separate from git-seed-v1 so machines advertising only the older capability deny it at their remote gate.
  • Exports are jobs read through git.seed.status; an unchanged seed answers unchanged and writes no bundle, and a retried export whose answer was lost returns the same job even once its bundle exists.
  • Only a seed's folder exports; repositories without the seed record are refused, and the bundle path must be outside the folder.
  • The machine makes the same INDEX and WORK commits the Mac's seed plan makes (shared snapshot code, fixed identity and date, shared messages), so the same state always yields the same commits.
  • The bundle carries refs/cmux-back/{head,index,work,stash?} with ^since_work and ^since_head, so only changed files travel; it is written atomically with 0600 permissions.
  • Comparison is against the newest record (refs/cmux-back preferred over refs/cmux-seed): a stash-only change still exports, a dropped stash sends nothing, and since need not be the newest record since the Mac can pull again from its own base.
  • The commits are recorded as refs/cmux-back/* in the folder, keeping them out of gc and letting a later delta seed build on an export.
  • The untracked-secret rule moved to cmux_tui_core::git_seed::secrets so both sides exclude the same .env* files, keys, and nested repositories, including secrets with non-UTF-8 names; an unreadable registry file now counts as holding a token.
  • Work inside a dirty submodule is refused (seed.submodule_local_work), never exported as unchanged.
  • CMUX_TEST_GIT_SEED_REMOTES is a debug-only seam for journey tests; release builds never read it.

The Mac side, cmux link seed pull, is the follow-up PR stacked on this one.