Rework Android gateway onboarding setup (#98752)

* feat(android): rework gateway onboarding setup

* fix(android): restore protocol mismatch onboarding guidance

* fix(android): sync onboarding native i18n

* fix(android): preserve LAN manual setup prefill

* fix(android): refine onboarding node approval step

* fix(android): polish onboarding recovery actions

* test(android): cover onboarding protocol mismatch copy

* fix(android): separate onboarding node approval

* fix(android): guard onboarding node approval transitions

* fix(android): keep onboarding approval states reachable

* fix(android): wait for node access before onboarding continue

* fix(android): refresh node approval after operator handoff

* fix(android): refresh onboarding approval states

* fix(android): require approval after onboarding permission changes

* fix(android): keep onboarding approval gates active

* fix(android): preserve permission reapproval state

* fix(android): skip node approval on legacy gateways

* fix(android): wait for node approval refresh

* fix(android): preserve camera setting on upgrade

* fix(android): avoid stuck approval check spinner

* fix(android): request talk secrets on operator connects

* fix(android): avoid missed node approval completion

* fix(android): keep nearby LAN setup local

* fix(android): complete onboarding after node approval

* fix(android): reconcile onboarding with gateway auth plans

* chore(android): refresh native i18n inventory after rebase

* Fix Android onboarding review edge cases

* Fix native i18n onboarding sentinels
This commit is contained in:
Jesse Merhi 2026-07-03 21:20:12 +10:00 committed by GitHub
parent 776b34a4e6
commit 64318d7624
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 3849 additions and 1635 deletions

View file

@ -179,7 +179,7 @@
},
{
"kind": "ui-state-text",
"line": 498,
"line": 499,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Offline",
"surface": "android",
@ -187,7 +187,7 @@
},
{
"kind": "conditional-branch",
"line": 1994,
"line": 2007,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected.",
"surface": "android",
@ -195,7 +195,7 @@
},
{
"kind": "conditional-branch",
"line": 1996,
"line": 2009,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed: secure endpoint reached, but TLS fingerprint verification timed out. Check Tailscale Serve or gateway TLS and retry.",
"surface": "android",
@ -203,7 +203,7 @@
},
{
"kind": "conditional-branch",
"line": 1998,
"line": 2011,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed: couldn't reach the secure gateway endpoint for this host.",
"surface": "android",
@ -371,7 +371,7 @@
},
{
"kind": "conditional-branch",
"line": 1095,
"line": 1096,
"path": "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt",
"source": "Connecting…",
"surface": "android",
@ -379,7 +379,7 @@
},
{
"kind": "conditional-branch",
"line": 1095,
"line": 1096,
"path": "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt",
"source": "Reconnecting…",
"surface": "android",
@ -1195,7 +1195,7 @@
},
{
"kind": "conditional-branch",
"line": 290,
"line": 292,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt",
"source": "Setup code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix",
"surface": "android",
@ -1203,7 +1203,7 @@
},
{
"kind": "conditional-branch",
"line": 292,
"line": 294,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt",
"source": "QR code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix",
"surface": "android",
@ -1211,7 +1211,7 @@
},
{
"kind": "conditional-branch",
"line": 294,
"line": 296,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt",
"source": "$remoteGatewaySecurityRule $remoteGatewaySecurityFix",
"surface": "android",
@ -1219,7 +1219,7 @@
},
{
"kind": "conditional-branch",
"line": 299,
"line": 301,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt",
"source": "Setup code has invalid gateway URL.",
"surface": "android",
@ -1227,7 +1227,7 @@
},
{
"kind": "conditional-branch",
"line": 300,
"line": 302,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt",
"source": "QR code did not contain a valid setup code.",
"surface": "android",
@ -1235,7 +1235,7 @@
},
{
"kind": "conditional-branch",
"line": 301,
"line": 303,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt",
"source": "Enter a valid manual endpoint to connect.",
"surface": "android",
@ -1666,48 +1666,240 @@
"id": "native.android.0bbe0d733b1328fd"
},
{
"kind": "ui-named-argument",
"line": 437,
"kind": "ui-call",
"line": 654,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "OPENCLAW",
"source": "Trust",
"surface": "android",
"id": "native.android.d42fe72f8e76b450"
"id": "native.android.9ac2d4498b17d478"
},
{
"kind": "ui-call",
"line": 659,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Cancel",
"surface": "android",
"id": "native.android.e26ee07a1d1911be"
},
{
"kind": "ui-named-argument",
"line": 442,
"line": 856,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Your personal AI assistant.\nExfoliate! Exfoliate!",
"source": "Welcome to OpenClaw",
"surface": "android",
"id": "native.android.a5c2d7f27576f0c4"
"id": "native.android.64c8db13ef91513c"
},
{
"kind": "ui-named-argument",
"line": 452,
"line": 938,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Connect to your Gateway",
"surface": "android",
"id": "native.android.cc878f72a6b70b23"
},
{
"kind": "ui-named-argument",
"line": 939,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Choose device permissions",
"surface": "android",
"id": "native.android.bf3750bf264f0063"
},
{
"kind": "ui-named-argument",
"line": 940,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Use OpenClaw from your phone",
"surface": "android",
"id": "native.android.3bfe29d4d13d3c3e"
},
{
"kind": "ui-named-argument",
"line": 962,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Security notice",
"surface": "android",
"id": "native.android.c0b539eeb4612f5d"
},
{
"kind": "ui-named-argument",
"line": 964,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "The connected OpenClaw agent can use device capabilities you enable. Continue only if you trust the Gateway and agent you connect to.",
"surface": "android",
"id": "native.android.5aed4b5c8d6b246d"
},
{
"kind": "ui-named-argument",
"line": 1005,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Connect Gateway",
"surface": "android",
"id": "native.android.63c31ee564d63347"
},
{
"kind": "ui-named-argument",
"line": 596,
"kind": "ui-toast",
"line": 1015,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Setup code issue",
"source": "Could not open setup guide.",
"surface": "android",
"id": "native.android.afdd3e2c10f56b75"
"id": "native.android.58d80589d9d4b7ff"
},
{
"kind": "ui-named-argument",
"line": 613,
"line": 1023,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Advanced",
"source": "Scan QR or setup code",
"surface": "android",
"id": "native.android.084eda7046e91737"
"id": "native.android.18d6dd32cfcc1c9a"
},
{
"kind": "ui-named-argument",
"line": 622,
"line": 1029,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Set up manually",
"surface": "android",
"id": "native.android.8b8acc5b98b8c99f"
},
{
"kind": "ui-named-argument",
"line": 1051,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Before you start",
"surface": "android",
"id": "native.android.debd7b3e59db6ab2"
},
{
"kind": "ui-named-argument",
"line": 1057,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Access to the Gateway device",
"surface": "android",
"id": "native.android.9cfcf68aae2c05c6"
},
{
"kind": "ui-named-argument",
"line": 1061,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Phone can reach the Gateway",
"surface": "android",
"id": "native.android.71ce00f359981a0b"
},
{
"kind": "ui-named-argument",
"line": 1068,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Android setup guide",
"surface": "android",
"id": "native.android.68a31cee94a9db86"
},
{
"kind": "ui-named-argument",
"line": 1122,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Setup Gateway",
"surface": "android",
"id": "native.android.d514a981f80779e9"
},
{
"kind": "ui-named-argument",
"line": 1128,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Start your Gateway.",
"surface": "android",
"id": "native.android.6a45e2dae35ab5a7"
},
{
"kind": "ui-named-argument",
"line": 1134,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Generate a QR code.",
"surface": "android",
"id": "native.android.c6f9da7008df05a6"
},
{
"kind": "ui-named-argument",
"line": 1155,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Choose from gallery",
"surface": "android",
"id": "native.android.96d40475de2885d6"
},
{
"kind": "ui-named-argument",
"line": 1209,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "QR code not accepted",
"surface": "android",
"id": "native.android.fd83bd36419ef526"
},
{
"kind": "ui-named-argument",
"line": 1224,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Choose another image",
"surface": "android",
"id": "native.android.4dde43318559c65d"
},
{
"kind": "ui-named-argument",
"line": 1277,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Scan QR code",
"surface": "android",
"id": "native.android.54bab6e4054484d0"
},
{
"kind": "ui-named-argument",
"line": 1279,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Open the camera and frame the code from openclaw qr.",
"surface": "android",
"id": "native.android.05987e822ac102ec"
},
{
"kind": "ui-named-argument",
"line": 1313,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Align the QR code inside the square.",
"surface": "android",
"id": "native.android.90fa932d2e0adbd9"
},
{
"kind": "ui-named-argument",
"line": 1331,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Camera access is needed to scan the setup QR.",
"surface": "android",
"id": "native.android.7a817ddf54373dbf"
},
{
"kind": "ui-named-argument",
"line": 1336,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Allow camera",
"surface": "android",
"id": "native.android.c8d5f12cb28f3853"
},
{
"kind": "ui-named-argument",
"line": 1357,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Close scanner",
"surface": "android",
"id": "native.android.7efba153567b1232"
},
{
"kind": "ui-named-argument",
"line": 1519,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Enter setup code",
"surface": "android",
"id": "native.android.b8d5431e1e441f74"
},
{
"kind": "ui-named-argument",
"line": 1520,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Setup code",
"surface": "android",
@ -1715,7 +1907,47 @@
},
{
"kind": "ui-named-argument",
"line": 624,
"line": 1524,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Paste setup code",
"surface": "android",
"id": "native.android.06a2214da1eee841"
},
{
"kind": "ui-named-argument",
"line": 1528,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Setup code was not accepted",
"surface": "android",
"id": "native.android.35aec8a97ac12e1c"
},
{
"kind": "ui-named-argument",
"line": 1532,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Use setup code",
"surface": "android",
"id": "native.android.9aaee5a05de93dbe"
},
{
"kind": "ui-named-argument",
"line": 1563,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Manual setup",
"surface": "android",
"id": "native.android.073c93545602b198"
},
{
"kind": "ui-named-argument",
"line": 1566,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway URL",
"surface": "android",
"id": "native.android.46d58ca58c32ad52"
},
{
"kind": "ui-named-argument",
"line": 1568,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Host",
"surface": "android",
@ -1723,47 +1955,55 @@
},
{
"kind": "ui-named-argument",
"line": 625,
"line": 1569,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Port",
"surface": "android",
"id": "native.android.e7f368b47b705410"
},
{
"kind": "conditional-branch",
"line": 628,
"kind": "ui-named-argument",
"line": 1572,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "TLS off",
"source": "Use the Gateway computer's LAN address or secure remote hostname.",
"surface": "android",
"id": "native.android.20d94e1cedd3f319"
},
{
"kind": "conditional-branch",
"line": 628,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "TLS on",
"surface": "android",
"id": "native.android.a3a158b4a10d34ba"
"id": "native.android.1b0d9893ae4c0682"
},
{
"kind": "ui-named-argument",
"line": 629,
"line": 1579,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Local",
"source": "Token",
"surface": "android",
"id": "native.android.a02f48c59c8b7b9d"
"id": "native.android.bfc48711d6deed92"
},
{
"kind": "ui-named-argument",
"line": 631,
"line": 1580,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Token optional",
"source": "Paste token",
"surface": "android",
"id": "native.android.aae4212f2ff4df98"
"id": "native.android.7ac2b9aec9d78cda"
},
{
"kind": "ui-named-argument",
"line": 632,
"line": 1582,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Paste a shared Gateway token or operator-issued token.",
"surface": "android",
"id": "native.android.96ba0e3919c3955a"
},
{
"kind": "ui-named-argument",
"line": 1589,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Password",
"surface": "android",
"id": "native.android.4a5a1702f0130328"
},
{
"kind": "ui-named-argument",
"line": 1590,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Password optional",
"surface": "android",
@ -1771,39 +2011,159 @@
},
{
"kind": "ui-named-argument",
"line": 637,
"line": 1594,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Pair with Gateway",
"source": "Connection type",
"surface": "android",
"id": "native.android.369ee061f6d7881f"
"id": "native.android.1259d0d0d1671ed7"
},
{
"kind": "ui-named-argument",
"line": 710,
"line": 1596,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Last gateway",
"source": "Local network",
"surface": "android",
"id": "native.android.82b296ed39647687"
"id": "native.android.278d9abb3abc12e6"
},
{
"kind": "ui-named-argument",
"line": 775,
"line": 1597,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Edit connection",
"source": "Secure remote",
"surface": "android",
"id": "native.android.e0e5d2bb055863fb"
"id": "native.android.4aa2b8be11a971eb"
},
{
"kind": "ui-named-argument",
"line": 778,
"line": 1600,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Copy diagnostic",
"source": "Local works for LAN or emulator hosts. Secure remote is for wss:// or Tailscale Serve/Funnel.",
"surface": "android",
"id": "native.android.ea2f18dad7749613"
"id": "native.android.8c31e2762b496d61"
},
{
"kind": "ui-named-argument",
"line": 825,
"line": 1608,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Could not test connection",
"surface": "android",
"id": "native.android.24a0a4819f223b87"
},
{
"kind": "ui-named-argument",
"line": 1613,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Test connection",
"surface": "android",
"id": "native.android.9a8f8ba539d4dd68"
},
{
"kind": "ui-call",
"line": 1791,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "View details",
"surface": "android",
"id": "native.android.a1c6ad798cdb4e32"
},
{
"kind": "ui-call",
"line": 1824,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Connection details",
"surface": "android",
"id": "native.android.ebdc41fd34eab0a5"
},
{
"kind": "ui-call",
"line": 1836,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Copy",
"surface": "android",
"id": "native.android.f982d1f6ad7b3e5d"
},
{
"kind": "ui-call",
"line": 1841,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Close",
"surface": "android",
"id": "native.android.fa2a2369f23aa195"
},
{
"kind": "ui-toast",
"line": 1853,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Details copied",
"surface": "android",
"id": "native.android.50cca1e186823cd6"
},
{
"kind": "ui-named-argument",
"line": 1938,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Approve node access",
"surface": "android",
"id": "native.android.31a4bf3ed4071f7c"
},
{
"kind": "ui-named-argument",
"line": 1945,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway pairing is complete. Approve this phone as a node so OpenClaw can use the device capabilities you enable.",
"surface": "android",
"id": "native.android.f43cb6ed03dc6766"
},
{
"kind": "ui-named-argument",
"line": 1953,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "On the Gateway computer, run:",
"surface": "android",
"id": "native.android.8e49feba12c350ac"
},
{
"kind": "ui-named-argument",
"line": 1962,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Use the requestId from the pending command in the approve command.",
"surface": "android",
"id": "native.android.6189b5e2af7cc793"
},
{
"kind": "ui-named-argument",
"line": 1973,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "I have approved",
"surface": "android",
"id": "native.android.53bcd45eeb574d74"
},
{
"kind": "ui-named-argument",
"line": 1986,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Still waiting for approval",
"surface": "android",
"id": "native.android.cdc44aaeb8bd6e4a"
},
{
"kind": "ui-named-argument",
"line": 1989,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Run the approve command on the Gateway computer, then check again.",
"surface": "android",
"id": "native.android.df5cc00eb95c21ad"
},
{
"kind": "ui-named-argument",
"line": 1996,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "OK",
"surface": "android",
"id": "native.android.f579f91c216dd6c4"
},
{
"kind": "ui-named-argument",
"line": 2146,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Copy approval command",
"surface": "android",
@ -1811,39 +2171,23 @@
},
{
"kind": "ui-named-argument",
"line": 852,
"line": 2172,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Allow permissions",
"source": "Only enable access you are comfortable letting OpenClaw use while this phone is connected. You can change these later in Android Settings.",
"surface": "android",
"id": "native.android.a31f2beb68298aa8"
"id": "native.android.d4c3bcf880727fb3"
},
{
"kind": "ui-named-argument",
"line": 857,
"line": 2184,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "These permissions keep OpenClaw secure\nand useful.",
"source": "Continue",
"surface": "android",
"id": "native.android.b82104d8df1d4b4a"
"id": "native.android.d7ea800cc2475013"
},
{
"kind": "ui-named-argument",
"line": 935,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Open $title",
"surface": "android",
"id": "native.android.03b95088ba72bd8e"
},
{
"kind": "ui-call",
"line": 996,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Choose what this phone can share with OpenClaw. You can change these later in Settings.",
"surface": "android",
"id": "native.android.f38ca3445453a6bb"
},
{
"kind": "ui-named-argument",
"line": 1017,
"line": 2211,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Back",
"surface": "android",
@ -1851,71 +2195,23 @@
},
{
"kind": "ui-named-argument",
"line": 1021,
"line": 2258,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Permission Setup",
"source": "Permissions",
"surface": "android",
"id": "native.android.4b54af42d17dec84"
"id": "native.android.f3f0527db434d756"
},
{
"kind": "conditional-branch",
"line": 1076,
"line": 2583,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Granted",
"source": "The code may have expired or been generated for another Gateway.",
"surface": "android",
"id": "native.android.437599c024df158e"
"id": "native.android.29732759e050c874"
},
{
"kind": "conditional-branch",
"line": 1076,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Not granted",
"surface": "android",
"id": "native.android.cf8344d3562e28e3"
},
{
"kind": "ui-named-argument",
"line": 1101,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Continue",
"surface": "android",
"id": "native.android.d7ea800cc2475013"
},
{
"kind": "conditional-branch",
"line": 1156,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Scan fresh setup code",
"surface": "android",
"id": "native.android.547f11a819721ffb"
},
{
"kind": "conditional-branch",
"line": 1158,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Retry connection",
"surface": "android",
"id": "native.android.3b169db8b57795b5"
},
{
"kind": "conditional-branch",
"line": 1337,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Setup code expired. Scan a fresh setup QR.",
"surface": "android",
"id": "native.android.e44224ed4df1700f"
},
{
"kind": "conditional-branch",
"line": 1344,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway access needs review. Check gateway authentication scopes, then retry.",
"surface": "android",
"id": "native.android.e03bb4ea463ba7a5"
},
{
"kind": "conditional-branch",
"line": 1345,
"line": 2587,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway password is required. Enter it again or edit this connection.",
"surface": "android",
@ -1923,7 +2219,7 @@
},
{
"kind": "conditional-branch",
"line": 1346,
"line": 2588,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway password is invalid. Re-enter it or reset this gateway connection.",
"surface": "android",
@ -1931,7 +2227,7 @@
},
{
"kind": "conditional-branch",
"line": 1347,
"line": 2589,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway token is required. Enter it again or edit this connection.",
"surface": "android",
@ -1939,7 +2235,7 @@
},
{
"kind": "conditional-branch",
"line": 1349,
"line": 2591,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway requires this device identity. Re-authenticate or reset this gateway connection.",
"surface": "android",
@ -1947,7 +2243,7 @@
},
{
"kind": "conditional-branch",
"line": 1353,
"line": 2595,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Saved authentication is invalid. Re-authenticate or reset this gateway connection.",
"surface": "android",
@ -1955,15 +2251,15 @@
},
{
"kind": "conditional-branch",
"line": 1354,
"line": 2596,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway authentication is not configured. Configure it on the gateway host, then retry.",
"source": "Gateway authentication is not configured. Edit this connection and try again.",
"surface": "android",
"id": "native.android.3f61f2c34784ed0f"
"id": "native.android.214b03b4cd2199b1"
},
{
"kind": "conditional-branch",
"line": 1355,
"line": 2597,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Gateway authentication needs review. Check gateway settings, then retry.",
"surface": "android",
@ -1971,7 +2267,7 @@
},
{
"kind": "conditional-branch",
"line": 1372,
"line": 2614,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "$summary $it",
"surface": "android",
@ -1979,19 +2275,27 @@
},
{
"kind": "ui-toast",
"line": 1419,
"line": 2757,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Approval command copied",
"surface": "android",
"id": "native.android.a7933196a18ec924"
},
{
"kind": "ui-toast",
"line": 1446,
"kind": "conditional-branch",
"line": 2819,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Diagnostic copied",
"source": "Granted",
"surface": "android",
"id": "native.android.bc9715de99c769c7"
"id": "native.android.437599c024df158e"
},
{
"kind": "conditional-branch",
"line": 2819,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt",
"source": "Not granted",
"surface": "android",
"id": "native.android.cf8344d3562e28e3"
},
{
"kind": "ui-named-argument",
@ -3929,6 +4233,14 @@
"surface": "android",
"id": "native.android.f23eab3d31c5e25c"
},
{
"kind": "ui-named-argument",
"line": 1213,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt",
"source": "Open ${row.title}",
"surface": "android",
"id": "native.android.8452bcda8a619361"
},
{
"kind": "ui-named-argument",
"line": 1287,
@ -4105,17 +4417,9 @@
"surface": "android",
"id": "native.android.45005deba537cc59"
},
{
"kind": "ui-named-argument",
"line": 1772,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt",
"source": "Open ${row.title}",
"surface": "android",
"id": "native.android.8452bcda8a619361"
},
{
"kind": "conditional-branch",
"line": 1791,
"line": 1793,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/ShellScreen.kt",
"source": "Main session",
"surface": "android",
@ -5445,98 +5749,18 @@
"kind": "resource-string",
"line": 25,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Gateway Setup",
"surface": "android",
"id": "native.android.3d9d01cc0af8727d"
},
{
"kind": "resource-string",
"line": 26,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Connect to your Gateway",
"surface": "android",
"id": "native.android.fff6a6f130e88354"
},
{
"kind": "resource-string",
"line": 27,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Scan setup code",
"surface": "android",
"id": "native.android.77a54beda22bfeb7"
},
{
"kind": "resource-string",
"line": 28,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Use your Gateway QR or setup code",
"surface": "android",
"id": "native.android.8f525f9218215b42"
},
{
"kind": "resource-string",
"line": 29,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Nearby gateway",
"surface": "android",
"id": "native.android.40da32c36971b959"
},
{
"kind": "resource-string",
"line": 30,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Enter gateway URL",
"surface": "android",
"id": "native.android.7423bd0086e6897e"
},
{
"kind": "resource-string",
"line": 31,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Connect using a manual URL",
"surface": "android",
"id": "native.android.c9420b6801f5a76f"
},
{
"kind": "resource-string",
"line": 32,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Permissions",
"surface": "android",
"id": "native.android.4b55356d9bf6e444"
},
{
"kind": "resource-string",
"line": 33,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Done",
"surface": "android",
"id": "native.android.c49a911b95af5d5d"
},
{
"kind": "resource-string",
"line": 34,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Verify the certificate fingerprint before trusting this gateway.\n\n%1$s",
"surface": "android",
"id": "native.android.288b83f93172c633"
},
{
"kind": "resource-string",
"line": 35,
"line": 26,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "The gateway certificate changed. Continue only if you expected this.\n\nOld SHA-256:\n%1$s\n\nNew SHA-256:\n%2$s",
"surface": "android",
"id": "native.android.6ad2d3f084142bed"
},
{
"kind": "resource-string",
"line": 36,
"path": "apps/android/app/src/main/res/values/strings.xml",
"source": "Gateway Recovery",
"surface": "android",
"id": "native.android.6ffa3d752514836d"
},
{
"kind": "plist-string",
"line": 8,

View file

@ -230,8 +230,9 @@ dependencies {
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.video)
implementation(libs.play.services.code.scanner)
implementation(libs.barcode.scanning)
// Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
implementation(libs.dnsjava)

View file

@ -5,6 +5,7 @@ import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.gateway.DeviceAuthEntry
import ai.openclaw.app.gateway.DeviceAuthStore
import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewayDiscovery
@ -760,6 +761,9 @@ class NodeRuntime(
updateStatus {
nodeConnectionProblem = gatewayConnectionProblem(error, pauseReconnect)
}
if (operatorConnected && nodeConnectFailureNeedsApprovalRefresh(error)) {
scope.launch { refreshNodesDevicesFromGateway() }
}
},
onEvent = { _, _ -> },
onInvoke = { req ->
@ -1851,10 +1855,13 @@ class NodeRuntime(
) {
activeGatewayAuth = auth
val tls = connectionManager.resolveTlsParams(endpoint)
val storedOperatorEntry = loadStoredRoleDeviceAuthEntry("operator")
val usesStoredOperatorDeviceToken =
operatorSessionUsesStoredDeviceToken(auth, storedOperatorEntry?.token)
val operatorAuth =
resolveOperatorSessionConnectAuth(
auth = auth,
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
storedOperatorToken = storedOperatorEntry?.token,
)
if (operatorAuth == null) {
updateStatus {
@ -1869,7 +1876,13 @@ class NodeRuntime(
operatorAuth.token,
operatorAuth.bootstrapToken,
operatorAuth.password,
connectionManager.buildOperatorConnectOptions(),
connectionManager.buildOperatorConnectOptions(
scopes =
operatorConnectScopesForAuth(
usesStoredDeviceToken = usesStoredOperatorDeviceToken,
storedOperatorScopes = storedOperatorEntry?.scopes,
),
),
tls,
)
}
@ -2015,9 +2028,9 @@ class NodeRuntime(
connect(GatewayEndpoint.manual(host = host, port = port))
}
private fun loadStoredRoleDeviceToken(role: String): String? {
private fun loadStoredRoleDeviceAuthEntry(role: String): DeviceAuthEntry? {
val deviceId = identityStore.loadOrCreate().deviceId
return deviceAuthStore.loadToken(deviceId, role)
return deviceAuthStore.loadEntry(deviceId, role)
}
private fun maybeStartOperatorSessionAfterNodeConnect(
@ -2027,10 +2040,13 @@ class NodeRuntime(
if (operatorConnected) {
return
}
val storedOperatorEntry = loadStoredRoleDeviceAuthEntry("operator")
val usesStoredOperatorDeviceToken =
operatorSessionUsesStoredDeviceToken(auth, storedOperatorEntry?.token)
val operatorAuth =
resolveOperatorSessionConnectAuth(
auth = auth,
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
storedOperatorToken = storedOperatorEntry?.token,
) ?: return
updateStatus {
operatorStatusText = "Connecting…"
@ -2041,7 +2057,13 @@ class NodeRuntime(
operatorAuth.token,
operatorAuth.bootstrapToken,
operatorAuth.password,
connectionManager.buildOperatorConnectOptions(),
connectionManager.buildOperatorConnectOptions(
scopes =
operatorConnectScopesForAuth(
usesStoredDeviceToken = usesStoredOperatorDeviceToken,
storedOperatorScopes = storedOperatorEntry?.scopes,
),
),
connectionManager.resolveTlsParams(endpoint),
)
}
@ -2449,6 +2471,7 @@ class NodeRuntime(
if (!refreshStarted) return
if (!operatorConnected) {
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodeCapabilityApproval.value = GatewayNodeCapabilityApproval.Loading
_nodesDevicesSummary.value =
GatewayNodesDevicesSummary(
nodes = emptyList(),
@ -2462,12 +2485,14 @@ class NodeRuntime(
try {
val nodesRes = operatorSession.request("node.list", "{}")
val nodesRoot = json.parseToJsonElement(nodesRes).asObjectOrNull()
val nodes = parseGatewayNodes(nodesRoot?.get("nodes") as? JsonArray)
val nodes = parseGatewayNodeList(nodesRoot)
val selfNodeId = identityStore.loadOrCreate().deviceId
val approval =
currentNodeCapabilityApproval(
nodes = nodes,
selfNodeId = identityStore.loadOrCreate().deviceId,
selfNodeId = selfNodeId,
)
val selfNodeConnected = nodes.firstOrNull { it.id == selfNodeId }?.connected == true
val publishedApproval =
nodeApprovalRefreshGuard.publishIfCurrent(refreshGeneration) {
_nodeCapabilityApproval.value = approval
@ -2475,6 +2500,13 @@ class NodeRuntime(
if (!publishedApproval) {
return
}
if (selfNodeConnected && !_nodeConnected.value) {
updateStatus {
nodeConnectionProblem = null
_nodeConnected.value = true
nodeStatusText = "Connected"
}
}
scheduleNodeApprovalCommandRefresh(refreshGeneration, approval)
val devicesRoot =
try {
@ -3409,6 +3441,27 @@ internal fun resolveOperatorSessionConnectAuth(
)
}
internal fun operatorSessionUsesStoredDeviceToken(
auth: NodeRuntime.GatewayConnectAuth,
storedOperatorToken: String?,
): Boolean {
val storedToken = storedOperatorToken?.trim()?.takeIf { it.isNotEmpty() }
if (storedToken == null) return false
val explicitToken = auth.token?.trim()?.takeIf { it.isNotEmpty() }
val explicitPassword = auth.password?.trim()?.takeIf { it.isNotEmpty() }
return explicitToken == null && explicitPassword == null
}
internal fun operatorConnectScopesForAuth(
usesStoredDeviceToken: Boolean,
storedOperatorScopes: List<String>?,
): List<String> {
if (usesStoredDeviceToken && storedOperatorScopes != null) {
return ConnectionManager.operatorScopesForStoredDeviceToken(storedOperatorScopes)
}
return ConnectionManager.nativeClientOperatorScopes
}
private enum class HomeCanvasGatewayState {
Connected,
Connecting,
@ -3572,6 +3625,12 @@ internal fun parseGatewayNodeApprovalState(raw: String?): GatewayNodeApprovalSta
else -> GatewayNodeApprovalState.Loading
}
internal fun gatewayEventInvalidatesNodesDevices(event: String): Boolean =
event == "node.pair.requested" || event == "node.pair.resolved"
internal fun nodeConnectFailureNeedsApprovalRefresh(error: GatewaySession.ErrorShape): Boolean =
error.details?.code == "PAIRING_REQUIRED"
internal fun currentNodeCapabilityApproval(
nodes: List<GatewayNodeSummary>,
selfNodeId: String,
@ -3618,6 +3677,25 @@ internal fun parseGatewayNodeSummary(item: JsonElement): GatewayNodeSummary? {
)
}
internal fun parseGatewayNodeList(root: JsonObject?): List<GatewayNodeSummary> {
if (root == null) return emptyList()
val seen = mutableSetOf<String>()
val result = mutableListOf<GatewayNodeSummary>()
fun append(nodes: JsonArray?) {
for (node in nodes?.mapNotNull(::parseGatewayNodeSummary).orEmpty()) {
if (seen.add(node.id)) {
result.add(node)
}
}
}
append(root["nodes"] as? JsonArray)
append(root["pending"] as? JsonArray)
append(root["paired"] as? JsonArray)
return result
}
data class GatewayNodeSummary(
val id: String,
val displayName: String?,

View file

@ -41,6 +41,7 @@ class SecurePrefs(
"notifications.forwarding.maxEventsPerMinute"
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
private const val installedAppsSharingEnabledKey = "device.apps.sharing.enabled"
private const val cameraEnabledKey = "camera.enabled"
private const val voiceMicEnabledKey = "voice.micEnabled"
private const val appearanceThemeModeKey = "appearance.themeMode"
}
@ -51,6 +52,7 @@ class SecurePrefs(
// Non-secret UI/runtime preferences stay readable for migration and backup behavior.
private val plainPrefs: SharedPreferences =
appContext.getSharedPreferences(plainPrefsName, Context.MODE_PRIVATE)
private val hadPlainPrefsBeforeInit = plainPrefs.all.isNotEmpty()
// Gateway credentials and arbitrary secret strings are isolated behind EncryptedSharedPreferences.
private val masterKey by lazy {
@ -68,7 +70,7 @@ class SecurePrefs(
MutableStateFlow(loadOrMigrateDisplayName(context = context))
val displayName: StateFlow<String> = _displayName
private val _cameraEnabled = MutableStateFlow(plainPrefs.getBoolean("camera.enabled", true))
private val _cameraEnabled = MutableStateFlow(loadCameraEnabled())
val cameraEnabled: StateFlow<Boolean> = _cameraEnabled
private val _locationMode = MutableStateFlow(loadLocationMode())
@ -199,7 +201,7 @@ class SecurePrefs(
}
fun setCameraEnabled(value: Boolean) {
plainPrefs.edit { putBoolean("camera.enabled", value) }
plainPrefs.edit { putBoolean(cameraEnabledKey, value) }
_cameraEnabled.value = value
}
@ -572,6 +574,15 @@ class SecurePrefs(
return resolved
}
private fun loadCameraEnabled(): Boolean {
if (plainPrefs.contains(cameraEnabledKey)) {
return plainPrefs.getBoolean(cameraEnabledKey, false)
}
val migratedValue = hadPlainPrefsBeforeInit
plainPrefs.edit { putBoolean(cameraEnabledKey, migratedValue) }
return migratedValue
}
private fun loadWakeWords(): List<String> {
val raw = plainPrefs.getString("voiceWake.triggerWords", null)?.trim()
if (raw.isNullOrEmpty()) return defaultWakeWords

View file

@ -705,6 +705,7 @@ class GatewaySession(
setOf(
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
)
scopes.filter { allowedOperatorScopes.contains(it) }.distinct().sorted()

View file

@ -32,6 +32,30 @@ class ConnectionManager(
private val manualTls: () -> Boolean,
) {
companion object {
internal val legacyOperatorScopes: List<String> =
listOf(
"operator.approvals",
"operator.read",
"operator.write",
)
internal val nativeClientOperatorScopes: List<String> =
listOf(
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
)
internal fun operatorScopesForStoredDeviceToken(storedScopes: List<String>): List<String> {
val normalized =
storedScopes
.map { it.trim() }
.filter { it.isNotEmpty() }
.distinct()
return normalized.ifEmpty { legacyOperatorScopes }
}
/**
* Decide whether a discovered/manual endpoint must use pinned TLS or can stay local cleartext.
*/
@ -187,15 +211,12 @@ class ConnectionManager(
)
/** Connect options for the Android operator session that drives approvals and UI actions. */
fun buildOperatorConnectOptions(): GatewayConnectOptions =
fun buildOperatorConnectOptions(
scopes: List<String> = nativeClientOperatorScopes,
): GatewayConnectOptions =
GatewayConnectOptions(
role = "operator",
scopes =
listOf(
"operator.approvals",
"operator.read",
"operator.write",
),
scopes = scopes,
caps = emptyList(),
commands = emptyList(),
permissions = emptyMap(),

View file

@ -92,7 +92,7 @@ internal fun resolveGatewayConnectConfig(
passwordInput: String,
): GatewayConnectConfig? {
if (useSetupCode) {
val setup = decodeGatewaySetupCode(setupCode) ?: return null
val setup = resolveSetupCodeCandidate(setupCode)?.let(::decodeGatewaySetupCode) ?: return null
val parsed = parseGatewayEndpointResult(setup.url).config ?: return null
val setupBootstrapToken =
setup.bootstrapToken
@ -264,6 +264,8 @@ internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
}
}
internal fun manualTokenLooksLikeSetupCode(rawInput: String): Boolean = resolveSetupCodeCandidate(rawInput)?.let(::decodeGatewaySetupCode) != null
/** Resolves QR scanner text to setup-code or validation error for UI copy. */
internal fun resolveScannedSetupCodeResult(rawInput: String): GatewayScannedSetupCodeResult {
val setupCode =

View file

@ -1459,9 +1459,9 @@ private fun SettingsShellScreen(
}
item {
SettingsGroup(
rows = listOf(SettingsRow("Sign Out", "Disconnect", Icons.AutoMirrored.Filled.ExitToApp)),
rows = listOf(SettingsRow("Sign Out", "Return to setup", Icons.AutoMirrored.Filled.ExitToApp)),
onOpen = { },
onAction = { viewModel.disconnect() },
onAction = { viewModel.pairNewGateway() },
)
}
@ -1724,6 +1724,7 @@ private fun SettingsGroup(
rows.forEachIndexed { index, row ->
SettingsListRow(
row = row,
showDisclosure = row.route != null || onAction != null,
onClick = {
val rowRoute = row.route
if (rowRoute == null) {
@ -1744,6 +1745,7 @@ private fun SettingsGroup(
@Composable
private fun SettingsListRow(
row: SettingsRow,
showDisclosure: Boolean,
onClick: () -> Unit,
) {
Row(
@ -1766,10 +1768,10 @@ private fun SettingsListRow(
row.status?.let { active ->
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (active) ClawTheme.colors.success else ClawTheme.colors.textSubtle))
}
if (row.route != null) {
if (showDisclosure) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Open ${row.title}",
contentDescription = if (row.route != null) "Open ${row.title}" else row.title,
modifier = Modifier.size(17.dp),
tint = ClawTheme.colors.text,
)

View file

@ -22,16 +22,6 @@
<string name="password">كلمة المرور</string>
<string name="run_onboarding_again">تشغيل الإعداد الأولي مرة أخرى</string>
<string name="resolved_endpoint">نقطة النهاية التي تم حلها</string>
<string name="gateway_setup">إعداد البوابة</string>
<string name="connect_to_gateway">الاتصال ببوابتك</string>
<string name="scan_setup_code">مسح رمز الإعداد</string>
<string name="use_gateway_qr">استخدم رمز QR الخاص ببوابتك أو رمز الإعداد</string>
<string name="nearby_gateway">بوابة قريبة</string>
<string name="enter_gateway_url">أدخل عنوان URL للبوابة</string>
<string name="connect_manual_url">الاتصال باستخدام عنوان URL يدوي</string>
<string name="permissions">الأذونات</string>
<string name="done">تم</string>
<string name="gateway_trust_first_seen">تحقق من بصمة الشهادة قبل الوثوق بهذه البوابة.\n\n%1$s</string>
<string name="gateway_trust_changed">تم تغيير شهادة البوابة. تابع فقط إذا كنت تتوقع هذا التغيير.\n\nSHA-256 القديم:\n%1$s\n\nSHA-256 الجديد:\n%2$s</string>
<string name="gateway_recovery">استرداد البوابة</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Passwort</string>
<string name="run_onboarding_again">Onboarding erneut ausführen</string>
<string name="resolved_endpoint">Aufgelöster Endpunkt</string>
<string name="gateway_setup">Gateway-Einrichtung</string>
<string name="connect_to_gateway">Mit Ihrem Gateway verbinden</string>
<string name="scan_setup_code">Einrichtungscode scannen</string>
<string name="use_gateway_qr">Verwenden Sie Ihren Gateway-QR- oder Einrichtungscode</string>
<string name="nearby_gateway">Gateway in der Nähe</string>
<string name="enter_gateway_url">Gateway-URL eingeben</string>
<string name="connect_manual_url">Über eine manuelle URL verbinden</string>
<string name="permissions">Berechtigungen</string>
<string name="done">Fertig</string>
<string name="gateway_trust_first_seen">Überprüfen Sie den Zertifikatfingerabdruck, bevor Sie diesem Gateway vertrauen.\n\n%1$s</string>
<string name="gateway_trust_changed">Das Gateway-Zertifikat wurde geändert. Fahren Sie nur fort, wenn Sie diese Änderung erwartet haben.\n\nAlter SHA-256-Wert:\n%1$s\n\nNeuer SHA-256-Wert:\n%2$s</string>
<string name="gateway_recovery">Gateway-Wiederherstellung</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Contraseña</string>
<string name="run_onboarding_again">Ejecutar la incorporación de nuevo</string>
<string name="resolved_endpoint">Endpoint resuelto</string>
<string name="gateway_setup">Configuración de Gateway</string>
<string name="connect_to_gateway">Conéctate a tu Gateway</string>
<string name="scan_setup_code">Escanear código de configuración</string>
<string name="use_gateway_qr">Usa el QR o código de configuración de tu Gateway</string>
<string name="nearby_gateway">Gateway cercano</string>
<string name="enter_gateway_url">Introduce la URL del gateway</string>
<string name="connect_manual_url">Conectar usando una URL manual</string>
<string name="permissions">Permisos</string>
<string name="done">Listo</string>
<string name="gateway_trust_first_seen">Verifica la huella digital del certificado antes de confiar en este gateway.\n\n%1$s</string>
<string name="gateway_trust_changed">El certificado del gateway cambió. Continúa solo si esperabas este cambio.\n\nSHA-256 anterior:\n%1$s\n\nSHA-256 nuevo:\n%2$s</string>
<string name="gateway_recovery">Recuperación del gateway</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">رمز عبور</string>
<string name="run_onboarding_again">اجرای دوباره فرایند شروع به کار</string>
<string name="resolved_endpoint">نقطه پایانی حل‌شده</string>
<string name="gateway_setup">راه‌اندازی دروازه</string>
<string name="connect_to_gateway">به دروازه خود متصل شوید</string>
<string name="scan_setup_code">اسکن کد راه‌اندازی</string>
<string name="use_gateway_qr">از QR دروازه یا کد راه‌اندازی خود استفاده کنید</string>
<string name="nearby_gateway">دروازه نزدیک</string>
<string name="enter_gateway_url">URL دروازه را وارد کنید</string>
<string name="connect_manual_url">اتصال با استفاده از URL دستی</string>
<string name="permissions">مجوزها</string>
<string name="done">انجام شد</string>
<string name="gateway_trust_first_seen">پیش از اعتماد به این دروازه، اثر انگشت گواهی را تأیید کنید.\n\n%1$s</string>
<string name="gateway_trust_changed">گواهی دروازه تغییر کرده است. فقط در صورتی ادامه دهید که انتظار این تغییر را داشتید.\n\nSHA-256 قدیمی:\n%1$s\n\nSHA-256 جدید:\n%2$s</string>
<string name="gateway_recovery">بازیابی دروازه</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Mot de passe</string>
<string name="run_onboarding_again">Relancer lintégration</string>
<string name="resolved_endpoint">Point de terminaison résolu</string>
<string name="gateway_setup">Configuration de la passerelle</string>
<string name="connect_to_gateway">Connectez-vous à votre Gateway</string>
<string name="scan_setup_code">Scanner le code de configuration</string>
<string name="use_gateway_qr">Utilisez le QR de votre Gateway ou le code de configuration</string>
<string name="nearby_gateway">Passerelle à proximité</string>
<string name="enter_gateway_url">Saisir lURL de la passerelle</string>
<string name="connect_manual_url">Se connecter avec une URL manuelle</string>
<string name="permissions">Autorisations</string>
<string name="done">Terminé</string>
<string name="gateway_trust_first_seen">Vérifiez lempreinte du certificat avant daccorder votre confiance à cette passerelle.\n\n%1$s</string>
<string name="gateway_trust_changed">Le certificat de la passerelle a changé. Continuez uniquement si vous vous attendiez à ce changement.\n\nAncien SHA-256 :\n%1$s\n\nNouveau SHA-256 :\n%2$s</string>
<string name="gateway_recovery">Récupération de la passerelle</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">पासवर्ड</string>
<string name="run_onboarding_again">ऑनबोर्डिंग फिर से चलाएँ</string>
<string name="resolved_endpoint">रिज़ॉल्व किया गया एंडपॉइंट</string>
<string name="gateway_setup">गेटवे सेटअप</string>
<string name="connect_to_gateway">अपने गेटवे से कनेक्ट करें</string>
<string name="scan_setup_code">सेटअप कोड स्कैन करें</string>
<string name="use_gateway_qr">अपने गेटवे QR या सेटअप कोड का उपयोग करें</string>
<string name="nearby_gateway">नज़दीकी गेटवे</string>
<string name="enter_gateway_url">गेटवे URL दर्ज करें</string>
<string name="connect_manual_url">मैन्युअल URL का उपयोग करके कनेक्ट करें</string>
<string name="permissions">अनुमतियाँ</string>
<string name="done">हो गया</string>
<string name="gateway_trust_first_seen">इस गेटवे पर भरोसा करने से पहले प्रमाणपत्र फ़िंगरप्रिंट सत्यापित करें।\n\n%1$s</string>
<string name="gateway_trust_changed">गेटवे प्रमाणपत्र बदल गया है। केवल तभी जारी रखें जब आपको इस बदलाव की अपेक्षा थी।\n\nपुराना SHA-256:\n%1$s\n\nनया SHA-256:\n%2$s</string>
<string name="gateway_recovery">गेटवे पुनर्प्राप्ति</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Kata sandi</string>
<string name="run_onboarding_again">Jalankan onboarding lagi</string>
<string name="resolved_endpoint">Endpoint yang diselesaikan</string>
<string name="gateway_setup">Penyiapan Gateway</string>
<string name="connect_to_gateway">Hubungkan ke Gateway Anda</string>
<string name="scan_setup_code">Pindai kode penyiapan</string>
<string name="use_gateway_qr">Gunakan QR Gateway atau kode penyiapan Anda</string>
<string name="nearby_gateway">Gateway terdekat</string>
<string name="enter_gateway_url">Masukkan URL gateway</string>
<string name="connect_manual_url">Hubungkan menggunakan URL manual</string>
<string name="permissions">Izin</string>
<string name="done">Selesai</string>
<string name="gateway_trust_first_seen">Verifikasi sidik jari sertifikat sebelum memercayai gateway ini.\n\n%1$s</string>
<string name="gateway_trust_changed">Sertifikat gateway berubah. Lanjutkan hanya jika Anda mengharapkan perubahan ini.\n\nSHA-256 lama:\n%1$s\n\nSHA-256 baru:\n%2$s</string>
<string name="gateway_recovery">Pemulihan gateway</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Password</string>
<string name="run_onboarding_again">Esegui di nuovo lonboarding</string>
<string name="resolved_endpoint">Endpoint risolto</string>
<string name="gateway_setup">Configurazione gateway</string>
<string name="connect_to_gateway">Connettiti al tuo Gateway</string>
<string name="scan_setup_code">Scansiona codice di configurazione</string>
<string name="use_gateway_qr">Usa il QR del tuo Gateway o il codice di configurazione</string>
<string name="nearby_gateway">Gateway nelle vicinanze</string>
<string name="enter_gateway_url">Inserisci URL del gateway</string>
<string name="connect_manual_url">Connetti usando un URL manuale</string>
<string name="permissions">Autorizzazioni</string>
<string name="done">Fine</string>
<string name="gateway_trust_first_seen">Verifica limpronta digitale del certificato prima di considerare attendibile questo gateway.\n\n%1$s</string>
<string name="gateway_trust_changed">Il certificato del gateway è cambiato. Continua solo se ti aspettavi questa modifica.\n\nSHA-256 precedente:\n%1$s\n\nNuovo SHA-256:\n%2$s</string>
<string name="gateway_recovery">Ripristino del gateway</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">パスワード</string>
<string name="run_onboarding_again">オンボーディングを再実行</string>
<string name="resolved_endpoint">解決済みエンドポイント</string>
<string name="gateway_setup">ゲートウェイ設定</string>
<string name="connect_to_gateway">ゲートウェイに接続</string>
<string name="scan_setup_code">セットアップコードをスキャン</string>
<string name="use_gateway_qr">ゲートウェイの QR またはセットアップコードを使用</string>
<string name="nearby_gateway">近くのゲートウェイ</string>
<string name="enter_gateway_url">ゲートウェイ URL を入力</string>
<string name="connect_manual_url">手動 URL で接続</string>
<string name="permissions">権限</string>
<string name="done">完了</string>
<string name="gateway_trust_first_seen">このゲートウェイを信頼する前に、証明書のフィンガープリントを確認してください。\n\n%1$s</string>
<string name="gateway_trust_changed">ゲートウェイの証明書が変更されました。想定した変更である場合のみ続行してください。\n\n以前の SHA-256:\n%1$s\n\n新しい SHA-256:\n%2$s</string>
<string name="gateway_recovery">ゲートウェイの復旧</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">비밀번호</string>
<string name="run_onboarding_again">온보딩 다시 실행</string>
<string name="resolved_endpoint">확인된 엔드포인트</string>
<string name="gateway_setup">게이트웨이 설정</string>
<string name="connect_to_gateway">게이트웨이에 연결</string>
<string name="scan_setup_code">설정 코드 스캔</string>
<string name="use_gateway_qr">게이트웨이 QR 또는 설정 코드 사용</string>
<string name="nearby_gateway">주변 게이트웨이</string>
<string name="enter_gateway_url">게이트웨이 URL 입력</string>
<string name="connect_manual_url">수동 URL을 사용하여 연결</string>
<string name="permissions">권한</string>
<string name="done">완료</string>
<string name="gateway_trust_first_seen">이 게이트웨이를 신뢰하기 전에 인증서 지문을 확인하세요.\n\n%1$s</string>
<string name="gateway_trust_changed">게이트웨이 인증서가 변경되었습니다. 예상한 변경인 경우에만 계속하세요.\n\n이전 SHA-256:\n%1$s\n\n새 SHA-256:\n%2$s</string>
<string name="gateway_recovery">게이트웨이 복구</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Wachtwoord</string>
<string name="run_onboarding_again">Onboarding opnieuw uitvoeren</string>
<string name="resolved_endpoint">Opgelost endpoint</string>
<string name="gateway_setup">Gateway instellen</string>
<string name="connect_to_gateway">Verbinden met je Gateway</string>
<string name="scan_setup_code">Setupcode scannen</string>
<string name="use_gateway_qr">Gebruik je Gateway-QR-code of setupcode</string>
<string name="nearby_gateway">Gateway in de buurt</string>
<string name="enter_gateway_url">Gateway-URL invoeren</string>
<string name="connect_manual_url">Verbinden met een handmatige URL</string>
<string name="permissions">Machtigingen</string>
<string name="done">Gereed</string>
<string name="gateway_trust_first_seen">Controleer de certificaatvingerafdruk voordat je deze gateway vertrouwt.\n\n%1$s</string>
<string name="gateway_trust_changed">Het gatewaycertificaat is gewijzigd. Ga alleen door als je deze wijziging verwachtte.\n\nOude SHA-256:\n%1$s\n\nNieuwe SHA-256:\n%2$s</string>
<string name="gateway_recovery">Gatewayherstel</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Hasło</string>
<string name="run_onboarding_again">Uruchom ponownie wdrażanie</string>
<string name="resolved_endpoint">Rozpoznany punkt końcowy</string>
<string name="gateway_setup">Konfiguracja bramy</string>
<string name="connect_to_gateway">Połącz ze swoją bramą</string>
<string name="scan_setup_code">Zeskanuj kod konfiguracji</string>
<string name="use_gateway_qr">Użyj kodu QR bramy lub kodu konfiguracji</string>
<string name="nearby_gateway">Pobliska brama</string>
<string name="enter_gateway_url">Wprowadź URL bramy</string>
<string name="connect_manual_url">Połącz, używając ręcznego URL</string>
<string name="permissions">Uprawnienia</string>
<string name="done">Gotowe</string>
<string name="gateway_trust_first_seen">Sprawdź odcisk certyfikatu, zanim zaufasz tej bramie.\n\n%1$s</string>
<string name="gateway_trust_changed">Certyfikat bramy uległ zmianie. Kontynuuj tylko wtedy, gdy oczekujesz tej zmiany.\n\nStary SHA-256:\n%1$s\n\nNowy SHA-256:\n%2$s</string>
<string name="gateway_recovery">Odzyskiwanie bramy</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Senha</string>
<string name="run_onboarding_again">Executar integração novamente</string>
<string name="resolved_endpoint">Endpoint resolvido</string>
<string name="gateway_setup">Configuração do Gateway</string>
<string name="connect_to_gateway">Conecte-se ao seu Gateway</string>
<string name="scan_setup_code">Escanear código de configuração</string>
<string name="use_gateway_qr">Use o QR do seu Gateway ou o código de configuração</string>
<string name="nearby_gateway">Gateway próximo</string>
<string name="enter_gateway_url">Inserir URL do gateway</string>
<string name="connect_manual_url">Conectar usando uma URL manual</string>
<string name="permissions">Permissões</string>
<string name="done">Concluído</string>
<string name="gateway_trust_first_seen">Verifique a impressão digital do certificado antes de confiar neste gateway.\n\n%1$s</string>
<string name="gateway_trust_changed">O certificado do gateway foi alterado. Continue somente se você esperava essa alteração.\n\nSHA-256 antigo:\n%1$s\n\nSHA-256 novo:\n%2$s</string>
<string name="gateway_recovery">Recuperação do gateway</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Пароль</string>
<string name="run_onboarding_again">Запустить настройку заново</string>
<string name="resolved_endpoint">Разрешенная конечная точка</string>
<string name="gateway_setup">Настройка шлюза</string>
<string name="connect_to_gateway">Подключитесь к своему шлюзу</string>
<string name="scan_setup_code">Сканировать код настройки</string>
<string name="use_gateway_qr">Используйте QR-код или код настройки вашего шлюза</string>
<string name="nearby_gateway">Шлюз поблизости</string>
<string name="enter_gateway_url">Введите URL шлюза</string>
<string name="connect_manual_url">Подключиться с помощью URL вручную</string>
<string name="permissions">Разрешения</string>
<string name="done">Готово</string>
<string name="gateway_trust_first_seen">Проверьте отпечаток сертификата, прежде чем доверять этому шлюзу.\n\n%1$s</string>
<string name="gateway_trust_changed">Сертификат шлюза изменился. Продолжайте, только если вы ожидали это изменение.\n\nСтарый SHA-256:\n%1$s\n\nНовый SHA-256:\n%2$s</string>
<string name="gateway_recovery">Восстановление шлюза</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Lösenord</string>
<string name="run_onboarding_again">Kör introduktionen igen</string>
<string name="resolved_endpoint">Löst slutpunkt</string>
<string name="gateway_setup">Gateway-konfiguration</string>
<string name="connect_to_gateway">Anslut till din gateway</string>
<string name="scan_setup_code">Skanna konfigurationskod</string>
<string name="use_gateway_qr">Använd gatewayens QR-kod eller konfigurationskod</string>
<string name="nearby_gateway">Gateway i närheten</string>
<string name="enter_gateway_url">Ange gateway-URL</string>
<string name="connect_manual_url">Anslut med en manuell URL</string>
<string name="permissions">Behörigheter</string>
<string name="done">Klar</string>
<string name="gateway_trust_first_seen">Verifiera certifikatets fingeravtryck innan du litar på denna gateway.\n\n%1$s</string>
<string name="gateway_trust_changed">Gateway-certifikatet har ändrats. Fortsätt bara om du förväntade dig denna ändring.\n\nTidigare SHA-256:\n%1$s\n\nNy SHA-256:\n%2$s</string>
<string name="gateway_recovery">Gateway-återställning</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">รหัสผ่าน</string>
<string name="run_onboarding_again">เรียกใช้การเริ่มต้นใช้งานอีกครั้ง</string>
<string name="resolved_endpoint">เอนด์พอยต์ที่แก้ไขแล้ว</string>
<string name="gateway_setup">การตั้งค่าเกตเวย์</string>
<string name="connect_to_gateway">เชื่อมต่อกับเกตเวย์ของคุณ</string>
<string name="scan_setup_code">สแกนรหัสตั้งค่า</string>
<string name="use_gateway_qr">ใช้ QR ของเกตเวย์หรือรหัสตั้งค่าของคุณ</string>
<string name="nearby_gateway">เกตเวย์ใกล้เคียง</string>
<string name="enter_gateway_url">ป้อน URL เกตเวย์</string>
<string name="connect_manual_url">เชื่อมต่อโดยใช้ URL ด้วยตนเอง</string>
<string name="permissions">สิทธิ์</string>
<string name="done">เสร็จสิ้น</string>
<string name="gateway_trust_first_seen">ตรวจสอบลายนิ้วมือของใบรับรองก่อนเชื่อถือเกตเวย์นี้\n\n%1$s</string>
<string name="gateway_trust_changed">ใบรับรองของเกตเวย์มีการเปลี่ยนแปลง ดำเนินการต่อเมื่อคุณคาดว่าจะมีการเปลี่ยนแปลงนี้เท่านั้น\n\nSHA-256 เดิม:\n%1$s\n\nSHA-256 ใหม่:\n%2$s</string>
<string name="gateway_recovery">การกู้คืนเกตเวย์</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Parola</string>
<string name="run_onboarding_again">Başlangıç sürecini tekrar çalıştır</string>
<string name="resolved_endpoint">Çözümlenen uç nokta</string>
<string name="gateway_setup">Ağ Geçidi Kurulumu</string>
<string name="connect_to_gateway">Ağ Geçidinize Bağlanın</string>
<string name="scan_setup_code">Kurulum kodunu tara</string>
<string name="use_gateway_qr">Gateway QR kodunuzu veya kurulum kodunuzu kullanın</string>
<string name="nearby_gateway">Yakındaki ağ geçidi</string>
<string name="enter_gateway_url">Ağ geçidi URLsini girin</string>
<string name="connect_manual_url">Manuel URL kullanarak bağlan</string>
<string name="permissions">İzinler</string>
<string name="done">Bitti</string>
<string name="gateway_trust_first_seen">Bu ağ geçidine güvenmeden önce sertifika parmak izini doğrulayın.\n\n%1$s</string>
<string name="gateway_trust_changed">Ağ geçidi sertifikası değişti. Yalnızca bu değişikliği bekliyorsanız devam edin.\n\nEski SHA-256:\n%1$s\n\nYeni SHA-256:\n%2$s</string>
<string name="gateway_recovery">Ağ geçidi kurtarma</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Пароль</string>
<string name="run_onboarding_again">Запустити адаптацію знову</string>
<string name="resolved_endpoint">Визначена кінцева точка</string>
<string name="gateway_setup">Налаштування шлюзу</string>
<string name="connect_to_gateway">Підключіться до свого шлюзу</string>
<string name="scan_setup_code">Сканувати код налаштування</string>
<string name="use_gateway_qr">Використайте QR-код свого шлюзу або код налаштування</string>
<string name="nearby_gateway">Шлюз поблизу</string>
<string name="enter_gateway_url">Введіть URL-адресу шлюзу</string>
<string name="connect_manual_url">Підключитися за допомогою URL-адреси вручну</string>
<string name="permissions">Дозволи</string>
<string name="done">Готово</string>
<string name="gateway_trust_first_seen">Перевірте відбиток сертифіката, перш ніж довіряти цьому шлюзу.\n\n%1$s</string>
<string name="gateway_trust_changed">Сертифікат шлюзу змінився. Продовжуйте, лише якщо ви очікували цю зміну.\n\nСтарий SHA-256:\n%1$s\n\nНовий SHA-256:\n%2$s</string>
<string name="gateway_recovery">Відновлення шлюзу</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Mật khẩu</string>
<string name="run_onboarding_again">Chạy hướng dẫn thiết lập lại</string>
<string name="resolved_endpoint">Điểm cuối đã phân giải</string>
<string name="gateway_setup">Thiết lập cổng</string>
<string name="connect_to_gateway">Kết nối với Gateway của bạn</string>
<string name="scan_setup_code">Quét mã thiết lập</string>
<string name="use_gateway_qr">Sử dụng mã QR Gateway hoặc mã thiết lập của bạn</string>
<string name="nearby_gateway">Cổng gần đây</string>
<string name="enter_gateway_url">Nhập URL cổng</string>
<string name="connect_manual_url">Kết nối bằng URL thủ công</string>
<string name="permissions">Quyền</string>
<string name="done">Xong</string>
<string name="gateway_trust_first_seen">Xác minh dấu vân tay chứng chỉ trước khi tin cậy cổng này.\n\n%1$s</string>
<string name="gateway_trust_changed">Chứng chỉ cổng đã thay đổi. Chỉ tiếp tục nếu bạn mong đợi thay đổi này.\n\nSHA-256 cũ:\n%1$s\n\nSHA-256 mới:\n%2$s</string>
<string name="gateway_recovery">Khôi phục cổng</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">密码</string>
<string name="run_onboarding_again">再次运行引导流程</string>
<string name="resolved_endpoint">已解析的端点</string>
<string name="gateway_setup">网关设置</string>
<string name="connect_to_gateway">连接到你的网关</string>
<string name="scan_setup_code">扫描设置代码</string>
<string name="use_gateway_qr">使用你的网关 QR 码或设置代码</string>
<string name="nearby_gateway">附近的网关</string>
<string name="enter_gateway_url">输入网关 URL</string>
<string name="connect_manual_url">使用手动 URL 连接</string>
<string name="permissions">权限</string>
<string name="done">完成</string>
<string name="gateway_trust_first_seen">验证证书指纹后再信任此网关。\n\n%1$s</string>
<string name="gateway_trust_changed">网关证书已更改。仅当这是您预期的更改时才继续。\n\n旧 SHA-256\n%1$s\n\n新 SHA-256\n%2$s</string>
<string name="gateway_recovery">网关恢复</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">密碼</string>
<string name="run_onboarding_again">再次執行新手導覽</string>
<string name="resolved_endpoint">已解析的端點</string>
<string name="gateway_setup">閘道設定</string>
<string name="connect_to_gateway">連接到您的閘道</string>
<string name="scan_setup_code">掃描設定碼</string>
<string name="use_gateway_qr">使用您的 Gateway QR 或設定碼</string>
<string name="nearby_gateway">附近的閘道</string>
<string name="enter_gateway_url">輸入閘道 URL</string>
<string name="connect_manual_url">使用手動 URL 連接</string>
<string name="permissions">權限</string>
<string name="done">完成</string>
<string name="gateway_trust_first_seen">請先驗證憑證指紋,再信任此閘道。\n\n%1$s</string>
<string name="gateway_trust_changed">閘道憑證已變更。僅在這是您預期的變更時繼續。\n\n舊 SHA-256\n%1$s\n\n新 SHA-256\n%2$s</string>
<string name="gateway_recovery">閘道復原</string>
</resources>

View file

@ -22,16 +22,6 @@
<string name="password">Password</string>
<string name="run_onboarding_again">Run onboarding again</string>
<string name="resolved_endpoint">Resolved endpoint</string>
<string name="gateway_setup">Gateway Setup</string>
<string name="connect_to_gateway">Connect to your Gateway</string>
<string name="scan_setup_code">Scan setup code</string>
<string name="use_gateway_qr">Use your Gateway QR or setup code</string>
<string name="nearby_gateway">Nearby gateway</string>
<string name="enter_gateway_url">Enter gateway URL</string>
<string name="connect_manual_url">Connect using a manual URL</string>
<string name="permissions">Permissions</string>
<string name="done">Done</string>
<string name="gateway_trust_first_seen">Verify the certificate fingerprint before trusting this gateway.\n\n%1$s</string>
<string name="gateway_trust_changed">The gateway certificate changed. Continue only if you expected this.\n\nOld SHA-256:\n%1$s\n\nNew SHA-256:\n%2$s</string>
<string name="gateway_recovery">Gateway Recovery</string>
</resources>

View file

@ -7,6 +7,7 @@ import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.gateway.GatewayTlsProbeFailure
import ai.openclaw.app.gateway.GatewayTlsProbeResult
import ai.openclaw.app.node.ConnectionManager
import ai.openclaw.app.node.InvokeDispatcher
import ai.openclaw.app.protocol.OpenClawTalkCommand
import ai.openclaw.app.voice.TalkModeManager
@ -212,6 +213,74 @@ class GatewayBootstrapAuthTest {
)
}
@Test
fun operatorConnectScopesForAuthUsesNativeScopesWhenNoStoredOperatorMetadata() {
assertEquals(
ConnectionManager.nativeClientOperatorScopes,
operatorConnectScopesForAuth(
usesStoredDeviceToken = false,
storedOperatorScopes = null,
),
)
}
@Test
fun operatorConnectScopesForAuthPreservesStoredScopesForReconnects() {
val storedScopes = listOf("operator.approvals", "operator.read", "operator.write")
assertEquals(
storedScopes,
operatorConnectScopesForAuth(
usesStoredDeviceToken = true,
storedOperatorScopes = storedScopes,
),
)
}
@Test
fun operatorConnectScopesForAuthFallsBackToLegacyScopesForOldStoredDeviceTokens() {
assertEquals(
ConnectionManager.legacyOperatorScopes,
operatorConnectScopesForAuth(
usesStoredDeviceToken = true,
storedOperatorScopes = emptyList(),
),
)
}
@Test
fun operatorConnectScopesForAuthUsesNativeScopesForExplicitReauth() {
assertEquals(
ConnectionManager.nativeClientOperatorScopes,
operatorConnectScopesForAuth(
usesStoredDeviceToken = false,
storedOperatorScopes = listOf("operator.approvals", "operator.read", "operator.write"),
),
)
}
@Test
fun operatorSessionUsesStoredDeviceTokenOnlyWithoutExplicitSharedAuth() {
assertTrue(
operatorSessionUsesStoredDeviceToken(
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = "bootstrap-1", password = null),
storedOperatorToken = "stored-token",
),
)
assertFalse(
operatorSessionUsesStoredDeviceToken(
auth = NodeRuntime.GatewayConnectAuth(token = "shared-token", bootstrapToken = null, password = null),
storedOperatorToken = "stored-token",
),
)
assertFalse(
operatorSessionUsesStoredDeviceToken(
auth = NodeRuntime.GatewayConnectAuth(token = null, bootstrapToken = null, password = "password"),
storedOperatorToken = "stored-token",
),
)
}
@Test
fun nodeConnectStartsOperatorAfterBootstrapHandoffWhenOperatorWasConnecting() {
val app = RuntimeEnvironment.getApplication()

View file

@ -1,6 +1,9 @@
package ai.openclaw.app
import ai.openclaw.app.gateway.GatewayConnectErrorDetails
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
@ -8,33 +11,6 @@ import org.junit.Assert.assertTrue
import org.junit.Test
class GatewayNodeApprovalStateTest {
@Test
fun exactApprovalCommandsAgeOutToStatusFallbacks() {
assertEquals(
GatewayNodeCapabilityApproval.PendingApproval(requestId = null),
GatewayNodeCapabilityApproval.PendingApproval(requestId = "request-1").withoutExactRequestId(),
)
assertEquals(
GatewayNodeCapabilityApproval.PendingReapproval(requestId = null),
GatewayNodeCapabilityApproval.PendingReapproval(requestId = "request-2").withoutExactRequestId(),
)
assertNull(GatewayNodeCapabilityApproval.PendingApproval(requestId = null).withoutExactRequestId())
val summary =
GatewayNodesDevicesSummary(
nodes = listOf(pendingNode(requestId = "request-1")),
pendingDevices = emptyList(),
pairedDevices = emptyList(),
)
assertNull(
summary
.withoutExactApprovalRequestIds()
.nodes
.single()
.pendingRequestId,
)
}
@Test
fun parsesGatewayNodeApprovalState() {
assertEquals(GatewayNodeApprovalState.Approved, parseGatewayNodeApprovalState("approved"))
@ -45,6 +21,48 @@ class GatewayNodeApprovalStateTest {
assertEquals(GatewayNodeApprovalState.Loading, parseGatewayNodeApprovalState("future-state"))
}
@Test
fun nodePairingEventsInvalidateNodeDeviceState() {
assertTrue(gatewayEventInvalidatesNodesDevices("node.pair.requested"))
assertTrue(gatewayEventInvalidatesNodesDevices("node.pair.resolved"))
assertFalse(gatewayEventInvalidatesNodesDevices("device.pair.resolved"))
assertFalse(gatewayEventInvalidatesNodesDevices("exec.approval.resolved"))
}
@Test
fun nodePairingFailuresRefreshNodeDeviceState() {
assertTrue(
nodeConnectFailureNeedsApprovalRefresh(
GatewaySession.ErrorShape(
code = "NOT_PAIRED",
message = "pairing required",
details =
GatewayConnectErrorDetails(
code = "PAIRING_REQUIRED",
canRetryWithDeviceToken = false,
recommendedNextStep = "wait_then_retry",
pauseReconnect = false,
reason = "not-paired",
),
),
),
)
assertFalse(
nodeConnectFailureNeedsApprovalRefresh(
GatewaySession.ErrorShape(
code = "UNAUTHORIZED",
message = "token mismatch",
details =
GatewayConnectErrorDetails(
code = "AUTH_TOKEN_MISMATCH",
canRetryWithDeviceToken = false,
recommendedNextStep = null,
),
),
),
)
}
@Test
fun parsesNodeListApprovalFields() {
val node =
@ -71,6 +89,45 @@ class GatewayNodeApprovalStateTest {
assertEquals(listOf("device.status"), node.commands)
}
@Test
fun parsesSplitNodeListShapeFromGateway() {
val root =
Json
.parseToJsonElement(
"""
{
"pending": [
{
"nodeId": "pending-node",
"paired": false,
"connected": false,
"approvalState": "pending-approval",
"pendingRequestId": "request-pending"
}
],
"paired": [
{
"nodeId": "self",
"paired": true,
"connected": true,
"approvalState": "approved",
"caps": ["device"],
"commands": ["device.status"]
}
]
}
""".trimIndent(),
).jsonObject
val nodes = parseGatewayNodeList(root)
assertEquals(2, nodes.size)
assertEquals(
GatewayNodeCapabilityApproval.Approved,
currentNodeCapabilityApproval(nodes = nodes, selfNodeId = "self"),
)
}
@Test
fun treatsMissingNodeApprovalStateAsUnsupported() {
val node =
@ -113,14 +170,14 @@ class GatewayNodeApprovalStateTest {
paired = true,
connected = true,
approvalState = GatewayNodeApprovalState.PendingApproval,
pendingRequestId = null,
pendingRequestId = "request-self",
capabilities = emptyList(),
commands = emptyList(),
),
)
assertEquals(
GatewayNodeCapabilityApproval.PendingApproval(requestId = null),
GatewayNodeCapabilityApproval.PendingApproval("request-self"),
currentNodeCapabilityApproval(nodes = nodes, selfNodeId = "self"),
)
assertEquals(
@ -129,47 +186,17 @@ class GatewayNodeApprovalStateTest {
)
}
@Test
fun currentPhoneApprovalCarriesOnlySafePendingRequestIds() {
val safe = pendingNode(requestId = "request-1")
val unsafe = pendingNode(requestId = "request-1;echo unsafe")
assertEquals(
GatewayNodeCapabilityApproval.PendingApproval("request-1"),
currentNodeCapabilityApproval(nodes = listOf(safe), selfNodeId = "self"),
)
assertEquals(
GatewayNodeCapabilityApproval.PendingApproval(requestId = null),
currentNodeCapabilityApproval(nodes = listOf(unsafe), selfNodeId = "self"),
)
}
@Test
fun ignoresStaleNodeApprovalRefreshResults() {
val guard = GatewayNodeApprovalRefreshGuard()
var approval: GatewayNodeCapabilityApproval = GatewayNodeCapabilityApproval.Loading
var approvalState = GatewayNodeApprovalState.Loading
val staleRefresh = guard.begin()
val currentRefresh = guard.begin()
assertFalse(guard.publishIfCurrent(staleRefresh) { approval = GatewayNodeCapabilityApproval.Approved })
assertFalse(guard.publishIfCurrent(staleRefresh) { approvalState = GatewayNodeApprovalState.Approved })
assertTrue(
guard.publishIfCurrent(currentRefresh) { approval = GatewayNodeCapabilityApproval.PendingReapproval("request-2") },
guard.publishIfCurrent(currentRefresh) { approvalState = GatewayNodeApprovalState.PendingReapproval },
)
assertEquals(GatewayNodeCapabilityApproval.PendingReapproval("request-2"), approval)
assertEquals(GatewayNodeApprovalState.PendingReapproval, approvalState)
}
private fun pendingNode(requestId: String): GatewayNodeSummary =
GatewayNodeSummary(
id = "self",
displayName = null,
remoteIp = null,
version = null,
deviceFamily = null,
paired = true,
connected = true,
approvalState = GatewayNodeApprovalState.PendingApproval,
pendingRequestId = requestId,
capabilities = emptyList(),
commands = emptyList(),
)
}

View file

@ -77,6 +77,37 @@ class SecurePrefsTest {
assertTrue(plainPrefs.getBoolean("device.apps.sharing.enabled", false))
}
@Test
fun cameraSharing_defaultsOffAndPersistsOptIn() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs.edit().clear().commit()
val prefs = SecurePrefs(context)
assertFalse(prefs.cameraEnabled.value)
assertFalse(plainPrefs.getBoolean("camera.enabled", true))
prefs.setCameraEnabled(true)
assertTrue(prefs.cameraEnabled.value)
assertTrue(plainPrefs.getBoolean("camera.enabled", false))
}
@Test
fun cameraSharing_migratesExistingInstallsToPreviousDefault() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs
.edit()
.clear()
.putString("node.instanceId", "existing-node")
.commit()
val prefs = SecurePrefs(context)
assertTrue(prefs.cameraEnabled.value)
assertTrue(plainPrefs.getBoolean("camera.enabled", false))
}
@Test
fun appearanceThemeMode_defaultsDarkForExistingInstalls() {
val context = RuntimeEnvironment.getApplication()

View file

@ -423,6 +423,7 @@ class GatewaySessionInvokeTest {
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.talk.secrets",
"operator.write",
),
)
@ -441,6 +442,7 @@ class GatewaySessionInvokeTest {
listOf(
"operator.approvals",
"operator.read",
"operator.talk.secrets",
"operator.write",
),
params.scopes(),
@ -602,7 +604,10 @@ class GatewaySessionInvokeTest {
assertEquals("bootstrap-node-token", nodeEntry?.token)
assertEquals(emptyList<String>(), nodeEntry?.scopes)
assertEquals("bootstrap-operator-token", operatorEntry?.token)
assertEquals(listOf("operator.approvals", "operator.read", "operator.write"), operatorEntry?.scopes)
assertEquals(
listOf("operator.approvals", "operator.read", "operator.talk.secrets", "operator.write"),
operatorEntry?.scopes,
)
} finally {
shutdownHarness(harness, server)
}

View file

@ -427,19 +427,33 @@ class ConnectionManagerTest {
}
@Test
fun buildOperatorConnectOptions_requestsQrBootstrapHandoffScopes() {
fun buildOperatorConnectOptions_requestsNativeClientOperatorScopes() {
val options = newManager().buildOperatorConnectOptions()
assertEquals(
listOf(
"operator.approvals",
"operator.read",
"operator.write",
),
ConnectionManager.nativeClientOperatorScopes,
options.scopes,
)
}
@Test
fun operatorScopesForStoredDeviceToken_preservesRecordedScopes() {
assertEquals(
listOf("operator.read", "operator.write"),
ConnectionManager.operatorScopesForStoredDeviceToken(
listOf("operator.read", "operator.write", "operator.read", " "),
),
)
}
@Test
fun operatorScopesForStoredDeviceToken_fallsBackToLegacyScopesWhenMetadataMissing() {
assertEquals(
ConnectionManager.legacyOperatorScopes,
ConnectionManager.operatorScopesForStoredDeviceToken(emptyList()),
)
}
@Test
fun buildNodeConnectOptions_advertisesRequestableSmsSearchWithoutSmsCapability() {
val options =

View file

@ -248,6 +248,17 @@ class GatewayConfigResolverTest {
assertNull(resolved.error)
}
@Test
fun resolveScannedSetupCodeResultAcceptsEmulatorSetupCode() {
val setupCode =
encodeSetupCode("""{"url":"ws://10.0.2.2:18789","bootstrapToken":"bootstrap-1"}""")
val resolved = resolveScannedSetupCodeResult(setupCode)
assertEquals(setupCode, resolved.setupCode)
assertNull(resolved.error)
}
@Test
fun resolveScannedSetupCodeResultAcceptsQrJsonPayload() {
val setupCode =
@ -404,6 +415,18 @@ class GatewayConfigResolverTest {
assertNull(decoded?.password)
}
@Test
fun manualTokenDetectsSetupCodePayloads() {
val setupCode =
encodeSetupCode("""{"url":"ws://10.0.2.2:18789","bootstrapToken":"bootstrap-1"}""")
val qrPayload = """{"setupCode":"$setupCode"}"""
assertEquals(true, manualTokenLooksLikeSetupCode(setupCode))
assertEquals(true, manualTokenLooksLikeSetupCode(qrPayload))
assertEquals(false, manualTokenLooksLikeSetupCode("local-mobile-test"))
assertEquals(false, manualTokenLooksLikeSetupCode(""))
}
@Test
fun resolveGatewayConnectConfigPrefersBootstrapTokenFromSetupCode() {
val setupCode =
@ -431,6 +454,32 @@ class GatewayConfigResolverTest {
assertEquals("", resolved?.password)
}
@Test
fun resolveGatewayConnectConfigAcceptsQrJsonSetupCodePayload() {
val setupCode =
encodeSetupCode("""{"url":"wss://gateway.example:18789","bootstrapToken":"bootstrap-1"}""")
val qrPayload = """{"setupCode":"$setupCode"}"""
val resolved =
resolveGatewayConnectConfig(
useSetupCode = true,
setupCode = qrPayload,
manualHostInput = "",
manualPortInput = "",
manualTlsInput = false,
bootstrapTokenInput = "",
tokenInput = "shared-token",
passwordInput = "shared-password",
)
assertEquals("gateway.example", resolved?.host)
assertEquals(18789, resolved?.port)
assertEquals(true, resolved?.tls)
assertEquals("bootstrap-1", resolved?.bootstrapToken)
assertEquals("", resolved?.token)
assertEquals("", resolved?.password)
}
@Test
fun resolveGatewayConnectConfigDefaultsPortlessWssSetupCodeTo443() {
val setupCode =

View file

@ -22,7 +22,7 @@ ktlint-gradle = "14.2.0"
kotlin = "2.4.0"
material = "1.14.0"
okhttp = "5.3.2"
play-services-code-scanner = "16.1.0"
barcode-scanning = "17.3.0"
robolectric = "4.16.1"
serialization-json = "1.11.0"
@ -32,6 +32,7 @@ androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" }
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidx-camera" }
androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "androidx-camera" }
androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "androidx-camera" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
@ -47,6 +48,7 @@ androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "a
androidx-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-uiautomator" }
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" }
bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcode-scanning" }
commonmark = { module = "org.commonmark:commonmark", version.ref = "commonmark" }
commonmark-ext-autolink = { module = "org.commonmark:commonmark-ext-autolink", version.ref = "commonmark" }
commonmark-ext-gfm-strikethrough = { module = "org.commonmark:commonmark-ext-gfm-strikethrough", version.ref = "commonmark" }
@ -63,7 +65,6 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
material = { module = "com.google.android.material:material", version.ref = "material" }
mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
play-services-code-scanner = { module = "com.google.android.gms:play-services-code-scanner", version.ref = "play-services-code-scanner" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
[plugins]

View file

@ -127,9 +127,9 @@ That bootstrap token carries the built-in pairing bootstrap profile:
`node` plus a bounded `operator` handoff
- the handed-off `node` token stays `scopes: []`
- the handed-off `operator` token is limited to `operator.approvals`,
`operator.read`, and `operator.write`
- `operator.admin` and `operator.pairing` are not granted by QR/setup-code
bootstrap; they require a separate approved operator pairing or token flow
`operator.read`, `operator.talk.secrets`, and `operator.write`
- `operator.admin` is not granted by QR/setup-code bootstrap; it requires a
separate approved operator pairing or token flow
- later token rotation/revocation remains bounded by both the device's approved
role contract and the caller session's operator scopes

View file

@ -495,7 +495,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
- `/pair approve` when there is only one pending request
- `/pair approve latest` for most recent
The setup code carries a short-lived bootstrap token. Built-in setup-code bootstrap is node-only: the first connect creates a pending node request, and after approval the Gateway returns a durable node token with `scopes: []`. It does not return a handed-off operator token; operator access requires a separate approved operator pairing or token flow.
The setup code carries a short-lived bootstrap token. Built-in setup-code bootstrap returns a durable node token with `scopes: []` plus a bounded operator handoff token for trusted mobile onboarding. That operator token can read setup-time native configuration, but it does not grant pairing mutation scopes or `operator.admin`.
If a device retries with changed auth details (for example role/scopes/public key), the previous pending request is superseded and the new request uses a different `requestId`. Re-run `/pair pending` before approving.

View file

@ -36,7 +36,7 @@ openclaw qr --url wss://gateway.example/ws
- `--token` and `--password` are mutually exclusive.
- The setup code itself now carries an opaque short-lived `bootstrapToken`, not the shared gateway token/password.
- Built-in setup-code bootstrap returns a primary `node` token with `scopes: []` plus a bounded `operator` handoff token for trusted mobile onboarding.
- The handed-off operator token is limited to `operator.approvals`, `operator.read`, `operator.talk.secrets`, and `operator.write`; `operator.admin` and `operator.pairing` require a separate approved operator pairing or token flow.
- The handed-off operator token is limited to `operator.approvals`, `operator.read`, `operator.talk.secrets`, and `operator.write`; pairing mutation scopes and `operator.admin` still require a separate approved operator pairing or token flow.
- Mobile pairing fails closed for Tailscale/public `ws://` gateway URLs. Private LAN addresses and `.local` Bonjour hosts remain supported over `ws://`, but Tailscale/public mobile routes should use Tailscale Serve/Funnel or a `wss://` gateway URL.
- With `--remote`, OpenClaw requires either `gateway.remote.url` or
`gateway.tailscale.mode=serve|funnel`.

View file

@ -169,10 +169,11 @@ operator token:
```
The operator handoff is intentionally bounded so QR onboarding can start the
mobile operator loop without granting `operator.admin` or `operator.pairing`.
It does include `operator.talk.secrets` so the native client can read the Talk
configuration it needs after bootstrap. Broader admin and pairing scopes require
a separate approved operator pairing or token flow. Clients should persist
mobile operator loop and complete native setup without granting pairing
mutation scopes or `operator.admin`. It includes `operator.talk.secrets` so the
native client can read the Talk configuration it needs after bootstrap. Broader
pairing and admin access requires a separate approved operator pairing or token
flow. Clients should persist
`hello-ok.auth.deviceTokens` only
when the connect used bootstrap auth on trusted transport such as `wss://` or
loopback/local pairing.
@ -731,8 +732,8 @@ rather than the pre-handshake defaults.
- Built-in setup-code bootstrap returns the primary node
`hello-ok.auth.deviceToken` plus a bounded operator token in
`hello-ok.auth.deviceTokens` for trusted mobile handoff. The operator token
includes `operator.talk.secrets` for native Talk configuration reads and
excludes `operator.admin` and `operator.pairing`.
includes `operator.talk.secrets` for native Talk configuration reads, but
excludes pairing mutation scopes and `operator.admin`.
- While a non-baseline setup-code bootstrap is waiting for approval, `PAIRING_REQUIRED`
details include `recommendedNextStep: "wait_then_retry"`, `retryable: true`,
and `pauseReconnect: false`. Clients should keep reconnecting with the same

View file

@ -1531,7 +1531,7 @@ lives on the [Models FAQ](/help/faq-models).
- On `AUTH_TOKEN_MISMATCH`, trusted clients can attempt one bounded retry with a cached device token when the gateway returns retry hints (`canRetryWithDeviceToken=true`, `recommendedNextStep=retry_with_device_token`).
- That cached-token retry now reuses the cached approved scopes stored with the device token. Explicit `deviceToken` / explicit `scopes` callers still keep their requested scope set instead of inheriting cached scopes.
- Outside that retry path, connect auth precedence is explicit shared token/password first, then explicit `deviceToken`, then stored device token, then bootstrap token.
- Built-in setup-code bootstrap is node-only. After approval, it returns a node device token with `scopes: []` and does not return a handed-off operator token.
- Built-in setup-code bootstrap returns a node device token with `scopes: []` plus a bounded operator handoff token for trusted mobile onboarding. The operator handoff can read setup-time native configuration but does not grant pairing mutation scopes or `operator.admin`.
Fix:

View file

@ -1201,7 +1201,6 @@ export function registerControlUiAndPairingSuite(): void {
"operator.write",
]);
expect(operatorHandoff?.scopes).not.toContain("operator.admin");
expect(operatorHandoff?.scopes).not.toContain("operator.pairing");
const pendingAfterInitial = await listDevicePairing();
const pendingForDevice = pendingAfterInitial.pending.filter(
@ -1370,7 +1369,6 @@ export function registerControlUiAndPairingSuite(): void {
"operator.write",
]);
expect(operatorHandoff?.scopes).not.toContain("operator.admin");
expect(operatorHandoff?.scopes).not.toContain("operator.pairing");
const pendingAfterInitial = await listDevicePairing();
expect(
@ -1510,7 +1508,6 @@ export function registerControlUiAndPairingSuite(): void {
"operator.write",
]);
expect(operatorHandoff?.scopes).not.toContain("operator.admin");
expect(operatorHandoff?.scopes).not.toContain("operator.pairing");
wsRetry.close();
await expect(

View file

@ -1398,8 +1398,7 @@ export function attachGatewayWsMessageHandler(params: GatewayWsMessageHandlerPar
// This is the native QR/setup-code onboarding seam. Mobile clients
// must prove their canonical client id and platform/family metadata
// agree before the Gateway can skip owner approval and hand off the
// bounded operator token below. Admin/pairing scopes still require
// an explicit owner flow.
// bounded operator token below. Admin/pairing still require an explicit owner flow.
const bootstrapPairingRoles = allowSetupCodeMobileBootstrapPairing
? uniqueStrings([role, ...boundBootstrapProfile.roles])
: undefined;

View file

@ -36,6 +36,7 @@ describe("device bootstrap profile", () => {
"node.exec",
"operator.admin",
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.talk.secrets",
"operator.write",

View file

@ -60,8 +60,8 @@ describe("native app i18n inventory", () => {
(entry) => entry.source === "Talk failed: Realtime provider closed unexpectedly.",
),
).toBe(true);
expect(entries.some((entry) => entry.source === "Scan fresh setup code")).toBe(true);
expect(entries.some((entry) => entry.source === "Retry connection")).toBe(true);
expect(entries.some((entry) => entry.source === "Scan QR code")).toBe(true);
expect(entries.some((entry) => entry.source === "Test connection")).toBe(true);
expect(entries.some((entry) => entry.source === "Searching…")).toBe(true);
expect(entries.some((entry) => entry.source === "Run now")).toBe(true);
expect(entries.some((entry) => entry.source === "Loading chat")).toBe(true);