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

1528 lines
50 KiB
Python

from __future__ import annotations
import asyncio
from types import SimpleNamespace
import pytest
class _SessionManager:
def __init__(self):
self.node = SimpleNamespace(
session_key="agent:main:webchat:abc",
agent_id="main",
origin=None,
)
self.sessions = {self.node.session_key: self.node}
self.created: list[str] = []
async def get_session(self, session_key: str):
return self.sessions.get(session_key)
async def get_or_create(self, session_key: str, agent_id: str = "main", **kwargs):
existing = self.sessions.get(session_key)
if existing is not None:
return existing, False
node = SimpleNamespace(
session_key=session_key,
agent_id=agent_id,
origin=None,
**kwargs,
)
self.sessions[session_key] = node
self.created.append(session_key)
return node, True
async def update(self, session_key: str, **fields):
node = self.sessions[session_key]
for key, value in fields.items():
setattr(node, key, value)
return node
def _ctx(
manager: _SessionManager,
*,
is_owner: bool = True,
run_mode: str = "standard",
sandbox: bool = True,
security_grading: bool = True,
permissions_default_mode: str = "off",
scopes: frozenset[str] | None = None,
):
from opensquilla.gateway.auth import Principal
from opensquilla.gateway.rpc import RpcContext
config = SimpleNamespace(
workspace_dir="/tmp/ws",
agents=[],
sandbox=SimpleNamespace(
run_mode=run_mode,
sandbox=sandbox,
security_grading=security_grading,
backend="noop",
network_default="proxy_allowlist",
),
permissions=SimpleNamespace(default_mode=permissions_default_mode),
)
return RpcContext(
conn_id="c",
principal=Principal(
role="operator",
scopes=scopes or frozenset(["operator.read", "operator.write"]),
is_owner=is_owner,
authenticated=True,
),
session_manager=manager,
config=config,
)
@pytest.fixture(autouse=True)
def _reset_resolved_overlays() -> None:
from opensquilla.sandbox.escalation import reset_resolved_run_context_overlays
reset_resolved_run_context_overlays()
yield
reset_resolved_run_context_overlays()
@pytest.mark.asyncio
async def test_rpc_add_domain_returns_updated_context() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_domain_add
manager = _SessionManager()
result = await _handle_sandbox_domain_add(
{
"sessionKey": manager.node.session_key,
"domain": "https://pypi.org/simple",
"scope": "workspace",
},
_ctx(manager),
)
assert result["domains"] == [
{"domain": "pypi.org", "scope": "workspace", "source": "manual"}
]
@pytest.mark.asyncio
async def test_rpc_add_mount_rejects_non_owner() -> None:
from opensquilla.gateway.rpc import RpcHandlerError
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_mount_add
manager = _SessionManager()
with pytest.raises(RpcHandlerError, match="requires owner principal"):
await _handle_sandbox_mount_add(
{"sessionKey": manager.node.session_key, "path": "/tmp/ws/extras"},
_ctx(manager, is_owner=False),
)
assert manager.node.origin is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
("handler_name", "params"),
[
("_handle_sandbox_mount_add", {}),
("_handle_sandbox_mount_add", {"path": ""}),
("_handle_sandbox_mount_add", {"path": " "}),
("_handle_sandbox_mount_remove", {}),
("_handle_sandbox_mount_remove", {"path": ""}),
("_handle_sandbox_mount_remove", {"path": " "}),
],
)
async def test_rpc_mount_mutations_require_path_without_mutating_origin(
handler_name: str,
params: dict[str, str],
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
manager = _SessionManager()
handler = getattr(rpc_sandbox, handler_name)
with pytest.raises(ValueError, match="params.path is required"):
await handler(
{"sessionKey": manager.node.session_key, **params},
_ctx(manager),
)
assert manager.node.origin is None
assert manager.created == []
@pytest.mark.asyncio
async def test_rpc_mutation_rejects_whitespace_session_key_without_creating_session() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_mount_add
manager = _SessionManager()
with pytest.raises(ValueError, match="params.sessionKey is required"):
await _handle_sandbox_mount_add(
{"sessionKey": " ", "path": "/tmp/ws/extras"},
_ctx(manager),
)
assert " " not in manager.sessions
assert manager.created == []
@pytest.mark.asyncio
async def test_rpc_run_context_get_includes_bundles_and_temporary_grants() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_run_context_get
from opensquilla.sandbox.run_context import (
PublicNetworkGrant,
RunContext,
TemporaryGrant,
persist_run_context,
)
from opensquilla.sandbox.run_mode import RunMode
from opensquilla.sandbox.user_grants import upsert_bundle_grant
manager = _SessionManager()
upsert_bundle_grant(
{
"bundle_id": "python-package-install",
"scope": "workspace",
"source": "manual",
}
)
await persist_run_context(
manager,
manager.node.session_key,
RunContext(
run_mode=RunMode.STANDARD,
workspace="/tmp/ws",
public_network=(PublicNetworkGrant(scope="chat", source="manual"),),
temporary_grants=(
TemporaryGrant(
kind="domain",
value="pypi.org",
fingerprint="abc123",
),
),
source="saved",
),
)
result = await _handle_sandbox_run_context_get(
{"sessionKey": manager.node.session_key},
_ctx(manager),
)
assert result["bundles"] == [
{
"bundle_id": "python-package-install",
"scope": "workspace",
"source": "manual",
}
]
assert result["publicNetwork"] == [
{
"scope": "chat",
"source": "manual",
}
]
assert result["temporaryGrants"] == [
{
"kind": "domain",
"value": "pypi.org",
"fingerprint": "abc123",
"expires_after": "once",
}
]
@pytest.mark.asyncio
async def test_exec_approval_resolve_allows_non_owner_chat_scoped_sandbox_grant() -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc_approvals import _handle_exec_approval_resolve
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
result = await _handle_exec_approval_resolve(
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
_ctx(
manager,
is_owner=False,
scopes=frozenset(["operator.approvals"]),
),
)
assert result["resolved"] is True
assert result["approved"] is True
context = await get_run_context(
manager,
manager.node.session_key,
config=_ctx(manager).config,
workspace="/tmp/ws",
)
assert ("example.com", "chat") in [(grant.domain, grant.scope) for grant in context.domains]
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_rejects_legacy_intent_flags() -> None:
# "Allow always" / rememberIntent were a removed no-op; a truthy value must
# now be rejected loudly rather than silently accepted, and must not resolve
# the approval or apply any grant.
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import RpcHandlerError
from opensquilla.gateway.rpc_approvals import _handle_exec_approval_resolve
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
reset_approval_queue()
manager = _SessionManager()
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
with pytest.raises(RpcHandlerError) as excinfo:
await _handle_exec_approval_resolve(
{
"id": approval_id,
"approved": True,
"choice": "allow_same_type",
"allowAlways": True,
"rememberIntent": True,
},
_ctx(
manager,
is_owner=False,
scopes=frozenset(["operator.approvals"]),
),
)
assert excinfo.value.code == "UNSUPPORTED_PARAM"
# The approval is untouched — rejection happens before any resolution.
assert queue.get(approval_id).resolved is False
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_rejects_non_owner_missing_sandbox_choice() -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import RpcHandlerError
from opensquilla.gateway.rpc_approvals import _handle_exec_approval_resolve
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
reset_approval_queue()
manager = _SessionManager()
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
with pytest.raises(RpcHandlerError, match="requires owner principal"):
await _handle_exec_approval_resolve(
{"id": approval_id, "approved": True},
_ctx(
manager,
is_owner=False,
scopes=frozenset(["operator.approvals"]),
),
)
pending = queue.get(approval_id)
assert pending.resolved is False
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_allows_non_owner_chat_scoped_path_mount(tmp_path) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc_approvals import _handle_exec_approval_resolve
from opensquilla.sandbox.escalation import build_path_approval_params
from opensquilla.sandbox.path_validation import MountDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
workspace.mkdir()
outside.mkdir()
params = build_path_approval_params(
MountDecision(
status="request",
normalized_path=str(outside.resolve(strict=False)),
access="ro",
reason="outside_sandbox_mounts",
),
session_key=manager.node.session_key,
workspace=str(workspace),
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
result = await _handle_exec_approval_resolve(
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
_ctx(
manager,
is_owner=False,
scopes=frozenset(["operator.approvals"]),
),
)
assert result["resolved"] is True
assert result["approved"] is True
context = await get_run_context(
manager,
manager.node.session_key,
config=_ctx(manager).config,
workspace=str(workspace),
)
assert (str(outside.resolve(strict=False)), "ro", "chat") in [
(grant.path, grant.access, grant.scope) for grant in context.mounts
]
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_allows_non_owner_sandbox_grant_denial() -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc_approvals import _handle_exec_approval_resolve
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
result = await _handle_exec_approval_resolve(
{"id": approval_id, "approved": False, "choice": "deny"},
_ctx(
manager,
is_owner=False,
scopes=frozenset(["operator.approvals"]),
),
)
assert result["resolved"] is True
assert result["approved"] is False
context = await get_run_context(
manager,
manager.node.session_key,
config=_ctx(manager).config,
workspace="/tmp/ws",
)
assert context.domains == ()
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_leaves_sandbox_approval_pending_when_mutation_fails(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import get_dispatcher
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
async def fail_apply(*args, **kwargs) -> None:
raise RuntimeError("mutation failed")
monkeypatch.setattr(
"opensquilla.gateway.rpc_approvals.apply_sandbox_approval_choice",
fail_apply,
)
result = await get_dispatcher().dispatch(
"r1",
"exec.approval.resolve",
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
_ctx(manager, scopes=frozenset(["operator.approvals"])),
)
assert result.error is not None
assert "mutation failed" in result.error.message
pending = queue.get(approval_id)
assert pending.resolved is False
assert pending.approved is False
context = await get_run_context(
manager,
manager.node.session_key,
config=_ctx(manager).config,
workspace="/tmp/ws",
)
assert context.domains == ()
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_claim_prevents_deny_race_from_landing_grant(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import get_dispatcher
from opensquilla.sandbox import escalation as escalation_mod
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
ctx = _ctx(manager, scopes=frozenset(["operator.approvals"]))
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
mutation_started = asyncio.Event()
release_mutation = asyncio.Event()
async def delayed_apply(*args, **kwargs) -> None:
mutation_started.set()
await release_mutation.wait()
await escalation_mod.apply_sandbox_approval_choice(*args, **kwargs)
monkeypatch.setattr(
"opensquilla.gateway.rpc_approvals.apply_sandbox_approval_choice",
delayed_apply,
)
approve_task = asyncio.create_task(
get_dispatcher().dispatch(
"approve",
"exec.approval.resolve",
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
ctx,
)
)
await asyncio.wait_for(mutation_started.wait(), timeout=1)
deny_result = await get_dispatcher().dispatch(
"deny",
"exec.approval.resolve",
{"id": approval_id, "approved": False, "choice": "deny"},
ctx,
)
release_mutation.set()
approve_result = await approve_task
assert deny_result.error is not None
assert approve_result.error is None, approve_result.error
resolved = queue.get(approval_id)
assert resolved.resolved is True
assert resolved.approved is True
context = await get_run_context(
manager,
manager.node.session_key,
config=ctx.config,
workspace="/tmp/ws",
)
assert ("example.com", "chat") in [(grant.domain, grant.scope) for grant in context.domains]
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_wait_and_consume_wait_for_sandbox_grant_apply(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import get_dispatcher
from opensquilla.sandbox import escalation as escalation_mod
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
reset_approval_queue()
manager = _SessionManager()
ctx = _ctx(manager, scopes=frozenset(["operator.approvals"]))
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
mutation_started = asyncio.Event()
release_mutation = asyncio.Event()
async def delayed_apply(*args, **kwargs) -> None:
mutation_started.set()
await release_mutation.wait()
await escalation_mod.apply_sandbox_approval_choice(*args, **kwargs)
monkeypatch.setattr(
"opensquilla.gateway.rpc_approvals.apply_sandbox_approval_choice",
delayed_apply,
)
approve_task = asyncio.create_task(
get_dispatcher().dispatch(
"approve",
"exec.approval.resolve",
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
ctx,
)
)
await asyncio.wait_for(mutation_started.wait(), timeout=1)
wait_task = asyncio.create_task(queue.wait(approval_id, timeout=1.0))
wait_decision_task = asyncio.create_task(
get_dispatcher().dispatch(
"wait",
"exec.approval.waitDecision",
{"id": approval_id},
ctx,
)
)
await asyncio.sleep(0.05)
assert wait_task.done() is False
assert wait_decision_task.done() is False
with pytest.raises(ValueError, match="in progress|not approved"):
queue.consume(approval_id)
release_mutation.set()
approve_result = await approve_task
assert approve_result.error is None, approve_result.error
assert await wait_task is True
wait_decision_result = await wait_decision_task
assert wait_decision_result.error is None, wait_decision_result.error
assert wait_decision_result.payload["approved"] is True
assert wait_decision_result.payload["resolved"] is True
queue.consume(approval_id)
assert queue.status(approval_id)["consumed"] is True
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_recovers_complete_failure_after_grant_apply(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import get_dispatcher
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
ctx = _ctx(manager, scopes=frozenset(["operator.approvals"]))
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
original_complete = queue.complete_claimed_resolution
attempts = 0
def fail_once_then_complete(*args, **kwargs) -> None:
nonlocal attempts
attempts += 1
if attempts == 1:
raise RuntimeError("transient complete failed")
original_complete(*args, **kwargs)
monkeypatch.setattr(queue, "complete_claimed_resolution", fail_once_then_complete)
result = await get_dispatcher().dispatch(
"approve",
"exec.approval.resolve",
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
ctx,
)
assert result.error is None, result.error
assert attempts == 2
assert queue.status(approval_id)["resolved"] is True
assert queue.status(approval_id)["approved"] is True
context = await get_run_context(
manager,
manager.node.session_key,
config=ctx.config,
workspace="/tmp/ws",
)
assert ("example.com", "chat") in [(grant.domain, grant.scope) for grant in context.domains]
reset_approval_queue()
@pytest.mark.asyncio
async def test_exec_approval_resolve_finalize_failure_does_not_land_grant(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.gateway.rpc import get_dispatcher
from opensquilla.sandbox.escalation import build_network_approval_params
from opensquilla.sandbox.network_guard import NetworkDecision
from opensquilla.sandbox.run_context import get_run_context
reset_approval_queue()
manager = _SessionManager()
ctx = _ctx(manager, scopes=frozenset(["operator.approvals"]))
params = build_network_approval_params(
NetworkDecision(
status="ask",
normalized_host="example.com",
reason="unknown_domain",
source=None,
),
session_key=manager.node.session_key,
workspace="/tmp/ws",
fingerprint="fp123",
)
assert params is not None
queue = get_approval_queue()
approval_id = queue.request(namespace="exec", params=params)
def fail_finalize(*args, **kwargs) -> None:
raise RuntimeError("finalize failed")
monkeypatch.setattr(queue, "finalize_claimed_resolution", fail_finalize)
result = await get_dispatcher().dispatch(
"r1",
"exec.approval.resolve",
{"id": approval_id, "approved": True, "choice": "allow_same_type"},
ctx,
)
assert result.error is not None
assert "finalize failed" in result.error.message
pending = queue.get(approval_id)
assert pending.resolved is False
assert queue.list_pending("exec")[0]["id"] == approval_id
context = await get_run_context(
manager,
manager.node.session_key,
config=ctx.config,
workspace="/tmp/ws",
)
assert context.domains == ()
reset_approval_queue()
def test_claimed_approval_reappears_after_claim_lease_expires(tmp_path) -> None:
from opensquilla.application.approval_queue import ApprovalQueue
db_path = tmp_path / "approval_queue.sqlite"
queue = ApprovalQueue(db_path=str(db_path), claim_ttl_seconds=0)
approval_id = queue.request(namespace="exec", params={"command": "echo ok"})
queue.claim_resolution(approval_id)
queue.close()
reloaded = ApprovalQueue(db_path=str(db_path), claim_ttl_seconds=0)
assert [item["id"] for item in reloaded.list_pending("exec")] == [approval_id]
reloaded.resolve(approval_id, False)
assert reloaded.status(approval_id)["resolved"] is True
assert reloaded.status(approval_id)["approved"] is False
reloaded.close()
@pytest.mark.asyncio
async def test_rpc_mount_remove_updates_resolved_overlay_for_current_tool_mounts() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_mount_remove
from opensquilla.sandbox.escalation import current_tool_mounts, remember_resolved_run_context
from opensquilla.sandbox.run_context import MountGrant, RunContext
from opensquilla.sandbox.run_mode import RunMode
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
manager = _SessionManager()
ctx = _ctx(manager)
remembered = RunContext(
run_mode=RunMode.STANDARD,
workspace="/tmp/ws",
mounts=(MountGrant(path="/tmp/ws/extras", access="ro", scope="chat"),),
source="saved",
)
remember_resolved_run_context(
manager.node.session_key,
"/tmp/ws",
remembered,
session_manager=manager,
config=ctx.config,
)
manager.node.origin = remembered.to_origin_payload() and {
"sandbox_run_context": remembered.to_origin_payload()
}
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
workspace_dir="/tmp/ws",
session_key=manager.node.session_key,
sandbox_mounts=[{"path": "/tmp/ws/extras", "access": "ro"}],
sandbox_run_context=remembered,
)
)
try:
assert current_tool_mounts() == [{"path": "/tmp/ws/extras", "access": "ro"}]
result = await _handle_sandbox_mount_remove(
{"sessionKey": manager.node.session_key, "path": "/tmp/ws/extras"},
ctx,
)
finally:
current_tool_context.reset(token)
assert result["mounts"] == []
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
workspace_dir="/tmp/ws",
session_key=manager.node.session_key,
sandbox_mounts=[{"path": "/tmp/ws/extras", "access": "ro"}],
sandbox_run_context=remembered,
)
)
try:
assert current_tool_mounts() == []
finally:
current_tool_context.reset(token)
@pytest.mark.asyncio
async def test_rpc_mount_remove_chat_scope_leaves_user_scope_mount_visible() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_mount_remove
from opensquilla.sandbox.path_validation import normalize_path
from opensquilla.sandbox.run_context import MountGrant, RunContext, get_run_context
from opensquilla.sandbox.run_mode import RunMode
from opensquilla.sandbox.user_grants import upsert_mount_grant
manager = _SessionManager()
ctx = _ctx(manager)
path = "/tmp/shared-mount"
normalized_path = str(normalize_path(path))
upsert_mount_grant({"path": path, "access": "ro", "scope": "workspace"})
manager.node.origin = {
"sandbox_run_context": RunContext(
run_mode=RunMode.STANDARD,
workspace="/tmp/ws",
mounts=(MountGrant(path=path, access="rw", scope="chat"),),
source="saved",
).to_origin_payload()
}
result = await _handle_sandbox_mount_remove(
{"sessionKey": manager.node.session_key, "path": path, "scope": "chat"},
ctx,
)
assert result["mounts"] == [
{"path": normalized_path, "access": "ro", "scope": "workspace"}
]
context = await get_run_context(
manager,
manager.node.session_key,
config=ctx.config,
workspace="/tmp/ws",
)
assert [(grant.path, grant.access, grant.scope) for grant in context.mounts] == [
(normalized_path, "ro", "workspace")
]
@pytest.mark.asyncio
async def test_rpc_domain_remove_updates_resolved_overlay_for_current_tool_context() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_domain_remove
from opensquilla.sandbox.escalation import (
current_tool_run_context,
remember_resolved_run_context,
)
from opensquilla.sandbox.network_guard import decide_network_access
from opensquilla.sandbox.run_context import DomainGrant, RunContext
from opensquilla.sandbox.run_mode import RunMode
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
manager = _SessionManager()
ctx = _ctx(manager)
remembered = RunContext(
run_mode=RunMode.STANDARD,
workspace="/tmp/ws",
domains=(DomainGrant(domain="example.com", scope="chat"),),
source="saved",
)
remember_resolved_run_context(
manager.node.session_key,
"/tmp/ws",
remembered,
session_manager=manager,
config=ctx.config,
)
manager.node.origin = {"sandbox_run_context": remembered.to_origin_payload()}
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
workspace_dir="/tmp/ws",
session_key=manager.node.session_key,
sandbox_run_context=remembered,
)
)
try:
merged = current_tool_run_context()
assert merged is not None
assert decide_network_access("example.com", merged).status == "allow"
result = await _handle_sandbox_domain_remove(
{"sessionKey": manager.node.session_key, "domain": "example.com"},
ctx,
)
finally:
current_tool_context.reset(token)
assert result["domains"] == []
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
workspace_dir="/tmp/ws",
session_key=manager.node.session_key,
sandbox_run_context=remembered,
)
)
try:
merged = current_tool_run_context()
assert merged is not None
assert decide_network_access("example.com", merged).status == "ask"
finally:
current_tool_context.reset(token)
@pytest.mark.asyncio
async def test_rpc_domain_remove_chat_scope_leaves_user_scope_domain_visible() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_domain_remove
from opensquilla.sandbox.run_context import DomainGrant, RunContext, get_run_context
from opensquilla.sandbox.run_mode import RunMode
from opensquilla.sandbox.user_grants import upsert_domain_grant
manager = _SessionManager()
ctx = _ctx(manager)
upsert_domain_grant(
{"domain": "example.com", "scope": "workspace", "source": "manual"}
)
manager.node.origin = {
"sandbox_run_context": RunContext(
run_mode=RunMode.STANDARD,
workspace="/tmp/ws",
domains=(DomainGrant(domain="example.com", scope="chat", source="manual"),),
source="saved",
).to_origin_payload()
}
result = await _handle_sandbox_domain_remove(
{"sessionKey": manager.node.session_key, "domain": "example.com", "scope": "chat"},
ctx,
)
assert result["domains"] == [
{"domain": "example.com", "scope": "workspace", "source": "manual"}
]
context = await get_run_context(
manager,
manager.node.session_key,
config=ctx.config,
workspace="/tmp/ws",
)
assert [(grant.domain, grant.scope) for grant in context.domains] == [
("example.com", "workspace")
]
@pytest.mark.asyncio
async def test_rpc_sandbox_status_reports_backend_managed_network_and_run_mode() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_status
manager = _SessionManager()
result = await _handle_sandbox_status({}, _ctx(manager))
assert result["run_mode"] == "standard"
assert result["run_mode_label"] == "Standard-Sandbox"
assert result["execution_target"] == "sandbox"
assert result["posture"] == "standard"
assert result["backend"] == "noop"
assert result["managed_network"] == "ready"
assert result["sandbox"] == {
"sandbox": True,
"security_grading": True,
"network_default": "proxy_allowlist",
}
catalog_by_id = {
bundle["bundle_id"]: set(bundle["domains"])
for bundle in result["bundle_catalog"]
}
expected_catalog_subsets = {
"python-package-install": {
"pypi.org",
"files.pythonhosted.org",
"pypi.python.org",
"bootstrap.pypa.io",
},
"node-package-install": {
"registry.npmjs.org",
"registry.yarnpkg.com",
"yarnpkg.com",
"nodejs.org",
},
"rust-package-install": {
"crates.io",
"static.crates.io",
"index.crates.io",
"github.com",
"objects.githubusercontent.com",
},
"go-package-install": {
"proxy.golang.org",
"sum.golang.org",
"go.dev",
"golang.org",
"storage.googleapis.com",
},
"github-default": {
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"codeload.github.com",
"objects.githubusercontent.com",
},
}
for bundle_id, expected_domains in expected_catalog_subsets.items():
assert bundle_id in catalog_by_id
assert expected_domains.issubset(catalog_by_id[bundle_id])
assert result["permissions"] == {"default_mode": "off"}
@pytest.mark.asyncio
async def test_rpc_sandbox_status_reports_full_host_access_without_managed_controls() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_status
result = await _handle_sandbox_status(
{},
_ctx(
_SessionManager(),
run_mode="full",
sandbox=False,
security_grading=False,
permissions_default_mode="full",
),
)
assert result["run_mode"] == "full"
assert result["run_mode_label"] == "Full Host Access"
assert result["execution_target"] == "host"
assert result["posture"] == "full"
assert result["managed_network"] == "inactive"
@pytest.mark.asyncio
async def test_rpc_sandbox_explain_returns_status_messages_and_optional_context() -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_explain
from opensquilla.sandbox.run_context import RunContext, persist_run_context
from opensquilla.sandbox.run_mode import RunMode
manager = _SessionManager()
await persist_run_context(
manager,
manager.node.session_key,
RunContext(run_mode=RunMode.TRUSTED, workspace="/tmp/ws", source="saved"),
)
result = await _handle_sandbox_explain(
{"sessionKey": manager.node.session_key},
_ctx(manager),
)
assert result["status"]["run_mode"] == "standard"
assert result["runContext"]["runMode"] == "trusted"
assert result["messages"] == [
{"kind": "run_mode", "message": "Run mode is standard."},
{
"kind": "managed_network",
"message": "Managed network allowlist is ready.",
},
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
("handler_name", "params"),
[
("_handle_sandbox_workspace_set", {"workspace": "/tmp/ws/project"}),
("_handle_sandbox_mount_remove", {"path": "/tmp/ws/extras"}),
("_handle_sandbox_domain_add", {"domain": "pypi.org"}),
("_handle_sandbox_domain_remove", {"domain": "pypi.org"}),
("_handle_sandbox_bundle_enable", {"bundleId": "python-package-install"}),
("_handle_sandbox_bundle_disable", {"bundleId": "python-package-install"}),
],
)
async def test_rpc_sandbox_mutations_reject_non_owner(
handler_name: str,
params: dict[str, str],
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
from opensquilla.gateway.rpc import RpcHandlerError
manager = _SessionManager()
handler = getattr(rpc_sandbox, handler_name)
with pytest.raises(RpcHandlerError, match="requires owner principal"):
await handler(
{"sessionKey": manager.node.session_key, **params},
_ctx(manager, is_owner=False),
)
assert manager.node.origin is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
("handler_name", "params", "message"),
[
("_handle_sandbox_domain_add", {}, "params.domain is required"),
("_handle_sandbox_domain_remove", {"domain": ""}, "params.domain is required"),
("_handle_sandbox_bundle_enable", {}, "params.bundleId is required"),
(
"_handle_sandbox_bundle_enable",
{"bundleId": "unknown-package-install"},
"unknown_package_bundle",
),
(
"_handle_sandbox_bundle_disable",
{"bundle_id": " "},
"params.bundleId is required",
),
("_handle_sandbox_workspace_set", {}, "params.workspace is required"),
],
)
async def test_rpc_sandbox_invalid_params_do_not_create_missing_session(
handler_name: str,
params: dict[str, str],
message: str,
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
manager = _SessionManager()
missing_session_key = "agent:main:webchat:missing"
handler = getattr(rpc_sandbox, handler_name)
with pytest.raises(ValueError, match=message):
await handler(
{"sessionKey": missing_session_key, **params},
_ctx(manager),
)
assert missing_session_key not in manager.sessions
assert manager.created == []
@pytest.mark.asyncio
async def test_rpc_sandbox_path_pick_validates_workspace_selection(
monkeypatch: pytest.MonkeyPatch,
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
manager = _SessionManager()
monkeypatch.setattr(rpc_sandbox, "_pick_directory_path", lambda initial_dir=None: "/etc")
with pytest.raises(ValueError, match="sensitive_path"):
await rpc_sandbox._handle_sandbox_path_pick(
{
"sessionKey": manager.node.session_key,
"kind": "workspace",
},
_ctx(manager),
)
@pytest.mark.asyncio
async def test_rpc_sandbox_path_pick_returns_valid_mount_selection(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
manager = _SessionManager()
selected = tmp_path / "external"
selected.mkdir()
monkeypatch.setattr(
rpc_sandbox,
"_pick_directory_path",
lambda initial_dir=None: str(selected),
)
result = await rpc_sandbox._handle_sandbox_path_pick(
{
"sessionKey": manager.node.session_key,
"kind": "mount",
"access": "ro",
},
_ctx(manager),
)
assert result == {"path": str(selected), "kind": "mount"}
@pytest.mark.asyncio
async def test_rpc_sandbox_path_list_returns_parent_directory_entries(
tmp_path,
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
manager = _SessionManager()
parent = tmp_path / "parent"
child = parent / "child"
sibling = parent / "sibling"
file_entry = parent / "notes.txt"
child.mkdir(parents=True)
sibling.mkdir()
file_entry.write_text("not a directory", encoding="utf-8")
handler = getattr(rpc_sandbox, "_handle_sandbox_path_list", None)
assert callable(handler)
result = await handler(
{
"sessionKey": manager.node.session_key,
"path": str(child),
},
_ctx(manager),
)
assert result["path"] == str(child)
assert result["parentPath"] == str(parent)
entries_by_path = {entry["path"]: entry for entry in result["entries"]}
entries_by_name = {entry["name"]: entry for entry in result["entries"]}
assert entries_by_name[".."] == {
"name": "..",
"path": str(parent),
"kind": "directory",
"selectable": True,
}
assert {
str(child),
str(sibling),
str(file_entry),
}.issubset(entries_by_path)
assert entries_by_path[str(child)]["name"] == "child"
assert entries_by_path[str(child)]["kind"] == "directory"
assert entries_by_path[str(child)]["selectable"] is True
assert entries_by_path[str(sibling)]["name"] == "sibling"
assert entries_by_path[str(sibling)]["kind"] == "directory"
assert entries_by_path[str(sibling)]["selectable"] is True
assert entries_by_path[str(file_entry)]["name"] == "notes.txt"
assert entries_by_path[str(file_entry)]["kind"] == "file"
assert entries_by_path[str(file_entry)]["selectable"] is True
@pytest.mark.asyncio
async def test_rpc_sandbox_path_list_requires_owner(tmp_path) -> None:
from opensquilla.gateway.rpc import RpcHandlerError
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_path_list
manager = _SessionManager()
target = tmp_path / "target"
target.mkdir()
with pytest.raises(RpcHandlerError, match="requires owner principal"):
await _handle_sandbox_path_list(
{
"sessionKey": manager.node.session_key,
"path": str(target),
},
_ctx(manager, is_owner=False),
)
@pytest.mark.asyncio
async def test_rpc_sandbox_path_list_supports_parent_row_and_child_drilldown(
tmp_path,
) -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_path_list
manager = _SessionManager()
parent = tmp_path / "parent"
child = parent / "child"
grandchild = child / "grandchild"
child_file = child / "inside.txt"
child.mkdir(parents=True)
grandchild.mkdir()
child_file.write_text("inside", encoding="utf-8")
result = await _handle_sandbox_path_list(
{
"sessionKey": manager.node.session_key,
"path": str(child),
"browseChildren": True,
},
_ctx(manager),
)
assert result["path"] == str(child)
assert result["parentPath"] == str(child)
entries_by_name = {entry["name"]: entry for entry in result["entries"]}
assert entries_by_name[".."] == {
"name": "..",
"path": str(parent),
"kind": "directory",
"selectable": True,
}
assert entries_by_name["grandchild"]["path"] == str(grandchild)
assert entries_by_name["grandchild"]["kind"] == "directory"
assert entries_by_name["grandchild"]["selectable"] is True
assert entries_by_name["inside.txt"]["path"] == str(child_file)
assert entries_by_name["inside.txt"]["kind"] == "file"
assert entries_by_name["inside.txt"]["selectable"] is True
@pytest.mark.asyncio
async def test_rpc_sandbox_path_browser_selection_is_validated_on_workspace_save(
tmp_path,
) -> None:
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_workspace_set
manager = _SessionManager()
selected = tmp_path / ".aws" / "credentials"
selected.parent.mkdir()
selected.write_text("secret", encoding="utf-8")
with pytest.raises(ValueError, match="sensitive_path"):
await _handle_sandbox_workspace_set(
{
"sessionKey": manager.node.session_key,
"workspace": str(selected),
},
_ctx(manager),
)
assert manager.node.origin is None
@pytest.mark.asyncio
@pytest.mark.parametrize(
("handler_name", "params", "message"),
[
("_handle_sandbox_domain_add", {"domain": "127.0.0.1"}, "ip_literal"),
("_handle_sandbox_domain_remove", {"domain": "*.com"}, "broad_wildcard"),
("_handle_sandbox_workspace_set", {"workspace": "/"}, "sensitive_path"),
(
"_handle_sandbox_workspace_set",
{"workspacePath": "/tmp/ws/.aws/credentials"},
"sensitive_path",
),
("_handle_sandbox_mount_add", {"path": "/etc/shadow"}, "sensitive_path"),
("_handle_sandbox_mount_remove", {"path": "/"}, "sensitive_path"),
],
)
async def test_rpc_sandbox_semantic_validation_does_not_create_missing_session(
handler_name: str,
params: dict[str, str],
message: str,
) -> None:
import opensquilla.gateway.rpc_sandbox as rpc_sandbox
manager = _SessionManager()
missing_session_key = "agent:main:webchat:missing"
handler = getattr(rpc_sandbox, handler_name)
with pytest.raises(ValueError, match=message):
await handler(
{"sessionKey": missing_session_key, **params},
_ctx(manager),
)
assert missing_session_key not in manager.sessions
assert manager.created == []
def test_non_owner_route_full_hint_coerces_to_trusted() -> None:
from opensquilla.gateway.routing import build_web_route_envelope, tool_context_from_envelope
from opensquilla.sandbox.run_context import RunContext
from opensquilla.sandbox.run_mode import RunMode
envelope = build_web_route_envelope(
session_key="agent:main:webchat:abc",
principal_is_owner=False,
)
envelope.metadata["run_mode"] = "full"
envelope.metadata["sandbox_run_context"] = RunContext(
run_mode=RunMode.FULL,
source="route_metadata",
).to_origin_payload()
ctx = tool_context_from_envelope(envelope, is_owner=False)
assert ctx.run_mode == "trusted"
assert ctx.sandbox_run_context is not None
assert ctx.sandbox_run_context.run_mode is RunMode.TRUSTED
def test_non_owner_route_trusted_hint_is_preserved() -> None:
from opensquilla.gateway.routing import build_web_route_envelope, tool_context_from_envelope
from opensquilla.sandbox.run_context import RunContext
from opensquilla.sandbox.run_mode import RunMode
envelope = build_web_route_envelope(
session_key="agent:main:webchat:abc",
principal_is_owner=False,
)
envelope.metadata["run_mode"] = "trusted"
envelope.metadata["sandbox_run_context"] = RunContext(
run_mode=RunMode.TRUSTED,
source="route_metadata",
).to_origin_payload()
ctx = tool_context_from_envelope(envelope, is_owner=False)
assert ctx.run_mode == "trusted"
assert ctx.sandbox_run_context is not None
assert ctx.sandbox_run_context.run_mode is RunMode.TRUSTED
@pytest.mark.asyncio
async def test_rpc_sandbox_resume_clears_denial_pause(monkeypatch) -> None:
# The owner-scoped resume RPC is the recovery surface for the sticky denial
# pause (issue #469): it must clear the pause so the run can continue.
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_resume
from opensquilla.sandbox.governance import DenialLedger
ledger = DenialLedger(threshold=3)
manager = _SessionManager()
key = manager.node.session_key
await ledger.mark_paused(key)
monkeypatch.setattr(
"opensquilla.sandbox.integration.get_runtime",
lambda: SimpleNamespace(ledger=ledger),
)
result = await _handle_sandbox_resume({"sessionKey": key}, _ctx(manager))
assert result["resumed"] is True
assert result["autonomousPaused"] is False
assert await ledger.is_paused(key) is False
@pytest.mark.asyncio
async def test_rpc_sandbox_resume_rejects_non_owner() -> None:
from opensquilla.gateway.rpc import RpcHandlerError
from opensquilla.gateway.rpc_sandbox import _handle_sandbox_resume
manager = _SessionManager()
with pytest.raises(RpcHandlerError, match="requires owner principal"):
await _handle_sandbox_resume(
{"sessionKey": manager.node.session_key},
_ctx(manager, is_owner=False),
)