Repository navigation
Cloud: cmux cloud start-chat --from-folder copies a Mac folder the composer's way - #36487
teamleaderleo wants to merge 30 commits into
Conversation
…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
|
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)
|
|
Fleet results at head 4fd34b4:
|
|
Swift, package-test-lane suite |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
|
On the BLOCK at 4fd34b4 (F1, " e2ac81c still writes |
…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
|
On the BLOCK at 81a42d2 (F1, "
Swift's memberwise initializer defaults a |
…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
left a comment
There was a problem hiding this comment.
Review (gpt-6-astra): SHIP at 5282e59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-b224b7e441ec6ef0e84d
…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
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
|
Review (codex gpt-6-astra, independent, different model family): SHIP at e50c1f9 |
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
|
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. |
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
left a comment
There was a problem hiding this comment.
Review (codex-gpt-6-astra): SHIP at 118f1dd
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-chatandcmux cloud start-sessioncould start one only in a folder already on the machine. This adds--from-folder PATHto both. The folder is copied to the machine exactly the way the composer copies one, and the chat starts in that copy.cmux link start-chat copy: seeded from its GitHub remote, else copied) moves intoCloudStartChat.copyFolder, which the composer andstart-sessionboth call. The hand-off's folder check moves intoAgentPaneCloudStart.checkedFolder, whichAgentPaneHandOffandstart-sessionboth call. The composer behaves the same.--from-folderwith--cwdis a usage error. A copy of a folder this Mac trusts is trusted on the machine (Cloud start: a copy of a folder this Mac trusts is trusted on the machine #31106).--from-folderskips the relay-first chat (Cloud: relay-first chat in cloud start-chat #35829), which cannot copy a folder.cmux cloud start-chat --helplists--from-folder PATH.This also moves some code without changing it, so that no file over its size limit grows (CmuxNextApp may not grow):
CloudStartSessionStringsand its strings move to CmuxNextCloudUI.CloudStartSession+RelayChat.swiftandcloud_start_chat/relay.rs.Tests
cloud_start_chat/tests.rs:--cwdcombinations refused;CloudStartSessionFolderTests:--cwd, a too-broad folder and a credential folder;cliwith or withoutcaller.terminal_id, a script, an agent) copies, and only to an owned machine (requireOwned);fromFolder's origin policy is held tocloudFileWrite's, so the two cannot drift.link/sync_cli: a plain copy leaves credential folders and files out at any depth; the Rust folder list equals the SwiftsecretFolders(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-foldercopies 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-folderfor 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.fromFolderhas no origin check, the same ascloudFileWrite, andtheFoldersOriginPolicyIsCloudFileWritesfails if either one gains a gate the other lacks.What protects the copy:
CloudStartSession.requireOwned(CloudStartSession.swift) refuses unless the machine's link answeredvm_kind: "owner"for this account; a teammate's machine would keep the copy after refusing the chat.AgentPaneCloudStart.checkedFolderruns before any machine is made or taken: an absolute, existing folder, never home or above it, never the disk root.AgentPaneCloudFolderRefusal.secretFolders(Swift) =skips.rsCREDENTIAL_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.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_NOFOLLOWon the last name only), and the git path's walk of.gitis path-based.Changelog
cmux cloud start-chat --from-folder PATHstarts 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
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Adds
--from-folder PATHtocmux cloud start-chatandcmux cloud start-sessionso 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-folderfor now.~/.config/gh,~/.cargo,~/.gem,~/.terraform.d,~/.password-store);--from-folderwith--cwdis 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.--from-folderskips 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.CmuxNextAppwithin its size limit. Thestart-sessionargument is exported to the action catalog (action-surfaces.json).Written for commit 118f1dd. Summary will update on new commits.