Skip to content

Commit 3d3e8fe

Browse files
committed
fix(macos): preserve unsupported remote gateway tokens
1 parent 3b7a72b commit 3d3e8fe

File tree

7 files changed

+190
-55
lines changed

7 files changed

+190
-55
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
1111
- Talk mode: add top-level `talk.silenceTimeoutMs` config so Talk waits a configurable amount of silence before auto-sending the current transcript, while keeping each platform's existing default pause window when unset. (#39607) Thanks @danodoesdesign. Fixes #17147.
1212
- CLI/install: include the short git commit hash in `openclaw --version` output when metadata is available, and keep installer version checks compatible with the decorated format. (#39712) thanks @sourman.
1313
- Docs/Web search: restore $5/month free-credit details, replace defunct "Data for Search"/"Data for AI" plan names with current "Search" plan, and note legacy subscription validity in Brave setup docs. Follows up on #26860. (#40111) Thanks @remusao.
14+
- macOS/onboarding: add a remote gateway token field for remote mode, preserve existing non-plaintext `gateway.remote.token` config values until explicitly replaced, and warn when the loaded token shape cannot be used directly from the macOS app. (#34614)
1415

1516
### Fixes
1617

apps/macos/Sources/OpenClaw/AppState.swift

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import SwiftUI
99
final class AppState {
1010
private let isPreview: Bool
1111
private var isInitializing = true
12+
private var isApplyingRemoteTokenConfig = false
1213
private var configWatcher: ConfigFileWatcher?
1314
private var suppressVoiceWakeGlobalSync = false
1415
private var voiceWakeGlobalSyncTask: Task<Void, Never>?
@@ -214,9 +215,17 @@ final class AppState {
214215
}
215216

216217
var remoteToken: String {
217-
didSet { self.syncGatewayConfigIfNeeded() }
218+
didSet {
219+
guard !self.isApplyingRemoteTokenConfig else { return }
220+
self.remoteTokenDirty = true
221+
self.remoteTokenUnsupported = false
222+
self.syncGatewayConfigIfNeeded()
223+
}
218224
}
219225

226+
private(set) var remoteTokenDirty = false
227+
private(set) var remoteTokenUnsupported = false
228+
220229
var remoteIdentity: String {
221230
didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } }
222231
}
@@ -285,6 +294,7 @@ final class AppState {
285294

286295
let configRoot = OpenClawConfigFile.loadDict()
287296
let configRemoteUrl = GatewayRemoteConfig.resolveUrlString(root: configRoot)
297+
let configRemoteToken = GatewayRemoteConfig.resolveTokenValue(root: configRoot)
288298
let configRemoteTransport = GatewayRemoteConfig.resolveTransport(root: configRoot)
289299
let resolvedConnectionMode = ConnectionModeResolver.resolve(root: configRoot).mode
290300
self.remoteTransport = configRemoteTransport
@@ -301,7 +311,9 @@ final class AppState {
301311
self.remoteTarget = storedRemoteTarget
302312
}
303313
self.remoteUrl = configRemoteUrl ?? ""
304-
self.remoteToken = GatewayRemoteConfig.resolveTokenString(root: configRoot) ?? ""
314+
self.remoteToken = configRemoteToken.textFieldValue
315+
self.remoteTokenDirty = false
316+
self.remoteTokenUnsupported = configRemoteToken.isUnsupportedNonString
305317
self.remoteIdentity = UserDefaults.standard.string(forKey: remoteIdentityKey) ?? ""
306318
self.remoteProjectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey) ?? ""
307319
self.remoteCliPath = UserDefaults.standard.string(forKey: remoteCliPathKey) ?? ""
@@ -379,14 +391,29 @@ final class AppState {
379391
return false
380392
}
381393

394+
private func applyRemoteTokenState(_ tokenValue: GatewayRemoteConfig.TokenValue) {
395+
let nextToken = tokenValue.textFieldValue
396+
let unsupported = tokenValue.isUnsupportedNonString
397+
guard self.remoteToken != nextToken || self.remoteTokenDirty || self.remoteTokenUnsupported != unsupported
398+
else {
399+
return
400+
}
401+
self.isApplyingRemoteTokenConfig = true
402+
self.remoteToken = nextToken
403+
self.isApplyingRemoteTokenConfig = false
404+
self.remoteTokenDirty = false
405+
self.remoteTokenUnsupported = unsupported
406+
}
407+
382408
private static func updatedRemoteGatewayConfig(
383409
current: [String: Any],
384410
transport: RemoteTransport,
385411
remoteUrl: String,
386412
remoteHost: String?,
387413
remoteTarget: String,
388414
remoteIdentity: String,
389-
remoteToken: String) -> (remote: [String: Any], changed: Bool)
415+
remoteToken: String,
416+
remoteTokenDirty: Bool) -> (remote: [String: Any], changed: Bool)
390417
{
391418
var remote = current
392419
var changed = false
@@ -423,7 +450,9 @@ final class AppState {
423450
changed = Self.updateGatewayString(&remote, key: "sshIdentity", value: remoteIdentity) || changed
424451
}
425452

426-
changed = Self.updateGatewayString(&remote, key: "token", value: remoteToken) || changed
453+
if remoteTokenDirty {
454+
changed = Self.updateGatewayString(&remote, key: "token", value: remoteToken) || changed
455+
}
427456

428457
return (remote, changed)
429458
}
@@ -447,7 +476,7 @@ final class AppState {
447476
let gateway = root["gateway"] as? [String: Any]
448477
let modeRaw = (gateway?["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
449478
let remoteUrl = GatewayRemoteConfig.resolveUrlString(root: root)
450-
let remoteToken = GatewayRemoteConfig.resolveTokenString(root: root) ?? ""
479+
let remoteToken = GatewayRemoteConfig.resolveTokenValue(root: root)
451480
let hasRemoteUrl = !(remoteUrl?
452481
.trimmingCharacters(in: .whitespacesAndNewlines)
453482
.isEmpty ?? true)
@@ -479,9 +508,7 @@ final class AppState {
479508
if remoteUrlText != self.remoteUrl {
480509
self.remoteUrl = remoteUrlText
481510
}
482-
if remoteToken != self.remoteToken {
483-
self.remoteToken = remoteToken
484-
}
511+
self.applyRemoteTokenState(remoteToken)
485512

486513
let targetMode = desiredMode ?? self.connectionMode
487514
if targetMode == .remote,
@@ -515,7 +542,8 @@ final class AppState {
515542
remoteTarget: String,
516543
remoteIdentity: String,
517544
remoteUrl: String,
518-
remoteToken: String) -> (root: [String: Any], changed: Bool)
545+
remoteToken: String,
546+
remoteTokenDirty: Bool) -> (root: [String: Any], changed: Bool)
519547
{
520548
var root = currentRoot
521549
var gateway = root["gateway"] as? [String: Any] ?? [:]
@@ -551,7 +579,8 @@ final class AppState {
551579
remoteHost: remoteHost,
552580
remoteTarget: remoteTarget,
553581
remoteIdentity: remoteIdentity,
554-
remoteToken: remoteToken)
582+
remoteToken: remoteToken,
583+
remoteTokenDirty: remoteTokenDirty)
555584
if updated.changed {
556585
gateway["remote"] = updated.remote
557586
changed = true
@@ -577,6 +606,7 @@ final class AppState {
577606
let remoteTransport = self.remoteTransport
578607
let remoteUrl = self.remoteUrl
579608
let remoteToken = self.remoteToken
609+
let remoteTokenDirty = self.remoteTokenDirty
580610

581611
Task { @MainActor in
582612
// Keep app-only connection settings local to avoid overwriting remote gateway config.
@@ -587,7 +617,8 @@ final class AppState {
587617
remoteTarget: remoteTarget,
588618
remoteIdentity: remoteIdentity,
589619
remoteUrl: remoteUrl,
590-
remoteToken: remoteToken)
620+
remoteToken: remoteToken,
621+
remoteTokenDirty: remoteTokenDirty)
591622
guard synced.changed else { return }
592623
OpenClawConfigFile.saveDict(synced.root)
593624
}
@@ -750,7 +781,8 @@ extension AppState {
750781
remoteHost: String?,
751782
remoteTarget: String,
752783
remoteIdentity: String,
753-
remoteToken: String) -> [String: Any]
784+
remoteToken: String,
785+
remoteTokenDirty: Bool) -> [String: Any]
754786
{
755787
Self.updatedRemoteGatewayConfig(
756788
current: current,
@@ -759,7 +791,8 @@ extension AppState {
759791
remoteHost: remoteHost,
760792
remoteTarget: remoteTarget,
761793
remoteIdentity: remoteIdentity,
762-
remoteToken: remoteToken).remote
794+
remoteToken: remoteToken,
795+
remoteTokenDirty: remoteTokenDirty).remote
763796
}
764797

765798
static func _testSyncedGatewayRoot(
@@ -769,7 +802,8 @@ extension AppState {
769802
remoteTarget: String,
770803
remoteIdentity: String,
771804
remoteUrl: String,
772-
remoteToken: String) -> [String: Any]
805+
remoteToken: String,
806+
remoteTokenDirty: Bool) -> [String: Any]
773807
{
774808
Self.syncedGatewayRoot(
775809
currentRoot: currentRoot,
@@ -778,7 +812,8 @@ extension AppState {
778812
remoteTarget: remoteTarget,
779813
remoteIdentity: remoteIdentity,
780814
remoteUrl: remoteUrl,
781-
remoteToken: remoteToken).root
815+
remoteToken: remoteToken,
816+
remoteTokenDirty: remoteTokenDirty).root
782817
}
783818
}
784819
#endif

apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,7 @@ actor GatewayEndpointStore {
188188

189189
private static func resolveConfigToken(isRemote: Bool, root: [String: Any]) -> String? {
190190
if isRemote {
191-
if let gateway = root["gateway"] as? [String: Any],
192-
let remote = gateway["remote"] as? [String: Any],
193-
let token = remote["token"] as? String
194-
{
195-
return token.trimmingCharacters(in: .whitespacesAndNewlines)
196-
}
197-
return nil
191+
return GatewayRemoteConfig.resolveTokenString(root: root)
198192
}
199193

200194
if let gateway = root["gateway"] as? [String: Any],

apps/macos/Sources/OpenClaw/GatewayRemoteConfig.swift

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ import Foundation
22
import OpenClawKit
33

44
enum GatewayRemoteConfig {
5+
enum TokenValue: Equatable {
6+
case missing
7+
case plaintext(String)
8+
case unsupportedNonString
9+
10+
var textFieldValue: String {
11+
switch self {
12+
case let .plaintext(token):
13+
token
14+
case .missing, .unsupportedNonString:
15+
""
16+
}
17+
}
18+
19+
var isUnsupportedNonString: Bool {
20+
if case .unsupportedNonString = self {
21+
return true
22+
}
23+
return false
24+
}
25+
}
26+
527
static func resolveTransport(root: [String: Any]) -> AppState.RemoteTransport {
628
guard let gateway = root["gateway"] as? [String: Any],
729
let remote = gateway["remote"] as? [String: Any],
@@ -24,15 +46,27 @@ enum GatewayRemoteConfig {
2446
return trimmed.isEmpty ? nil : trimmed
2547
}
2648

27-
static func resolveTokenString(root: [String: Any]) -> String? {
49+
static func resolveTokenValue(root: [String: Any]) -> TokenValue {
2850
guard let gateway = root["gateway"] as? [String: Any],
2951
let remote = gateway["remote"] as? [String: Any],
30-
let tokenRaw = remote["token"] as? String
52+
let tokenRaw = remote["token"]
3153
else {
32-
return nil
54+
return .missing
55+
}
56+
guard let tokenString = tokenRaw as? String else {
57+
return .unsupportedNonString
58+
}
59+
let trimmed = tokenString.trimmingCharacters(in: .whitespacesAndNewlines)
60+
return trimmed.isEmpty ? .missing : .plaintext(trimmed)
61+
}
62+
63+
static func resolveTokenString(root: [String: Any]) -> String? {
64+
switch self.resolveTokenValue(root: root) {
65+
case let .plaintext(token):
66+
token
67+
case .missing, .unsupportedNonString:
68+
nil
3369
}
34-
let trimmed = tokenRaw.trimmingCharacters(in: .whitespacesAndNewlines)
35-
return trimmed.isEmpty ? nil : trimmed
3670
}
3771

3872
static func resolveGatewayUrl(root: [String: Any]) -> URL? {

apps/macos/Sources/OpenClaw/GeneralSettings.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,21 @@ struct GeneralSettings: View {
298298
Text("Gateway token")
299299
.font(.callout.weight(.semibold))
300300
.frame(width: self.remoteLabelWidth, alignment: .leading)
301-
SecureField("remote gateway auth token (gateway.auth.token)", text: self.$state.remoteToken)
301+
SecureField("remote gateway auth token (gateway.remote.token)", text: self.$state.remoteToken)
302302
.textFieldStyle(.roundedBorder)
303303
.frame(maxWidth: .infinity)
304304
}
305305
Text("Used when the remote gateway requires token auth.")
306306
.font(.caption)
307307
.foregroundStyle(.secondary)
308308
.padding(.leading, self.remoteLabelWidth + 10)
309+
if self.state.remoteTokenUnsupported {
310+
Text(
311+
"The current gateway.remote.token value is not plain text. OpenClaw for macOS cannot use it directly; enter a plaintext token here to replace it.")
312+
.font(.caption)
313+
.foregroundStyle(.orange)
314+
.padding(.leading, self.remoteLabelWidth + 10)
315+
}
309316
}
310317
}
311318

apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,21 @@ extension OnboardingView {
203203
Text("Gateway token")
204204
.font(.callout.weight(.semibold))
205205
.frame(width: labelWidth, alignment: .leading)
206-
SecureField("remote gateway auth token (gateway.auth.token)", text: self.$state.remoteToken)
206+
SecureField("remote gateway auth token (gateway.remote.token)", text: self.$state.remoteToken)
207207
.textFieldStyle(.roundedBorder)
208208
.frame(width: fieldWidth)
209209
}
210+
if self.state.remoteTokenUnsupported {
211+
GridRow {
212+
Text("")
213+
.frame(width: labelWidth, alignment: .leading)
214+
Text(
215+
"The current gateway.remote.token value is not plain text. OpenClaw for macOS cannot use it directly; enter a plaintext token here to replace it.")
216+
.font(.caption)
217+
.foregroundStyle(.orange)
218+
.frame(width: fieldWidth, alignment: .leading)
219+
}
220+
}
210221
if self.state.remoteTransport == .direct {
211222
GridRow {
212223
Text("Gateway URL")

0 commit comments

Comments
 (0)