feat(bridge): Phase 1 MV3 read-only browser extension bridge#490
Conversation
Wake-and-Connect architecture: FCM silent push wakes MV3 service worker, opens WebSocket to /agent/browser, SESSION_END tears down. Firestore as cross-instance session rendezvous. FCM physical-tap approval gate for destructive actions (Phase 2). Task DSL with two-layer destructive action classifier (Cloud Coordinator + local extension DOM validator). Supersedes April 2026 draft spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cross-instance routing: voice-side watchTasks listener owns result delivery, not sessionBridge.voiceWs (which is same-instance only) - browser_action tool schema added to buildLiveTools and edge tools; includes credit billing and voice-side Firestore listener pattern - Firebase auth in MV3 SW: side panel stores refreshToken, SW mints idToken via Secure Token REST API on each wake - FCM push corrected to chrome.gcm API (not firebase/messaging SDK); Admin SDK accepts GCM tokens natively - Phase 1 scope expanded to all 6 read-only/navigation actions - Billing: 1 credit per browser_action, refund on EXTENSION_OFFLINE - Manifest key note: private .pem must not be committed to VCS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use Firestore-backed wake timeouts, strict security rules, cloud-only browser_action, offscreen Firebase Auth, and active-device selection. Co-authored-by: Cursor <cursoragent@cursor.com>
Fix silent double-billing: billingTimer must pause during browser_action on voice path; pauseBilling()/resumeBilling() contract now explicit. Replace 60-min Cloud Run assumption with 30s Promise.race keyed to GCP LB backend timeout; adds EXECUTION_TIMEOUT error code to cover extension- connected-but-hung case on text path. Replace "cryptographic proof of intent" with "deterministic proof of identity" for approval flow; clarify approvalToken proves uid, not action. Add isPaused to device doc; getActiveDevice returns null (no credit spent) when device paused — prevents billing bleed on Pause kill switch. Drop @google-cloud/firestore prereq (firebase-admin already exposes it). Add chrome.gcm deprecation and Cloud Run scale-down to risk table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
27-task TDD plan covering cloud-agent coordinator + MV3 extension. Shared DSL types/schema, Firestore rules, browser_action tool with contextual billing, /agent/browser WS handler, and extension scaffold through manual E2E gate. Phases 2-4 deferred to separate plans. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ship wake-and-connect coordination via Firestore, browser_action tool, /agent/browser WebSocket, and the desktop extension scaffold. Co-authored-by: Cursor <cursoragent@cursor.com>
Add voice execution timeout, Layer-1 intentRequiresAuth classifier, lastSeenAt refresh on session_ready, paused-device WS rejection, and build-time extension env injection. Update design spec. Remove superseded ephemeral superpowers plans. Co-authored-by: Cursor <cursoragent@cursor.com>
- Split BrowserActionDeps.uid into firebaseUid (Firestore paths) and userId (Postgres billing) to fix E2E UID mismatch - Add shared/hostPolicy.ts with cloud-side blocklist rejecting chrome://, file://, etc. before credit spend or FCM dispatch - Wire HOST_NOT_ALLOWED check in browserAction tool - Extension content-bridge writes pendingHost/pendingOrigin on HOST_PERMISSION_REQUIRED for side-panel grant flow - Service-worker opens side panel on notification click - Panel.ts shows/hides Grant Access button reactively Co-authored-by: Cursor <cursoragent@cursor.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Reflect MV3 Desktop Bridge architecture in C4 diagrams and regenerate HOOKS and MACHINES dependency charts. Co-authored-by: Cursor <cursoragent@cursor.com>
Exclude extension and shared bridge tests from Jest (they use node:test with .js ESM imports) and run them in the staging workflow via tsx. Co-authored-by: Cursor <cursoragent@cursor.com>
/fix-pr follow-upCommit: Review resolution
Verification
|
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (1)
cloud-agent/src/tools/browserAction.test.ts (1)
73-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a regression for post-charge setup failures.
This suite still doesn't cover the case where
createSession,writeTask, orwakeExtensionthrows afterspendCredit()succeeds. That gap would miss the leaked pause/refund path in the voice flow.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cloud-agent/src/tools/browserAction.test.ts` around lines 73 - 131, Add a regression test in browserAction.test.ts for the voice flow where spendCredit() has already run but a later setup step fails, covering createSession, writeTask, or wakeExtension throwing after the charge. Update the browserActionTool execute path test setup to simulate one of those post-charge failures and assert the failure is surfaced while the paused billing state is cleaned up correctly. Use the browserActionTool and execute symbols to locate the flow, and verify the refund/resumeBilling path is not leaked when the exception happens after charging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cloud-agent/src/handlers/wsBrowserAgentHandler.test.ts`:
- Around line 82-88: The invalid-device test in wsBrowserAgentHandler.test.ts is
using a non-UUID sessionId, so auth fails before the validateDevice path in
handleBrowserWsUpgrade can run. Update the test data in rejects auth when
deviceId invalid to use a valid UUID-formatted sessionId (and keep the invalid
deviceId) so the request reaches the device validation branch and asserts the
correct close code.
In `@cloud-agent/src/handlers/wsBrowserAgentHandler.ts`:
- Around line 23-29: The task_error frame schema in wsBrowserAgentHandler is too
permissive because errorFrameSchema currently accepts any code and failedAction
values before casting them into BridgeErrorCode and SingleAction. Tighten the
validation by using the shared DSL/types for these fields directly in the schema
and in the task_error handling path so malformed frames are rejected before
being written to Firestore; update the parsing logic around errorFrameSchema and
the task_error processing code to rely on the shared contract instead of unsafe
coercion.
- Around line 60-83: Handle rejections from the async WebSocket handlers instead
of firing them with void: in wsBrowserAgentHandler’s onAuth flow and the
message/error handlers around it, wrap calls like verifyToken, resolveUserId,
validateDevice, fs.getSession, fs.getFirstTask, and fs.markBrowserConnected in a
try/catch (or central promise handler) so any failure closes the socket with a
deterministic terminal response and logs the error. Use the existing onAuth and
related handler wiring to locate the fix, and avoid leaving rejected promises
unobserved.
- Around line 70-72: The session gate in wsBrowserAgentHandler should treat
aborted sessions as terminal too, not just closed. Update the check after
fs.getSession(resolved, sid) so that the WebSocket is rejected when
session.status is either 'closed' or 'aborted', matching the terminal behavior
already modeled by closeSession and preventing reconnects for aborted sessions.
In `@cloud-agent/src/handlers/wsLiveAgentHandler.test.ts`:
- Around line 123-126: The test in wsLiveAgentHandler.test.ts has a duplicate
fakeSetInterval declaration, causing a parse-time duplicate identifier error.
Update the billing controller test setup in the pauseBilling/resume block so
fakeSetInterval is defined only once, and keep the single declaration wired into
makeBillingController and the interval-spending assertions.
In `@cloud-agent/src/handlers/wsLiveAgentHandler.ts`:
- Around line 380-384: The delayed browser result in pushToLive is using a new
random UUID instead of the original Gemini FunctionCall.id, so the response
cannot be matched correctly. Update wsLiveAgentHandler’s browser_action flow to
capture the original call id when the function is invoked and pass that same id
into pushToLive, so geminiSession.sendToolResponse sends a functionResponse with
the original identifier rather than crypto.randomUUID().
In `@cloud-agent/src/index.ts`:
- Around line 323-338: The browser bridge routes are mounted even when the Admin
SDK is not initialized, so authenticated requests can reach handlers like
register-device and later fail inside
admin.firestore()/defaultFirestoreSession(). Add an early guard in the browser
HTTP/WS entry points (for example in runAgentReal() setup and the route handlers
around register-device / related browser bridge methods) that checks whether the
bridge is configured and returns a clear “bridge unavailable” response instead
of proceeding. Use the existing bridge setup condition based on
admin.apps.length > 0 to gate these endpoints consistently.
- Around line 311-320: The upsert logic in upsertDevice currently overwrites
device pause state and the original registration timestamp on every
re-registration. Update the firestore write in the upsertDevice callback so
isPaused is only included when body.isPaused is explicitly provided, and make
registeredAt set only on first insert rather than being reset by later merges.
Add a regression test covering the omitted-isPaused path to verify a token
refresh does not unpause the device or تغيير the first-paired timestamp.
In `@cloud-agent/src/services/firestoreSession.ts`:
- Around line 43-53: The `getActiveDevice()` query in `firestoreSession.ts` is
stricter than the authentication logic in `cloud-agent/src/index.ts` because it
only accepts `isPaused === false`, while the browser connect path treats a
missing `isPaused` as active. Update `getActiveDevice()` to use the same
semantics as the connect flow (or ensure the field is backfilled consistently)
so older `DeviceDoc` records can still be selected; focus on the
`getActiveDevice` method and its `where('isPaused', '==', false)` filter.
- Around line 69-74: The state transition in markBrowserConnected is split
across two independent Firestore writes, so the session can be left in routing
if the task update fails. Update this flow to use a single Firestore transaction
or batch that writes both sessionPath(...) and taskPath(...) together, or add a
rollback path that restores the session if the task update does not succeed.
In `@cloud-agent/src/services/sessionBridge.ts`:
- Around line 24-27: The deregistration logic in sessionBridge.deregister
currently removes the entire session entry, which causes wsBrowserAgentHandler’s
browser socket close path to tear down voiceWs and firestoreUnsub too. Split the
cleanup in sessionBridge so browser-side disconnects only clear the browser
socket state, and preserve the voice side plus watcher until the whole session
is actually finished; only fully delete the map entry when both sides are done.
In `@cloud-agent/src/tools/browserAction.ts`:
- Around line 84-99: The browser action flow in the main handler pauses billing
and charges a credit, but failures in createSession, writeTask, or wakeExtension
can exit without restoring state. Wrap the post-charge setup in a try/catch
around the browserAction path so that any rejection after spendCredit() triggers
deps.resumeBilling() and deps.creditService.refundCredit(txId) when txId exists,
while still preserving the existing out-of-credits early return behavior.
- Around line 120-133: The timeout path in waitForTerminalTask currently returns
executionTimeoutTask() without stopping the active Firestore listener or
wakeTimer started by watch/fs.watchTask. Update the browserAction.ts logic so
the Promise.race timeout cleanup unsubscribes the watch and clears the timer
before returning the timeout task, using the existing watch(), unsub, wakeTimer,
and settled flow to ensure no later enforceWakeTimeout() callback can fire after
EXECUTION_TIMEOUT.
---
Nitpick comments:
In `@cloud-agent/src/tools/browserAction.test.ts`:
- Around line 73-131: Add a regression test in browserAction.test.ts for the
voice flow where spendCredit() has already run but a later setup step fails,
covering createSession, writeTask, or wakeExtension throwing after the charge.
Update the browserActionTool execute path test setup to simulate one of those
post-charge failures and assert the failure is surfaced while the paused billing
state is cleaned up correctly. Use the browserActionTool and execute symbols to
locate the flow, and verify the refund/resumeBilling path is not leaked when the
exception happens after charging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 21d1d6b6-4e16-4b4e-b878-50e3f3770f5e
⛔ Files ignored due to path filters (4)
extension/icons/icon-128.pngis excluded by!**/*.pngextension/icons/icon-16.pngis excluded by!**/*.pngextension/icons/icon-48.pngis excluded by!**/*.pngextension/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (80)
.github/workflows/staging-test.ymlcloud-agent/src/agent.live.test.tscloud-agent/src/handlers/wsBrowserAgentHandler.test.tscloud-agent/src/handlers/wsBrowserAgentHandler.tscloud-agent/src/handlers/wsLiveAgentHandler.test.tscloud-agent/src/handlers/wsLiveAgentHandler.tscloud-agent/src/index.test.tscloud-agent/src/index.tscloud-agent/src/registerDevice.test.tscloud-agent/src/services/agentCore.tscloud-agent/src/services/fcmDispatcher.test.tscloud-agent/src/services/fcmDispatcher.tscloud-agent/src/services/firestoreSession.test.tscloud-agent/src/services/firestoreSession.tscloud-agent/src/services/instanceId.tscloud-agent/src/services/liveToolAdapter.tscloud-agent/src/services/sessionBridge.test.tscloud-agent/src/services/sessionBridge.tscloud-agent/src/tools/browserAction.test.tscloud-agent/src/tools/browserAction.tscloud-agent/src/tools/browserActionWiring.test.tsdocs/flowcharts/HOOKS.mddocs/flowcharts/MACHINES.mddocs/flowcharts/c4/containers.mddocs/flowcharts/c4/system-context.mddocs/superpowers/plans/2026-06-19-agent-tool-mixing-live-test-plan.mddocs/superpowers/plans/2026-06-19-web-search-tool-plan.mddocs/superpowers/plans/2026-06-20-cloud-document-conversion-plan.mddocs/superpowers/plans/2026-06-20-document-upload-progress-feedback-plan.mddocs/superpowers/plans/2026-06-20-document-upload-reliability-hardening.mddocs/superpowers/plans/2026-06-20-organization-schema.mddocs/superpowers/plans/2026-06-23-cloud-ontology-graph-traversal-plan.mddocs/superpowers/plans/2026-06-23-phase2-edge-loop-ontology-ui-design.mddocs/superpowers/plans/2026-06-25-websocket-agent-streaming.mddocs/superpowers/plans/2026-06-26-live-voice-backend.mddocs/superpowers/plans/2026-06-26-real-time-voice-chat.mddocs/superpowers/plans/2026-06-26-web-voice.mddocs/superpowers/plans/2026-06-27-native-live-voice-playback.mddocs/superpowers/plans/2026-06-29-mv3-bridge-phase1.mddocs/superpowers/specs/2026-06-29-mv3-browser-extension-bridge-design.mdextension/esbuild.mjsextension/manifest.jsonextension/package.jsonextension/src/background/auth-bridge.test.tsextension/src/background/auth-bridge.tsextension/src/background/content-bridge.test.tsextension/src/background/content-bridge.tsextension/src/background/service-worker.tsextension/src/background/task-dispatcher.test.tsextension/src/background/task-dispatcher.tsextension/src/background/ws-client.test.tsextension/src/background/ws-client.tsextension/src/content/dom-extractor.test.tsextension/src/content/dom-extractor.tsextension/src/content/executor.test.tsextension/src/content/executor.tsextension/src/content/safety-classifier.test.tsextension/src/content/safety-classifier.tsextension/src/env.tsextension/src/offscreen/auth.htmlextension/src/offscreen/auth.tsextension/src/shared/constants.tsextension/src/shared/dsl-types.tsextension/src/ui/popup/index.htmlextension/src/ui/popup/popup.tsextension/src/ui/side-panel/index.htmlextension/src/ui/side-panel/panel.tsextension/test/chrome-stub.tsextension/tsconfig.jsonfirebase.jsonfirestore.indexes.jsonfirestore.rulesjest.config.jsshared/constants.test.tsshared/constants.tsshared/dsl-schema.test.tsshared/dsl-schema.tsshared/dsl-types.tsshared/hostPolicy.test.tsshared/hostPolicy.ts
💤 Files with no reviewable changes (13)
- docs/superpowers/plans/2026-06-20-organization-schema.md
- docs/superpowers/plans/2026-06-20-document-upload-reliability-hardening.md
- docs/superpowers/plans/2026-06-26-web-voice.md
- docs/superpowers/plans/2026-06-23-cloud-ontology-graph-traversal-plan.md
- docs/superpowers/plans/2026-06-20-cloud-document-conversion-plan.md
- docs/superpowers/plans/2026-06-25-websocket-agent-streaming.md
- docs/superpowers/plans/2026-06-27-native-live-voice-playback.md
- docs/superpowers/plans/2026-06-19-web-search-tool-plan.md
- docs/superpowers/plans/2026-06-26-real-time-voice-chat.md
- docs/superpowers/plans/2026-06-20-document-upload-progress-feedback-plan.md
- docs/superpowers/plans/2026-06-23-phase2-edge-loop-ontology-ui-design.md
- docs/superpowers/plans/2026-06-19-agent-tool-mixing-live-test-plan.md
- docs/superpowers/plans/2026-06-26-live-voice-backend.md
Harden browser bridge auth/validation, billing cleanup, device upsert semantics, and session lifecycle handling from review threads. Co-authored-by: Cursor <cursoragent@cursor.com>
/fix-pr follow-upCommit: Review resolution
Verification
|
Description
Phase 1 of the MV3 browser extension bridge — enables read-only remote browser actions (extract, read_dom, summarize, scroll, open/focus tab) triggered from the cloud agent via Firestore + FCM + WebSocket relay.
Type of Change
Changes Made
Spec & Plan
docs/superpowers/specs/2026-06-29-mv3-browser-extension-bridge-design.md)docs/superpowers/plans/2026-06-29-mv3-bridge-phase1.md)Shared
shared/dsl-types.ts— canonical wire types (SingleAction, SequenceAction, TaskIntent, TaskResult, etc.)shared/dsl-schema.ts— Zod validation + helpers (actionTier, validateTaskIntent)shared/constants.ts— Layer-1 classifier (intentRequiresAuth,DESTRUCTIVE_ACTION_PATTERN)shared/hostPolicy.ts— cloud-side URL blocklist (rejectschrome://,file://, etc.)Cloud Agent
firestoreSession.ts— Firestore CRUD for sessions/tasks/devicesfcmDispatcher.ts— FCM v1 wake dispatchsessionBridge.ts— in-memory WS registrybrowserAction.ts—browser_actionFunctionTool (host blocklist → device lookup → billing → Firestore persist → FCM wake → durable timeout → result delivery)wsBrowserAgentHandler.ts— browser extension WebSocket endpoint (auth, task dispatch, result relay)register-deviceroute for extension device upsertfirebaseUid(Firestore paths) vsuserId(Postgres billing)Extension (
extension/)content-bridge.ts— host permission gate withpendingHost/pendingOriginfor grant UXpanel.ts— reactive Grant Access button, pause toggle, action logesbuild.mjs— build-time env injection from repo.envInfra
firestore.rules— security rules forusers/{uid}/sessionsanddevicesfirestore.indexes.json— composite indexes for session/task queriesfirebase.json— Firestore targetTesting
Test Results (all green)
Manual E2E (Task 27 — not yet executed)
Requires real Firebase project with Firestore Native mode, FCM configured, and extension loaded unpacked. Tracked as follow-up.
Platforms Tested
Checklist
Additional Notes
Made with Cursor