fix(android): show specific gateway auth-recovery reason instead of generic label#98698
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 2, 2026, 8:37 AM ET / 12:37 UTC. Summary PR surface: Other +409. Total +409 across 17 files. Reproducibility: yes. Current main source shows Overview and Settings collapsing auth-looking gateway status text to generic labels while structured gateway problem codes already exist, and the PR body supplies after-fix emulator/gateway proof for the changed labels. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the code with edited or squashed non-closing merge text, and keep #98046 open for the remaining Android setup-recovery work. Do we have a high-confidence way to reproduce the issue? Yes. Current main source shows Overview and Settings collapsing auth-looking gateway status text to generic labels while structured gateway problem codes already exist, and the PR body supplies after-fix emulator/gateway proof for the changed labels. Is this the best way to solve the issue? Yes. Centralizing the structured auth label mapping and publishing correlated display/problem state is the maintainable fix; separately collecting raw status and problem flows at each UI surface would keep the stale-state mismatch risk. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 3ae5e98bf605. Label changesLabel justifications:
Evidence reviewedPR surface: Other +409. Total +409 across 17 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
Security-sensitive guard clearedThis PR no longer has blocked security-sensitive file changes. A future security-sensitive change requires a fresh
|
Land-ready maintainer proofExact head: Maintainer rewrite keeps connection status, connectivity, and diagnostics atomic and structured:
Validation:
The contributor's emulator before/after evidence remains valid for the user-visible label change. Broader multi-cause recovery work remains tracked by #98046. |
…e generic label gatewaySummary()/gatewayStatusLabel() collapsed every auth failure (expired setup QR, missing/invalid token, missing/invalid password, stale device identity) into "Authentication needed", discarding the already-computed MainViewModel.gatewayConnectionProblem signal consumed elsewhere in the app (OnboardingFlow's recoveryGatewayAuthDetail). Thread it into both status labels via a shared gatewayAuthNeededSummary() helper so the Overview and Settings screens tell the user which recovery action applies. Fixes openclaw#98046
…ed message format Verified against src/gateway/server/ws-connection/auth-messages.ts: formatGatewayAuthFailureMessage always returns strings starting with "unauthorized", which contains the "auth" substring the status-text gate checks for. Add tests using the exact real-world message text (not just synthetic "auth failed" strings) so this stays regression-locked.
…ity failures
CONTROL_UI_DEVICE_IDENTITY_REQUIRED/DEVICE_IDENTITY_REQUIRED real gateway
messages ("device identity required", "control ui requires device
identity...") don't contain "auth", so the status.contains("auth") gate
never reached their specific label. Add "device identity" as an additional
substring the gate checks for those two codes, with regression tests using
the exact real message text (message-handler.ts).
Verified via GitHub-style codex review round 2 (round 1's "unauthorized
doesn't contain auth" claim was checked against source and rejected as
false — "unauthorized" does contain "auth" — but this second, narrower
finding about device-identity codes held up under the same source check).
|
Merged via squash.
|
What Problem This Solves
Related #98046. During Android gateway setup/reconnect,
gatewaySummary()(Overview screen /Settings row) and
gatewayStatusLabel()(Gateway settings metric) collapse every auth failureinto one generic "Authentication needed" label — whether the setup QR expired, a token/password
is missing or invalid, or the saved device identity is stale. Users cannot distinguish an expired
setup code from an invalid or stale saved credential in these two UI surfaces.
This covers only the Overview and Settings screens. #98094 (merged) / #98275 / #98280 already
cover the same issue's Onboarding/Recovery-screen copy (
OnboardingFlow.kt) — a differentuser-visible surface than this PR touches.
Why This Change Was Made
Client-only change — no gateway protocol, config, or wire-format change.
MainViewModel. gatewayConnectionProblem: StateFlow<GatewayConnectionProblem?>already carries a machine-readablecodefrom the gateway's auth-reject response (AUTH_BOOTSTRAP_TOKEN_INVALID,AUTH_TOKEN_MISSING,AUTH_PASSWORD_MISSING,AUTH_PASSWORD_MISMATCH,AUTH_TOKEN_MISMATCH/AUTH_DEVICE_TOKEN_MISMATCH,CONTROL_UI_DEVICE_IDENTITY_REQUIRED/DEVICE_IDENTITY_REQUIRED), already consumed byrecoveryGatewayAuthDetail()on the Onboarding/Recovery screen, but the Overview and Settingsscreens never received it and fell back to a plain substring match on
statusText. This threadsthe existing signal into both functions via a shared
gatewayAuthNeededSummary()helper withshort, code-specific labels, wiring
viewModel.gatewayConnectionProbleminto the three call sitesthat already have
viewModelin scope. The iOS app already solved the same problem with anequivalent
GatewayConnectionIssueenum(
apps/ios/Sources/Gateway/GatewayConnectionIssue.swift) derived from the same problem shape.User Impact
specific recovery guidance (e.g. "Setup code expired", "Gateway token needed", "Saved auth
invalid", "Device identity required") instead of a generic "Authentication needed" whenever the
gateway has already reported why auth failed.
code is present. Non-auth states (connecting, pairing, TLS, certificate) are unaffected.
Evidence
Behavior proof (real device)
Set up an isolated Android emulator (API 31,
aosp_atdx86_64 image, software-rendered — nohardware acceleration available in this environment) plus a real, isolated OpenClaw gateway
instance (fresh state, throwaway auth token, destroyed after this proof). Built and installed
this branch's
thirdPartyDebugAPK, paired the emulator with the real gateway end-to-end (realWebSocket handshake, real token auth, real device approval via
nodes approve, realnode-capability approval), then removed the paired-device entry server-side
(
devices remove <id>) to force a genuinedevice_token_mismatchrejection on reconnect — thereal-world equivalent of a stale/revoked saved credential.
Gateway-side log for the real rejection:
device_token_mismatchis the gateway's auth-reject reason string; the gateway maps it to thestructured detail code via
resolveAuthConnectErrorDetailCode(
packages/gateway-protocol/src/connect-error-details.js) before sending it, which is whatpopulates
GatewayConnectionProblem.code = "AUTH_DEVICE_TOKEN_MISMATCH"on the Android side.Resulting real app state, captured via
uiautomator dump(accessibility-tree snapshot of theactual running Compose UI) — screenshot capture in this software-rendered emulator session
produced only a black frame, so the UI-tree dump is the retained visual evidence:
OverviewPrimaryPanel, driven bygatewaySummary()): agent row shows"Saved auth invalid"under the agent name (previously this and the Settings label belowwould both have shown the pre-fix generic
"Authentication needed"), and a"Reconnect gateway"action appears.GatewaySettingsScreen, driven bygatewayStatusLabel()):Status: "Saved auth invalid". The same session confirmedConnection: Connected/Status: Readymoments earlier while still paired, before the forced rejection.This exercises the entire real path this PR changes: real gateway auth rejection ->
NodeRuntime.handleGatewayConnectFailure-> realGatewayConnectionProblem(code = "AUTH_DEVICE_TOKEN_MISMATCH")-> realgatewaySummary()/gatewayStatusLabel()-> real Composerecomposition -> real rendered label, with no code under test mocked or stubbed.
Behavior proof (compiled-function invocation, all 6 codes)
The device proof above covers one code end-to-end; JVM invocation of the actual compiled
ShellScreen.kt/SettingsScreens.ktproduction functions covers all 6 codes with the exact realgateway auth-reject message text, as precedent-following supplementary coverage:
Unit tests
gatewaySummary()andgatewayStatusLabel()were promoted fromprivatetointernal(thisfile's existing pattern for JVM-unit-tested logic, e.g.
overviewHeaderState), with newdeterministic tests per auth code, a null/unrecognized-code fallback, unrelated-state
non-interference, and two tests per file using the exact real server message text (
src/gateway/ server/ws-connection/auth-messages.tsandmessage-handler.ts) rather than only syntheticstrings.
Rebased on
upstream/main@adcfebc276:Lint
./gradlew :app:ktlintCheck— the touched files (ShellScreen.kt,SettingsScreens.kt,ShellScreenLogicTest.kt,SettingsScreensTest.kt) are clean. 5 pre-existing violations remainin three unrelated files (
GatewayExecApprovals.kt,ClawSurfaces.kt,OnboardingFlow.kt), nonetouched by this diff.
i18n
pnpm native:i18n:syncrun and committed (apps/.i18n/native-source.json) — the auth-recoverywiring shifted line numbers for existing catalogued strings in the same two files;
pnpm native:i18n:checkpasses clean after the sync.Review
autoreview(Codex,--mode branch --base upstream/main): clean, no accepted/actionablefindings,
overall: patch is correct (0.86), re-run after the i18n sync commit.