diff --git a/apps/macos/Sources/Clawdbot/AppState.swift b/apps/macos/Sources/Clawdbot/AppState.swift index 6ccb83369..269cec531 100644 --- a/apps/macos/Sources/Clawdbot/AppState.swift +++ b/apps/macos/Sources/Clawdbot/AppState.swift @@ -403,30 +403,10 @@ final class AppState { self.remoteUrl = remoteUrlText } - let targetMode = desiredMode ?? self.connectionMode - if targetMode == .remote, - remoteTransport != .direct, - let host = AppState.remoteHost(from: remoteUrl) - { - self.updateRemoteTarget(host: host) - } - } - - private func updateRemoteTarget(host: String) { - let trimmed = self.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines) - guard let parsed = CommandResolver.parseSSHTarget(trimmed) else { return } - let trimmedUser = parsed.user?.trimmingCharacters(in: .whitespacesAndNewlines) - let user = (trimmedUser?.isEmpty ?? true) ? nil : trimmedUser - let port = parsed.port - let assembled: String - if let user { - assembled = port == 22 ? "\(user)@\(host)" : "\(user)@\(host):\(port)" - } else { - assembled = port == 22 ? host : "\(host):\(port)" - } - if assembled != self.remoteTarget { - self.remoteTarget = assembled - } + // Note: We intentionally do NOT auto-populate remoteTarget here. + // The SSH target is user-controlled via the UI; auto-populating it from + // gateway.remote.url would override user edits (including clearing the field). + // Initial population happens in init() for first-time setup only. } private func syncGatewayConfigIfNeeded() { diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 18dd423a2..551513a79 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -588,6 +588,11 @@ extension GeneralSettings { } private static func sshCheckCommand(target: String, identity: String) -> [String] { + guard let parsed = CommandResolver.parseSSHTarget(target) else { + // Fallback: pass target as-is (will likely fail, but matches old behavior) + return ["/usr/bin/ssh", target, "echo ok"] + } + var args: [String] = [ "/usr/bin/ssh", "-o", "BatchMode=yes", @@ -595,10 +600,14 @@ extension GeneralSettings { "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", ] + if parsed.port > 0 && parsed.port != 22 { + args.append(contentsOf: ["-p", String(parsed.port)]) + } if !identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { args.append(contentsOf: ["-i", identity]) } - args.append(target) + let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host + args.append(userHost) args.append("echo ok") return args } diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 024c0b1c5..09a5c7ec7 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -2890,6 +2890,8 @@ Auth and Tailscale: Remote client defaults (CLI): - `gateway.remote.url` sets the default Gateway WebSocket URL for CLI calls when `gateway.mode = "remote"`. - `gateway.remote.transport` selects the macOS remote transport (`ssh` default, `direct` for ws/wss). When `direct`, `gateway.remote.url` must be `ws://` or `wss://`. `ws://host` defaults to port `18789`. +- `gateway.remote.sshTarget` sets the SSH target for CLI probes and macOS app tunneling (`user@host` or `user@host:port`; port defaults to 22). +- `gateway.remote.sshIdentity` sets the SSH identity file path for remote connections. - `gateway.remote.token` supplies the token for remote calls (leave unset for no auth). - `gateway.remote.password` supplies the password for remote calls (leave unset for no auth). @@ -2911,6 +2913,23 @@ macOS app behavior: } ``` +SSH tunnel example with non-standard SSH port (macOS app): + +```json5 +{ + gateway: { + mode: "remote", + remote: { + transport: "ssh", + sshTarget: "admin@gateway.example.com:2222", + sshIdentity: "~/.ssh/id_ed25519", + url: "ws://127.0.0.1:18789", + token: "your-token" + } + } +} +``` + Direct transport example (macOS app): ```json5 diff --git a/docs/platforms/mac/remote.md b/docs/platforms/mac/remote.md index 6d36700f9..25fd32650 100644 --- a/docs/platforms/mac/remote.md +++ b/docs/platforms/mac/remote.md @@ -27,7 +27,8 @@ Remote mode supports two transports: 1) Open *Settings → General*. 2) Under **Clawdbot runs**, pick **Remote over SSH** and set: - **Transport**: **SSH tunnel** or **Direct (ws/wss)**. - - **SSH target**: `user@host` (optional `:port`). + - **SSH target**: `user@host` or `user@host:port` (port defaults to 22). + - Example with non-standard SSH port: `admin@gateway.example.com:2222` - If the gateway is on the same LAN and advertises Bonjour, pick it from the discovered list to auto-fill this field. - **Gateway URL** (Direct only): `wss://gateway.example.ts.net` (or `ws://...` for local/LAN). - **Identity file** (advanced): path to your key.