Skip to content

fix: restore nightly CLI compilation after Cursor approvals - #10820

Merged
austinywang merged 2 commits into
mainfrom
fix-nightly-worktree-compilation
Aug 26, 2026
Merged

austinywang merged 2 commits into
mainfrom
fix-nightly-worktree-compilation

Conversation

@austinywang

@austinywang austinywang commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Restore cmux-cli compilation under Xcode 26.5 / Swift 6.3 by explicitly encoding the persisted PendingCursorShellApproval fields while keeping the legacy command key decode-only.
  • Rename the mutable project-root discovery variable so the final resolved projectRoot binding is not an invalid redeclaration.
  • Preserve the existing Cursor approval persistence/config behavior and all fix: remove stored identity defaults from worktree result #10787 worktree filesystem-identity safety behavior.
  • This complete two-diagnostic repair supersedes the one-error-only open PR fix(cli): restore Encodable synthesis for PendingCursorShellApproval #10808.
  • Workspace identity: cmux163: #10787 nightly worktree compilation.

Testing

  • Extended CLINotifyProcessIntegrationRegressionTests.testCursorShellApprovalDistinguishesUnsandboxedAndSandboxedPayloads to assert that persisted approval metadata round-trips through the real CLI hook path and does not emit the legacy decode-only key.
  • Test-only commit: ef4670da0f.
  • Fix commit: 3704118e5f.
  • Static repository guards pass (git diff --check, pbxproj normalization, package-resolved policy, workspace package grouping, and test wiring). The mandated file-length script/TSV is absent from this checkout; no budget file was created or changed.
  • Full Xcode validation will run on the shared GitHub macOS lane; no local app build or XCUITest was run.

Demo Video

Not applicable: compiler-only repair.

Review Trigger (Copy/Paste as PR comment)

@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed (not applicable)
  • I requested bot reviews after my latest commit
  • All code review bot comments are resolved
  • All human review comments are resolved

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

Summary by CodeRabbit

  • Bug Fixes
    • Improved persistence of shell-command approvals.
    • Approval records now retain the command’s UTF-8 length without storing the raw command text.
    • Project-root detection continues to fall back to the current working directory when no repository root is found.

@austinywang
austinywang merged commit 7e5b56c into main Aug 26, 2026
9 of 11 checks passed
@coderabbitai

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c9864d32-cd62-4d99-adcc-de4e16f6cba2

📥 Commits

Reviewing files that changed from the base of the PR and between 27ed978 and 3704118.

📒 Files selected for processing (2)
  • CLI/cmux.swift
  • cmuxTests/CLIGenericHookPersistenceTests.swift

📝 Walkthrough

Walkthrough

The CLI now persists approval metadata without the legacy command text. The Cursor persistence test verifies command length and omission. Project-root discovery uses a renamed variable and retains the current-directory fallback.

Changes

Cursor hook persistence

Layer / File(s) Summary
Approval persistence validation
CLI/cmux.swift, cmuxTests/CLIGenericHookPersistenceTests.swift
Approval encoding stores metadata without the legacy command. The Cursor test verifies commandLength and confirms that the command is not persisted.

Project-root discovery

Layer / File(s) Summary
Project-root fallback
CLI/cmux.swift
Project-root discovery uses the renamed candidate variable and falls back to cwdURL when no repository root exists.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: lawrencecchen, azooz2003-bit

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-nightly-worktree-compilation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

austinywang added a commit that referenced this pull request Aug 26, 2026
… in the rebase

The pre-rebase toolchain-fix commit had absorbed these cmux.swift hunks when
it was amended, so dropping it in favor of main's #10820/#10822 dropped
them too: the vm-tui-connect dispatch, tui in every vm usage string and the
help block, and the internal access on applyWindowOrCallerContext /
setTerminalForegroundProcessGroup that CMUXCLI+VMTui.swift needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Aug 26, 2026
… in the rebase

The pre-rebase toolchain-fix commit had absorbed these cmux.swift hunks when
it was amended, so dropping it in favor of main's #10820/#10822 dropped
them too: the vm-tui-connect dispatch, tui in every vm usage string and the
help block, and the internal access on applyWindowOrCallerContext /
setTerminalForegroundProcessGroup that CMUXCLI+VMTui.swift needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Aug 26, 2026
… in the rebase

The pre-rebase toolchain-fix commit had absorbed these cmux.swift hunks when
it was amended, so dropping it in favor of main's #10820/#10822 dropped
them too: the vm-tui-connect dispatch, tui in every vm usage string and the
help block, and the internal access on applyWindowOrCallerContext /
setTerminalForegroundProcessGroup that CMUXCLI+VMTui.swift needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Aug 26, 2026
… in the rebase

The pre-rebase toolchain-fix commit had absorbed these cmux.swift hunks when
it was amended, so dropping it in favor of main's #10820/#10822 dropped
them too: the vm-tui-connect dispatch, tui in every vm usage string and the
help block, and the internal access on applyWindowOrCallerContext /
setTerminalForegroundProcessGroup that CMUXCLI+VMTui.swift needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Aug 27, 2026
…kspaces project them (local + cloud, one drag/drop path) (#10887)

* cloud: one bulky free machine with a 5-day window, Pro gets five

Plan shape: the free plan now includes one full-size machine (24 GB
default and cap — the free machine demos the product; the paywall is
the window and the count, not the machine's usefulness) and Pro includes
five machines (24 GB default, 32 GB cap). 24576 joins the memory picker
options. All numbers stay env-overridable per plan.

Free access window: a free-plan machine older than 5 days
(CMUX_VM_FREE_ACCESS_WINDOW_DAYS, 0 disables) is preserved but
unreachable — attach, ssh, exec, ports, and sessions fail with a 402
vm_access_requires_pro upgrade prompt, while list/status/rename/delete
keep working so the machine stays visible and disposable. The gate keys
on the caller's CURRENT plan, so upgrading unlocks existing machines
immediately. Enforced in one place (requireAccessibleUserVm) that every
access workflow shares; the five REST routes thread the caller's plan
and map the typed error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Machines panel: free-window countdowns and locked rows (#10760)

* machines: surface the free access window — countdown rows, locked rows, upgrade routing

The list payload now carries freeAccessWindowDays (0 for paid plans) so
clients render policy from the wire instead of hardcoding it. The
Machines panel mirrors the backend's window math per row: free-plan
machines show a days-left countdown in the subtitle, and a machine past
the window renders locked — lock glyph in place of the activity dot,
Locked in the subtitle, and double-click/context-menu routing to the
shared Pro upgrade presenter instead of a doomed connect (the backend
still enforces with 402s; the UI just stops walking into them).
Rename/Status/Delete stay available on locked rows so the machine
remains manageable and disposable. Strings localized en+ja; snapshot
window math unit-tested against the backend's boundary behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* machines: flip free-window rows at the boundary itself, not on a poll tick

Review follow-up: the countdown/lock facet was only as fresh as the 45s
list poll. Expiry is a known future timestamp the client can compute
(createdAt + window), so the panel now arms a one-shot timer at exactly
the next transition across the fleet — each day-boundary where the label
decrements, and finally the expiry — and recomputes the facet locally
with no network, re-arming for the next boundary. Rows flip at the
moment the state changes; the slow poll is left covering only what
genuinely needs the server (machines created or deleted elsewhere). The
recompute happens above the lazy-list snapshot boundary, so the panel's
snapshot rule (cmux#2586) holds. Boundary math unit-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* worktree: drop stored defaults on identity lets so Xcode 26.6 builds main

After #10781, worktreeDeviceID/worktreeFileID were both defaulted at the
declaration and assigned in the explicit init, which the current toolchain
rejects ("immutable value may only be initialized once"). The init's
parameter defaults keep the same call-site contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: cmux vm run/push/pull/wait and the cmux-cloud-vm agent skill

vm run routes a command to a cloud machine without naming one: sticky
per-directory binding, then an idle agent-pool machine, then a sleeper,
then a freshly provisioned pool machine. push/pull move files over the
exec channel (base64 chunks, SHA-256 verified, directories as tarballs);
wait blocks until ready and optionally wakes the machine. The skill lets
any coding agent drive machines from plain CLI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* vm push: 64 KiB argv-bound chunks, no AppleDouble sidecars, line-safe progress

Live dogfood on Blaxel: a 512 KiB chunk base64-encodes past Linux's 128 KiB
per-argument limit ("argument list too long"), macOS tar shipped ._* files
onto the machine, and chunk progress ran together when stderr was captured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* vm run: pool membership is the persisted id list, not the display label; review fixes

- The router now only drafts machines it provisioned itself (ids recorded in
  ~/.cmuxterm/vm-run-pool.json at create time, pruned when machines vanish); a
  user machine renamed agent-pool is never used. Test covers the impostor.
- Staging tarball is removed if reading it throws before the defer is armed.
- Push/pull chunk progress is localized (cli.vm.push.progress, cli.vm.pull.progress).
- vm --help, the usage contract, and the contract doc list open/ports/tools/
  handoff/promote-template, which the dispatcher already handled.
- Sticky-binding fixture uses a fixed instant, not the host clock.
- Skill recipes: --sync runs inside the synced dir (no remote $PWD), port
  readiness poll instead of sleep, eligibility filter instead of .vms[0],
  background test exit status captured to a status file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: free access window is 7 days

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* vm run: lock the pool store across processes; idempotent, run-scoped recipes

- updateVMRunPool does the read-modify-write under flock on a sibling lock
  file, so two routers provisioning at once both land in the store; covered by
  a two-process test against two mock sockets.
- Dev-server recipe reuses a live server or starts one with a workspace pidfile
  and log; test recipe uses per-run log/status paths written atomically.
- Document that --sync is additive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* vm run: pool-store failures propagate; recipes validate the dev server and use unique run ids

- updateVMRunPool/saveVMRunPool throw on lock or write failure and createPoolVM
  reports the machine it provisioned but could not record, instead of a silent
  unlocked update.
- The dev-server recipe reuses a server only when the recorded pid is alive and
  owns :3000 (netstat -p), refuses to start a second server on a port someone
  else owns, and clears stale metadata.
- Test-run ids come from uuidgen, not the epoch second.
- Skill docs: cmux vm shell is a cmux-tui session now that machines run the
  cmux-tui remote daemon; agents keep working through vm run/exec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: run the cmux-tui remote daemon on Blaxel machines beside cmuxd-remote (Phase 1)

Implements the next open item of docs/cloud-cmux-tui-daemon.md (#10800): every
Blaxel machine gets the pinned static-musl cmux-tui (CMUX_VM_BLAXEL_TUI_URL +
_SHA256, verified in-VM, installed on the persistent home volume) running
`server start --remote-ws` under the sandbox supervisor, with its own private
preview. attach-endpoint accepts transport:"cmux-remote" and returns the
tokenized /v1/link route plus a single-use enrollment invitation when the
caller's device is not enrolled; a new cmux-remote/approve route approves the
pending claim the control plane invited. Opt-in per deployment; no change
for clients that do not ask.

Measured on Blaxel: a WebSocket upgrade with the preview token as a query
parameter completes on the raw <hash>.preview.bl.run host but is refused
through the vm.cmux.sh custom domain, so the daemon preview is created
unbranded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: cmux vm shell and the Machines panel open cmux-tui sessions

- `cmux vm tui <id>` and, by default, `cmux vm shell <id>` (which the Machines
  panel launches) open a workspace whose pane runs the local cmux-tui client
  against the machine's authenticated /v1/link route; the hidden
  vm-tui-connect helper hands the terminal to the client and approves the
  device enrollment through the app socket, remembering the device
  fingerprint per machine. The websocket attach remains only for
  deployments without a cmux-tui pin.
- Socket methods vm.cmux_remote_info / vm.cmux_remote_approve and the
  VMClient calls behind them; capabilities list updated.
- With the pin configured, new Blaxel machines get cmux-tui only: no
  cmuxd-remote install or process; the sleep watcher counts cmux-tui's
  terminal children as work.
- Client discovery probes candidates with `remote-probe --json` so the
  SSH-remote bootstrap's shell wrapper at ~/.cmux/bin/cmux is skipped.
- Localized en+ja strings; help, usage contract and docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* blaxel: fresh-machine cmux-tui bootstrap waits for the sandbox API and installs curl

Found by creating a machine under the pin: a just-created sandbox 404s its
API for a few seconds (the cmuxd path only survived because encoding the Go
binary took that long), and a stock blaxel/base-image has no curl until the
background provisioning adds it. The first write now retries until the API
answers, and the installer adds curl via apk or falls back to busybox wget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: cmux-tui is pinned by the published manifest and the app bundles the client

- The Blaxel driver resolves the daemon build from the artifacts manifest
  (rolling latest by default, CMUX_VM_CMUX_TUI_MANIFEST_URL to pin a commit,
  CMUX_VM_CMUX_TUI_ENABLED=0 as the kill switch); the sha256 comes from the
  manifest, never from env. An installed daemon that no longer matches the
  manifest is reinstalled on attach. The endpoint reports the daemon's build
  identity and remote protocol.
- scripts/install-cmux-tui-client.sh bundles a universal, sha256-verified
  cmux-tui client into Contents/Resources/bin like the Ghostty helper;
  reload.sh, ci.yml and release.yml run it. The CLI looks there first and
  checks the client/daemon remote protocol before opening a pane, naming the
  stale side.
- cmux-tui-artifacts.yml publishes on main pushes again so latest/ tracks main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cmux-remote: send a User-Agent on direct WebSocket dials

Hosted ingress in front of cmux Cloud machines (CloudFront on the branded
vm.cmux.sh domain) refuses upgrades that omit User-Agent, and tungstenite
sends none by default, so the daemon route had to fall back to the raw
preview host. Direct dials now carry cmux-tui/<version>; no Origin is set
because the daemon rejects browser-style upgrades. Unit test covers the
header and that the endpoint query (route token, lane) survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* nightly: bundle the cmux-tui client like ci/release do

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* integration: restore vm tui dispatch/usage/help/helper access

* cloud: one bulky free machine with a 5-day window, Pro gets five

Plan shape: the free plan now includes one full-size machine (24 GB
default and cap — the free machine demos the product; the paywall is
the window and the count, not the machine's usefulness) and Pro includes
five machines (24 GB default, 32 GB cap). 24576 joins the memory picker
options. All numbers stay env-overridable per plan.

Free access window: a free-plan machine older than 5 days
(CMUX_VM_FREE_ACCESS_WINDOW_DAYS, 0 disables) is preserved but
unreachable — attach, ssh, exec, ports, and sessions fail with a 402
vm_access_requires_pro upgrade prompt, while list/status/rename/delete
keep working so the machine stays visible and disposable. The gate keys
on the caller's CURRENT plan, so upgrading unlocks existing machines
immediately. Enforced in one place (requireAccessibleUserVm) that every
access workflow shares; the five REST routes thread the caller's plan
and map the typed error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Machines panel: free-window countdowns and locked rows (#10760)

* machines: surface the free access window — countdown rows, locked rows, upgrade routing

The list payload now carries freeAccessWindowDays (0 for paid plans) so
clients render policy from the wire instead of hardcoding it. The
Machines panel mirrors the backend's window math per row: free-plan
machines show a days-left countdown in the subtitle, and a machine past
the window renders locked — lock glyph in place of the activity dot,
Locked in the subtitle, and double-click/context-menu routing to the
shared Pro upgrade presenter instead of a doomed connect (the backend
still enforces with 402s; the UI just stops walking into them).
Rename/Status/Delete stay available on locked rows so the machine
remains manageable and disposable. Strings localized en+ja; snapshot
window math unit-tested against the backend's boundary behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* machines: flip free-window rows at the boundary itself, not on a poll tick

Review follow-up: the countdown/lock facet was only as fresh as the 45s
list poll. Expiry is a known future timestamp the client can compute
(createdAt + window), so the panel now arms a one-shot timer at exactly
the next transition across the fleet — each day-boundary where the label
decrements, and finally the expiry — and recomputes the facet locally
with no network, re-arming for the next boundary. Rows flip at the
moment the state changes; the slow poll is left covering only what
genuinely needs the server (machines created or deleted elsewhere). The
recompute happens above the lazy-list snapshot boundary, so the panel's
snapshot rule (cmux#2586) holds. Boundary math unit-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* cloud: free access window is 7 days

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: run the cmux-tui remote daemon on Blaxel machines beside cmuxd-remote (Phase 1)

Implements the next open item of docs/cloud-cmux-tui-daemon.md (#10800): every
Blaxel machine gets the pinned static-musl cmux-tui (CMUX_VM_BLAXEL_TUI_URL +
_SHA256, verified in-VM, installed on the persistent home volume) running
`server start --remote-ws` under the sandbox supervisor, with its own private
preview. attach-endpoint accepts transport:"cmux-remote" and returns the
tokenized /v1/link route plus a single-use enrollment invitation when the
caller's device is not enrolled; a new cmux-remote/approve route approves the
pending claim the control plane invited. Opt-in per deployment; no change
for clients that do not ask.

Measured on Blaxel: a WebSocket upgrade with the preview token as a query
parameter completes on the raw <hash>.preview.bl.run host but is refused
through the vm.cmux.sh custom domain, so the daemon preview is created
unbranded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: cmux vm shell and the Machines panel open cmux-tui sessions

- `cmux vm tui <id>` and, by default, `cmux vm shell <id>` (which the Machines
  panel launches) open a workspace whose pane runs the local cmux-tui client
  against the machine's authenticated /v1/link route; the hidden
  vm-tui-connect helper hands the terminal to the client and approves the
  device enrollment through the app socket, remembering the device
  fingerprint per machine. The websocket attach remains only for
  deployments without a cmux-tui pin.
- Socket methods vm.cmux_remote_info / vm.cmux_remote_approve and the
  VMClient calls behind them; capabilities list updated.
- With the pin configured, new Blaxel machines get cmux-tui only: no
  cmuxd-remote install or process; the sleep watcher counts cmux-tui's
  terminal children as work.
- Client discovery probes candidates with `remote-probe --json` so the
  SSH-remote bootstrap's shell wrapper at ~/.cmux/bin/cmux is skipped.
- Localized en+ja strings; help, usage contract and docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* blaxel: fresh-machine cmux-tui bootstrap waits for the sandbox API and installs curl

Found by creating a machine under the pin: a just-created sandbox 404s its
API for a few seconds (the cmuxd path only survived because encoding the Go
binary took that long), and a stock blaxel/base-image has no curl until the
background provisioning adds it. The first write now retries until the API
answers, and the installer adds curl via apk or falls back to busybox wget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: cmux-tui is pinned by the published manifest and the app bundles the client

- The Blaxel driver resolves the daemon build from the artifacts manifest
  (rolling latest by default, CMUX_VM_CMUX_TUI_MANIFEST_URL to pin a commit,
  CMUX_VM_CMUX_TUI_ENABLED=0 as the kill switch); the sha256 comes from the
  manifest, never from env. An installed daemon that no longer matches the
  manifest is reinstalled on attach. The endpoint reports the daemon's build
  identity and remote protocol.
- scripts/install-cmux-tui-client.sh bundles a universal, sha256-verified
  cmux-tui client into Contents/Resources/bin like the Ghostty helper;
  reload.sh, ci.yml and release.yml run it. The CLI looks there first and
  checks the client/daemon remote protocol before opening a pane, naming the
  stale side.
- cmux-tui-artifacts.yml publishes on main pushes again so latest/ tracks main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cmux-remote: send a User-Agent on direct WebSocket dials

Hosted ingress in front of cmux Cloud machines (CloudFront on the branded
vm.cmux.sh domain) refuses upgrades that omit User-Agent, and tungstenite
sends none by default, so the daemon route had to fall back to the raw
preview host. Direct dials now carry cmux-tui/<version>; no Origin is set
because the daemon rejects browser-style upgrades. Unit test covers the
header and that the endpoint query (route token, lane) survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* nightly: bundle the cmux-tui client like ci/release do

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: restore the vm tui dispatch, usage, help, and helper access lost in the rebase

The pre-rebase toolchain-fix commit had absorbed these cmux.swift hunks when
it was amended, so dropping it in favor of main's #10820/#10822 dropped
them too: the vm-tui-connect dispatch, tui in every vm usage string and the
help block, and the internal access on applyWindowOrCallerContext /
setTerminalForegroundProcessGroup that CMUXCLI+VMTui.swift needs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: the free access window gates cmux-tui attaches too

openVmCmuxRemote and approveVmCmuxRemoteEnrollment resolve the machine through
requireAccessibleUserVm with the caller's current plan, and both routes map
VmFreeAccessExpiredError to the same 402 upgrade prompt the websocket attach
uses, so a free machine past its window is locked on every transport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cmux-remote: rustfmt

* integration: rustfmt

* cmux-tui: remote-probe advertises direct-ws-user-agent

So a control plane can hand a client the branded machine host only when its
direct WebSocket dials carry a User-Agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: portable sha256sum pin check for cmux-tui install; advertise direct-ws-user-agent capability

The cmux-tui install script used `sha256sum -c -s`; `-s` is BusyBox-only and GNU
coreutils (the xfce-vnc desktop image) rejects it, so every create failed with
`sha256sum: invalid option -- 's'` and POST /api/vm returned 502. Redirect output
instead, which both implementations accept.

Also plumb `clientCapabilities` from the attach request through to the Blaxel
driver so only cmux-tui clients that send a User-Agent get the branded machine host.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: cloud cmux-tui daemon design (from #10800)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: Blaxel machines are cmux-tui only; desktop pane redirects top-level; home volume scales with memory

- Remove cmuxd-remote from the Blaxel driver: no daemon injection, no CMUX_VM_BLAXEL_DAEMON_*,
  no legacy websocket PTY attach, no kill switch. The watcher, previews and revoke paths are
  cmux-tui only; the machine's bare branded host (<machine>.vm.cmux.sh) now belongs to the
  cmux-tui preview. openAttach on Blaxel fails with vm_attach_transport_unsupported (409)
  pointing clients at transport "cmux-remote".
- Desktop wrapper: the noVNC page is a top-level redirect, not an iframe. The gateway's
  bl_preview_token cookie is third-party inside a cross-site frame and WebKit drops it, which
  rendered unstyled noVNC with a dead Connect button. Also opts the page out of prerendering
  (connection()) and drops the nested html/body layout that caused hydration mismatches.
- Home volume: 24 GB plan default machines got a 5 GB disk. Size the volume from memory
  (<=16 GB -> 32 GB, <=32 GB -> 64 GB, else 128 GB); CMUX_VM_BLAXEL_HOME_VOLUME_MB still wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: every Cloud VM open goes through cmux-tui; exec the client in place so the pane can type

- vmOpenShell is the single open path for vm new/fork/restore/shell/attach/base open/base reset,
  the sidebar cloud button and the Machines panel; it opens the cmux-tui workspace first and only
  falls back to websocket/SSH when the control plane reports no cmux-tui at all.
  vm_attach_transport_unsupported is never a fallback signal.
- vm-tui-connect no longer spawns the client and races tcsetpgrp: it starts a detached
  vm-tui-approve helper for the enrollment approval and execs the cmux-tui client in place, so the
  pane's foreground process is the TUI from its first tty read. Intermittent swallowed keystrokes
  came from the client reaching raw mode before the handoff.
- The desktop split re-focuses the terminal surface after opening.
- remote-probe capabilities are forwarded as clientCapabilities so the control plane can hand out
  the branded <machine>.vm.cmux.sh route to clients that send a User-Agent.
- Workspaces carry a cloud VM binding (workspace.cloud_vm_bind) so Base detection works without a
  legacy remote configuration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: provision machines with the standard toolset and agents; clamp home volume to Blaxel's 16 GB ceiling

The old provision step was Alpine-only (apk) and a no-op on the Ubuntu desktop image, so a
machine came with python3 and wget and nothing else. Machines now run a background
provisioning script on every bootstrap: ripgrep/fd/jq/tmux/git/curl/gh/xdotool, node 22,
bun, uv, Claude Code / Codex / OpenCode / Pi (into the persistent /root so they survive
sandbox resurrection), and the CUA driver (cua-computer-server) where the image lacks it.

Blaxel refuses volumes above 16 GB, so the memory-scaled tiers stop there: <=4 GB -> 8 GB,
otherwise 16 GB (the 24 GB plan default previously got 5 GB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* web: desktop wrapper route is never instant-navigated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: free plans read 'N of 1 machine' and show when free cloud access expires

The list payload carries a server-authoritative freeAccessExpiresAt per machine (and the
earliest across them). The Machines panel meter uses singular/plural forms, and free plans get a
banner under the control bar counting down the 7-day window (expires in 6d 23h / expires today /
expired) that opens the existing Pro upgrade flow. cmux vm ls prints the same footer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: shared Cloud tree model (machines → cmux-tui workspaces → terminals, desktop, ports)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: cmux vm tree / open <target> / route / agent, and the cloud-vm skill teaches agents to route work to machines

vm tree shows machines → cmux-tui workspaces → terminals (title, cwd, agent badge, open marker),
desktop and ports; vm open addresses any node (<m>/<ws>/<term>, <m>:desktop, <m>:port/<n>) and
keeps the <id> <port> form; vm route exposes the machine chooser vm run already uses; vm agent runs
Claude Code / Codex / OpenCode / Pi inside the chosen machine's cmux-tui session as a new terminal
that shows up in the tree. vm desktop and the shell's desktop split share one path (vm.desktop_open).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: Finder-like Cloud tree in the right sidebar with drag-to-pane

The Cloud tab is an NSOutlineView: machine → Workspaces (cmux-tui) → terminals (lifecycle,
title, cwd, agent badge, open marker) → Desktop → Ports, with asleep/connecting/error
placeholders, persisted expansion, keyboard navigation, per-node context menus, and a
com.cmux.cloud-surface.transfer drag that drops a terminal, desktop or port as a pane at the
drop position through the same CloudTreeServicing path the CLI uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud: headless cmux-tui links per machine, tree service, and vm.tree/terminal_open/terminal_new/desktop_open/port_open/link_socket

The app keeps one headless 'remote connect --headless --json' link per awake machine (never waking a
sleeping one), reads 'session current snapshot' and follows 'session current events' to build the
Cloud tree, and opens a remote terminal locally as a pane running 'attach --terminal <id>'. Bindings
between local surfaces and remote terminals make terminal_open reuse an open pane. Deleting a
machine now closes its cmux-tui-bound workspace too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: CLIVMTransferTests uses ProcessRunResult; cloud drop handler is main-actor isolated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: resolve the cloud tree service inside the main-actor drop handler

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: cloud tree menu item action runs on the main actor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: Int64 createdAt literals in MachinesPanelModelTests; drop a no-op await in the link manager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: persist the cloud VM workspace binding in the session snapshot

Restored vm:<id> workspaces keep their WorkspaceCloudVMBinding so
workspace(forCloudVMID:), the sidebar cloud button's Base reuse, and
vm.terminal_open find the machine's workspace again after relaunch.
Only the binding is persisted; the pane's one-shot link is not replayed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: cloud VM binding survives the session snapshot round-trip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: clear the five Swift warnings over the CI budget

Three never-mutated vars and an unused optional binding in CLI/cmux.swift and
TerminalController+MobileWorkspaceList.swift, plus the occlusion observer in
GhosttyTerminalView calling a main-actor method from its main-queue closure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cli: opening a machine lands a plain terminal pane; vm tui is the explicit full-client attach

vm new / base open / shell / fork / restore, the sidebar cloud button and the Machines
panel now create a terminal in the machine's cmux-tui session through vm.terminal_new and
show it as a single-terminal pane (attach --terminal), like an ssh session — no cmux-tui
sidebar or tabs in the pane. vm tree renders link state (connecting / asleep / error)
instead of hiding it behind '(none yet)'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud tree: drops honor the drop side; monochrome rows

A drag from the Cloud tree now carries the real Bonsplit destination (pane, orientation,
insert-first → left/right/up/down, or tab index) through CloudTreeOpenTarget into the same
surface.split / surface.create path every other pane drag uses, so dropping on the left edge
splits left instead of always splitting right. vm.terminal_open/desktop_open/port_open accept
pane_id/surface_id/direction/tab_index.

Rows follow the Files sidebar: secondary/tertiary labels and template symbols, one status
dot per machine, a single dim 'CPU · Mem · Disk' line instead of colored gauges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cmux-tui: freeze the operation catalog at 125 after terminal.output_read

#10855 added terminal.output_read to spec/resource-operations-v2.json
without bumping the frozen count in test_check_resource_api_boundary,
so the cmux-tui SDKs and cmux-tui spec checks fail on every merge with
main (125 != 124).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud tree: one row grid — chevron slot, icon column, dot with its own slot, chevron on the name line

Every row lays out as (level+1)×16pt indent → 6pt → 16pt icon slot → 8pt → title, so glyphs form a
column and rows without a chevron reserve its slot. Machine rows put the status dot in its own 10pt
slot and top-align the disclosure with the name line instead of letting it float between the
subtitle lines next to the dot. Trailing markers get a 10pt gap and an 8pt edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud tree: an untitled terminal shows its cwd or 'terminal', never its raw id

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* surfaces: the catalog — terminals, screens and browsers as resources; panes as projections

One @mainactor owner (SurfaceCatalog) holds resource identities (local | cloud machine ×
terminal | screen | browser) and their projections (resource, workspace, panel). Providers
push resources in and materialize panes; project(_:into:) is the single open/reuse path;
projections persist as records and re-resolve when a provider reports the resource again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* surfaces: local + cmux-tui providers, the tree as a catalog view, one drag path, surface.* socket + CLI

- LocalSurfaceProvider registers every terminal/browser pane as a resource; projections are
  recorded at the single panels-will-change seam (add/remove/transfer), moved on tab transfer,
  and persisted (remote ones) as surfaceProjections in the session snapshot; a restored remote
  pane is a placeholder until its provider re-projects it.
- CmuxTuiSurfaceProvider (one per cloud machine, registry-driven) turns the headless link's
  snapshot/events into terminal / screen / port-browser resources and materializes panes through
  SurfacePaneFactory — the one place a SurfaceDestination becomes a pane.
- The right-sidebar tree is a view of SurfaceCatalog.snapshot: This Mac first (terminals grouped
  by local workspace, browsers), then each machine (workspaces → terminals, Desktop, Ports);
  every open is catalog.project; one com.cmux.surface-resource drag/drop path for every row and
  both machines, landing at the drop pane and edge.
- surface.catalog / surface.project / surface.new_terminal socket methods; vm.* wrappers keep their
  shapes; cmux vm tree / surface ls|open|new-terminal on the CLI; vm new/shell/agent create
  terminals through the catalog. CloudTreeService/ServiceAccess/Model are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: machine deletion tells the cmux-tui provider registry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: quote the Workspace+SurfaceCatalog.swift path in the project

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: SurfacePaneFactory imports Bonsplit for PaneID

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: snapshot memberwise argument order

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: agent payload literal is [String: Any]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: refresh task is explicitly Task<Void, Never>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: provider init sets machineID

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* surfaces: socket destinations accept pane/surface handle refs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: irx keepalive waits for the first pong with a deadline instead of a fixed sleep

Inherited from #10782; the test-determinism gate on main flags the sleep-then-assert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* surfaces: resolve split anchors through the workspace's live panes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* surfaces: one catalog change notification per runloop turn

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud tree: cloud machines only for now; no reloads during a drag; coalesced catalog reads

The sidebar shows only cloud machines (This Mac stays in the catalog and behind
CloudTreeNodeBuilder.includesLocalMachine). Catalog changes collapse to one read per runloop
turn, are deferred while a drag is in flight, and update rows in place when the tree structure
is unchanged — a busy remote shell retitling no longer re-runs reloadData under the cursor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: test files import the app as cmux_DEV under the CI scheme; two warnings under budget

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* cloud tree: drop the unused stats gauge view and four orphaned strings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: main-actor tree tests run on the main actor

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant