Add remote-aware resume bindings for SSH workspaces - #8441
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis change adds remote execution metadata to surface resume bindings, authenticates relayed resume commands, persists remote launch context, and restores persistent SSH PTY sessions with shell-specific bootstrap commands. End-to-end tests cover relayed registration and persistent restore behavior. ChangesRemote resume lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Sequence Diagram(s)sequenceDiagram
participant Relay
participant WorkspaceRemoteRelayCommandRewriter
participant TerminalController
participant Workspace
participant RemoteShellBootstrap
Relay->>WorkspaceRemoteRelayCommandRewriter: send surface.resume.set payload
WorkspaceRemoteRelayCommandRewriter->>TerminalController: rewrite identifiers and add authentication
TerminalController->>Workspace: validate and register persistent SSH resume binding
Workspace->>RemoteShellBootstrap: build attach command with initial resume command
RemoteShellBootstrap-->>Workspace: execute shell-specific remote resume bootstrap
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (23 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR adds remote-aware SSH resume bindings: a new
Confidence Score: 5/5Safe to merge; all guard chains correctly enforce workspace-ID matching, HMAC authentication, and persistent-SSH configuration prerequisites before registering a remote resume binding. The relay authentication path is correctly symmetric between signing and verification. The SurfaceResumeRemoteContext.matches nil-empty guard fix from the prior review is confirmed. Session-restore refactoring moves restoredRemotePTYSessionID earlier so binding migration works correctly, and the launchFlavor == .local guard suppresses local launches for remote-flavored bindings. One silent-degradation edge case was noted but has no security or correctness impact. No files require special attention. Sources/Workspace.swift has the densest logic change and is worth a careful read during merge. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant R as Remote Shell (relay)
participant RW as WorkspaceRemoteRelayCommandRewriter
participant CS as Control Socket Handler
participant TC as TerminalController
participant WS as Workspace
R->>RW: surface.resume.set (relay command)
RW->>RW: rewriteRemoteRelayCommandLineAndExtractMethod() injects _cmux_remote_workspace_id
RW->>RW: authenticatedRemoteResumeCommandLine() HMAC-SHA256 sign with relay token
RW->>CS: signed surface.resume.set + _cmux_remote_relay_authentication_code
CS->>CS: validate _cmux_remote_workspace_id (UUID)
CS->>TC: controlSurfaceResumeSet(inputs, relayParameters)
TC->>TC: "guard remoteWorkspaceID == target.workspace.id"
TC->>WS: authenticatesRemoteResumeParameters(relayParams, relayToken)
WS-->>TC: Bool (HMAC verify)
TC->>WS: persistentSSHResumeContext(panelID)
WS-->>TC: SurfaceResumeRemoteContext
TC->>TC: binding.registeredForPersistentSSH(context)
TC->>WS: setSurfaceResumeBinding(locatedBinding)
Note over WS: On session restore
WS->>WS: migratingLegacyPersistentSSHResumeBinding()
WS->>WS: persistentSSHResumeCommand() base64 resume cmd
WS->>WS: remotePTYAttachStartupCommand(sessionID, remoteCommand)
WS->>R: ssh-pty-attach --command-b64 resume --require-existing
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant R as Remote Shell (relay)
participant RW as WorkspaceRemoteRelayCommandRewriter
participant CS as Control Socket Handler
participant TC as TerminalController
participant WS as Workspace
R->>RW: surface.resume.set (relay command)
RW->>RW: rewriteRemoteRelayCommandLineAndExtractMethod() injects _cmux_remote_workspace_id
RW->>RW: authenticatedRemoteResumeCommandLine() HMAC-SHA256 sign with relay token
RW->>CS: signed surface.resume.set + _cmux_remote_relay_authentication_code
CS->>CS: validate _cmux_remote_workspace_id (UUID)
CS->>TC: controlSurfaceResumeSet(inputs, relayParameters)
TC->>TC: "guard remoteWorkspaceID == target.workspace.id"
TC->>WS: authenticatesRemoteResumeParameters(relayParams, relayToken)
WS-->>TC: Bool (HMAC verify)
TC->>WS: persistentSSHResumeContext(panelID)
WS-->>TC: SurfaceResumeRemoteContext
TC->>TC: binding.registeredForPersistentSSH(context)
TC->>WS: setSurfaceResumeBinding(locatedBinding)
Note over WS: On session restore
WS->>WS: migratingLegacyPersistentSSHResumeBinding()
WS->>WS: persistentSSHResumeCommand() base64 resume cmd
WS->>WS: remotePTYAttachStartupCommand(sessionID, remoteCommand)
WS->>R: ssh-pty-attach --command-b64 resume --require-existing
Reviews (13): Last reviewed commit: "Fix remote resume verifier assertions" | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator`+Surface3.swift:
- Around line 50-59: Update the invalid workspace ID error in the surface split
validation to use the plain string “Missing or invalid workspace_id” directly
instead of String(localized:). Keep the existing invalid_params response
structure and surrounding validation behavior unchanged.
In `@Sources/RemoteInteractiveShellBootstrapBuilder.swift`:
- Around line 139-170: Add a concise inline comment in the `.bash` resumed-shell
branch of `loginShellLaunchLine`, immediately before the nested command’s
`--rcfile "$CMUX_SHELL_INTEGRATION_DIR/.bashrc"` reference, documenting that the
inner freshly exec’d shell must use the exported variable while the outer shell
uses `$cmux_shell_dir`.
🪄 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: f6ed0281-9af3-4f04-9bb4-415db096b3d1
📒 Files selected for processing (21)
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlCommandCoordinator+Surface3.swiftPackages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceResumeBinding.swiftPackages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceResumeSetInputs.swiftSources/RemoteInteractiveShellBootstrapBuilder.swiftSources/SSHPTYAttachStartupCommandBuilder.swiftSources/SessionPersistence.swiftSources/SessionRemoteWorkspaceSnapshot+Restore.swiftSources/SurfaceResumeBindingSnapshot+Remote.swiftSources/SurfaceResumeLaunchFlavor.swiftSources/SurfaceResumeRemoteContext.swiftSources/TabManager.swiftSources/TerminalController+ControlSurfaceContext.swiftSources/TerminalController+ControlSurfaceContext4.swiftSources/Workspace+PersistentRemotePTYReattach.swiftSources/Workspace+RemoteRelayCommandRewrite.swiftSources/Workspace+RemoteSessionLifecycle.swiftSources/Workspace+RemoteSurfaceResumeBinding.swiftSources/Workspace.swiftSources/WorkspaceRemoteRelayCommandRewriter.swiftcmux.xcodeproj/project.pbxprojcmuxTests/RemoteResumeBindingTests.swift
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/WorkspaceRemoteRelayCommandRewriter.swift (1)
8-29: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winDetect
surface.resume.setvia parsed JSON, not a raw byte substring search.
authenticatesRemoteResume(Line 20) is decided by searching the raw, unparsed command-line bytes for the quoted string"surface.resume.set". This gates whetherremoteWorkspaceIDgets forwarded intoWorkspace.rewriteRemoteRelayCommandLinefor any command whose bytes happen to contain that substring (e.g., embedded in an unrelated field like acommandvalue), even though the actual JSONmethodmight differ.authenticatedRemoteResumeCommandLinebelow already parses the JSON and checksrequest["method"]properly — reusing that parse for the initial gate (instead of a second, less reliable heuristic) would remove this fragility. Also consider renamingauthenticatesRemoteResumesince at that point it only means "looks like a resume-set request," not "has been authenticated."♻️ Sketch: gate on the parsed method instead of a byte search
- let authenticatesRemoteResume = commandLine.range(of: Self.remoteResumeMethodNeedle) != nil + let isSurfaceResumeSet = Self.parsedMethod(of: commandLine) == "surface.resume.set" let rewritten = Workspace.rewriteRemoteRelayCommandLine( commandLine, workspaceAliases: workspaceAliases, surfaceAliases: surfaceAliases, - remoteWorkspaceID: authenticatesRemoteResume ? remoteWorkspaceID : nil + remoteWorkspaceID: isSurfaceResumeSet ? remoteWorkspaceID : nil ) - guard authenticatesRemoteResume else { return rewritten } + guard isSurfaceResumeSet else { return rewritten } return authenticatedRemoteResumeCommandLine(rewritten)🤖 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 `@Sources/WorkspaceRemoteRelayCommandRewriter.swift` around lines 8 - 29, Replace the raw-byte `remoteResumeMethodNeedle` search in `rewriteRemoteRelayCommandLine` with parsed-JSON method detection, reusing the existing parsing and `request["method"]` check from `authenticatedRemoteResumeCommandLine` where practical. Rename `authenticatesRemoteResume` to reflect that it identifies a resume-set request rather than authentication, and ensure only requests whose actual method is `surface.resume.set` pass `remoteWorkspaceID` and receive authentication handling.
🤖 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.
Outside diff comments:
In `@Sources/WorkspaceRemoteRelayCommandRewriter.swift`:
- Around line 8-29: Replace the raw-byte `remoteResumeMethodNeedle` search in
`rewriteRemoteRelayCommandLine` with parsed-JSON method detection, reusing the
existing parsing and `request["method"]` check from
`authenticatedRemoteResumeCommandLine` where practical. Rename
`authenticatesRemoteResume` to reflect that it identifies a resume-set request
rather than authentication, and ensure only requests whose actual method is
`surface.resume.set` pass `remoteWorkspaceID` and receive authentication
handling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 55faee6a-7918-45a7-950e-231955798f7c
📒 Files selected for processing (6)
Sources/RemoteInteractiveShellBootstrapBuilder.swiftSources/SurfaceResumeLaunchFlavor.swiftSources/SurfaceResumeRemoteContext.swiftSources/Workspace+RemoteSessionLifecycle.swiftSources/WorkspaceRemoteRelayCommandRewriter.swiftcmuxTests/RemoteResumeBindingTests.swift
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cmuxTests/RemoteResumeBindingTests.swift (1)
362-376: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove unused parameters from the test helper.
The
workspaceIDandsurfaceIDparameters are explicitly ignored with_ =to suppress compiler warnings. Since the assertions correctly check for the__CMUX_WORKSPACE_ID__and__CMUX_SURFACE_ID__placeholder literals rather than the actual UUIDs, you can safely remove these unused parameters from the helper and its call sites to clean up the code.🧹 Proposed refactoring
- private func expectRemoteResumeBootstrap( - _ command: String, - workspaceID: UUID, - surfaceID: UUID - ) throws { + private func expectRemoteResumeBootstrap( + _ command: String + ) throws { `#expect`(command.contains("export CMUX_SOCKET_PATH=127.0.0.1:\(relayPort)"), "\(command)") `#expect`(command.contains("__CMUX_WORKSPACE_ID__"), "\(command)") `#expect`(command.contains("__CMUX_SURFACE_ID__"), "\(command)") `#expect`(command.contains("/srv/remote project"), "\(command)") `#expect`(command.contains("REMOTE_FLAG=value with spaces"), "\(command)") `#expect`(command.contains("session-remote-7989"), "\(command)") `#expect`(!command.contains("ANTHROPIC_API_KEY"), "\(command)") - _ = workspaceID - _ = surfaceID }Update the call sites accordingly:
- try expectRemoteResumeBootstrap( - liveFirstRemoteCommand, - workspaceID: restoredWorkspace.id, - surfaceID: restoredSurfaceID - ) + try expectRemoteResumeBootstrap(liveFirstRemoteCommand)🤖 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 `@cmuxTests/RemoteResumeBindingTests.swift` around lines 362 - 376, Remove the unused workspaceID and surfaceID parameters from expectRemoteResumeBootstrap, delete the corresponding _ = assignments, and update every call site to pass only the command argument while preserving the existing placeholder assertions.
🤖 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.
Outside diff comments:
In `@cmuxTests/RemoteResumeBindingTests.swift`:
- Around line 362-376: Remove the unused workspaceID and surfaceID parameters
from expectRemoteResumeBootstrap, delete the corresponding _ = assignments, and
update every call site to pass only the command argument while preserving the
existing placeholder assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 83be6e55-ee6b-4c52-8b1e-201b3d10593e
📒 Files selected for processing (5)
Sources/SessionPersistence.swiftSources/SurfaceResumeBindingSnapshot+Remote.swiftSources/Workspace+RemoteSurfaceResumeBinding.swiftSources/Workspace.swiftcmuxTests/RemoteResumeBindingTests.swift
|
Correction after the escaped-method red proof:
Focused final-HEAD reruns are in progress: ShellStartupMatrixTests 29675238385 and RemoteResumeBindingTests 29675238337. |
There was a problem hiding this comment.
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 `@cmuxTests/RemoteResumeBindingTests.swift`:
- Line 350: Remove throws from expectRemoteResumeBootstrap and remove try from
its call sites in cmuxTests/RemoteResumeBindingTests.swift at lines 72, 107, and
129; no other behavior changes are needed.
🪄 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: 91e0701c-5b0d-42d4-9432-f985ec79618e
📒 Files selected for processing (1)
cmuxTests/RemoteResumeBindingTests.swift
…ume-bindings # Conflicts: # Sources/RemoteInteractiveShellBootstrapBuilder.swift # cmux.xcodeproj/project.pbxproj
…ume-bindings # Conflicts: # cmux.xcodeproj/project.pbxproj
Summary\n- persist an explicit local or persistent-SSH resume launch flavor with owning workspace, surface, and PTY session IDs\n- mark relayed SessionStart registrations after restored-ID alias rewriting and expose remote context through surface.resume.get\n- reattach live remote PTYs without duplication, while recreating missing PTYs with the approved resume command executed through the remote shell bootstrap\n- preserve remote cwd/environment sanitization and retarget ownership after restore or surface moves\n\n## Tests and validation\n- added behavior coverage for relayed registration, restored aliases, remote cwd/environment sanitization, persistence, and live-versus-missing PTY restore\n- kept the required two-commit test-then-fix history\n- ./scripts/reload.sh --tag issue-7989-remote-resume-bindings --launch\n- ./scripts/check-pbxproj.sh\n- ./scripts/lint-pbxproj-test-wiring.sh\n- python3 scripts/swift_warning_budget.py --log /tmp/cmux-reload-issue-7989-remote-resume-bindings.log\n- git diff --check\n- Swift parse checks for all changed files\n- cmux-unit build-for-testing succeeded on final HEAD 4e6fc46\n- focused final-HEAD tests passed: 18 tests in RemoteResumeBindingTests, RemoteInitialCommandBootstrapFailureTests, and ShellStartupMatrixTests\n- tagged-runtime managed-SSH verification passed both live-PTY reattach (same PID, zero resume reruns) and missing-PTY recreation (remote-only resume exactly once)\n- binding retargeting, spaced cwd/environment, secret filtering, focus preservation, forged-provenance rejection, and reconnect stability were verified through the tag-bound socket\n- cmux-unit and focused tests were run locally; XCUITests were not required for this socket/remote-runtime path\n\n## Localization\n- no new UI, settings, help, or documentation text\n- the new validation path reuses an existing English/Japanese localized socket error key; the catalog parses successfully\n\nCloses #7989
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Adds remote-aware SSH resume bindings with a persisted launch flavor and HMAC-authenticated relay. Live PTYs reattach; ended/missing PTYs recreate and run the approved resume exactly once via the remote bootstrap, including before unsupported shells. Legacy snapshots without a workspace ID migrate to persistent SSH and retarget on restore/detach (Linear #7989).
New Features
launchFlavor(localorremote_ssh) and expose remote context;surface.resume.getreturnsexecution_location,remote_workspace_id,remote_surface_id,remote_pty_session_id.surface.resume.setaccepts authenticated relayed inputs; the command rewriter injects_cmux_remote_workspace_idplus an HMAC and classifies methods from decoded JSON.remoteCommandso the bootstrap runs the resume only on session recreation; it now runs before unsupported shells and only once.Bug Fixes
_cmux_remote_workspace_idin control socket requests.surface.resume.setusing the relay token while preserving shell selection and single-run resume behavior.Written for commit fefcb08. Summary will update on new commits.
Summary by CodeRabbit
execution_locationand remote linkage identifiers for reconnect-safe retargeting.