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

510 lines
16 KiB
Python

from __future__ import annotations
import json
from types import SimpleNamespace
import pytest
from opensquilla.tools.types import CallerKind, ToolContext, ToolError, current_tool_context
@pytest.mark.asyncio
async def test_trusted_sandbox_does_not_mark_shell_host_elevated(monkeypatch) -> None:
from opensquilla.tools.builtin import shell
calls: list[tuple[str, object]] = []
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
async def _fake_gate_action(**kwargs):
calls.append(("gate", kwargs))
policy = SimpleNamespace()
request = SimpleNamespace(cwd="/tmp", action_kind="shell.exec", policy=policy)
return object(), policy, request
async def _fake_run_under_backend(request, *, runtime=None):
calls.append(("backend", request))
return SimpleNamespace(
returncode=0,
stdout="sandboxed\n",
stderr="",
backend_notes=(),
)
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
monkeypatch.setattr(shell, "run_under_backend", _fake_run_under_backend)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
)
token = current_tool_context.set(
ToolContext(is_owner=True, caller_kind=CallerKind.WEB, session_key="s1", run_mode="trusted")
)
try:
result = await shell.exec_command("echo hi")
finally:
current_tool_context.reset(token)
assert "sandboxed" in result
assert [name for name, _ in calls] == ["gate", "backend"]
@pytest.mark.asyncio
async def test_ordinary_approval_result_does_not_carry_elevated_mode(monkeypatch) -> None:
from opensquilla.application.approval_queue import ApprovalQueue
queue = ApprovalQueue(db_path=":memory:")
try:
approval_id = queue.request(
namespace="exec",
params={"sessionKey": "s1", "command": "rm x"},
)
queue.resolve(approval_id, True)
status = queue.status(approval_id)
assert "elevatedMode" not in status["params"]
finally:
queue.close()
@pytest.mark.asyncio
async def test_warnlist_shell_uses_sandbox_gate_without_exec_approval(monkeypatch) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.tools.builtin import shell
calls: list[tuple[str, object]] = []
reset_approval_queue()
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
async def _fake_gate_action(**kwargs):
calls.append(("gate", kwargs))
policy = SimpleNamespace()
request = SimpleNamespace(cwd="/tmp", action_kind="shell.exec", policy=policy)
return object(), policy, request
async def _fake_run_under_backend(request, *, runtime=None):
calls.append(("backend", request))
return SimpleNamespace(
returncode=0,
stdout="sandboxed after approval\n",
stderr="",
backend_notes=(),
)
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
monkeypatch.setattr(shell, "run_under_backend", _fake_run_under_backend)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(
allowed=True,
needs_approval=True,
reason="command requires approval",
),
)
token = current_tool_context.set(
ToolContext(is_owner=True, caller_kind=CallerKind.CLI, session_key="s1")
)
try:
result = await shell.exec_command("rm x")
finally:
current_tool_context.reset(token)
reset_approval_queue()
assert "sandboxed after approval" in result
assert get_approval_queue().list_pending("exec") == []
assert [name for name, _ in calls] == ["gate", "backend"]
hints = calls[0][1]["hints"] # type: ignore[index]
assert hints.high_impact is True
@pytest.mark.asyncio
async def test_trusted_workspace_shell_cleanup_stays_out_of_locked_approval(
monkeypatch,
tmp_path,
) -> None:
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.tools.builtin import shell
calls: list[tuple[str, object]] = []
reset_approval_queue()
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
async def _fake_gate_action(**kwargs):
calls.append(("gate", kwargs))
policy = SimpleNamespace()
request = SimpleNamespace(cwd=tmp_path, action_kind="shell.exec", policy=policy)
return object(), policy, request
async def _fake_run_under_backend(request, *, runtime=None):
calls.append(("backend", request))
return SimpleNamespace(
returncode=0,
stdout="shell-workspace-ok\n",
stderr="",
backend_notes=(),
)
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
monkeypatch.setattr(shell, "run_under_backend", _fake_run_under_backend)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(
allowed=True,
needs_approval=True,
reason="command requires approval",
),
)
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.WEB,
session_key="s1",
run_mode="trusted",
workspace_dir=str(tmp_path),
)
)
try:
result = await shell.exec_command(
'printf "%s\\n" shell-workspace-ok > sandbox_probe_shell.txt '
"&& cat sandbox_probe_shell.txt && rm sandbox_probe_shell.txt",
workdir=str(tmp_path),
)
finally:
current_tool_context.reset(token)
reset_approval_queue()
assert "shell-workspace-ok" in result
assert get_approval_queue().list_pending("exec") == []
assert [name for name, _ in calls] == ["gate", "backend"]
hints = calls[0][1]["hints"] # type: ignore[index]
assert hints.high_impact is False
@pytest.mark.asyncio
async def test_backend_denial_does_not_fall_back_to_host(monkeypatch) -> None:
from opensquilla.tools.builtin import shell
calls: list[str] = []
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
class _Proc:
pid = 999999
returncode = 0
async def wait(self):
return 0
def terminate(self) -> None:
return None
def kill(self) -> None:
return None
async def _fake_gate_action(**kwargs):
calls.append("gate")
policy = SimpleNamespace()
request = SimpleNamespace(
cwd="/tmp",
action_kind="shell.exec",
policy=policy,
reason="",
)
return object(), policy, request
backend_results = [
SimpleNamespace(
returncode=1,
stdout="",
stderr="denied",
backend_notes=("exec denied",),
),
SimpleNamespace(
returncode=0,
stdout="sandboxed again\n",
stderr="",
backend_notes=(),
),
]
async def _fake_run_under_backend(request, *, runtime=None):
calls.append("backend")
return backend_results.pop(0)
async def _fake_escalate_backend_denial(*args, **kwargs):
calls.append("escalate")
return object()
async def _fake_create_subprocess_shell(*args, **kwargs):
calls.append("host")
assert kwargs["stdout"] != shell.asyncio.subprocess.PIPE
assert kwargs["stderr"] == shell.asyncio.subprocess.STDOUT
if shell.os.name == "posix":
assert kwargs["start_new_session"] is True
return _Proc()
class _FakeTemporaryFile:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb) -> None:
return None
def flush(self) -> None:
return None
def seek(self, offset: int) -> None:
return None
def read(self) -> bytes:
return b"host fallback should not run\n"
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
monkeypatch.setattr(shell, "run_under_backend", _fake_run_under_backend)
monkeypatch.setattr(shell, "escalate_backend_denial", _fake_escalate_backend_denial)
monkeypatch.setattr(shell.asyncio, "create_subprocess_shell", _fake_create_subprocess_shell)
monkeypatch.setattr(shell.tempfile, "TemporaryFile", lambda: _FakeTemporaryFile())
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
)
token = current_tool_context.set(
ToolContext(is_owner=True, caller_kind=CallerKind.CLI, session_key="s1")
)
try:
with pytest.raises(ToolError, match="host fallback disabled"):
await shell.exec_command("echo hi")
finally:
current_tool_context.reset(token)
assert calls == ["gate", "backend", "escalate"]
@pytest.mark.asyncio
async def test_full_host_access_code_exec_resolves_host_python(monkeypatch, tmp_path) -> None:
from opensquilla.tools.builtin import code_exec
resolve_calls: list[bool] = []
child_env: dict[str, str] = {}
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
workspace = tmp_path
class _Proc:
returncode = 0
async def communicate(self):
return b"host python\n", b""
def _fake_resolve_python_bin(*, sandbox_enabled: bool) -> str:
resolve_calls.append(sandbox_enabled)
return "/host/python"
async def _fake_create_subprocess_exec(*args, **kwargs):
assert args[:2] == ("/host/python", "-c")
child_env.update(kwargs["env"])
return _Proc()
monkeypatch.setenv("SystemRoot", r"C:\Windows")
monkeypatch.setenv("WINDIR", r"C:\Windows")
monkeypatch.setenv("ComSpec", r"C:\Windows\System32\cmd.exe")
monkeypatch.setenv("TEMP", str(tmp_path / "temp"))
monkeypatch.setenv("TMP", str(tmp_path / "temp"))
monkeypatch.setattr(code_exec, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(code_exec, "_resolve_python_bin", _fake_resolve_python_bin)
monkeypatch.setattr(code_exec.asyncio, "create_subprocess_exec", _fake_create_subprocess_exec)
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
session_key="s1",
run_mode="full",
workspace_dir=str(tmp_path),
)
)
try:
result = await code_exec.execute_code("print('hi')")
finally:
current_tool_context.reset(token)
payload = json.loads(result)
assert payload["exit_code"] == 0
assert payload["stdout"] == "host python\n"
assert resolve_calls == [False]
folded_env = {key.upper(): value for key, value in child_env.items()}
assert folded_env["SYSTEMROOT"] == r"C:\Windows"
assert folded_env["WINDIR"] == r"C:\Windows"
assert folded_env["COMSPEC"] == r"C:\Windows\System32\cmd.exe"
assert folded_env["TEMP"] == str(tmp_path / "temp")
assert folded_env["TMP"] == str(tmp_path / "temp")
@pytest.mark.asyncio
async def test_full_host_access_shell_uses_host_and_skips_sandbox_gates(
monkeypatch,
tmp_path,
) -> None:
from opensquilla.tools.builtin import shell
calls: list[str] = []
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
async def _fail_gate_action(**kwargs):
pytest.fail("Full Host Access shell should not enter sandbox gate_action")
async def _fake_host_shell_command(*args, **kwargs):
calls.append("host")
return "exit_code=1\nhead: cannot open '/etc/shadow' for reading: Permission denied\n"
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell, "gate_action", _fail_gate_action)
monkeypatch.setattr(shell, "_run_host_shell_command", _fake_host_shell_command)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
)
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
session_key="s1",
run_mode="full",
workspace_dir=str(tmp_path),
)
)
try:
result = await shell.exec_command("head -n 1 /etc/shadow 2>&1; echo exit=$?")
finally:
current_tool_context.reset(token)
assert calls == ["host"]
assert "Permission denied" in result
assert "sensitive_path" not in result
@pytest.mark.asyncio
async def test_full_host_access_shell_strips_managed_proxy_environment(
monkeypatch,
tmp_path,
) -> None:
from opensquilla.tools.builtin import shell
seen_env: dict[str, str] = {}
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
async def _fake_host_shell_command(*args, **kwargs):
seen_env.update(kwargs["env"])
return "exit_code=0\n"
monkeypatch.setenv("HTTP_PROXY", "http://127.0.0.1:48123")
monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:48123")
monkeypatch.setenv("OPENSQUILLA_SANDBOX_NETWORK", "proxy_allowlist")
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell, "_run_host_shell_command", _fake_host_shell_command)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
)
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
session_key="s1",
run_mode="full",
workspace_dir=str(tmp_path),
)
)
try:
await shell.exec_command("env")
finally:
current_tool_context.reset(token)
assert "OPENSQUILLA_SANDBOX_NETWORK" not in seen_env
assert "HTTP_PROXY" not in seen_env
assert "HTTPS_PROXY" not in seen_env
@pytest.mark.asyncio
async def test_full_host_access_background_strips_managed_proxy_environment(
monkeypatch,
tmp_path,
) -> None:
from opensquilla.tools.builtin import shell
seen_env: dict[str, str] = {}
class _Runtime:
effective = SimpleNamespace(sandbox_enabled=True)
class _FakeProcess:
stdout = None
stdin = None
returncode = 0
async def wait(self) -> int:
return 0
async def _fake_create_subprocess_shell(*args, **kwargs):
seen_env.update(kwargs["env"])
return _FakeProcess()
monkeypatch.setenv("HTTP_PROXY", "http://127.0.0.1:48123")
monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:48123")
monkeypatch.setenv("OPENSQUILLA_SANDBOX_NETWORK", "proxy_allowlist")
monkeypatch.setattr(shell, "get_runtime", lambda: _Runtime())
monkeypatch.setattr(shell.asyncio, "create_subprocess_shell", _fake_create_subprocess_shell)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
)
token = current_tool_context.set(
ToolContext(
is_owner=True,
caller_kind=CallerKind.CLI,
session_key="s1",
run_mode="full",
workspace_dir=str(tmp_path),
)
)
try:
result = await shell.background_process("env")
session_id = result.splitlines()[0].split("=", 1)[1]
session = shell._bg_sessions[session_id]
assert session.collector_task is not None
await session.collector_task
finally:
current_tool_context.reset(token)
assert "OPENSQUILLA_SANDBOX_NETWORK" not in seen_env
assert "HTTP_PROXY" not in seen_env
assert "HTTPS_PROXY" not in seen_env