Files
2026-07-13 13:12:33 +08:00

227 lines
6.9 KiB
Python

from __future__ import annotations
import asyncio
from pathlib import Path
from types import SimpleNamespace
import pytest
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.sandbox import integration as integration_mod
from opensquilla.sandbox.network_proxy import SandboxProxyServer
from opensquilla.sandbox.network_runtime import (
NetworkApprovalService,
NetworkPolicyRequest,
NetworkProtocol,
)
from opensquilla.sandbox.run_context import RunContext
from opensquilla.sandbox.run_mode import RunMode
from opensquilla.sandbox.types import (
NetworkMode,
ResourceLimits,
SandboxPolicy,
SandboxRequest,
SecurityLevel,
)
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
def _proxy_policy() -> SandboxPolicy:
return SandboxPolicy(
level=SecurityLevel.STANDARD,
network=NetworkMode.PROXY_ALLOWLIST,
mounts=(),
workspace_rw=True,
tmp_writable=True,
limits=ResourceLimits(),
env_allowlist=("PATH",),
require_approval=False,
)
async def _send_proxy_request(server: SandboxProxyServer, request: bytes) -> bytes:
reader, writer = await asyncio.open_connection(server.host, server.port)
try:
writer.write(request)
await writer.drain()
return await asyncio.wait_for(reader.read(4096), timeout=2.0)
finally:
writer.close()
await writer.wait_closed()
async def _wait_for_pending_network_approval() -> dict:
for _ in range(100):
pending = get_approval_queue().list_pending("exec")
if pending:
assert len(pending) == 1
return pending[0]
await asyncio.sleep(0.01)
raise AssertionError("network approval was not queued")
async def test_proxy_runtime_approval_waits_and_forwards_after_allow(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
reset_approval_queue()
upstream_requests: list[bytes] = []
async def handle_upstream(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
) -> None:
upstream_requests.append(await reader.readuntil(b"\r\n\r\n"))
writer.write(
b"HTTP/1.1 200 OK\r\n"
b"Content-Length: 2\r\n"
b"Connection: close\r\n"
b"\r\n"
b"ok"
)
await writer.drain()
writer.close()
await writer.wait_closed()
upstream = await asyncio.start_server(handle_upstream, "127.0.0.1", 0)
upstream_socket = next(iter(upstream.sockets or ()), None)
assert upstream_socket is not None
upstream_host, upstream_port = upstream_socket.getsockname()[:2]
real_open_connection = asyncio.open_connection
async def fake_open_connection(host: str, port: int, *args: object, **kwargs: object):
if host == "93.184.216.34" and port == upstream_port:
return await real_open_connection(str(upstream_host), upstream_port, *args, **kwargs)
return await real_open_connection(host, port, *args, **kwargs)
monkeypatch.setattr(asyncio, "open_connection", fake_open_connection)
request = SandboxRequest(
argv=("exec_command", "curl", "http://unknown.test/path"),
cwd=tmp_path,
action_kind="shell.exec",
policy=_proxy_policy(),
session_id="s1",
run_mode="standard",
)
runtime = SimpleNamespace(workspace=tmp_path)
service = NetworkApprovalService(
context=RunContext(run_mode=RunMode.STANDARD),
request=request,
runtime=runtime,
approval_timeout_seconds=2.0,
)
server = SandboxProxyServer(
policy_decider=service,
resolver=lambda host, port: ("93.184.216.34", upstream_port),
)
await server.start()
try:
response_task = asyncio.create_task(
_send_proxy_request(
server,
b"GET http://unknown.test/path HTTP/1.1\r\n"
b"Host: unknown.test\r\n"
b"\r\n",
)
)
pending = await _wait_for_pending_network_approval()
params = pending["params"]
assert params["approvalKind"] == "sandbox_network"
assert params["host"] == "unknown.test"
assert params["sessionKey"] == "s1"
assert params["fingerprint"]
get_approval_queue().resolve(str(pending["id"]), True)
response = await response_task
finally:
await server.stop()
upstream.close()
await upstream.wait_closed()
assert response.startswith(b"HTTP/1.1 200 OK")
assert b"ok" in response
assert upstream_requests == [
b"GET /path HTTP/1.1\r\n"
b"Host: unknown.test\r\n"
b"Connection: close\r\n"
b"\r\n"
]
async def test_trusted_runtime_network_decider_allows_without_approval(
tmp_path: Path,
) -> None:
reset_approval_queue()
request = SandboxRequest(
argv=("exec_command", "curl", "https://new-public.example"),
cwd=tmp_path,
action_kind="shell.exec",
policy=_proxy_policy(),
session_id="s1",
run_mode="trusted",
)
service = NetworkApprovalService(
context=RunContext(run_mode=RunMode.TRUSTED),
request=request,
runtime=SimpleNamespace(workspace=tmp_path),
approval_timeout_seconds=0.01,
)
decision = await service.decide(
NetworkPolicyRequest(
protocol=NetworkProtocol.HTTPS_CONNECT,
host="new-public.example",
port=443,
method="CONNECT",
tool_name="exec_command",
command="curl https://new-public.example",
)
)
assert decision.status == "allow"
assert decision.reason == "auto_trusted"
assert get_approval_queue().list_pending("exec") == []
async def test_subprocess_preflight_leaves_explicit_url_approval_to_proxy_runtime(
tmp_path: Path,
) -> None:
reset_approval_queue()
request = SandboxRequest(
argv=("exec_command", "curl", "https://unknown.test/path"),
cwd=tmp_path,
action_kind="shell.exec",
policy=_proxy_policy(),
session_id="s1",
run_mode="standard",
)
runtime = SimpleNamespace(
backend=SimpleNamespace(name="noop"),
workspace=tmp_path,
)
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
workspace_dir=str(tmp_path),
session_key="s1",
run_mode="standard",
sandbox_run_context=RunContext(
run_mode=RunMode.STANDARD,
workspace=str(tmp_path),
),
)
)
try:
result = await integration_mod.preflight_subprocess_managed_network(
request,
runtime,
)
finally:
current_tool_context.reset(token)
assert result is None
assert get_approval_queue().list_pending("exec") == []