Skip to content

Cloud: cmux cloud start-chat --from-folder copies a Mac folder the composer's way - #36487

Open
teamleaderleo wants to merge 30 commits into
feat-cmux-nextfrom
lane/cloud-start-from-folder
Open

teamleaderleo wants to merge 30 commits into
feat-cmux-nextfrom
lane/cloud-start-from-folder

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Leo: "you pick a big folder, you pick a small folder, you pick a folder that has an existing kind of, like, Codex workspace type thing ... you should be able to see all that and then be able to do stuff based off that."

What

Only the agent pane composer could start a Cloud chat from a folder on this Mac. cmux cloud start-chat and cmux cloud start-session could start one only in a folder already on the machine. This adds --from-folder PATH to both. The folder is copied to the machine exactly the way the composer copies one, and the chat starts in that copy.

This also moves some code without changing it, so that no file over its size limit grows (CmuxNextApp may not grow):

  • CloudStartSessionStrings and its strings move to CmuxNextCloudUI.
  • The relay-chat code moves to CloudStartSession+RelayChat.swift and cloud_start_chat/relay.rs.

Tests

  • Rust, cloud_start_chat/tests.rs:
    • parsing and canonicalizing the path;
    • relative, missing, file, missing-value and --cwd combinations refused;
    • forwarded to start-session with no relay call;
    • the retry command and the pending message.
  • Swift, CloudStartSessionFolderTests:
    • request parsing;
    • refusals for a relative path, --cwd, a too-broad folder and a credential folder;
    • the copy key is stable for the same machine and folder;
    • the run key differs per folder;
    • the catalog argument;
    • the shared check against a temporary Git folder;
    • every origin (this Mac's CLI, a socket caller saying cli with or without caller.terminal_id, a script, an agent) copies, and only to an owned machine (requireOwned);
    • credential folders are refused for every origin;
    • fromFolder's origin policy is held to cloudFileWrite's, so the two cannot drift.
  • Rust, link/sync_cli: a plain copy leaves credential folders and files out at any depth; the Rust folder list equals the Swift secretFolders (the test reads the Swift source); a folder swapped for a symlink mid-walk is not followed.

The cmuxterm-hq start-matrix probe has three Mac-folder rows (a small folder, a big folder and one with a GitHub remote) that use this on production once a Nightly has it.

Threat model

--from-folder copies a folder from this Mac to a Cloud machine. It takes every origin that may already write to that machine, and no origin gate:

  • cloudExec (Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Machine.swift, line 46) runs any command on a machine the account has, for a CLI, agent or script origin, through the Cloud API's exec.
  • cloudFileWrite (same file, line 94) writes any file contents there, for every origin.

A caller that can do either can already copy any file this Mac lets it read, so refusing --from-folder for those origins protects nothing, and it broke the one-command start Leo asked for ("testing it, like as a single command line argument, in conjunction with the arguments of folder or no folder"). An earlier revision refused every origin but the person's in-app gesture, then CLI script origins; both are reverted for this reason. CloudStartSession.fromFolder has no origin check, the same as cloudFileWrite, and theFoldersOriginPolicyIsCloudFileWrites fails if either one gains a gate the other lacks.

What protects the copy:

  • An owned machine only. CloudStartSession.requireOwned (CloudStartSession.swift) refuses unless the machine's link answered vm_kind: "owner" for this account; a teammate's machine would keep the copy after refusing the chat.
  • Checked on disk first. AgentPaneCloudStart.checkedFolder runs before any machine is made or taken: an absolute, existing folder, never home or above it, never the disk root.
  • Credentials never go. One list, AgentPaneCloudFolderRefusal.secretFolders (Swift) = skips.rs CREDENTIAL_FOLDERS (Rust, held equal by a test): a selected folder that is, holds or sits under one is refused for every origin, and a copy leaves those folders out at any depth, along with credential files (.netrc, .npmrc, .pypirc, .git-credentials, .pgpass, .config/hub) and the existing .env* and key-file skips.
  • No symlink escape. The plain-copy walk opens each folder with openat(O_NOFOLLOW | O_DIRECTORY) from its parent's descriptor and lists it through that descriptor, so a folder swapped for a symlink mid-walk is never walked through.

Not covered here: file reads at push time still open by path (O_NOFOLLOW on the last name only), and the git path's walk of .git is path-based.

Changelog

  • Added: cmux cloud start-chat --from-folder PATH starts a Cloud chat in a copy of a folder from this Mac, the way the composer does.

Observed / Not run

Fleet results are in the comments below. Not run: a live start-chat on a tagged build.
Agent session: agent session

🤖 Generated with Claude Code


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


Summary by cubic

Adds --from-folder PATH to cmux cloud start-chat and cmux cloud start-session so a Cloud chat can start in a copy of a folder on this Mac, copied the same way the composer's Cloud start copies one. Only the composer (the person's own run in the app) copies today: a command cannot prove it is the person's, so both verbs refuse --from-folder for now.

  • The composer's copy and folder-check logic are factored into shared code both paths call; the composer behaves the same. The base merge keeps this while folding base's newer relay-chat code (login refusal codes, typed errors, new-machine tracking, the stage trace) into the moved files and sharing the login-refusal mapping between starts and relay chat.
  • PATH must be an absolute, existing folder: never home or above it, never the disk root, never a credential folder (~/.config/gh, ~/.cargo, ~/.gem, ~/.terraform.d, ~/.password-store); --from-folder with --cwd is a usage error. The folder is checked on disk before any machine is made or taken. A copy leaves out the App's credential folders at any depth (the refusal list: .secrets, .docker, .cargo, Library/Keychains, .config/gcloud, ...) and credential files (.netrc, .npmrc, .pypirc, .git-credentials, ...), and each folder is opened by descriptor so a folder swapped for a symlink mid-walk is not followed.
  • The copy key comes from the machine and the folder, so re-running the same command reuses the finished copy instead of overwriting it.
  • --from-folder skips the relay-first chat, copies only to a machine this account owns, and only on the person's own run in the app (a gesture, never a socket request): an agent's start, any command (cli or script), or a socket caller — even one claiming the person's terminal, which an agent in that terminal also claims — is refused.
  • The copied folder's trust comes from the app link's owner answer; string and relay-chat code moves between packages to keep CmuxNextApp within its size limit. The start-session argument is exported to the action catalog (action-surfaces.json).

Written for commit 118f1dd. Summary will update on new commits.

View guided diff Turn on auto-fix

…mposer's way

`cmux cloud start-chat --from-folder PATH` and the app verb it runs,
`cmux cloud start-session --from-folder PATH`, start the chat on the Cloud
machine in a copy of a folder on this Mac, copied exactly as the agent pane
composer's Cloud start copies one.

- CloudStartChat.copyFolder: the composer's copy (CloudOutdatedMachine.copy +
  `cmux link start-chat copy`), factored out; the composer calls it unchanged.
- AgentPaneCloudStart.checkedFolder (new AgentPaneCloudFolder.swift): the
  hand-off's folder check (canonical folder, its Git working copy and git
  folders; never home or above, never a credential folder, #35019), factored out
  of AgentPaneHandOff, which calls it; AgentPaneCloudFolderRefusal moved there
  and made public, with `written(_:home:)` for a check from the words alone.
- CloudStartSession: `fromFolder` is checked from its words before anything
  starts (absolute, not with --cwd, not home/root/credential), then on disk
  with the shared check once the machine is connected; the copy runs before the
  session starts, under CloudMoveFolder.copyKey (a UUID spelling of
  SHA-256(machine, canonical folder)), so the same command again on the same
  machine reuses the finished copy. The session starts in the copy's chat
  folder; a source this Mac trusts is trusted on the machine, as the composer
  does. Runs that differ in the folder are different runs (CloudStartRunKey).
- The verb's catalog entry lists `fromFolder`; refusals are localized.
- start-chat: --from-folder (absolute, canonicalized, must exist; not with
  --cwd) is forwarded to start-session and skips the relay-first chat (which
  copies no folder); --help lists it.
- To keep CmuxNextApp from growing (arch-ratchet app_lines, now -77):
  CloudStartSessionStrings and its CloudSessions.xcstrings moved to
  CmuxNextCloudUI (public, its login refusal takes a CloudUI enum), and the
  relay-chat code moved to its own files (Swift and Rust) so neither god file
  grows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
@teamleaderleo
teamleaderleo temporarily deployed to cmux-next-cloud-journey November 5, 2026 11:30 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 118f1dd: 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=pr36487 -f ref=lane/cloud-start-from-folder -f platform=macos -f backend_mode=local

@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 5, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Fleet results at head 4fd34b4:

  • Rust, cargo test -p cmux-tui --lib -- cloud_start_chat (https://github.com/manaflow-ai/cmux/actions/runs/42020156561): 39 passed, 0 failed. This includes the new --from-folder tests (from_folder_is_an_existing_folder_of_this_mac_answered_canonical, from_folder_goes_to_start_session_and_skips_the_relay_chat, a_pending_start_from_a_folder_says_the_copy_may_still_run) and every relay and prompt test that moved into cloud_start_chat/relay.rs.

  • Swift, cmux-ci run --class light package-test-lane suites: the package builds with the strings move, and these all pass:

    Suite Tests passed
    CloudStartSessionFolderTests 10
    CloudStartSessionTests 28
    CloudStartSessionAsksTests 26
    CloudAskDialogsTests 8
    CloudStartChatMachineTests 10
    CloudStartChatLoginTests 9
    CloudStartChatAppLinkTests and CloudMoveAppLinkOwnerTests 3

    The step's one red is a filter I mistyped (AgentPaneHandOffTests names no suite: 0 tests). The real hand-off suite, AgentPaneCloudStartTests, is running separately.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Swift, package-test-lane suite AgentPaneCloudStartTests at 4fd34b4: 18 passed in 2 suites. This covers the hand-off folder check that moved into AgentPaneCloudStart.checkedFolder.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

On the BLOCK at 4fd34b4 (F1, "fromFolder has no default, so the tests don't compile"): Swift's memberwise initializer gives an optional var with no initial value a default of nil. The calls that leave out fromFolder: therefore compiled. At 4fd34b4, CloudStartSessionTests passed 28 tests and CloudStartSessionFolderTests passed 10, both suites built from those same call sites.

e2ac81c still writes var fromFolder: String? = nil out in full, so the default is explicit for readers and later reviews. The suites are rerunning at e2ac81c.

teamleaderleo and others added 3 commits November 5, 2026 12:23
…al_initialization); say so where it is declared

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
…om-folder is checked on disk before any machine is made

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
…he disk check before any machine on one line (App lines flat)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

On the BLOCK at 81a42d2 (F1, "fromFolder has no default, so the test target fails to compile"): this is false at this exact head. The suites built from those call sites compiled and passed at 81a42d2:

  • CloudStartSessionFolderTests: 11 tests.
  • CloudStartSessionTests: 28 tests.
  • AgentPaneCloudStartTests: 18 tests.

Swift's memberwise initializer defaults a var of optional type with no initial value to nil. Writing = nil out is refused by the lint gate's implicit_optional_initialization rule, which is why the default stays implicit.

…ammate's would keep the copy after refusing the chat)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review (gpt-6-astra): SHIP at 5282e59

…no CLI origin counts

The control socket takes a caller's own origin (cli, script, ...) and its
caller.terminal_id, and an agent in the person's terminal says both too,
so neither proves the person ran it. fromFolder now requires isPersonsOwnRun
(a gesture in this app, never a socket request), and start-chat no longer
sets CMUX_CLI_STDOUT_CAPTURED (an environment variable carries no
authority). The refusal and start-chat's help say a command cannot copy a
Mac folder for now; the composer's Cloud start does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
teamleaderleo and others added 3 commits November 6, 2026 20:07
requireOwned(_:) is the check start-session already made inline before a
copy, so a test can run it against an owned and a non-owned machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
… an owned machine only (red)

cloudFileWrite (and cloudExec) already let any CLI, script, agent or
socket origin write to a machine this account owns, so refusing those
origins a folder copy guards nothing. fromFolder copies for every origin
(this Mac's CLI, a socket caller saying cli with or without its terminal,
a script, an agent), a test holds its origin policy to cloudFileWrite's,
credential folders are refused for every origin, and the copy goes to an
owned machine only (requireOwned).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
… owned machine

cloudExec and cloudFileWrite already let any CLI, script, agent or socket
origin run commands on and write files to a machine this account owns, so
an origin gate on the folder copy guarded nothing and broke the one-command
start Leo asked for. fromFolder takes every origin, as cloudFileWrite does;
what stays: the machine must be this account's own (requireOwned), the
folder is checked on disk before any machine, credential folders are
refused and skipped, and the walk never follows a symlink. The refusal
string for agents goes, and start-chat's help no longer says it refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra, independent, different model family): SHIP at e50c1f9

teamleaderleo and others added 2 commits November 6, 2026 21:24
relay_chat moved to cloud_start_chat/relay.rs on this branch; the base's
machine-cap answer (start_refused) is ported there for a new machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
plans/cmux-next/action-surfaces.json lists start-session's fromFolder
argument (the generated-files job's regenerated copy). skips.rs's
ends_with reads the tail with checked_sub and get instead of a slice,
so the panic ratchet holds at its baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra, independent, different model family): SHIP at 49fcdde. The merge keeps start_refused on relay-created machines, on direct start-session refusals and on pending failed-machine cap errors. --from-folder still works and is exported correctly. The new ends_with is equivalent for reachable bounds and handles invalid ones safely. 92 Rust tests and the compile check pass.

teamleaderleo and others added 3 commits November 6, 2026 23:26
The base's new_machine tracking (relay_chat's `made`) is ported into
cloud_start_chat/relay.rs, and the relay still skips a --from-folder
chat. CloudStartSessionStrings keeps this branch's public accessors with
the base's new relayLoginUnverified wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
The base's stage trace (#37293) is kept: Timed wraps the runner in
cloud_start_chat.rs, and relay_chat in cloud_start_chat/relay.rs passes on
the relay answer's RELAY_STAGES times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
The base's rustfmt of RELAY_STAGES is already in cloud_start_chat/relay.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review (codex-gpt-6-astra): SHIP at 118f1dd

This branch was successfully deployed

2 active and 2 inactive deployments
ci-routing-agent — 118f1dd8 Deployed Nov 7, 2026 by github-actions[bot] via cmux-next macOS placement #76182
artifacts — 118f1dd8 Deployed Nov 7, 2026 by teamleaderleo via publish tree-addressed cmux-tui #120259
sentry-dsyms — 118f1dd8 Deployed Nov 7, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #120259
cmux-next-cloud-journey — 118f1dd8 Deployed Nov 7, 2026 by teamleaderleo via journey #235
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant