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
32 changes: 26 additions & 6 deletions .github/workflows/chrome-web-store.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ on:
required: true
type: string
action:
description: "status reads the item state; upload replaces the draft package; submit uploads and submits for review"
description: "status reads the item state; upload replaces the draft package; submit uploads and submits for review; cancel cancels the pending review"
required: true
default: status
type: choice
options:
- status
- upload
- submit
- cancel

run-name: Chrome Web Store ${{ inputs.action }} ${{ inputs.ref }}

Expand All @@ -35,20 +36,20 @@ jobs:
ref: ${{ inputs.ref }}

- uses: pnpm/action-setup@v4
if: ${{ inputs.action != 'status' }}
if: ${{ inputs.action == 'upload' || inputs.action == 'submit' }}
with:
version: 10.33.2

- uses: actions/setup-node@v4
if: ${{ inputs.action != 'status' }}
if: ${{ inputs.action == 'upload' || inputs.action == 'submit' }}
with:
node-version: 22
cache: pnpm

- if: ${{ inputs.action != 'status' }}
- if: ${{ inputs.action == 'upload' || inputs.action == 'submit' }}
run: pnpm install --frozen-lockfile

- if: ${{ inputs.action != 'status' }}
- if: ${{ inputs.action == 'upload' || inputs.action == 'submit' }}
run: pnpm run check

- name: Get Chrome Web Store access token
Expand Down Expand Up @@ -78,7 +79,7 @@ jobs:
fi

- name: Upload extension package
if: ${{ inputs.action != 'status' }}
if: ${{ inputs.action == 'upload' || inputs.action == 'submit' }}
shell: bash
env:
ACCESS_TOKEN: ${{ steps.auth.outputs.access_token }}
Expand All @@ -104,10 +105,29 @@ jobs:
set -euo pipefail
response="$(curl -sS -w '\n%{http_code}' -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Length: 0" \
"https://chromewebstore.googleapis.com/v2/publishers/${CHROME_PUBLISHER_ID}/items/${EXTENSION_ID}:publish")"
status="$(printf '%s' "$response" | tail -n 1)"
body="$(printf '%s' "$response" | sed '$d')"
printf 'Chrome Web Store publish returned HTTP %s:\n%s\n' "$status" "$body"
if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then
exit 1
fi

- name: Cancel pending review
if: ${{ inputs.action == 'cancel' }}
shell: bash
env:
ACCESS_TOKEN: ${{ steps.auth.outputs.access_token }}
run: |
set -euo pipefail
response="$(curl -sS -w '\n%{http_code}' -X POST \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Length: 0" \
"https://chromewebstore.googleapis.com/v2/publishers/${CHROME_PUBLISHER_ID}/items/${EXTENSION_ID}:cancelSubmission")"
status="$(printf '%s' "$response" | tail -n 1)"
body="$(printf '%s' "$response" | sed '$d')"
printf 'Chrome Web Store cancelSubmission returned HTTP %s:\n%s\n' "$status" "$body"
if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then
exit 1
fi
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm run check
env:
OPZERO_CHROME_EXTENSION_ID: ${{ vars.OPZERO_CHROME_EXTENSION_ID }}
BROWSER_CONTROL_EXTENSION_ID: ${{ vars.BROWSER_CONTROL_EXTENSION_ID }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/release/browser-control-extension.zip
dist/release/chrome-control-skill.zip
dist/release/browser-control-skill.zip
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Chrome derives an unpacked extension's ID from its folder path, so every checkou
2. Install the native host. It needs Node.js 18 or later on macOS or Linux:

```sh
mkdir -p ~/.config/opencode/skills/chrome-control
curl -fsSL https://github.com/opzero1/browser-control/releases/latest/download/chrome-control-skill.zip -o /tmp/chrome-control-skill.zip
unzip -o /tmp/chrome-control-skill.zip -d ~/.config/opencode/skills/chrome-control
cd ~/.config/opencode/skills/chrome-control
mkdir -p ~/.config/opencode/skills/browser-control
curl -fsSL https://github.com/opzero1/browser-control/releases/latest/download/browser-control-skill.zip -o /tmp/browser-control-skill.zip
unzip -o /tmp/browser-control-skill.zip -d ~/.config/opencode/skills/browser-control
cd ~/.config/opencode/skills/browser-control
node scripts/install-native-host.js --extension-id dcnjjnecbhipdbngkhjppkckpkellmld
```

Expand All @@ -37,7 +37,7 @@ The popup also has **Pause host**, which disconnects the host and ends every age
To install the agent skill with one command instead:

```sh
curl -fsSL https://raw.githubusercontent.com/opzero1/browser-control/main/scripts/install-chrome-control-skill.sh | sh
curl -fsSL https://raw.githubusercontent.com/opzero1/browser-control/main/scripts/install-browser-control-skill.sh | sh
```

## Verify
Expand All @@ -58,7 +58,7 @@ pnpm install
pnpm run check
```

`pnpm run check` builds the extension, native host and installable skill, then runs type checks, tests and the project checks. The headless-browser tests for private input run only when `OPZERO_SYNTHETIC_CHROME` names a Chrome for Testing binary.
`pnpm run check` builds the extension, native host and installable skill, then runs type checks, tests and the project checks. The headless-browser tests for private input run only when `BROWSER_CONTROL_SYNTHETIC_CHROME` names a Chrome for Testing binary.

## Release

Expand Down
16 changes: 14 additions & 2 deletions docs/DEVELOPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ Open the extension popup and click **Reload host** after you install the native

The installer writes `com.opzero.chrome.json` into Chrome's per-user `NativeMessagingHosts` directory, and it saves the extension ID to `dist/scripts/extension-id.json` so follow-up checks can run without `--extension-id`. Pass `--socket-path <path>` to give the host a private socket other than `~/.opzero-chrome/default.sock`.

The host and client read these variables. They replaced the `OPZERO_CHROME_*` names, which are no longer read:

| Variable | Use |
| --- | --- |
| `BROWSER_CONTROL_HOST_SOCKET` | Unix socket path. Default `~/.opzero-chrome/default.sock`. The installer's `--socket-path` writes it into the wrapper. |
| `BROWSER_CONTROL_HOST_TRANSPORT` | `tcp` selects the loopback TCP transport, which Windows always uses. |
| `BROWSER_CONTROL_HOST_PORT` | TCP port. Default `17365`. |
| `BROWSER_CONTROL_HOST_TOKEN_FILE` | File that holds the TCP connection token. |
| `BROWSER_CONTROL_REQUEST_TIMEOUT_MS` | Host request timeout. Default `30000`. |
| `BROWSER_CONTROL_EXTENSION_ID` | Extension ID for the installer and checks, in place of `--extension-id`. |
| `BROWSER_CONTROL_USER_DATA_DIR`, `BROWSER_CONTROL_PREFERENCES_PATH` | Chrome profile that `check-extension` inspects. |

For a disposable Chrome for Testing profile, write the host manifest to `<user-data-dir>/NativeMessagingHosts/` instead, so the default Chrome profile keeps its own host. `store/capture/launch.sh` shows the full sequence.

## Host controls
Expand All @@ -50,8 +62,8 @@ pnpm run client -- ping
pnpm run client -- getInfo
```

To also run the headless-browser private-input tests, point `OPZERO_SYNTHETIC_CHROME` at a Chrome for Testing binary:
To also run the headless-browser private-input tests, point `BROWSER_CONTROL_SYNTHETIC_CHROME` at a Chrome for Testing binary:

```sh
OPZERO_SYNTHETIC_CHROME="/path/to/Google Chrome for Testing" pnpm exec vitest run tests/security/private-input.browser.test.ts
BROWSER_CONTROL_SYNTHETIC_CHROME="/path/to/Google Chrome for Testing" pnpm exec vitest run tests/security/private-input.browser.test.ts
```
33 changes: 28 additions & 5 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ A release has four parts: the extension package on the Chrome Web Store, the Git
| Privacy policy | <https://browser-control.pages.dev/privacy/> |
| Store API service account | `cws-publisher@opzero-chrome.iam.gserviceaccount.com` (Google Cloud project `opzero-chrome`) |

Release builds embed the store extension ID from `scripts/extension-id.store.json` in the packaged skill, so the native host installer allows the store extension by default. Set `OPZERO_CHROME_EXTENSION_ID` only to build a package for a different extension ID.
Release builds embed the store extension ID from `scripts/extension-id.store.json` in the packaged skill, so the native host installer allows the store extension by default. Set `BROWSER_CONTROL_EXTENSION_ID` only to build a package for a different extension ID. The `Release` workflow reads it from the repository variable of the same name, and falls back to the store ID when the variable is unset.

The store package must not contain a manifest `key`. `pnpm run check` refuses one, and it also pins the exact permission list. Unpacked builds get their ID from their folder path.

## 1. Prepare the version

1. Raise `version` in `src/extension/manifest.json` and `package.json`, and the host version in `src/native-host/host.ts`. The version must be higher than every version the store has, including rejected drafts. Check the dashboard's **Package** page or run the workflow with `action: status`.
2. If a permission, data flow or stored key changes, update `store/listing.md`, `site/privacy/index.html` and `docs/PRIVACY.md` together. The dashboard's Privacy tab must match the privacy policy.
3. Run `pnpm run check`. It rebuilds `dist/` and the committed `skills/chrome-control` files. Commit the regenerated files.
3. Run `pnpm run check`. It rebuilds `dist/` and the committed `skills/browser-control` files. Commit the regenerated files.

## 2. Merge and tag

Expand All @@ -31,7 +31,7 @@ git tag vX.Y.Z
git push origin vX.Y.Z
```

The `Release` workflow creates the GitHub Release with `browser-control-extension.zip` and `chrome-control-skill.zip`. The reviewer instructions and the README download `chrome-control-skill.zip` from the latest release, so publish the release before you submit to the store.
The `Release` workflow creates the GitHub Release with `browser-control-extension.zip` and `browser-control-skill.zip`. The README downloads `browser-control-skill.zip` from the latest release, so publish the release before you submit to the store.

## 3. Deploy the website

Expand All @@ -40,11 +40,33 @@ Deploy after every release, and whenever anything under `site/` changes. The sit
```sh
pnpm run build
mkdir -p site/download
cp dist/release/chrome-control-skill.zip site/download/
cp dist/release/browser-control-skill.zip site/download/browser-control-skill.zip
cp dist/release/browser-control-skill.zip site/download/chrome-control-skill.zip
npx wrangler pages deploy site --project-name browser-control --branch main
```

Check that `/`, `/privacy/`, `/support/`, `/support/reviewers/` and `/download/chrome-control-skill.zip` return HTTP 200. Keep the `google-site-verification` meta tag in `site/index.html`. It proves ownership of the site in Google Search Console, which the listing's official URL requires. Cloudflare Pages redirects `.html` URLs to extensionless ones, so the HTML-file verification method does not work on this site.
`/download/chrome-control-skill.zip` is an alias that serves the same file as `/download/browser-control-skill.zip`. It exists for the dashboard's "Additional instructions for reviewers" field. That field links to the old path, and the Chrome Web Store API cannot change it. After the field is updated in the dashboard to link to `/download/browser-control-skill.zip`, the alias can go: update the short form in `store/reviewer-test-instructions.md` and `store/listing.md`, delete `site/download/chrome-control-skill.zip`, remove the second `cp` line, and deploy again.

Check that `/`, `/privacy/`, `/support/`, `/support/reviewers/`, `/download/browser-control-skill.zip` and `/download/chrome-control-skill.zip` return HTTP 200, and that both zips match the build:

```sh
shasum -a 256 dist/release/browser-control-skill.zip
curl -fsSL https://browser-control.pages.dev/download/browser-control-skill.zip | shasum -a 256
curl -fsSL https://browser-control.pages.dev/download/chrome-control-skill.zip | shasum -a 256
```

Keep the `google-site-verification` meta tag in `site/index.html`. It proves ownership of the site in Google Search Console, which the listing's official URL requires. Cloudflare Pages redirects `.html` URLs to extensionless ones, so the HTML-file verification method does not work on this site.

### Renamed helper

0.2.2 is the first release after the rename. Its extension code is the same as in 0.2.1 apart from the version. The skill is `browser-control` and its zip is `browser-control-skill.zip`. The host, client and scripts read `BROWSER_CONTROL_*` variables and no longer read the `OPZERO_CHROME_*` names. The installer writes a `browser-control-host` wrapper. The native host name `com.opzero.chrome` and the default socket `~/.opzero-chrome/default.sock` are unchanged.

What changed with it:

- 0.2.2 replaces the pending 0.2.1 submission, which still describes the old helper. Cancel that review with `action: cancel` before you upload 0.2.2.
- `store/listing.md`, `store/reviewer-test-instructions.md` and the pages under `site/` describe the 0.2.2 helper: the `/download/browser-control-skill.zip` link, the `browser-control-host` wrapper, the `BROWSER_CONTROL_HOST_SOCKET` excerpt and `"version":"0.2.2"` from `getInfo`. Only the dashboard short form still links to `/download/chrome-control-skill.zip`, through the alias above.
- Create the repository variable `BROWSER_CONTROL_EXTENSION_ID` if the old `OPZERO_CHROME_EXTENSION_ID` variable was set. The `Release` workflow no longer reads the old name.
- Existing installs keep working until they are reinstalled. A reinstall from the new zip goes to `~/.config/opencode/skills/browser-control`, so remove the old `skills/chrome-control` folder.

## 4. Upload to the Chrome Web Store

Expand All @@ -59,6 +81,7 @@ gh workflow run chrome-web-store.yml -f ref=vX.Y.Z -f action=upload
| `status` | Reads the published and submitted state of the item. Changes nothing. |
| `upload` | Builds `ref`, runs the checks, and replaces the draft package. |
| `submit` | Same as `upload`, then submits the draft for review. |
| `cancel` | Cancels the pending review submission. It does not build `ref` or change the package. The status in the same run is read before the cancel, so run `status` again to confirm. |

The workflow signs in to Google through Workload Identity Federation. GitHub's OIDC token is exchanged for a short-lived access token for the `cws-publisher` service account, so the repository holds no Google credential. The trust is limited to workflows in `opzero1/browser-control`.

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opzero-chrome",
"version": "0.2.1",
"name": "@op1/browser-control",
"version": "0.2.2",
"private": true,
"description": "Browser Control extension with native messaging and CDP bridge.",
"packageManager": "pnpm@10.33.2",
Expand Down
28 changes: 16 additions & 12 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ for (const [index, entry] of ["background", "content-scripts/opzero-chrome", "po
stdio: "inherit",
env: {
...process.env,
OPZERO_EXTENSION_ENTRY: entry,
OPZERO_EXTENSION_EMPTY: index === 0 ? "1" : "0"
BROWSER_CONTROL_EXTENSION_ENTRY: entry,
BROWSER_CONTROL_EXTENSION_EMPTY: index === 0 ? "1" : "0"
}
});
}
Expand All @@ -61,7 +61,7 @@ copyFile("src/extension/popup.css", "dist/extension/popup.css");
copyDir("src/extension/images", "dist/extension/images");

run("pnpm", ["exec", "vite", "build", "--config", "vite.node.config.ts"]);
writeExecutable("dist/native-host/opzero-chrome-host", `#!/usr/bin/env sh
writeExecutable("dist/native-host/browser-control-host", `#!/usr/bin/env sh
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
if command -v node >/dev/null 2>&1; then
exec node "$SCRIPT_DIR/host.js"
Expand All @@ -72,16 +72,16 @@ fi
if [ -x /usr/local/bin/node ]; then
exec /usr/local/bin/node "$SCRIPT_DIR/host.js"
fi
echo "Unable to find node executable for opzero-chrome-host" >&2
echo "Unable to find node executable for browser-control-host" >&2
exit 127
`);
writeExecutable("dist/native-host/opzero-chrome-host.cmd", `@echo off
writeExecutable("dist/native-host/browser-control-host.cmd", `@echo off
node "%~dp0host.js"
`);
copyFile("scripts/extension-id.example.json", "dist/scripts/extension-id.example.json");
if (process.env.OPZERO_CHROME_EXTENSION_ID) {
if (process.env.BROWSER_CONTROL_EXTENSION_ID) {
fs.writeFileSync("dist/scripts/extension-id.json", `${JSON.stringify({
extensionId: process.env.OPZERO_CHROME_EXTENSION_ID,
extensionId: process.env.BROWSER_CONTROL_EXTENSION_ID,
extensionHostName: "com.opzero.chrome"
}, null, 2)}\n`);
} else if (fs.existsSync("scripts/extension-id.store.json")) {
Expand All @@ -90,7 +90,11 @@ if (process.env.OPZERO_CHROME_EXTENSION_ID) {

function syncInstallableSkill(skillDir) {
fs.mkdirSync(skillDir, { recursive: true });
copyFile("skills/chrome-control/SKILL.md", path.join(skillDir, "SKILL.md"));
copyFile("skills/browser-control/SKILL.md", path.join(skillDir, "SKILL.md"));
const references = path.join(root, "skills", "browser-control", "references");
if (path.resolve(skillDir) !== path.dirname(references) && fs.existsSync(references)) {
copyDir(references, path.join(skillDir, "references"));
}
for (const generatedPath of ["native-host", "scripts", "chunks"]) {
fs.rmSync(path.join(skillDir, generatedPath), { recursive: true, force: true });
}
Expand All @@ -99,13 +103,13 @@ function syncInstallableSkill(skillDir) {
if (fs.existsSync(path.join(dist, "chunks"))) copyDir("dist/chunks", path.join(skillDir, "chunks"));
}

const sourceSkill = path.join(root, "skills", "chrome-control");
const skillDist = path.join(dist, "skill", "chrome-control");
const sourceSkill = path.join(root, "skills", "browser-control");
const skillDist = path.join(dist, "skill", "browser-control");
syncInstallableSkill(sourceSkill);
syncInstallableSkill(skillDist);

fs.mkdirSync(path.join(dist, "release"), { recursive: true });
zipDir(path.join(dist, "extension"), path.join(dist, "release", "browser-control-extension.zip"));
zipDir(skillDist, path.join(dist, "release", "chrome-control-skill.zip"));
zipDir(skillDist, path.join(dist, "release", "browser-control-skill.zip"));

process.stdout.write("Built dist/extension and dist/skill/chrome-control\n");
process.stdout.write("Built dist/extension and dist/skill/browser-control\n");
21 changes: 11 additions & 10 deletions scripts/check-project.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ const requiredFiles = [
"src/scripts/installed-browsers.ts",
"scripts/extension-id.example.json",
"scripts/extension-id.store.json",
"skills/chrome-control/SKILL.md",
"skills/chrome-control/native-host/client.js",
"skills/chrome-control/native-host/host.js",
"skills/chrome-control/native-host/opzero-chrome-host",
"skills/chrome-control/scripts/install-native-host.js",
"skills/chrome-control/scripts/check-native-host-manifest.js",
"skills/chrome-control/scripts/extension-id.json",
"skills/browser-control/SKILL.md",
"skills/browser-control/references/native-host.md",
"skills/browser-control/native-host/client.js",
"skills/browser-control/native-host/host.js",
"skills/browser-control/native-host/browser-control-host",
"skills/browser-control/scripts/install-native-host.js",
"skills/browser-control/scripts/check-native-host-manifest.js",
"skills/browser-control/scripts/extension-id.json",
".github/workflows/check.yml",
".github/workflows/chrome-web-store.yml",
".github/workflows/release.yml",
Expand All @@ -42,9 +43,9 @@ const requiredFiles = [
"dist/native-host/client.js",
"dist/scripts/install-native-host.js",
"dist/scripts/extension-id.json",
"dist/skill/chrome-control/SKILL.md",
"dist/skill/chrome-control/native-host/opzero-chrome-host",
"dist/skill/chrome-control/scripts/install-native-host.js",
"dist/skill/browser-control/SKILL.md",
"dist/skill/browser-control/native-host/browser-control-host",
"dist/skill/browser-control/scripts/install-native-host.js",
"src/scripts/check-native-host-manifest.ts",
"README.md",
"docs/DEVELOPER.md",
Expand Down
Loading
Loading