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:
@@ -0,0 +1,105 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
import pytest
|
||||
|
||||
from browser_cli import BrowserCLI
|
||||
from browser_cli.cli import main
|
||||
from browser_cli.command_security import CommandPolicy, assert_command_allowed, command_category
|
||||
|
||||
def test_extension_info_cli_renders_capabilities():
|
||||
with patch("browser_cli.send_command", return_value={"version": "1.2.3", "capabilities": ["extension.info"]}):
|
||||
result = CliRunner().invoke(main, ["extension", "info"])
|
||||
assert result.exit_code == 0
|
||||
assert "1.2.3" in result.output
|
||||
assert "extension.info" in result.output
|
||||
|
||||
def test_script_runs_raw_commands(tmp_path: Path):
|
||||
script = tmp_path / "workflow.json"
|
||||
script.write_text(json.dumps([{"tabs.count": {"pattern": "example.com"}}]), encoding="utf-8")
|
||||
with patch("browser_cli.send_command", return_value={"count": 2}) as send_command:
|
||||
result = CliRunner().invoke(main, ["script", str(script), "--json"])
|
||||
assert result.exit_code == 0
|
||||
assert "tabs.count" in result.output
|
||||
send_command.assert_called_once_with("tabs.count", {"pattern": "example.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_session_export_cli_prints_json():
|
||||
with patch("browser_cli.send_command", return_value={"name": "work", "session": {"tabs": ["https://example.com"]}}):
|
||||
result = CliRunner().invoke(main, ["session", "export", "work"])
|
||||
assert result.exit_code == 0
|
||||
assert '"name": "work"' in result.output
|
||||
|
||||
def test_nav_open_reuse_navigates_existing_tab_instead_of_opening_new():
|
||||
calls = []
|
||||
|
||||
def sender(command, args=None, **kwargs):
|
||||
calls.append((command, args))
|
||||
if command == "tabs.list":
|
||||
return [{"id": 7, "windowId": 1, "active": False, "muted": False, "title": "Example", "url": "https://example.com"}]
|
||||
return {}
|
||||
|
||||
BrowserCLI(browser="testing", _command_sender=sender).nav.open("https://example.com", reuse=True)
|
||||
assert calls == [
|
||||
("tabs.list", {}),
|
||||
("navigate.to", {"tabId": 7, "url": "https://example.com"}),
|
||||
]
|
||||
|
||||
def test_tabs_tree_command_available():
|
||||
with patch("browser_cli.send_command", return_value=[]):
|
||||
result = CliRunner().invoke(main, ["tabs", "tree"])
|
||||
assert result.exit_code == 0
|
||||
assert "Tabs" in result.output
|
||||
|
||||
def test_doctor_command_reports_connection_failure_cleanly():
|
||||
with patch("browser_cli.commands.doctor.active_browser_targets", return_value=[]), \
|
||||
patch("browser_cli.send_command", side_effect=RuntimeError("no browser")):
|
||||
result = CliRunner().invoke(main, ["doctor"])
|
||||
assert result.exit_code == 1
|
||||
assert "Connection" in result.output
|
||||
|
||||
def test_serve_http_no_auth_rejected_on_public_host():
|
||||
result = CliRunner().invoke(main, ["serve-http", "--host", "0.0.0.0", "--no-auth"])
|
||||
assert result.exit_code != 0
|
||||
assert "--no-auth is only allowed on loopback" in result.output
|
||||
|
||||
def test_raw_command_blocks_dangerous_by_default():
|
||||
result = CliRunner().invoke(main, ["command", "dom.eval", '{"code":"document.title"}'])
|
||||
assert result.exit_code != 0
|
||||
assert "blocked by default" in result.output
|
||||
|
||||
def test_raw_command_allows_dangerous_with_explicit_flag():
|
||||
with patch("browser_cli.send_command", return_value="Example") as send_command:
|
||||
result = CliRunner().invoke(main, ["command", "--allow-dangerous", "dom.eval", '{"code":"document.title"}'])
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once_with("dom.eval", {"code": "document.title"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_script_blocks_control_without_explicit_flag(tmp_path: Path):
|
||||
script = tmp_path / "workflow.json"
|
||||
script.write_text(json.dumps([{"navigate.open": {"url": "https://example.com"}}]), encoding="utf-8")
|
||||
result = CliRunner().invoke(main, ["script", str(script), "--json"])
|
||||
assert result.exit_code != 0
|
||||
assert "blocked by default" in result.output
|
||||
|
||||
def test_script_allows_control_with_explicit_flag(tmp_path: Path):
|
||||
script = tmp_path / "workflow.json"
|
||||
script.write_text(json.dumps([{"navigate.open": {"url": "https://example.com"}}]), encoding="utf-8")
|
||||
with patch("browser_cli.send_command", return_value={}) as send_command:
|
||||
result = CliRunner().invoke(main, ["script", str(script), "--json", "--allow-control"])
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once_with("navigate.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_command_policy_categories_and_flags():
|
||||
assert command_category("tabs.list") == "safe"
|
||||
assert command_category("extract.text") == "read-page"
|
||||
assert command_category("dom.click") == "control"
|
||||
assert command_category("storage.get") == "dangerous"
|
||||
assert_command_allowed("tabs.list", CommandPolicy())
|
||||
with pytest.raises(PermissionError):
|
||||
assert_command_allowed("extract.text", CommandPolicy())
|
||||
assert_command_allowed("extract.text", CommandPolicy(allow_read_page=True))
|
||||
with pytest.raises(PermissionError):
|
||||
assert_command_allowed("storage.get", CommandPolicy(allow_read_page=True, allow_control=True))
|
||||
assert_command_allowed("storage.get", CommandPolicy(allow_dangerous=True))
|
||||
Reference in New Issue
Block a user