🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

browser-pilot-cli

Package Overview
Dependencies
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

browser-pilot-cli - npm Package Compare versions

Comparing version
0.5.1
to
0.6.0
+108
docs/releases/v0.6.0.md
# Browser Pilot 0.6.0
Browser Pilot 0.6.0 raises the supported npm runtime to an actively maintained
Node.js release and adds static checks to the pull-request safety net.
## Runtime and validation
- The npm distribution now requires Node.js 22 or newer. Node.js 18 and 20 are
no longer supported. Native Browser Pilot executables are unaffected.
- CI runs ESLint across TypeScript sources, scripts, and tests, including
type-aware checks for floating promises in production code.
- The lint baseline rejects unused variables and duplicate imports.
## Managed Agent skill
- The repository skill can now be installed independently by any shell-capable
Agent host. Its mandatory preflight checks `bp --version`, installs the exact
tested npm CLI when the command is absent or incompatible, and verifies the
installed version again before browser work starts.
- Remote skill managers should install `plugin/skills/browser-pilot` from the
`skill-stable` branch. The release workflow advances that branch only after
the matching npm package and GitHub Release are public.
- `BROWSER_PILOT_OUTPUT_DIR` is now an enforced boundary for screenshots, PDFs,
download exports, and saved network bodies. When set, outside absolute paths,
`..` escapes, and existing symbolic-link escapes fail before browser or
network work starts. Unset behavior remains unchanged.
## Broker endpoint compatibility
- The private Broker RPC contract advances from version 2 to version 3. Broker
endpoints now require a per-process credential, reject unauthenticated local
callers, and fail closed when endpoint ownership cannot be verified.
- This is a breaking mixed-version boundary. A v0.6.0 CLI will not reuse or
automatically stop a Broker started by v0.5.1. If startup reports
`protocol_incompatible`, run `disconnect` with the older executable named in
the error, or use a separate `BROWSER_PILOT_HOME` for deliberate isolation.
- The removed legacy private endpoints (`/cdp`, `/discovered`, `/dialogs`, and
`/targets`) are no longer available. Supported clients use the authenticated
Broker RPC surface exclusively.
## Retry recovery
- Mutating CLI operations now derive their Broker idempotency key from the
stable request ID, tool name, canonical arguments, and resolved target.
Retrying the same operation from a new CLI process no longer depends on the
number of setup calls that preceded it.
## Broker ownership
- The Broker now holds an atomic lifetime owner lock that binds its PID to the
operating system's process start identity. A second daemon cannot unlink or
overwrite a live Broker's endpoint and metadata.
- Stale Broker state is reclaimed only after the recorded process is dead or
its PID has been provably reused. If a live owner's start identity cannot be
read, startup fails closed with manual recovery guidance.
## Observation safety and limits
- Browser-side observation scripts now run in an isolated execution world.
Page-defined or overwritten globals no longer affect or appear in
Browser Pilot observations.
- Snapshot work is bounded by DOM text-node limits. A truncated Observation can
now report `work_limit`, and protocol limits expose `maxDomTextNodes` so
clients can distinguish bounded work from element or byte truncation.
- Commands that require a current tab now return the public
`no_selected_tab` error instead of creating or selecting a tab implicitly.
## CDP and Chrome discovery
- CDP failures preserve Chrome's protocol error code and structured data, while
connection handshakes and keepalive loss now fail within bounded deadlines.
- Chrome discovery verifies profile ownership against live process metadata and
reports setup or authorization remediation without opening a speculative
browser connection.
## Managed target recovery
- Janitor adoption now reports ownership for each requested target instead of
treating the transitive adoption count as a success flag. Popup descendants
and targets auto-tracked during adoption no longer cause false failures.
## Human-readable output
- Page-derived names, values, titles, URLs, and frame labels are now escaped
and bounded before being placed in human-readable structural lines. Embedded
control characters can no longer fabricate Observation refs or extra rows.
- JSON output continues to expose the original semantic values unchanged.
## CLI output compatibility
- `bp keyboard` JSON now uses the same Observation shape as other page
actions, including page geometry, hints, and Profile context. The redundant
`typed` echo is no longer returned.
- `bp net show` returns one CLI request `id` instead of also exposing the
Broker's duplicate numeric `sequence` field.
- Output mode now follows Commander's parsed global `--human` option. A
positional value that happens to equal `--human` remains command data.
- `bp net remove` accepts both the full `rule:<uuid>` value shown by rule
commands and its bare UUID form.
## URL wildcard matching
- `bp wait --url` and `bp net --url` now share case-insensitive, full-URL
wildcard matching. A pattern without `*` must match the entire URL; use
`*fragment*` for the previous contains-style wait behavior.
- `*` is the only wildcard operator. Regex metacharacters, including `?`, are
matched literally; existing network rules that relied on `?` as a regex
quantifier must be rewritten.
+220
-32

@@ -38,2 +38,21 @@ // src/cdp.ts

// src/cdp.ts
var CDP_HANDSHAKE_TIMEOUT_CODE = "cdp_handshake_timeout";
var CDPError = class extends Error {
constructor(code, message, data) {
super(message);
this.code = code;
this.data = data;
this.name = "CDPError";
}
};
var DEFAULT_HANDSHAKE_TIMEOUT_MS = 8e3;
var DEFAULT_PING_INTERVAL_MS = 15e3;
var DEFAULT_PONG_TIMEOUT_MS = 5e3;
function positiveTiming(value, fallback, name) {
const resolved = value ?? fallback;
if (!Number.isSafeInteger(resolved) || resolved < 1) {
throw new Error(`${name} must be a positive integer`);
}
return resolved;
}
var CDPClient = class {

@@ -48,2 +67,18 @@ ws;

closeRequested = false;
handshakeTimeoutMs;
pingIntervalMs;
pongTimeoutMs;
handshakeTimer;
heartbeatTimer;
pongTimer;
missedPongs = 0;
constructor(options = {}) {
this.handshakeTimeoutMs = positiveTiming(
options.handshakeTimeoutMs,
DEFAULT_HANDSHAKE_TIMEOUT_MS,
"handshakeTimeoutMs"
);
this.pingIntervalMs = positiveTiming(options.pingIntervalMs, DEFAULT_PING_INTERVAL_MS, "pingIntervalMs");
this.pongTimeoutMs = positiveTiming(options.pongTimeoutMs, DEFAULT_PONG_TIMEOUT_MS, "pongTimeoutMs");
}
get connectionState() {

@@ -62,5 +97,20 @@ return this.state;

let settled = false;
this.handshakeTimer = setTimeout(() => {
if (socket !== this.ws || settled) return;
this.handshakeTimer = void 0;
const error = new CDPError(
CDP_HANDSHAKE_TIMEOUT_CODE,
`Chrome DevTools WebSocket handshake timed out after ${this.handshakeTimeoutMs}ms`
);
settled = true;
reject(error);
this.stopHeartbeat();
this.transition("disconnected", error);
socket.terminate();
}, this.handshakeTimeoutMs);
this.handshakeTimer.unref();
const rejectConnect = (error) => {
if (settled) return;
settled = true;
this.clearHandshakeTimer();
reject(error);

@@ -71,2 +121,3 @@ };

settled = true;
this.clearHandshakeTimer();
resolve();

@@ -77,2 +128,3 @@ };

const stable = this.disconnectedError(error);
this.stopHeartbeat();
this.failPending(stable);

@@ -84,4 +136,5 @@ rejectConnect(stable);

const onOpen = () => {
if (socket !== this.ws) return;
if (socket !== this.ws || settled) return;
this.transition("connected");
this.startHeartbeat(socket);
resolveConnect();

@@ -102,3 +155,7 @@ };

this.callbacks.delete(msg.id);
msg.error ? cb.reject(new Error(msg.error.message)) : cb.resolve(msg.result ?? {});
msg.error ? cb.reject(new CDPError(
typeof msg.error.code === "number" || typeof msg.error.code === "string" ? msg.error.code : "cdp_error",
typeof msg.error.message === "string" ? msg.error.message : "Chrome DevTools command failed",
msg.error.data
)) : cb.resolve(msg.result ?? {});
}

@@ -122,2 +179,4 @@ } else if (msg.method) {

this.ws = void 0;
this.clearHandshakeTimer();
this.stopHeartbeat();
const error = this.disconnectedError();

@@ -178,2 +237,4 @@ this.failPending(error);

this.closeRequested = true;
this.clearHandshakeTimer();
this.stopHeartbeat();
const socket = this.ws;

@@ -218,2 +279,56 @@ if (!socket) {

}
startHeartbeat(socket) {
this.stopHeartbeat();
this.missedPongs = 0;
socket.on("pong", () => {
if (socket !== this.ws || this.state !== "connected") return;
if (this.pongTimer) clearTimeout(this.pongTimer);
this.pongTimer = void 0;
this.missedPongs = 0;
this.scheduleHeartbeat(socket);
});
this.scheduleHeartbeat(socket);
}
scheduleHeartbeat(socket) {
if (this.heartbeatTimer) clearTimeout(this.heartbeatTimer);
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(socket), this.pingIntervalMs);
this.heartbeatTimer.unref();
}
sendHeartbeat(socket) {
this.heartbeatTimer = void 0;
if (socket !== this.ws || this.state !== "connected" || socket.readyState !== WebSocket.OPEN) return;
socket.ping((error) => {
if (error && socket === this.ws) this.disconnectUnresponsiveSocket(socket, error);
});
this.pongTimer = setTimeout(() => {
this.pongTimer = void 0;
if (socket !== this.ws || this.state !== "connected") return;
this.missedPongs += 1;
if (this.missedPongs >= 2) {
this.disconnectUnresponsiveSocket(socket, new Error("Chrome DevTools keepalive timed out"));
return;
}
this.sendHeartbeat(socket);
}, this.pongTimeoutMs);
this.pongTimer.unref();
}
disconnectUnresponsiveSocket(socket, cause) {
if (socket !== this.ws) return;
const error = this.disconnectedError(cause);
this.stopHeartbeat();
this.failPending(error);
this.transition(this.closeRequested ? "closed" : "disconnected", error);
if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
}
clearHandshakeTimer() {
if (this.handshakeTimer) clearTimeout(this.handshakeTimer);
this.handshakeTimer = void 0;
}
stopHeartbeat() {
if (this.heartbeatTimer) clearTimeout(this.heartbeatTimer);
if (this.pongTimer) clearTimeout(this.pongTimer);
this.heartbeatTimer = void 0;
this.pongTimer = void 0;
this.missedPongs = 0;
}
failPending(error) {

@@ -250,3 +365,15 @@ for (const callback of this.callbacks.values()) callback.reject(error);

}
var cdp = new CDPClient();
function clientOptions(value) {
if (!value) return {};
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Invalid managed target janitor CDP options");
}
const allowed = /* @__PURE__ */ new Set(["handshakeTimeoutMs", "pingIntervalMs", "pongTimeoutMs"]);
if (Object.keys(parsed).some((key) => !allowed.has(key))) {
throw new Error("Invalid managed target janitor CDP options");
}
return parsed;
}
var cdp = new CDPClient(clientOptions(process.argv[3]));
var ownedTargets = /* @__PURE__ */ new Map();

@@ -256,3 +383,5 @@ var pending = Buffer.alloc(0);

var finishing = false;
var shutdownRequested = false;
var outputAvailable = true;
var discardingOversizedLine = false;
function emit(value) {

@@ -271,2 +400,15 @@ if (typeof process.send === "function" && process.connected) {

}
function serializeError(error, fallbackCode) {
if (error instanceof CDPError) {
return {
code: error.code,
message: boundedMessage(error),
...error.data !== void 0 ? { data: error.data } : {}
};
}
if (error instanceof BrowserPilotError) {
return { code: error.code, message: boundedMessage(error) };
}
return { code: fallbackCode, message: boundedMessage(error) };
}
function validateRequest(value) {

@@ -276,3 +418,3 @@ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid janitor request");

if (!Number.isSafeInteger(request.id) || Number(request.id) < 1) throw new Error("Invalid janitor request ID");
if (request.method !== "create" && request.method !== "adopt" && request.method !== "cdp.send") {
if (request.method !== "create" && request.method !== "adopt" && request.method !== "cdp.send" && request.method !== "shutdown") {
throw new Error("Invalid janitor method");

@@ -292,6 +434,6 @@ }

}
async function trackTarget(targetId, openerTargetId) {
async function trackTarget(targetId, openerTargetId, closeOnCapacity = true) {
if (ownedTargets.has(targetId)) return true;
if (ownedTargets.size >= MAX_TRACKED_TARGETS) {
await cdp.send("Target.closeTarget", { targetId }).catch(() => {
if (closeOnCapacity) await cdp.send("Target.closeTarget", { targetId }).catch(() => {
});

@@ -344,3 +486,5 @@ return false;

for (const targetId of requested) {
if (live.has(targetId)) await trackTarget(targetId, live.get(targetId));
if (live.has(targetId) && !await trackTarget(targetId, live.get(targetId), false)) {
throw new Error("Managed target capacity reached");
}
}

@@ -352,6 +496,12 @@ let changed = true;

if (!openerTargetId || !ownedTargets.has(openerTargetId) || ownedTargets.has(targetId)) continue;
if (await trackTarget(targetId, openerTargetId)) changed = true;
if (!await trackTarget(targetId, openerTargetId, false)) {
throw new Error("Managed target capacity reached");
}
changed = true;
}
}
return { adopted: ownedTargets.size - before };
return {
adopted: ownedTargets.size - before,
owned: Object.fromEntries(requested.map((targetId) => [targetId, ownedTargets.has(targetId)]))
};
}

@@ -376,2 +526,9 @@ async function forwardCdp(params) {

try {
if (request.method === "shutdown") {
if (request.params.cleanup !== false) throw new Error("Invalid janitor shutdown request");
shutdownRequested = true;
emit({ id: request.id, result: { shuttingDown: true } });
await finish(false);
return;
}
const result = request.method === "create" ? await createTarget(request.params) : request.method === "adopt" ? await adoptTargets(request.params) : await forwardCdp(request.params);

@@ -382,9 +539,50 @@ emit({ id: request.id, result });

id: request.id,
error: {
code: request.method === "cdp.send" ? "cdp_error" : "janitor_error",
message: boundedMessage(error)
}
error: request.method === "cdp.send" ? serializeError(error, "cdp_error") : { code: "janitor_error", message: boundedMessage(error) }
});
}
}
function reportProtocolError(error) {
emit({
event: "protocol_error",
error: { code: "janitor_protocol_error", message: boundedMessage(error) }
});
}
function consumeInput(chunk, enqueue) {
let offset = 0;
while (offset < chunk.length) {
if (discardingOversizedLine) {
const newline2 = chunk.indexOf(10, offset);
if (newline2 < 0) return;
discardingOversizedLine = false;
offset = newline2 + 1;
continue;
}
const newline = chunk.indexOf(10, offset);
if (newline < 0) {
const fragment2 = chunk.subarray(offset);
if (pending.length + fragment2.length > MAX_LINE_BYTES) {
pending = Buffer.alloc(0);
discardingOversizedLine = true;
reportProtocolError(new Error("Invalid janitor request size"));
} else {
pending = pending.length === 0 ? fragment2 : Buffer.concat([pending, fragment2]);
}
return;
}
const fragment = chunk.subarray(offset, newline);
offset = newline + 1;
if (pending.length + fragment.length > MAX_LINE_BYTES) {
pending = Buffer.alloc(0);
reportProtocolError(new Error("Invalid janitor request size"));
continue;
}
const line = pending.length === 0 ? fragment : Buffer.concat([pending, fragment]);
pending = Buffer.alloc(0);
try {
enqueue(parseRequest(line));
} catch (error) {
reportProtocolError(error);
}
}
}
function cleanupOrder() {

@@ -435,14 +633,19 @@ const depth = (targetId) => {

process.stdin.on("end", () => {
shutdownRequested = true;
void commandTail.finally(() => finish(true));
});
process.stdin.on("error", () => {
shutdownRequested = true;
void finish(true, 1);
});
process.on("disconnect", () => {
shutdownRequested = true;
void commandTail.finally(() => finish(true));
});
process.on("SIGTERM", () => {
shutdownRequested = true;
void finish(true);
});
process.on("SIGINT", () => {
shutdownRequested = true;
void finish(true);

@@ -489,21 +692,3 @@ });

const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
if (pending.length + chunk.length > MAX_LINE_BYTES * 2) {
void finish(true, 2);
return;
}
pending = pending.length === 0 ? chunk : Buffer.concat([pending, chunk]);
let newline = pending.indexOf(10);
while (newline >= 0) {
const line = pending.subarray(0, newline);
pending = pending.subarray(newline + 1);
try {
enqueue(parseRequest(line));
} catch (error) {
process.stderr.write(`${boundedMessage(error)}
`);
void finish(true, 2);
return;
}
newline = pending.indexOf(10);
}
consumeInput(chunk, enqueue);
});

@@ -513,2 +698,5 @@ }

main().catch((error) => {
if (!shutdownRequested) {
emit({ event: "startup_error", error: serializeError(error, "janitor_error") });
}
if (!finishing) process.stderr.write(`${boundedMessage(error)}

@@ -515,0 +703,0 @@ `);

@@ -40,4 +40,5 @@ # Browser Pilot Platform Specification

This decision applies equally to Agent-managed installs and products that
bundle Browser Pilot. A product makes the CLI available on the Agent command
environment's `PATH`, installs the same skill, and injects stable environment.
pre-provision Browser Pilot. Both install the same skill and inject stable
environment. The CLI is either installed by the skill's mandatory preflight or
already available on the Agent command environment's `PATH`.

@@ -67,3 +68,5 @@ ## 3. Public Contract

- the complete Browser Pilot skill;
- a compatible `bp` executable on the command environment's `PATH`;
- permission for the Agent to install the skill's exact npm CLI through the
normal shell approval flow, or a compatible `bp` executable already on
`PATH`;
- a stable `BROWSER_PILOT_CLIENT_KEY` per independent Agent;

@@ -77,6 +80,6 @@ - an absolute task-owned `BROWSER_PILOT_OUTPUT_DIR` when file results are used;

The host should pin the CLI version tested by its product release. The skill
declares a compatible range so a compatible user-installed command can also be
used. The host must update its executable, skill, compatibility metadata, and
checksums together.
Managed skill installations must track `plugin/skills/browser-pilot` from the
`skill-stable` branch, which advances only after the matching npm package and
GitHub Release are public. Pre-provisioned installations should pin a CLI
version inside the skill's declared compatible range.

@@ -331,8 +334,13 @@ ## 5. Browser Scope and Authorization

Output resolution order is:
When `BROWSER_PILOT_OUTPUT_DIR` is set, it is an enforced output boundary:
1. explicit filename;
2. `BROWSER_PILOT_OUTPUT_DIR`;
3. current working directory.
- relative filenames resolve inside that absolute directory;
- absolute filenames are accepted only when they remain inside it;
- `..` and existing symbolic-link escapes are rejected before browser or
network work starts;
- generated default filenames are written inside it.
When the variable is unset, explicit filenames resolve normally and generated
defaults use the current working directory for backward compatibility.
The Broker stores media and downloads in private bounded temporary storage,

@@ -413,2 +421,3 @@ then exports them to a caller-authorized absolute path. Binary bytes are never

- managed Agent skill with an exact npm CLI preflight;
- global npm install;

@@ -432,3 +441,4 @@ - local npm install or `npx --no-install`;

CLI, skill, plugin manifests, compatibility metadata, and checksums must be
released from one synchronized root version.
released from one synchronized root version. After publication succeeds, the
release commit advances the non-force-updated `skill-stable` branch.

@@ -448,2 +458,83 @@ ## 20. Security and Privacy Invariants

### 20.1 Local Broker Endpoint Threat Model
The Broker endpoint is local to one OS account. On Unix, a mode-0600 socket
inside a mode-0700 directory prevents access by other OS users. On Windows,
the named pipe relies on the platform's default DACL; the resulting limitation
and the selected hardening strategy are recorded separately below.
Endpoint authentication is in scope for:
- cross-user access, in addition to the Unix filesystem boundary;
- same-user sandboxed processes that can reach the local transport but cannot
read the Browser Pilot state directory;
- accidental or confused-deputy calls from same-user software that has no
reason to control the Broker;
- session impersonation based only on guessing a predictable client session
identifier.
A malicious process running as the same OS user with access to the Browser
Pilot state directory is out of scope. Such a process can read any shared
secret stored there. The endpoint token is therefore not a defense against
same-user malware. Defending that case would require per-client credentials
issued through user interaction or an OS- or host-provided isolation boundary.
### 20.2 Windows Named-Pipe Access Control
Windows retains the named-pipe transport and its default DACL. A shared-secret
endpoint token is the primary Windows boundary for processes that can reach the
pipe but cannot read the Browser Pilot state directory. At daemon startup,
Browser Pilot restricts that directory and the locator containing the token to
the current Windows user, local administrators, and the system account. The
default pipe DACL is not treated as an independently verified current-user-only
boundary.
This decision follows these platform constraints:
- Node's `net.Server.listen()` API cannot attach a Windows security descriptor
to a named pipe. Its `readableAll` and `writableAll` options broaden access
and cannot express a current-user-SID ACL.
- Applying a pipe ACL directly would require a native addon or helper. The
standalone distribution is a single Node SEA executable built from bundled
CommonJS. A helper would add architecture-specific build output, packaging
and checksum entries, process supervision, and an additional binary to sign
and verify on each release platform. A native addon would likewise have to
remain external to the SEA payload and match the embedded Node ABI. That
cost is disproportionate to the boundary defined in 20.1.
- Loopback TCP would avoid the named-pipe API limitation but would make the
endpoint reachable through the host network stack by every local account,
introduce port allocation and firewall behavior, and replace the existing
stable local-IPC discovery contract. It offers no compensating protection
once possession of the token remains the authentication boundary.
The Windows directory and locator ACL are defense in depth around token
storage, not a claim to defend against a malicious same-user process that can
already read the state directory.
### 20.3 Broker Endpoint Authentication
Each Broker process generates a new random endpoint token and stores it only in
the private Broker locator. Every endpoint except `GET /health` requires that
token as a Bearer credential. Authentication happens before request parsing or
Broker dispatch, and a rejected request returns a structured error.
The unauthenticated health response is read-only and contains only `ok`,
`brokerProtocol`, `serviceVersion`, `executableVersion`, `executableIdentity`,
`brokerProcessIdentity`, `browser`, and `clients`. Optional fields may be absent,
but no other state is exposed. In particular, the token and browser CDP
WebSocket URL are never health fields.
The CLI derives its stable internal client session identifier as an HMAC of the
client key and executable version, keyed by the endpoint token. A token or
Broker identity change during client initialization discards the entire client
construction, reloads the locator once, recomputes the HMAC, and starts again
from `initialize`. A dispatched tool call is never replayed by this recovery
path.
The authenticated private transport is Broker RPC version 3. Mixed versions
fail closed after discovery through the open health endpoint. A live
incompatible Broker is never replaced automatically; the user must stop it with
the executable that started it, stop its recorded process explicitly, or use a
separate `BROWSER_PILOT_HOME`.
## 21. Acceptance Gates

@@ -470,2 +561,3 @@

with complete metadata.
- `BROWSER_PILOT_OUTPUT_DIR` confines every CLI-created task file when set.

@@ -472,0 +564,0 @@ Real-site canaries report drift separately and do not make deterministic release

+21
-17
{
"name": "browser-pilot-cli",
"version": "0.5.1",
"version": "0.6.0",
"description": "CLI tool to control your browser via Chrome DevTools Protocol",

@@ -12,2 +12,3 @@ "repository": "https://github.com/relixiaobo/browser-pilot",

"scripts": {
"prebuild": "node scripts/clean-dist.mjs",
"build": "tsup",

@@ -21,18 +22,21 @@ "build:standalone:js": "tsup --config tsup.standalone.config.ts",

"release:check-version": "node scripts/sync-release-version.mjs --check",
"version": "node scripts/sync-release-version.mjs",
"release:prune": "node scripts/prune-release-artifacts.mjs",
"version": "node scripts/sync-release-version.mjs && git add -- package-lock.json plugin/package.json plugin/.claude-plugin/plugin.json .claude-plugin/marketplace.json plugin/skills/browser-pilot/compatibility.json",
"verify:standalone": "node scripts/verify-standalone.mjs",
"dev": "tsup --watch",
"start": "node dist/cli.js",
"test": "tsup && node --test tests/*.test.mjs && npx playwright test --project core --project compat --project network",
"test:unit": "tsup && node --test tests/*.test.mjs",
"test:capabilities": "tsup && node --test tests/browser-capability-fixtures.test.mjs",
"test:protocol": "tsup && node --test tests/protocol.test.mjs",
"test:distribution": "tsup && node scripts/verify-npm-distribution.mjs",
"test:canary": "tsup && node scripts/run-real-site-canaries.mjs",
"test:canary:strict": "tsup && node scripts/run-real-site-canaries.mjs --strict",
"validate:skill": "node scripts/validate-managed-skill.mjs",
"lint": "eslint src scripts tests *.ts",
"test": "npm run build && node scripts/run-node-tests.mjs unit && node scripts/run-node-tests.mjs browser && npx playwright test --project core --project compat --project network",
"test:unit": "npm run build && node scripts/run-node-tests.mjs unit",
"test:browser": "npm run build && node scripts/run-node-tests.mjs browser",
"test:capabilities": "npm run build && node scripts/run-node-tests.mjs browser browser-capability-fixtures.test.mjs",
"test:protocol": "npm run build && node --test tests/protocol.test.mjs",
"test:distribution": "npm run build && node scripts/verify-npm-distribution.mjs",
"test:canary": "npm run build && node scripts/run-real-site-canaries.mjs",
"test:canary:strict": "npm run build && node scripts/run-real-site-canaries.mjs --strict",
"test:integration": "npm run test:canary:strict",
"test:all": "npm test && npm run test:canary",
"test:agent": "python3 tests/agent/run.py",
"test:agent:dry": "python3 tests/agent/run.py --dry-run",
"test:agent:download": "python3 tests/agent/download_data.py"
"test:agent:dry": "python3 tests/agent/run.py --dry-run"
},

@@ -44,6 +48,2 @@ "files": [

"docs/architecture/browser-pilot-platform-spec.md",
"docs/plans/browser-capability-evolution.md",
"docs/plans/profile-context-routing.md",
"docs/plans/v0.3.0-stabilization.md",
"docs/plans/universal-agent-integration.md",
"docs/releases/",

@@ -61,5 +61,9 @@ "README.md",

"@types/ws": "^8.5.0",
"eslint": "^9.39.5",
"playwright": "^1.59.1",
"postject": "^1.0.0-alpha.6",
"tsup": "^8.0.0",
"typescript": "^5.7.0"
"typescript": "^5.7.0",
"typescript-eslint": "^8.65.0",
"yaml": "^2.9.0"
},

@@ -75,5 +79,5 @@ "keywords": [

"engines": {
"node": ">=18"
"node": ">=22"
},
"license": "MIT"
}

@@ -366,4 +366,12 @@ # Browser Pilot

npm run package:standalone
npm run release:prune -- --dry-run # preview local artifact retention
npm run release:prune # retain the two newest version groups
```
Agent evaluations are maintainer-only and use a separate Caliper checkout:
```bash
CALIPER_ROOT=/absolute/path/to/caliper npm run test:agent:dry
```
The root `package.json` version is the release source of truth. The npm version

@@ -381,3 +389,3 @@ lifecycle synchronizes the lockfile, plugin manifests, skill compatibility

- Chrome 144+, Edge, Brave, or another supported Chromium browser
- Node.js 18 or newer for the npm distribution
- Node.js 22 or newer for the npm distribution
- Remote debugging enabled at `chrome://inspect/#remote-debugging`

@@ -384,0 +392,0 @@ - Apple Silicon macOS, x64 Linux, or x64 Windows for native releases

# Browser Capability and Reliability Evolution Plan
Status: **Implemented through B7**
Source of truth: `docs/architecture/browser-pilot-platform-spec.md`
Workstream: B
## Goal
Improve Browser Pilot's page understanding, frame correctness, action
reliability, event production, and Agent guidance using validated ideas from
browser-use while preserving Browser Pilot's controlled target model, network
capabilities, real-profile behavior, and CLI workflow.
Research baseline: browser-use commit
`24e96d35a5f94794130a06b0e4b1acdeefc4a23f`.
## Adopt, Adapt, and Reject
Adopt or adapt:
- DOM + AX + DOMSnapshot fusion.
- CDP-session-aware element identity.
- Visible quads and occlusion checks.
- Checkbox/radio post-click verification.
- React/Vue/contenteditable input readback.
- Remaining-action cancellation after page, frame, focus, or document change.
- Typed browser watchdog events.
- Guidance for autocomplete, modals, filters, 403 responses, and loops.
- Multiple bounded page representations: interactive refs, readable text,
targeted text search, DOM metadata, and annotated screenshots.
- First-class page/container scrolling and verified native/custom dropdown
operations.
- Repeated-action detection that also checks whether bounded page state changed.
Do not copy:
- Automatic acceptance of confirm or beforeunload dialogs.
- Cloud browser, captcha, or hosted-session architecture.
- A concurrency model that treats the user's Chrome as one globally shared
Agent browser.
- Agent-framework-specific history, memory, or model prompting internals.
Preserve Browser Pilot advantages:
- Network observe/block/mock/header operations.
- HTTP auth, cookies, PDF, and explicit Pilot windows.
- Concise URL/read/snapshot/eval guidance for direct CLI Agents.
## Delivery Order
### B0. Establish baselines and shared contracts
- [x] **B0.1** Record browser-use ideas to adopt, adapt, and reject.
- Covers: FR-8, AC-8.
- [x] **B0.2** Build a fixture matrix for AX-only, DOM-only, shadow DOM,
same-origin iframe, cross-origin/OOPIF, overlays, contenteditable, React
controlled input, navigation, and document replacement.
- Covers: AC-8.
- Complete: a reusable local fixture catalog defines every listed scenario
with bounded explicit signals and no third-party dependency. Isolated
system-Chrome tests verify AX-vs-DOM semantics, open Shadow DOM, overlay hit
testing, rich editing, asynchronous controlled-input rollback, same-origin
frames, a forced cross-origin OOPIF target, top-level navigation, and same-URL
Document replacement. The compatibility fixture server exposes the same
catalog for later public-surface regression tests.
- [x] **B0.3** Freeze Observation v1 public fields, internal node identity,
invalidation reasons, limits, and truncation metadata with Workstream A.
- Covers: BR-13 through BR-16, NFR-1.
- Complete: runtime constants and canonical schemas now define the public
fields, deterministic truncation reasons, field/aggregate/depth/UTF-8 byte
limits, TTL, and capacity. Internal resolution binds process, browser
generation, Workspace, Lease, target, CDP session, frame, loader, Document
backend identity, and backend node. Same-context `stale_ref` errors expose a
stable invalidation reason while ownership mismatches remain opaque. Direct
CLI snapshots now preserve truncation metadata as additive JSON fields.
- A0 decision: `document_replaced` is an additive optional
`stale_ref.context.reason` under protocol 1.0 and 1.1. Older clients already
branch on `stale_ref` and rebuild state; no capability or version change is
required.
- [x] **B0.4** Add quantitative baselines: observable target recall, false
interactable rate, action verification failures, stale-ref detection, and
output size.
- Covers: FR-8.
- Complete: a versioned, local-only system-Chrome corpus now measures all ten
capability fixtures through Browser Pilot Observations, including selected
same-process and OOPIF frames. The recorded v1 baseline is 10/10 actionable
target recall, 2/12 false interactable refs, 2/2 expected action failures
detected, 2/2 same-document stale refs detected, and an initial 208-byte
maximum normalized Observation sample. B7 intentionally added bounded page
geometry and re-baselined that maximum at 426 bytes, still far below the
2 MiB protocol budget. Ground truth uses only public role/name pairs;
ephemeral origins, selectors, and CDP identity are excluded.
### B1. Build the observation engine
- [x] **B1.1** Collect DOMSnapshot layout and frame metadata alongside AX
nodes, without exposing raw page dumps to the Agent.
- Covers: BR-16, CON-5.
- Complete: every Observation now captures DOMSnapshot documents, shared
strings, frame/document relationships, backend-node identity, attributes,
browser clickable state, form state, computed display/visibility/opacity/
pointer behavior, layout bounds, and paint order in an ephemeral internal
index. Raw DOMSnapshot data, attributes, selectors, layout, and CDP IDs are
never returned or persisted.
- [x] **B1.2** Fuse AX semantics, DOM attributes, layout bounds, visibility,
editability, and form state into normalized observable elements.
- Covers: AC-8.
- Complete: AX role/name/value remain authoritative while DOM facts fill
missing names, values, checked state, autocomplete state, and modal
ancestry. Visible browser-marked clickable nodes absent from the semantic
AX surface become bounded DOM-only controls in stable document order.
Hidden, zero-layout, disabled, readonly, inert, and duplicate candidates
are excluded. Unit and isolated system-Chrome fixtures cover AX-only,
DOM-only, hidden/disabled controls, Shadow DOM, contenteditable, and
controlled-input readback without changing Observation v1 fields.
- [x] **B1.3** Preserve Shadow DOM traversal and add session-aware frame/OOPIF
traversal with deterministic ordering.
- Covers: AC-8.
- Complete: frame discovery now recursively joins same-process frame trees
with descendant OOPIF targets in stable parent-first order while excluding
iframe targets owned by other tabs. Observations, refs, actions, reads,
evaluation, uploads, captures, dialogs, network handling, continuity, and
invalidation use the selected frame's actual CDP session. Same-process
pointer coordinates are translated through the live frame-owner content
box; OOPIF roots remain frame-local. Child sessions are detached and their
state invalidated on navigation, detach, Lease release, reconnect, and
target teardown.
- [x] **B1.4** Add explicit element/page/text limits and truncation reasons;
keep output lean enough for Agent context.
- Covers: BR-16, NFR-1.
- Complete: Observation v1 enforces field and aggregate text limits, requested
and absolute element limits, AX tree depth, and a 2 MiB UTF-8 serialized
data budget. It emits canonical reasons in deterministic order, stores only
returned refs, and preserves the metadata through both Broker results and
additive direct-CLI JSON fields.
### B2. Replace ref storage and resolution
- [x] **B2.1** Move refs out of `~/.browser-pilot/refs.json` into ephemeral,
Workspace-scoped Observation records.
- Covers: FR-3, BR-13.
- Complete: Broker and direct CLI calls use the same bounded in-memory
Observation records scoped by Workspace, Lease, target, session, loader,
and browser generation. The file-backed ref store has been removed.
- [x] **B2.2** Resolve refs using browser generation, target, CDP session,
frame, loader, backend node, and document generation.
- Covers: BR-13 through BR-15.
- Complete: Observations bind and validate browser process and
connection generation, Workspace, Lease, target, CDP session, selected
frame, loader, Document backend identity, and per-ref backend node before
live-node resolution. Direct CLI refs resolve through the compatibility
Workspace's latest live Observation rather than a separate mapping.
- [x] **B2.3** Hard-invalidate on navigation, loader replacement, frame/session
detach, target detach, and reconnect, emitting typed reasons.
- Covers: BR-14, AC-5.
- Complete: every listed lifecycle, including browser generation restoration,
invalidates Broker and CLI refs with a typed `stale_ref` result.
- [x] **B2.4** Revalidate live nodes after same-document mutation and return
`stale_ref` instead of acting on a changed semantic target.
- Covers: BR-15, AC-8.
- Complete: ref-based click, type, and upload now resolve the original
backend node and revalidate its connected state plus the same AX-first,
DOM-fallback role/name identity used by Observation. Ordinary controls use
a targeted partial AX query; AX-name fallbacks and DOM-only controls use a
current DOMSnapshot and retain browser clickability checks. A removed node,
changed role/name, or lost DOM-only clickability returns the existing
`stale_ref` without a new protocol reason, browser identity, or semantic
text in the error. Only that ref fails: the Observation and its unchanged
refs remain usable. Broker tools and compatibility CLI actions now execute
the same canonical validator path.
### B3. Make actions verifiable
- [x] **B3.1** Before pointer actions, verify current bounds, viewport
intersection, hit-test target, enabled state, and obstruction.
- Covers: AC-8.
- Complete: ref clicks now scroll and revalidate connected layout, viewport
intersection, native/ARIA disabled state, and multiple hit-test points
across visible client rects. Descendant, associated-label, and shadow-host
hits are accepted; detached refs return `stale_ref`, while non-interactable
refs return bounded `action_not_verified` reasons before any pointer event.
Explicit coordinate clicks remain the low-level escape hatch.
- [x] **B3.2** After click, verify expected checkbox, radio, selection, focus,
dialog, navigation, popup, or document effects where observable.
- Covers: AC-8.
- Complete: click results now contain bounded typed evidence for native and
ARIA checked, selected, pressed, expanded, and composed focus state. The
Broker merges loader/URL changes, normalized Observation differences,
dialog openings, and attributable popup creation without exposing raw CDP
IDs. Expected checkbox/radio/option state failures report `mismatch`;
coordinate and otherwise unobservable clicks report `unavailable`.
- [x] **B3.3** Unify input behavior for native fields, controlled React/Vue
fields, and contenteditable; read back the effective value/content.
- Covers: AC-8.
- Complete: text controls, email/number controls, and contenteditable now use
Chrome's native editing path with trusted, cancelable input semantics;
date/time/color/range controls use a bounded value path without emitting an
early `change`. Targets are classified and rejected before dispatch when
detached, disabled, readonly, inert, or unsupported. Final readback detects
framework acceptance, synchronous/asynchronous rollback, browser
sanitization, and editor interception. Password evidence contains lengths
only, deep open-Shadow-DOM focus is readable, and keyboard clear uses
Chrome's `SelectAll` editing command.
- [x] **B3.4** Return typed action evidence and failure reasons instead of
reporting success solely because CDP dispatch completed.
- Covers: command reliability contract.
- Complete: click, type, keyboard, press, and upload results use bounded
discriminated evidence. Press combines focused-backend-node control changes
with Broker-owned navigation, document, dialog, and popup signals. Upload
verifies the selected file count and browser filename after dispatch.
Unsupported effects report `unavailable`; observable wrong states report
`mismatch`; exact machine input can raise `action_not_verified`. Legacy CLI
output remains compatible.
- [x] **B3.5** Stop any remaining composite action steps when target, frame,
focus, loader, or document generation changes unexpectedly.
- Covers: BR-10 through BR-12, AC-5.
- Complete: Broker and compatibility CLI actions create the same isolated
CDP continuity guard. It pins target/session ownership, browser generation,
selected frame, loader, and Document identity; type/keyboard additionally
pin deep composed focus. Checks run between clear, delete, per-character
keyboard input, and submit. Pre-mutation changes return retryable
`action_not_verified`; partial mutations preserve a structured
`unknown_outcome`, stop all remaining steps, and are never replayed.
### B4. Produce typed browser events and recovery state
- [x] **B4.1** Normalize CDP events into the BrowserEvent taxonomy for
navigation, document, target, popup, dialog, download, connection, and
observation invalidation.
- Covers: event contract, NFR-3.
- Complete: navigation, document change, target attach/detach and control,
managed popup, dialog, Observation invalidation, Command status, and Lease
expiry, sanitized Workspace network, scoped download, and browser connection
recovery producers are connected.
- [x] **B4.2** Add watchdogs for browser disconnect, stalled navigation,
detached frames, unhandled dialogs, and repeated no-progress actions.
- Covers: FR-5, FR-8.
- Complete: WebSocket loss emits connection state and leaves the browser
disconnected. Selected-profile endpoint metadata refreshes passively, while
only an explicit `browser.connect` can establish another WebSocket. Navigation timeouts return structured
`unknown_outcome`; selected-frame detach clears frame state; pending dialogs
emit a bounded reminder without auto-response; and browser-observable action
evidence drives a Lease/target-scoped no-progress streak. Timers and streaks
are transient, cleaned up with their owning resources, and never expose raw
CDP IDs, refs, typed text, or credentials.
- [x] **B4.3** Replace dialog auto-accept with explicit pending state and
accept/dismiss commands.
- Covers: DEC-5.
- Complete: daemon auto-accept is removed; pending Broker dialogs are
Lease/target scoped and use explicit list/respond tools without target-actor
deadlock. One-shot CLI calls use an isolated explicit list/respond path.
- [x] **B4.4** Ensure event producers are deterministic under target actor
serialization and reconnect generations.
- Covers: NFR-4.
- Complete: Browser events and Command records carry their source connection
generation. The Broker fences queued commands before dispatch and completed
results after execution, retains old-generation Command terminal and
reconnect cleanup events, and drops delayed ordinary CDP events from retired
generations. Target inventory refreshes serialize per Workspace and verify
their generation before applying an awaited snapshot. Tests cover same-target
ordering, cross-target interleaving, reconnect races, and stale inventory.
### B5. Improve Agent-facing guidance and data handling
- [x] **B5.1** Add structured hints for autocomplete, modal overlays, filters,
blocked/403 pages, login transitions, downloads, and repeated action loops.
- Covers: FR-8.
- Complete: Observation results now contain a bounded discriminated `hints`
array derived from DOM/AX signals without collecting page text or input
values. Authentication transitions are scoped to the live target session
and selected frame. Main-document 403/429, target-session download states,
and the no-progress threshold publish the same Agent-neutral hint contract
in events; subresource failures are excluded. Refs, reason text, counts,
and arrays are bounded, and focused tests cover all hint variants.
- A0 decision: this is an optional additive output field under protocol 1.0
and 1.1, with no new capability. It exposes no operation and derives only
data already covered by `observation.read` or `event.read`; older clients
ignore unknown response fields and older executors may omit it.
- [x] **B5.2** Mark passwords, cookies, auth, network bodies, uploads,
downloads, screenshots, and selected page text with sensitivity metadata.
- Covers: CON-5.
- Complete: the canonical tool schemas now carry validated
`x-browser-pilot-sensitivity` annotations on selected page text,
Observation element values, auth fields, cookie values, network
headers/bodies, upload Artifact references, prompt text, and eval content.
Tool-level possible classifications must cover every field annotation.
Password-capable Observation results declare `credential`; existing
password action evidence remains value-free and reports only `sensitive`
plus bounded lengths. Artifacts and events retain their runtime sensitivity.
- A0 decision: field annotations are additive schema metadata under protocol
1.0 and 1.1 and do not change argument or result value shapes. No new
capability is required; older clients ignore the annotation, while updated
adapters propagate it when constructing model content.
- [x] **B5.3** Return model-sized screenshot previews and original Artifacts
through Workstream A's Artifact service.
- Covers: BR-20, AC-6.
- Complete: large machine captures return a Chrome-scaled preview by default
and an optional original with an explicit `previewOf` relationship.
- [x] **B5.4** Update the universal skill with decision guidance grounded in
actual tool errors and state, not framework-specific prompts.
- Covers: FR-1, FR-8.
- Complete: the Agent-neutral skill now uses the same CLI workflow for
Agent-installed and product-bundled use, starts from current tab/page state, uses
fresh refs, verifies action results, and gives explicit recovery decisions
for structured evidence, hints, stable errors, reconnects, dialogs, and
local file results. Command guidance now reflects all-tab inventory, explicit
dialog handling, managed-only bulk cleanup, and actual local capture output.
It names no Agent framework in its runtime decisions and adds no adapter or
prompt-specific production behavior.
### B6. Reliability and regression gates
- [x] **B6.1** Add deterministic fixture tests for every B0.2 scenario and
every invalidation transition.
- Covers: AC-3, AC-5, AC-8.
- Complete: the local system-Chrome benchmark manifest is exhaustively tied
to all ten B0.2 scenarios and observes same-process frames and OOPIFs through
their actual session models. A canonical transition matrix exhaustively
covers all twelve Observation invalidation reasons and asserts the typed
owner-visible `stale_ref` result. Expiry now preserves its `expired` reason
for the owning context while remaining opaque across Workspace boundaries.
- [x] **B6.2** Add action verification tests for obstruction, checkbox/radio,
controlled inputs, contenteditable, focus loss, popup, and navigation.
- Covers: AC-8.
- Complete: isolated system-Chrome tests cover trusted controlled-input
events and rollback, canceled `beforeinput`, native email/number selection,
nested contenteditable replacement/append, blocked fields, special value
controls, native checkbox and radio click success plus prevented-default
mismatch, Shadow DOM readback, keyboard clear, press effects, upload
readback, real obstruction, and composite focus loss. Public tool-surface
tests verify typed obstruction failure, loader replacement cancellation,
and merged navigation, Document, dialog, and popup evidence for click and
press actions.
- [x] **B6.3** Add real-site canaries that report drift without making release
tests depend on third-party availability.
- Covers: FR-8.
- Complete: the existing real-site suite now preflights its third-party host,
marks transport availability failures explicitly, and treats missing
expected controls as drift instead of silently skipping behavior. An
Agent-neutral reporter writes a bounded versioned JSON report that
distinguishes `healthy`, `drift`, `unavailable`, and runner `error` states.
`npm run test:canary` is non-blocking, strict mode remains available, and
the deterministic release command does not execute the canary project.
- [x] **B6.4** Compare metrics against B0.4 and reject changes that increase
unsafe false-positive interactions or unbounded output.
- Covers: NFR-1, AC-8.
- Complete: `npm run test:capabilities` compares the live isolated-Chrome
report with the versioned B0.4 baseline using directional gates. Recall,
action-failure detection, and stale-ref detection may increase; the false
interactable rate may decrease. Corpus drift, unclassified refs, any metric
regression, output growth beyond the recorded maximum, or violation of the
protocol byte budget fails the gate.
### B7. Add adaptive page primitives from browser-use
- [x] **B7.1** Add bounded visible-text search and bounded CSS element
inspection as distinct page representations.
- Complete: `browser.search` returns at most 200 visible matches with nearby
context and viewport geometry. `browser.elements.find` returns at most 200
safe metadata records and only explicitly requested attributes. Both can
traverse open Shadow DOM, expose no DOM/CDP handles, and use the existing
`observation.read` capability.
- [x] **B7.2** Add first-class scrolling with fresh page state.
- Complete: `browser.scroll` supports page, selector, Observation ref, text,
relative distance, and start/end boundaries. It returns typed evidence and
a fresh Observation whose optional `page` block reports viewport/document
dimensions, scroll position, remaining pixels, and percentages.
- [x] **B7.3** Add native and ARIA dropdown inspection and selection.
- Complete: native selects enumerate bounded options, emit input/change, and
verify browser state. Custom controls use trusted ref clicks, refresh the
Observation after opening, and select only an option ref from that fresh
state. No framework-specific selectors or persistent handles were added.
- [x] **B7.4** Add screenshot annotations without changing the page.
- Complete: viewport capture can revalidate up to 200 refs from one
Observation, read live geometry, and draw numbered boxes on an unattached
canvas in a Chrome isolated world. Screenshot bytes are never evaluated in
the page's default world. Full-page/selector combinations fail explicitly,
output remains a protected Artifact, and tests assert the page DOM is unchanged.
- [x] **B7.5** Detect repeated identical actions against stagnant page state.
- Complete: action signatures and bounded page fingerprints remain internal.
Three repeated actions without a fingerprint change emit the existing
`watchdog.no_progress` event and `repeated_action` hint with reason
`stagnant_page`; action parameters, typed values, page text, and hashes are
not exposed.
- [x] **B7.6** Keep the additions Agent-neutral and backward compatible.
- Complete: all CLI commands use the Broker's canonical internal
implementations and schemas. The commands reuse existing capabilities and are additive
for protocol 1.0/1.1. Multi-action queues, LLM extraction, planning, and
memory remain Host-Agent responsibilities.
## Parallel Work Rules
- B1 can proceed while A1 extracts service modules, provided it targets the
agreed Observation interface rather than Commander handlers.
- B2 must use A2 Workspace target control and may not introduce another
persistent ref store.
- B3 depends on the per-target ordering boundary from A2.4 but its pure action
verification logic and fixtures can be developed earlier.
- B4 event producers can run in parallel with A4 transport delivery.
- B5 Artifact work integrates only after A5 defines storage and authorization.
- Any proposed public field or error change returns to A0 contract review and
receives a version/capability decision before implementation.
## Release Gate
Workstream B is complete only when AC-8 passes through the public tool surface,
all existing CLI/network tests remain green, stale refs fail closed across
targets and frames, action success includes evidence, output is bounded, and no
adopted browser-use behavior weakens Browser Pilot's ManagedTabSet,
Workspace/Lease isolation, target-control, Host policy, or dialog rules.
# Profile Context Routing Plan
Status: **Complete**
Target: Browser Pilot `v0.4.0`, protocol `1.3`
Source of truth: `docs/architecture/browser-pilot-platform-spec.md`
## Goal
Make one Browser Pilot connection correctly inventory and control ordinary tabs
from every live Chrome Profile context, while ensuring every new managed target
is created in an explicitly resolved Profile context. The design remains
Agent-neutral, extension-free, transient, and compatible with existing
single-Profile CLI workflows.
## Real Chrome Findings
The implementation contract is based on a read-only and transient-target probe
against a user-authorized Chrome endpoint:
- one browser-level CDP endpoint exposed two ordinary Profile contexts and all
eligible tabs from both contexts;
- ordinary Profile contexts were present on `TargetInfo.browserContextId` but
absent from `Target.getBrowserContexts`;
- `Target.createTarget({ browserContextId })` was not reliable for ordinary
Profile contexts and could return `Failed to find browser context`;
- dispatching `Target.createTarget` through a target session did not inherit the
target's Profile context;
- an isolated-world `window.open` with `userGesture: true` created a target in
the representative tab's exact Profile context;
- popup window features created a separate normal Chrome window, and subsequent
`Target.createTarget({ windowId })` calls stayed in that Profile context;
- a temporary `chrome://version` target could map a runtime context to Chrome
`Local State`, but doing that is user-visible and therefore is not permitted
during passive Profile discovery.
No production logic may assume behavior contradicted by these findings.
## Model
### BrowserEndpoint
The existing `BrowserInstance` represents one running Chromium browser endpoint:
product/channel, user-data root, CDP endpoint, process identity, connection
generation, and connection state. A Chrome Profile directory is not a separate
BrowserInstance when it shares that endpoint.
### ProfileContext
A `ProfileContext` is one live regular browser context observed on an endpoint.
It has:
- a public `profileContextId`, opaque to clients;
- an internal raw CDP browser-context ID, never exposed;
- the owning BrowserInstance and connection generation;
- a connection-scoped neutral label;
- connection-generation-scoped identity status plus optional verified Profile
name, account name/email, and Profile directory;
- current total and eligible tab counts;
- one or more bounded representative targets for routing and user recognition.
`profileContextId` is valid only for its browser connection generation. The
Broker retains stale public IDs long enough to return `profile_context_stale`,
but never routes them to a new raw context.
### Workspace Selection
`BrowserWorkspace.selectedProfileContextId` is transient Broker memory. It is
updated by explicit selection and by switching to a target whose context is
known. It is never written as a global default and is cleared logically by
browser reconnect because the old ID becomes stale.
### ManagedTabSet Binding
Each ManagedTabSet is permanently bound to at most one ProfileContext. A
Workspace may own multiple ManagedTabSets, one per Profile context used for
managed work. The first target binds an unbound set; opening managed work in a
different context creates another set. Workspace cleanup closes every managed
set and still leaves user tabs open.
### ControlledTarget Context
Every managed, managed-popup, and user target records a public
`profileContextId`. Popup adoption requires the popup context to equal its
managed ancestor's ManagedTabSet context.
## Protocol 1.2
Protocol 1.2 adds:
- `browser.profiles.list`;
- `browser.profiles.select`;
- optional `profileContextId` input on `browser.open`;
- `profileContextId` on every `browser.tabs.list` target;
- selected Profile context on Workspace results;
- Profile context binding on ManagedTabSet results;
- `profile_selection_required`, `profile_context_stale`, and
`profile_context_unavailable` stable errors.
`browser.profiles.list` is a Workspace-scoped read-only tool. It returns bounded
Profile summaries and representative eligible tabs. It never opens, navigates,
attaches to, or focuses a target merely to discover a display name.
## Protocol 1.3
Protocol 1.3 adds `browser.profiles.identify` as an explicit, mutating identity
operation. For each requested unidentified Profile it creates a temporary
visible `chrome://version` target in that exact context, reads `#profile_path`,
verifies that path as an immediate child of the connected browser's
`userDataRoot`, reads bounded metadata from Chrome `Local State`, then detaches
and proves the temporary target was closed. Failure to prove cleanup returns
`unknown_outcome`; Browser Pilot never silently leaves an identity page behind.
Results use `identityStatus: unidentified | verified | unavailable` plus
optional `profileName`, `accountName`, `accountEmail`, `profileDirectory`, and a
stable `identityErrorCode`. Identity is cached only for the current browser
connection generation. `refresh: true` explicitly re-probes. No directory
ordering, tab title, or raw context ID is treated as account identity.
Protocol 1.3 also renames browser candidate `profile` to `userDataRoot` and tab
inventory `active` to `selected`. The latter is the Lease's logical target, not
Chrome foreground focus. Protocol 1.2 clients retain their legacy fields.
`browser.profiles.select` accepts only a public `profileContextId`. Human CLI
selectors such as one-based index, neutral label, or verified Profile/account
name or email are resolved
client-side from a fresh list. The machine protocol never accepts an ambiguous
free-form selector. Selecting a Profile clears the Lease's logical selected-target
anchor without releasing control or changing Chrome focus, so the next new
managed target uses that explicit Workspace selection.
## Selection Algorithm
When `browser.open` needs a new target, resolve exactly one Profile context in
this order:
1. an explicit, current-generation `profileContextId`;
2. the current Lease's selected target context;
3. the Workspace's current, valid selection;
4. the only currently available Profile context;
5. otherwise fail with `profile_selection_required` before browser dispatch.
Listing or controlling an existing target never requires prior Profile
selection. Supplying `profileContextId` while navigating an existing target is
valid only when it equals that target's context; Browser Pilot never moves a
physical target between Profiles.
`profile_selection_required` contains only bounded Profile summaries. It is a
normal structured result for an Agent host to turn into a user question. Direct
CLI JSON mode exits without reading interactive stdin.
## Managed Target Creation
Creation is serialized by Workspace/Profile context and remains owned by the
managed-target janitor:
1. If the context's ManagedTabSet already has a live window ID, create
`about:blank` with that `windowId` and verify the returned target context.
2. For the first target, try browser-level creation with `newWindow: true` and
the raw browser-context ID. Verification is mandatory because Chrome support
varies by regular Profile context.
3. If Chrome rejects or misroutes that call, attach to a bounded representative
page target in the selected context, create an isolated world, and call
`window.open(<unique blank marker>, <unique window name>, <fixed popup window
features>)` with a debugger user gesture.
4. Identify exactly one new page target by the pre-dispatch target set, unique
URL marker, and Profile context. Prove ownership through either the expected
opener ID or exact readback of the per-command random `window.name`; Chrome
may normalize the reported opener across regular Profile tabs. Ambiguous or
unverified targets are closed and the operation fails.
5. Explicitly adopt the verified target into the janitor before registering it
publicly, so Broker crash cleanup still closes it.
6. Read and retain its window ID, bind the ManagedTabSet, register the opaque
target, then navigate through the normal target actor.
The fallback does not execute page-defined JavaScript, inject DOM content, read
page data, or navigate the representative user tab. It may cause ordinary
focus/blur effects inherent to Chrome opening a new window. Failure after any
possible target creation returns `unknown_outcome` unless the Broker proves all
candidate targets were closed.
## Compatibility
- Protocol 1.0/1.1 clients continue to list and control existing tabs.
- In a single available context, their existing `browser.open` behavior remains
automatic.
- In multiple contexts, a selected target remains an unambiguous anchor.
- With multiple contexts and no anchor, older clients receive a structured
failure and Browser Pilot never silently chooses the first context.
- New Profile fields on existing results are additive; raw CDP IDs remain
private.
- The current CLI negotiates protocol 1.3, while the Broker continues to accept
1.0 through 1.2.
## Delivery
- [x] P0: probe ordinary multi-Profile Chrome behavior.
- [x] P1: freeze model, protocol, selection, and creation rules.
- [x] P2: implement Profile context registry, target propagation, Workspace
selection, multiple ManagedTabSets, and janitor adoption.
- [x] P3: implement `bp profiles`, `bp profile`, and `bp open --profile`.
- [x] P4: update CLI integration guidance, skill, and release docs.
- [x] P5: add protocol, dual-Profile, concurrency, reconnect, cleanup, and
compatibility tests.
- [x] P6: run real Chrome acceptance and publish `v0.3.0-rc.2`.
## Acceptance
- [x] All eligible user tabs across live Profile contexts appear in one inventory.
- [x] Existing user tabs can be controlled without selecting a Profile first.
- [x] No multi-context open silently selects a first Profile.
- [x] Explicit, selected-target, Workspace, and single-context resolution follow the
specified order.
- [x] Every managed target and popup matches its ManagedTabSet Profile context.
- [x] Two Workspaces may select different contexts and create targets concurrently
without cross-routing.
- [x] Reconnect invalidates old Profile IDs, target mappings, frames, and refs.
- [x] Passive listing creates no target and never focuses or navigates a user tab.
- [x] Explicit identity verifies Profile paths, returns structured account-aware
fields, cleans temporary targets, caches only within one connection
generation, and never guesses unavailable metadata.
- [x] Janitor EOF/SIGKILL cleanup closes fallback-created managed windows but never
user tabs.
- [x] Protocol 1.1 clients retain existing-tab control and cannot open ambiguously.
- [x] Single-Profile CLI and fixture behavior remains compatible.
# Universal Agent CLI Integration Plan
Status: implementation and local release verification complete for the
CLI-only architecture; formal publication remains.
## Goal
Allow any shell-capable Agent to control the user's eligible Chrome tabs by
installing the Browser Pilot skill and invoking the same `bp` CLI, whether the
CLI was installed by the Agent or bundled by an Agent product.
```text
Agent -> skill -> existing shell tool -> bp CLI -> shared Broker -> Chrome
```
The integration must not require an extension, MCP, Native SDK, native Browser
Pilot tools, a persistent adapter, or Agent-specific runtime code.
## Decisions
- [x] Keep one public Agent interface: skill plus CLI.
- [x] Keep the Broker and CDP transport private.
- [x] Use stable `BROWSER_PILOT_CLIENT_KEY` values for independent Agents.
- [x] Preserve logical Agent state across short-lived CLI calls.
- [x] Expose all eligible managed and user-opened tabs.
- [x] Keep user tabs open during release and crash cleanup.
- [x] Leave task-intent permission policy to the Agent host.
- [x] Support product bundling by putting a pinned CLI on the Agent `PATH`.
- [x] Support Apple Silicon macOS, x64 Linux, and x64 Windows only.
## Workstream A: CLI Contract
- [x] Provide stable JSON success and error results.
- [x] Add stable error codes to parser, input, service, and browser failures.
- [x] Make `--client-key` discoverable in top-level help.
- [x] Add `bp status` with service, browser, selected state, command state, and
structured recovery.
- [x] Add bounded `bp commands`, `bp command`, and `bp cancel` commands.
- [x] Add global `--request-id` and deterministic idempotent recovery.
- [x] Add global `--timeout` and best-effort cancellation on process signals.
- [x] Add browser-visible `bp wait` conditions.
- [x] Add download list/export commands.
- [x] Add `BROWSER_PILOT_OUTPUT_DIR` and structured file metadata.
- [x] Keep `bp --help` as the canonical runtime command inventory.
## Workstream B: Shared State and Concurrency
- [x] Replace global active-tab/ref files with Broker-owned keyed state.
- [x] Isolate selected profile, target, frame, refs, auth, network rules,
downloads, and commands by Agent key.
- [x] Serialize commands per physical tab and allow independent tabs to run in
parallel.
- [x] Enforce one controlling Agent per physical user tab.
- [x] Return stable `target_busy` rather than stealing control.
- [x] Reclaim expired state and managed targets.
- [x] Preserve user tabs on Agent release and crash cleanup.
- [x] Invalidate browser-generation-scoped state after reconnect.
## Workstream C: Cross-Installation Broker Reuse
- [x] Negotiate compatibility by protocol rather than executable path.
- [x] Allow compatible global and product-bundled CLIs to share one Broker.
- [x] Keep one stable Agent principal across compatible CLI versions.
- [x] Use separate internal client sessions when compatible versions differ.
- [x] Let `bp disconnect` release the invoking Agent's namespace from any
compatible installation.
- [x] Require exact executable identity only for whole-Broker shutdown.
- [x] Refuse incompatible protocol clients without replacing the live Broker.
- [x] Support explicit incompatible isolation through `BROWSER_PILOT_HOME`.
## Workstream D: Product Embedding
- [x] Document one generic integration for Tenon, OpenClaw, and other Agents.
- [x] Require the complete skill and a normal command runner.
- [x] Require a pinned native CLI or npm CLI plus pinned Node runtime.
- [x] Put the bundled CLI directory on the Agent command environment's `PATH`.
- [x] Inject a stable client key per logical Agent.
- [x] Inject a task-owned absolute output directory.
- [x] Return files by absolute path for the host's existing image/file tools.
- [x] Avoid command-to-native-tool mapping and permanent tool-schema context.
- [x] Keep Agent-managed and bundled installs on the same behavior path.
## Workstream E: Skill and Documentation
- [x] Keep the main skill focused on the browser operating loop.
- [x] Split command, recovery, async, and embedding details into on-demand
references.
- [x] Remove internal state IDs and transport concepts from Agent guidance.
- [x] Document all eligible user tabs and explicit user-tab close behavior.
- [x] Document Chrome profile identity and routing.
- [x] Document tab-group control limitations without limiting grouped tabs.
- [x] Document local screenshot, PDF, download, and network-body results.
- [x] Document stable request recovery and uncertain mutation rules.
## Workstream F: Distribution
- [x] Keep CLI, plugin, skill compatibility, and marketplace versions synced
from the root package version.
- [x] Publish a release index binding CLI, skill range, protocol, platforms,
checksums, and tested version.
- [x] Exclude Intel Mac assets.
- [x] Verify global, local, and product-bundled npm use.
- [x] Verify native archives do not need system Node.
- [x] Bump the next release version and produce local release artifacts.
- [ ] Publish the npm package, plugin archive, native archives, checksums, and
release index.
## Acceptance Criteria
- [x] An Agent can install Browser Pilot itself and follow the skill.
- [x] A product can bundle Browser Pilot without adding native browser tools.
- [x] Two Agent products can share one Broker with isolated state.
- [x] A compatible bundled CLI can use a Broker started by a global CLI.
- [x] A compatible non-owner CLI can disconnect cleanly without stopping that
Broker.
- [x] Every eligible user-opened tab can be selected and controlled.
- [x] The same physical tab cannot be controlled by two Agents concurrently.
- [x] Browser authorization is requested only by explicit connection.
- [x] Stable request IDs prevent duplicate dispatch during recovery.
- [x] Screenshots, PDFs, downloads, and saved bodies are available as local
files with structured metadata.
- [x] Removed persistent adapter commands are absent from CLI help and release
packages.
- [x] Full unit, Playwright, distribution, standalone, and controlled real
Chrome release gates pass for the new version.
# Browser Pilot v0.3.0 Stabilization Plan
Status: **Complete**
Released baseline: `v0.3.0`, protocol `1.2`
## Goal
Promote protocol 1.2 to `v0.3.0` only after the published executable passes
the same multi-host workflows that Tenon, OpenClaw, Codex, and other Agent
products depend on. No Agent-specific behavior enters Browser Pilot.
## Release gate
- [x] Add a black-box host integration runner that launches two independent
stdio bridge clients against an exact executable command.
- [x] Verify separate Connections, Workspaces, Leases, opaque target IDs, one
shared Broker, and one shared in-flight browser connection.
- [x] Verify eligible user-tab exclusivity, `target_busy`, explicit release,
and handoff without navigation, editing, or closure.
- [x] Verify concurrent CLI use and separate managed inventories.
- [x] Verify screenshot conversion to native image content and PDF/download
export to host-owned absolute paths.
- [x] Verify event replay until explicit acknowledgement.
- [x] Verify Workspace cleanup closes only managed targets and preserves the
initial user-tab inventory.
- [x] Verify isolated browser loss/restoration events and invalidate old
Profile and target identities after reconnect.
- [x] Replace invalid per-session download redirection with Chrome-default
downloads, controlled Page-event ownership, browser-level completion paths,
and copy-only Artifacts. Verify unowned downloads are ignored and user source
files survive Artifact and Workspace cleanup.
- [x] Pass TypeScript, 277 Node, 109 Playwright, 13 stdio conformance, three
distribution-mode, and 13 host integration checks on the candidate source.
- [x] Publish `v0.3.0-rc.3` through trusted publishing and pass all 13 host
integration checks against the installed standalone release executable.
- [x] Run one controlled user-Chrome host acceptance with explicit Profile
choices and a single authorization attempt.
- [x] Publish `v0.3.0-rc.4` with Broker-backed CLI browser status and verify
that passive discovery reports the live authorized connection.
- [x] Publish `v0.3.0-rc.5` and verify text-targeted scrolling plus annotated
capture on GitHub. Managed cleanup preserved all user tabs but exposed a
transient closing-target inventory race.
- [x] Publish `v0.3.0-rc.6` with generation-scoped managed-target tombstones.
The installed standalone returned `closed: 1, remaining: 6` immediately after
managed cleanup and left all six user tabs available, with no managed, busy,
controlled, or automatically active target.
- [x] Soak real Agent tasks through the installed candidates across multiple
Chrome Profiles: public GitHub reading, search, precise scrolling, screenshot
annotation, PDF export, a disposable form with verified input and submission,
and managed-only cleanup. User tabs and exported files were preserved.
- [x] Tag `v0.3.0`, publish a non-prerelease GitHub Release and npm
`latest=0.3.0`, and retain `next=0.3.0-rc.6` plus the local RC.6 standalone
as rollback paths. The installed stable executable connected through one
authorization request and repeated managed open/cleanup with
`closed: 1, remaining: 6`; all six user tabs remained available.
## Stable decision
The first new host gate exposed a deeper CDP constraint: target-session calls to
`Page.setDownloadBehavior` still share download behavior across a Chrome
Profile. One host could therefore redirect another host's download, including
an uncontrolled user tab. `v0.3.0-rc.2` must not be promoted. RC.3 preserves
Chrome's default download destination and creates an Artifact only after a
session-scoped Page event proves ownership and a browser-level completion event
provides the final path.
RC.4 through RC.6 then closed the remaining real-browser gaps in passive
connection status, text-targeted scroll verification, restrictive-CSP screenshot
annotation, and asynchronous managed-target closure. The published RC.6
standalone now satisfies every pre-stable source, distribution, Host, and live
Chrome gate above. The identical stable runtime passed the complete source and
distribution gates, published successfully on all three platforms, and passed
the installed user-Chrome smoke check.

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display