Skip to content

fix(auth): hand off qr bootstrap to bounded device tokens#60238

Merged
steipete merged 3 commits into
mainfrom
ios/qr-bootstrap-fixes
Apr 4, 2026
Merged

fix(auth): hand off qr bootstrap to bounded device tokens#60238
steipete merged 3 commits into
mainfrom
ios/qr-bootstrap-fixes

Conversation

@ngutman

@ngutman ngutman commented Apr 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: iOS QR bootstrap onboarding could connect the node session, but it did not hand off the bounded operator auth needed for post-onboarding operator-capable flows.
  • Why it matters: fresh QR-only onboarding could not immediately participate in approval-gated operator flows, and visible push permission was easy to miss after a reinstall.
  • What changed: QR bootstrap now seeds bounded node + operator device tokens, persists only the trusted bootstrap handoff tokens on iOS, and requests notification authorization immediately after successful QR onboarding.
  • What did NOT change (scope boundary): no exec approval push/modal UI in this PR; no approval inbox/history UI; no widening to operator.admin or operator.pairing.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: QR setup code/bootstrap auth was intentionally node-bootstrap-oriented and iOS refused to start the operator loop from bootstrap-only auth.
  • Missing detection / guardrail: we did not return/store bounded per-role device tokens from the successful bootstrap handshake.
  • Prior context (git blame, prior PR, issue, or refactor if known): split out from the broader iOS exec approval work previously carried on ios/exec-approval-push / PR feat(ios): add exec approval push flow #59732.
  • Why this regressed now: N/A; this is a missing onboarding handoff rather than a newly introduced regression on a shipped iOS user base.
  • If unknown, what was ruled out: generic APNs transport and QR pairing itself were ruled out during manual device testing.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • src/infra/device-bootstrap.test.ts
    • src/infra/device-pairing.test.ts
    • src/gateway/server.auth.control-ui.test.ts
    • src/pairing/setup-code.test.ts
    • apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayNodeSessionTests.swift
    • apps/ios/Tests/GatewayConnectionControllerTests.swift
    • apps/ios/Tests/NodeAppModelInvokeTests.swift
  • Scenario the test should lock in: QR bootstrap can only seed the bounded baseline, returns/stores node + operator device tokens, and requests notification authorization after successful bootstrap onboarding.
  • Why this is the smallest reliable guardrail: the issue crosses gateway auth, device pairing persistence, and iOS onboarding state.
  • Existing test that already covers this (if any): none covered the full handoff before this change.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

  • Fresh QR onboarding on iOS now silently hands off bounded node + operator device auth.
  • The stored operator token includes operator.approvals.
  • iOS requests notification permission immediately after successful QR onboarding instead of waiting for a later local-notification path.

Diagram (if applicable)

Before:
[scan QR] -> [bootstrap node connect only] -> [no stored operator auth] -> [operator flows still blocked]

After:
[scan QR] -> [bootstrap node connect] -> [gateway seeds bounded node+operator tokens] -> [iOS stores both] -> [operator loop can start] -> [notification auth prompt shown]

Security Impact (required)

  • New permissions/capabilities? (Yes/No) Yes
  • Secrets/tokens handling changed? (Yes/No) Yes
  • New/changed network calls? (Yes/No) No
  • Command/tool execution surface changed? (Yes/No) No
  • Data access scope changed? (Yes/No) Yes
  • If any Yes, explain risk + mitigation:
    • Risk: QR bootstrap now seeds operator-capable auth in addition to node auth.
    • Mitigation: the seeded baseline is explicitly bounded to operator.approvals, operator.read, operator.talk.secrets, and operator.write; bootstrap remains single-use; public-key binding is normalized; iOS only persists handoff tokens for trusted bootstrap contexts (wss or loopback) and filters roles/scopes before storing.

Repro + Verification

Environment

  • OS: macOS 26.3.1
  • Runtime/container: Node 24.x, local gateway
  • Model/provider: N/A
  • Integration/channel (if any): iOS app / gateway QR onboarding
  • Relevant config (redacted): isolated local gateway with QR bootstrap setup

Steps

  1. Generate a fresh QR setup payload and bootstrap token.
  2. Connect the iOS app via QR bootstrap.
  3. Inspect paired device roles/tokens and confirm operator handoff and notification auth prompt behavior.

Expected

  • The paired iOS device gets bounded node + operator auth, with operator.approvals present.
  • The bootstrap token is single-use.
  • Notification authorization is requested immediately after successful onboarding.

Actual

  • Matches expected with the changes in this PR.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • reviewed the branch scope against the intended QR-bootstrap-only split
    • validated targeted bootstrap/device-token tests
    • validated targeted iOS simulator tests for operator scope baseline and post-bootstrap notification auth prompt
  • Edge cases checked:
    • bootstrap token single-use / binding
    • bounded token persistence filtering
    • non-operator token scope normalization
  • What you did not verify:
    • a new real-phone rerun on this split branch alone

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No) Yes
  • Config/env changes? (Yes/No) No
  • Migration needed? (Yes/No) No
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: storing extra bootstrap handoff tokens too broadly on iOS.
    • Mitigation: only persist on trusted bootstrap transports and aggressively filter the allowed roles/scopes.
  • Risk: widening QR bootstrap beyond the minimum needed baseline.
    • Mitigation: keep the bootstrap profile narrowly bounded and do not add operator.admin or operator.pairing.

@ngutman
ngutman requested a review from a team as a code owner April 3, 2026 10:58
@openclaw-barnacle openclaw-barnacle Bot added app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime size: L maintainer Maintainer-authored PR labels Apr 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8840c8443d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/ios/Sources/Model/NodeAppModel.swift Outdated
@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the QR bootstrap onboarding path on iOS: a fresh QR scan now seeds both bounded node and operator device tokens (instead of only the node token), iOS persists those tokens only for trusted transports (wss or loopback), and notification authorization is requested immediately after a successful QR onboarding flow.

Key changes and how they fit the codebase:

  • Gateway protocol (frames.ts): adds an optional deviceTokens array to HelloOkSchema.auth — additive and backward-compatible.
  • Bootstrap profile (device-bootstrap-profile.ts): PAIRING_SETUP_BOOTSTRAP_PROFILE now includes operator.approvals in the baseline scope set.
  • device-bootstrap.ts: adds normalizeBootstrapPublicKey to reconcile PEM/standard-base64/base64url encodings before binding comparisons, and getBoundDeviceBootstrapProfile for a post-verify profile read.
  • device-pairing.ts: introduces approveBootstrapDevicePairing — an explicit trust path that seeds node + operator tokens from the bootstrap profile without routing through the interactive callerScopes checker. Also fixes stale operator scopes leaking onto non-operator tokens on re-approval.
  • message-handler.ts: threads bootstrapProfile through the pairing decision, uses approveBootstrapDevicePairing for bootstrap approval, and populates deviceTokens in hello-ok.
  • iOS GatewayChannel.swift: gates token persistence to bootstrap + trusted transports with a hard-coded per-role scope allow-list — solid security tightening.
  • iOS NodeAppModel.swift: extracts handleSuccessfulBootstrapGatewayOnboarding, adds shouldRequestOperatorApprovalScope for backward-compat reconnects, and makeOperatorConnectOptions conditionally adds operator.approvals.

Test coverage is thorough across all three layers (TypeScript infra, gateway integration, Swift).

Confidence Score: 5/5

Safe to merge — no P0 or P1 issues found; the QR bootstrap handoff logic is well-bounded, single-use invariants are preserved, and token persistence is correctly gated to trusted transports.

All findings are P2 (a redundant type assertion and a note about duplicated verification logic). The security-sensitive paths — bootstrap profile scoping, scope allow-listing in Swift, single-use token revocation, and the explicit trust path in approveBootstrapDevicePairing — are implemented correctly and backed by targeted tests at every layer.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/server/ws-connection/message-handler.ts
Line: 1045-1046

Comment:
**Redundant type assertion after null check**

`bootstrapProfileForHello` is typed as `DeviceBootstrapProfile | null`, and the outer `if (device && bootstrapProfileForHello !== null)` guard already narrows it to `DeviceBootstrapProfile`. The intermediate `as DeviceBootstrapProfile` cast on line 1046 is a no-op that TypeScript should not need.

```suggestion
        if (device && bootstrapProfileForHello !== null) {
          for (const bootstrapRole of bootstrapProfileForHello.roles) {
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/infra/device-bootstrap.ts
Line: 257-297

Comment:
**`getBoundDeviceBootstrapProfile` repeats verification logic without consuming the token**

The function re-opens the lock, re-finds the token record, and re-checks the device binding — all steps already performed by `verifyDeviceBootstrapToken` moments earlier in the same handshake. While the double-lock is safe (sequential, not nested) and the function is read-only, the duplication means any future change to the binding check in `verifyDeviceBootstrapToken` needs to be mirrored here. A short clarifying comment explaining why this is intentionally separate from the verify path (read-only profile lookup vs. state-mutating verify + bind) would help future readers.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "fix(auth): address qr bootstrap review f..." | Re-trigger Greptile

Comment thread src/infra/device-pairing.ts Outdated
@ngutman

ngutman commented Apr 3, 2026

Copy link
Copy Markdown
Member Author

Addressed the review feedback in 1e3af8a.

What changed:

  • preserved backward-compatible operator reconnect scopes on iOS by only requesting operator.approvals when the current auth path can already support it (shared token/password or a stored operator token that already carries that scope)
  • removed the unused/confusing approvedScopesOverride path from src/infra/device-pairing.ts
  • documented the getBoundDeviceBootstrapProfile(...) precondition
  • stopped duplicating the primary node token in hello-ok.auth.deviceTokens
  • added regression coverage for the iOS scope-selection behavior and for the bootstrap hello payload expectations

Validation rerun:

  • pnpm test -- src/infra/device-bootstrap.test.ts src/infra/device-pairing.test.ts src/gateway/server.auth.control-ui.test.ts -t 'auto-approves fresh node bootstrap pairing from qr setup code|fails closed for operator approvals when caller scopes are omitted|allows operator scope subsets within the issued bootstrap profile|stores and reads explicit bootstrap profiles'
  • xcodegen generate in apps/ios
  • xcodebuild -project apps/ios/OpenClaw.xcodeproj -scheme OpenClaw -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6' -only-testing:'OpenClawTests/GatewayConnectionControllerTests/operatorConnectOptionsOnlyRequestApprovalScopeWhenEnabled()' -only-testing:'OpenClawTests/GatewayConnectionControllerTests/operatorApprovalScopeRequestsStayBackwardCompatible()' -only-testing:'OpenClawTests/NodeAppModelInvokeTests/operatorLoopWaitsForBootstrapHandoffBeforeUsingStoredToken()' test
  • pnpm build
  • pnpm check

Note: swift test --package-path apps/shared/OpenClawKit --filter GatewayNodeSessionTests/bootstrapHelloStoresAdditionalDeviceTokens currently runs into unrelated preexisting TalkSystemSpeechSynthesizerTests actor-isolation compile errors in the package test target, so I did not treat that as caused by this PR.

@greptile-apps and @chatgpt-codex-connector please re-review this PR.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ngutman
ngutman force-pushed the ios/qr-bootstrap-fixes branch from 1e3af8a to 25897b9 Compare April 3, 2026 11:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 25897b90a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/ios/Sources/Model/NodeAppModel.swift
@ngutman
ngutman force-pushed the ios/qr-bootstrap-fixes branch from 25897b9 to b35d417 Compare April 3, 2026 20:25
@openclaw-barnacle openclaw-barnacle Bot added the app: android App: android label Apr 4, 2026
@steipete
steipete force-pushed the ios/qr-bootstrap-fixes branch from 512b9c0 to f6223e6 Compare April 4, 2026 06:57
@steipete
steipete merged commit 20266ff into main Apr 4, 2026
3 checks passed
@steipete
steipete deleted the ios/qr-bootstrap-fixes branch April 4, 2026 06:57
@steipete

steipete commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Landed via rebase onto main.

  • Gate: swift test --package-path apps/shared/OpenClawKit --filter GatewayNodeSessionTests; ANDROID_HOME="$HOME/Library/Android/sdk" ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTest; ANDROID_HOME="$HOME/Library/Android/sdk" ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" pnpm test; ANDROID_HOME="$HOME/Library/Android/sdk" ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" pnpm build; ANDROID_HOME="$HOME/Library/Android/sdk" ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" pnpm check
  • Land commit: f6223e609c2a9b8de9960bdb875740757ea6fe29
  • Merge commit: 20266ff

Thanks @ngutman!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6223e609c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +598 to +600
if authSource == .bootstrapToken {
guard self.shouldPersistBootstrapHandoffTokens() else {
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist primary bootstrap token even on ws LAN onboarding

This guard drops the main auth.deviceToken whenever bootstrap auth is used over non-loopback ws://, so QR onboarding on private-LAN cleartext URLs ends up with no durable credential saved. That is a functional break because setup-code resolution still allows private-LAN ws:// URLs (src/pairing/setup-code.ts), while the gateway now consumes the bootstrap token after hello (src/gateway/server/ws-connection/message-handler.ts), so a later reconnect can fail with invalid bootstrap auth. Keep transport gating for extra handoff tokens, but always persist the primary node token.

Useful? React with 👍 / 👎.

Comment on lines +465 to +467
if (authSource == GatewayConnectAuthSource.BOOTSTRAP_TOKEN) {
if (!shouldPersistBootstrapHandoffTokens(authSource)) return
persistBootstrapHandoffToken(deviceId, role, token, scopes)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Save bootstrap primary token before transport trust check

This early return skips saving the primary bootstrap-issued node token on non-loopback ws:// connections, which breaks durability for LAN QR onboarding. The setup-code flow can still produce private-LAN ws:// endpoints (src/pairing/setup-code.ts), and bootstrap tokens are revoked after successful hello (src/gateway/server/ws-connection/message-handler.ts), so the next reconnect may have no valid credential and pause on auth failure. Persist the primary token unconditionally and only gate additional bootstrap handoff tokens.

Useful? React with 👍 / 👎.

KimGLee pushed a commit to KimGLee/openclaw that referenced this pull request Apr 4, 2026
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: android App: android app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants