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
+43 -3
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from browser_cli.models import Tab
from browser_cli.sdk.base import Namespace, sdk_command
def _open_args(self, url, *, background=False, focus=False, window=None, group=None):
def _open_args(self, url, *, background=False, focus=False, window=None, group=None, **_ignored):
return {"url": url, "background": background or not focus, "focus": focus, "window": window, "group": group}
def _tab_args(self, tab_id=None):
@@ -13,7 +13,6 @@ def _tab_args(self, tab_id=None):
class NavigationNS(Namespace):
"""Open URLs, navigate history, and focus tabs."""
@sdk_command("navigate.open", _open_args)
def open(
self,
url: str,
@@ -22,8 +21,23 @@ class NavigationNS(Namespace):
focus: bool = False,
window: str | None = None,
group: str | None = None,
reuse: bool = False,
reuse_domain: bool = False,
reuse_title: str | None = None,
) -> None:
"""Open *url* in a new tab without stealing OS focus by default."""
"""Open *url* in a new tab without stealing OS focus by default.
``reuse``/``reuse_domain``/``reuse_title`` navigate an existing matching tab
instead of creating a new one.
"""
tab = self._reuse_target(url, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
if tab is not None:
self.to(tab.id, url)
if focus:
self._c.tabs.activate(tab.id)
return None
self.command("navigate.open", _open_args(self, url, background=background, focus=focus, window=window, group=group))
return None
def open_wait(
self,
@@ -34,8 +48,17 @@ class NavigationNS(Namespace):
focus: bool = False,
window: str | None = None,
group: str | None = None,
reuse: bool = False,
reuse_domain: bool = False,
reuse_title: str | None = None,
) -> Tab:
"""Open *url* in a new tab and block until fully loaded. Returns the Tab."""
tab = self._reuse_target(url, reuse=reuse, reuse_domain=reuse_domain, reuse_title=reuse_title)
if tab is not None:
self.to(tab.id, url)
if focus:
self._c.tabs.activate(tab.id)
return self._c.tabs.wait_for_load(tab.id, timeout=timeout)
return self.require_tab(
self.command("navigate.open_wait", {
"url": url, "timeout": int(timeout * 1000),
@@ -68,6 +91,23 @@ class NavigationNS(Namespace):
def to(self, tab_id: int, url: str) -> None:
"""Navigate a specific tab to *url* in place."""
def _reuse_target(self, url: str, *, reuse: bool, reuse_domain: bool, reuse_title: str | None):
if not (reuse or reuse_domain or reuse_title):
return None
from urllib.parse import urlparse
wanted = urlparse(url)
wanted_host = wanted.netloc.lower()
for tab in self._c.tabs.list():
tab_url = tab.url or ""
parsed = urlparse(tab_url)
if reuse and tab_url == url:
return tab
if reuse_domain and wanted_host and parsed.netloc.lower() == wanted_host:
return tab
if reuse_title and reuse_title.lower() in (tab.title or "").lower():
return tab
return None
def search(
self, engine: str, query: str, *,
background: bool = False, focus: bool = False, window: str | None = None, group: str | None = None,