mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 14:45:46 +00:00
fix: preserve mobile bootstrap auth fallback (#60238) (thanks @ngutman)
This commit is contained in:
@@ -588,6 +588,31 @@ public actor GatewayChannelActor {
|
||||
scopes: filteredScopes)
|
||||
}
|
||||
|
||||
private func persistIssuedDeviceToken(
|
||||
authSource: GatewayAuthSource,
|
||||
deviceId: String,
|
||||
role: String,
|
||||
token: String,
|
||||
scopes: [String]
|
||||
) {
|
||||
if authSource == .bootstrapToken {
|
||||
guard self.shouldPersistBootstrapHandoffTokens() else {
|
||||
return
|
||||
}
|
||||
self.persistBootstrapHandoffToken(
|
||||
deviceId: deviceId,
|
||||
role: role,
|
||||
token: token,
|
||||
scopes: scopes)
|
||||
return
|
||||
}
|
||||
_ = DeviceAuthStore.storeToken(
|
||||
deviceId: deviceId,
|
||||
role: role,
|
||||
token: token,
|
||||
scopes: scopes)
|
||||
}
|
||||
|
||||
private func handleConnectResponse(
|
||||
_ res: ResponseFrame,
|
||||
identity: DeviceIdentity?,
|
||||
@@ -618,18 +643,21 @@ public actor GatewayChannelActor {
|
||||
} else if let tick = ok.policy["tickIntervalMs"]?.value as? Int {
|
||||
self.tickIntervalMs = Double(tick)
|
||||
}
|
||||
if let auth = ok.auth, let identity, self.shouldPersistBootstrapHandoffTokens() {
|
||||
if let auth = ok.auth, let identity {
|
||||
if let deviceToken = auth["deviceToken"]?.value as? String {
|
||||
let authRole = auth["role"]?.value as? String ?? role
|
||||
let scopes = (auth["scopes"]?.value as? [ProtoAnyCodable])?
|
||||
.compactMap { $0.value as? String } ?? []
|
||||
self.persistBootstrapHandoffToken(
|
||||
self.persistIssuedDeviceToken(
|
||||
authSource: self.lastAuthSource,
|
||||
deviceId: identity.deviceId,
|
||||
role: authRole,
|
||||
token: deviceToken,
|
||||
scopes: scopes)
|
||||
}
|
||||
if let tokenEntries = auth["deviceTokens"]?.value as? [ProtoAnyCodable] {
|
||||
if self.shouldPersistBootstrapHandoffTokens(),
|
||||
let tokenEntries = auth["deviceTokens"]?.value as? [ProtoAnyCodable]
|
||||
{
|
||||
for entry in tokenEntries {
|
||||
guard let rawEntry = entry.value as? [String: ProtoAnyCodable],
|
||||
let deviceToken = rawEntry["deviceToken"]?.value as? String,
|
||||
|
||||
@@ -190,6 +190,7 @@ private actor SeqGapProbe {
|
||||
func value() -> Bool { self.saw }
|
||||
}
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayNodeSessionTests {
|
||||
@Test
|
||||
func scannedSetupCodePrefersBootstrapAuthOverStoredDeviceToken() async throws {
|
||||
@@ -321,7 +322,7 @@ struct GatewayNodeSessionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
func nonBootstrapHelloDoesNotOverwriteStoredDeviceTokens() async throws {
|
||||
func nonBootstrapHelloStoresPrimaryDeviceTokenButNotAdditionalBootstrapTokens() async throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
@@ -374,6 +375,71 @@ struct GatewayNodeSessionTests {
|
||||
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
|
||||
})
|
||||
|
||||
let nodeEntry = try #require(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node"))
|
||||
#expect(nodeEntry.token == "server-node-token")
|
||||
#expect(nodeEntry.scopes == [])
|
||||
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") == nil)
|
||||
|
||||
await gateway.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
func untrustedBootstrapHelloDoesNotPersistBootstrapHandoffTokens() async throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
||||
let previousStateDir = ProcessInfo.processInfo.environment["OPENCLAW_STATE_DIR"]
|
||||
setenv("OPENCLAW_STATE_DIR", tempDir.path, 1)
|
||||
defer {
|
||||
if let previousStateDir {
|
||||
setenv("OPENCLAW_STATE_DIR", previousStateDir, 1)
|
||||
} else {
|
||||
unsetenv("OPENCLAW_STATE_DIR")
|
||||
}
|
||||
try? FileManager.default.removeItem(at: tempDir)
|
||||
}
|
||||
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
let session = FakeGatewayWebSocketSession(helloAuth: [
|
||||
"deviceToken": "untrusted-node-token",
|
||||
"role": "node",
|
||||
"scopes": [],
|
||||
"deviceTokens": [
|
||||
[
|
||||
"deviceToken": "untrusted-operator-token",
|
||||
"role": "operator",
|
||||
"scopes": [
|
||||
"operator.approvals",
|
||||
"operator.read",
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
let gateway = GatewayNodeSession()
|
||||
let options = GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: [],
|
||||
commands: [],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-ios-test",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "iOS Test",
|
||||
includeDeviceIdentity: true)
|
||||
|
||||
try await gateway.connect(
|
||||
url: URL(string: "ws://example.invalid")!,
|
||||
token: nil,
|
||||
bootstrapToken: "fresh-bootstrap-token",
|
||||
password: nil,
|
||||
connectOptions: options,
|
||||
sessionBox: WebSocketSessionBox(session: session),
|
||||
onConnected: {},
|
||||
onDisconnected: { _ in },
|
||||
onInvoke: { req in
|
||||
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
|
||||
})
|
||||
|
||||
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "node") == nil)
|
||||
#expect(DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: "operator") == nil)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import XCTest
|
||||
@testable import OpenClawKit
|
||||
|
||||
@MainActor
|
||||
final class TalkSystemSpeechSynthesizerTests: XCTestCase {
|
||||
func testWatchdogTimeoutDefaultsToLatinProfile() {
|
||||
let timeout = TalkSystemSpeechSynthesizer.watchdogTimeoutSeconds(
|
||||
|
||||
Reference in New Issue
Block a user