Skip to content

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

Merged
teamleaderleo merged 6 commits into
feat-cmux-nextfrom
seed-export
Nov 1, 2026
Merged

teamleaderleo merged 6 commits into
feat-cmux-nextfrom
seed-export

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

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.

Written for commit 9b205ca. Summary will update on new commits.

View guided diff Turn on auto-fix

…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
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 9b205ca: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30633 -f ref=seed-export -f platform=macos -f backend_mode=local

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026

@cubic-dev-ai cubic-dev-ai 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.

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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
pub fn run_env(
pub(super) fn run_env(

Comment on lines +25 to +27
if name == ".netrc" {
return Some("credentials file (.netrc)");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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, &params.folder)?;
if !std::fs::symlink_metadata(folder.join(".git")).is_ok_and(|meta| meta.is_dir()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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, &params.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)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
.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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, router-review): BLOCK at 74cfbb1

BLOCK at 74cfbb1

Visual verdict: no images; Leo tries the build

  1. [P1] [blocking] Secret exclusion fails for non-UTF-8 filenames. snapshot.rs:101 converts paths lossily, then uses those altered paths as exclusions. Reproduced with an untracked .env-\xff: it appears in left_out but its secret contents enter WORK and the export bundle. Preserve raw filename bytes for exclusions; convert only display text.

  2. [P2] [follow-up] Successful exports cannot be retried reliably. job.rs:287 rejects an existing bundle before checking for the matching job. Once the bundle is written, retrying a request whose response was lost returns seed.bad_request. Resolve the existing job before checking output availability.

  3. [P2] [follow-up] Stash-only changes produce no bundle. export.rs:132 determines unchanged solely from WORK. Reproduced by editing and stashing in a clean seed: WORK remains identical, so the response names a new stash commit without exporting its objects. Include stash changes in the unchanged decision.

Validation: source review and isolated Git reproductions; Cargo was unavailable.

@teamleaderleo teamleaderleo removed the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
…, 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

@cubic-dev-ai cubic-dev-ai 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.

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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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
@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 15 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpdwth6bxw/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpiplc01i0/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g/
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g
View Logs
test_each_app_reads_the_feed_its_publisher_writes: AssertionError()/
test_each_app_reads_the_feed_its_publisher_writes: AssertionError()
View Logs
test_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit (main.WorkflowTests.t
est_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit)/
test_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit (main.WorkflowTests.t
est_helper_never_takes_a_hosted_mac_and_skips_compile_on_hit)
View Logs

...and 5 more test failures. View all on Blacksmith: run 41292709121, run 41292709164

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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 - git.seed.export accepts any Git repository under the served roots; it only checks for a .git directory and that the supplied SHAs exist, so an authorized caller can bundle unrelated repositories and tracked secrets - require the folder’s origin and recorded refs/cmux-seed/*/refs/cmux-back/* record to match the requested seed before exporting.

@cubic-dev-ai cubic-dev-ai 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.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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"] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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"] {

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

  1. [P1][follow-up] cmux-tui/crates/cmux-tui-core/src/git_seed/export.rs:201-224 accepts a clean submodule checkout whose commit exists only locally. The Mac-side plan rejects commits absent from refs/remotes; this export only checks porcelain status. The outer bundle contains the gitlink but not the submodule’s commit objects, so the subsequent pull cannot materialize that submodule state. Add the same remote-containment check used by link/seed/plan.rs.

Follow-ups:

  • [P1][follow-up] Reject clean submodules whose checked-out commit is not present on a remote-tracking ref.

@teamleaderleo teamleaderleo removed the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026

@cmux-bot cmux-bot 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.

cmux-bot approves b682860: an independent review passed this exact head (router-review receipt by teamleaderleo). A push needs a new review; a later BLOCK at this head dismisses this approval.

@github-actions github-actions Bot added the risk:high Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
# Conflicts:
#	cmux-tui/crates/cmux-tui-core/src/git_seed.rs
@teamleaderleo
teamleaderleo merged commit 367b84b into feat-cmux-next Nov 1, 2026
56 of 61 checks passed
@teamleaderleo
teamleaderleo deleted the seed-export branch November 1, 2026 12:34
@lawrencecchen

Copy link
Copy Markdown
Contributor

Base red on macOS from these tests: a_secret_with_a_non_utf8_name_* cannot create their .env-\xff file on APFS (EILSEQ). Fix in #35889, the #32091 pattern.

(BASE RED SUITE for the cmux-next Performance Lead.)

cmux-bot Bot pushed a commit that referenced this pull request Nov 4, 2026
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants