mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-29 09:12:24 +01:00
feat(runtime): bind browser resources to authority
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import asyncio
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from src import browser_identity as browser
|
||||
from src.agent_runtime.resources import ResourceIdentityError
|
||||
from tests.test_browser_resource_identity import producer, observed, authority
|
||||
from tests.test_runtime_resource_integration import approval_for, dispatch
|
||||
|
||||
|
||||
@pytest.mark.parametrize("phase", ["timeout", "cancel", "spawn_cancel"])
|
||||
async def test_client_is_killed_before_resend_deadline_without_retry(monkeypatch, phase):
|
||||
calls = []
|
||||
class Child:
|
||||
returncode = None
|
||||
killed = False
|
||||
async def wait(self):
|
||||
if self.killed:
|
||||
self.returncode = -9
|
||||
return -9
|
||||
await asyncio.Future()
|
||||
def kill(self): self.killed = True
|
||||
child = Child()
|
||||
started, release = asyncio.Event(), asyncio.Event()
|
||||
async def spawn(*args, **kwargs):
|
||||
calls.append(args); started.set()
|
||||
if phase == "spawn_cancel": await release.wait()
|
||||
return child
|
||||
monkeypatch.setattr(browser.asyncio, "create_subprocess_exec", spawn)
|
||||
original = asyncio.wait_for
|
||||
async def bounded(awaitable, timeout):
|
||||
assert timeout == browser.CLIENT_DEADLINE_S and timeout < 30
|
||||
return await original(awaitable, .01 if phase == "timeout" else timeout)
|
||||
monkeypatch.setattr(browser.asyncio, "wait_for", bounded)
|
||||
task = asyncio.create_task(browser.run_client(["trusted-producer", "session", "info"], env={}, cwd="/"))
|
||||
await started.wait()
|
||||
if phase != "timeout": task.cancel()
|
||||
release.set()
|
||||
with pytest.raises((asyncio.TimeoutError, asyncio.CancelledError)): await task
|
||||
assert child.killed and len(calls) == 1
|
||||
|
||||
|
||||
async def test_sidecar_allowlist_has_no_enable_mutation_or_arbitrary_cdp():
|
||||
client = browser.CDPSidecar("ws://127.0.0.1:1234/devtools/browser/12345678-1234-1234-1234-123456789abc")
|
||||
for method in ("Page.enable", "Runtime.evaluate", "Page.navigate", "Target.closeTarget", "Browser.close"):
|
||||
with pytest.raises(ValueError): await client.call(method)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("envelope", [[], None, {"id": True, "result": {}}, {"id": "1", "result": {}}, {"id": 1, "error": {}, "result": {}}])
|
||||
async def test_sidecar_rejects_malformed_identity_envelopes(monkeypatch, envelope):
|
||||
client = browser.CDPSidecar("ws://127.0.0.1:1234/devtools/browser/12345678-1234-1234-1234-123456789abc")
|
||||
async def send(*args): pass
|
||||
async def receive(): return envelope
|
||||
monkeypatch.setattr(client, "_send", send)
|
||||
monkeypatch.setattr(client, "_message", receive)
|
||||
with pytest.raises(ResourceIdentityError):
|
||||
await client.call("Target.getTargets")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["namespace", "runtimeError", "restoreKey"])
|
||||
async def test_missing_nullable_lifecycle_fields_are_not_valid_observations(producer, field):
|
||||
record = await observed(producer)
|
||||
info = await record.command("session", "info")
|
||||
del (info["runtime"] if field == "restoreKey" else info)[field]
|
||||
with pytest.raises(ResourceIdentityError): browser.daemon_observation(record, info)
|
||||
|
||||
|
||||
async def test_observation_cancellation_while_waiting_for_lock_invalidates_session(producer):
|
||||
record = await observed(producer)
|
||||
await record.lock.acquire()
|
||||
task = asyncio.create_task(browser.observe_registered(record))
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError): await task
|
||||
record.lock.release()
|
||||
assert record.session is None and record.pages == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [{"action": "click", "page": "t1"}, {"action": "batch", "commands": [["click", "e1"]]}])
|
||||
async def test_central_dispatch_cannot_bypass_page_denial(producer, args):
|
||||
await observed(producer)
|
||||
current = authority()
|
||||
producer.calls.clear(); producer.cdp_calls.clear()
|
||||
_, result = await dispatch(current, "private_browser", json.dumps(args))
|
||||
assert result["failure_kind"] == browser.PAGE_FAILURE and result["executed"] is False
|
||||
assert not producer.calls and not producer.cdp_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("replacement", ["browser", "daemon"])
|
||||
async def test_exact_approval_revalidates_before_claim(producer, replacement):
|
||||
record = await observed(producer)
|
||||
current = authority()
|
||||
content = '{"action":"session_info"}'
|
||||
approval = approval_for(current, "private_browser", content)
|
||||
if replacement == "daemon":
|
||||
producer.pid += 1
|
||||
else:
|
||||
record._endpoint = "ws://127.0.0.1:1234/devtools/browser/87654321-1234-1234-1234-123456789abc"
|
||||
_, result = await dispatch(current, "private_browser", content, approval)
|
||||
assert result["exit_code"] == 1 and not approval._claimed
|
||||
assert record.session is None and record.pages == ()
|
||||
|
||||
|
||||
async def test_metadata_revalidation_never_auto_launches_or_calls_get_cdp_url(producer):
|
||||
await observed(producer)
|
||||
current = authority()
|
||||
producer.calls.clear()
|
||||
_, result = await dispatch(current, "private_browser", '{"action":"session_info"}')
|
||||
assert result["exit_code"] == 0
|
||||
assert producer.calls and all(command == ("session", "info") for command in producer.calls)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", ["EOF", "connection reset", "EAGAIN", "read timeout"])
|
||||
async def test_page_failures_never_enter_producer_internal_retry_path(producer, status, monkeypatch):
|
||||
async def forbidden(*args, **kwargs):
|
||||
pytest.fail("Producer retry hazard reached: " + status)
|
||||
monkeypatch.setattr(browser, "run_client", forbidden)
|
||||
producer.calls.clear()
|
||||
from src.agent_tools.web_tools import PrivateBrowserTool
|
||||
result = await PrivateBrowserTool().execute('{"action":"wait","page":"t1","timeout_ms":120000}', {})
|
||||
assert result["executed"] is False and result["retryable"] is False
|
||||
assert producer.calls == []
|
||||
|
||||
|
||||
async def test_page_scoped_child_still_cannot_execute_even_matching_observation(producer):
|
||||
await observed(producer)
|
||||
from dataclasses import replace
|
||||
parent = replace(authority(), browser_sessions=())
|
||||
child = parent.intersect(authority())
|
||||
_, result = await dispatch(child, "private_browser", '{"action":"click","page":"t1","ref":"e1"}')
|
||||
assert result["failure_kind"] == browser.PAGE_FAILURE and result["executed"] is False
|
||||
|
||||
|
||||
async def test_observed_url_or_alias_change_is_not_resource_authority(producer):
|
||||
record = await observed(producer)
|
||||
from dataclasses import replace
|
||||
original = record.pages[0]
|
||||
metadata = replace(original, resolved_alias="t99", observed_url="https://different.example")
|
||||
assert original.authority_key() == metadata.authority_key()
|
||||
metadata.validate()
|
||||
|
||||
|
||||
def test_raw_global_playwright_and_native_backend_are_not_substitutable():
|
||||
from src.agent_runtime.resources import ExternalResource
|
||||
from src.agent_runtime.authority import ExactOperation
|
||||
assert not browser.native_browser(ExactOperation.normalize("private_browser", '{"action":"session_info"}'),
|
||||
ExternalResource("mcp", "endpoint", "server", "tool", "epoch"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool", ["browser_click", "browser_snapshot", "browser_evaluate", "browser_navigate", "browser_run_code"])
|
||||
async def test_raw_mcp_browser_execution_cannot_evade_disabled_page_contract(tool):
|
||||
from src.agent_runtime.authority import RequestAuthority, OperationGrant
|
||||
name = "mcp__builtin_browser__" + tool
|
||||
current = RequestAuthority("request", "alice", "thread", "", (OperationGrant(name),))
|
||||
_, result = await dispatch(current, name, '{}')
|
||||
assert result["failure_kind"] == browser.PAGE_FAILURE and result["executed"] is False
|
||||
Reference in New Issue
Block a user