Skip to content

fix(control-ui): share gateway token scope across loopback hosts#73987

Open
openclaw-clownfish[bot] wants to merge 1 commit into
mainfrom
clownfish/ghcrawl-157066-autonomous-smoke
Open

fix(control-ui): share gateway token scope across loopback hosts#73987
openclaw-clownfish[bot] wants to merge 1 commit into
mainfrom
clownfish/ghcrawl-157066-autonomous-smoke

Conversation

@openclaw-clownfish

Copy link
Copy Markdown
Contributor

Repair the existing #43254 branch rather than replacing it. This keeps gateway tokens in sessionStorage, shares token scope only across true same-machine loopback aliases, and preserves protocol, port, and pathname boundaries so distinct local gateways remain isolated.

This repair addresses the hydrated bot findings by avoiding broad 127.* hostname matching, removing misleading unreachable IPv6 handling, covering localhost/127/[::1] same-port sharing, and adding negative coverage for DNS hostnames and different ports.

Credit: original implementation and reproduction analysis by @ssdiwu in #43254. #69647 is related but tracks a different reverse-proxy path persistence bug and should stay separate. Security-routed linked issues remain out of this fix path.

ProjectClownfish replacement details:

@openclaw-clownfish openclaw-clownfish Bot added the clawsweeper Tracked by ClawSweeper automation label Apr 29, 2026
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui size: S r: too-many-prs Auto-close: author has more than twenty active PRs. labels Apr 29, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

@vincentkoc vincentkoc reopened this Apr 29, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the r: too-many-prs Auto-close: author has more than twenty active PRs. label Apr 29, 2026
@greptile-apps

greptile-apps Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR normalizes gateway token scope so that localhost, 127.x.x.x, and [::1] loopback aliases on the same protocol, port, and path share the same token in sessionStorage, while keeping distinct gateways isolated. The IPv4 loopback detection and host reconstruction logic are sound, but there is one present defect:

  • IPv6 loopback [::1] is never detected: normalizeGatewayTokenScopeHost checks normalized === \"[::1]\", but URL.hostname strips brackets from IPv6 literals and returns \"::1\". The condition is always false, so IPv6 loopback aliases silently fall through and do not share token scope. The companion test also does not catch this because setTestLocation mocks location.hostname as \"[::1]\", but the normalization path derives the hostname via URL.hostname (not location.hostname), so the test exercises the wrong value. The one-character fix is normalized === \"::1\".

Confidence Score: 3/5

Not safe to merge as-is — IPv6 loopback token sharing is silently broken and the test masking the bug should also be corrected.

Two related P1 findings: the [::1] bracket mismatch in normalizeGatewayTokenScopeHost (a definite present defect on the changed code path) and the companion test that doesn't catch it. The fix is a single-character change but the test infrastructure also needs adjustment. Multiple P1s pull the score below the 4/5 ceiling.

ui/src/ui/storage.ts line 33 (IPv6 check) and ui/src/ui/storage.node.test.ts setTestLocation hostname derivation for IPv6 hosts.

Comments Outside Diff (1)

  1. ui/src/ui/storage.node.test.ts, line 16-17 (link)

    P1 setTestLocation produces a bracketed hostname that doesn't match URL.hostname

    For host = "[::1]:18789", the test mock derives hostname by stripping the port with host.replace(/:\d+$/, ""), yielding "[::1]" (brackets retained). Real browser location.hostname for an IPv6 address is "::1" (no brackets), so this mock already diverges from reality — but more critically, normalizeGatewayTokenScope calls URL.hostname on a freshly-parsed URL, not location.hostname, so the bracketed value in the mock has no bearing on whether normalizeGatewayTokenScopeHost matches "[::1]". The test exercises a code path that would pass with the bracketed mock but fail in production where URL.hostname always returns the un-bracketed form.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: ui/src/ui/storage.node.test.ts
    Line: 16-17
    
    Comment:
    **`setTestLocation` produces a bracketed `hostname` that doesn't match `URL.hostname`**
    
    For `host = "[::1]:18789"`, the test mock derives `hostname` by stripping the port with `host.replace(/:\d+$/, "")`, yielding `"[::1]"` (brackets retained). Real browser `location.hostname` for an IPv6 address is `"::1"` (no brackets), so this mock already diverges from reality — but more critically, `normalizeGatewayTokenScope` calls `URL.hostname` on a freshly-parsed URL, not `location.hostname`, so the bracketed value in the mock has no bearing on whether `normalizeGatewayTokenScopeHost` matches `"[::1]"`. The test exercises a code path that would pass with the bracketed mock but fail in production where `URL.hostname` always returns the un-bracketed form.
    
    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/storage.ts
Line: 33

Comment:
**`[::1]` IPv6 loopback check never matches**

`normalizeGatewayTokenScopeHost` receives `parsed.hostname` from the WHATWG `URL` API. For a URL like `ws://[::1]:18789/openclaw`, `URL.hostname` returns `"::1"` (brackets stripped), but the condition compares against `"[::1]"` (with brackets). The check `normalized === "[::1]"` is therefore always `false`, so IPv6 loopback addresses are never normalized to `"localhost"` and the stated same-machine token sharing for `[::1]` is silently broken.

```suggestion
  if (normalized === "localhost" || normalized === "::1" || isIpv4LoopbackLiteral(normalized)) {
```

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

---

This is a comment left during a code review.
Path: ui/src/ui/storage.node.test.ts
Line: 16-17

Comment:
**`setTestLocation` produces a bracketed `hostname` that doesn't match `URL.hostname`**

For `host = "[::1]:18789"`, the test mock derives `hostname` by stripping the port with `host.replace(/:\d+$/, "")`, yielding `"[::1]"` (brackets retained). Real browser `location.hostname` for an IPv6 address is `"::1"` (no brackets), so this mock already diverges from reality — but more critically, `normalizeGatewayTokenScope` calls `URL.hostname` on a freshly-parsed URL, not `location.hostname`, so the bracketed value in the mock has no bearing on whether `normalizeGatewayTokenScopeHost` matches `"[::1]"`. The test exercises a code path that would pass with the bracketed mock but fail in production where `URL.hostname` always returns the un-bracketed form.

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

Reviews (1): Last reviewed commit: "fix(control-ui): share gateway token sco..." | Re-trigger Greptile

Comment thread ui/src/ui/storage.ts

function normalizeGatewayTokenScopeHost(hostname: string): string {
const normalized = hostname.toLowerCase();
if (normalized === "localhost" || normalized === "[::1]" || isIpv4LoopbackLiteral(normalized)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 [::1] IPv6 loopback check never matches

normalizeGatewayTokenScopeHost receives parsed.hostname from the WHATWG URL API. For a URL like ws://[::1]:18789/openclaw, URL.hostname returns "::1" (brackets stripped), but the condition compares against "[::1]" (with brackets). The check normalized === "[::1]" is therefore always false, so IPv6 loopback addresses are never normalized to "localhost" and the stated same-machine token sharing for [::1] is silently broken.

Suggested change
if (normalized === "localhost" || normalized === "[::1]" || isIpv4LoopbackLiteral(normalized)) {
if (normalized === "localhost" || normalized === "::1" || isIpv4LoopbackLiteral(normalized)) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/storage.ts
Line: 33

Comment:
**`[::1]` IPv6 loopback check never matches**

`normalizeGatewayTokenScopeHost` receives `parsed.hostname` from the WHATWG `URL` API. For a URL like `ws://[::1]:18789/openclaw`, `URL.hostname` returns `"::1"` (brackets stripped), but the condition compares against `"[::1]"` (with brackets). The check `normalized === "[::1]"` is therefore always `false`, so IPv6 loopback addresses are never normalized to `"localhost"` and the stated same-machine token sharing for `[::1]` is silently broken.

```suggestion
  if (normalized === "localhost" || normalized === "::1" || isIpv4LoopbackLiteral(normalized)) {
```

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

@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed July 3, 2026, 4:13 PM ET / 20:13 UTC.

Summary
The PR changes Control UI storage to normalize loopback gateway hosts to a shared localhost token scope, adds node storage coverage, and adds a changelog entry.

PR surface: Source +31, Tests +84, Docs +1. Total +116 across 3 files.

Reproducibility: yes. by source inspection: current main keys gateway tokens and per-gateway session selection by exact parsed host, while the linked discussion documents localhost versus 127.0.0.1 token fragmentation. I did not run a live browser reproduction.

Review metrics: 1 noteworthy metric.

  • Gateway Storage Scope Consumers: 1 normalizer changed; 4 consumers affected. The same scope drives token sessionStorage keys, URL-edit token preservation, settings localStorage keys, and sessionsByGateway selection.

Stored data model
Persistent data-model change detected: durable storage schema: ui/src/ui/storage.node.test.ts, durable storage schema: ui/src/ui/storage.ts, serialized state: ui/src/ui/storage.node.test.ts, unknown-data-model-change: ui/src/ui/storage.node.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: canonical
Canonical: #73987
Summary: This PR is the current open replacement candidate for the closed loopback token-scope PR; broader token-missing, reverse-proxy, and gateway-token-scope items are related context but not the same landing target.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🌊 off-meta tidepool
Patch quality: 🧂 unranked krab
Result: blocked by patch quality or review findings.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Confirm or narrow the accepted loopback alias boundary and update the normalizer/tests accordingly.
  • [P2] Add same-origin gatewayUrl-edit coverage or browser-real proof for token reuse and isolation.
  • Remove the CHANGELOG.md edit and refresh or replace the conflicting branch.

Proof path suggestion
A browser-visible Control UI proof would materially show whether token reuse and isolation work across the accepted gateway URL aliases. Mantis is currently scoped to Telegram, Discord, and web UI chat proof, so it is not the right proof path for this surface.
Use maintainer screenshot/manual proof, browser or Playwright proof, Crabbox where appropriate, or normal local artifact proof instead.

Risk before merge

  • [P1] Merging as-is can reuse one gateway token and selected session across distinct local listeners bound to different numeric 127/8 addresses with the same port and path.
  • [P1] The added storage test keeps one mocked sessionStorage while changing page origins, so it does not prove the browser behavior users would hit when moving between localhost and 127.0.0.1.
  • [P1] The PR head is conflicting and maintainerCanModify=false, which blocks direct maintainer repair of this branch.
  • [P1] The PR edits release-owned CHANGELOG.md instead of keeping release-note context in the PR body or merge message.

Maintainer options:

  1. Narrow And Prove The Loopback Scope (recommended)
    Confirm the accepted alias set, avoid broad 127/8 sharing unless explicitly intended, and add same-origin URL-edit coverage or browser-real proof before merge.
  2. Accept Full 127/8 Sharing
    Maintainers can intentionally treat every numeric 127/8 address as one Control UI auth/session scope, but that compatibility and security tradeoff should be stated before merge.
  3. Replace The Conflicting Branch
    If this head cannot be updated, open a clean replacement that preserves the source PR credit and close this PR only after the replacement is viable.

Next step before merge

  • [P2] Human or author action is needed because the branch is conflicting and uneditable, and the accepted loopback auth/session boundary is a maintainer security/product decision.

Security
Needs attention: The diff broadens Control UI gateway-token and session lookup scope across numeric 127/8 loopback hosts, which needs maintainer approval and proof before merge.

Review findings

  • [P1] Do not collapse all numeric 127/8 token scopes — ui/src/ui/storage.ts:33
  • [P2] Prove alias reuse without changing page origins — ui/src/ui/storage.node.test.ts:284
  • [P3] Remove the release-owned changelog entry — CHANGELOG.md:16
Review details

Best possible solution:

Land a refreshed Control UI storage fix only after maintainers confirm the accepted loopback alias boundary, preserve isolation for distinct loopback gateways, add same-origin or browser-real proof, and remove the changelog edit.

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

Yes, by source inspection: current main keys gateway tokens and per-gateway session selection by exact parsed host, while the linked discussion documents localhost versus 127.0.0.1 token fragmentation. I did not run a live browser reproduction.

Is this the best way to solve the issue?

No as submitted. The Control UI storage layer is a plausible owner path, but the broad 127/8 alias boundary, browser-origin proof gap, changelog edit, and conflicting uneditable branch state need resolution first.

Full review comments:

  • [P1] Do not collapse all numeric 127/8 token scopes — ui/src/ui/storage.ts:33
    isIpv4LoopbackLiteral() makes ws://127.0.0.1:18789/openclaw and ws://127.255.255.255:18789/openclaw normalize to the same storage key. That key feeds gateway tokens, URL-edit token preservation, and sessionsByGateway, so a distinct local listener on another 127/8 address can receive the previous gateway's auth token/session unless maintainers explicitly accept that boundary.
    Confidence: 0.88
  • [P2] Prove alias reuse without changing page origins — ui/src/ui/storage.node.test.ts:284
    This test switches location.host between loopback aliases while retaining one mocked sessionStorage. Browser sessionStorage is origin-scoped, so the test can pass without proving a dashboard navigation between those hosts carries the token; add same-origin gatewayUrl-edit coverage or browser-real proof for the supported flow.
    Confidence: 0.84
  • [P3] Remove the release-owned changelog entry — CHANGELOG.md:16
    CHANGELOG.md is release-generated in this repo, and normal PR release-note context belongs in the PR body or commit message. Keeping this line turns a narrow UI auth/storage fix into release bookkeeping and should be dropped before merge.
    Confidence: 0.93

Overall correctness: patch is incorrect
Overall confidence: 0.89

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 5361e5a0b455.

Label changes

Label justifications:

  • P2: This is a normal-priority Control UI auth/storage bug fix with limited blast radius, but the submitted PR needs revision before merge.
  • merge-risk: 🚨 compatibility: The PR changes browser storage key equivalence for existing gateway URLs and can alter which token or settings record is loaded after upgrade.
  • merge-risk: 🚨 session-state: The normalized gateway scope is also used for sessionsByGateway, so selected Control UI sessions can become associated with a different loopback gateway URL.
  • merge-risk: 🚨 security-boundary: The PR broadens gateway auth-token lookup from exact host matching to a shared numeric 127/8 loopback bucket.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🌊 off-meta tidepool and patch quality is 🧂 unranked krab.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Not applicable: This is a bot replacement PR, so the external-contributor proof gate does not apply, but browser-real proof is still useful for the sessionStorage origin behavior.
Evidence reviewed

PR surface:

Source +31, Tests +84, Docs +1. Total +116 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 32 1 +31
Tests 1 84 0 +84
Docs 1 1 0 +1
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 117 1 +116

Security concerns:

  • [medium] Broad numeric loopback token-scope sharing — ui/src/ui/storage.ts:33
    The PR can reuse one gateway token for another local service bound to a different numeric 127/8 address with the same port and path, crossing the exact-host auth boundary current behavior preserves.
    Confidence: 0.88

What I checked:

  • Current main exact-host scope: Current main builds the gateway storage scope from protocol, exact parsed host, and pathname, so localhost and 127.0.0.1 remain distinct today. (ui/src/ui/storage.ts:151, 5361e5a0b455)
  • Scope consumers: The same normalizer feeds token sessionStorage keys, per-gateway session selection, same-scope URL-edit token preservation, settings keys, and sessionsByGateway persistence. (ui/src/ui/storage.ts:157, 5361e5a0b455)
  • PR broadens numeric loopback equivalence: The PR maps every dotted-decimal 127/8 literal to localhost, so 127.0.0.1 and 127.255.255.255 on the same protocol, port, and path share one token/session scope. (ui/src/ui/storage.ts:33, 2057d4113898)
  • In-app URL edit path: The Overview gateway URL input calls resolveGatewayTokenForUrlEdit while editing, so a broadened normalized scope can carry the current token into a newly typed gateway URL without changing page origin. (ui/src/ui/views/overview.ts:272, 5361e5a0b455)
  • Browser proof gap in added test: The added test changes location.host between loopback aliases while retaining one mocked sessionStorage object, which does not model browser origin-scoped sessionStorage behavior. (ui/src/ui/storage.node.test.ts:284, 2057d4113898)
  • Release-owned changelog policy: Root policy says CHANGELOG.md is release-only and normal PRs should carry release-note context in the PR body, squash message, or direct commit instead. (AGENTS.md:234, 5361e5a0b455)

Likely related people:

  • velvet-shark: PR fix(ui): preserve control-ui auth across refresh #40892 introduced the Control UI token/session storage path that this PR changes. (role: introduced behavior; confidence: high; commits: f2f561fab1bf; files: ui/src/ui/storage.ts, ui/src/ui/storage.node.test.ts, ui/src/ui/app-settings.ts)
  • sallyom: PR Scope Control UI sessions per gateway #47453 scoped Control UI sessions per gateway using the same gateway scope affected by this PR. (role: feature contributor; confidence: high; commits: d37e3d582fd8; files: ui/src/ui/storage.ts, ui/src/ui/storage.node.test.ts)
  • steipete: History shows adjacent work keeping dashboard tokens out of URL storage, and the open maintainer-labeled gateway token scopes PR overlaps the broader auth-token area. (role: adjacent owner; confidence: medium; commits: 10d0e3f3ca92; files: ui/src/ui/storage.ts, ui/src/ui/storage.node.test.ts, docs/web/dashboard.md)
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.

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.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label May 30, 2026
@clawsweeper clawsweeper Bot added the rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. label May 30, 2026
@barnacle-openclaw barnacle-openclaw Bot removed the stale Marked as stale due to inactivity label May 30, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Jun 14, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 14, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Jun 15, 2026
@clawsweeper clawsweeper Bot removed the rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. label Jun 15, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 15, 2026
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 clawsweeper Tracked by ClawSweeper automation merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: S status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant