Skip to content

fix(control-ui): keep chat UI mounted across transient reconnects#72522

Closed
Sanjays2402 wants to merge 3 commits into
openclaw:mainfrom
Sanjays2402:fix/webchat-credentials-flash-72500
Closed

fix(control-ui): keep chat UI mounted across transient reconnects#72522
Sanjays2402 wants to merge 3 commits into
openclaw:mainfrom
Sanjays2402:fix/webchat-credentials-flash-72500

Conversation

@Sanjays2402

Copy link
Copy Markdown
Contributor

Fixes #72500.

Problem

Webchat flashes the full credentials gate every time the browser tab loses and regains focus. On slower hardware (Pi-class) the flash lasts long enough that it looks like a forced logout. Same flash happens on any transient WebSocket close — gateway restart, network blip, brief sleep.

Root cause is exactly what @brandco diagnosed:

  1. app-render.ts renders renderLoginGate(state) unconditionally on !state.connected.
  2. app-gateway.ts flips connected = false immediately on every WebSocket close, including expected transient closes when the tab backgrounds.
  3. There is no distinction between "never authenticated" and "temporarily disconnected from a known-good session."

Fix

Add a sticky hasEverConnected flag on the gateway host (and on AppViewState / the Lit element). It latches to true on the first successful hello handshake. The renderer now only shows the credentials gate when the user has both never connected AND is currently disconnected:

if (!state.connected && !state.hasEverConnected) {
  return html` ${renderLoginGate(state)} ${renderGatewayUrlConfirmation(state)} `;
}

For genuine re-auth (token rotated, password changed, pairing revoked) the flag is cleared in the onClose handler when the close detail code matches an auth/pairing failure. New helper isAuthFailureDetailCode() catches AUTH_*, DEVICE_AUTH_*, PAIRING_REQUIRED, DEVICE_IDENTITY_REQUIRED, and the CONTROL_UI_* identity errors from connect-error-details.ts.

Plain transport closes (1006, 1012 service restart, network blips) keep the flag set, so the chat UI stays mounted. The existing sidebar renderSidebarConnectionStatus already surfaces the offline state via the status dot, satisfying the "non-blocking reconnecting indicator" expectation in the issue without adding a new banner.

Tests

pnpm test ui/src/ui/
# 661 passed (661)

Three new tests in app-gateway.node.test.ts:

  • Sticky flag survives transient 1006 closes and reconnect cycles.
  • AUTH_TOKEN_MISMATCH close clears the flag → gate reappears.
  • PAIRING_REQUIRED close clears the flag.

Diff

5 files, +115/-1.

File Change
ui/src/ui/app-gateway.ts Add hasEverConnected to GatewayHost; set in onHello; conditionally clear in onClose; new isAuthFailureDetailCode helper.
ui/src/ui/app-render.ts Gate condition now !connected && !hasEverConnected.
ui/src/ui/app-view-state.ts Add hasEverConnected to view state.
ui/src/ui/app.ts Add @state() hasEverConnected = false to the Lit element.
ui/src/ui/app-gateway.node.test.ts Initialize flag + 3 new tests.

Out of scope

The issue also suggested a 1–2s debounce on the connected = false transition. That's not needed here — the sticky flag already absorbs the transition without any timing dependency, which is more robust on Pi-class hardware where reconnect latency varies wildly.

cc @brandco — would appreciate confirmation this fixes the flash on your Pi 5 setup.

@aisle-research-bot

aisle-research-bot Bot commented Apr 27, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Sensitive UI data remains visible after disconnect due to sticky hasEverConnected gate
2 🟡 Medium UI auth gate bypass via sticky hasEverConnected not cleared on auth-related WebSocket closes without detail code
1. 🟡 Sensitive UI data remains visible after disconnect due to sticky `hasEverConnected` gate
Property Value
Severity Medium
CWE CWE-200
Location ui/src/ui/app-render.ts:642-653

Description

The dashboard/chat UI is now gated only when the client is both disconnected and has never connected (!connected && !hasEverConnected). After a tab connects once, a later WebSocket disconnect will no longer trigger the credentials/login gate, leaving the full UI (including previously loaded chat history, approvals queue, etc.) mounted and visible while connected=false.

This creates an information disclosure risk in scenarios where disconnects correlate with session invalidation or where an attacker gains brief physical access during a transient disconnect:

  • After an initial successful connection, sensitive UI state remains on screen during subsequent disconnects.
  • Previously, !connected alone would show the login gate, providing a “screen lock” effect that hid sensitive content.

Vulnerable logic:

if (!state.connected && !state.hasEverConnected) {
  return html` ${renderLoginGate(state)} ${renderGatewayUrlConfirmation(state)} `;
}

Recommendation

If the login gate is intended to act as a lock screen (hide sensitive UI whenever disconnected), restore that behavior or replace it with a dedicated disconnect/lock overlay.

Options:

  1. Always hide sensitive UI when disconnected (original behavior):
if (!state.connected) {
  return html`${renderLoginGate(state)} ${renderGatewayUrlConfirmation(state)}`;
}
  1. Keep UI mounted for UX but prevent disclosure by rendering an overlay that obscures content when !connected (while allowing reconnect status), e.g.:
const showLockOverlay = !state.connected;
return html`
  ${renderDashboard(state)}
  ${showLockOverlay ? renderDisconnectedOverlay({ onReconnect: state.connect }) : ""}
  ${renderGatewayUrlConfirmation(state)}
`;

Additionally consider clearing or redacting sensitive in-memory state (chat messages, approvals) on disconnects that may indicate session loss, and/or providing a user setting like “Hide content when disconnected”.

2. 🟡 UI auth gate bypass via sticky `hasEverConnected` not cleared on auth-related WebSocket closes without detail code
Property Value
Severity Medium
CWE CWE-287
Location ui/src/ui/app-gateway.ts:400-410

Description

The UI login/credentials gate is now bypassed whenever state.hasEverConnected is true, even if the gateway is currently disconnected.

In app-gateway.ts, hasEverConnected is only cleared on close when an auth failure is detected via host.lastErrorCode and that code is in AUTH_FAILURE_DETAIL_CODES:

  • host.lastErrorCode is derived from resolveGatewayErrorDetailCode(error) or error.code
  • However, GatewayBrowserClient only supplies error on close when a connect handshake fails (pendingConnectError). For server-initiated disconnects after a successful hello (e.g., token/session revoked/expired), the WebSocket close event can arrive with an auth-related close code/reason but with no structured error payload.
  • In that case, host.lastErrorCode is null, so hasEverConnected remains true.
  • app-render.ts renders the full dashboard whenever !state.connected && state.hasEverConnected, keeping privileged UI mounted without re-authentication.

Impact depends on what the mounted UI reveals when disconnected, but it can result in stale privileged UI/state remaining visible after credentials are no longer valid (auth revocation/expiry), potentially exposing cached sensitive data to anyone with access to the tab.

Recommendation

Treat auth-related WebSocket close codes/reasons as sufficient to clear hasEverConnected, not only structured detail codes.

Options:

  1. Clear on explicit close codes used for auth failures (e.g., 4001 unauthorized, 4003 pairing required), even when error is missing.
const AUTH_FAILURE_CLOSE_CODES = new Set([4001, 4003]);

if (
  (host.lastErrorCode && isAuthFailureDetailCode(host.lastErrorCode)) ||
  AUTH_FAILURE_CLOSE_CODES.has(code)
) {
  host.hasEverConnected = false;
}
  1. Propagate server auth failure details into onClose by extending the gateway protocol/client so that when the server closes due to auth, the client captures the last auth error (or a dedicated close frame) and passes a structured error object to onClose.

Also add tests that simulate server closes with code/reason but no error object to ensure the credentials gate reappears on auth revocation/expiry flows.


Analyzed PR: #72522 at commit ff7cee4

Last updated on: 2026-04-27T03:28:21Z

@greptile-apps

greptile-apps Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a sticky hasEverConnected flag to prevent the credentials gate from flashing during transient WebSocket reconnects (tab refocus, 1006/1012 closes, network blips). The gate is now only shown on first connect or after explicit auth/pairing failures. The approach is solid and well-tested with three targeted new tests.

Confidence Score: 4/5

Safe to merge; one P2 concern about the breadth of the auth-code prefix check is non-blocking.

Only a single P2 finding: isAuthFailureDetailCode uses startsWith("AUTH_") which captures transient infrastructure codes (AUTH_TAILSCALE_WHOIS_FAILED, AUTH_RATE_LIMITED, etc.) that would unnecessarily clear hasEverConnected. The core fix is logically correct and the tests cover the intended cases.

ui/src/ui/app-gateway.ts — the isAuthFailureDetailCode helper's prefix matching may be too broad for Tailscale infrastructure error codes.

Comments Outside Diff (1)

  1. ui/src/ui/app-gateway.ts, line 86-93 (link)

    P2 startsWith("AUTH_") is broader than "genuine re-auth" failures

    The catch-all code.startsWith("AUTH_") clears hasEverConnected for codes that represent transient infrastructure issues, not just credential failures. Specifically AUTH_RATE_LIMITED (temporary server-side block, credentials are valid), AUTH_TAILSCALE_WHOIS_FAILED, AUTH_TAILSCALE_PROXY_MISSING, and AUTH_TAILSCALE_IDENTITY_MISSING can all appear on a reconnect after a brief network blip — they map to conditions the user cannot fix by re-entering credentials. On a Tailscale-auth setup, a momentary WhoIs lookup failure would clear the flag and flash the full credentials gate, reproducing the exact UX regression this PR aims to prevent.

    Consider using an explicit allowlist matching the set in gateway.ts isAuthOrPairingError (lines 84–91), which already excludes transient infrastructure codes, rather than the prefix wildcard.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: ui/src/ui/app-gateway.ts
    Line: 86-93
    
    Comment:
    **`startsWith("AUTH_")` is broader than "genuine re-auth" failures**
    
    The catch-all `code.startsWith("AUTH_")` clears `hasEverConnected` for codes that represent transient infrastructure issues, not just credential failures. Specifically `AUTH_RATE_LIMITED` (temporary server-side block, credentials are valid), `AUTH_TAILSCALE_WHOIS_FAILED`, `AUTH_TAILSCALE_PROXY_MISSING`, and `AUTH_TAILSCALE_IDENTITY_MISSING` can all appear on a reconnect after a brief network blip — they map to conditions the user cannot fix by re-entering credentials. On a Tailscale-auth setup, a momentary WhoIs lookup failure would clear the flag and flash the full credentials gate, reproducing the exact UX regression this PR aims to prevent.
    
    Consider using an explicit allowlist matching the set in `gateway.ts` `isAuthOrPairingError` (lines 84–91), which already excludes transient infrastructure codes, rather than the prefix wildcard.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/app-gateway.ts
Line: 86-93

Comment:
**`startsWith("AUTH_")` is broader than "genuine re-auth" failures**

The catch-all `code.startsWith("AUTH_")` clears `hasEverConnected` for codes that represent transient infrastructure issues, not just credential failures. Specifically `AUTH_RATE_LIMITED` (temporary server-side block, credentials are valid), `AUTH_TAILSCALE_WHOIS_FAILED`, `AUTH_TAILSCALE_PROXY_MISSING`, and `AUTH_TAILSCALE_IDENTITY_MISSING` can all appear on a reconnect after a brief network blip — they map to conditions the user cannot fix by re-entering credentials. On a Tailscale-auth setup, a momentary WhoIs lookup failure would clear the flag and flash the full credentials gate, reproducing the exact UX regression this PR aims to prevent.

Consider using an explicit allowlist matching the set in `gateway.ts` `isAuthOrPairingError` (lines 84–91), which already excludes transient infrastructure codes, rather than the prefix wildcard.

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

Reviews (1): Last reviewed commit: "fix(control-ui): keep chat UI mounted ac..." | Re-trigger Greptile

Comment thread ui/src/ui/app-gateway.ts Outdated
Comment on lines +86 to +93
}
return (
code.startsWith("AUTH_") ||
code.startsWith("DEVICE_AUTH_") ||
code === ConnectErrorDetailCodes.DEVICE_IDENTITY_REQUIRED ||
code === ConnectErrorDetailCodes.CONTROL_UI_ORIGIN_NOT_ALLOWED ||
code === ConnectErrorDetailCodes.CONTROL_UI_DEVICE_IDENTITY_REQUIRED
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 startsWith("AUTH_") is broader than "genuine re-auth" failures

The catch-all code.startsWith("AUTH_") clears hasEverConnected for codes that represent transient infrastructure issues, not just credential failures. Specifically AUTH_RATE_LIMITED (temporary server-side block, credentials are valid), AUTH_TAILSCALE_WHOIS_FAILED, AUTH_TAILSCALE_PROXY_MISSING, and AUTH_TAILSCALE_IDENTITY_MISSING can all appear on a reconnect after a brief network blip — they map to conditions the user cannot fix by re-entering credentials. On a Tailscale-auth setup, a momentary WhoIs lookup failure would clear the flag and flash the full credentials gate, reproducing the exact UX regression this PR aims to prevent.

Consider using an explicit allowlist matching the set in gateway.ts isAuthOrPairingError (lines 84–91), which already excludes transient infrastructure codes, rather than the prefix wildcard.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/app-gateway.ts
Line: 86-93

Comment:
**`startsWith("AUTH_")` is broader than "genuine re-auth" failures**

The catch-all `code.startsWith("AUTH_")` clears `hasEverConnected` for codes that represent transient infrastructure issues, not just credential failures. Specifically `AUTH_RATE_LIMITED` (temporary server-side block, credentials are valid), `AUTH_TAILSCALE_WHOIS_FAILED`, `AUTH_TAILSCALE_PROXY_MISSING`, and `AUTH_TAILSCALE_IDENTITY_MISSING` can all appear on a reconnect after a brief network blip — they map to conditions the user cannot fix by re-entering credentials. On a Tailscale-auth setup, a momentary WhoIs lookup failure would clear the flag and flash the full credentials gate, reproducing the exact UX regression this PR aims to prevent.

Consider using an explicit allowlist matching the set in `gateway.ts` `isAuthOrPairingError` (lines 84–91), which already excludes transient infrastructure codes, rather than the prefix wildcard.

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

@Sanjays2402

Copy link
Copy Markdown
Contributor Author

Good catch on AUTH_RATE_LIMITED and the AUTH_TAILSCALE_* family — a transient WhoIs blip on a Tailscale-auth setup would have re-flashed the gate.

Replaced the prefix wildcards in isAuthFailureDetailCode with an explicit AUTH_FAILURE_DETAIL_CODES set that excludes the transient infra codes (AUTH_RATE_LIMITED, AUTH_TAILSCALE_WHOIS_FAILED, AUTH_TAILSCALE_PROXY_MISSING, AUTH_TAILSCALE_IDENTITY_MISSING). AUTH_TAILSCALE_IDENTITY_MISMATCH stays in (genuine identity failure, not a transient lookup error).

Added a parameterized test covering all four excluded codes — connected = false after a 1006 close with one of those codes, but hasEverConnected stays true.

pnpm test ui/src/ui/app-gateway.node.test.ts → 42/42 pass.

Pushed in ff7cee4.

@clawsweeper

clawsweeper Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Latest ClawSweeper review: 2026-05-20 19:35 UTC / May 20, 2026, 3:35 PM ET.

Workflow note: Future ClawSweeper reviews update this same comment in place.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

Summary
The PR adds a sticky per-tab hasEverConnected Control UI state, uses it to keep the dashboard mounted after transient disconnects, resets it for selected auth/revocation closes, adds gateway tests, and updates the changelog.

Reproducibility: yes. at source level. Current main clears connected into a login-gate render path on reconnect, and PR head still keeps cached UI mounted after structured AUTH_SCOPE_MISMATCH because that detail is missing from the reset allowlist.

PR rating
Overall: 🦪 silver shellfish
Proof: 🦪 silver shellfish
Patch quality: 🦐 gold shrimp
Summary: Useful bug-fix direction, but merge confidence is capped by mock-only proof, a blocking scope-mismatch reset gap, and a conflicting branch.

Rank-up moves:

  • Add AUTH_SCOPE_MISMATCH to AUTH_FAILURE_DETAIL_CODES with regression coverage.
  • Rebase onto current main and resolve conflicts.
  • Post redacted real Control UI/browser proof for transient reconnect and auth/scope failure behavior.
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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.

Real behavior proof
Needs real behavior proof before merge: Only unit-test output and mocked close-event reproduction are posted; there is no redacted real Control UI/browser proof showing the changed reconnect and auth-failure behavior after the patch. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Mantis proof suggestion
A short visible Control UI proof would materially help verify the dashboard stays mounted during transient reconnects and returns to the gate on auth failure. A maintainer can ask Mantis to capture proof by posting a new PR comment that starts with the OpenClaw Mantis account mention, followed by:

visual task: verify Control UI keeps chat mounted during a transient WebSocket reconnect and returns to the credentials gate on an explicit auth or scope failure.

Risk before merge

  • Merging as-is can leave cached Control UI content mounted after AUTH_SCOPE_MISMATCH, even though current main treats that error as requiring operator action and no reconnect retry.
  • The branch is currently conflicting with main, so CI and reviewer confidence do not cover the exact merge result.
  • The PR has only unit-test/mock close-event proof; it still needs redacted real Control UI/browser proof for both transient reconnect and auth/scope failure behavior.

Maintainer options:

  1. Fix auth reset coverage before merge (recommended)
    Add AUTH_SCOPE_MISMATCH to the sticky reset path with regression coverage, rebase the branch, and require redacted real browser proof before merge.
  2. Replace the stale branch if needed
    If the contributor cannot rebase or provide proof, maintainers can prepare a narrow replacement PR that preserves the useful sticky-state design and credits this source work.

Next step before merge
Human PR handling is needed because contributor real behavior proof is missing, the branch conflicts with current main, and the remaining defect is security-sensitive auth gating.

Security
Needs attention: The sticky gate can preserve privileged Control UI state after a current-main scope-mismatch auth failure.

Review findings

  • [P2] Reset sticky state on scope mismatch — ui/src/ui/app-gateway.ts:81-104
Review details

Best possible solution:

Keep the sticky prior-hello approach only after it resets for every current nonrecoverable auth/scope failure, has regression coverage, is rebased, and has redacted browser proof.

Do we have a high-confidence way to reproduce the issue?

Yes, at source level. Current main clears connected into a login-gate render path on reconnect, and PR head still keeps cached UI mounted after structured AUTH_SCOPE_MISMATCH because that detail is missing from the reset allowlist.

Is this the best way to solve the issue?

No, not as-is. The sticky prior-hello state is a narrow maintainable direction, but the auth reset contract, branch conflicts, and real behavior proof must be fixed before merge.

Label justifications:

  • P2: This is a focused Control UI bug fix with a real but limited blast radius and one blocking auth-gating defect.
  • merge-risk: 🚨 security-boundary: The diff can preserve privileged cached UI after a current-main scope auth failure if merged as-is.
  • merge-risk: 🚨 auth-provider: The PR changes UI behavior around gateway token, pairing, revocation, and scope-failure handling.
  • rating: 🦪 silver shellfish: Current PR rating is 🦪 silver shellfish because proof is 🦪 silver shellfish, patch quality is 🦐 gold shrimp, and Useful bug-fix direction, but merge confidence is capped by mock-only proof, a blocking scope-mismatch reset gap, and a conflicting branch.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: Only unit-test output and mocked close-event reproduction are posted; there is no redacted real Control UI/browser proof showing the changed reconnect and auth-failure behavior after the patch. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Full review comments:

  • [P2] Reset sticky state on scope mismatch — ui/src/ui/app-gateway.ts:81-104
    The reset allowlist omits ConnectErrorDetailCodes.AUTH_SCOPE_MISMATCH, while current main treats that detail as nonrecoverable and stops reconnecting. After a prior successful hello, a scope-mismatch connect failure leaves hasEverConnected true, so renderApp keeps cached Control UI content mounted instead of returning to credentials or scope guidance; add the detail code and coverage.
    Confidence: 0.91

Overall correctness: patch is incorrect
Overall confidence: 0.91

Security concerns:

  • [medium] Sticky UI survives scope mismatch — ui/src/ui/app-gateway.ts:81
    Current main classifies AUTH_SCOPE_MISMATCH as nonrecoverable, but the PR does not clear hasEverConnected for that detail code. That can leave cached chat/admin UI mounted after a scope failure requiring operator action.
    Confidence: 0.91

Acceptance criteria:

  • node scripts/run-vitest.mjs ui/src/ui/app-gateway.node.test.ts ui/src/ui/gateway.node.test.ts src/gateway/reconnect-gating.test.ts
  • Redacted real Control UI browser proof for transient reconnect plus auth/scope failure behavior
  • Rebase onto current main and resolve conflicts before relying on CI

What I checked:

  • Current-main flash path remains real: Current main still returns the login gate whenever state.connected is false, so the reported transient reconnect flash remains present on main. (ui/src/ui/app-render.ts:904, 1a7669bc63a0)
  • Current gateway clears connected during reconnect: Current main clears host.connected = false at the start of connectGateway, feeding the disconnected render branch during reconnects. (ui/src/ui/app-gateway.ts:505, 1a7669bc63a0)
  • PR sticky render path: PR head changes the gate to !state.connected && !state.hasEverConnected, so any auth failure that does not clear the sticky flag keeps cached dashboard UI mounted. (ui/src/ui/app-render.ts:641, aafc2455ea4c)
  • PR reset allowlist still omits scope mismatch: PR head's AUTH_FAILURE_DETAIL_CODES includes token, password, device, origin, and pairing failures but not AUTH_SCOPE_MISMATCH. (ui/src/ui/app-gateway.ts:81, aafc2455ea4c)
  • Current auth contract treats scope mismatch as nonrecoverable: Current main classifies AUTH_SCOPE_MISMATCH as a nonrecoverable auth error, and the regression test asserts reconnect stops for that detail code. (ui/src/ui/gateway.ts:133, 1a7669bc63a0)
  • Gateway client forwards structured connect errors to close handling: Current main passes the pending connect error into onClose and then suppresses reconnect for nonrecoverable auth errors, so the UI reset classification must include the same nonrecoverable scope error. (ui/src/ui/gateway.ts:515, 1a7669bc63a0)

Likely related people:

  • BunsDev: Recent history includes Control UI reconnect/auth-loop work and the AUTH_SCOPE_MISMATCH scope handling that this PR's reset allowlist must match. (role: recent Control UI gateway/auth contributor; confidence: high; commits: 51b73b39a8b8, 348ffe60612c, 4935e24c7a7f; files: ui/src/ui/gateway.ts, ui/src/ui/app-gateway.ts, src/gateway/reconnect-gating.test.ts)
  • steipete: Recent history touches app-render.ts and gateway.ts, including protocol mismatch and Control UI rendering work adjacent to login-gate decisions. (role: recent adjacent Control UI/gateway contributor; confidence: medium; commits: a535978352ca, 84ec0c27bf75, 4f4d10863916; files: ui/src/ui/app-render.ts, ui/src/ui/gateway.ts)
  • joshavant: Earlier gateway token fallback and auth reconnect hardening is part of the dependency contract this PR's sticky UI state must preserve. (role: earlier gateway auth/reconnect hardening contributor; confidence: medium; commits: a76e81019333; files: src/gateway/protocol/connect-error-details.ts, ui/src/ui/gateway.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against 1a7669bc63a0.

Webchat was flashing the credentials gate on every browser tab refocus
or gateway restart because app-render renders the login gate
unconditionally on `!state.connected`, and app-gateway sets
`connected = false` immediately on any WebSocket close — including
expected transient closes.

Add a sticky `hasEverConnected` flag on the host that latches to true
on the first successful hello handshake and only resets on explicit
auth/pairing failures. The renderer now keeps the chat UI mounted
during transient reconnects; the existing sidebar status indicator
already surfaces the offline state, so users see a brief 'offline' dot
instead of the full credentials form.

Auth-failure reset uses a small helper that catches AUTH_*,
DEVICE_AUTH_*, PAIRING_REQUIRED, DEVICE_IDENTITY_REQUIRED, and the
CONTROL_UI_* identity errors so genuine re-auth still surfaces the
gate.

Adds three tests covering the sticky flag across reconnects, AUTH_*
resets, and PAIRING_REQUIRED resets.

Fixes openclaw#72500
Replaces broad startsWith("AUTH_") / startsWith("DEVICE_AUTH_") checks in
isAuthFailureDetailCode with an explicit allowlist of detail codes that
represent genuine credential/identity failures.

Transient infra codes (AUTH_RATE_LIMITED, AUTH_TAILSCALE_WHOIS_FAILED,
AUTH_TAILSCALE_PROXY_MISSING, AUTH_TAILSCALE_IDENTITY_MISSING) no longer
clear hasEverConnected — those represent server-side conditions the user
can't fix by re-entering credentials, and treating them as auth failures
would re-flash the credentials gate on a brief WhoIs blip (the exact
regression this PR prevents).

Adds parameterized test covering all four transient infra codes.

Tests: pnpm test ui/src/ui/app-gateway.node.test.ts → 42/42 pass.
Address codex P2 review on openclaw#72522: the previous reset condition only
fired when the close carried a structured detail code via
isAuthFailureDetailCode. Server-initiated revocation paths (shared-auth
rotation, device removal, session revocation) close active sockets with
code 4001 + a reason string, and the browser client forwards only
{code, reason} for post-hello closes (error is set only for the pending
connect error). Without classifying these directly, cached Control UI
content survived explicit revocation until the next reconnect outcome.

Add isAuthRevocationClose() that matches code 4001 + a lowercased reason
substring against an allowlist (gateway auth changed/rotated/revoked,
shared auth changed/rotated/revoked, device removed/revoked, session
revoked, pairing revoked). Wire it into onClose so hasEverConnected is
also dropped on these code-only revocation closes, preserving the
existing transient-infra and structured-error paths.

Tests cover all 8 revocation reasons (parametrized), plus two defensive
cases: (a) a non-revocation 4001 reason stays sticky, (b) a non-4001
close that happens to mention 'device removed' in its transient reason
stays sticky.

  61 tests pass (was 51; +10 = 8 it.each + 2 single).
@Sanjays2402
Sanjays2402 force-pushed the fix/webchat-credentials-flash-72500 branch from ff7cee4 to aafc245 Compare May 2, 2026 10:36
@Sanjays2402

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed security review — addressed in aafc245.

Fix: Added isAuthRevocationClose() in ui/src/ui/app-gateway.ts that classifies code: 4001 close frames whose reason contains any of the explicit revocation substrings: gateway auth changed/rotated/revoked, shared auth changed/rotated/revoked, device removed/revoked, session revoked, pairing revoked. Wired into onClose after the existing isAuthFailureDetailCode branch, so hasEverConnected is now dropped on:

  1. Structured lastErrorCode matching the auth-failure allowlist (existing).
  2. code: 4001 + a known revocation reason, even when error is undefined (NEW — covers shared-auth rotation at src/gateway/server-shared-auth-generation.ts:27 and device removal at src/gateway/server-request-context.ts:118).

Reproduction (codex): post-hello { code: 4001, reason: "gateway auth changed", error: undefined } — was leaving sticky chat UI mounted; now correctly resets to credentials gate.

Why substring match (not exact equality): the gateway emits reason variants (changed/rotated/revoked) and the codex review explicitly called out maintainer-confirmed allowlist as a risk, so the substring set is conservative and easy to extend without forcing every gateway path to ship structured detail codes.

Regression tests added in app-gateway.node.test.ts:

  • it.each(8) — every revocation reason on code: 4001 with error: undefined clears hasEverConnected.
  • code: 4001 with non-revocation reason ("policy violation", no error) stays sticky — defensive lower bound.
  • code: 1006 with reason text mentioning device removed stays sticky — defensive upper bound (substring only matters when paired with code 4001).

Acceptance criteria run locally:

  • pnpm test ui/src/ui/app-gateway.node.test.ts → 61/61 pass (was 51 before; +10 new cases).
  • pnpm exec oxfmt --check --threads=1 ui/src/ui/app-gateway.ts ui/src/ui/app-gateway.node.test.ts CHANGELOG.md → clean.

Updated changelog entry to mention the revocation-close handling.

Open question for maintainers: if there are other 4001 reason strings in production that should clear the sticky flag, happy to extend AUTH_REVOCATION_REASON_SUBSTRINGS. Confirmed against grep of code: 4001\|code:.4001 across src/gateway/.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels May 20, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 20, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@Sanjays2402

Copy link
Copy Markdown
Contributor Author

Closing — stale (15+ days). Will reopen / resubmit if still relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Control UI webchat flashes credentials gate on tab re-focus due to immediate connected=false render

1 participant