Skip to content

Commit 94d8391

Browse files
authored
[codex] restore QR bootstrap operator handoff (#83684)
Merged via squash. Prepared head SHA: 2dc955c Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com> Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com> Reviewed-by: @ngutman
1 parent e00cb66 commit 94d8391

24 files changed

Lines changed: 509 additions & 148 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
3030
- WhatsApp: clarify inbound group diagnostics so observed but unregistered groups point to `channels.whatsapp.groups` without changing routing or sender authorization. (#83846) Thanks @neeravmakwana.
3131
- WhatsApp: drain pending outbound deliveries on a 30s periodic timer in addition to the reconnect handler, so messages enqueued while the provider is already connected no longer wait for the next reconnect to send. (#79083) Thanks @Oviemudiaga.
3232
- CLI/TUI: include gateway plugin slash commands in TUI autocomplete, so connected sessions can suggest plugin-owned commands exposed by the running Gateway. (#83640) Thanks @se7en-agent.
33+
- Gateway/mobile: restore QR setup-code handoff of bounded operator tokens for iOS and Android onboarding while keeping admin and pairing scopes out of bootstrap. (#83684) Thanks @ngutman.
3334

3435
## 2026.5.19
3536

apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,6 @@ class GatewaySession(
605605
setOf(
606606
"operator.approvals",
607607
"operator.read",
608-
"operator.talk.secrets",
609608
"operator.write",
610609
)
611610
scopes.filter { allowedOperatorScopes.contains(it) }.distinct().sorted()

apps/android/app/src/main/java/ai/openclaw/app/node/ConnectionManager.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,14 @@ class ConnectionManager(
162162
fun buildOperatorConnectOptions(): GatewayConnectOptions =
163163
GatewayConnectOptions(
164164
role = "operator",
165-
scopes = listOf("operator.read", "operator.write", "operator.talk.secrets"),
165+
// QR bootstrap hands Android a bounded operator token that includes approvals; keep the
166+
// default operator reconnect request aligned so the post-bootstrap loop can approve work.
167+
scopes =
168+
listOf(
169+
"operator.approvals",
170+
"operator.read",
171+
"operator.write",
172+
),
166173
caps = emptyList(),
167174
commands = emptyList(),
168175
permissions = emptyMap(),

apps/android/app/src/test/java/ai/openclaw/app/gateway/GatewaySessionInvokeTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ class GatewaySessionInvokeTest {
365365
assertEquals(emptyList<String>(), nodeEntry?.scopes)
366366
assertEquals("bootstrap-operator-token", operatorEntry?.token)
367367
assertEquals(
368-
listOf("operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"),
368+
listOf("operator.approvals", "operator.read", "operator.write"),
369369
operatorEntry?.scopes,
370370
)
371371
} finally {

apps/android/app/src/test/java/ai/openclaw/app/node/ConnectionManagerTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,20 @@ class ConnectionManagerTest {
368368
assertEquals(false, params?.allowTOFU)
369369
}
370370

371+
@Test
372+
fun buildOperatorConnectOptions_requestsQrBootstrapHandoffScopes() {
373+
val options = newManager().buildOperatorConnectOptions()
374+
375+
assertEquals(
376+
listOf(
377+
"operator.approvals",
378+
"operator.read",
379+
"operator.write",
380+
),
381+
options.scopes,
382+
)
383+
}
384+
371385
@Test
372386
fun buildNodeConnectOptions_advertisesRequestableSmsSearchWithoutSmsCapability() {
373387
val options =
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import OpenClawKit
3+
4+
enum GatewayOnboardingReset {
5+
static func reset(
6+
appModel: NodeAppModel,
7+
instanceId: String,
8+
defaults: UserDefaults = .standard)
9+
{
10+
appModel.disconnectGateway()
11+
12+
let trimmedInstanceId = instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
13+
if !trimmedInstanceId.isEmpty {
14+
GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
15+
}
16+
17+
GatewaySettingsStore.clearLastGatewayConnection()
18+
GatewaySettingsStore.clearPreferredGatewayStableID()
19+
GatewaySettingsStore.clearLastDiscoveredGatewayStableID()
20+
GatewayTLSStore.clearAllFingerprints()
21+
OnboardingStateStore.reset(defaults: defaults)
22+
23+
defaults.set(false, forKey: "gateway.onboardingComplete")
24+
defaults.set(false, forKey: "gateway.hasConnectedOnce")
25+
defaults.set(false, forKey: "gateway.manual.enabled")
26+
defaults.set("", forKey: "gateway.manual.host")
27+
defaults.set("", forKey: "gateway.setupCode")
28+
defaults.set(defaults.integer(forKey: "onboarding.requestID") + 1, forKey: "onboarding.requestID")
29+
}
30+
}

apps/ios/Sources/Onboarding/OnboardingWizardView.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,10 +1016,24 @@ struct OnboardingWizardView: View {
10161016
}
10171017

10181018
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
1019+
if problem.suggestsOnboardingReset { return "Scan QR again" }
10191020
problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
10201021
}
10211022

10221023
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
1024+
if problem.suggestsOnboardingReset {
1025+
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
1026+
self.gatewayToken = ""
1027+
self.gatewayPassword = ""
1028+
self.connectingGatewayID = nil
1029+
self.connectMessage = nil
1030+
self.issue = .none
1031+
self.pairingRequestId = nil
1032+
self.statusLine = "Scan a fresh setup QR code from this gateway."
1033+
self.step = .connect
1034+
self.showQRScanner = true
1035+
return
1036+
}
10231037
if problem.canTrustRotatedCertificate {
10241038
self.connectingGatewayID = "trust-certificate"
10251039
self.connectMessage = "Updating gateway certificate…"

apps/ios/Sources/RootCanvas.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct RootCanvas: View {
1515
@AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0
1616
@AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false
1717
@AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false
18+
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
1819
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
1920
@AppStorage("gateway.manual.enabled") private var manualGatewayEnabled: Bool = false
2021
@AppStorage("gateway.manual.host") private var manualGatewayHost: String = ""
@@ -102,6 +103,9 @@ struct RootCanvas: View {
102103
},
103104
retryGatewayConnection: {
104105
Task { await self.gatewayController.connectLastKnown() }
106+
},
107+
resetOnboarding: {
108+
self.resetOnboardingFromGatewayProblem()
105109
})
106110
.preferredColorScheme(.dark)
107111

@@ -429,6 +433,13 @@ struct RootCanvas: View {
429433
guard shouldPresent else { return }
430434
self.presentedSheet = .quickSetup
431435
}
436+
437+
private func resetOnboardingFromGatewayProblem() {
438+
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
439+
self.presentedSheet = nil
440+
self.onboardingAllowSkip = false
441+
self.showOnboarding = true
442+
}
432443
}
433444

434445
private struct HomeCanvasPayload: Codable {
@@ -469,6 +480,7 @@ private struct CanvasContent: View {
469480
var openChat: () -> Void
470481
var openSettings: () -> Void
471482
var retryGatewayConnection: () -> Void
483+
var resetOnboarding: () -> Void
472484

473485
private var brightenButtons: Bool {
474486
self.systemColorScheme == .light
@@ -578,12 +590,15 @@ private struct CanvasContent: View {
578590

579591
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
580592
if problem.canTrustRotatedCertificate { return "Trust certificate" }
593+
if problem.suggestsOnboardingReset { return "Reset onboarding" }
581594
return problem.retryable ? "Retry" : "Open Settings"
582595
}
583596

584597
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) {
585598
if problem.canTrustRotatedCertificate {
586599
Task { await self.gatewayController.trustRotatedGatewayCertificate(from: problem) }
600+
} else if problem.suggestsOnboardingReset {
601+
self.resetOnboarding()
587602
} else if problem.retryable {
588603
self.retryGatewayConnection()
589604
} else {

apps/ios/Sources/Settings/SettingsTab.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,10 +1057,15 @@ struct SettingsTab: View {
10571057
}
10581058

10591059
private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String {
1060+
if problem.suggestsOnboardingReset { return "Reset onboarding" }
10601061
problem.canTrustRotatedCertificate ? "Trust certificate" : "Retry connection"
10611062
}
10621063

10631064
private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async {
1065+
if problem.suggestsOnboardingReset {
1066+
self.resetOnboarding()
1067+
return
1068+
}
10641069
if problem.canTrustRotatedCertificate {
10651070
_ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem)
10661071
return
@@ -1070,7 +1075,6 @@ struct SettingsTab: View {
10701075

10711076
private func resetOnboarding() {
10721077
// Disconnect first so RootCanvas doesn't instantly mark onboarding complete again.
1073-
self.appModel.disconnectGateway()
10741078
self.connectingGatewayID = nil
10751079
self.setupStatusText = nil
10761080
self.setupCode = ""
@@ -1082,19 +1086,7 @@ struct SettingsTab: View {
10821086
self.gatewayToken = ""
10831087
self.gatewayPassword = ""
10841088

1085-
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
1086-
if !trimmedInstanceId.isEmpty {
1087-
GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId)
1088-
}
1089-
1090-
// Reset onboarding state + clear saved gateway connection (the two things RootCanvas checks).
1091-
GatewaySettingsStore.clearLastGatewayConnection()
1092-
GatewaySettingsStore.clearPreferredGatewayStableID()
1093-
GatewaySettingsStore.clearLastDiscoveredGatewayStableID()
1094-
// Resetting onboarding should also forget trusted gateway TLS fingerprints.
1095-
// Otherwise a restarted dev gateway can stay stuck in a local TLS cancel loop.
1096-
GatewayTLSStore.clearAllFingerprints()
1097-
OnboardingStateStore.reset()
1089+
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
10981090

10991091
// RootCanvas also short-circuits onboarding when these are true.
11001092
self.onboardingComplete = false

apps/ios/SwiftSources.input.xcfilelist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Sources/Model/NodeAppModel+WatchNotifyNormalization.swift
3535
Sources/Model/NodeAppModel.swift
3636
Sources/Model/WatchReplyCoordinator.swift
3737
Sources/Motion/MotionService.swift
38+
Sources/Onboarding/GatewayOnboardingReset.swift
3839
Sources/Onboarding/GatewayOnboardingView.swift
3940
Sources/Onboarding/OnboardingStateStore.swift
4041
Sources/Onboarding/OnboardingWizardView.swift

0 commit comments

Comments
 (0)