Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tidy-shrimps-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@opencode-ai/browser-control": patch
---

Pin production WebSocket access to the assigned Chrome Web Store extension ID
while retaining an explicit source-development path for unpacked extensions.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ browser-control skill

- Load `extension/dist` as the unpacked extension.
- The relay listens on `127.0.0.1:19989` by default.
- Current shim version is `0.0.22` and extension protocol version is `1`.
- Current shim version is `0.0.23` and extension protocol version is `1`.
- Store and npm versions may differ when protocol `1` remains compatible.
- On socket open the shim sends `hello` and then re-announces every tab it still
has `chrome.debugger` attached to (`debugger.attached` events), so a restarted
Expand Down
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ require a new extension capture protocol and permission model.
link`.
- Until the first Store review completes, the browser extension is loaded
unpacked from the npm package's `extension/dist` directory or a source build.
Its current shim version is `0.0.22`.
Its current shim version is `0.0.23`.
- Extension and npm releases are independently versioned. The extension hello
reports an explicit protocol version, and compatibility rather than exact
package-version equality determines whether the local driver may use it.
Expand Down
10 changes: 4 additions & 6 deletions docs/CHROME_WEB_STORE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,9 @@ requires a separate explicit CLI request.
Store assets live under `docs/chrome-web-store/`:

- `icon-128.png`
- `browser-control-1280x800.jpg`
- `small-promo-440x280.png`

A 1280x800 or 640x400 product screenshot is still required before submission;
capture it from the final Store-ID-pinned build rather than staging a mock.

Run:

```bash
Expand All @@ -111,6 +109,6 @@ pnpm package:extension
Upload `artifacts/browser-control-extension-<version>.zip`. Record the printed
SHA-256 digest with the release notes.

Before submission, add the Store-assigned extension ID to the relay's accepted
production extension origins while preserving an explicit source-development
path.
The production relay accepts Store extension ID
`gmjpoplfomnnjipeiojccjbpjlodkjhn`. Source-mode relays additionally accept
unpacked development extension origins.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Browser Control",
"version": "0.0.22",
"version": "0.0.23",
"description": "Connect browser tabs to the local Browser Control driver.",
"minimum_chrome_version": "120",
"permissions": ["activeTab", "alarms", "debugger", "offscreen", "tabCapture", "tabGroups"],
Expand Down
9 changes: 7 additions & 2 deletions src/relay-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RelayErrorCode } from "./relay-schema.ts"

export const defaultHost = "127.0.0.1"
export const defaultPort = 19989
export const chromeWebStoreExtensionOrigin = "chrome-extension://gmjpoplfomnnjipeiojccjbpjlodkjhn"

const maxCliBodyBytes = 1_000_000

Expand Down Expand Up @@ -54,16 +55,20 @@ export function validateBrowserFetchSite(request: http.IncomingMessage): string

export function validateWebSocketOrigin(options: {
readonly origin: string | undefined
readonly allowAnyChromeExtension?: boolean
readonly requireChromeExtension?: boolean
}): string | undefined {
if (!options.origin) {
return options.requireChromeExtension ? "Extension WebSocket origin is required" : undefined
}
if (options.origin.startsWith("chrome-extension://")) {
if (options.origin === chromeWebStoreExtensionOrigin) {
return undefined
}
if (options.allowAnyChromeExtension && options.origin.startsWith("chrome-extension://")) {
return undefined
}
if (options.requireChromeExtension) {
return "Extension WebSocket origin must be chrome-extension://"
return "Extension WebSocket origin is not allowed"
}
return "WebSocket origin is not allowed"
}
Expand Down
6 changes: 4 additions & 2 deletions src/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { RecordingRelay } from "./recording-relay.ts"
import { appendManagedRelayProcessLog } from "./relay-log.ts"
import { boundedToken, runtimeFailureKind, summarizeDiagnosticUrl, summarizeRuntimeEvaluate } from "./runtime-diagnostics.ts"
import { resolveTargetInfoTarget, shouldExposeChildTarget, TargetRegistry, type RootTargetChange, type TargetOwnershipChange } from "./target-registry.ts"
import { browserControlVersion } from "./version.ts"

export type { RelayServer } from "./relay-types.ts"

Expand Down Expand Up @@ -147,6 +148,7 @@ const makeRelay = Effect.fnUntraced(function* (options: {
const releaseTargetGraceMs = Math.max(0, options.releaseTargetGraceMs ?? 10_000)
const browserId = crypto.randomUUID()
const endpointUrl = `http://${formatHostForUrl(host)}:${port}`
const allowAnyChromeExtension = browserControlVersion === "0.0.0-dev"
const sessionCatalog = options.sessionCatalogPath === null
? undefined
: new SessionCatalog(options.sessionCatalogPath ?? defaultSessionCatalogPath(port))
Expand Down Expand Up @@ -602,7 +604,7 @@ const makeRelay = Effect.fnUntraced(function* (options: {
const requestUrl = new URL(request.url ?? "/", endpointUrl)
const origin = Array.isArray(request.headers.origin) ? request.headers.origin[0] : request.headers.origin
if (requestUrl.pathname === "/extension") {
const originError = validateWebSocketOrigin({ origin, requireChromeExtension: true })
const originError = validateWebSocketOrigin({ origin, requireChromeExtension: true, allowAnyChromeExtension })
if (originError) {
sendUpgradeError({ socket, status: 403, message: originError })
return
Expand All @@ -613,7 +615,7 @@ const makeRelay = Effect.fnUntraced(function* (options: {
return
}
if (requestUrl.pathname.startsWith("/devtools/browser/")) {
const originError = validateWebSocketOrigin({ origin })
const originError = validateWebSocketOrigin({ origin, allowAnyChromeExtension })
if (originError) {
sendUpgradeError({ socket, status: 403, message: originError })
return
Expand Down
11 changes: 9 additions & 2 deletions test/relay-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"
import {
chromeWebStoreExtensionOrigin,
generateSessionId,
getTargetInfo,
isRestrictedTarget,
Expand Down Expand Up @@ -48,15 +49,21 @@ describe("validateBrowserFetchSite", () => {
})

describe("validateWebSocketOrigin", () => {
it("accepts extension origins and missing origins for non-extension clients", () => {
expect(validateWebSocketOrigin({ origin: "chrome-extension://abc" })).toBeUndefined()
it("accepts the Store extension and missing origins for non-extension clients", () => {
expect(validateWebSocketOrigin({ origin: chromeWebStoreExtensionOrigin })).toBeUndefined()
expect(validateWebSocketOrigin({ origin: undefined })).toBeUndefined()
})

it("accepts unpacked extension origins only in source development", () => {
expect(validateWebSocketOrigin({ origin: "chrome-extension://unpacked", allowAnyChromeExtension: true })).toBeUndefined()
expect(validateWebSocketOrigin({ origin: "chrome-extension://unpacked", requireChromeExtension: true })).toBeDefined()
})

it("rejects web origins for the extension endpoint", () => {
expect(validateWebSocketOrigin({ origin: undefined, requireChromeExtension: true })).toBeDefined()
expect(validateWebSocketOrigin({ origin: "https://example.com", requireChromeExtension: true })).toBeDefined()
expect(validateWebSocketOrigin({ origin: "https://example.com" })).toBeDefined()
expect(validateWebSocketOrigin({ origin: "chrome-extension://different-extension" })).toBeDefined()
})
})

Expand Down