Skip to content

Fall back to SSH when Cloud VM attach endpoint is unavailable - #6079

Merged
lawrencecchen merged 6 commits into
mainfrom
feat-vm-new-polish
Jun 14, 2026
Merged

lawrencecchen merged 6 commits into
mainfrom
feat-vm-new-polish

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • fall back to Freestyle SSH when the preferred WebSocket/RPC attach path fails or lacks daemon metadata
  • clear cmux vm new create idempotency immediately after create succeeds so attach failures do not poison the next create
  • add a Freestyle provider regression test for required-daemon fallback

Benchmark

  • cmux vm new --detach against production: clean samples 2.9s, 9.8s, 2.6s, 2.1s, median 2.8s
  • npx freestyle@latest vm create --snapshot sh-17agfasevrc18c8f15nn: clean samples 1.0s, 4.3s, 2.7s, median 2.7s
  • npx freestyle@latest ... --json currently creates VMs but fails with Converting circular structure to JSON in freestyle@0.1.63

Verification

  • bun test tests/vm-freestyle-provider.test.ts
  • bunx tsc --noEmit
  • ./scripts/reload-cloud.sh --tag vmnew
  • local dev server warmed /, /handler/sign-in, /handler/after-sign-in with final HTTP 200
  • tagged CMUX_TAG=vmnew scripts/cmux-debug-cli.sh vm new opened an Ubuntu shell via SSH fallback; temporary VM and workspaces cleaned up

Production observation

  • current production cmux rpc vm.attach_info {"id":"99neulfwov26elo7oci6","require_daemon":true} still reproduces HTTP 502 operation: openAttach before this PR is deployed

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Medium Risk
Changes VM attach behavior and when short-lived SSH identities are minted; fallback is narrowed to specific failures but wrong classification could still attach over SSH or block valid paths.

Overview
Freestyle openAttach no longer treats requireDaemon: true as the only reason to try SSH after WebSocket attach fails. It now falls back only when the error looks recoverable—missing cmuxd RPC metadata, cmuxd /healthz failures, or fetch failed—and rethrows everything else (e.g. API 401) without minting SSH credentials.

The SSH fallback span records cmux.vm.attach.require_daemon for telemetry. Helpers shouldFallbackAttachToSSH and errorMessage inspect the error and ProviderError cause messages to decide eligibility.

Adds vm-freestyle-provider.test.ts covering health-check/missing-daemon fallback, happy-path WebSocket+daemon, and no-SSH on unexpected errors.

Reviewed by Cursor Bugbot for commit 81c5f35. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Falls back to SSH for VM attach when the Freestyle WebSocket/RPC path is unhealthy or missing daemon metadata, including health-check timeouts, so required-daemon attaches don’t fail hard. Preserves VM create retry idempotency by clearing it only after a successful create, so a failed attach doesn’t affect the next create.

  • Bug Fixes
    • Freestyle openAttach: SSH fallback only for specific errors (missing daemon metadata, cmuxd WebSocket health-check failures or timeouts); no fallback on unrelated errors (e.g., 401). Adds cmux.vm.attach.require_daemon span attribute.
    • cmux CLI: clear create idempotency immediately after create succeeds to preserve retry idempotency. Adds regression tests, including health-check timeout fallback.

Written for commit 580bd15. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced error detection for VM attachment, improving SSH fallback decision logic with more precise condition evaluation.
    • Improved health check error messages for clearer diagnostics when connectivity checks fail.
  • Tests

    • Added comprehensive test coverage for SSH fallback behavior during VM attachment across various failure scenarios.

@vercel

vercel Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 14, 2026 5:57am
cmux-staging Building Building Preview, Comment Jun 14, 2026 5:57am

@coderabbitai

coderabbitai Bot commented Jun 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

FreestyleProvider.openAttach's SSH fallback gate is changed from options.requireDaemon to a new shouldFallbackAttachToSSH(err) helper that inspects error messages for cmuxd/websocket unavailability substrings. A companion errorMessage helper normalizes error values to strings. ensureFreestyleWebSocketHealthy now catches healthz fetch failures and rethrows with a normalized prefix. The span attribute cmux.vm.attach.require_daemon is set as a strict boolean. A new test suite exercises all four fallback scenarios.

Changes

FreestyleProvider SSH Fallback Eligibility

Layer / File(s) Summary
SSH fallback decision logic, helpers, and healthz normalization
web/services/vms/drivers/freestyle.ts
openAttach's catch replaces the options.requireDaemon rethrow gate with shouldFallbackAttachToSSH(err); span attribute cmux.vm.attach.require_daemon is set as options?.requireDaemon === true. New helpers shouldFallbackAttachToSSH and errorMessage check error message substrings for cmuxd/websocket unavailability. ensureFreestyleWebSocketHealthy catches healthz fetch failures and rethrows with a normalized "Freestyle cmuxd websocket health check failed:" prefix.
Tests for openAttach SSH fallback scenarios
web/tests/vm-freestyle-provider.test.ts
Adds mock SSHEndpoint/WebSocketPtyEndpoint fixtures, a TestFreestyleProvider subclass with configurable openWebSocketPty and call-counting openSSH, and four test cases: WebSocket unavailable falls back to SSH, unexpected error rejects without calling SSH, missing daemon metadata falls back to SSH, and present daemon metadata retains WebSocket.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hoppity-hop through the websocket gate,
If cmuxd is missing, I'll SSH — it's fate!
shouldFallbackAttachToSSH sniffs every clue,
errorMessage normalizes the hullabaloo.
Four test cases checked, no surprises remain,
The fallback now flows down the right little lane! 🌿


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error Production code in web/services/vms/drivers/freestyle.ts contains user-facing error messages that violate the rule by exposing upstream vendor names (Freestyle, cmuxd), internal provider-specific... Sanitize error messages: Replace "cmuxd" with generic terms like "Cloud VM service", remove "cmuxd-remote snapshot" details, strip vendor names ("Freestyle") and raw stderr from user-visible messages, keeping details only in internal log...
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately captures the main change: implementing SSH fallback for unavailable Cloud VM attach endpoints.
Description check ✅ Passed Description is mostly complete with Summary, Testing, and Verification sections covering the key changes, test execution, and manual validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Custom check for Swift 6 actor isolation is not applicable; PR only modifies TypeScript files (.ts), not Swift files (.swift).
Cmux Swift Blocking Runtime ✅ Passed No Swift files are modified in this PR. Changes are TypeScript only (web/services/vms/drivers/freestyle.ts and web/tests/vm-freestyle-provider.test.ts). Check is not applicable.
Cmux Expensive Synchronous Load ✅ Passed Custom check for Swift expensive synchronous loaders not applicable; PR contains only TypeScript changes (freestyle.ts, vm-freestyle-provider.test.ts), no Swift code.
Cmux Cache Substitution Correctness ✅ Passed PR changes only affect VM attach/SSH fallback logic with fresh health checks and expiry-validated RPC lease reads. No cache substitution in persistence, history, undo, or snapshot paths detected.
Cmux No Hacky Sleeps ✅ Passed No hacky sleeps, polling, or timing-based race workarounds found. The only timing patterns are legitimate AbortSignal.timeout() cancellation-aware abstractions for fetch operations, which are allowed.
Cmux Algorithmic Complexity ✅ Passed Code introduces only O(1) helpers operating on fixed-size error data (max 2 messages); no loops, rescans, or scalable collections; helpers called once per VM attach, not in batch/hot paths.
Cmux Swift Concurrency ✅ Passed PR contains no Swift code changes; check applies only to Swift files. Changes are TypeScript/JavaScript only (freestyle.ts, vm-freestyle-provider.test.ts).
Cmux Swift @Concurrent ✅ Passed This PR contains only TypeScript/JavaScript changes (web/services/vms/drivers/freestyle.ts and web/tests/vm-freestyle-provider.test.ts) with no Swift files modified. The custom check applies exclus...
Cmux Swift File And Package Boundaries ✅ Passed PR contains only TypeScript/JavaScript changes (web/services/vms/drivers/freestyle.ts and web/tests/vm-freestyle-provider.test.ts); no Swift files modified, making the Swift file/package boundaries...
Cmux Swift Logging ✅ Passed Check is not applicable: PR contains only TypeScript changes (.ts files), while the check applies to Swift (.swift) logging rules.
Cmux Full Internationalization ✅ Passed PR contains only backend service logic changes with internal error messages not exposed to end-users, plus developer-only tests. No user-facing text, API responses, or UI strings added or modified.
Cmux Swiftui State Layout ✅ Passed PR contains only TypeScript/web service changes (freestyle.ts, vm-freestyle-provider.test.ts); no SwiftUI code present. Check is not applicable.
Cmux Architecture Rethink ✅ Passed Custom check requires "Swift architecture changes"; PR contains only TypeScript/JavaScript changes in web VM service. Not applicable.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed This PR contains only TypeScript/JavaScript changes (web/services/vms/drivers/freestyle.ts and web/tests/vm-freestyle-provider.test.ts) with no Swift UI code, so the Swift auxiliary window close-sh...
Cmux Source Artifacts ✅ Passed Both changed files (freestyle.ts and vm-freestyle-provider.test.ts) are hand-written source and test files in appropriate directories, not artifacts like logs, caches, or build output.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-vm-new-polish

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 and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e258674228

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 333 to 334
async (span) => {
recordSpanError(span, err);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep fallback split attaches on SSH

When this fallback branch is reached for a VM that still has a PTY websocket but no daemon, cmux vm new creates an SSH fallback workspace, and runSSHWithOptions stores cmux vm ssh-attach --id … as the reusable terminal command (CLI/cmux.swift:8223-8227). That command calls vm.attach_info without require_daemon and then parses the response as SSH (CLI/cmux.swift:9691-9693), so the next split on that workspace can receive the PTY websocket instead of SSH and fail. Force the reusable attach path to request an SSH endpoint, or otherwise make the fallback decision sticky for the workspace.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Narrows the Freestyle openAttach SSH fallback from a blunt requireDaemon check to a targeted shouldFallbackAttachToSSH predicate that only falls back for health-check failures and missing-daemon metadata, preventing credential minting for unrelated errors like 401s. Also wraps fetch network errors in ensureFreestyleWebSocketHealthy with a consistent prefix so they are correctly classified as fallback-eligible.

  • freestyle.ts: replaces if (options?.requireDaemon) throw err with if (!shouldFallbackAttachToSSH(err)) throw err; adds the cmux.vm.attach.require_daemon span attribute; wraps fetch rejections with a \"Freestyle cmuxd websocket health check failed:\" prefix that the predicate can match.
  • vm-freestyle-provider.test.ts: adds five regression tests for the fallback predicate covering health-check errors, timeout, unexpected errors, and daemon-present happy path.

Confidence Score: 5/5

The fallback narrowing is well-scoped: only the three specific error substrings trigger SSH, and the predicate correctly inspects both the ProviderError message and its cause.

All three fallback paths (missing daemon, health-check non-200, fetch failure) are structurally correct and the predicate correctly handles the ProviderError+cause wrapping that production openWebSocketPty always produces. The test coverage gap does not affect production correctness.

web/tests/vm-freestyle-provider.test.ts — the TestFreestyleProvider override throws plain Errors rather than ProviderError-wrapped ones, so the regression tests exercise a slightly different code path than production.

Important Files Changed

Filename Overview
web/services/vms/drivers/freestyle.ts Replaces the blunt requireDaemon gate on the SSH fallback with shouldFallbackAttachToSSH, which matches specific error substrings (missing daemon, health-check failure, fetch failure). Adds cmux.vm.attach.require_daemon span attribute and wraps fetch network errors in a consistent prefix. Logic is sound.
web/tests/vm-freestyle-provider.test.ts New Bun test file covering SSH fallback scenarios. The TestFreestyleProvider override throws plain Error instead of ProviderError, so tests exercise shouldFallbackAttachToSSH through the non-ProviderError branch; production code always goes through the ProviderError+cause branch, leaving the unexpected error gate under-tested.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant openAttach
    participant openWebSocketPty
    participant ensureWSHealthy as ensureFreestyleWebSocketHealthy
    participant shouldFallback as shouldFallbackAttachToSSH
    participant openSSH

    Caller->>openAttach: "openAttach(vmId, {requireDaemon})"
    openAttach->>openWebSocketPty: openWebSocketPty(vmId)
    openWebSocketPty->>ensureWSHealthy: fetch healthz (10s timeout)
    alt fetch fails / non-200
        ensureWSHealthy-->>openWebSocketPty: Error(health check failed/returned)
        openWebSocketPty-->>openAttach: "ProviderError(cause=healthCheckError)"
        openAttach->>shouldFallback: shouldFallbackAttachToSSH(err)
        shouldFallback-->>openAttach: true
        openAttach->>openSSH: openSSH(vmId)
        openSSH-->>Caller: SSHEndpoint
    else WebSocket ok, no daemon
        openWebSocketPty-->>openAttach: WebSocketPtyEndpoint (no daemon)
        openAttach->>openAttach: throw ProviderError(requires a cmuxd RPC endpoint)
        openAttach->>shouldFallback: shouldFallbackAttachToSSH(err)
        shouldFallback-->>openAttach: true
        openAttach->>openSSH: openSSH(vmId)
        openSSH-->>Caller: SSHEndpoint
    else WebSocket ok, daemon present
        openWebSocketPty-->>openAttach: WebSocketPtyEndpoint (with daemon)
        openAttach-->>Caller: WebSocketPtyEndpoint
    else unexpected error
        openWebSocketPty-->>openAttach: "ProviderError(cause=401 error)"
        openAttach->>shouldFallback: shouldFallbackAttachToSSH(err)
        shouldFallback-->>openAttach: false
        openAttach-->>Caller: throws ProviderError
    end
Loading

Reviews (2): Last reviewed commit: "Handle VM attach health check timeouts" | Re-trigger Greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 76b2d58. Configure here.

Comment thread web/services/vms/drivers/freestyle.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 81c5f355c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +503 to +507
return messages.some((message) =>
message.includes("requires a cmuxd RPC endpoint")
|| message.includes("Freestyle cmuxd websocket health check returned")
|| message.includes("fetch failed")
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat health-check timeouts as SSH-fallback eligible

When the cmuxd /healthz request hangs instead of returning a non-200 response, ensureFreestyleWebSocketHealthy aborts via AbortSignal.timeout(10_000), which rejects as TimeoutError: The operation timed out. in Bun rather than matching any of these substrings. In that common “attach endpoint unavailable” case, openAttach(..., { requireDaemon: true }) still throws instead of falling back to SSH, so the fallback added here only handles returned 5xx/network failures but not hung endpoints.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/tests/vm-freestyle-provider.test.ts`:
- Around line 55-65: The current test in the "falls back to SSH when the
WebSocket health check times out" test validates a raw Error message, but
production code receives a ProviderError with a cause property. Add a second
test case that wraps the websocket timeout error as a cause within a
ProviderError object to verify the fallback to SSH still works when the error is
wrapped. This ensures the actual production path through openAttach (which
receives ProviderError from openWebSocketPty) is properly tested and protected
by assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b099eee8-d693-4fc7-a7f1-3fb63b562e77

📥 Commits

Reviewing files that changed from the base of the PR and between 76b2d58 and 580bd15.

📒 Files selected for processing (2)
  • web/services/vms/drivers/freestyle.ts
  • web/tests/vm-freestyle-provider.test.ts

Comment on lines +55 to +65
test("falls back to SSH when the WebSocket health check times out", async () => {
const provider = new TestFreestyleProvider();
provider.websocketResult = new Error(
"Freestyle cmuxd websocket health check failed: The operation was aborted",
);

const endpoint = await provider.openAttach("vm-1", { requireDaemon: true });

expect(endpoint).toEqual(sshEndpoint);
expect(provider.sshCalls).toBe(1);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add coverage for wrapped ProviderError.cause fallback path.

At Line 55, this test validates message matching on a raw Error, but production openAttach usually receives a ProviderError from openWebSocketPty and relies on inspecting cause. Please add one case that throws a wrapped ProviderError so the real branch is protected.

Proposed test addition
+import { ProviderError } from "../services/vms/drivers/types";
 import type {
   SSHEndpoint,
   WebSocketPtyEndpoint,
 } from "../services/vms/drivers/types";
@@
   test("falls back to SSH when the WebSocket health check times out", async () => {
@@
     expect(provider.sshCalls).toBe(1);
   });
+
+  test("falls back to SSH when health check failure is wrapped in ProviderError cause", async () => {
+    const provider = new TestFreestyleProvider();
+    provider.websocketResult = new ProviderError(
+      "freestyle",
+      "openWebSocketPty(vm-1)",
+      new Error("Freestyle cmuxd websocket health check failed: The operation was aborted"),
+    );
+
+    const endpoint = await provider.openAttach("vm-1", { requireDaemon: true });
+
+    expect(endpoint).toEqual(sshEndpoint);
+    expect(provider.sshCalls).toBe(1);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/tests/vm-freestyle-provider.test.ts` around lines 55 - 65, The current
test in the "falls back to SSH when the WebSocket health check times out" test
validates a raw Error message, but production code receives a ProviderError with
a cause property. Add a second test case that wraps the websocket timeout error
as a cause within a ProviderError object to verify the fallback to SSH still
works when the error is wrapped. This ensures the actual production path through
openAttach (which receives ProviderError from openWebSocketPty) is properly
tested and protected by assertions.

@lawrencecchen
lawrencecchen merged commit 78f2748 into main Jun 14, 2026
21 of 23 checks passed
@lawrencecchen
lawrencecchen deleted the feat-vm-new-polish branch June 14, 2026 05:43
@lawrencecchen
lawrencecchen restored the feat-vm-new-polish branch July 18, 2026 10:24

This branch was successfully deployed

1 active deployment
Preview – cmux — 580bd15f Deployed Jun 14, 2026 by vercel[bot]
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