Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion cmux-tui/crates/cmux-tui-core/src/git_seed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,28 @@ pub const GIT_SEED_COMMANDS: [&str; 2] = ["git.seed.start", "git.seed.status"];

static SERVICE: OnceLock<GitSeedService> = OnceLock::new();

/// Test seam, debug builds only: `CMUX_TEST_GIT_SEED_GITHUB=<absolute dir>`
/// makes the daemon's seeds clone `https://github.com/OWNER/NAME.git` from
/// `file://<dir>/OWNER/NAME.git`, with no GitHub path
/// ([`GitSeedService::with_test_github`]). Release builds never read it.
#[cfg(debug_assertions)]
const TEST_GITHUB_VAR: &str = "CMUX_TEST_GIT_SEED_GITHUB";

/// The seed owner, when this daemon serves `fs-v1` (it seeds under the same
/// roots).
#[must_use]
pub fn installed() -> Option<&'static GitSeedService> {
let fs = crate::fs_ops::installed()?;
Some(SERVICE.get_or_init(|| GitSeedService::new(fs.roots().clone())))
Some(SERVICE.get_or_init(|| {
#[cfg(debug_assertions)]
if let Some(dir) = std::env::var_os(TEST_GITHUB_VAR)

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: A relative CMUX_TEST_GIT_SEED_GITHUB is ignored without any signal, so the seed quietly clones from real GitHub instead of the test directory. Warn or panic when the variable is set but not absolute, rather than filtering it out.

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 52:

<comment>A relative `CMUX_TEST_GIT_SEED_GITHUB` is ignored without any signal, so the seed quietly clones from real GitHub instead of the test directory. Warn or panic when the variable is set but not absolute, rather than filtering it out.</comment>

<file context>
@@ -35,12 +35,28 @@ pub const GIT_SEED_COMMANDS: [&str; 2] = ["git.seed.start", "git.seed.status"];
-    Some(SERVICE.get_or_init(|| GitSeedService::new(fs.roots().clone())))
+    Some(SERVICE.get_or_init(|| {
+        #[cfg(debug_assertions)]
+        if let Some(dir) = std::env::var_os(TEST_GITHUB_VAR)
+            .map(std::path::PathBuf::from)
+            .filter(|dir| dir.is_absolute())
</file context>

.map(std::path::PathBuf::from)
.filter(|dir| dir.is_absolute())
{
return GitSeedService::with_test_github(fs.roots().clone(), &dir);
}
GitSeedService::new(fs.roots().clone())
}))
}

/// The capability to advertise in `identify`, when served.
Expand Down
21 changes: 21 additions & 0 deletions cmux-tui/crates/cmux-tui-core/src/git_seed/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,27 @@ impl GitSeedService {
Self { roots, config, jobs: Mutex::new(Jobs::default()) }
}

/// For tests (and the debug-only daemon seam in [`super::installed`]):
/// a service whose GitHub is the folder `github`, so
/// `https://github.com/OWNER/NAME.git` is cloned from
/// `file://<github>/OWNER/NAME.git`, with no credential. Nothing on the
/// wire reaches it.
#[doc(hidden)]
#[must_use]
pub fn with_test_github(roots: Roots, github: &std::path::Path) -> Self {

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: with_test_github disables the GitHub credential path and enables protocol.file, but it is compiled into release builds. Gate it with #[cfg(debug_assertions)] to match the debug-only seam in installed(), so release builds cannot construct this service.

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 193:

<comment>`with_test_github` disables the GitHub credential path and enables `protocol.file`, but it is compiled into release builds. Gate it with `#[cfg(debug_assertions)]` to match the debug-only seam in `installed()`, so release builds cannot construct this service.</comment>

<file context>
@@ -183,6 +183,27 @@ impl GitSeedService {
+    /// wire reaches it.
+    #[doc(hidden)]
+    #[must_use]
+    pub fn with_test_github(roots: Roots, github: &std::path::Path) -> Self {
+        let local = format!("file://{}/", github.display());
+        let config = Config {
</file context>
Suggested change
pub fn with_test_github(roots: Roots, github: &std::path::Path) -> Self {
#[cfg(debug_assertions)]
pub fn with_test_github(roots: Roots, github: &std::path::Path) -> Self {

let local = format!("file://{}/", github.display());

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: A non-UTF-8 absolute directory accepted by the environment seam is lossy-converted by display(), so the rewritten file:// URL points elsewhere and seeding fails. Encode the OS path without loss or reject 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/job.rs, line 194:

<comment>A non-UTF-8 absolute directory accepted by the environment seam is lossy-converted by `display()`, so the rewritten `file://` URL points elsewhere and seeding fails. Encode the OS path without loss or reject it.</comment>

<file context>
@@ -183,6 +183,27 @@ impl GitSeedService {
+    #[doc(hidden)]
+    #[must_use]
+    pub fn with_test_github(roots: Roots, github: &std::path::Path) -> Self {
+        let local = format!("file://{}/", github.display());
+        let config = Config {
+            git: Git {
</file context>

let config = Config {
git: Git {
extra_config: vec![
format!("url.{local}.insteadOf=https://github.com/"),
"protocol.file.allow=always".into(),
],
},
credentials: Some(vec![Credential::Anonymous]),
};
Self::with_config(roots, config)
}

/// Runs `cmd` with the request object `params`.
pub fn call(&self, cmd: &str, params: &Value) -> Result<Value, SeedRefusal> {
match cmd {
Expand Down
62 changes: 44 additions & 18 deletions cmux-tui/crates/cmux-tui/src/link/agent_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@
//! (`agent_session.host_unverified`) and gives the chat back. A replacement
//! link keeps the socket path, so a rerun still resolves an unresolved move.
//!
//! `--sync push|pull --sync-socket LINK --sync-local LOCAL --sync-remote
//! `--sync push|pull|seed --sync-socket LINK --sync-local LOCAL --sync-remote
//! REMOTE`: once the chat is frozen (after the begin, before the receive), the
//! pump copies its folder's last changes, `cmux link sync push LOCAL REMOTE`
//! (Move to Cloud) or `cmux link sync pull LOCAL REMOTE` (Bring Back Local)
//! over the machine link socket `LINK`. A chat moved mid-turn (`--interrupt`)
//! made changes up to the freeze, after the caller's own sync; this takes
//! (Move to Cloud), `cmux link seed push --since-last LOCAL REMOTE` (Move to
//! Cloud of a folder `cmux link seed push` put there: a delta bundle, or a
//! re-push of its copy) or `cmux link sync pull LOCAL REMOTE` (Bring Back
//! Local) over the machine link socket `LINK`. A chat moved mid-turn
//! (`--interrupt`) made changes up to the freeze, after the caller's own sync; this takes
//! them along, so the turn goes on there with them (acpmux queues its
//! continuation, cmuxterm-hq#5846 item 1). A sync that fails, or a pull that
//! conflicts (`sync_conflict` with `conflicts`), gives the chat back. It runs
Expand Down Expand Up @@ -101,7 +103,7 @@ use record::{Record, Records};
const USAGE: &str = "usage: cmux link agent move --from-socket LOCAL_DAEMON_SOCKET \
--surface SURFACE_ID --to-socket REMOTE_CONNECT_SOCKET --cwd REMOTE_FOLDER [--interrupt] \
[--ask-first] [--vm-kind KIND [--to-pid PID]] [--state-dir DIR] \
[--sync push|pull --sync-socket LINK_SOCKET --sync-local LOCAL --sync-remote REMOTE]
[--sync push|pull|seed --sync-socket LINK_SOCKET --sync-local LOCAL --sync-remote REMOTE]
--to-socket: the local_socket of `cmux-tui remote connect --headless --json` for the \
machine (its connection-snapshot event), not the machine link socket of `cmux link sync`";
/// Bytes asked for per page (the daemon serves at most 4 MiB).
Expand All @@ -127,14 +129,22 @@ struct Args {
sync: Option<Sync>,
}

/// `--sync push|pull --sync-socket --sync-local --sync-remote` (module docs).
/// `--sync push|pull|seed --sync-socket --sync-local --sync-remote` (module docs).
struct Sync {
pull: bool,
way: Way,
socket: PathBuf,
local: PathBuf,
remote: String,
}

/// Which way `--sync` copies.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Way {
Push,
Pull,
Seed,
}

/// A move that failed, as its JSON line reports it.
struct Failed {
code: String,
Expand Down Expand Up @@ -254,8 +264,9 @@ fn parse(args: &[String]) -> Option<Args> {
"--ask-first" => ask_first = true,
"--sync" => {
sync = Some(match rest.next()?.as_str() {
"push" => false,
"pull" => true,
"push" => Way::Push,
"pull" => Way::Pull,
"seed" => Way::Seed,
_ => return None,
});
}
Expand All @@ -269,8 +280,8 @@ fn parse(args: &[String]) -> Option<Args> {
// All four or none; the remote folder is absolute, as `cmux link sync` takes it.
let sync = match (sync, sync_socket, sync_local, sync_remote) {
(None, None, None, None) => None,
(Some(pull), Some(socket), Some(local), Some(remote)) if remote.starts_with('/') => {
Some(Sync { pull, socket, local, remote: remote.trim_end_matches('/').to_owned() })
(Some(way), Some(socket), Some(local), Some(remote)) if remote.starts_with('/') => {
Some(Sync { way, socket, local, remote: remote.trim_end_matches('/').to_owned() })
}
_ => return None,
};
Expand Down Expand Up @@ -459,13 +470,28 @@ fn fresh(args: &Args, records: &Records) -> Result<Moved, Failed> {
/// `--sync`'s copy of a frozen chat's folder (module docs).
fn sync_frozen(sync: &Sync) -> Result<(), Failed> {
use super::sync_cli::{self, Back};
if !sync.pull {
return sync_cli::push(&sync.socket, &sync.local, &sync.remote).map(|_| ()).map_err(|e| {
Failed::new(
sync_cli::push_failure_code(&e),
format!("the folder's last changes: {e:#}"),
)
});
match sync.way {
Way::Push => {
return sync_cli::push(&sync.socket, &sync.local, &sync.remote).map(|_| ()).map_err(
|e| {
Failed::new(
sync_cli::push_failure_code(&e),
format!("the folder's last changes: {e:#}"),
)
},
);
}
Way::Seed => {
return super::seed_cli::push_since_last(&sync.socket, &sync.local, &sync.remote)
.map(|_| ())
.map_err(|e| {
Failed::new(
super::seed_cli::failure_code(&e),
format!("the folder's last changes: {e:#}"),
)
});
}
Way::Pull => {}
}
match sync_cli::pull_into(&sync.socket, &sync.local, &sync.remote) {
Back::Done(_) => Ok(()),
Expand Down
6 changes: 5 additions & 1 deletion cmux-tui/crates/cmux-tui/src/link/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ mod dial;
mod dial_cli;
mod enrollment;
mod ensure;
// Not wired into start-chat yet; its CLI entry is `cmux link seed plan`.
// `cmux link seed plan`; `cmux link seed push|clone` is [`seed_cli`].
#[cfg_attr(not(test), allow(dead_code))]
mod seed;
mod seed_cli;
mod start_chat_cli;
mod sync_cli;
// Wired into `serve` when Cloud hosts run the link (needs the TeamDO peer
Expand Down Expand Up @@ -119,6 +120,9 @@ pub(crate) fn run(args: &[String]) -> i32 {
return sync_cli::run(&args[1..]);
}
if args.first().map(String::as_str) == Some("seed") {
if matches!(args.get(1).map(String::as_str), Some("push" | "clone")) {
return seed_cli::run(&args[1..]);
}
return seed::run(&args[1..]);
}
// So do `agent` and `access`.
Expand Down
Loading
Loading