Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion supacode/Clients/Zmx/ZmxClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,11 @@ nonisolated enum ZmxAttach {
/// reconnect loop (ssh lines included) is single-quoted by `buildCommand`
/// for the local zmx-wrapping `/bin/sh -c`; the ssh lines' inner quoting
/// survives that outer level.
///
/// Ghostty runs a surface command as `bash -c "exec -l <command>"`, so it
/// must lead with an executable. The zmx form leads with the zmx path; the
/// bare loop leads with a `trap` builtin `exec` can't run (#737), so wrap it
/// in `/bin/sh -c` (no leading `exec`, which would break the same way).
static func buildRemoteCommand(
_ launch: RemoteSurfaceLaunch,
localZmxExecutablePath: String?
Expand All @@ -501,7 +506,7 @@ nonisolated enum ZmxAttach {
remoteCommand: posixShellWrapped(remoteReconnectScript(launch))
)
let loop = SSHReconnectLoop.script(connect: connectLine, reconnect: reconnectLine)
guard let localZmxExecutablePath else { return loop }
guard let localZmxExecutablePath else { return "/bin/sh -c " + shellQuote(loop) }
// The local and host-side sessions share the `supa-<surfaceID>` name.
return buildCommand(
executablePath: localZmxExecutablePath,
Expand Down
67 changes: 54 additions & 13 deletions supacodeTests/RemoteSSHCommandTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -538,23 +538,64 @@ struct ZmxAttachRemoteTests {
@Test func buildRemoteCommandFallsBackToBareReconnectLoopWhenLocalZmxUnavailable() {
let launch = makeLaunch(hostPersistenceEnabled: false)
let command = ZmxAttach.buildRemoteCommand(launch, localZmxExecutablePath: nil)
// No local zmx: still a reconnect loop, just without quit persistence.
#expect(
command
== SSHReconnectLoop.script(
connect: SSHCommand.commandLine(
host: launch.host,
remoteCommand: ZmxAttach.posixShellWrapped(ZmxAttach.remoteConnectScript(launch))
),
reconnect: SSHCommand.commandLine(
host: launch.host,
remoteCommand: ZmxAttach.posixShellWrapped(ZmxAttach.remoteReconnectScript(launch))
)
)
let loop = SSHReconnectLoop.script(
connect: SSHCommand.commandLine(
host: launch.host,
remoteCommand: ZmxAttach.posixShellWrapped(ZmxAttach.remoteConnectScript(launch))
),
reconnect: SSHCommand.commandLine(
host: launch.host,
remoteCommand: ZmxAttach.posixShellWrapped(ZmxAttach.remoteReconnectScript(launch))
)
)
// No local zmx: still a reconnect loop, just without quit persistence, but
// wrapped in `/bin/sh -c` so Ghostty's `exec -l <command>` leads with an
// executable and not the loop's opening `trap` builtin (#737).
#expect(command == "/bin/sh -c " + ZmxAttach.shellQuote(loop))
#expect(command.hasPrefix("/bin/sh -c "))
#expect(!command.contains("zmx attach"))
}

@Test func bareReconnectLoopSurvivesGhosttyExecLoginWrapping() async throws {
// Ghostty runs a surface command as `bash -c "exec -l <command>"`, so it
// must lead with an executable. `sh -n` confirms the fallback's outer
// wrapper is balanced (it can't descend into the single-quoted inner loop;
// that template is parse-checked in
// `reconnectLoopScriptsAreValidShAndPassExitCodesThrough`). The benign runs
// below then prove the `/bin/sh -c` prefix lets `exec -l` resolve an
// executable, while the unwrapped loop still dies with `trap: not found`
// (#737).
let launch = makeLaunch(hostPersistenceEnabled: false)
let command = ZmxAttach.buildRemoteCommand(launch, localZmxExecutablePath: nil)
let parse = Process()
parse.executableURL = URL(fileURLWithPath: "/bin/sh")
parse.arguments = ["-n", "-c", command]
try await parse.runToExit()
#expect(parse.terminationStatus == 0, "sh -n rejected: \(command)")

// A stand-in loop with connect lines that exit 0 (non-255, so the retry
// body never runs) exercises the exact `bash -c "exec -l <command>"` shape
// Ghostty applies, isolated from real ssh dialing.
let benignLoop = SSHReconnectLoop.script(connect: "sh -c 'exit 0'", reconnect: "sh -c 'exit 0'")

let wrapped = Process()
wrapped.executableURL = URL(fileURLWithPath: "/bin/bash")
wrapped.arguments = ["--noprofile", "--norc", "-c", "exec -l /bin/sh -c \(ZmxAttach.shellQuote(benignLoop))"]
wrapped.standardOutput = FileHandle.nullDevice
wrapped.standardError = FileHandle.nullDevice
try await wrapped.runToExit()
#expect(wrapped.terminationStatus == 0, "wrapped loop failed under exec -l")

// Control: the unwrapped loop is exactly what broke #737.
let bare = Process()
bare.executableURL = URL(fileURLWithPath: "/bin/bash")
bare.arguments = ["--noprofile", "--norc", "-c", "exec -l \(benignLoop)"]
bare.standardOutput = FileHandle.nullDevice
bare.standardError = FileHandle.nullDevice
try await bare.runToExit()
#expect(bare.terminationStatus == 127, "bare loop unexpectedly survived exec -l")
}

@Test func buildRemoteCommandForwardsUsernameAndPort() {
let command = ZmxAttach.buildRemoteCommand(
makeLaunch(host: RemoteHost(alias: "box", username: "alice", port: 2222)),
Expand Down
Loading