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
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:
@@ -4,7 +4,7 @@ Each namespace groups related browser commands under a short accessor on the
|
||||
client (``b.tabs``, ``b.dom``, ``b.session``, ...), mirroring the command groups
|
||||
in the browser extension.
|
||||
"""
|
||||
from browser_cli.sdk.browser_data import CookiesNS, StorageNS
|
||||
from browser_cli.sdk.browser_data import StorageNS
|
||||
from browser_cli.sdk.decorators import DecoratorsNS
|
||||
from browser_cli.sdk.dom import DomNS, ExtractNS, PageNS
|
||||
from browser_cli.sdk.extension import ExtensionNS
|
||||
@@ -24,7 +24,6 @@ NAMESPACE_SPECS = (
|
||||
("extract", ExtractNS),
|
||||
("page", PageNS),
|
||||
("storage", StorageNS),
|
||||
("cookies", CookiesNS),
|
||||
("session", SessionNS),
|
||||
("perf", PerfNS),
|
||||
("extension", ExtensionNS),
|
||||
@@ -40,7 +39,6 @@ __all__ = [
|
||||
"ExtractNS",
|
||||
"PageNS",
|
||||
"StorageNS",
|
||||
"CookiesNS",
|
||||
"SessionNS",
|
||||
"PerfNS",
|
||||
"ExtensionNS",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Storage and cookies namespaces: ``b.storage.*``, ``b.cookies.*``."""
|
||||
"""Storage namespace: ``b.storage.*``."""
|
||||
from __future__ import annotations
|
||||
|
||||
from browser_cli.sdk.base import Namespace, sdk_command
|
||||
@@ -35,51 +35,3 @@ class StorageNS(Namespace):
|
||||
tab_id: int | None = None,
|
||||
) -> None:
|
||||
"""Set a localStorage/sessionStorage entry."""
|
||||
|
||||
class CookiesNS(Namespace):
|
||||
"""List, get, and set cookies."""
|
||||
|
||||
@sdk_command("cookies.list", lambda self, *, url=None, domain=None, name=None: {
|
||||
"url": url,
|
||||
"domain": domain,
|
||||
"name": name,
|
||||
}, default=[])
|
||||
def list(
|
||||
self,
|
||||
*,
|
||||
url: str | None = None,
|
||||
domain: str | None = None,
|
||||
name: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""List cookies, optionally filtered by url, domain, or name."""
|
||||
|
||||
@sdk_command("cookies.get", lambda self, url, name: {"url": url, "name": name})
|
||||
def get(self, url: str, name: str) -> dict | None:
|
||||
"""Get a single cookie by url and name."""
|
||||
|
||||
@sdk_command("cookies.set", lambda self, url, name, value, *, domain=None, path=None, secure=None,
|
||||
http_only=None, expiration_date=None, same_site=None: {
|
||||
"url": url,
|
||||
"name": name,
|
||||
"value": value,
|
||||
"domain": domain,
|
||||
"path": path,
|
||||
"secure": secure,
|
||||
"httpOnly": http_only,
|
||||
"expirationDate": expiration_date,
|
||||
"sameSite": same_site,
|
||||
})
|
||||
def set(
|
||||
self,
|
||||
url: str,
|
||||
name: str,
|
||||
value: str,
|
||||
*,
|
||||
domain: str | None = None,
|
||||
path: str | None = None,
|
||||
secure: bool | None = None,
|
||||
http_only: bool | None = None,
|
||||
expiration_date: float | None = None,
|
||||
same_site: str | None = None,
|
||||
) -> dict:
|
||||
"""Set a cookie. Returns the created cookie dict."""
|
||||
|
||||
@@ -6,6 +6,14 @@ from browser_cli.sdk.base import Namespace, sdk_command
|
||||
class ExtensionNS(Namespace):
|
||||
"""Control the browser-cli extension itself."""
|
||||
|
||||
@sdk_command("extension.info", default={})
|
||||
def info(self) -> dict:
|
||||
"""Return extension version, runtime metadata, and capabilities."""
|
||||
|
||||
@sdk_command("extension.capabilities", default=[])
|
||||
def capabilities(self) -> list[str]:
|
||||
"""Return feature capability strings advertised by the extension."""
|
||||
|
||||
@sdk_command("extension.reload")
|
||||
def reload(self) -> None:
|
||||
"""Reload the browser-cli extension service worker.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -48,6 +48,14 @@ class SessionNS(Namespace):
|
||||
def diff(self, name_a: str, name_b: str) -> dict:
|
||||
"""Diff two saved sessions."""
|
||||
|
||||
@sdk_command("session.export", lambda self, name=None: {"name": name}, default={})
|
||||
def export(self, name: str | None = None) -> dict:
|
||||
"""Export one saved session, or all sessions when *name* is omitted."""
|
||||
|
||||
@sdk_command("session.import", lambda self, name, session, overwrite=False: {"name": name, "session": session, "overwrite": overwrite}, default={})
|
||||
def import_(self, name: str, session: dict, *, overwrite: bool = False) -> dict:
|
||||
"""Import a saved session payload under *name*."""
|
||||
|
||||
def list(self) -> list[dict]:
|
||||
"""Return saved sessions.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user