[STG-1940] feat: add browser-swarm extension bridge POC#100
Conversation
d8c3cb7 to
86a3aa3
Compare
| waitingForDebugger: false | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Synthetic event references wrong target for attachToTarget
Low Severity
In emitSyntheticEvents, the Target.attachToTarget path looks up the target via this.findTarget(params.targetId, client), but handleCdpCommand resolves the target via sessionTarget || this.findTarget(null, client) when params.targetId is absent. For a root client with multiple targets, sessionTarget (from the provided sessionId) may differ from the first target returned by findTarget(null, ...). The synthetic Target.attachedToTarget event would then carry targetInfo for the wrong target compared to the sessionId in the response.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit edad4bb. Configure here.
| }, 30000); | ||
| this.extensionRequests.set(id, { resolve, reject, timer }); | ||
| this.extension.send(JSON.stringify(payload)); | ||
| }); |
There was a problem hiding this comment.
Relay timeout too short for multi-tab ensureTabs
Medium Severity
sendToExtension uses a fixed 30-second timeout for all extension requests. The extension's ensureTabs calls waitForTabLoad with a 15-second timeout per tab sequentially, plus attachTab overhead. Creating 3+ tabs pointing to slow-loading URLs can exceed 30 seconds total, causing the relay to time out and return an error to the caller even though the extension is still successfully creating tabs. This leaves relay and extension state out of sync.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit edad4bb. Configure here.
| params, | ||
| targetId: firstTarget?.targetId | ||
| }).catch(() => {}); | ||
| return {}; |
There was a problem hiding this comment.
AutoAttach silently lost when no targets exist yet
Low Severity
When Target.setAutoAttach is sent before any targets exist, the relay forwards it to the extension with targetId: undefined. The extension's forwardCDPCommand calls findTarget, which returns null and throws — but the relay swallows this with .catch(() => {}) and returns {} to the client. The extension's autoAttachParams is never set, so subsequently created tabs skip auto-attach setup in attachTab, even though the client believes auto-attach is active.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit edad4bb. Configure here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 5 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 33f4f64. Configure here.
| return { | ||
| ...result, | ||
| targets: this.enrichedTargets() | ||
| }; |
There was a problem hiding this comment.
Ensure returns stale swarm targets
Medium Severity
Calling ensure with a smaller --count (or re-running ensure on an already-populated swarm) only merges the newly attached targets into relay state and never removes older ones. The handler then returns this.enrichedTargets(), which lists every entry still in the relay map, so the JSON can include extra wsUrl values and /health targetCount can stay above the requested count.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 33f4f64. Configure here.
| const existing = targetsByTab.get(tabId); | ||
| if (existing?.state === "connected") { | ||
| return existing; | ||
| } |
There was a problem hiding this comment.
Re-ensure skips label updates
Low Severity
When ensureTabs reuses a tab that is already debugger-attached, attachTab returns the existing target immediately and ignores the new label passed from ensure. A second swarm in the same profile can therefore report outdated worker labels in ensure output and relay metadata even though new --label values were supplied.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 33f4f64. Configure here.


Summary
browser-swarmskill with a Chrome extension bridge and localhost relaybrowse --session+--cdpcontracts and structured worker reportschrome.tabGroups.*orchrome.tabs.groupInput.*CDP commands/healthso stale Arc extension reloads are visibleLinear: https://linear.app/browserbase/issue/STG-1940/add-browser-swarm-extension-bridge-poc
E2E Test Matrix
node --checkfor browser-swarm relay, e2e, setup, launcher, and extension service workerextension/manifest.jsonparsed as JSONBROWSER_SWARM_PORT=19997 BROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2efromskills/browser-swarm0.1.1, three labeled tabs created in a Chrome tab group, each worker session read title/url,tab listreturned exactly one tab, snapshots and screenshots were captured, and relay CLIscreenshot --pathwrote a1509844byte PNG--session/--cdpbrowse workflow works end to end in a disposable browser, even while Arc has an installed browser-swarm extension on the default port. Also proves the relay screenshot CLI honors--path.BROWSER_SWARM_PORT=20000 BROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2efromskills/browser-swarm0.1.1; session-scopedTarget.getTargetssaw exactly one worker target; session-scoped sibling attach/info errored; rootTarget.createTargetincreased target count3 -> 4; rootTarget.closeTargetreduced it4 -> 3; relay screenshot wrote a1510778byte PNG; three same-page workers submittedalpha-worker/beta-worker/gamma-workerin parallelBROWSER_SWARM_PORT=20005 BROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2efromskills/browser-swarm0.1.1; rootTarget.createTargetwith a rootsessionIdincreased target count3 -> 4; rootTarget.closeTargetwith the samesessionIdreduced it4 -> 3; the root CDP client observed exactly oneTarget.detachedFromTargetevent for the closed target; relay screenshot wrote a1510792byte PNG; same-page workers again submittedalpha-worker/beta-worker/gamma-workerin parallelsessionIdstill go through relay bookkeeping instead of raw forwarding.BROWSER_SWARM_PORT=20007 BROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2efromskills/browser-swarm0.1.1; worker endpoint isolation and session-scoped sibling/lifecycle rejection passed; rootTarget.createTarget/Target.closeTargetwith a rootsessionIdmoved target count3 -> 4 -> 3; exactly one detach event was observed; relay screenshot wrote a1509933byte PNG; same-page workers filled and clicked submit foralpha-worker/beta-worker/gamma-workerin paralleld455b35only update browser-swarm docs.BROWSER_SWARM_PORT=20018 BROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2efromskills/browser-swarm112ecf8with extension idbljijffbkipealmmglkacdmfjjpdjphm, extension version0.1.1, and service workerservice-worker-v0-1-1.js; worker endpoint isolation and session-scoped sibling/lifecycle rejection passed; rootTarget.createTarget/Target.closeTargetwith a rootsessionIdmoved target count3 -> 4 -> 3; exactly one detach event was observed; relay screenshot wrote a1515911byte PNG; same-page workers filled and clicked submit foralpha-worker/beta-worker/gamma-workerin parallel, and each finaltab listexposed exactly one targetnpm run e2ebrowse fillwrotealpha-worker/beta-worker/gamma-worker, parallelclick #submitsubmitted each form, and final title/text/value/tab-list assertions all passednode scripts/setup-real-browser.mjs --browser arc --no-open --no-start-relay --timeout 2 --json,node scripts/setup-real-browser.mjs --browser chrome --no-open --no-start-relay --port 19995 --timeout 10 --json, andnode scripts/setup-real-browser.mjs --browser not-a-browser --no-open --no-start-relay --no-wait3with connected version0.1.0, expected manifest version0.1.1, andversionMatches: false; disposable Chrome exited0with connected version0.1.1andversionMatches: true; unknown browser exited1with the supported browser listextensionConnected: trueas sufficient when a stale MV3 service worker is active, and validates browser names before relay/browser side effects. It detects the current Arc blocker and passes on a fresh Chrome happy path.npm run e2eTarget.attachToTargetandTarget.getTargetInforeturned errors; workerTarget.createTargetandTarget.closeTargetreturned explicit browser-swarm harness errors; the same lifecycle commands with an attachedsessionIdalso returned errors; an unknownsessionIdfailed closed instead of falling back to the worker targetworkeragents against three target-bound Chrome endpoints; each worker reported one visible target, operated only on its own assigned page, and captured a screenshot (/tmp/browser-swarm-subagent-alpha.png,/tmp/browser-swarm-subagent-beta.png,/tmp/browser-swarm-subagent-gamma.png)112ecf8/ recorded inb172bf8: spawned three real Codexworkeragents (Franklin,Raman,Mencius) against three target-bound endpoints in one disposable Chrome profile on identicalhttp://127.0.0.1:18105/sametabs; workers wroteproper-codex-alpha/proper-codex-beta/proper-codex-gamma, returned structured JSON, and the main harness independently verified each endpoint exposed exactly one target with its own title/text/value stateclaude -p --permission-mode bypassPermissions --allowedTools Bash --output-format jsonagent ran concurrently in the same disposable Chrome profile on identical same-page tabs; each reported distinct title/text/value/url/tabCount/targetId/screenshot; the main harness independently verified one visible target per endpointa3fe8ad: one real Codexworkersubagent and oneclaude -p --permission-mode bypassPermissions --allowedTools Bash --output-format jsonworker ran concurrently in the same disposable Chrome profile on identicalhttp://127.0.0.1:18087/sametabs; Codex reportedlatest-codex-worker, Claude reportedlatest-claude-worker, and the main harness independently verified each target-bound endpoint saw exactly one tab with its own title/text/value state87267b6: one real Codexworkersubagent and oneclaude -p --permission-mode bypassPermissions --allowedTools Bash --output-format jsonworker ran concurrently in a disposable Chrome profile on identical same-page tabs. Codex reportedcurrent-head-codex-worker-87267b6; Claude reportedcurrent-head-claude-worker-87267b6; the main harness independently verified each target-bound endpoint exposed exactly one target with distinct title/text/value state. Follow-up strict browse-only probing showed long session names can block browse daemon startup, while short sessionbs-claude-9039passedget title,fill,click,tab list,get text,get value, andscreenshot --pathwith one visible tab.d455b35: one real Codexworkersubagent and oneclaude -p --permission-mode bypassPermissions --allowedTools Bash --output-format jsonworker ran concurrently in a disposable Chrome profile on identicalhttp://127.0.0.1:18090/sametabs. Codex reportedcurrent-head-codex-d455b35; Claude reportedcurrent-head-claude-d455b35; screenshots were written to/tmp/browser-swarm-current-codex-d455b35.pngand/tmp/browser-swarm-current-claude-d455b35.png; the main harness independently verified each target-bound endpoint exposed exactly one target with distinct title/text/value state.d455b35only update browser-swarm docs.aba8036: one real Codexworkersubagent and oneclaude -p --permission-mode bypassPermissions --allowedTools Bash --output-format jsonworker ran concurrently in a disposable Chrome profile with extension version0.1.1on identicalhttp://127.0.0.1:55459/sametabs. Codex reportedcodex-latest-a726f6c; Claude reportedclaude-latest-a726f6c; screenshots were written to/tmp/browser-swarm-mixed-codex-a726f6c.png(29743bytes) and/tmp/browser-swarm-mixed-claude-a726f6c.png(29104bytes); the main harness independently verified each target-bound endpoint exposed exactly one tab with distinct title/text/value state.arc-alpha,arc-beta, andarc-gamma0.1.0service worker.Target.createTargetcreated a fourth target, relay navigation worked on that target, target-bound endpoint for the created tab saw only itself, worker endpoints blockedTarget.createTarget/Target.closeTarget, and rootTarget.closeTargetcleaned the target back to the original threehttp://127.0.0.1:18080/sameand identical titlesame-page-action-test; issued parallel actions; final DOM/title states were distinct (alpha-worker,beta-worker,gamma-worker) and each workertab liststill returned exactly one targetfill --press-enter/parallel input flake that led to theInput.*queue fix.chrome.tabGroups.query/chrome.tabs.group; Arc 1.146.0 crashed shortly after--no-groupand documents Arc mode as no visual grouping, with isolation still provided by target-bound endpoints./healthreturnedextensionConnected: true;ensure --no-group --count 1returnedgroupId: null,groupDisabled: true, and one target;browse get title/url/tab list --session <arc-smoke> --cdp <target-ws>returnedExample Domain,https://example.com/, and exactly one visible target; relay emitted no extension warnings; Arc process stayed runningbrowse --session+--cdpendpoint loaded public Google result pages, extracted titles/snapshots, and captured screenshots;tab listreturned exactly one visible target per worker after session initialization; raw CDPTarget.getTargetsreturned only the assigned target for each endpoint, and siblingTarget.attachToTargetfailed; relay emitted no extension warnings; Arc stayed runningBROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2e:arc-serialized-clickfromskills/browser-swarm0.1.0while the unpacked manifest expected0.1.1, created two no-group target-bound tabs on the same local URL, filledarc-serialized-alpha/arc-serialized-betain parallel, clicked#submitsequentially through the top-level harness, verified each tab title/result/input matched its assigned value, verified each endpoint reported exactly one tab, then closed its tabs and daemons./healthreports version0.1.1, then rerun parallel pointer-click submission against the latest service worker input queue.npm run diagnose:arc-worker -- --jsonfromskills/browser-swarm3withstatus: "STALE_ARC_SERVICE_WORKER_REGISTRATION", connected version0.1.0, expected version0.1.1; Arc Secure Preferences points at the current unpacked extension path but still records service-worker registration version0.1.0; exact old worker URL hits18, exact expected worker URL hits0/healthreports extension version0.1.1.BROWSER_SWARM_BROWSE_BIN=<local oclif browse> npm run e2e:arc-parallel-clickfromskills/browser-swarm3withstatus: "BLOCKED_STALE_EXTENSION", expected extension version0.1.1, connected Arc worker version0.1.0, andtargetCount: 0./healthreports extension version0.1.1.Artifacts from the real e2e runs:
<temp>/browser-swarm-e2e/report.json<temp>/browser-swarm-e2e/flights.png<temp>/browser-swarm-e2e/rentals.png<temp>/browser-swarm-e2e/dinner.png/tmp/browser-swarm-subagent-alpha.png/tmp/browser-swarm-subagent-beta.png/tmp/browser-swarm-subagent-gamma.png/tmp/browser-swarm-mixed-codex.png/tmp/browser-swarm-mixed-claude.png<temp>/browser-swarm-realworld-flights.png<temp>/browser-swarm-realworld-rentals.png<temp>/browser-swarm-realworld-dinner.pngNote
Medium Risk
New MV3 extension uses debugger and broad host permissions to control the user's real browser profile over localhost; isolation is enforced in relay code rather than the browser sandbox, so regressions could leak cross-tab CDP access.
Overview
Adds a new
browser-swarmskill and marketplace plugin so multiple agents can share one real Chromium profile while each worker only sees its assigned tab via target-boundbrowse --session+--cdpendpoints.The stack is a localhost relay (
swarm-relay.mjs), an MV3 extension that manages tabs (optional Chrome tab groups;--no-groupfor Arc), forwards CDP, and serializesInput.*commands, plus setup/launch scripts and extensive SKILL.md worker contracts. The relay scopes worker CDP (single target, blocks sibling attach and worker tab create/close) while the harness keeps root lifecycle on the unscoped browser endpoint.Also ships e2e flows (disposable Chrome + Arc serialized/parallel click harnesses), Arc stale MV3 worker diagnostics, and
/healthextension version metadata for reload checks.Reviewed by Cursor Bugbot for commit 33f4f64. Bugbot is set up for automated code reviews on this repo. Configure here.