diff --git a/apps/macos/Sources/OpenClaw/GatewayConnection.swift b/apps/macos/Sources/OpenClaw/GatewayConnection.swift index f7509236dc..a76fc72f0f 100644 --- a/apps/macos/Sources/OpenClaw/GatewayConnection.swift +++ b/apps/macos/Sources/OpenClaw/GatewayConnection.swift @@ -360,11 +360,12 @@ actor GatewayConnection { await client.shutdown() } self.lastSnapshot = nil + let resolvedSessionBox = self.sessionBox ?? Self.buildSessionBox(url: url) self.client = GatewayChannelActor( url: url, token: token, password: password, - session: self.sessionBox, + session: resolvedSessionBox, pushHandler: { [weak self] push in await self?.handle(push: push) }) @@ -380,6 +381,21 @@ actor GatewayConnection { private static func defaultConfigProvider() async throws -> Config { try await GatewayEndpointStore.shared.requireConfig() } + + private static func buildSessionBox(url: URL) -> WebSocketSessionBox? { + guard url.scheme?.lowercased() == "wss" else { return nil } + let host = url.host ?? "gateway" + let port = url.port ?? 443 + let stableID = "\(host):\(port)" + let stored = GatewayTLSStore.loadFingerprint(stableID: stableID) + let params = GatewayTLSParams( + required: true, + expectedFingerprint: stored, + allowTOFU: true, + storeKey: stableID) + let session = GatewayTLSPinningSession(params: params) + return WebSocketSessionBox(session: session) + } } // MARK: - Typed gateway API diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift index 736ef0f720..676b10abb1 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift @@ -428,9 +428,7 @@ public actor GatewayChannelActor { guard let self else { return } await self.watchTicks() } - if let pushHandler = self.pushHandler { - Task { await pushHandler(.snapshot(ok)) } - } + await self.pushHandler?(.snapshot(ok)) } private func listen() { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index dbc7dba3d6..5aa1021919 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -16,7 +16,6 @@ public actor GatewayNodeSession { private let logger = Logger(subsystem: "ai.openclaw", category: "node.gateway") private let decoder = JSONDecoder() private let encoder = JSONEncoder() - private static let defaultInvokeTimeoutMs = 30_000 private var channel: GatewayChannelActor? private var activeURL: URL? private var activeToken: String? @@ -36,8 +35,8 @@ public actor GatewayNodeSession { ) async -> BridgeInvokeResponse { let timeoutLogger = Logger(subsystem: "ai.openclaw", category: "node.gateway") let timeout: Int = { - if let timeoutMs { return max(0, timeoutMs) } - return Self.defaultInvokeTimeoutMs + guard let timeoutMs else { return 0 } + return max(0, timeoutMs) }() guard timeout > 0 else { return await onInvoke(request) @@ -154,8 +153,10 @@ public actor GatewayNodeSession { do { try await channel.connect() - _ = await self.waitForSnapshot(timeoutMs: 500) - await self.notifyConnectedIfNeeded() + let snapshotReady = await self.waitForSnapshot(timeoutMs: 500) + if snapshotReady { + await self.notifyConnectedIfNeeded() + } } catch { await onDisconnected(error.localizedDescription) throw error diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift index a0cbcd375f..eae636fd45 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayTLSPinning.swift @@ -73,6 +73,11 @@ public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLS if let expected { if fingerprint == expected { completionHandler(.useCredential, URLCredential(trust: trust)) + } else if params.allowTOFU { + if let storeKey = params.storeKey { + GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey) + } + completionHandler(.useCredential, URLCredential(trust: trust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) } diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index 1528726b8d..d3d77b89fe 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -454,7 +454,7 @@ export function createNodesTool(options?: { invokeParams = JSON.parse(invokeParamsJson); } catch (err) { const message = err instanceof Error ? err.message : String(err); - throw new Error(`invokeParamsJson must be valid JSON: ${message}`); + throw new Error(`invokeParamsJson must be valid JSON: ${message}`, { cause: err }); } } const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs); diff --git a/src/auto-reply/reply/commands-ptt.ts b/src/auto-reply/reply/commands-ptt.ts index bbf415a642..1e4847de3b 100644 --- a/src/auto-reply/reply/commands-ptt.ts +++ b/src/auto-reply/reply/commands-ptt.ts @@ -187,10 +187,7 @@ export const handlePTTCommand: CommandHandler = async (params, allowTextCommands params: invokeParams, config: cfg, }); - const payload = - res.payload && typeof res.payload === "object" - ? (res.payload as Record) - : {}; + const payload = res.payload && typeof res.payload === "object" ? res.payload : {}; const lines = [`PTT ${actionKey} → ${nodeId}`]; if (typeof payload.status === "string") {