Repository navigation
fix: preserve SSH ProxyCommand child environment #18285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
fa3895b
test: cover SSH ProxyCommand child environment
austinywang 0ba5fbb
fix: preserve environment for SSH ProxyCommand children
austinywang cebdfbc
test: update SSH environment expectation
austinywang 179e370
Merge main (efb680adc206) into codex-17560-ssh-proxy
austinywang 9e3e8bc
test: keep SSH environment assertions private
austinywang 2143957
fix: preserve disabled SSH agent semantics
austinywang e91c211
Merge main (6b02ff8fd332) into codex-17560-ssh-proxy
austinywang 6a58c56
Merge branch 'main' of https://github.com/manaflow-ai/cmux into codex…
austinywang c203403
fix: preserve SSH agent override semantics
austinywang ff585fb
Merge main (5d12d7b4087f) into codex-17560-ssh-proxy
austinywang 73ccd79
fix: compile SSH route identity digest
austinywang da10c53
fix: bind SSH route identity to captured environment
austinywang 8221f0c
fix: prevent SSH agent routes sharing custom masters
austinywang e6db0c8
fix: preserve inherited command environments
austinywang 76b271e
fix: keep preflight agent selection connection-owned
austinywang 2ef5d13
fix: honor explicit disabled SSH agent option
austinywang 6d1401a
test: cover OpenSSH agent option precedence across SSH launches
austinywang 10c98f0
fix: keep SSH config agent precedence during restore
austinywang 79da3b5
test: preserve saved SSH route socket on restore
austinywang 504431b
test: isolate ProxyCommand from runner shell startup
austinywang 98ed64f
fix: preserve SSH route identity across TUI handoff
austinywang b839a9b
Merge main (8aae7c01b0d9) into codex-17560-ssh-proxy
austinywang bc35186
test: retain silent cloud proxy connections
austinywang f6a9713
fix: preserve restored SSH carrier socket
austinywang 7dce358
Revert "test: retain silent cloud proxy connections"
austinywang e8b1f1e
fix: make desktop probe timeout deterministic
austinywang 102f4a6
fix: preserve validated SSH agent during restore
austinywang fd80de2
test: use a live socket for restored SSH agent fallback
austinywang 9f04418
Merge main (c5abdafd61c) into codex-17560-ssh-proxy
austinywang 807e573
Merge main (b9c0111a67bf) into codex-17560-ssh-proxy
austinywang 49464a5
test: cover stale SSH agent restore state
austinywang 90b8793
fix: allow SSH agent restore to recover later
austinywang fee672c
test: exercise stale SSH agent persistence
austinywang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
Packages/macOS/CmuxCloud/Tests/CmuxCloudTests/SSHTuiAgentOptionsTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import CmuxCloud | ||
| import CmuxCore | ||
| import CmuxFoundation | ||
| import Foundation | ||
| import Testing | ||
|
|
||
| @Suite("SSH carrier agent option precedence") | ||
| struct SSHTuiAgentOptionsTests { | ||
| @Test("Auth, preflight, carrier and browser share the marked agent route") | ||
| func markedRouteUsesOneControlPathAcrossLaunches() throws { | ||
| let routeIdentifier = String(repeating: "d", count: 64) | ||
| let connection = connection( | ||
| agent: "/tmp/cmux-route-agent.sock", | ||
| options: ["ProxyCommand=/bin/sh -c true", "__cmux_route_sensitive=\(routeIdentifier)"], | ||
| environment: ["PATH": "/usr/bin", "SSH_AUTH_SOCK": "/tmp/cmux-route-agent.sock"] | ||
| ) | ||
| let paths = sshInvocations(connection).compactMap { arguments in | ||
| arguments.first { $0.hasPrefix("ControlPath=") } | ||
| } | ||
| #expect(paths.count == 4) | ||
| #expect(Set(paths).count == 1) | ||
| } | ||
|
|
||
| @Test("A restored cmux carrier keeps its saved route socket with an agent") | ||
| func restoredCmuxCarrierKeepsSavedControlPath() throws { | ||
| let socketDirectory = try #require(SSHConnectionSharingOptions().controlSocketDirectoryPath) | ||
| let savedPath = socketDirectory + "/" + String(repeating: "b", count: 40) | ||
| var snapshot = SessionRemoteWorkspaceSnapshot( | ||
| transport: .ssh, | ||
| destination: "example.invalid", | ||
| sshOptions: ["ControlPath=\(savedPath)"], | ||
| agentSocketPath: "/tmp/cmux-saved-agent.sock", | ||
| preserveAfterTerminalExit: true | ||
| ) | ||
| snapshot.sshSessionOwner = "cmux-tui" | ||
| var configuration = WorkspaceRemoteConfiguration( | ||
| destination: "example.invalid", port: nil, identityFile: nil, | ||
| sshOptions: ["ControlPath=\(savedPath)"], | ||
| localProxyPort: nil, relayPort: nil, relayID: nil, relayToken: nil, | ||
| localSocketPath: nil, terminalStartupCommand: nil, | ||
| agentSocketPath: "/tmp/cmux-saved-agent.sock", | ||
| preserveAfterTerminalExit: true | ||
| ) | ||
| configuration.restoredSSHSession = snapshot | ||
|
|
||
| let connection = SSHTuiConnection( | ||
| configuration: configuration, | ||
| environment: ["PATH": "/usr/bin", "SSH_AUTH_SOCK": "/tmp/cmux-saved-agent.sock"] | ||
| ) | ||
| #expect(connection.authenticationArguments.contains("ControlPath=\(savedPath)")) | ||
| } | ||
|
|
||
| @Test("A forwarded socket preserves the user's IdentityAgent", arguments: [false, true]) | ||
| func forwardedSocketPreservesIdentityAgent(explicitOption: Bool) throws { | ||
| let connection = connection( | ||
| agent: "/tmp/cmux-caller-environment.sock", | ||
| options: explicitOption ? ["IdentityAgent=/tmp/cmux-option-agent.sock"] : [] | ||
| ) | ||
| let expected = explicitOption ? "/tmp/cmux-option-agent.sock" : "/tmp/cmux-config-agent.sock" | ||
| for arguments in sshInvocations(connection) { | ||
| #expect(try resolvedIdentityAgent(arguments) == expected) | ||
| } | ||
| } | ||
|
|
||
| @Test("Explicit disable preserves IdentityAgent in options and SSH config", arguments: [false, true]) | ||
| func disabledSocketPreservesIdentityAgent(explicitOption: Bool) throws { | ||
| let connection = connection( | ||
| agent: "", | ||
| options: explicitOption ? ["IdentityAgent=/tmp/cmux-option-agent.sock"] : [] | ||
| ) | ||
| for arguments in sshInvocations(connection) { | ||
| let expected = explicitOption ? "/tmp/cmux-option-agent.sock" : "/tmp/cmux-config-agent.sock" | ||
| #expect(try resolvedIdentityAgent(arguments) == expected) | ||
| } | ||
| } | ||
|
|
||
| /// Builds the same socket override the CLI sends to the app. | ||
| private func connection( | ||
| agent: String, | ||
| options: [String], | ||
| environment: [String: String] = [:] | ||
| ) -> SSHTuiConnection { | ||
| SSHTuiConnection(configuration: WorkspaceRemoteConfiguration( | ||
| destination: "example.invalid", port: nil, identityFile: nil, sshOptions: options, | ||
| localProxyPort: nil, relayPort: nil, relayID: nil, relayToken: nil, localSocketPath: nil, | ||
| terminalStartupCommand: nil, agentSocketPath: agent | ||
| ), environment: environment) | ||
| } | ||
|
|
||
| /// Exercises interactive authentication, batch preflight, carrier and browser launches. | ||
| private func sshInvocations(_ connection: SSHTuiConnection) -> [[String]] { | ||
| let carriers = [ | ||
| connection.arguments(stateDirectory: "/tmp/cmux-test", deviceName: "test"), | ||
| connection.browserArguments(stateDirectory: "/tmp/cmux-test") | ||
| ].map { arguments in | ||
| arguments.indices.compactMap { index -> String? in | ||
| guard index > 0, arguments[index - 1] == "--ssh-arg" else { return nil } | ||
| return arguments[index] | ||
| } + [connection.configuration.destination] | ||
| } | ||
| return [Array(connection.authenticationArguments.dropFirst()), | ||
| Array(connection.preflightArguments.dropFirst())] + carriers | ||
| } | ||
|
|
||
| /// Asks system OpenSSH to resolve our isolated configuration without opening a connection. | ||
| private func resolvedIdentityAgent(_ arguments: [String]) throws -> String? { | ||
| let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) | ||
| try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) | ||
| defer { try? FileManager.default.removeItem(at: directory) } | ||
| let config = directory.appendingPathComponent("config") | ||
| try "Host *\n IdentityAgent /tmp/cmux-config-agent.sock\n".write(to: config, atomically: true, encoding: .utf8) | ||
| let process = Process() | ||
| let output = Pipe() | ||
| process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh") | ||
| process.arguments = ["-G", "-F", config.path] + arguments | ||
| process.standardInput = FileHandle.nullDevice | ||
| process.standardError = FileHandle.nullDevice | ||
| process.standardOutput = output | ||
| try process.run() | ||
| let data = output.fileHandleForReading.readDataToEndOfFile() | ||
| process.waitUntilExit() | ||
| try #require(process.terminationStatus == 0) | ||
| return String(decoding: data, as: UTF8.self).split(separator: "\n") | ||
| .first { $0.hasPrefix("identityagent ") } | ||
| .map { String($0.dropFirst("identityagent ".count)) } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.