mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
macOS/onboarding: prompt for remote gateway auth tokens (#43100)
Merged via squash.
Prepared head SHA: 00e2ad847b
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
This commit is contained in:
@@ -348,10 +348,18 @@ struct GeneralSettings: View {
|
||||
Text("Testing…")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
case .ok:
|
||||
Label("Ready", systemImage: "checkmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.green)
|
||||
case let .ok(success):
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Label(success.title, systemImage: "checkmark.circle.fill")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.green)
|
||||
if let detail = success.detail {
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
case let .failed(message):
|
||||
Text(message)
|
||||
.font(.caption)
|
||||
@@ -518,7 +526,7 @@ struct GeneralSettings: View {
|
||||
private enum RemoteStatus: Equatable {
|
||||
case idle
|
||||
case checking
|
||||
case ok
|
||||
case ok(RemoteGatewayProbeSuccess)
|
||||
case failed(String)
|
||||
}
|
||||
|
||||
@@ -558,114 +566,14 @@ extension GeneralSettings {
|
||||
@MainActor
|
||||
func testRemote() async {
|
||||
self.remoteStatus = .checking
|
||||
let settings = CommandResolver.connectionSettings()
|
||||
if self.state.remoteTransport == .direct {
|
||||
let trimmedUrl = self.state.remoteUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedUrl.isEmpty else {
|
||||
self.remoteStatus = .failed("Set a gateway URL first")
|
||||
return
|
||||
}
|
||||
guard Self.isValidWsUrl(trimmedUrl) else {
|
||||
self.remoteStatus = .failed(
|
||||
"Gateway URL must use wss:// for remote hosts (ws:// only for localhost)")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
guard !settings.target.isEmpty else {
|
||||
self.remoteStatus = .failed("Set an SSH target first")
|
||||
return
|
||||
}
|
||||
|
||||
// Step 1: basic SSH reachability check
|
||||
guard let sshCommand = Self.sshCheckCommand(
|
||||
target: settings.target,
|
||||
identity: settings.identity)
|
||||
else {
|
||||
self.remoteStatus = .failed("SSH target is invalid")
|
||||
return
|
||||
}
|
||||
let sshResult = await ShellExecutor.run(
|
||||
command: sshCommand,
|
||||
cwd: nil,
|
||||
env: nil,
|
||||
timeout: 8)
|
||||
|
||||
guard sshResult.ok else {
|
||||
self.remoteStatus = .failed(self.formatSSHFailure(sshResult, target: settings.target))
|
||||
return
|
||||
}
|
||||
switch await RemoteGatewayProbe.run() {
|
||||
case let .ready(success):
|
||||
self.remoteStatus = .ok(success)
|
||||
case let .authIssue(issue):
|
||||
self.remoteStatus = .failed(issue.statusMessage)
|
||||
case let .failed(message):
|
||||
self.remoteStatus = .failed(message)
|
||||
}
|
||||
|
||||
// Step 2: control channel health check
|
||||
let originalMode = AppStateStore.shared.connectionMode
|
||||
do {
|
||||
try await ControlChannel.shared.configure(mode: .remote(
|
||||
target: settings.target,
|
||||
identity: settings.identity))
|
||||
let data = try await ControlChannel.shared.health(timeout: 10)
|
||||
if decodeHealthSnapshot(from: data) != nil {
|
||||
self.remoteStatus = .ok
|
||||
} else {
|
||||
self.remoteStatus = .failed("Control channel returned invalid health JSON")
|
||||
}
|
||||
} catch {
|
||||
self.remoteStatus = .failed(error.localizedDescription)
|
||||
}
|
||||
|
||||
// Restore original mode if we temporarily switched
|
||||
switch originalMode {
|
||||
case .remote:
|
||||
break
|
||||
case .local:
|
||||
try? await ControlChannel.shared.configure(mode: .local)
|
||||
case .unconfigured:
|
||||
await ControlChannel.shared.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private static func isValidWsUrl(_ raw: String) -> Bool {
|
||||
GatewayRemoteConfig.normalizeGatewayUrl(raw) != nil
|
||||
}
|
||||
|
||||
private static func sshCheckCommand(target: String, identity: String) -> [String]? {
|
||||
guard let parsed = CommandResolver.parseSSHTarget(target) else { return nil }
|
||||
let options = [
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "ConnectTimeout=5",
|
||||
"-o", "StrictHostKeyChecking=accept-new",
|
||||
"-o", "UpdateHostKeys=yes",
|
||||
]
|
||||
let args = CommandResolver.sshArguments(
|
||||
target: parsed,
|
||||
identity: identity,
|
||||
options: options,
|
||||
remoteCommand: ["echo", "ok"])
|
||||
return ["/usr/bin/ssh"] + args
|
||||
}
|
||||
|
||||
private func formatSSHFailure(_ response: Response, target: String) -> String {
|
||||
let payload = response.payload.flatMap { String(data: $0, encoding: .utf8) }
|
||||
let trimmed = payload?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.split(whereSeparator: \.isNewline)
|
||||
.joined(separator: " ")
|
||||
if let trimmed,
|
||||
trimmed.localizedCaseInsensitiveContains("host key verification failed")
|
||||
{
|
||||
let host = CommandResolver.parseSSHTarget(target)?.host ?? target
|
||||
return "SSH check failed: Host key verification failed. Remove the old key with " +
|
||||
"`ssh-keygen -R \(host)` and try again."
|
||||
}
|
||||
if let trimmed, !trimmed.isEmpty {
|
||||
if let message = response.message, message.hasPrefix("exit ") {
|
||||
return "SSH check failed: \(trimmed) (\(message))"
|
||||
}
|
||||
return "SSH check failed: \(trimmed)"
|
||||
}
|
||||
if let message = response.message {
|
||||
return "SSH check failed (\(message))"
|
||||
}
|
||||
return "SSH check failed"
|
||||
}
|
||||
|
||||
private func revealLogs() {
|
||||
|
||||
Reference in New Issue
Block a user