feat!: harden raw browser control and packaging
Testing / remote-protocol-compat (0.9.3) (push) Successful in 40s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 38s
Testing / test (push) Failing after 1m3s
Package Extension / package-extension (push) Successful in 29s
Build & Publish Package / publish (push) Successful in 33s

- Add safe-by-default policy gates for raw command surfaces: command, script, and serve-http /command.

- Require explicit opt-ins for page reads, browser control, and high-risk commands such as dom.eval, storage.*, and screenshots.

- Remove all cookies support from CLI, SDK, extension commands, permissions, constants, docs, and tests.

- Add diagnostic, events, watch, workspace, remote, raw command, script, HTTP gateway, tree-view, session import/export, and extension info/capability commands.

- Add Chrome Web Store packaging that strips manifest.key while keeping local packages with a stable native-messaging extension ID.

- Bump browser-cli and extension version to 0.14.1 and cover the new behavior with pytest and extension packaging tests.

BREAKING CHANGE: cookies commands and the b.cookies SDK namespace have been removed; generic raw command execution now blocks non-safe commands unless explicitly allowed.
This commit is contained in:
2026-06-14 14:33:15 +02:00
parent 3e3b8d529c
commit 5cec57e06d
43 changed files with 1184 additions and 375 deletions
+68
View File
@@ -0,0 +1,68 @@
from __future__ import annotations
import json
from pathlib import Path
import click
from rich.console import Console
from browser_cli.command_security import CommandPolicy, assert_command_allowed
from browser_cli.commands import client_from_ctx, handle_errors
console = Console()
def _load_steps(path: Path):
text = path.read_text(encoding="utf-8")
if path.suffix.lower() in {".yaml", ".yml"}:
try:
import yaml # type: ignore
except Exception as exc:
raise click.ClickException("YAML scripts require PyYAML; use JSON or install PyYAML") from exc
return yaml.safe_load(text)
return json.loads(text)
def _parse_step(step):
if isinstance(step, str):
return step, {}
if isinstance(step, dict):
if "command" in step:
return step["command"], step.get("args") or {}
if len(step) == 1:
command, args = next(iter(step.items()))
return command, args or {}
raise click.ClickException(f"Invalid script step: {step!r}")
@click.command("script")
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
@click.option("--json", "json_output", is_flag=True, help="Print all step results as JSON")
@click.option("--continue-on-error", is_flag=True, help="Continue after failed steps")
@click.option("--allow-read-page", is_flag=True, help="Allow page-content read commands such as extract.* and dom.text")
@click.option("--allow-control", is_flag=True, help="Allow browser-control commands such as nav.*, tabs.close, dom.click")
@click.option("--allow-dangerous", is_flag=True, help="Allow high-risk commands such as dom.eval, storage.*, screenshots")
@handle_errors
def cmd_script(file: Path, json_output: bool, continue_on_error: bool, allow_read_page: bool, allow_control: bool, allow_dangerous: bool):
"""Run a JSON/YAML batch script of browser-cli wire commands."""
steps = _load_steps(file)
if not isinstance(steps, list):
raise click.ClickException("Script root must be a list")
client = client_from_ctx()
policy = CommandPolicy(allow_read_page=allow_read_page, allow_control=allow_control, allow_dangerous=allow_dangerous)
results = []
for index, step in enumerate(steps, start=1):
command, args = _parse_step(step)
try:
assert_command_allowed(command, policy)
result = client.command(command, args)
results.append({"index": index, "command": command, "ok": True, "result": result})
if not json_output:
console.print(f"[green]✓[/green] {index}: {command}")
except Exception as exc:
results.append({"index": index, "command": command, "ok": False, "error": str(exc)})
if not continue_on_error:
if json_output:
click.echo(json.dumps(results, indent=2, default=str))
raise
if not json_output:
console.print(f"[red]✗[/red] {index}: {command}: {exc}")
if json_output:
click.echo(json.dumps(results, indent=2, default=str))