Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
bf973f6
Cloud tunnel: vendor WireGuardKit and build the wireguard-go bridge
austinywang Sep 3, 2026
cea4a99
Cloud tunnel: packet-tunnel system extension and on-demand app-manage…
austinywang Sep 3, 2026
d73b0e5
cmux vpn: app-managed shims when the app owns the tunnel
austinywang Sep 3, 2026
164ca89
Release: declare the Cloud tunnel entitlements, reconcile them with t…
austinywang Sep 3, 2026
14ae079
build-wireguard-go: build the pinned module set in readonly mode
austinywang Sep 3, 2026
67dd76e
Merge origin/main into issue-11760-vpn-networkextension
austinywang Sep 3, 2026
ebe4ce3
build-wireguard-go: require Go only for Release builds
austinywang Sep 3, 2026
5d4ce36
CloudTunnelCoordinator: adopt an already-connected tunnel; guard supe…
austinywang Sep 3, 2026
44a81f9
Cloud tunnel: one top-level type per file
austinywang Sep 3, 2026
15f5625
AppDelegate: adopt the three-parameter machine-create launcher closure
austinywang Sep 3, 2026
37f1bd4
VMClientSocketCommands: share socketWorkerString with the tunnel verb…
austinywang Sep 3, 2026
0549cf8
CloudTunnelCoordinator: stop inherited tunnels; back off after a fail…
austinywang Sep 3, 2026
86e79ce
Cloud tunnel: lift nested Timing, Purpose, and the controller error t…
austinywang Sep 3, 2026
c86cb48
CloudTunnelCoordinatorTests: no await inside the ?? autoclosure
austinywang Sep 3, 2026
96c183d
Cloud tunnel: verify the real engine by its Go build info; prune dead…
austinywang Sep 3, 2026
73ad55f
Cloud tunnel: serialize starts behind stops, fail fast on adopted-lin…
austinywang Sep 3, 2026
babe628
Merge origin/main into issue-11760-vpn-networkextension
austinywang Sep 3, 2026
c12b3d8
Fix VPN status variable redeclaration
austinywang Sep 3, 2026
36e4a92
Update Freestyle SDK pin test
austinywang Sep 3, 2026
918f668
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
lawrencecchen Sep 3, 2026
e01cebb
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
lawrencecchen Sep 4, 2026
d25126b
fix(cloud): isolate dev VPN tunnels by build identity
austinywang Sep 3, 2026
41ad9da
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
lawrencecchen Sep 4, 2026
1ba9157
ci: add fast notarized nightly dogfood path
lawrencecchen Sep 4, 2026
eed787f
test(cloud): cover full Mac access revoke
lawrencecchen Sep 4, 2026
1d94964
ci: thin bundled clients in fast nightly builds
lawrencecchen Sep 4, 2026
2f67f34
cloud: model Mac access grants and tunnel roles
lawrencecchen Sep 4, 2026
ceaada0
fix(ci): make tunnel engine verification deterministic
lawrencecchen Sep 4, 2026
e05fa14
test(release): require system-extension-safe app entitlements
lawrencecchen Sep 4, 2026
a49b830
fix(release): sign packet tunnel apps for macOS system extensions
lawrencecchen Sep 4, 2026
5625d2a
test(release): require tunnel profile
lawrencecchen Sep 4, 2026
7de906c
fix(ci): smoke signed nightlies before notarization
lawrencecchen Sep 4, 2026
c9272d3
feat(cloud): use private WireGuard access end to end
lawrencecchen Sep 4, 2026
795f564
Merge remote-tracking branch 'origin/issue-11760-vpn-networkextension…
lawrencecchen Sep 4, 2026
e46babe
style(cmux-tui): format WireGuard transport
lawrencecchen Sep 4, 2026
6568e6a
fix(cli): preserve global socket diagnostics
lawrencecchen Sep 4, 2026
b9d648a
fix(release): keep hardened runtime on tunnel extension
lawrencecchen Sep 4, 2026
8803cfd
Merge remote-tracking branch 'origin/issue-11760-vpn-networkextension…
lawrencecchen Sep 4, 2026
fd2eb0f
test(release): require matching WireGuard client
lawrencecchen Sep 4, 2026
c8ee2f1
Merge remote-tracking branch 'origin/issue-11760-vpn-networkextension…
lawrencecchen Sep 4, 2026
4eee10b
fix(release): pin the private network client
lawrencecchen Sep 4, 2026
918c5a4
fix(dev): reject stale private network clients
lawrencecchen Sep 4, 2026
3926897
fix(ci): allow runner setup before artifact planning
lawrencecchen Sep 4, 2026
fa0a8d7
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
lawrencecchen Sep 4, 2026
0bed349
test(cloud): require private-link port discovery
lawrencecchen Sep 4, 2026
d01b924
test(cmux-tui): require direct port inventory command
lawrencecchen Sep 4, 2026
9073047
fix(cloud): discover ports over the private link
lawrencecchen Sep 4, 2026
9343493
style(cmux-tui): format port inventory command
lawrencecchen Sep 4, 2026
328bebe
test(dev): require immutable client pin
lawrencecchen Sep 4, 2026
87d1338
fix(dev): pin private network client by URL
lawrencecchen Sep 4, 2026
39f6540
fix(ci): generate private link SDK bindings
lawrencecchen Sep 4, 2026
9046bd2
test(cloud): cover Mac access revoke request
lawrencecchen Sep 4, 2026
9400a28
fix(cloud): stop local access on revoke
lawrencecchen Sep 4, 2026
0e43efa
test(cloud): cover tunnel child cleanup
lawrencecchen Sep 4, 2026
76a81f6
fix(cloud): fence tunnel helper lifetimes
lawrencecchen Sep 4, 2026
b64d4ea
test(cloud): model mandatory private networks
lawrencecchen Sep 4, 2026
d184f1a
test(cloud): cover link process teardown
lawrencecchen Sep 4, 2026
dda0e65
fix(cloud): reap link helpers before release
lawrencecchen Sep 4, 2026
c172ed1
test(sdk): track direct metadata command
lawrencecchen Sep 4, 2026
668ec78
test(cloud): cover device mutation fencing
lawrencecchen Sep 4, 2026
a082f1d
fix(cloud): serialize Mac access mutations
lawrencecchen Sep 4, 2026
722bf5e
fix(cloud): cover serial provider deadlines
lawrencecchen Sep 4, 2026
5f72353
docs(cloud): remove obsolete host fallback
lawrencecchen Sep 4, 2026
6ff34d9
ci: decouple branch TUI artifacts from relay audit
lawrencecchen Sep 4, 2026
7055d80
test(cloud): keep tunnel status read-only
lawrencecchen Sep 4, 2026
ab557e4
fix(cloud): keep tunnel status read-only
lawrencecchen Sep 4, 2026
13f9bd8
ci: build fast nightly TUI client in app job
lawrencecchen Sep 4, 2026
b2ba38f
ci: route fast nightly through Blacksmith
lawrencecchen Sep 4, 2026
9769947
test(cloud): cover tunnel error sanitization
lawrencecchen Sep 4, 2026
0109c99
fix(cloud): harden Network Extension lifecycle
lawrencecchen Sep 4, 2026
5ecbe52
fix(cloud): capture tunnel redactor explicitly
lawrencecchen Sep 4, 2026
19404fd
test(ci): accept fast Nightly runner routing
lawrencecchen Sep 4, 2026
d1d03c6
fix(cloud): fail closed on unknown activation results
lawrencecchen Sep 4, 2026
fced8d8
ci: build exact cmux-tui in Blacksmith reloads
lawrencecchen Sep 4, 2026
4cb37f0
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
lawrencecchen Sep 4, 2026
5e17b5a
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
lawrencecchen Sep 4, 2026
a04c14e
fix(cloud): clear new compiler warnings
lawrencecchen Sep 4, 2026
9f5256f
test(cloud): accept legacy tunnel response shape
lawrencecchen Sep 4, 2026
b532202
fix(cloud): tolerate older tunnel response fields
lawrencecchen Sep 4, 2026
2d4c980
ci: use available runner for fast nightly dogfood
lawrencecchen Sep 4, 2026
9cce57c
ci: honor configured runner for fast nightly builds
lawrencecchen Sep 4, 2026
4e895e1
fix(cmux-tui): refresh lockfile for wireguard transport
lawrencecchen Sep 4, 2026
f1f2471
test(cloud): accept digit in WireGuard key padding
lawrencecchen Sep 4, 2026
7d3a160
fix(cloud): accept all canonical WireGuard public keys
lawrencecchen Sep 4, 2026
8930c55
fix(cloud): declare system extension usage description
lawrencecchen Sep 4, 2026
2d05309
ci: use Blacksmith for fast nightly dogfood
lawrencecchen Sep 4, 2026
0ea2dd1
fix pending tunnel consumer and sanitize activation errors
austinywang Sep 4, 2026
0ecbf91
Merge origin/main into cloud tunnel PR branch
austinywang Sep 4, 2026
df6def0
fix malformed localization catalog after main merge
austinywang Sep 4, 2026
c97fa2d
merge main localization catalog without formatting churn
austinywang Sep 4, 2026
eda3854
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
austinywang Sep 4, 2026
e4bdb9e
refresh generated cmux-tui SDK bindings
austinywang Sep 4, 2026
4b2d4fb
fix web tunnel API compatibility after main merge
austinywang Sep 4, 2026
459cb69
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
austinywang Sep 5, 2026
60566b1
fix(cmux-tui): update generated event coverage counts
austinywang Sep 5, 2026
a01780b
Merge remote-tracking branch 'origin/main' into issue-11760-vpn-netwo…
austinywang Sep 5, 2026
a1769af
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 5, 2026
da92086
Merge branch 'main' into issue-11760-vpn-networkextension
austinywang Sep 5, 2026
d9b54c0
Merge branch 'main' of https://github.com/manaflow-ai/cmux into issue…
austinywang Sep 5, 2026
567ae47
fix(ci): align cloud tests with current contracts
austinywang Sep 5, 2026
6cc46b6
Merge remote-tracking branch 'origin/issue-11760-vpn-networkextension…
austinywang Sep 5, 2026
c41c08a
fix(web): align VM tests with private image contracts
austinywang Sep 5, 2026
24bc717
fix(web): split Freestyle remote attach flow
austinywang Sep 5, 2026
6c7b9b3
fix(web): correct Freestyle remote VM helper type
austinywang Sep 5, 2026
6461410
Merge branch 'main' into issue-11760-vpn-networkextension
austinywang Sep 5, 2026
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2308,6 +2308,12 @@ jobs:
- name: Sanitize Swift package cache
run: python3 scripts/ci/sanitize-xcode-source-packages-cache.py .spm-cache

- name: Set up Go for the Cloud tunnel extension
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.26.x'
cache: false

- name: Build universal app (Release)
run: |
set -euo pipefail
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cloud-vm-image-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ concurrency:

jobs:
contract:
runs-on: ubuntu-24.04
runs-on: ${{ vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
defaults:
run:
working-directory: web
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/cmux-tui-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ on:
workflow_dispatch: {}

concurrency:
group: cmux-tui-artifacts-${{ github.ref }}
# Raw immutable objects are write-once. Queue a second dispatch instead of
# cancelling the first half-written publication.
# Main remains serialized because each main run also updates latest/. Branch
# dogfood runs publish commit-addressed objects only, so different commits
# can build in parallel instead of waiting behind an obsolete branch run.
group: cmux-tui-artifacts-${{ github.ref == 'refs/heads/main' && 'main' || github.sha }}
# Raw immutable objects are write-once. Queue a rerun of the same commit
# instead of cancelling a half-written publication.
cancel-in-progress: false

permissions: {}
Expand All @@ -64,6 +67,10 @@ jobs:
version: 0.0.0-r2.sha-${{ github.sha }}
package_npm: false
package_pypi: false
# This lane publishes the raw cmux-tui, cmux-relay, and chatmux-relay
# binaries. The Cloudflare Worker is a separate deployment artifact and
# its npm audit must not block these commit-addressed files.
build_cloudflare_relay: false
# The Rust machine relay has no Windows PTY backend yet. Keep Windows on
# the Node rollback lane until a tested backend exists; do not publish a
# misleading Rust machine package or manifest entry.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cmux-tui-build-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
plan-build:
name: select binary targets
runs-on: ${{ inputs.linux_runner != '' && inputs.linux_runner || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}
timeout-minutes: 5
timeout-minutes: 15
outputs:
matrix: ${{ steps.targets.outputs.matrix }}
linux_package_matrix: ${{ steps.targets.outputs.linux_package_matrix }}
Expand Down
193 changes: 177 additions & 16 deletions .github/workflows/nightly.yml

Large diffs are not rendered by default.

59 changes: 58 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
submodules: recursive

- name: Install zig
Expand Down Expand Up @@ -251,6 +252,13 @@ jobs:
echo "Derived Sparkle public key: $DERIVED_PUBLIC_KEY"
echo "SPARKLE_PUBLIC_KEY=$DERIVED_PUBLIC_KEY" >> "$GITHUB_ENV"

- name: Set up Go for the Cloud tunnel extension
if: steps.guard_release_assets.outputs.skip_all != 'true'
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: '1.26.x'
cache: false

- name: Build universal app (Release)
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
Expand Down Expand Up @@ -316,12 +324,20 @@ jobs:
./scripts/install-prebuilt-ghostty-cli-helper.sh \
ghostty-cli-helper/ghostty \
build-universal/Build/Products/Release/cmux.app
./scripts/install-cmux-tui-client.sh build-universal/Build/Products/Release/cmux.app
cmux_tui_commit="$(git log -1 --format=%H -- cmux-tui ghostty .github/workflows/cmux-tui-artifacts.yml .github/workflows/cmux-tui-build-package.yml)"
test -n "$cmux_tui_commit"
./scripts/install-cmux-tui-client.sh \
build-universal/Build/Products/Release/cmux.app \
--manifest-url "https://files.cmux.com/cmux-tui/${cmux_tui_commit}/manifest.json" \
--expected-commit "$cmux_tui_commit" \
--require-capability wireguard-hub

- name: Verify binary architectures
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
set -euo pipefail
APP="build-universal/Build/Products/Release/cmux.app"
./scripts/normalize-system-extension-bundle.sh "$APP" "7WLXT3NR37.com.cmuxterm.app.tunnel"
APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux"
CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux"
HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty"
Expand All @@ -337,6 +353,12 @@ jobs:
[[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]]
[[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]]
[[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]]
TUNNEL_BINARY="$APP/Contents/Library/SystemExtensions/7WLXT3NR37.com.cmuxterm.app.tunnel.systemextension/Contents/MacOS/cmuxTunnel"
[ -x "$TUNNEL_BINARY" ] || { echo "Cloud tunnel extension binary not found at $TUNNEL_BINARY" >&2; exit 1; }
TUNNEL_ARCHS="$(lipo -archs "$TUNNEL_BINARY")"
echo "Tunnel extension architectures: $TUNNEL_ARCHS"
[[ "$TUNNEL_ARCHS" == *arm64* && "$TUNNEL_ARCHS" == *x86_64* ]]
./scripts/verify-tunnel-extension-engine.sh "$TUNNEL_BINARY"
./scripts/verify-diff-sidecar-artifact.sh "$DIFF_SIDECAR"
[[ "$SDK_VERSION" == 26.* ]]

Expand Down Expand Up @@ -402,10 +424,35 @@ jobs:
fi
cp "$TMP_PROFILE" "$PROFILE_PATH"

- name: Embed Cloud tunnel extension provisioning profile
if: steps.guard_release_assets.outputs.skip_all != 'true'
env:
APPLE_RELEASE_TUNNEL_PROVISIONING_PROFILE_BASE64: ${{ secrets.APPLE_RELEASE_TUNNEL_PROVISIONING_PROFILE_BASE64 }}
run: |
set -euo pipefail
./scripts/ci/embed-tunnel-extension-profile.sh \
"build-universal/Build/Products/Release/cmux.app" \
"7WLXT3NR37.com.cmuxterm.app.tunnel" \
"${APPLE_RELEASE_TUNNEL_PROVISIONING_PROFILE_BASE64:-}"

- name: Strip release binaries
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: ./scripts/strip-release-bundle.sh "build-universal/Build/Products/Release/cmux.app"

- name: Verify Cloud tunnel engine before signing
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
set -euo pipefail
app="build-universal/Build/Products/Release/cmux.app"
sysext="$(find "$app/Contents/Library/SystemExtensions" -mindepth 1 -maxdepth 1 -name '*.systemextension' -print -quit 2>/dev/null || true)"
if [[ -z "$sysext" ]]; then
echo "::error::Cloud tunnel system extension is missing"
exit 1
fi
executable="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$sysext/Contents/Info.plist")"
binary="$sysext/Contents/MacOS/$executable"
./scripts/verify-tunnel-extension-engine.sh "$binary"

- name: Codesign app
if: steps.guard_release_assets.outputs.skip_all != 'true'
env:
Expand All @@ -422,6 +469,16 @@ jobs:
cmux.release.entitlements \
"$APPLE_SIGNING_IDENTITY"

- name: Smoke launch signed app before notarization
if: steps.guard_release_assets.outputs.skip_all != 'true'
run: |
set -euo pipefail
# Catch macOS policy failures before the Apple ticket wait and DMG
# work. Direct execution is isolated from the runner's Dock state.
CMUX_SMOKE_DIRECT_EXEC=1 CMUX_SMOKE_DEBUG_LOGS=1 \
./scripts/smoke-launch-macos-app.sh \
"build-universal/Build/Products/Release/cmux.app"

- name: Notarize app
if: steps.guard_release_assets.outputs.skip_all != 'true'
env:
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/reload-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,57 @@ jobs:
echo "cmux-cua build script absent in source ref; skipping Rust provisioning"
fi
# The app's Cloud machine path requires the wireguard-hub cmux-tui
# capability. Do this before Xcode or cache work, and pass the exact
# source-built binary into reload.sh. A rolling download can lag the app
# source and otherwise fails only after the full macOS build completes.
- name: Install Zig for bundled cmux-tui (macOS)
if: ${{ inputs.platform == 'macos' && hashFiles('cmux-tui/Cargo.toml') != '' }}
run: ./scripts/install-zig-ci.sh

- name: Build and validate exact cmux-tui client (macOS)
if: ${{ inputs.platform == 'macos' && hashFiles('cmux-tui/Cargo.toml') != '' }}
working-directory: cmux-tui
env:
CARGO_TARGET_DIR: ${{ runner.temp }}/cmux-tui-target
CMUX_TUI_DISTRIBUTION_VERSION: 0.0.0-dev-${{ inputs.tag }}
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
source_sha="$(git -C .. rev-parse HEAD)"
ghostty_sha="$(git -C ../ghostty rev-parse HEAD)"
export CMUX_TUI_BUILD_COMMIT="$source_sha"
export CMUX_TUI_GHOSTTY_COMMIT="$ghostty_sha"
target="$(rustc -vV | awk '/^host: / { print $2 }')"
case "$target" in
aarch64-apple-darwin|x86_64-apple-darwin) ;;
*) echo "unsupported macOS Rust host target: $target" >&2; exit 1 ;;
esac
cargo build -p cmux-tui --bin cmux-tui --release --locked --target "$target"
client="$CARGO_TARGET_DIR/$target/release/cmux-tui"
test -x "$client"
probe="$($client remote-probe --json)"
version="$($client --version)"
PROBE="$probe" VERSION="$version" EXPECTED_COMMIT="$source_sha" EXPECTED_GHOSTTY="$ghostty_sha" python3 - <<'PY'
import json
import os
probe = json.loads(os.environ["PROBE"])
version = os.environ["VERSION"]
expected = os.environ["EXPECTED_COMMIT"]
expected_ghostty = os.environ["EXPECTED_GHOSTTY"]
if probe.get("build_identity") != expected:
raise SystemExit(
f"cmux-tui build identity {probe.get('build_identity')!r} != {expected!r}"
)
if "wireguard-hub" not in probe.get("capabilities", []):
raise SystemExit("cmux-tui client lacks wireguard-hub capability")
if expected_ghostty not in version:
raise SystemExit("cmux-tui client does not carry the checked-out Ghostty identity")
PY
echo "CMUX_TUI_CLIENT_LOCAL=$client" >> "$GITHUB_ENV"
echo "cmux-tui ready: $client"
- name: Prepare macOS cache metadata
if: ${{ inputs.platform == 'macos' }}
id: cache_meta
Expand Down
91 changes: 26 additions & 65 deletions CLI/CMUXCLI+VMTui.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import Foundation
/// shares — `cmux vm shell|new|fork|restore|base open|base reset`, the Machines
/// panel, and the sidebar cloud button all land in `openVMTuiWorkspace`.
///
/// The control plane returns a tokenized `/v1/link` route and, for a device that has
/// not enrolled with this machine's daemon yet, a single-use invitation. A workspace
/// The app uses the machine's private `/v1/link` route through its user-space
/// WireGuard hub. Only a device that has not enrolled with this machine's
/// daemon asks the control plane for a single-use invitation. A workspace
/// pane runs the hidden `vm-tui-connect` helper, which hands the terminal to the
/// local cmux-tui client (`remote connect`) and, while the client claims the
/// invitation, asks the control plane to approve the pending enrollment through the
/// app socket. After the first enrollment the device key lives in the client's state
/// directory and later attaches need only a fresh route.
/// app socket. After the first enrollment the device key and private route are
/// local facts. Later attaches make no connection or approval request.
extension CMUXCLI {
struct VMTuiConnectConfig: Codable {
let vmId: String
Expand All @@ -22,6 +23,9 @@ extension CMUXCLI {
let clientPath: String
let stateDir: String
let deviceName: String
/// The app's WireGuard hub socket for a private-network route (`--wireguard-hub`);
/// Required for every Cloud VM route.
var wireguardHubSocket: String? = nil
}

/// How an entrypoint wants the machine's workspace shaped; the session itself is
Expand Down Expand Up @@ -54,9 +58,6 @@ extension CMUXCLI {
let updatedAtUnix: Int
}

static let vmTuiApprovalPollSeconds: TimeInterval = 2
static let vmTuiApprovalTimeoutSeconds: TimeInterval = 5 * 60

static var vmTuiUsage: String {
"""
Usage: cmux vm tui <id> [--window <id|ref|index>]
Expand Down Expand Up @@ -241,12 +242,6 @@ extension CMUXCLI {
/// stay alive until that split lands; it is closed right after.
static let vmPlainTerminalPlaceholderCommand = "sleep 60"

/// The shared cloud open path (`vmOpenShell`) calls this first for every entrypoint.
/// Returns nil only when the control plane says the machine's deployment does not
/// run cmux-tui at all (providers that predate the migration), so the caller may
/// fall back to their transport. Any other failure — including a machine that
/// reports it attaches through cmux-tui only — surfaces as-is; nothing falls back
/// to a websocket attach the backend will refuse.
/// True when `workspaceRaw` (a UUID or handle) is the selected workspace of the
/// window in question. Unknown (socket error, no such workspace) reads as false:
/// when in doubt, do not move focus.
Expand All @@ -260,37 +255,6 @@ extension CMUXCLI {
return candidates.contains { $0.caseInsensitiveCompare(workspaceRaw) == .orderedSame }
}

func openVMShellViaCmuxTuiIfAvailable(
vmId: String,
windowRaw: String?,
options: VMTuiOpenOptions = VMTuiOpenOptions(),
client: SocketClient
) throws -> VMTuiOpenResult? {
do {
return try openVMTuiWorkspace(vmId: vmId, windowRaw: windowRaw, options: options, client: client)
} catch let error as CLIError where Self.isCmuxTuiUnavailable(error) {
return nil
}
}

/// Backend code the control plane returns when a machine refuses the legacy attach
/// because it runs cmux-tui only; it means "use cmux-tui", never "fall back".
static let vmAttachTransportUnsupportedCode = "vm_attach_transport_unsupported"

static func isCmuxTuiUnavailable(_ error: CLIError) -> Bool {
if error.vmBackendCode == vmAttachTransportUnsupportedCode {
return false
}
let text = error.message.lowercased()
if text.contains(vmAttachTransportUnsupportedCode) || text.contains("cmux-tui only") {
return false
}
return text.contains("not enabled for this deployment")
|| text.contains("not supported by this deployment")
|| text.contains("does not run the cmux-tui")
|| text.contains("unknown method")
}

func runVMTuiCommand(rest: [String], windowRaw: String?, client: SocketClient, jsonOutput: Bool) throws {
if rest.contains("--help") || rest.contains("-h") {
print(Self.vmTuiUsage)
Expand Down Expand Up @@ -333,10 +297,8 @@ extension CMUXCLI {
) throws -> VMTuiOpenResult {
let startedAt = Date()
let known = Self.loadVMTuiDevices()[vmId]
// Probe the local client before asking the control plane: what it can do
// (`capabilities`) decides which machine host the route points at. A missing
// client is still only reported once the machine is confirmed reachable
// through cmux-tui, so deployments without the daemon fall back cleanly.
// Probe the local client before asking the app for connection data. Its
// WireGuard capability is mandatory for every Cloud VM route.
let clientPath = locateCmuxTuiClient()
let clientProbe = clientPath.flatMap { Self.cmuxTuiClientProbe(at: $0) }
var infoParams: [String: Any] = ["id": vmId]
Expand Down Expand Up @@ -386,7 +348,8 @@ extension CMUXCLI {
invitationId: invitationId,
clientPath: clientPath,
stateDir: stateDir.path,
deviceName: Self.vmTuiDeviceName()
deviceName: Self.vmTuiDeviceName(),
wireguardHubSocket: info["wireguard_hub_socket"] as? String
)
let configURL = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-vm-tui-\(UUID().uuidString.lowercased()).json")
Expand Down Expand Up @@ -549,6 +512,9 @@ extension CMUXCLI {
if let inviteFilePath, !inviteFilePath.isEmpty {
arguments += ["--invite-file", inviteFilePath]
}
if let hubSocket = config.wireguardHubSocket, !hubSocket.isEmpty {
arguments += ["--wireguard-hub", hubSocket]
}
return arguments
}

Expand Down Expand Up @@ -658,8 +624,8 @@ extension CMUXCLI {
// MARK: - cmux vm-tui-approve --id <vm> --invitation-id <id> [--invite-file <path>] (detached)

/// Approves a pending cmux-tui enrollment through the app while the pane's client
/// claims the invitation. Silent: it owns no terminal. Ends when the claim is
/// approved or `vmTuiApprovalTimeoutSeconds` pass, and deletes the invite file
/// claims the invitation. Silent: it owns no terminal. The app makes one request;
/// the VM waits for the claim on its local daemon socket. The invite file is deleted
/// either way.
func runVMTuiApprove(commandArgs: [String], client: SocketClient) throws {
let (vmIdOpt, rest0) = parseOption(commandArgs, name: "--id")
Expand All @@ -673,20 +639,15 @@ extension CMUXCLI {
try? FileManager.default.removeItem(atPath: inviteFileOpt)
}
}
let deadline = Date().addingTimeInterval(Self.vmTuiApprovalTimeoutSeconds)
while Date() < deadline {
Thread.sleep(forTimeInterval: Self.vmTuiApprovalPollSeconds)
guard let result = try? client.sendV2(
method: "vm.cmux_remote_approve",
params: ["id": vmId, "invitation_id": invitationId],
responseTimeout: 60
) else { continue }
if (result["state"] as? String) == "approved" {
if let fingerprint = result["device_fingerprint"] as? String, !fingerprint.isEmpty {
Self.saveVMTuiDevice(vmId: vmId, deviceFingerprint: fingerprint)
}
return
}
guard let result = try? client.sendV2(
method: "vm.cmux_remote_approve",
params: ["id": vmId, "invitation_id": invitationId],
responseTimeout: 75
) else { return }
if (result["state"] as? String) == "approved",
let fingerprint = result["device_fingerprint"] as? String,
!fingerprint.isEmpty {
Self.saveVMTuiDevice(vmId: vmId, deviceFingerprint: fingerprint)
}
}
}
Expand Down
Loading
Loading