Repository navigation
git_seed: git.seed.export bundles a seed's changes back (git-seed-export-v1) - #30633
Conversation
…ort-v1) Piece 6, machine side, of the Cloud project seed (design/cloud.md, "Getting the project onto the machine", rule 6). The machine makes INDEX and WORK commits of a seeded folder exactly as the Mac's seed plan does (shared snapshot, fixed identity and date, secrets and nested repositories left out by one shared rule), and writes one bundle of refs/cmux-back/* with ^since_work ^since_head through fs-v1's atomic write. A job read with git.seed.status; unchanged writes nothing. A delta seed may build on an export. Debug-only CMUX_TEST_GIT_SEED_REMOTES seam for journey tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Shipped in:
|
|
Owned by an agent session · session |
There was a problem hiding this comment.
10 issues found across 14 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/place.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/place.rs:209">
P2: A bundle path under a missing intermediate directory passes `output()` but fails late in `write_output`, after the full `git bundle create`. The error text is also misleading ("outside the folders this machine serves"). Either reject the missing parent up front in `output()`, or give `NotFound` its own message.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui-core/src/git_seed/place.rs:219">
P2: `output()` validates only the original path; `PendingWrite::begin` follows a final symlink, so a symlink created before the worker writes can redirect the bundle into the seed folder. Use a no-follow target or revalidate the resolved destination at write time.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/git.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/git.rs:310">
P2: `run_env` is public and applies caller `env` after the sandbox strips in `command()`, so it can re-set `GIT_DIR`, `GIT_SSH_COMMAND`, `GIT_ASKPASS`, or `GIT_CONFIG_GLOBAL`. Either restrict it to the seed module, or reject names that `command()` removes before applying them.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs:13">
P3: The `.env` check is case-sensitive, while the SSH and key-extension checks lowercase the name. A file like `.ENV` would not be excluded. Lowercase the name before the prefix check.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs:22">
P2: Java keystores are not excluded. Add `.jks` and `.keystore` to the suffix list so they stay out of the seed, matching the checkpoint rule.</violation>
<violation number="3" location="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs:25">
P2: The secret list misses credential files that the checkpoint rule in the same crate excludes: `.pgpass`, `.git-credentials`, `.htpasswd`, and `credentials`. Since the export now returns machine-made files to the Mac, these would come back in the bundle. Add them to the name check.</violation>
<violation number="4" location="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs:34">
P2: This unbounded read follows an untracked `.npmrc`/`.pypirc` symlink; a link to `/dev/zero` can stall export while consuming memory. Reject symlinks and non-regular files, and cap the bytes read.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/tests.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/tests.rs:79">
P3: `seed_commit` inherits `GIT_DIR`, so `git -C dir` can commit into the caller-selected repository instead of this fixture. Clear `GIT_DIR` and `GIT_INDEX_FILE` as `git_env` does.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed.rs:113">
P3: `frame_command` now accepts `git.seed.export`, so `FsGate` admits it too. Update the gate documentation in `fs_wire.rs` and `remote_entry.rs`, which still says the gate admits only two `git-seed-v1` ops.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/job.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/job.rs:281">
P2: This accepts any nested clone as a seed; unlike `git.seed.start`, export skips `place::not_nested`, so it can bundle a nested repository that normal snapshots deliberately exclude. Apply the same nesting guard before scheduling the export.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| SeedRefusal { code: "seed.disk_full", message: "the disk is full".into() } | ||
| } | ||
| FsError::Exists => SeedRefusal::bad_request("something is already at the bundle path"), | ||
| FsError::PermissionDenied | FsError::NotFound => outside(), |
There was a problem hiding this comment.
P2: A bundle path under a missing intermediate directory passes output() but fails late in write_output, after the full git bundle create. The error text is also misleading ("outside the folders this machine serves"). Either reject the missing parent up front in output(), or give NotFound its own message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/place.rs, line 209:
<comment>A bundle path under a missing intermediate directory passes `output()` but fails late in `write_output`, after the full `git bundle create`. The error text is also misleading ("outside the folders this machine serves"). Either reject the missing parent up front in `output()`, or give `NotFound` its own message.</comment>
<file context>
@@ -166,6 +166,74 @@ pub fn delete_upload(roots: &Roots, path: &str) {
+ SeedRefusal { code: "seed.disk_full", message: "the disk is full".into() }
+ }
+ FsError::Exists => SeedRefusal::bad_request("something is already at the bundle path"),
+ FsError::PermissionDenied | FsError::NotFound => outside(),
+ other => SeedRefusal::bad_request(format!("cannot write the bundle: {}", other.message())),
+ };
</file context>
| FsError::PermissionDenied | FsError::NotFound => outside(), | |
| FsError::PermissionDenied => outside(), | |
| FsError::NotFound => SeedRefusal::bad_request("the bundle's folder does not exist"), |
| } | ||
|
|
||
| /// [`Self::run`] with `env` set for git (an index file, a fixed author). | ||
| pub fn run_env( |
There was a problem hiding this comment.
P2: run_env is public and applies caller env after the sandbox strips in command(), so it can re-set GIT_DIR, GIT_SSH_COMMAND, GIT_ASKPASS, or GIT_CONFIG_GLOBAL. Either restrict it to the seed module, or reject names that command() removes before applying them.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/git.rs, line 310:
<comment>`run_env` is public and applies caller `env` after the sandbox strips in `command()`, so it can re-set `GIT_DIR`, `GIT_SSH_COMMAND`, `GIT_ASKPASS`, or `GIT_CONFIG_GLOBAL`. Either restrict it to the seed module, or reject names that `command()` removes before applying them.</comment>
<file context>
@@ -300,11 +300,25 @@ impl Git {
+ }
+
+ /// [`Self::run`] with `env` set for git (an index file, a fixed author).
+ pub fn run_env(
+ &self,
+ cwd: Option<&Path>,
</file context>
| pub fn run_env( | |
| pub(super) fn run_env( |
| if name == ".netrc" { | ||
| return Some("credentials file (.netrc)"); | ||
| } |
There was a problem hiding this comment.
P2: The secret list misses credential files that the checkpoint rule in the same crate excludes: .pgpass, .git-credentials, .htpasswd, and credentials. Since the export now returns machine-made files to the Mac, these would come back in the bundle. Add them to the name check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs, line 25:
<comment>The secret list misses credential files that the checkpoint rule in the same crate excludes: `.pgpass`, `.git-credentials`, `.htpasswd`, and `credentials`. Since the export now returns machine-made files to the Mac, these would come back in the bundle. Add them to the name check.</comment>
<file context>
@@ -0,0 +1,82 @@
+ if [".pem", ".p12", ".pfx", ".key"].iter().any(|ext| lower.ends_with(ext)) {
+ return Some("key or certificate file");
+ }
+ if name == ".netrc" {
+ return Some("credentials file (.netrc)");
+ }
</file context>
| if name == ".netrc" { | |
| return Some("credentials file (.netrc)"); | |
| } | |
| if [".netrc", ".pgpass", ".git-credentials", ".htpasswd", "credentials"].contains(&name) { | |
| return Some("credentials file"); | |
| } |
| { | ||
| return Some("SSH private key"); | ||
| } | ||
| if [".pem", ".p12", ".pfx", ".key"].iter().any(|ext| lower.ends_with(ext)) { |
There was a problem hiding this comment.
P2: Java keystores are not excluded. Add .jks and .keystore to the suffix list so they stay out of the seed, matching the checkpoint rule.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs, line 22:
<comment>Java keystores are not excluded. Add `.jks` and `.keystore` to the suffix list so they stay out of the seed, matching the checkpoint rule.</comment>
<file context>
@@ -0,0 +1,82 @@
+ {
+ return Some("SSH private key");
+ }
+ if [".pem", ".p12", ".pfx", ".key"].iter().any(|ext| lower.ends_with(ext)) {
+ return Some("key or certificate file");
+ }
</file context>
| if [".pem", ".p12", ".pfx", ".key"].iter().any(|ext| lower.ends_with(ext)) { | |
| if [".pem", ".p12", ".pfx", ".key", ".jks", ".keystore"].iter().any(|ext| lower.ends_with(ext)) { |
| fn export(&self, params: &Value) -> Result<Value, SeedRefusal> { | ||
| let params = ExportParams::parse(params)?; | ||
| let folder = place::folder(&self.roots, ¶ms.folder)?; | ||
| if !std::fs::symlink_metadata(folder.join(".git")).is_ok_and(|meta| meta.is_dir()) { |
There was a problem hiding this comment.
P2: This accepts any nested clone as a seed; unlike git.seed.start, export skips place::not_nested, so it can bundle a nested repository that normal snapshots deliberately exclude. Apply the same nesting guard before scheduling the export.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/job.rs, line 281:
<comment>This accepts any nested clone as a seed; unlike `git.seed.start`, export skips `place::not_nested`, so it can bundle a nested repository that normal snapshots deliberately exclude. Apply the same nesting guard before scheduling the export.</comment>
<file context>
@@ -266,6 +272,79 @@ impl GitSeedService {
+ fn export(&self, params: &Value) -> Result<Value, SeedRefusal> {
+ let params = ExportParams::parse(params)?;
+ let folder = place::folder(&self.roots, ¶ms.folder)?;
+ if !std::fs::symlink_metadata(folder.join(".git")).is_ok_and(|meta| meta.is_dir()) {
+ return Err(SeedRefusal {
+ code: "seed.since_mismatch",
</file context>
| .map_err(|error| SeedRefusal::bad_request(format!("cannot read the bundle: {error}")))? | ||
| .len(); | ||
| let mut pending = | ||
| PendingWrite::begin(roots, path, WriteMode::Create, Some(size)).map_err(refused)?; |
There was a problem hiding this comment.
P2: output() validates only the original path; PendingWrite::begin follows a final symlink, so a symlink created before the worker writes can redirect the bundle into the seed folder. Use a no-follow target or revalidate the resolved destination at write time.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/place.rs, line 219:
<comment>`output()` validates only the original path; `PendingWrite::begin` follows a final symlink, so a symlink created before the worker writes can redirect the bundle into the seed folder. Use a no-follow target or revalidate the resolved destination at write time.</comment>
<file context>
@@ -166,6 +166,74 @@ pub fn delete_upload(roots: &Roots, path: &str) {
+ .map_err(|error| SeedRefusal::bad_request(format!("cannot read the bundle: {error}")))?
+ .len();
+ let mut pending =
+ PendingWrite::begin(roots, path, WriteMode::Create, Some(size)).map_err(refused)?;
+ pending.set_permissions(0o600).map_err(refused)?;
+ let mut chunk = vec![0u8; 1 << 20];
</file context>
| /// or None when it goes. | ||
| pub fn reason(top: &Path, relative: &str) -> Option<&'static str> { | ||
| let name = relative.rsplit('/').next().unwrap_or(relative); | ||
| if name.starts_with(".env") { |
There was a problem hiding this comment.
P3: The .env check is case-sensitive, while the SSH and key-extension checks lowercase the name. A file like .ENV would not be excluded. Lowercase the name before the prefix check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs, line 13:
<comment>The `.env` check is case-sensitive, while the SSH and key-extension checks lowercase the name. A file like `.ENV` would not be excluded. Lowercase the name before the prefix check.</comment>
<file context>
@@ -0,0 +1,82 @@
+/// or None when it goes.
+pub fn reason(top: &Path, relative: &str) -> Option<&'static str> {
+ let name = relative.rsplit('/').next().unwrap_or(relative);
+ if name.starts_with(".env") {
+ return Some(".env file: it may hold secrets, so it is shared only on purpose");
+ }
</file context>
| if name.starts_with(".env") { | |
| if name.to_ascii_lowercase().starts_with(".env") { |
| .arg(dir) | ||
| .args(["commit-tree", "--no-gpg-sign", "-p", parent, "-m", message, tree]) | ||
| .env("GIT_CONFIG_GLOBAL", "/dev/null") | ||
| .env("GIT_CONFIG_NOSYSTEM", "1") |
There was a problem hiding this comment.
P3: seed_commit inherits GIT_DIR, so git -C dir can commit into the caller-selected repository instead of this fixture. Clear GIT_DIR and GIT_INDEX_FILE as git_env does.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/tests.rs, line 79:
<comment>`seed_commit` inherits `GIT_DIR`, so `git -C dir` can commit into the caller-selected repository instead of this fixture. Clear `GIT_DIR` and `GIT_INDEX_FILE` as `git_env` does.</comment>
<file context>
@@ -67,6 +67,28 @@ fn git_env(dir: &Path, args: &[&str], env: &[(&str, &Path)]) -> String {
+ .arg(dir)
+ .args(["commit-tree", "--no-gpg-sign", "-p", parent, "-m", message, tree])
+ .env("GIT_CONFIG_GLOBAL", "/dev/null")
+ .env("GIT_CONFIG_NOSYSTEM", "1")
+ .env("GIT_AUTHOR_NAME", "cmux seed")
+ .env("GIT_AUTHOR_EMAIL", "seed@cmux.invalid")
</file context>
| .env("GIT_CONFIG_NOSYSTEM", "1") | |
| .env("GIT_CONFIG_NOSYSTEM", "1") | |
| .env_remove("GIT_DIR") | |
| .env_remove("GIT_INDEX_FILE") |
| } | ||
| let cmd = object.get("cmd")?.as_str()?; | ||
| GIT_SEED_COMMANDS.iter().copied().find(|known| *known == cmd) | ||
| GIT_SEED_COMMANDS.iter().chain(&GIT_SEED_EXPORT_COMMANDS).copied().find(|known| *known == cmd) |
There was a problem hiding this comment.
P3: frame_command now accepts git.seed.export, so FsGate admits it too. Update the gate documentation in fs_wire.rs and remote_entry.rs, which still says the gate admits only two git-seed-v1 ops.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed.rs, line 113:
<comment>`frame_command` now accepts `git.seed.export`, so `FsGate` admits it too. Update the gate documentation in `fs_wire.rs` and `remote_entry.rs`, which still says the gate admits only two `git-seed-v1` ops.</comment>
<file context>
@@ -59,7 +110,7 @@ pub fn frame_command(line: &str) -> Option<&'static str> {
}
let cmd = object.get("cmd")?.as_str()?;
- GIT_SEED_COMMANDS.iter().copied().find(|known| *known == cmd)
+ GIT_SEED_COMMANDS.iter().chain(&GIT_SEED_EXPORT_COMMANDS).copied().find(|known| *known == cmd)
}
</file context>
|
Review (codex gpt-6-astra high, router-review): BLOCK at 74cfbb1 BLOCK at 74cfbb1 Visual verdict: no images; Leo tries the build
Validation: source review and isolated Git reproductions; Cargo was unavailable. |
…, export a stash-only change Review fixes (router-review BLOCK at 74cfbb1): a non-UTF-8 secret was excluded by its lossy spelling and went in WORK; a retried export was refused once its bundle existed; a stash-only change exported no bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/tests/export.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/tests/export.rs:234">
P2: This assertion can't hold: `git stash -q` resets the working tree to HEAD, so WORK no longer matches the seed's tree. To model a stash-only change, drop the extra `write` before the stash and restore the seeded tree with `git stash apply -q` after it, then keep the WORK assertion.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| let bundle = world.home.join("back.bundle"); | ||
| let status = export(&service, &export_params(&folder, &first, &bundle)); | ||
| assert_eq!(status["state"], "done", "{status}"); | ||
| assert_eq!(status["work"], first.work.as_str(), "the working tree is the seed's"); |
There was a problem hiding this comment.
P2: This assertion can't hold: git stash -q resets the working tree to HEAD, so WORK no longer matches the seed's tree. To model a stash-only change, drop the extra write before the stash and restore the seeded tree with git stash apply -q after it, then keep the WORK assertion.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/tests/export.rs, line 234:
<comment>This assertion can't hold: `git stash -q` resets the working tree to HEAD, so WORK no longer matches the seed's tree. To model a stash-only change, drop the extra `write` before the stash and restore the seeded tree with `git stash apply -q` after it, then keep the WORK assertion.</comment>
<file context>
@@ -178,3 +178,62 @@ fn an_export_is_refused_outside_a_seed_or_into_its_folder() {
+ let bundle = world.home.join("back.bundle");
+ let status = export(&service, &export_params(&folder, &first, &bundle));
+ assert_eq!(status["state"], "done", "{status}");
+ assert_eq!(status["work"], first.work.as_str(), "the working tree is the seed's");
+ assert_eq!(status["unchanged"], false, "{status}");
+ let stash = status["stash"].as_str().unwrap();
</file context>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
|
Found 15 test failures on Blacksmith runners: Failures
...and 5 more test failures. View all on Blacksmith: run 41292709121, run 41292709164
|
…record Review follow-ups (router-review SHIP at dc8e290): an export needs a seed's folder (refs/cmux-seed), and the back record, the newer, is preferred for the stash comparison; a dropped stash moves the record and sends nothing. since need not be the newest record: an export the Mac could not merge moved it, and the Mac pulls again from its own base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…odule Review findings (router-review BLOCK at 8fed88e): an .npmrc under a non-UTF-8 folder had its token check read a lossy path that did not exist (an unreadable registry file now counts as a secret), and work inside a submodule on the machine read as unchanged (now seed.submodule_local_work, as the Mac's plan refuses it on the way up). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
|
Review (independent Codex gpt-6.1-sol, high, via the review router; full diff from merge base 61ad0ed to this head): BLOCK at 8fed88e. [P1] [blocking] cmux-tui/crates/cmux-tui-core/src/git_seed/job.rs:281 - |
There was a problem hiding this comment.
4 issues found across 5 files (changes from recent commits).
You’re at about 91% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs:73">
P3: The new "could not be read" fallback has no test. Removing `.npmrc` from the None list leaves nothing asserting that an unreadable registry file is excluded. Add a positive assertion for it.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui/src/link/seed/plan.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/seed/plan.rs:617">
P1: A secret path containing non-UTF-8 bytes is detected here but stored lossily; `build` later uses that display string as the `git rm` pathspec, which does not match the original path, so the secret remains in the seed bundle. Preserve the raw bytes for exclusion and use the lossy string only for display.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui-core/src/git_seed/export.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui-core/src/git_seed/export.rs:206">
P1: `ls-files -z` returns raw path bytes, but this lossy decode changes non-UTF-8 submodule names before the `.git` check and can let a dirty submodule pass as unchanged. Keep the path bytes through filesystem operations and decode only for the error message.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui-core/src/git_seed/export.rs:215">
P2: The check refuses only uncommitted submodule work. A submodule with a local commit not on its remote is exported with its new gitlink, but the commit is not in the bundle. The Mac's plan refuses that case, so add the same `refs/remotes` containment test here, or narrow the comment so it does not claim parity.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| for path in listed.split(|byte| *byte == 0).filter(|path| !path.is_empty()) { | ||
| let path = String::from_utf8_lossy(path).into_owned(); | ||
| for raw in listed.split(|byte| *byte == 0).filter(|path| !path.is_empty()) { | ||
| let path = String::from_utf8_lossy(raw).into_owned(); |
There was a problem hiding this comment.
P1: A secret path containing non-UTF-8 bytes is detected here but stored lossily; build later uses that display string as the git rm pathspec, which does not match the original path, so the secret remains in the seed bundle. Preserve the raw bytes for exclusion and use the lossy string only for display.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/plan.rs, line 617:
<comment>A secret path containing non-UTF-8 bytes is detected here but stored lossily; `build` later uses that display string as the `git rm` pathspec, which does not match the original path, so the secret remains in the seed bundle. Preserve the raw bytes for exclusion and use the lossy string only for display.</comment>
<file context>
@@ -613,12 +613,12 @@ fn untracked_left_out(repo: &Repo) -> Result<Vec<Skipped>, Fallback> {
- for path in listed.split(|byte| *byte == 0).filter(|path| !path.is_empty()) {
- let path = String::from_utf8_lossy(path).into_owned();
+ for raw in listed.split(|byte| *byte == 0).filter(|path| !path.is_empty()) {
+ let path = String::from_utf8_lossy(raw).into_owned();
let why = if path.ends_with('/') {
Some("a separate git repository inside this folder (not a submodule)")
</file context>
| fn dirty_submodule(&self) -> Result<Option<String>, Failure> { | ||
| let staged = self.git(&["ls-files", "-z", "-s"])?; | ||
| for entry in staged.split(|byte| *byte == 0).filter(|entry| !entry.is_empty()) { | ||
| let entry = String::from_utf8_lossy(entry); |
There was a problem hiding this comment.
P1: ls-files -z returns raw path bytes, but this lossy decode changes non-UTF-8 submodule names before the .git check and can let a dirty submodule pass as unchanged. Keep the path bytes through filesystem operations and decode only for the error message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/export.rs, line 206:
<comment>`ls-files -z` returns raw path bytes, but this lossy decode changes non-UTF-8 submodule names before the `.git` check and can let a dirty submodule pass as unchanged. Keep the path bytes through filesystem operations and decode only for the error message.</comment>
<file context>
@@ -190,6 +198,32 @@ impl Export {
+ fn dirty_submodule(&self) -> Result<Option<String>, Failure> {
+ let staged = self.git(&["ls-files", "-z", "-s"])?;
+ for entry in staged.split(|byte| *byte == 0).filter(|entry| !entry.is_empty()) {
+ let entry = String::from_utf8_lossy(entry);
+ let Some((meta, path)) = entry.split_once('\t') else { continue };
+ if !meta.starts_with("160000 ") {
</file context>
| if !checkout.join(".git").exists() { | ||
| continue; | ||
| } | ||
| let status = self.git.quiet( |
There was a problem hiding this comment.
P2: The check refuses only uncommitted submodule work. A submodule with a local commit not on its remote is exported with its new gitlink, but the commit is not in the bundle. The Mac's plan refuses that case, so add the same refs/remotes containment test here, or narrow the comment so it does not claim parity.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/export.rs, line 215:
<comment>The check refuses only uncommitted submodule work. A submodule with a local commit not on its remote is exported with its new gitlink, but the commit is not in the bundle. The Mac's plan refuses that case, so add the same `refs/remotes` containment test here, or narrow the comment so it does not claim parity.</comment>
<file context>
@@ -190,6 +198,32 @@ impl Export {
+ if !checkout.join(".git").exists() {
+ continue;
+ }
+ let status = self.git.quiet(
+ Some(&checkout),
+ &Credential::Anonymous,
</file context>
| ] { | ||
| assert!(reason(top, path.as_bytes()).is_some(), "{path}"); | ||
| } | ||
| for path in ["README.md", "id_rsa.pub", "src/env.rs", ".envrc-notes/x", "keyboard.rs"] { |
There was a problem hiding this comment.
P3: The new "could not be read" fallback has no test. Removing .npmrc from the None list leaves nothing asserting that an unreadable registry file is excluded. Add a positive assertion for it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui-core/src/git_seed/secrets.rs, line 73:
<comment>The new "could not be read" fallback has no test. Removing `.npmrc` from the None list leaves nothing asserting that an unreadable registry file is excluded. Add a positive assertion for it.</comment>
<file context>
@@ -60,12 +68,10 @@ mod tests {
- ["README.md", "id_rsa.pub", "src/env.rs", ".envrc-notes/x", "keyboard.rs", ".npmrc"]
- {
- assert_eq!(reason(top, path), None, "{path}");
+ for path in ["README.md", "id_rsa.pub", "src/env.rs", ".envrc-notes/x", "keyboard.rs"] {
+ assert_eq!(reason(top, path.as_bytes()), None, "{path}");
}
</file context>
| for path in ["README.md", "id_rsa.pub", "src/env.rs", ".envrc-notes/x", "keyboard.rs"] { | |
| assert!(reason(top, b".npmrc").is_some(), "unreadable registry file counts as a secret"); | |
| for path in ["README.md", "id_rsa.pub", "src/env.rs", ".envrc-notes/x", "keyboard.rs"] { |
|
Review (codex, router-review): SHIP at b682860 Reviewer: codex gpt-6-astra high, fast tier. Follow-ups: 1. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward. SHIP at b682860 Visual verdict: no images; Leo tries the build
Follow-ups:
|
# Conflicts: # cmux-tui/crates/cmux-tui-core/src/git_seed.rs
… APFS (macOS base red) (#35889) a_secret_with_a_non_utf8_name_is_left_out (#30923) and a_secret_with_a_non_utf8_name_never_goes (#30633) create a file named b".env-\xff". APFS refuses a name that is not UTF-8 (EILSEQ, 'Illegal byte sequence'), so both panicked at setup on every macOS run since they landed; no folder on macOS can hold such a secret, so the product path they check cannot meet one there. They now accept EILSEQ on macOS only, as git_seed/secrets.rs does since #32091; on Linux (ext4 takes raw bytes) the assertions run unchanged.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
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:
Spec: leo-workspace
design/cloud.md, "Getting the project onto the machine", rule 6 ("Bringing changes back is a diff too").What it adds
git.seed.export {folder, since_work, since_head, bundle}, under a new capability,git-seed-export-v1. It is not added togit-seed-v1because a machine advertising onlygit-seed-v1would deny the new op at its remote gate. The Mac needs a capability it can ask for first (machine_gate::outdated). The link'sFsGateadmits the op throughgit_seed::frame_command.build_capabilitiesleaves the new capability out, as it already does forgit-seed-v1, because whether a host serves it depends on the host.add -Aover the whole working tree, which can take a while on a large repository. So the op answers{job}at once, likegit.seed.start, andgit.seed.statusreads it. Once the job is done its status adds{head_ref, head, index, work, stash, unchanged, bundle_bytes, left_out}.cmux seed <seed@cmux.invalid>, the fixed date946684800 +0000, and the same messages. The shared code lives ingit_seed/snapshot.rs, which delta seeds'check_deltanow uses too. The "untracked secret" rule moved tocmux_tui_core::git_seed::secrets, and the Mac'slink/seed/secrets.rsre-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 answersunchangedwith no bundle written.refs/cmux-back/{head,index,work,stash?}with the prerequisites^since_workand^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 atomicPendingWrite(create-new, 0600, free space checked), outside the seed's folder. The commits are also recorded in the folder asrefs/cmux-back/*, which keeps them out of gc, and a later delta seed may build on an export:check_deltaacceptssinceequal torefs/cmux-seed/workor torefs/cmux-back/work.git.seed.start. Ignored files (node_modules, build output) never go into the bundle.CMUX_TEST_GIT_SEED_REMOTESis a test seam for debug builds only: it readshttps://github.com/O/N.gitasfile://<dir>/O/N.gitso 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_seedon the fleet (cmux-ci test). New tests ingit_seed/tests/export.rs:.envand 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.sincethe folder lacks.Clippy with
-D warningsis clean in the files this PR touches. The remaining clippy errors on the base are inserver/agent_session_folders.rsandcli/cloud_repo.rsand 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.git-seed-export-v1, kept separate fromgit-seed-v1so machines advertising only the older capability deny it at their remote gate.git.seed.status; an unchanged seed answersunchangedand writes no bundle, and a retried export whose answer was lost returns the same job even once its bundle exists.refs/cmux-back/{head,index,work,stash?}with^since_workand^since_head, so only changed files travel; it is written atomically with 0600 permissions.refs/cmux-backpreferred overrefs/cmux-seed): a stash-only change still exports, a dropped stash sends nothing, andsinceneed not be the newest record since the Mac can pull again from its own base.refs/cmux-back/*in the folder, keeping them out of gc and letting a later delta seed build on an export.cmux_tui_core::git_seed::secretsso 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.seed.submodule_local_work), never exported as unchanged.CMUX_TEST_GIT_SEED_REMOTESis 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.Written for commit 9b205ca. Summary will update on new commits.