fix(macos): parse SSH port correctly in remote connection test

- Fix "Test Remote" button failing for non-standard SSH ports (e.g., user@host:2222)
  The raw target string was passed directly to SSH, which interpreted host:2222
  as the hostname. Now uses -p flag for the port, matching RemotePortTunnel.swift.

- Fix SSH target field auto-repopulating after being cleared
  The config file watcher would reconstruct the SSH target from gateway.remote.url,
  undoing user edits. Removed auto-population from applyConfigOverrides(); initial
  population still happens in init() for first-time setup only.

- Document gateway.remote.sshTarget and gateway.remote.sshIdentity config keys
- Add non-standard SSH port example to docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kenny Lee 2026-01-26 11:11:15 -08:00
parent a486940781
commit 1e15174a0a
4 changed files with 35 additions and 26 deletions

View File

@ -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() {

View File

@ -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
}

View File

@ -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

View File

@ -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.