fix: use canonical main session keys in apps

This commit is contained in:
Peter Steinberger
2026-01-15 08:57:08 +00:00
parent 5f87f7bbf5
commit b77b47bb98
25 changed files with 294 additions and 64 deletions

View File

@@ -36,7 +36,7 @@ actor MacNodeBridgeSession {
func connect(
endpoint: NWEndpoint,
hello: BridgeHello,
onConnected: (@Sendable (String) async -> Void)? = nil,
onConnected: (@Sendable (String, String?) async -> Void)? = nil,
onDisconnected: (@Sendable (String) async -> Void)? = nil,
onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)
async throws
@@ -98,7 +98,8 @@ actor MacNodeBridgeSession {
let ok = try self.decoder.decode(BridgeHelloOk.self, from: data)
self.state = .connected(serverName: ok.serverName)
self.startPingLoop()
await onConnected?(ok.serverName)
let mainKey = ok.mainSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
await onConnected?(ok.serverName, mainKey?.isEmpty == false ? mainKey : nil)
} else if base.type == "error" {
let err = try self.decoder.decode(BridgeErrorFrame.self, from: data)
self.state = .failed(message: "\(err.code): \(err.message)")

View File

@@ -67,8 +67,11 @@ final class MacNodeModeCoordinator {
try await self.session.connect(
endpoint: endpoint,
hello: hello,
onConnected: { [weak self] serverName in
onConnected: { [weak self] serverName, mainSessionKey in
self?.logger.info("mac node connected to \(serverName, privacy: .public)")
if let mainSessionKey {
await self?.runtime.updateMainSessionKey(mainSessionKey)
}
},
onDisconnected: { reason in
await MacNodeModeCoordinator.handleBridgeDisconnect(reason: reason)

View File

@@ -7,6 +7,7 @@ actor MacNodeRuntime {
private let cameraCapture = CameraCaptureService()
private let makeMainActorServices: () async -> any MacNodeRuntimeMainActorServices
private var cachedMainActorServices: (any MacNodeRuntimeMainActorServices)?
private var mainSessionKey: String = "main"
init(
makeMainActorServices: @escaping () async -> any MacNodeRuntimeMainActorServices = {
@@ -16,6 +17,12 @@ actor MacNodeRuntime {
self.makeMainActorServices = makeMainActorServices
}
func updateMainSessionKey(_ sessionKey: String) {
let trimmed = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
self.mainSessionKey = trimmed
}
func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse {
let command = req.command
if self.isCanvasCommand(command), !Self.canvasEnabled() {
@@ -72,28 +79,32 @@ actor MacNodeRuntime {
let placement = params.placement.map {
CanvasPlacement(x: $0.x, y: $0.y, width: $0.width, height: $0.height)
}
let sessionKey = self.mainSessionKey
try await MainActor.run {
_ = try CanvasManager.shared.showDetailed(
sessionKey: "main",
sessionKey: sessionKey,
target: url,
placement: placement)
}
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdbotCanvasCommand.hide.rawValue:
let sessionKey = self.mainSessionKey
await MainActor.run {
CanvasManager.shared.hide(sessionKey: "main")
CanvasManager.shared.hide(sessionKey: sessionKey)
}
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdbotCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(ClawdbotCanvasNavigateParams.self, from: req.paramsJSON)
let sessionKey = self.mainSessionKey
try await MainActor.run {
_ = try CanvasManager.shared.show(sessionKey: "main", path: params.url)
_ = try CanvasManager.shared.show(sessionKey: sessionKey, path: params.url)
}
return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdbotCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(ClawdbotCanvasEvalParams.self, from: req.paramsJSON)
let sessionKey = self.mainSessionKey
let result = try await CanvasManager.shared.eval(
sessionKey: "main",
sessionKey: sessionKey,
javaScript: params.javaScript)
let payload = try Self.encodePayload(["result": result] as [String: String])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
@@ -109,7 +120,8 @@ actor MacNodeRuntime {
}()
let quality = params?.quality ?? 0.9
let path = try await CanvasManager.shared.snapshot(sessionKey: "main", outPath: nil)
let sessionKey = self.mainSessionKey
let path = try await CanvasManager.shared.snapshot(sessionKey: sessionKey, outPath: nil)
defer { try? FileManager.default.removeItem(atPath: path) }
let data = try Data(contentsOf: URL(fileURLWithPath: path))
guard let image = NSImage(data: data) else {
@@ -319,7 +331,8 @@ actor MacNodeRuntime {
private func handleA2UIReset(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
try await self.ensureA2UIHost()
let json = try await CanvasManager.shared.eval(sessionKey: "main", javaScript: """
let sessionKey = self.mainSessionKey
let json = try await CanvasManager.shared.eval(sessionKey: sessionKey, javaScript: """
(() => {
if (!globalThis.clawdbotA2UI) return JSON.stringify({ ok: false, error: "missing clawdbotA2UI" });
return JSON.stringify(globalThis.clawdbotA2UI.reset());
@@ -358,7 +371,8 @@ actor MacNodeRuntime {
}
})()
"""
let resultJSON = try await CanvasManager.shared.eval(sessionKey: "main", javaScript: js)
let sessionKey = self.mainSessionKey
let resultJSON = try await CanvasManager.shared.eval(sessionKey: sessionKey, javaScript: js)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON)
}
@@ -369,8 +383,9 @@ actor MacNodeRuntime {
NSLocalizedDescriptionKey: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
])
}
let sessionKey = self.mainSessionKey
_ = try await MainActor.run {
try CanvasManager.shared.show(sessionKey: "main", path: a2uiUrl)
try CanvasManager.shared.show(sessionKey: sessionKey, path: a2uiUrl)
}
if await self.isA2UIReady(poll: true) { return }
throw NSError(domain: "Canvas", code: 31, userInfo: [
@@ -389,7 +404,8 @@ actor MacNodeRuntime {
let deadline = poll ? Date().addingTimeInterval(6.0) : Date()
while true {
do {
let ready = try await CanvasManager.shared.eval(sessionKey: "main", javaScript: """
let sessionKey = self.mainSessionKey
let ready = try await CanvasManager.shared.eval(sessionKey: sessionKey, javaScript: """
(() => String(Boolean(globalThis.clawdbotA2UI)))()
""")
let trimmed = ready.trimmingCharacters(in: .whitespacesAndNewlines)