Skip to content

Commit c64a306

Browse files
authored
fix(macos): preserve PATH for SSH helpers (#100214)
1 parent 51255f6 commit c64a306

5 files changed

Lines changed: 42 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
3131

3232
### Fixes
3333

34+
- **macOS SSH tunnels:** resolve user-installed SSH `ProxyCommand` helpers through the app's managed PATH while preserving inherited connection environment, so remote aliases work after Finder and sanitized-script launches.
3435
- **Control UI terminal rendering:** adopt the shared `@openclaw/libterminal` browser lifecycle and add Nerd Font fallbacks so icon-enabled shell listings render their glyphs when a compatible local font is installed.
3536
- **Slack transcript history:** let Codex app-server own its persisted assistant replies so Slack does not append redundant delivery-mirror rows, while the Control UI keeps legacy duplicate mirrors hidden.
3637
- **Control UI chat history:** hide redundant channel-final delivery mirrors when the preceding app-server assistant reply already shows the same text.

apps/macos/Sources/OpenClaw/CommandResolver.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ enum CommandResolver {
107107
"/usr/local/bin",
108108
"/usr/bin",
109109
"/bin",
110+
home.appendingPathComponent(".local/bin").path,
110111
]
111112
#if DEBUG
112113
// Dev-only convenience. Avoid project-local PATH hijacking in release builds.
@@ -338,6 +339,15 @@ enum CommandResolver {
338339

339340
// MARK: - SSH helpers
340341

342+
static func sshEnvironment(
343+
base: [String: String] = ProcessInfo.processInfo.environment,
344+
searchPaths: [String]? = nil) -> [String: String]
345+
{
346+
var environment = base
347+
environment["PATH"] = (searchPaths ?? self.preferredPaths()).joined(separator: ":")
348+
return environment
349+
}
350+
341351
private static func sshNodeCommand(subcommand: String, extraArgs: [String], settings: RemoteSettings) -> [String]? {
342352
guard !settings.target.isEmpty else { return nil }
343353
guard let parsed = self.parseSSHTarget(settings.target) else { return nil }

apps/macos/Sources/OpenClaw/RemoteGatewayProbe.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ enum RemoteGatewayProbe {
186186
let sshResult = await ShellExecutor.run(
187187
command: sshCommand,
188188
cwd: nil,
189-
env: nil,
189+
env: CommandResolver.sshEnvironment(),
190190
timeout: 8)
191191
guard sshResult.ok else {
192192
return .failed(self.formatSSHFailure(sshResult, target: settings.target))

apps/macos/Sources/OpenClaw/RemotePortTunnel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ final class RemotePortTunnel: @unchecked Sendable {
109109
let process = Process()
110110
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
111111
process.arguments = args
112+
process.environment = CommandResolver.sshEnvironment()
112113

113114
let pipe = Pipe()
114115
process.standardError = pipe

apps/macos/Tests/OpenClawIPCTests/CommandResolverTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,35 @@ import Testing
187187
#expect(managerIndex < systemIndex)
188188
}
189189

190+
@Test func `preferred paths include local user bin after system bins`() throws {
191+
let home = try makeTempDirForTests()
192+
let localBin = home.appendingPathComponent(".local/bin").path
193+
let paths = CommandResolver.preferredPaths(
194+
home: home,
195+
current: [],
196+
projectRoot: home)
197+
198+
let localIndex = try #require(paths.firstIndex(of: localBin))
199+
let systemIndex = try #require(paths.firstIndex(of: "/bin"))
200+
#expect(localIndex > systemIndex)
201+
#expect(paths.count(where: { $0 == localBin }) == 1)
202+
}
203+
204+
@Test func `SSH environment replaces path without dropping inherited values`() {
205+
let paths = ["/usr/bin", "/bin", "/Users/test/.local/bin", "/opt/homebrew/bin"]
206+
let environment = CommandResolver.sshEnvironment(
207+
base: [
208+
"HOME": "/Users/test",
209+
"PATH": "/stale/path",
210+
"SSH_AUTH_SOCK": "/tmp/ssh-agent.sock",
211+
],
212+
searchPaths: paths)
213+
214+
#expect(environment["PATH"] == paths.joined(separator: ":"))
215+
#expect(environment["HOME"] == "/Users/test")
216+
#expect(environment["SSH_AUTH_SOCK"] == "/tmp/ssh-agent.sock")
217+
}
218+
190219
@Test func `validated CLI preference expires when the app requires a newer version`() throws {
191220
let defaults = self.makeDefaults()
192221
let root = try makeTempDirForTests()

0 commit comments

Comments
 (0)