fix: restore Python 3.10/3.11 support and report MCP version
Testing / test (3.10) (push) Successful in 48s
Testing / test (3.12) (push) Successful in 47s
Testing / test (3.14) (push) Successful in 1m7s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 6m59s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 6m47s
Testing / test (3.15) (push) Successful in 6m20s
Testing / test (3.10) (push) Successful in 48s
Testing / test (3.12) (push) Successful in 47s
Testing / test (3.14) (push) Successful in 1m7s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 6m59s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 6m47s
Testing / test (3.15) (push) Successful in 6m20s
- Drop backslash from f-string expression in markdown/html.py - That syntax needs 3.12+ (PEP 701) and broke import on 3.10/3.11 - Add Python matrix 3.10/3.12/3.14/3.15 to the Gitea testing workflow - Pass project_version() to MCPServer; serverInfo.version was empty - Cover the reported version with an in-memory MCP client test - Swap deprecated click.get_text_stream for sys.stdin.isatty() - Add explicit encoding to native host shim and manifest writes - Add explicit encoding to the registry lock file open - Add explicit encoding to every read_text/write_text call in tests - Bump version to 0.16.10 in pyproject.toml and extension/manifest.json - Move .python-version to 3.15 and refresh uv.lock Extension code is unchanged; only the manifest version string moved. Verified with 778 tests on 3.10 through 3.15, plus a strict run using -X warn_default_encoding with EncodingWarning and DeprecationWarning as errors.
This commit is contained in:
@@ -8,6 +8,14 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.12"
|
||||
- "3.14"
|
||||
- "3.15"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -17,10 +25,10 @@ jobs:
|
||||
uses: astral-sh/setup-uv@v5
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync --group dev --managed-python
|
||||
run: uv sync --group dev --managed-python --python ${{ matrix.python-version }}
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest
|
||||
run: uv run --python ${{ matrix.python-version }} pytest
|
||||
|
||||
remote-protocol-compat:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.13
|
||||
3.15
|
||||
|
||||
@@ -123,7 +123,7 @@ def cmd_auth_policy(ctx, identifier, keys_file, server_default, safe_only, allow
|
||||
console.print("[red]Choose exactly one policy mode:[/red] --server-default, --safe, or one/more --allow-* flags")
|
||||
sys.exit(1)
|
||||
|
||||
is_interactive = click.get_text_stream("stdin").isatty()
|
||||
is_interactive = bool(sys.stdin is not None and sys.stdin.isatty())
|
||||
current_categories = None
|
||||
if not identifier:
|
||||
if not is_interactive:
|
||||
|
||||
@@ -38,7 +38,7 @@ def write_native_host_exe(path: Path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
else:
|
||||
path.write_text(f'#!{sys.executable}\nfrom browser_cli.native.host import main\nmain()\n')
|
||||
path.write_text(f'#!{sys.executable}\nfrom browser_cli.native.host import main\nmain()\n', encoding="utf-8")
|
||||
path.chmod(path.stat().st_mode | 0o111)
|
||||
|
||||
def _windows_registry_views():
|
||||
@@ -156,7 +156,7 @@ def _install_manifest(browser: str, host_exe: Path, manifest: dict) -> list:
|
||||
try:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = directory / f"{NATIVE_HOST_NAME}.json"
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2))
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
installed.append(manifest_path)
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Could not write to {directory}: {e}[/yellow]")
|
||||
|
||||
@@ -114,7 +114,8 @@ def _inline_text(node):
|
||||
return f"[{text or href}]({href})" if href else text
|
||||
if tag == "code":
|
||||
text = _normalize_inline("".join(_inline_text(child) for child in node.children))
|
||||
return f"`{text.replace('`', r'\\`')}`" if text else ""
|
||||
escaped = text.replace("`", r"\\`")
|
||||
return f"`{escaped}`" if text else ""
|
||||
if tag in {"strong", "b"}:
|
||||
text = _normalize_inline("".join(_inline_text(child) for child in node.children))
|
||||
return f"**{text}**" if text else ""
|
||||
|
||||
@@ -18,6 +18,7 @@ from browser_cli.mcp.naming import resolve_tool_prefix, tool_name
|
||||
from browser_cli.mcp.serialization import structured
|
||||
from browser_cli.mcp.targets import resolve_tab_id
|
||||
from browser_cli.url_match import find_reusable_tab, needs_navigation, tab_id as read_tab_id, tab_url
|
||||
from browser_cli.version_manager import project_version
|
||||
|
||||
ClientFactory = Callable[..., BrowserCLI]
|
||||
|
||||
@@ -70,6 +71,7 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
||||
"browser-cli",
|
||||
description="Control a real running browser through the browser-cli SDK.",
|
||||
instructions=_SERVER_INSTRUCTIONS,
|
||||
version=project_version(),
|
||||
)
|
||||
|
||||
@mcp.tool(name=tool_name("tabs_list", prefix))
|
||||
|
||||
@@ -15,7 +15,7 @@ def _file_lock(path: Path) -> Iterator[None]:
|
||||
"""Best-effort cross-process lock for registry read/modify/write updates."""
|
||||
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
lock_path = path.with_suffix(path.suffix + ".lock")
|
||||
with lock_path.open("a+") as lock_file:
|
||||
with lock_path.open("a+", encoding="utf-8") as lock_file:
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import msvcrt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "0.16.9",
|
||||
"version": "0.16.10",
|
||||
"description": "Control your browser from the terminal or Python SDK",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "real-browser-cli"
|
||||
version = "0.16.9"
|
||||
version = "0.16.10"
|
||||
description = "Control your real running browser from the terminal or Python SDK"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ class TestAuthorizedKeys:
|
||||
|
||||
def test_ignores_comment_lines(self, tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
path.write_text("# this is a comment\n")
|
||||
path.write_text("# this is a comment\n", encoding="utf-8")
|
||||
assert load_authorized_keys(path) == []
|
||||
|
||||
def test_returns_empty_for_missing_file(self, tmp_path):
|
||||
|
||||
+1
-1
@@ -208,7 +208,7 @@ def test_write_native_host_exe_unix(tmp_path):
|
||||
write_native_host_exe(host)
|
||||
|
||||
assert host.exists()
|
||||
content = host.read_text()
|
||||
content = host.read_text(encoding="utf-8")
|
||||
assert content.startswith(f"#!{sys.executable}")
|
||||
assert "from browser_cli.native.host import main" in content
|
||||
assert host.stat().st_mode & 0o111 # executable bit set
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_resolve_socket_uses_only_active_registry_entry(monkeypatch, tmp_path):
|
||||
socket_path = tmp_path / "browser.sock"
|
||||
listener = _listening_unix_socket(socket_path)
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"abc-uuid": str(socket_path)}))
|
||||
registry_path.write_text(json.dumps({"abc-uuid": str(socket_path)}), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_resolve_socket_raises_when_multiple_active_entries(monkeypatch, tmp_pat
|
||||
first_listener = _listening_unix_socket(first_socket)
|
||||
second_listener = _listening_unix_socket(second_socket)
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"uuid-1": str(first_socket), "uuid-2": str(second_socket)}))
|
||||
registry_path.write_text(json.dumps({"uuid-1": str(first_socket), "uuid-2": str(second_socket)}), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_active_browser_targets_filters_stale_entries(monkeypatch, tmp_path):
|
||||
stale_listener = _listening_unix_socket(stale_socket)
|
||||
stale_listener.close()
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"work": str(active_socket), "default": str(stale_socket)}))
|
||||
registry_path.write_text(json.dumps({"work": str(active_socket), "default": str(stale_socket)}), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
|
||||
|
||||
@@ -102,7 +102,7 @@ def test_active_browser_targets_filters_stale_entries(monkeypatch, tmp_path):
|
||||
|
||||
def test_active_browser_targets_keeps_windows_registry_entries(monkeypatch, tmp_path):
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}))
|
||||
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
|
||||
monkeypatch.setattr("browser_cli.client.targets.is_windows", lambda: True)
|
||||
@@ -174,7 +174,7 @@ def test_send_command_prefers_active_local_profile_over_saved_remote_alias(monke
|
||||
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
|
||||
socket_path = tmp_path / "work.sock"
|
||||
socket_path.write_text("")
|
||||
socket_path.write_text("", encoding="utf-8")
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"work": str(socket_path)}), encoding="utf-8")
|
||||
monkeypatch.setattr("browser_cli.client.targets.REGISTRY_PATH", registry_path)
|
||||
|
||||
@@ -8,14 +8,14 @@ def read_built_background() -> str:
|
||||
background_path = ROOT / "extension" / "background.js"
|
||||
if not background_path.exists():
|
||||
pytest.skip("extension/background.js is a generated build artifact; run npm run build:extension")
|
||||
return background_path.read_text()
|
||||
return background_path.read_text(encoding="utf-8")
|
||||
|
||||
def test_extension_retries_error_page_script_injection_before_failing():
|
||||
# core.ts was split into a core/ subfolder during the structure refactor:
|
||||
# the URL/error classifiers live in core/errors.ts and the executeScript
|
||||
# retry wrapper (which calls isTransientScriptError) lives in core/scripting.ts.
|
||||
errors = (ROOT / "extension" / "src" / "core" / "errors.ts").read_text()
|
||||
scripting = (ROOT / "extension" / "src" / "core" / "scripting.ts").read_text()
|
||||
errors = (ROOT / "extension" / "src" / "core" / "errors.ts").read_text(encoding="utf-8")
|
||||
scripting = (ROOT / "extension" / "src" / "core" / "scripting.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "isBrowserErrorUrl" in errors
|
||||
assert "isErrorPageScriptError" in errors
|
||||
@@ -27,7 +27,7 @@ def test_extension_retries_error_page_script_injection_before_failing():
|
||||
assert "isTransientScriptError(e)" in scripting
|
||||
|
||||
def test_read_only_dom_commands_have_error_page_fallbacks():
|
||||
dom = (ROOT / "extension" / "src" / "commands" / "dom.ts").read_text()
|
||||
dom = (ROOT / "extension" / "src" / "commands" / "dom.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "fallbackForErrorPageDomOp" in dom
|
||||
assert 'case "domExists":' in dom
|
||||
@@ -40,8 +40,8 @@ def test_read_only_dom_commands_have_error_page_fallbacks():
|
||||
def test_navigation_and_tabs_report_browser_error_pages():
|
||||
# tabs.watch_url (which reports tab error pages) moved into the read-only
|
||||
# TabsQueryCommands class in tabs-query.ts during the structure refactor.
|
||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs-query.ts").read_text()
|
||||
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text()
|
||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs-query.ts").read_text(encoding="utf-8")
|
||||
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "lastUrl" in tabs
|
||||
assert "lastStatus" in tabs
|
||||
@@ -56,10 +56,10 @@ def test_large_extension_operations_yield_between_batches():
|
||||
# batch loop (cancel-check -> batch call -> progress -> yield) was then
|
||||
# centralized into the processInBatches() helper in core/throttle.ts, so the
|
||||
# command modules call that instead of inlining the loop.
|
||||
core = (ROOT / "extension" / "src" / "core" / "throttle.ts").read_text()
|
||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
|
||||
groups = (ROOT / "extension" / "src" / "commands" / "groups.ts").read_text()
|
||||
session = (ROOT / "extension" / "src" / "commands" / "session.ts").read_text()
|
||||
core = (ROOT / "extension" / "src" / "core" / "throttle.ts").read_text(encoding="utf-8")
|
||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text(encoding="utf-8")
|
||||
groups = (ROOT / "extension" / "src" / "commands" / "groups.ts").read_text(encoding="utf-8")
|
||||
session = (ROOT / "extension" / "src" / "commands" / "session.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "yieldForLargeOperation" in core
|
||||
assert "getLargeOperationThrottle" in core
|
||||
@@ -105,10 +105,10 @@ def test_large_extension_operations_yield_between_batches():
|
||||
# and the command registry/groups during the class-based refactor. The
|
||||
# behavior is identical; assert the defining patterns still exist in their
|
||||
# new homes.
|
||||
jobs_module = (ROOT / "extension" / "src" / "classes" / "JobManager.ts").read_text()
|
||||
connection = (ROOT / "extension" / "src" / "classes" / "NativeConnection.ts").read_text()
|
||||
perf = (ROOT / "extension" / "src" / "commands" / "perf.ts").read_text()
|
||||
tabs_module = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
|
||||
jobs_module = (ROOT / "extension" / "src" / "classes" / "JobManager.ts").read_text(encoding="utf-8")
|
||||
connection = (ROOT / "extension" / "src" / "classes" / "NativeConnection.ts").read_text(encoding="utf-8")
|
||||
perf = (ROOT / "extension" / "src" / "commands" / "perf.ts").read_text(encoding="utf-8")
|
||||
tabs_module = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "background: true" in tabs_module # background command flag (was BACKGROUND_COMMANDS)
|
||||
assert "persistJobs" in jobs_module
|
||||
@@ -120,8 +120,8 @@ def test_large_extension_operations_yield_between_batches():
|
||||
assert "__background" in connection
|
||||
|
||||
def test_tab_activation_open_and_merge_do_not_steal_audible_video_window():
|
||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
|
||||
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text()
|
||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text(encoding="utf-8")
|
||||
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "await chrome.windows.update(tab.windowId, { focused: true });" not in tabs
|
||||
assert "active: Boolean(focus) && !background" in navigation
|
||||
@@ -151,8 +151,8 @@ def test_session_autosave_is_debounced_and_non_overlapping():
|
||||
# AutoSaveManager (autosave.ts) during the structure refactor; the shared
|
||||
# snapshot/signature helpers live in session-snapshot.ts. Behavior is
|
||||
# identical — the defining patterns just live in their new homes.
|
||||
autosave = (ROOT / "extension" / "src" / "commands" / "autosave.ts").read_text()
|
||||
snapshot = (ROOT / "extension" / "src" / "commands" / "session-snapshot.ts").read_text()
|
||||
autosave = (ROOT / "extension" / "src" / "commands" / "autosave.ts").read_text(encoding="utf-8")
|
||||
snapshot = (ROOT / "extension" / "src" / "commands" / "session-snapshot.ts").read_text(encoding="utf-8")
|
||||
|
||||
assert "autoSaveTimer" in autosave
|
||||
assert "autoSaveInFlight" in autosave
|
||||
@@ -172,12 +172,12 @@ def test_session_autosave_is_debounced_and_non_overlapping():
|
||||
assert "clearTimeout(this.autoSaveTimer)" in autosave
|
||||
|
||||
def test_cli_and_sdk_expose_gentle_restore_controls():
|
||||
session_cli = (ROOT / "browser_cli" / "commands" / "session.py").read_text()
|
||||
tabs_cli = (ROOT / "browser_cli" / "commands" / "tabs.py").read_text()
|
||||
groups_cli = (ROOT / "browser_cli" / "commands" / "groups.py").read_text()
|
||||
commands_init = (ROOT / "browser_cli" / "commands" / "__init__.py").read_text()
|
||||
sdk_session = (ROOT / "browser_cli" / "sdk" / "session.py").read_text()
|
||||
sdk_perf = (ROOT / "browser_cli" / "sdk" / "perf.py").read_text()
|
||||
session_cli = (ROOT / "browser_cli" / "commands" / "session.py").read_text(encoding="utf-8")
|
||||
tabs_cli = (ROOT / "browser_cli" / "commands" / "tabs.py").read_text(encoding="utf-8")
|
||||
groups_cli = (ROOT / "browser_cli" / "commands" / "groups.py").read_text(encoding="utf-8")
|
||||
commands_init = (ROOT / "browser_cli" / "commands" / "__init__.py").read_text(encoding="utf-8")
|
||||
sdk_session = (ROOT / "browser_cli" / "sdk" / "session.py").read_text(encoding="utf-8")
|
||||
sdk_perf = (ROOT / "browser_cli" / "sdk" / "perf.py").read_text(encoding="utf-8")
|
||||
|
||||
# The --gentle-mode flag is defined once via the shared gentle_mode_option helper.
|
||||
assert '"--gentle-mode"' in commands_init
|
||||
@@ -200,8 +200,8 @@ def test_cli_and_sdk_expose_gentle_restore_controls():
|
||||
assert "def status" in sdk_perf
|
||||
assert "def set_profile" in sdk_perf
|
||||
|
||||
perf_cli = (ROOT / "browser_cli" / "commands" / "perf.py").read_text()
|
||||
root_cli = (ROOT / "browser_cli" / "cli.py").read_text()
|
||||
perf_cli = (ROOT / "browser_cli" / "commands" / "perf.py").read_text(encoding="utf-8")
|
||||
root_cli = (ROOT / "browser_cli" / "cli.py").read_text(encoding="utf-8")
|
||||
assert "perf_group" in perf_cli
|
||||
assert "perf.status" in perf_cli
|
||||
assert "perf.set_profile" in perf_cli
|
||||
|
||||
@@ -15,6 +15,7 @@ from browser_cli.mcp.serialization import structured
|
||||
from browser_cli.mcp.naming import resolve_tool_prefix
|
||||
from browser_cli.mcp.server import _screenshot_bytes, create_server, main
|
||||
from browser_cli.models import Tab
|
||||
from browser_cli.version_manager import project_version
|
||||
|
||||
class FakeClient:
|
||||
instances: list["FakeClient"] = []
|
||||
@@ -147,6 +148,15 @@ async def test_environment_pins_all_calls_to_one_browser(client, monkeypatch):
|
||||
|
||||
assert FakeClient.instances[-1].target == ("testing", None, None)
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_server_reports_the_project_version_to_clients(client):
|
||||
info = client.server_info
|
||||
|
||||
assert info is not None
|
||||
assert info.name == "browser-cli"
|
||||
assert info.version == project_version()
|
||||
assert info.version not in ("", "unknown")
|
||||
|
||||
def test_tool_prefix_defaults_to_browser_and_is_configurable():
|
||||
assert resolve_tool_prefix({}) == "browser_"
|
||||
assert resolve_tool_prefix({"BROWSER_CLI_MCP_TOOL_PREFIX": ""}) == ""
|
||||
|
||||
@@ -14,9 +14,9 @@ def _raise_system_exit(code: int):
|
||||
def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path):
|
||||
alias = "work"
|
||||
socket_path = tmp_path / "work.sock"
|
||||
socket_path.write_text("")
|
||||
socket_path.write_text("", encoding="utf-8")
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({alias: str(socket_path), "other": str(tmp_path / "other.sock")}))
|
||||
registry_path.write_text(json.dumps({alias: str(socket_path), "other": str(tmp_path / "other.sock")}), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(native_host, "REGISTRY_PATH", registry_path)
|
||||
monkeypatch.setattr(native_host, "_socket_path_for", lambda alias: str(socket_path))
|
||||
@@ -25,7 +25,7 @@ def test_cleanup_removes_socket_and_registry_entry(monkeypatch, tmp_path):
|
||||
native_host._cleanup(alias)
|
||||
|
||||
assert not socket_path.exists()
|
||||
assert json.loads(registry_path.read_text()) == {"other": str(tmp_path / "other.sock")}
|
||||
assert json.loads(registry_path.read_text(encoding="utf-8")) == {"other": str(tmp_path / "other.sock")}
|
||||
|
||||
def test_stdin_reader_cleans_up_on_eof(monkeypatch):
|
||||
cleaned = []
|
||||
@@ -42,14 +42,14 @@ def test_stdin_reader_cleans_up_on_eof(monkeypatch):
|
||||
|
||||
def test_cleanup_windows_skips_socket_unlink(monkeypatch, tmp_path):
|
||||
registry_path = tmp_path / "registry.json"
|
||||
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}))
|
||||
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}), encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(native_host, "REGISTRY_PATH", registry_path)
|
||||
monkeypatch.setattr(native_host, "is_windows", lambda: True)
|
||||
|
||||
native_host._cleanup("work")
|
||||
|
||||
assert json.loads(registry_path.read_text()) == {}
|
||||
assert json.loads(registry_path.read_text(encoding="utf-8")) == {}
|
||||
|
||||
def test_stdin_reader_cleans_up_on_bye(monkeypatch):
|
||||
cleaned = []
|
||||
|
||||
@@ -245,7 +245,7 @@ def test_serve_tcp_rate_limit_zero_disables():
|
||||
|
||||
def test_serve_tcp_per_key_policies_loaded_from_authorized_keys(tmp_path):
|
||||
keys = tmp_path / "authorized_keys"
|
||||
keys.write_text("abc123 reader allow:read-page\ndef456 admin allow:all\nghi789 plain\n")
|
||||
keys.write_text("abc123 reader allow:read-page\ndef456 admin allow:all\nghi789 plain\n", encoding="utf-8")
|
||||
security = _serve_security_for(["--authorized-keys", str(keys)])
|
||||
assert security.key_policies["abc123"] == CommandPolicy(allow_read_page=True)
|
||||
assert security.key_policies["def456"] == CommandPolicy.unrestricted()
|
||||
@@ -260,7 +260,7 @@ def test_auth_trust_writes_inline_policy_token(tmp_path):
|
||||
"--allow-read-page", "--allow-control",
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
line = keys.read_text().strip()
|
||||
line = keys.read_text(encoding="utf-8").strip()
|
||||
assert line == f"{pub} ci bot allow:read-page,control"
|
||||
|
||||
def test_auth_trust_without_allow_flags_writes_no_token(tmp_path):
|
||||
@@ -268,11 +268,11 @@ def test_auth_trust_without_allow_flags_writes_no_token(tmp_path):
|
||||
pub = "b" * 64
|
||||
result = CliRunner().invoke(main, ["auth", "trust", pub, "--name", "plain", "--file", str(keys)])
|
||||
assert result.exit_code == 0
|
||||
assert keys.read_text().strip() == f"{pub} plain"
|
||||
assert keys.read_text(encoding="utf-8").strip() == f"{pub} plain"
|
||||
|
||||
def test_auth_keys_local_shows_policy_column(tmp_path):
|
||||
keys = tmp_path / "authorized_keys"
|
||||
keys.write_text(f"{'a' * 64} reader allow:read-page\n{'b' * 64} admin allow:all\n{'c' * 64} plain\n")
|
||||
keys.write_text(f"{'a' * 64} reader allow:read-page\n{'b' * 64} admin allow:all\n{'c' * 64} plain\n", encoding="utf-8")
|
||||
result = CliRunner().invoke(main, ["auth", "keys", "--file", str(keys)])
|
||||
assert result.exit_code == 0
|
||||
assert "Policy" in result.output
|
||||
@@ -286,37 +286,37 @@ def test_auth_keys_local_shows_policy_column(tmp_path):
|
||||
def test_auth_policy_updates_existing_key_policy(tmp_path):
|
||||
keys = tmp_path / "authorized_keys"
|
||||
pub = "a" * 64
|
||||
keys.write_text(f"{pub} YubiKey 5C NFC FIPS\n")
|
||||
keys.write_text(f"{pub} YubiKey 5C NFC FIPS\n", encoding="utf-8")
|
||||
result = CliRunner().invoke(main, [
|
||||
"auth", "policy", pub, "--file", str(keys), "--allow-read-page", "--allow-control",
|
||||
])
|
||||
assert result.exit_code == 0
|
||||
assert keys.read_text().strip() == f"{pub} YubiKey 5C NFC FIPS allow:read-page,control"
|
||||
assert keys.read_text(encoding="utf-8").strip() == f"{pub} YubiKey 5C NFC FIPS allow:read-page,control"
|
||||
assert "Updated policy" in result.output
|
||||
|
||||
def test_auth_policy_can_set_safe_and_server_default_by_name(tmp_path):
|
||||
keys = tmp_path / "authorized_keys"
|
||||
pub = "b" * 64
|
||||
keys.write_text(f"{pub} laptop allow:all\n")
|
||||
keys.write_text(f"{pub} laptop allow:all\n", encoding="utf-8")
|
||||
|
||||
safe_result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys), "--safe"])
|
||||
assert safe_result.exit_code == 0
|
||||
assert keys.read_text().strip() == f"{pub} laptop allow:"
|
||||
assert keys.read_text(encoding="utf-8").strip() == f"{pub} laptop allow:"
|
||||
|
||||
default_result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys), "--server-default"])
|
||||
assert default_result.exit_code == 0
|
||||
assert keys.read_text().strip() == f"{pub} laptop"
|
||||
assert keys.read_text(encoding="utf-8").strip() == f"{pub} laptop"
|
||||
|
||||
def test_auth_policy_requires_policy_mode_when_not_interactive(tmp_path):
|
||||
keys = tmp_path / "authorized_keys"
|
||||
keys.write_text(f"{'a' * 64} laptop\n")
|
||||
keys.write_text(f"{'a' * 64} laptop\n", encoding="utf-8")
|
||||
result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys)])
|
||||
assert result.exit_code == 1
|
||||
assert "Choose a policy mode" in result.output
|
||||
|
||||
def test_auth_policy_rejects_conflicting_policy_modes(tmp_path):
|
||||
keys = tmp_path / "authorized_keys"
|
||||
keys.write_text(f"{'a' * 64} laptop\n")
|
||||
keys.write_text(f"{'a' * 64} laptop\n", encoding="utf-8")
|
||||
result = CliRunner().invoke(main, ["auth", "policy", "laptop", "--file", str(keys), "--safe", "--allow-all"])
|
||||
assert result.exit_code == 1
|
||||
assert "Choose exactly one policy mode" in result.output
|
||||
|
||||
+10
-10
@@ -74,7 +74,7 @@ class TestChallenge:
|
||||
monkeypatch.setattr("browser_cli.auth.pq_kex_server_keypair", lambda: ("fake-private", b"fake-public"))
|
||||
path = tmp_path / "authorized_keys"
|
||||
_, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
|
||||
client, server = _pair()
|
||||
t = _spawn(server, path)
|
||||
@@ -106,7 +106,7 @@ class TestRejection:
|
||||
def test_missing_pubkey_sig_rejected(self, tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
_, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
client, t, _ = self._connect(path)
|
||||
msg = {"id": "x", "command": "tabs.list", "args": {}, "user_agent": FAKE_UA}
|
||||
_send_framed(client, json.dumps(msg).encode())
|
||||
@@ -119,7 +119,7 @@ class TestRejection:
|
||||
def test_untrusted_pubkey_rejected(self, tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
_, trusted_pub = generate_keypair()
|
||||
path.write_text(trusted_pub + "\n")
|
||||
path.write_text(trusted_pub + "\n", encoding="utf-8")
|
||||
|
||||
pem, untrusted_pub = generate_keypair()
|
||||
key_path = tmp_path / "other.pem"
|
||||
@@ -140,7 +140,7 @@ class TestRejection:
|
||||
def test_bad_signature_rejected(self, tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
_, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
client, t, _ = self._connect(path)
|
||||
msg = {"id": "x", "command": "tabs.list", "args": {}, "user_agent": FAKE_UA, "pubkey": pub, "sig": "00" * 64}
|
||||
_send_framed(client, json.dumps(msg).encode())
|
||||
@@ -154,7 +154,7 @@ class TestRejection:
|
||||
monkeypatch.setattr("browser_cli.auth.pq_kex_server_keypair", lambda: ("fake-private", b"fake-public"))
|
||||
path = tmp_path / "authorized_keys"
|
||||
pem, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
priv_path = tmp_path / "client.pem"
|
||||
priv_path.write_bytes(pem)
|
||||
priv = load_private_key(priv_path)
|
||||
@@ -200,7 +200,7 @@ class TestAuthSuccess:
|
||||
"""Correct signature → error must be 'browser not connected', not 'unauthorized'."""
|
||||
path = tmp_path / "authorized_keys"
|
||||
pem, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
key_path = tmp_path / "client.key.pem"
|
||||
key_path.write_bytes(pem)
|
||||
priv = load_private_key(key_path)
|
||||
@@ -236,7 +236,7 @@ class TestAuthSuccess:
|
||||
|
||||
path = tmp_path / "authorized_keys"
|
||||
pem, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
key_path = tmp_path / "client.key.pem"
|
||||
key_path.write_bytes(pem)
|
||||
priv = load_private_key(key_path)
|
||||
@@ -276,7 +276,7 @@ class TestAuthSuccess:
|
||||
|
||||
path = tmp_path / "authorized_keys"
|
||||
pem, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
key_path = tmp_path / "client.key.pem"
|
||||
key_path.write_bytes(pem)
|
||||
priv = load_private_key(key_path)
|
||||
@@ -411,7 +411,7 @@ class TestPerKeyPolicy:
|
||||
monkeypatch.setattr("browser_cli.client.targets.resolve_socket", _mock_no_browser)
|
||||
path = tmp_path / "authorized_keys"
|
||||
pem, pub = generate_keypair()
|
||||
path.write_text(pub + "\n")
|
||||
path.write_text(pub + "\n", encoding="utf-8")
|
||||
key_path = tmp_path / "client.key.pem"
|
||||
key_path.write_bytes(pem)
|
||||
priv = load_private_key(key_path)
|
||||
@@ -441,7 +441,7 @@ class TestPerKeyPolicy:
|
||||
|
||||
path = tmp_path / "authorized_keys"
|
||||
pem, pub = generate_keypair()
|
||||
path.write_text(pub + " mykey\n")
|
||||
path.write_text(pub + " mykey\n", encoding="utf-8")
|
||||
key_path = tmp_path / "client.key.pem"
|
||||
key_path.write_bytes(pem)
|
||||
priv = load_private_key(key_path)
|
||||
|
||||
@@ -64,39 +64,39 @@ def test_full_control_still_cannot_manage_keys():
|
||||
def test_set_policy_updates_by_pubkey(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
pub = "a" * 64
|
||||
path.write_text(f"{pub} laptop\n")
|
||||
path.write_text(f"{pub} laptop\n", encoding="utf-8")
|
||||
assert set_authorized_key_policy(path, pub, ["control"]) == (pub, "laptop")
|
||||
assert load_authorized_keys_with_policies(path) == [(pub, "laptop", ["control"])]
|
||||
|
||||
def test_set_policy_by_name_and_remove_with_none(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
pub = "b" * 64
|
||||
path.write_text(f"{pub} ci-bot allow:all\n")
|
||||
path.write_text(f"{pub} ci-bot allow:all\n", encoding="utf-8")
|
||||
assert set_authorized_key_policy(path, "ci-bot", None) == (pub, "ci-bot") # remove token
|
||||
assert load_authorized_keys_with_policies(path) == [(pub, "ci-bot", None)]
|
||||
|
||||
def test_set_policy_safe_only_writes_empty_token(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
pub = "c" * 64
|
||||
path.write_text(f"{pub} reader\n")
|
||||
path.write_text(f"{pub} reader\n", encoding="utf-8")
|
||||
set_authorized_key_policy(path, pub, [])
|
||||
assert path.read_text().strip() == f"{pub} reader allow:"
|
||||
assert path.read_text(encoding="utf-8").strip() == f"{pub} reader allow:"
|
||||
|
||||
def test_set_policy_not_found_returns_none(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
path.write_text(f"{'a' * 64} laptop\n")
|
||||
path.write_text(f"{'a' * 64} laptop\n", encoding="utf-8")
|
||||
assert set_authorized_key_policy(path, "nonexistent", ["control"]) is None
|
||||
|
||||
def test_set_policy_ambiguous_name_raises(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
path.write_text(f"{'a' * 64} dup\n{'b' * 64} dup\n")
|
||||
path.write_text(f"{'a' * 64} dup\n{'b' * 64} dup\n", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="ambiguous"):
|
||||
set_authorized_key_policy(path, "dup", ["control"])
|
||||
|
||||
def test_set_policy_preserves_other_lines(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
a, b = "a" * 64, "b" * 64
|
||||
path.write_text(f"{a} first\n{b} second allow:read-page\n")
|
||||
path.write_text(f"{a} first\n{b} second allow:read-page\n", encoding="utf-8")
|
||||
set_authorized_key_policy(path, a, ["control"])
|
||||
assert load_authorized_keys_with_policies(path) == [
|
||||
(a, "first", ["control"]),
|
||||
@@ -139,7 +139,8 @@ def test_key_policies_from_authorized_keys(tmp_path):
|
||||
path.write_text(
|
||||
"AABBCC laptop allow:all\n"
|
||||
"ddee01 ci-bot allow:read-page,control\n"
|
||||
"112233 readonly\n" # no allow token → no override entry
|
||||
"112233 readonly\n", # no allow token → no override entry
|
||||
encoding="utf-8",
|
||||
)
|
||||
policies = key_policies_from_authorized_keys(path)
|
||||
assert policies["aabbcc"] == CommandPolicy.unrestricted() # normalised to lowercase
|
||||
@@ -151,13 +152,13 @@ def test_key_policies_none_returns_empty():
|
||||
|
||||
def test_key_policies_rejects_unknown_category(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
path.write_text("abc name allow:bogus\n")
|
||||
path.write_text("abc name allow:bogus\n", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="unknown command category"):
|
||||
key_policies_from_authorized_keys(path)
|
||||
|
||||
def test_load_with_names_ignores_allow_token(tmp_path):
|
||||
path = tmp_path / "authorized_keys"
|
||||
path.write_text("abc my laptop allow:control\n")
|
||||
path.write_text("abc my laptop allow:control\n", encoding="utf-8")
|
||||
assert load_authorized_keys_with_names(path) == [("abc", "my laptop")]
|
||||
assert load_authorized_keys_with_policies(path) == [("abc", "my laptop", ["control"])]
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 5
|
||||
requires-python = ">=3.10"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
|
||||
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten')",
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
@@ -396,6 +396,9 @@ wheels = [
|
||||
name = "httpcore2"
|
||||
version = "2.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
{ name = "truststore" },
|
||||
@@ -405,21 +408,62 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/06/a8d6a8f90d11b76d9086242b6bb0ee4b503473bfc8be34c2d5b067275c22/httpcore2-2.14.0-py3-none-any.whl", hash = "sha256:4c4f64f4b544afeb778c42c0ffb5941b29e671157e90ed3f6dc787c4bca90cd1", size = 84291, upload-time = "2026-10-18T16:29:19.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore2"
|
||||
version = "2.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "h11" },
|
||||
{ name = "truststore" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/a9/11bc39967bb9dfaab0981ab4d49f519818acbee662a7e76d06813319c838/httpcore2-2.15.1.tar.gz", hash = "sha256:a94b6b3e6260e1a3f035915408d3dcd7dc8e844c2b544b642e227cf98a747af0", size = 69859, upload-time = "2026-10-19T20:14:23.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/9b/e2b556e26fbc67716ab1dfc18832eb6fe196d88363f7094cb1da3d7c0d3e/httpcore2-2.15.1-py3-none-any.whl", hash = "sha256:730f603058c260a3288378217b464713350c23e3b8fe8bba753701054032d98a", size = 85055, upload-time = "2026-10-19T20:14:21.363Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx2"
|
||||
version = "2.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpcore2", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpcore2", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "idna" },
|
||||
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/67/58353f1848b4ba568e1ec22ea1e0d8e039f0bca5fde7e2d1f07739fd83e7/httpx2-2.14.0.tar.gz", hash = "sha256:47ad61a3c40282cd8d0dc96311531f00f5bfc71a066c6fe3b36d8f63fb9e4f6e", size = 102036, upload-time = "2026-10-18T16:29:22.627Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/5a/469616aa8c2cbb0bb9dd67a3c18d07c01ab604cc0b236ee1c47d260c7f6e/httpx2-2.14.0-py3-none-any.whl", hash = "sha256:9d20c13a10bae1773a0be3b0f1e5beeb4bdd549bc3a2ba18efa32897c65403e4", size = 96901, upload-time = "2026-10-18T16:29:20.819Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx2"
|
||||
version = "2.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpcore2", version = "2.15.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" },
|
||||
{ name = "idna" },
|
||||
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/67/58353f1848b4ba568e1ec22ea1e0d8e039f0bca5fde7e2d1f07739fd83e7/httpx2-2.14.0.tar.gz", hash = "sha256:47ad61a3c40282cd8d0dc96311531f00f5bfc71a066c6fe3b36d8f63fb9e4f6e", size = 102036, upload-time = "2026-10-18T16:29:22.627Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/85/07978f4afd168d6b408fa75c1dc13e3010653a81e182aa1b6d89ab94d9c6/httpx2-2.15.1.tar.gz", hash = "sha256:ff75485308fbe441e75b1218546e4e78d884df41e1a9741c1f79335ed8fcfef7", size = 101673, upload-time = "2026-10-19T20:14:24.587Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/5a/469616aa8c2cbb0bb9dd67a3c18d07c01ab604cc0b236ee1c47d260c7f6e/httpx2-2.14.0-py3-none-any.whl", hash = "sha256:9d20c13a10bae1773a0be3b0f1e5beeb4bdd549bc3a2ba18efa32897c65403e4", size = 96901, upload-time = "2026-10-18T16:29:20.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/bf/56068f774620330c8330e4b4915fae889a5b2d4b52667b3b7c8f66c4a9ad/httpx2-2.15.1-py3-none-any.whl", hash = "sha256:75841cd83d16b417f7d6da8f2089ce8120806efdbf96af37d46c5129c49015be", size = 96411, upload-time = "2026-10-19T20:14:22.789Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -495,7 +539,8 @@ version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx2" },
|
||||
{ name = "httpx2", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||
{ name = "httpx2", version = "2.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "mcp-types" },
|
||||
{ name = "opentelemetry-api" },
|
||||
@@ -954,7 +999,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "real-browser-cli"
|
||||
version = "0.16.9"
|
||||
version = "0.16.10"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -1158,7 +1203,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
|
||||
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten')",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" }
|
||||
wheels = [
|
||||
@@ -1328,7 +1373,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14'",
|
||||
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||
"(python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')",
|
||||
"python_full_version == '3.11.*' or (python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten')",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
||||
Reference in New Issue
Block a user