Skip to content

feat(browser): side panel copilot with per-tab agent sessions#93680

Closed
anagnorisis2peripeteia wants to merge 18 commits into
openclaw:mainfrom
anagnorisis2peripeteia:feat/my-browser-node-tools
Closed

feat(browser): side panel copilot with per-tab agent sessions#93680
anagnorisis2peripeteia wants to merge 18 commits into
openclaw:mainfrom
anagnorisis2peripeteia:feat/my-browser-node-tools

Conversation

@anagnorisis2peripeteia

@anagnorisis2peripeteia anagnorisis2peripeteia commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What Problem This Solves

#100619 gave OpenClaw a way to drive your signed-in Chrome: the bundled extension
attaches with chrome.debugger, and the OpenClaw tab group is the consent boundary. What it
does not give you is a way to talk to the agent about the tab you are looking at.

Today, asking anything about the page in front of you means leaving the browser — Telegram, the
CLI, or the Control UI — and then telling the agent, in words, which tab you meant. Two things
fall out of that:

  • No per-tab context. Every surface shares one conversation, so a question about the tab you
    are on lands in the same thread as everything else, and the agent has to guess which of the
    shared tabs you mean.
  • The high-value cases are the annoying ones. Filling a long form or a detailed
    questionnaire is exactly the task you want to hand to an agent while looking at it — and
    exactly the task that is painful to drive from a chat app on your phone.

Why This Change Was Made

The extension already owns the hard parts — the relay, chrome.debugger, and the tab-group
consent model. The missing piece is a conversation surface pinned to a tab, so this PR adds
a side panel on top of what #100619 merged rather than building anything new underneath it:

  • Transport is unchanged. The panel does not touch the relay. It opens its own gateway
    WebSocket as an ordinary operator client (protocol v4, Ed25519 device identity), so the relay
    stays a pure CDP/tab-consent plane and the panel is just another gateway client.
  • The tab is the conversation. Each pinned tab gets a deterministic session key
    (<agent-main>:thread:tab-<id>), so two tabs never mix context and reopening the panel on a
    tab resumes its thread. The key is the identity — no client-side history storage.
  • Consent is reused, not re-invented. The panel can only ask the agent to act on tabs you
    shared into the OpenClaw tab group; its "Stop sharing" button routes through the extension's
    existing toggleShareTab handler. Dragging a tab out (or dismissing Chrome's banner) revokes
    the agent exactly as before.
  • Least privilege. The panel requests only operator.read + operator.write. Two
    consequences, both deliberate: "New chat" mints a fresh per-tab key generation instead of
    calling the admin-only sessions.reset, and tab targeting rides a message preamble rather
    than the admin-only browser.request. No admin scope, no new privileged surface.

Scope boundary. This supersedes this PR's previous node-anchored design, which stacked on
#93411 (gateway.tools.byNode). #100619 made that unnecessary — it proxies browser actions to a
node host natively — so #93411 is closed and all of the node-anchored routing, the
extension-bridge driver, and the second bridge/relay are dropped. What remains is the side
panel and per-tab sessions, layered on the merged extension. No core, gateway, or protocol
changes.

User Impact

Open the popup, click Open copilot panel, and ask for things in plain language about the tab
you are on — "fill this form with my details", "summarize this thread". The agent drives that
tab through the normal browser tool.

  • Per-tab conversations. Each tab keeps its own thread; New chat starts a fresh one.
  • Setup: one required config step. The panel is an extension page, so its WebSocket always
    sends Origin: chrome-extension://<id>, and the Gateway checks any origin against
    gateway.controlUi.allowedOrigins — which is empty by default. The extension's origin must be
    added there once, on the same machine too; loopback does not exempt it. Without it the panel now
    reports Gateway refused the connection: origin not allowed rather than looping silently. After
    that it pairs silently locally; a remote gateway needs a one-time openclaw devices approval.
    Gateway URL/token live in the panel's ⚙ settings, and a gateway-hosted relay pairing already
    names the gateway, so remote setups need no second URL. The panel needs Chrome 137+ (Ed25519
    device keys); the rest of the extension keeps working below that and says so.
  • Nothing changes if you do not open it. The popup, relay, tab group, and chrome profile
    behave exactly as they do today; the panel is additive.
  • Compatibility. No new config keys, no defaults changed, no migration — but the panel does
    require one entry in the existing gateway.controlUi.allowedOrigins list (see Setup). Nothing
    changes for existing users who never open the panel.

Docs: docs/tools/chrome-extension.md gains a "Side panel copilot" section.

Evidence

Real behavior, paired before/after, same surface, real Chromium 152 (not headless
Chrome-for-Testing). Identical scenario both sides: load the extension, pair, share the
example.org tab, open the popup from the toolbar, and use the panel to ask the agent to navigate
that shared tab to wikipedia.org.

Base (56eda48f20f) This PR (c688f94de6c)
Popup buttons Stop sharing this tab, Unpair + Open copilot panel
sidepanel.html does not exist docks as a real side panel
Ask about this tab no surface exists panel pins the tab, streams the reply
Shared tab stays on example.org driven to wikipedia.org

Recording: the whole thing in one take (32s, real Chromium 152)

One continuous take, no cuts: fresh per-tab thread, request typed, agent drives the shared tab.
Driven at this PR's head on the gateway config this page documents — allowedOrigins holding
only the extension's own origin, nothing permissive.
The stills below are frames of this same recording.

https://raw.githubusercontent.com/anagnorisis2peripeteia/openclaw/d1f3da6b98fb22f18e679ba2abb92881e9657b8b/05-panel-demo.mp4

Visual before/after (same surface, real Chromium 152)

Before — base 56eda48f20f. Same extension popup, same clicks. Only "Stop sharing this tab"
and "Unpair": there is no copilot surface to open, so the scenario cannot start. (This capture was
taken at c94da0df6f3 before the branch was rebased; no commit between that revision and the
current base touches extensions/browser/, so the base extension is byte-identical and the popup
is unchanged. Re-verified on the current base: sidepanel.html still does not exist and
popup.js still contains zero panel references.)

Before: base popup has no copilot panel button

After — this PR. The popup gains "Open copilot panel"; clicking it docks the side panel,
which shows green Connected, pins the shared tab, and reports "This tab has its own
conversation".

After: side panel docked, Connected, pinned to the shared tab

After — asking in natural language. Typing the request into the panel:

After: request typed into the side panel

After — the agent drives the shared tab. The panel streams the reply and two browser tool
steps, and the same tab moves example.org → wikipedia.org (note the tab is still inside the
orange OpenClaw tab group — the consent boundary held):

After: panel conversation and the shared tab now on wikipedia.org

  • Before (base): the extension exposes no copilot surface — sidepanel.html is absent and
    popup.html/popup.js contain zero panel references. The popup renders only "Stop sharing
    this tab" and "Unpair", so the scenario cannot proceed: there is nothing to open.
  • After (this PR): the same popup exposes "Open copilot panel"; the panel docks, shows green
    Connected, pins the shared tab ("This tab has its own conversation"), and typing
    "Navigate this tab to wikipedia.org" + Send streamed the assistant reply plus two browser
    tool steps and drove the same tab (E1A50B8F1A849579F7B316844119F354) from example.org to
    https://www.wikipedia.org/. Independently confirmed:
# before the turn
1. Example Domain [use: t1 tab: t1]
   https://example.org/
   id: E1A50B8F1A849579F7B316844119F354

# after the turn (same id, no new tab)
1. Wikipedia [use: t1 tab: t1]
   https://www.wikipedia.org/
   id: E1A50B8F1A849579F7B316844119F354

What driving this for real, and reviewing it hard, actually caught — none of it visible to unit
tests or static checks:

  • Live-only handshake. The panel first sent an invented client.id, which the gateway enums —
    the handshake failed with invalid connect params: at /client/id. Only a real gateway handshake
    surfaces that.
  • The documented setup did not work. The panel is an extension page, so its WebSocket always
    sends an Origin header, which forces the gateway's origin check; allowedOrigins is empty by
    default, so the "pairs silently on loopback" flow this PR originally claimed actually failed
    closed, and the panel showed only a bare status code. The first proof runs missed it because the
    rig had allowedOrigins: ["*"]. The recording above is now driven with only this extension's
    own origin allowlisted
    , which is what the docs now tell you to do, and the panel reports what
    the gateway said instead of looping silently.
  • Prompt injection through the tab title. buildTabPreamble interpolated the page-controlled
    document.title and URL straight into every turn, so a hostile title carrying ] and a newline
    could break out and issue instructions with the user's own authority — bypassing the boundary
    this plugin already enforces, where browser-originated text is fenced as untrusted before agents
    see it (browser-tool.actions.ts). Both values are now sanitized and fenced the same way, with
    tests that fail against the unfenced version.
  • Silent-loss lifecycle bugs, the class this panel exists to avoid. A reconnect mid-run can
    deliver only the terminal chat event (the gateway dedupes its pre-terminal flush), and the panel
    discarded that snapshot, so the reply never rendered. The composer only re-enables on a run's
    terminal event, so New chat mid-run — or a dropped socket — stranded it permanently; the
    lockout was reproduced and then fixed red-green on the live rig. Subscriptions were recorded
    before the gateway confirmed them and never re-established after a reconnect. Teardown could race
    its own retry timers.

Tests / checks

  • node scripts/run-vitest.mjs extensions/browser/chrome-extension/modules/panel-core.test.ts — 44 passing.
    Covers the per-tab key derivation (incl. the generation suffix), the protocol-v4 delta
    segmenter (incremental deltaText, cumulative-snapshot idempotency, replace=true,
    runId reset, tool-boundary segmentation, buffer-reset rebase), markdown escaping, and the
    gateway-URL derivation.
  • Mutation (marmorkrebs --tool stryker on modules/panel-core.js, tests scoped to
    panel-core.test.ts): 187 mutants, score 0.95. It earned its place twice. It first showed the
    "ws:" literal in gatewayUrlFromRelayUrl() could be replaced with "" and leave every test
    green — nothing exercised a ws:// relay that should succeed, though the check deliberately
    accepts it. Later it showed the new preamble-sanitizer tests only asserted the hostile payload was
    absent, so a sanitizer that swapped the delimiters for other junk, or dropped newlines instead of
    spacing them, still passed while mangling honest titles. Both gaps now have tests, each red-green
    verified against the unfixed code.
    Two caveats stated rather than hidden. The score moves between runs of identical source because the
    command runner starts a fresh vitest per mutant, so most mutants land as Timeout rather than
    Killed and that split races. And the survivor list is not trustworthy at face value — each was
    re-applied by hand, and at least one (if (!payload || typeof payload !== "object") -> false) is
    a false survivor: applying it fails the suite. The ones that do reproduce are equivalent mutants
    (a scratch array whose seeded element is never referenced; String(x ?? "") fallbacks where any
    replacement yields the identical result, since new URL("…") throws exactly as new URL("") does).
    Scope: the pure module is the unit-tested surface, mirroring this extension's existing boundary
    (modules/relay-core.js has relay-core.test.ts; background.js/popup.js have no unit
    tests). The chrome/DOM glue (sidepanel.js, device-identity.js, popup.js) is covered by the
    before/after proof above rather than mocked unit tests.
  • node scripts/run-oxlint.mjs extensions/browser/chrome-extension — clean.
  • oxfmt clean; node scripts/format-docs.mjs --check clean; docs/docs_map.md regenerated.

Size (this PR only)

 docs/docs_map.md                                               |    1 +
 docs/tools/chrome-extension.md                                 |   47 +
 extensions/browser/chrome-extension/device-identity.js         |   76 +
 extensions/browser/chrome-extension/manifest.json              |    3 +
 extensions/browser/chrome-extension/modules/panel-core.d.ts    |   33 +
 extensions/browser/chrome-extension/modules/panel-core.js      |  225 +
 extensions/browser/chrome-extension/modules/panel-core.test.ts |  351 +
 extensions/browser/chrome-extension/popup.html                 |    1 +
 extensions/browser/chrome-extension/popup.js                   |   12 +
 extensions/browser/chrome-extension/sidepanel.html             |  211 +
 extensions/browser/chrome-extension/sidepanel.js               |  824 +
 11 files changed, 1783 insertions(+), 1 deletion(-)

The extension ships unbundled, so sidepanel.* and modules/panel-core.* ride the existing
copy-chrome-extension.mjs static copy with no build change, and the colocated
panel-core.test.ts and panel-core.d.ts are excluded from the shipped artifact by that
script's existing filter (matching relay-core.js / .d.ts / .test.ts).

Known limitation. chrome.sidePanel.open() requires a real user gesture, so the panel opens
from the popup button rather than the toolbar icon directly (the action already owns the popup
for pairing).

Review history: every review round on this branch (autoreview + local ClawSweeper), with the complete reviews and per-finding dispositions: https://gist.github.com/871e4fb8db67e9016c570197ccb5f099

@openclaw-barnacle openclaw-barnacle Bot added app: macos App: macos gateway Gateway runtime commands Command implementations agents Agent runtime and tooling proof: supplied External PR includes structured after-fix real behavior proof. size: XL labels Jun 16, 2026
@clawsweeper

clawsweeper Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed July 16, 2026, 12:14 AM ET / 04:14 UTC.

Summary
Adds a Chrome extension side-panel chat client with per-browser-session tab threads, operator gateway authentication, streamed replies, sharing controls, documentation, and focused helper tests.

PR surface: Source +1383, Tests +351, Docs +48. Total +1782 across 11 files.

Reproducibility: yes. for the review blocker: the required documentation command sets the complete allowed-origin array, so following it without manually reconstructing existing entries demonstrably replaces those settings. The feature itself is not a bug reproduction and has convincing positive Chromium proof.

Review metrics: 2 noteworthy metrics.

  • Configuration surface: 0 keys added, 0 defaults changed, 1 existing allowlist newly required. The feature is additive at rest, but enabling it requires modifying a whole-array security setting and therefore needs upgrade-safe setup.
  • Gateway client surfaces: 1 new operator WebSocket client. The extension now independently owns authentication, subscriptions, streaming, reconnects, and session lifecycle rather than only the existing CDP relay.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🦐 gold shrimp
Result: needs maintainer review before merge.

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

Rank-up moves:

  • Replace manual whole-array origin configuration with an append-safe guided setup or Doctor/CLI flow.
  • Obtain explicit browser-extension and security-owner approval for the bundled webchat identity and persistent operator credential lifecycle.

Risk before merge

  • [P1] The required copy-paste setup replaces the entire gateway.controlUi.allowedOrigins array; an incomplete manual merge can disconnect an existing Control UI or other allowed browser client.
  • [P1] The new panel implements a second long-lived gateway chat, subscription, streaming, and reconnect client inside the extension, so future protocol or lifecycle drift could duplicate, suppress, or strand messages despite the current focused coverage.
  • [P1] The extension persists reusable operator device-key material and may persist a gateway token while identifying as the existing webchat client class; that security and client-identity model needs explicit owner approval before becoming bundled behavior.

Maintainer options:

  1. Repair setup and confirm boundaries (recommended)
    Replace the whole-list configuration step with a safe guided append flow and obtain owner approval for the webchat identity and persistent operator credentials.
  2. Accept the advanced-user contract
    Merge with the manual allowlist reconstruction and credential design explicitly documented as intentional supported behavior.
  3. Pause the bundled surface
    Close or redesign the PR if maintainers want the Chrome extension to remain a transport and consent adapter rather than a second chat client.

Next step before merge

  • [P2] A maintainer must first approve the bundled chat-client boundary and credential model; after that, the contributor has a concrete append-safe setup repair to make on this branch.

Maintainer decision needed

  • Question: Should the bundled Chrome extension become an operator chat client with persistent per-tab sessions, and if so must origin enrollment and credential storage be redesigned before merge?
  • Rationale: The implementation is functional and well-proven, but repository policy does not resolve whether the transport-focused bundled extension should own a full chat surface or require users to modify a security-sensitive allowlist manually.
  • Likely owner: steipete — The current browser-extension architecture and consent model entered main through their merged work, so they are the strongest available owner for whether this UI belongs on that surface.
  • Options:
    • Sponsor with safer setup (recommended): Approve the bundled side panel, but require an append-safe guided origin-enrollment flow and explicit approval of the stored operator-credential model before merge.
    • Accept the documented setup: Approve the current manual allowlist merge and persistent credential design as an intentional advanced-user contract.
    • Keep the extension transport-only: Decline the bundled chat surface and move tab-focused conversation work to a separately owned UI or plugin design.

Security
Needs attention: No third-party dependency or workflow supply-chain change is present, but the new persistent operator credential and client-identity model needs explicit security-owner approval.

Review findings

  • [P1] Preserve existing allowed origins during setup — docs/tools/chrome-extension.md:138-142
Review details

Best possible solution:

Keep the per-tab copilot only with browser-extension owner approval, add a guided setup or CLI/Doctor path that safely appends the exact extension origin without replacing existing settings, and document and test the approved operator-credential lifecycle and reconnect behavior.

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

Yes for the review blocker: the required documentation command sets the complete allowed-origin array, so following it without manually reconstructing existing entries demonstrably replaces those settings. The feature itself is not a bug reproduction and has convincing positive Chromium proof.

Is this the best way to solve the issue?

No, not yet. The side-panel and per-tab session design is plausible, but requiring manual whole-list security configuration is not the safest setup path, and bundled chat ownership plus credential persistence still requires explicit product and security approval.

Full review comments:

  • [P1] Preserve existing allowed origins during setup — docs/tools/chrome-extension.md:138-142
    The required setup command replaces the entire gateway.controlUi.allowedOrigins array. A user who follows the copy-paste example without perfectly reconstructing every existing value can revoke the Control UI or another allowed client and lock it out. Provide an append-safe CLI/Doctor or guided in-product enrollment path rather than making manual whole-list merging the supported setup.
    Confidence: 0.94

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 4b7f193c9d57.

Label changes

Label changes:

  • add proof: 🎥 video: Contributor real behavior proof includes video or recording evidence. A continuous real-Chromium recording and paired screenshots show the after-fix panel connecting with the documented exact-origin allowlist, sending a natural-language request, streaming the response, and driving the same shared tab; the evidence is appropriately redacted.

Label justifications:

  • P2: This is a substantial optional browser-workflow improvement with bounded impact, strong proof, and remaining setup and ownership decisions rather than an urgent existing-user regression.
  • merge-risk: 🚨 compatibility: The required setup can overwrite existing gateway.controlUi.allowedOrigins entries and stop already configured browser clients from connecting.
  • merge-risk: 🚨 message-delivery: The new extension client independently handles streamed snapshots, terminal events, subscriptions, session swaps, and reconnects, where drift can duplicate or suppress visible conversation output.
  • merge-risk: 🚨 security-boundary: The PR adds persistent operator credentials, extension-origin enrollment, hostile-page preamble handling, and loopback authentication decisions to a browser extension with debugger access.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (recording): A continuous real-Chromium recording and paired screenshots show the after-fix panel connecting with the documented exact-origin allowlist, sending a natural-language request, streaming the response, and driving the same shared tab; the evidence is appropriately redacted.
  • proof: sufficient: Contributor real behavior proof is sufficient. A continuous real-Chromium recording and paired screenshots show the after-fix panel connecting with the documented exact-origin allowlist, sending a natural-language request, streaming the response, and driving the same shared tab; the evidence is appropriately redacted.
  • proof: 🎥 video: Contributor real behavior proof includes video or recording evidence. A continuous real-Chromium recording and paired screenshots show the after-fix panel connecting with the documented exact-origin allowlist, sending a natural-language request, streaming the response, and driving the same shared tab; the evidence is appropriately redacted.
Evidence reviewed

PR surface:

Source +1383, Tests +351, Docs +48. Total +1782 across 11 files.

View PR surface stats
Area Files Added Removed Net
Source 8 1384 1 +1383
Tests 1 351 0 +351
Docs 2 48 0 +48
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 11 1783 1 +1782

Security concerns:

  • [medium] Confirm persistent exportable operator-key storage — extensions/browser/chrome-extension/device-identity.js:17
    The panel generates an extractable Ed25519 key and persists the exported private key in extension-local storage for reuse. This creates a reusable operator credential inside a debugger-capable extension; confirm it matches the approved gateway client threat model or use a less-exportable storage design.
    Confidence: 0.82

What I checked:

Likely related people:

  • steipete: Authored the merged browser-extension relay and tab-group consent implementation that owns the current-main surface this side panel extends. (role: introduced the current browser-extension architecture; confidence: high; commits: d6801f23d4e5; files: extensions/browser/chrome-extension/background.js, extensions/browser/chrome-extension/manifest.json, extensions/browser/src/browser/extension-relay/relay-bridge.ts)
  • vincentkoc: The PR timeline records vincentkoc assigning the item during its initial review cycle, making them a useful routing candidate for the remaining product and setup decision. (role: assigned reviewer and likely product-routing owner; confidence: medium; files: extensions/browser/chrome-extension/manifest.json, docs/tools/chrome-extension.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.
Review history (2 earlier review cycles)
  • reviewed 2026-06-26T17:04:55.643Z sha e01d567 :: found issues before merge. :: [P2] Regenerate config help for extension-bridge | [P2] Align the public SDK profile driver contract
  • reviewed 2026-07-16T03:04:47.392Z sha c688f94 :: found issues before merge. :: [P1] Bind browser actions to the panel's pinned tab

@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@vincentkoc vincentkoc self-assigned this Jun 16, 2026
@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@anagnorisis2peripeteia
anagnorisis2peripeteia marked this pull request as ready for review June 17, 2026 05:36
@anagnorisis2peripeteia
anagnorisis2peripeteia requested a review from a team as a code owner June 17, 2026 05:36
@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@github-actions github-actions Bot added the dependencies-changed PR changes dependency-related files label Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Dependency graph guard cleared

This PR no longer has blocked dependency graph changes. A future dependency graph change requires a fresh /allow-dependencies-change comment after the guard blocks that new head SHA.

  • Current SHA: c688f94de6c1112c9624a8864fe4852fc8a323bd

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. 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: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jun 17, 2026
@openclaw-barnacle openclaw-barnacle Bot added scripts Repository scripts and removed commands Command implementations labels Jun 20, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. and removed status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 20, 2026
The panel recorded subscribedKey before the subscribe round trip and only
cleared it when settings changed, so a dropped socket or a failed subscribe
left a stale key that made subscribeSession() early-return and silently never
resubscribe. Record the key only once the gateway confirms, and clear it
whenever the socket goes away.

The NOT_PAIRED retry also closed the socket before calling connect(), which
awaits storage before reassigning ws - so the close listener still saw the
current socket and scheduled a competing reconnect. Route both teardown sites
through dropSocket(), which nulls ws before close() the way the settings path
already did.
Mutation testing surfaced a real gap, not an equivalent mutant: replacing the
"ws:" literal in gatewayUrlFromRelayUrl() with "" left every test passing,
because wss:// misses that literal, http:// fails both branches identically,
and the loopback case returns null via the pathname check regardless. Nothing
exercised a ws:// relay that should succeed, even though the protocol check
deliberately accepts it.
…s thread

Two silent-failure paths the review surfaced. The handshake sets 'Connected'
before handleHelloOk() binds the tab session, and the rejection was dropped, so
a bind failure left the panel looking healthy over a session that never
attached; handleHelloOk() now owns that failure and reports it.

The tabs.onRemoved handler cleared pinnedTabId but left sessionKey and the
input live, and sendMessage() only guards on the socket - so a turn could still
be sent into the thread of a tab that no longer exists. Release the session and
close input when the pinned tab goes away.
…ad session

sendMessage() disables input and the only path that re-enables it is the run's
terminal chat event, which handleChatEvent() drops when payload.sessionKey no
longer matches. Two deterministic lockouts followed. Clicking New chat mid-run
swaps sessionKey, so the in-flight run's terminal event is filtered and the
composer never comes back; a socket drop mid-run is worse, because no timeout
covers a chat event that will never arrive.

Abandon the turn with the socket - fail waiting requests instead of letting them
sit until the 30s timeout, finalize the bubble, and hand the composer back - and
re-enable input when a fresh thread starts. Also report a send attempted while
reconnecting instead of silently dropping it.
…acing itself

Three lifecycle gaps the review found. bindTabSession() returned early when the
derived key already matched sessionKey, which is exactly the reconnect case, so
a reconnected panel never resubscribed and silently received no chat events -
clearing subscribedKey on close was not enough on its own. dropSocket() left a
reconnect timer scheduled by an earlier close running, so it fired alongside the
connect() that follows the drop and left two sockets. And abandonInFlightTurn()
re-enabled the composer unconditionally, undoing the deliberate lockout for a
tab that had been closed.
Four gaps the review found. A reconnect mid-run can deliver only the terminal
chat event - the gateway dedupes its pre-terminal flush against what it already
broadcast, so the reply reaches the new socket once, in that payload's snapshot
- and the terminal branch discarded it whenever no streamed bubble existed, so
the reply silently never rendered. It dropped errorMessage the same way when a
run failed before any delta. Render both.

The NOT_PAIRED retry timer was untracked while the gateway also closes the
socket, so a stale timer could tear down an already-healthy connection after
the device was approved. onNewChat() bumped the persisted tab generation before
any gateway round trip and threw into a void handler, stranding the pane on the
old thread while sessionKey pointed at the new one. And Ed25519 WebCrypto only
ships enabled from Chrome 137 while the manifest supports 125, where the panel
died as a bare 'Auth failed' - handleChallenge now reports the real cause.
…panel origin

The review found the panel's own trust boundary was the one being crossed.
buildTabPreamble interpolated document.title and the URL straight into every
turn, so a hostile page title carrying ']' and a newline could break out of the
preamble and issue instructions with the user's authority - while this plugin
already fences browser-originated text as untrusted before agents see it
(browser-tool.actions.ts). Sanitize both values and fence them the same way.

The documented setup was also wrong. The panel is an extension page, so its
WebSocket always sends an Origin header, which forces the gateway's origin
check; allowedOrigins is empty by default, so the 'pairs silently on loopback'
flow actually failed closed and surfaced only a bare status code. Make the
allowlist step unconditional in the docs and body, and report what the gateway
said instead of a lone code.

Also clear the pairing retry timer on hello-ok - the close-driven backoff beats
it, so an approval landing in that window was torn down at +5s - and dedupe the
auth-failure notice, which otherwise repeats forever on pre-137 Chrome.
…oad's absence

Mutation surfaced that the injection tests only checked the hostile payload was
absent, so a sanitizer that swapped the delimiters for other junk, or dropped
newlines instead of spacing them, still passed while mangling honest titles.
Pin the exact fenced text, and pin the instructions the preamble must still
carry.
…und trips

startFreshThread() persisted the generation bump and swapped sessionKey, then
awaited the gateway before touching the pane. If either call threw, the pane
kept showing the old thread while every later send landed in the new one, and
the error claimed the fresh conversation had not started when the swap was
already permanent. Show the new thread first, so a failure reports only what is
actually missing - the gateway session - against a pane that matches where sends
go.

bindTabSession() also subscribed and hydrated with its locally derived key while
ensureSession() adopts the gateway's canonical echo into sessionKey, which is
what handleChatEvent filters on. Identity today, but a canonicalizing gateway
would leave that path listening to a key no event carries; both paths now use
the canonical key.
…ng the reply

Dropping the socket mid-run freezes the partial bubble and resets the stream, so
a run that survived the reconnect came back with an unmatched runId and rendered
its whole cumulative snapshot underneath the partial - the same reply twice. No
data was lost, but it is a display bug in exactly the lifecycle this panel is
meant to get right. chat.history carries the in-flight run, so redraw from it on
a same-key reconnect.

The docs also handed users a foot-gun: 'openclaw config set
gateway.controlUi.allowedOrigins' sets the whole list, so a copy-paste would
revoke any origins already allowed, the Control UI's own included. Say so.
The previous commit redrew from chat.history on a same-key reconnect to stop a
surviving run repeating its text. Driving it proved that trade backwards: the
redraw wiped the pane mid-run and rebuilt from a history that did not yet carry
the just-sent user message, so the turn vanished from the pane while the agent
worked on it. A duplicated reply is cosmetic; a lost message is not. Keep the
resubscribe, drop the rehydrate, and record why at the call site.
…a dead branch

The terminal chat event's snapshot was only applied when no bubble existed. The
gateway broadcasts the pre-terminal flush drop-if-slow but not the terminal
event, so a slow connection could miss the flush and keep stale partial text as
the final answer. applyChatDelta is snapshot-idempotent, so applying it every
time costs nothing and closes that gap.

The session.tool branch could never fire: that mirror goes to sessions.subscribe
subscribers and the panel subscribes to messages, not sessions. Worse, the
comment claimed the opposite. Drop the branch and say what actually reaches the
panel - a run it did not start shows no step lines.
…hole URL

isLoopbackUrl decides whether the gateway token is optional, and it matched
//localhost anywhere in the string - including in a path, query or fragment. So
https://evil.example/x//localhost/ read as loopback and the panel would have
connected to a remote gateway with no token at all. The existing test only
covered a loopback-looking host label, never a loopback-looking path.

Parse the URL and compare the hostname. Tests pin the path, query, fragment and
userinfo variants, and fail against the old regex.
Tab ids are only unique within a browser session - Chrome reissues them from a
low counter every launch - but gateway sessions persist. Keying a thread on the
tab id alone therefore handed a brand-new tab the previous launch's conversation
for that id: hydrateHistory rendered a stranger's thread and new turns continued
it. That breaks the panel's central promise that two tabs never mix context, and
it hits low-numbered tabs after every restart.

Fold a per-launch nonce (kept in storage.session, alongside the generation map
it already scopes) into the derived key, and refuse to derive a key without it
rather than falling back to a colliding one. This trades resume-across-restart,
which tab-id reuse had already made meaningless, for correctness.
@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Force-pushed to c688f94de6c (was a8c8f2c68ab). Rebased onto current main and reworked; the
Evidence section is rebuilt against this head.

Since the last push, driven review found and fixed real defects rather than cosmetics — each one
verified against source or executed on a live rig, and each fix red/green proven against the unfixed
code:

  • Auth: isLoopbackUrl pattern-matched //localhost anywhere in the URL, so
    https://evil.example/x//localhost/ read as loopback and waived the gateway token for a remote
    gateway. Now parses the host.
  • Prompt injection: the tab's page-controlled title/URL rode every turn unsanitized; a hostile
    title could break out and issue instructions at user authority. Now sanitized and fenced as
    EXTERNAL_UNTRUSTED_CONTENT, matching this plugin's own boundary.
  • Context bleed: per-tab threads keyed on the tab id alone, but Chrome reissues ids from a low
    counter each launch while gateway sessions persist — so a new tab inherited a stranger's
    conversation after a restart. Keys are now scoped to a per-launch nonce.
  • Setup was wrong: the panel's WebSocket always sends an Origin, which forces the gateway's
    origin check, and allowedOrigins is empty by default — so the documented "pairs silently on
    loopback" flow failed closed. The docs now make the allowlist step unconditional, and the panel
    reports the gateway's actual refusal instead of a bare code.
  • Plus a set of silent-loss lifecycle fixes (composer lockouts, reconnect resubscribe, terminal-only
    replies, teardown races).

The recording is driven on the gateway config the docs prescribe — allowedOrigins holding only
this extension's origin, nothing permissive — after an earlier run was found to have leaned on a
wildcard.

Review history, with every finding, disposition and the evidence behind it:
https://gist.github.com/871e4fb8db67e9016c570197ccb5f099

@clawsweeper

clawsweeper Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Command router queued. I will update this comment with the next step.

@openclaw-barnacle

Copy link
Copy Markdown

This assigned pull request has been automatically marked as stale after being open for 27 days.
Please add updates or it will be closed.

@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

superseded by #109817

@steipete

Copy link
Copy Markdown
Contributor

AI-assisted (approved by Peter)

Thanks @2CB2 — your implementation established the side-panel copilot direction and served as the basis for the secure rebuild in #109817. The landed version keeps that product model while adding dedicated device pairing and token custody, Gateway-owned targeted session delivery, typed tab enforcement, per-tab Side Panel instances, durable archive-on-tab-close retention, denial UX, and Chromium isolation/lifecycle proof.

Closing this prototype in favor of #109817. Thank you for the substantial foundation and for pushing this forward.

@anagnorisis2peripeteia

Copy link
Copy Markdown
Contributor Author

AI-assisted (approved by Peter)

Thanks @2CB2 — your implementation established the side-panel copilot direction and served as the basis for the secure rebuild in #109817. The landed version keeps that product model while adding dedicated device pairing and token custody, Gateway-owned targeted session delivery, typed tab enforcement, per-tab Side Panel instances, durable archive-on-tab-close retention, denial UX, and Chromium isolation/lifecycle proof.

Closing this prototype in favor of #109817. Thank you for the substantial foundation and for pushing this forward.

@steipete thanks! That's my handle on a different app if you wouldn't mind using: @anagnorisis2peripeteia

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

Labels

docs Improvements or additions to documentation merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. size: XL stale Marked as stale due to inactivity 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.

4 participants