refactor: modularize auth transport and markdown
Testing / remote-protocol-compat (0.9.5) (push) Successful in 1m4s
Testing / test (push) Successful in 1m22s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m7s
Package Extension / package-extension (push) Successful in 1m1s
Build & Publish Package / publish (push) Successful in 1m5s

- Split auth into focused package modules for agent keys, file keys,
  signing, and post-quantum transport helpers while keeping the public
  browser_cli.auth import surface intact.
- Move transport encoding internals into a package with separate codec and
  binary-hoisting helpers, preserving browser_cli.transport compatibility.
- Extract remote TCP auth/socket helpers and serve challenge setup out of the
  runtime paths to make connection handling easier to reason about.
- Move the extension markdown extractor into a dedicated content/markdown
  folder with separate root selection, code normalization, renderer, and utils.
- Centralize CLI Rich rendering helpers for tab/window tree and table output,
  and add rendering tests for the shared builders.
- Remove local typing ignores in SDK/decorator/script plumbing and bump the
  package and extension version to 0.15.3.
This commit is contained in:
2026-06-15 01:23:57 +02:00
parent 0b43408a8d
commit 7cb2a8b618
34 changed files with 1502 additions and 1325 deletions
+3 -3
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar
from typing import Any, TypeVar, cast
F = TypeVar("F", bound=Callable)
_MISSING = object()
@@ -54,8 +54,8 @@ def sdk_command(
return _clone_default(default)
return result
wrapper._browser_cli_command = name # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
setattr(wrapper, "_browser_cli_command", name)
return cast(F, wrapper)
return decorator
+5 -5
View File
@@ -5,7 +5,7 @@ import asyncio
import functools
import inspect
from collections.abc import Callable
from typing import TypeVar
from typing import TypeVar, cast
from browser_cli.sdk.base import Namespace
from browser_cli.sdk.workflow_decorators import WorkflowDecoratorsMixin, _NO_INJECT
@@ -53,7 +53,7 @@ class DecoratorsNS(WorkflowDecoratorsMixin, Namespace):
finally:
if cleanup is not None:
await asyncio.to_thread(cleanup, value)
return async_wrapper # type: ignore[return-value]
return cast(F, async_wrapper)
return WorkflowDecoratorsMixin._value_decorator(
self, fn, get_value, keyword=keyword, cleanup=cleanup
)
@@ -74,7 +74,7 @@ class DecoratorsNS(WorkflowDecoratorsMixin, Namespace):
finally:
if previous:
await asyncio.to_thread(self._c.perf.set_profile, previous)
return async_wrapper # type: ignore[return-value]
return cast(F, async_wrapper)
return WorkflowDecoratorsMixin.performance_profile(self, profile, restore=restore)(fn)
return decorator
@@ -101,7 +101,7 @@ class DecoratorsNS(WorkflowDecoratorsMixin, Namespace):
raise
if delay > 0:
await asyncio.sleep(delay)
raise last_error # type: ignore[misc]
return async_wrapper # type: ignore[return-value]
raise cast(BaseException, last_error)
return cast(F, async_wrapper)
return WorkflowDecoratorsMixin.retry(self, times=times, delay=delay, exceptions=exceptions)(fn)
return decorator
+35 -14
View File
@@ -4,11 +4,32 @@ from __future__ import annotations
import functools
import time
from collections.abc import Callable
from typing import TypeVar
from typing import Protocol, TypeVar, cast
F = TypeVar("F", bound=Callable)
_NO_INJECT = object()
class _WorkflowTabs(Protocol):
def active(self): ...
def open(self, *args, **kwargs): ...
def watch_url(self, *args, **kwargs): ...
class _WorkflowDom(Protocol):
def wait_for(self, *args, **kwargs): ...
class _WorkflowPerf(Protocol):
def status(self): ...
def set_profile(self, profile: str): ...
class _WorkflowSession(Protocol):
def save(self, name: str): ...
class _WorkflowClient(Protocol):
tabs: _WorkflowTabs
dom: _WorkflowDom
perf: _WorkflowPerf
session: _WorkflowSession
class WorkflowDecoratorsMixin:
"""Shared implementation for sync and async workflow decorators.
@@ -17,7 +38,7 @@ class WorkflowDecoratorsMixin:
in lockstep.
"""
_c: object
_c: _WorkflowClient
@staticmethod
def _inject(kwargs: dict, keyword: str | None, value):
@@ -62,7 +83,7 @@ class WorkflowDecoratorsMixin:
finally:
if cleanup is not None:
self._run(cleanup, value)
return wrapper # type: ignore[return-value]
return cast(F, wrapper)
return decorator(func) if func is not None else decorator
@@ -72,7 +93,7 @@ class WorkflowDecoratorsMixin:
By default the tab is injected as ``tab=...``. Pass ``keyword=None`` to
pass it as the first positional argument instead.
"""
return self._value_decorator(func, self._c.tabs.active, keyword=keyword) # type: ignore[attr-defined]
return self._value_decorator(func, self._c.tabs.active, keyword=keyword)
def new_tab(
self,
@@ -93,7 +114,7 @@ class WorkflowDecoratorsMixin:
wrapped function returns or raises.
"""
def open_tab():
return self._c.tabs.open( # type: ignore[attr-defined]
return self._c.tabs.open(
url,
wait=wait,
timeout=timeout,
@@ -124,7 +145,7 @@ class WorkflowDecoratorsMixin:
the wrapped function. By default the result is not injected.
"""
def wait():
return self._c.dom.wait_for( # type: ignore[attr-defined]
return self._c.dom.wait_for(
selector,
timeout=timeout,
visible=visible,
@@ -145,7 +166,7 @@ class WorkflowDecoratorsMixin:
):
"""Wait until a tab URL matches *pattern* before calling the function."""
def wait():
return self._c.tabs.watch_url(pattern, tab_id=tab_id, timeout=timeout) # type: ignore[attr-defined]
return self._c.tabs.watch_url(pattern, tab_id=tab_id, timeout=timeout)
inject = keyword if keyword is not None else _NO_INJECT
return self._value_decorator(None, wait, keyword=inject)
@@ -157,19 +178,19 @@ class WorkflowDecoratorsMixin:
def wrapper(*args, **kwargs):
previous = None
if restore:
previous = self._run(self._c.perf.status).get("performanceProfile") # type: ignore[attr-defined]
self._run(self._c.perf.set_profile, profile) # type: ignore[attr-defined]
previous = self._run(self._c.perf.status).get("performanceProfile")
self._run(self._c.perf.set_profile, profile)
try:
return self._call_wrapped(fn, *args, **kwargs)
finally:
if previous:
self._run(self._c.perf.set_profile, previous) # type: ignore[attr-defined]
return wrapper # type: ignore[return-value]
self._run(self._c.perf.set_profile, previous)
return cast(F, wrapper)
return decorator
def save_session_before(self, name: str):
"""Save the current browser session before running the function."""
return self._value_decorator(None, lambda: self._c.session.save(name), keyword=_NO_INJECT) # type: ignore[attr-defined]
return self._value_decorator(None, lambda: self._c.session.save(name), keyword=_NO_INJECT)
def retry(
self,
@@ -194,7 +215,7 @@ class WorkflowDecoratorsMixin:
raise
if delay > 0:
self._sleep(delay)
raise last_error # type: ignore[misc]
return wrapper # type: ignore[return-value]
raise cast(BaseException, last_error)
return cast(F, wrapper)
return decorator