@sema-agent/connector-browser 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog: @sema-agent/connector-browser
2
+
3
+ All notable changes to this package are documented here. Released sections are
4
+ frozen: once a version is published its section is never edited again; new text
5
+ goes into the next version's section.
6
+
7
+ ## 0.1.1 (2026-10-16)
8
+
9
+ ### Documentation
10
+
11
+ - `docs/CONNECTORS/browser.md` now shows a recommended `connectors/browser/tools.json` (the standalone local
12
+ per-tool policy, CN-054, `@sema-agent/connectors` 0.7.0) that leaves `browser_run_code_unsafe`,
13
+ `browser_evaluate`, `browser_network_request` and `browser_network_requests` out of the allow list; retires
14
+ KL-16 (every tool offered by default, with no way to narrow it), whose upgrade criterion this policy meets.
15
+ Mounting the connector still grants every tool by default: nothing in this package writes the policy itself.
16
+
17
+ ### Changed
18
+
19
+ - Depends on `@sema-agent/connectors` 0.7.0.
20
+
21
+ ## 0.1.0 (2026-10-16)
22
+
23
+ ### Added
24
+
25
+ - The browser connector (CN-027), `sema-connector-browser`. With no arguments it
26
+ is an MCP stdio server: it starts the pinned Playwright MCP server
27
+ (`@playwright/mcp` 0.0.83, with `playwright` and `playwright-core`
28
+ 1.64.0-alpha-1790635538000) on the Node.js that runs the connector, driving
29
+ the pinned Chrome for Testing 155.0.8059.12, and passes MCP straight through.
30
+ `install` fetches the three npm tarballs and this platform's Chrome for Testing
31
+ zip from the sources `binaries.lock.json` names, compares each with its sha256
32
+ before anything is unpacked, and unpacks them below
33
+ `<configuration directory>/connectors/browser/bin/`; nothing is fetched at run
34
+ time and npm is never run. Chrome for Testing is pinned for `darwin-arm64` and
35
+ `linux-x64`.
36
+ - `auth` reads one line from standard input: `{}` stores a clean profile, and
37
+ `{"storageStateFile": "<absolute path>"}` imports a Playwright storage state
38
+ (at most 4 MiB, a JSON object with exactly `cookies` and `origins`). Either is
39
+ stored as the secret handle `BROWSER_STORAGE_STATE`.
40
+ - Security baseline: the server always gets `--headless --isolated --browser
41
+ chromium --sandbox --block-service-workers --no-webmcp --timeout-navigation
42
+ 60000 --output-max-size 67108864`, the Chrome for Testing executable, the
43
+ output directory and the per-launch copy of the login state, and nothing else;
44
+ no `PLAYWRIGHT_MCP_*` variable reaches it. Each launch has an output
45
+ directory of its own, `out` in the launch's directory, which is the server's
46
+ working directory and goes when the launch ends.
package/NOTICE ADDED
@@ -0,0 +1,31 @@
1
+ @sema-agent/connector-browser
2
+ Copyright 2026 The sema-agent Authors
3
+
4
+ This package is licensed under the Apache License, Version 2.0 (see the LICENSE
5
+ file of the sema-connectors repository).
6
+
7
+ Third-party programs: Playwright MCP, Playwright and Chrome for Testing
8
+ ------------------------------------------------------------------------
9
+
10
+ This package contains no third-party code. Its install subcommand downloads
11
+ the third-party programs binaries.lock.json pins, checks each against the
12
+ sha256 binaries.lock.json pins, and keeps them in the user's configuration
13
+ directory:
14
+
15
+ - @playwright/mcp, playwright and playwright-core
16
+ (https://github.com/microsoft/playwright-mcp,
17
+ https://github.com/microsoft/playwright), from the npm registry. They are
18
+ distributed under the Apache License, Version 2.0, with the notice:
19
+
20
+ Playwright
21
+ Copyright (c) Microsoft Corporation
22
+
23
+ This software contains code derived from the Puppeteer project
24
+ (https://github.com/puppeteer/puppeteer), available under the Apache 2.0
25
+ license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE).
26
+
27
+ - Chrome for Testing (https://developer.chrome.com/blog/chrome-for-testing),
28
+ a build of Google Chrome made for automated testing, from the download host
29
+ Playwright uses for it. It is distributed by Google LLC under its own terms
30
+ (Copyright Google LLC; the Chromium open source project and other open source
31
+ software, whose notices the build lists at chrome://credits).
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // sema-connector-browser (CN-027): the browser connector's executable. With no arguments it runs the pinned Playwright
3
+ // MCP server as an MCP stdio server; a first argument dispatches install, auth, mount and status
4
+ // (docs/CONNECTORS/browser.md). Implementation: src/cli.ts, built to dist/cli.js.
5
+ import { main } from '../dist/cli.js'
6
+
7
+ process.exitCode = await main(process.argv.slice(2), process.env)
@@ -0,0 +1,60 @@
1
+ {
2
+ "playwright-mcp": {
3
+ "version": "0.0.83",
4
+ "archives": [
5
+ {
6
+ "platform": "*",
7
+ "file": "playwright-mcp-0.0.83.tgz",
8
+ "source": "https://registry.npmjs.org/@playwright/mcp/-/mcp-{version}.tgz",
9
+ "sha256": "f0014a13361b1ee2d8607a4e187868ab9a6e8ac637dbcd66e4af43f738d0c8cb",
10
+ "unpack": {
11
+ "directory": "package",
12
+ "to": "node_modules/@playwright/mcp"
13
+ }
14
+ },
15
+ {
16
+ "platform": "*",
17
+ "file": "playwright-1.64.0-alpha-1790635538000.tgz",
18
+ "source": "https://registry.npmjs.org/playwright/-/{file}",
19
+ "sha256": "436022a21e7a7c34436970fe04ab3fccae87a5045d3ed5af3f66c6e3ba377ee8",
20
+ "unpack": {
21
+ "directory": "package",
22
+ "to": "node_modules/playwright"
23
+ }
24
+ },
25
+ {
26
+ "platform": "*",
27
+ "file": "playwright-core-1.64.0-alpha-1790635538000.tgz",
28
+ "source": "https://registry.npmjs.org/playwright-core/-/{file}",
29
+ "sha256": "54ad1d38174d6ef56d1f165e52547ece8e6c24b865f2a072d9ebaacfbad1ca47",
30
+ "unpack": {
31
+ "directory": "package",
32
+ "to": "node_modules/playwright-core"
33
+ }
34
+ },
35
+ {
36
+ "platform": "darwin-arm64",
37
+ "file": "chrome-mac-arm64.zip",
38
+ "source": "https://cdn.playwright.dev/builds/cft/155.0.8059.12/mac-arm64/{file}",
39
+ "sha256": "d64771a096fffd48b49a65e481dda50ae43a7de55fb93acfa6aece26ec3b10db",
40
+ "unpack": {
41
+ "directory": "chrome-mac-arm64",
42
+ "to": "chrome"
43
+ }
44
+ },
45
+ {
46
+ "platform": "linux-x64",
47
+ "file": "chrome-linux64.zip",
48
+ "source": "https://cdn.playwright.dev/builds/cft/155.0.8059.12/linux64/{file}",
49
+ "sha256": "a24546e0e6c763bf864861ce50498f38783663e24cd2af2a16798c25dc3a9489",
50
+ "unpack": {
51
+ "directory": "chrome-linux64",
52
+ "to": "chrome"
53
+ }
54
+ }
55
+ ],
56
+ "program": {
57
+ "node": "node_modules/@playwright/mcp/cli.js"
58
+ }
59
+ }
60
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function auth(configDir: string): Promise<number>;
package/dist/auth.js ADDED
@@ -0,0 +1,59 @@
1
+ import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs';
2
+ import { isAbsolute } from 'node:path';
3
+ import { openFileSecrets, readStdinJson } from '@sema-agent/connectors';
4
+ import { NAME, STATE_HANDLE, complain, say } from './names.js';
5
+ import { EMPTY_STATE, STATE_LIMIT, readState } from './state.js';
6
+ const INPUT_LIMIT = 64 * 1024;
7
+ const FILE_KEY = 'storageStateFile';
8
+ function stateFromFile(path) {
9
+ if (!isAbsolute(path))
10
+ return { ok: false, problem: `${FILE_KEY} must be an absolute path` };
11
+ let fd;
12
+ try {
13
+ fd = openSync(path, 'r');
14
+ }
15
+ catch {
16
+ return { ok: false, problem: `the ${FILE_KEY} cannot be read` };
17
+ }
18
+ try {
19
+ const st = fstatSync(fd);
20
+ if (!st.isFile())
21
+ return { ok: false, problem: `the ${FILE_KEY} is not a regular file` };
22
+ if (st.size > STATE_LIMIT)
23
+ return { ok: false, problem: `the login state is larger than ${STATE_LIMIT} bytes` };
24
+ return readState(readFileSync(fd));
25
+ }
26
+ catch {
27
+ return { ok: false, problem: `the ${FILE_KEY} cannot be read` };
28
+ }
29
+ finally {
30
+ closeSync(fd);
31
+ }
32
+ }
33
+ export async function auth(configDir) {
34
+ const input = await readStdinJson(INPUT_LIMIT, [], [FILE_KEY]);
35
+ if (!input.ok) {
36
+ complain(input.problem);
37
+ return 1;
38
+ }
39
+ const file = input.values[FILE_KEY];
40
+ let state = EMPTY_STATE;
41
+ if (file !== undefined) {
42
+ const read = stateFromFile(file);
43
+ if (!read.ok) {
44
+ complain(read.problem);
45
+ return 1;
46
+ }
47
+ state = read.text;
48
+ }
49
+ const secrets = openFileSecrets({ configDir, connector: NAME });
50
+ try {
51
+ secrets.put(STATE_HANDLE, state);
52
+ }
53
+ catch (e) {
54
+ complain(`the login state was not stored (run auth again): ${e instanceof Error ? e.message : 'unknown error'}`);
55
+ return 1;
56
+ }
57
+ say(`stored ${file === undefined ? 'an empty login state (a clean profile)' : 'the login state of the file'} of connector ${NAME} as handle ${STATE_HANDLE} under ${secrets.directory}`);
58
+ return 0;
59
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { type Env } from '@sema-agent/connectors';
2
+ export declare function main(argv: readonly string[], env: Env): Promise<number | undefined>;
package/dist/cli.js ADDED
@@ -0,0 +1,9 @@
1
+ import { connectorCli } from '@sema-agent/connectors';
2
+ import { auth } from './auth.js';
3
+ import { NAME } from './names.js';
4
+ import { serve } from './serve.js';
5
+ import { PLAYWRIGHT_MCP } from './upstream.js';
6
+ const AUTH_READS = '{} or {"storageStateFile": "<absolute path>"} as one line';
7
+ export function main(argv, env) {
8
+ return connectorCli({ name: NAME, serve, install: PLAYWRIGHT_MCP.install, auth, authReads: AUTH_READS }, argv, env);
9
+ }
@@ -0,0 +1,3 @@
1
+ export declare const NAME = "browser";
2
+ export declare const STATE_HANDLE = "BROWSER_STORAGE_STATE";
3
+ export declare const PROGRAM: string, say: (line: string) => void, complain: (line: string) => void;
package/dist/names.js ADDED
@@ -0,0 +1,4 @@
1
+ import { cliOutput } from '@sema-agent/connectors';
2
+ export const NAME = 'browser';
3
+ export const STATE_HANDLE = 'BROWSER_STORAGE_STATE';
4
+ export const { program: PROGRAM, say, complain } = cliOutput(NAME);
@@ -0,0 +1,2 @@
1
+ import type { Env } from '@sema-agent/connectors';
2
+ export declare function serve(env: Env): Promise<number>;
package/dist/serve.js ADDED
@@ -0,0 +1,74 @@
1
+ import { closeSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { NAME, PROGRAM, STATE_HANDLE, complain } from './names.js';
4
+ import { STATE_LIMIT, readState } from './state.js';
5
+ import { CHROME_EXECUTABLE, OUT_DIR, PLAYWRIGHT_MCP, SETTINGS_PREFIX, argsFor } from './upstream.js';
6
+ function copyOf(path) {
7
+ let fd;
8
+ try {
9
+ fd = openSync(path, 'r');
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ try {
15
+ const st = fstatSync(fd);
16
+ if (!st.isFile())
17
+ return null;
18
+ if (st.size > STATE_LIMIT)
19
+ return Buffer.alloc(STATE_LIMIT + 1);
20
+ return readFileSync(fd);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ finally {
26
+ closeSync(fd);
27
+ }
28
+ }
29
+ function outputDirectory(out) {
30
+ try {
31
+ mkdirSync(out, { mode: 0o700 });
32
+ return null;
33
+ }
34
+ catch (e) {
35
+ if (e.code !== 'EEXIST')
36
+ return `the output directory could not be made (${e.code ?? 'unknown error'})`;
37
+ }
38
+ return lstatSync(out).isDirectory() ? null : `${OUT_DIR} next to the ${STATE_HANDLE} copy is not a directory: start the connector with sema-connector run ${NAME}`;
39
+ }
40
+ export function serve(env) {
41
+ const refuse = (problem) => {
42
+ complain(problem);
43
+ return Promise.resolve(1);
44
+ };
45
+ const own = Object.fromEntries(Object.entries(env).filter(([k]) => !k.startsWith(SETTINGS_PREFIX)));
46
+ const start = PLAYWRIGHT_MCP.start(own);
47
+ if (!start.ok)
48
+ return refuse(start.problem);
49
+ const state = start.files[STATE_HANDLE];
50
+ const bytes = copyOf(state);
51
+ if (bytes === null)
52
+ return refuse(`the ${STATE_HANDLE} handle cannot be read`);
53
+ const read = readState(bytes);
54
+ if (!read.ok)
55
+ return refuse(`the stored ${STATE_HANDLE} handle is refused: ${read.problem}; store it again with ${PROGRAM} auth`);
56
+ const where = CHROME_EXECUTABLE[process.platform];
57
+ if (where === undefined)
58
+ return refuse(`Chrome for Testing is not pinned for ${process.platform}`);
59
+ const chrome = join(start.binDir, where);
60
+ let executable = false;
61
+ try {
62
+ const st = lstatSync(chrome);
63
+ executable = st.isFile() && (st.mode & 0o111) !== 0;
64
+ }
65
+ catch { }
66
+ if (!executable)
67
+ return refuse(`Chrome for Testing is not in the bin directory: run ${PROGRAM} install`);
68
+ const out = join(dirname(state), OUT_DIR);
69
+ const problem = outputDirectory(out);
70
+ if (problem !== null)
71
+ return refuse(problem);
72
+ process.chdir(out);
73
+ return start.run(argsFor({ chrome, out, state }), {});
74
+ }
@@ -0,0 +1,10 @@
1
+ export declare const STATE_LIMIT: number;
2
+ export declare const EMPTY_STATE = "{\"cookies\":[],\"origins\":[]}";
3
+ export type StateReading = {
4
+ readonly ok: true;
5
+ readonly text: string;
6
+ } | {
7
+ readonly ok: false;
8
+ readonly problem: string;
9
+ };
10
+ export declare function readState(bytes: Uint8Array): StateReading;
package/dist/state.js ADDED
@@ -0,0 +1,25 @@
1
+ export const STATE_LIMIT = 4 * 1024 * 1024;
2
+ export const EMPTY_STATE = '{"cookies":[],"origins":[]}';
3
+ export function readState(bytes) {
4
+ if (bytes.length > STATE_LIMIT)
5
+ return { ok: false, problem: `the login state is larger than ${STATE_LIMIT} bytes` };
6
+ let text;
7
+ try {
8
+ text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
9
+ }
10
+ catch {
11
+ return { ok: false, problem: 'the login state is not UTF-8 text' };
12
+ }
13
+ let value;
14
+ try {
15
+ value = JSON.parse(text);
16
+ }
17
+ catch {
18
+ return { ok: false, problem: 'the login state is not JSON' };
19
+ }
20
+ const object = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
21
+ const keys = object === null ? [] : Object.keys(object).sort();
22
+ if (object === null || keys.join('\0') !== 'cookies\0origins' || !Array.isArray(object.cookies) || !Array.isArray(object.origins))
23
+ return { ok: false, problem: 'the login state must be one JSON object with exactly the keys cookies and origins, each a list (a Playwright storage state)' };
24
+ return { ok: true, text };
25
+ }
@@ -0,0 +1,12 @@
1
+ export declare const UPSTREAM = "playwright-mcp";
2
+ export declare const PLAYWRIGHT_MCP: import("@sema-agent/connectors").PinnedUpstream;
3
+ export declare const OUTPUT_MAX_SIZE: number;
4
+ export declare const OUT_DIR = "out";
5
+ export declare const SETTINGS_PREFIX = "PLAYWRIGHT_MCP_";
6
+ export declare const CHROME_EXECUTABLE: Readonly<Record<string, string>>;
7
+ export declare const FIXED_FLAGS: readonly string[];
8
+ export declare function argsFor(paths: {
9
+ readonly chrome: string;
10
+ readonly out: string;
11
+ readonly state: string;
12
+ }): readonly string[];
@@ -0,0 +1,15 @@
1
+ import { pinnedUpstream } from '@sema-agent/connectors';
2
+ import { NAME, STATE_HANDLE } from './names.js';
3
+ export const UPSTREAM = 'playwright-mcp';
4
+ export const PLAYWRIGHT_MCP = pinnedUpstream({ connector: NAME, upstream: UPSTREAM, lockFile: new URL('../binaries.lock.json', import.meta.url), handles: [STATE_HANDLE] });
5
+ export const OUTPUT_MAX_SIZE = 64 * 1024 * 1024;
6
+ export const OUT_DIR = 'out';
7
+ export const SETTINGS_PREFIX = 'PLAYWRIGHT_MCP_';
8
+ export const CHROME_EXECUTABLE = Object.freeze({
9
+ darwin: 'chrome/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
10
+ linux: 'chrome/chrome',
11
+ });
12
+ export const FIXED_FLAGS = Object.freeze(['--headless', '--isolated', '--browser', 'chromium', '--sandbox', '--block-service-workers', '--no-webmcp', '--timeout-navigation', '60000', '--output-max-size', String(OUTPUT_MAX_SIZE)]);
13
+ export function argsFor(paths) {
14
+ return [...FIXED_FLAGS, '--executable-path', paths.chrome, '--output-dir', paths.out, '--storage-state', paths.state];
15
+ }
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@sema-agent/connector-browser",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Browser connector: runs the pinned Playwright MCP server and a pinned Chrome for Testing behind the sema-connector launcher, in an isolated, headless session whose login state is a secret handle.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "bin": {
8
+ "sema-connector-browser": "bin/sema-connector-browser.mjs"
9
+ },
10
+ "exports": {
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "dist",
16
+ "CHANGELOG.md",
17
+ "binaries.lock.json",
18
+ "NOTICE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json"
22
+ },
23
+ "dependencies": {
24
+ "@sema-agent/connectors": "0.7.0",
25
+ "@sema-agent/connectors-contract": "0.1.1"
26
+ },
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/sema-agent/sema-connectors.git",
33
+ "directory": "packages/connector-browser"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.