1556 lines
49 KiB
Python
1556 lines
49 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from opensquilla.tools.types import CallerKind, ToolContext, current_tool_context
|
|
|
|
|
|
def _windows_runtime() -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
effective=SimpleNamespace(sandbox_enabled=True),
|
|
backend=SimpleNamespace(name="windows_default"),
|
|
)
|
|
|
|
|
|
def _configure_approval_queue(
|
|
monkeypatch,
|
|
tmp_path,
|
|
mode: str,
|
|
*,
|
|
allow_patterns: list[str] | None = None,
|
|
deny_patterns: list[str] | None = None,
|
|
) -> None:
|
|
from opensquilla.application import approval_queue as approval_queue_mod
|
|
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
|
|
|
|
monkeypatch.setattr(
|
|
approval_queue_mod,
|
|
"_DEFAULT_APPROVAL_QUEUE_PATH",
|
|
tmp_path / "approval_queue.sqlite",
|
|
)
|
|
reset_approval_queue()
|
|
get_approval_queue().set_settings(
|
|
mode,
|
|
allow_patterns=allow_patterns,
|
|
deny_patterns=deny_patterns,
|
|
)
|
|
|
|
|
|
def test_windows_exec_command_uses_shell_host_wrapper(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
|
|
monkeypatch.setattr(
|
|
shell,
|
|
"_trusted_windows_powershell_path",
|
|
lambda: r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
|
)
|
|
|
|
argv = shell._sandbox_shell_backend_argv("Write-Output ok", runtime, cwd=tmp_path)
|
|
|
|
assert argv[:3] == (
|
|
sys.executable,
|
|
"-c",
|
|
shell._WINDOWS_SANDBOX_SHELL_HOST_CODE,
|
|
)
|
|
assert argv[3] == r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
assert argv[4] == "Write-Output ok"
|
|
assert argv[5] == str(tmp_path)
|
|
assert argv[6] == str(tmp_path / ".opensquilla-cache" / "shell-host")
|
|
|
|
|
|
def test_windows_exec_command_unwraps_nested_powershell_command(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
|
|
monkeypatch.setattr(
|
|
shell,
|
|
"_trusted_windows_powershell_path",
|
|
lambda: r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
|
)
|
|
|
|
argv = shell._sandbox_shell_backend_argv(
|
|
(
|
|
"& 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' "
|
|
'-NoProfile -Command "Write-Output child-ok"'
|
|
),
|
|
runtime,
|
|
cwd=tmp_path,
|
|
)
|
|
|
|
assert argv[:3] == (
|
|
sys.executable,
|
|
"-c",
|
|
shell._WINDOWS_SANDBOX_SHELL_HOST_CODE,
|
|
)
|
|
assert argv[3] == r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
assert argv[4] == "Write-Output child-ok"
|
|
|
|
direct_argv = shell._sandbox_shell_backend_argv(
|
|
'powershell.exe -NoProfile -Command "Write-Output child-ok"',
|
|
runtime,
|
|
cwd=tmp_path,
|
|
)
|
|
|
|
assert direct_argv[4] == "Write-Output child-ok"
|
|
|
|
|
|
def test_windows_exec_command_prefers_cmd_package_manager_shims(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
|
|
monkeypatch.setattr(
|
|
shell,
|
|
"_trusted_windows_powershell_path",
|
|
lambda: r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe",
|
|
)
|
|
|
|
argv = shell._sandbox_shell_backend_argv("npm view lodash version", runtime, cwd=tmp_path)
|
|
|
|
assert argv[4] == "& 'npm.cmd' 'view' 'lodash' 'version'"
|
|
|
|
|
|
def test_windows_shell_host_skips_windowsapps_git_alias(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
if os.name != "nt":
|
|
pytest.skip("Windows command shims are Windows-only")
|
|
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
alias_dir = tmp_path / "Microsoft" / "WindowsApps"
|
|
real_dir = tmp_path / "Git" / "cmd"
|
|
alias_dir.mkdir(parents=True)
|
|
real_dir.mkdir(parents=True)
|
|
(alias_dir / "git.cmd").write_text("@echo off\r\necho alias-git\r\n", encoding="utf-8")
|
|
(real_dir / "git.cmd").write_text("@echo off\r\necho real-git %1\r\n", encoding="utf-8")
|
|
monkeypatch.setenv("PATH", os.pathsep.join((str(alias_dir), str(real_dir))))
|
|
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
shell._WINDOWS_SANDBOX_SHELL_HOST_CODE,
|
|
"powershell.exe",
|
|
"git --version",
|
|
str(tmp_path),
|
|
str(tmp_path / ".opensquilla-cache" / "shell-host"),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert completed.returncode == 0
|
|
assert "real-git --version" in completed.stdout
|
|
assert "alias-git" not in completed.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auto_approve_owner_host_effect_command_runs_on_host_once(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "auto-approve")
|
|
runtime = _windows_runtime()
|
|
host_calls: list[dict[str, object]] = []
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("auto host escalation should not enter sandbox gate")
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append({"command": command, **kwargs})
|
|
return "exit_code=0\nhost-ok\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)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("winget install Tencent.QQ.NT", workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert [call["command"] for call in host_calls] == ["winget install Tencent.QQ.NT"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trusted_owner_host_effect_command_runs_on_host_without_legacy_auto_approve(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
host_calls: list[dict[str, object]] = []
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("trusted host-effect command should not enter sandbox gate")
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append({"command": command, **kwargs})
|
|
return "exit_code=0\nhost-ok\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)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("winget install Tencent.QQ.NT", workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert [call["command"] for call in host_calls] == ["winget install Tencent.QQ.NT"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trusted_powershell_file_with_installer_actions_runs_on_host(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
script_path = tmp_path / "install_dingtalk.ps1"
|
|
script_path.write_text(
|
|
"\n".join(
|
|
[
|
|
"$installer = Join-Path $PSScriptRoot 'DingTalkSetup.exe'",
|
|
(
|
|
"Invoke-WebRequest "
|
|
"https://dtapp-pub.dingtalk.com/desktop/Win/Release/DingTalkSetup.exe "
|
|
"-OutFile $installer"
|
|
),
|
|
"Start-Process $installer -Wait",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
host_calls: list[dict[str, object]] = []
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("trusted installer script should not enter sandbox gate")
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append({"command": command, **kwargs})
|
|
return "exit_code=0\nhost-ok\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)
|
|
monkeypatch.setattr(
|
|
shell,
|
|
"check_safe_bin",
|
|
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
|
|
)
|
|
|
|
command = f'powershell -NoProfile -ExecutionPolicy Bypass -File "{script_path}"'
|
|
token = current_tool_context.set(
|
|
ToolContext(
|
|
is_owner=True,
|
|
caller_kind=CallerKind.CLI,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(command, workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert [call["command"] for call in host_calls] == [command]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"command",
|
|
[
|
|
"Get-WmiObject Win32_Product | Where-Object { $_.Name -like '*DingTalk*' }",
|
|
(
|
|
"Get-ItemProperty HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* "
|
|
"| Where-Object { $_.DisplayName -like '*DingTalk*' }"
|
|
),
|
|
'Start-Process "C:\\Program Files\\DingTalk\\uninst.exe" -Wait',
|
|
"Stop-Process -Name DingTalk -Force",
|
|
],
|
|
)
|
|
async def test_trusted_windows_uninstall_flow_commands_run_on_host(
|
|
monkeypatch,
|
|
tmp_path,
|
|
command,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
host_calls: list[str] = []
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("trusted uninstall flow command should not enter sandbox gate")
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append(command)
|
|
return "exit_code=0\nhost-ok\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)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(command, workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert host_calls == [command]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trusted_host_shell_folds_windows_env_key_duplicates(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
if os.name != "nt":
|
|
pytest.skip("Windows environment variables are case-insensitive")
|
|
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
host_envs: list[dict[str, str]] = []
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_envs.append(dict(kwargs["env"]))
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setenv("SystemRoot", r"C:\Windows")
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(
|
|
"winget uninstall DingTalk",
|
|
workdir=str(tmp_path),
|
|
env={"SYSTEMROOT": r"C:\Windows"},
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert host_envs
|
|
systemroot_keys = [key for key in host_envs[0] if key.upper() == "SYSTEMROOT"]
|
|
assert systemroot_keys == ["SystemRoot"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auto_host_escalation_adds_user_windowsapps_to_host_path(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "auto-approve")
|
|
runtime = _windows_runtime()
|
|
local_appdata = tmp_path / "Users" / "lrk" / "AppData" / "Local"
|
|
windows_apps = local_appdata / "Microsoft" / "WindowsApps"
|
|
windows_apps.mkdir(parents=True)
|
|
(windows_apps / "winget.exe").write_text("", encoding="utf-8")
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
host_calls: list[dict[str, object]] = []
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("auto host escalation should not enter sandbox gate")
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append({"command": command, **kwargs})
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setenv("LOCALAPPDATA", str(local_appdata))
|
|
monkeypatch.setenv("PATH", str(bin_dir))
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", fail_gate_action)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("winget install Tencent.QQ.NT", workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert host_calls
|
|
path_entries = host_calls[0]["env"]["PATH"].split(";")
|
|
assert path_entries[-1] == str(windows_apps)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"command",
|
|
[
|
|
"winget install Tencent.QQ.NT",
|
|
'powershell -NoProfile -Command "winget install Tencent.QQ.NT"',
|
|
],
|
|
)
|
|
async def test_auto_host_escalation_honors_deny_patterns(
|
|
monkeypatch,
|
|
tmp_path,
|
|
command,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(
|
|
monkeypatch,
|
|
tmp_path,
|
|
"auto-approve",
|
|
deny_patterns=["winget *"],
|
|
)
|
|
runtime = _windows_runtime()
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("denied host-effect command should not enter sandbox gate")
|
|
|
|
async def fail_host(command, **kwargs):
|
|
raise AssertionError("denied host-effect command should not run on host")
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", fail_gate_action)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fail_host)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(command, workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
payload = json.loads(result)
|
|
assert payload["status"] == "approval_denied"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auto_host_escalation_background_process_runs_on_host_once(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
class FakeStdout:
|
|
async def read(self, _size):
|
|
return b""
|
|
|
|
class FakeProcess:
|
|
stdout = FakeStdout()
|
|
returncode = 0
|
|
|
|
async def wait(self):
|
|
return 0
|
|
|
|
def terminate(self):
|
|
self.returncode = -15
|
|
|
|
def kill(self):
|
|
self.returncode = -9
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "auto-approve")
|
|
runtime = _windows_runtime()
|
|
spawn_calls: list[dict[str, object]] = []
|
|
|
|
async def fail_gate_action(**kwargs):
|
|
raise AssertionError("auto host escalation should not enter sandbox gate")
|
|
|
|
async def fake_create_subprocess_shell(command, **kwargs):
|
|
spawn_calls.append({"command": command, **kwargs})
|
|
return FakeProcess()
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", fail_gate_action)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.background_process("msiexec /i app.msi", workdir=str(tmp_path))
|
|
session_id = result.splitlines()[0].split("=", 1)[1]
|
|
session = shell._bg_sessions[session_id]
|
|
if session.collector_task is not None:
|
|
await session.collector_task
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
shell._bg_sessions.clear()
|
|
reset_approval_queue()
|
|
|
|
assert [call["command"] for call in spawn_calls] == ["msiexec /i app.msi"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auto_host_background_batch_requests_write_path_access_before_host(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
outside = tmp_path / "outside-background.txt"
|
|
command = f"msiexec /i app.msi; Set-Content -Path {outside} -Value hi"
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
|
|
async def fail_create_subprocess_shell(*args, **kwargs):
|
|
raise AssertionError("background auto-host batch should not spawn before path approval")
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell.asyncio, "create_subprocess_shell", fail_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,
|
|
workspace_dir=str(workspace),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.background_process(command, workdir=str(workspace))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
payload = json.loads(result)
|
|
assert payload["status"] == "approval_required"
|
|
assert payload["access"] == "rw"
|
|
assert payload["path"] == str(outside.resolve(strict=False))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("mode", "is_owner"),
|
|
[
|
|
("prompt", False),
|
|
("auto-approve", False),
|
|
],
|
|
)
|
|
async def test_host_effect_command_stays_sandboxed_without_owner(
|
|
monkeypatch,
|
|
tmp_path,
|
|
mode,
|
|
is_owner,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.sandbox.config import SandboxSettings
|
|
from opensquilla.sandbox.policy import build_policy
|
|
from opensquilla.sandbox.types import SecurityLevel
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, mode)
|
|
runtime = _windows_runtime()
|
|
policy = build_policy(
|
|
SecurityLevel.STANDARD,
|
|
"shell.exec",
|
|
tmp_path,
|
|
SandboxSettings(
|
|
sandbox=True,
|
|
security_grading=True,
|
|
backend="windows_default",
|
|
network_default="none",
|
|
),
|
|
trusted=True,
|
|
)
|
|
request = SimpleNamespace(
|
|
cwd=tmp_path,
|
|
action_kind="shell.exec",
|
|
policy=policy,
|
|
reason="",
|
|
session_id="s1",
|
|
run_mode="trusted",
|
|
)
|
|
backend_calls: list[object] = []
|
|
host_calls: list[str] = []
|
|
|
|
async def fake_gate_action(**kwargs):
|
|
return object(), policy, request
|
|
|
|
async def fake_preflight(*args, **kwargs):
|
|
return None
|
|
|
|
async def fake_backend(request, *, runtime=None):
|
|
backend_calls.append(request)
|
|
return SimpleNamespace(returncode=0, stdout="sandbox-ok\n", stderr="", backend_notes=())
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append(command)
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", fake_gate_action)
|
|
monkeypatch.setattr(shell, "preflight_subprocess_managed_network", fake_preflight)
|
|
monkeypatch.setattr(shell, "_run_backend_with_managed_network", fake_backend)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
monkeypatch.setattr(
|
|
shell,
|
|
"check_safe_bin",
|
|
lambda command: SimpleNamespace(allowed=True, needs_approval=False, reason=""),
|
|
)
|
|
|
|
token = current_tool_context.set(
|
|
ToolContext(
|
|
is_owner=is_owner,
|
|
caller_kind=CallerKind.CLI,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("winget list", workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "sandbox-ok" in result
|
|
assert backend_calls
|
|
assert host_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_standard_auto_approve_owner_host_effect_command_stays_sandboxed(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.sandbox.config import SandboxSettings
|
|
from opensquilla.sandbox.policy import build_policy
|
|
from opensquilla.sandbox.types import SecurityLevel
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "auto-approve")
|
|
runtime = _windows_runtime()
|
|
policy = build_policy(
|
|
SecurityLevel.STANDARD,
|
|
"shell.exec",
|
|
tmp_path,
|
|
SandboxSettings(
|
|
sandbox=True,
|
|
security_grading=True,
|
|
backend="windows_default",
|
|
network_default="none",
|
|
),
|
|
trusted=False,
|
|
)
|
|
request = SimpleNamespace(
|
|
cwd=tmp_path,
|
|
action_kind="shell.exec",
|
|
policy=policy,
|
|
reason="",
|
|
session_id="s1",
|
|
run_mode="standard",
|
|
)
|
|
backend_calls: list[object] = []
|
|
host_calls: list[str] = []
|
|
|
|
async def fake_gate_action(**kwargs):
|
|
return object(), policy, request
|
|
|
|
async def fake_preflight(*args, **kwargs):
|
|
return None
|
|
|
|
async def fake_backend(request, *, runtime=None):
|
|
backend_calls.append(request)
|
|
return SimpleNamespace(returncode=0, stdout="sandbox-ok\n", stderr="", backend_notes=())
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append(command)
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", fake_gate_action)
|
|
monkeypatch.setattr(shell, "preflight_subprocess_managed_network", fake_preflight)
|
|
monkeypatch.setattr(shell, "_run_backend_with_managed_network", fake_backend)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="standard",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("winget list", workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "sandbox-ok" in result
|
|
assert backend_calls
|
|
assert host_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trusted_windows_host_probe_adds_user_windowsapps_to_host_env(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
local_appdata = tmp_path / "Users" / "lrk" / "AppData" / "Local"
|
|
windows_apps = local_appdata / "Microsoft" / "WindowsApps"
|
|
windows_apps.mkdir(parents=True)
|
|
(windows_apps / "winget.exe").write_text("", encoding="utf-8")
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
host_calls: list[dict[str, object]] = []
|
|
|
|
async def fake_gate_action(**kwargs):
|
|
raise AssertionError("trusted host probe should not enter sandbox gate")
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append({"command": command, **kwargs})
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setenv("LOCALAPPDATA", str(local_appdata))
|
|
monkeypatch.setenv("PATH", str(bin_dir))
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", fake_gate_action)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("where winget", workdir=str(tmp_path))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
assert "host-ok" in result
|
|
assert host_calls
|
|
assert host_calls[0]["command"] == "where winget"
|
|
assert host_calls[0]["env"]["PATH"].split(";")[-1] == str(windows_apps)
|
|
|
|
|
|
def test_windows_shell_host_blocks_icmp_diagnostics_when_proxy_allowlist(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
if os.name != "nt":
|
|
pytest.skip("Windows command shims are Windows-only")
|
|
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
env = os.environ.copy()
|
|
env["OPENSQUILLA_SANDBOX_NETWORK"] = "proxy_allowlist"
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
shell._WINDOWS_SANDBOX_SHELL_HOST_CODE,
|
|
"powershell.exe",
|
|
"ping",
|
|
str(tmp_path),
|
|
str(tmp_path / ".opensquilla-cache" / "shell-host"),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
check=False,
|
|
)
|
|
|
|
assert completed.returncode != 0
|
|
assert "blocks ICMP" in completed.stderr
|
|
|
|
|
|
def test_windows_shell_host_fallback_sets_powershell_proxy_defaults(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
if os.name != "nt":
|
|
pytest.skip("PowerShell proxy defaults are Windows-only")
|
|
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
proxy_url = "http://127.0.0.1:48123"
|
|
env = os.environ.copy()
|
|
env["HTTP_PROXY"] = proxy_url
|
|
env["HTTPS_PROXY"] = proxy_url
|
|
command = (
|
|
"try { "
|
|
"if ([System.Net.WebRequest]::DefaultWebProxy -and "
|
|
"[System.Net.WebRequest]::DefaultWebProxy.Address) { "
|
|
"Write-Output ([System.Net.WebRequest]::DefaultWebProxy.Address.AbsoluteUri) "
|
|
"} else { Write-Output 'NO_PROXY' } "
|
|
"} catch { Write-Output ('ERR:' + $_.Exception.Message) }"
|
|
)
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
shell._WINDOWS_SANDBOX_SHELL_HOST_CODE,
|
|
"powershell.exe",
|
|
command,
|
|
str(tmp_path),
|
|
str(tmp_path / ".opensquilla-cache" / "shell-host"),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
check=False,
|
|
)
|
|
|
|
assert completed.returncode == 0, completed.stderr
|
|
assert proxy_url in completed.stdout
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_exec_command_does_not_mount_program_files_tools_per_command(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.sandbox.config import SandboxSettings
|
|
from opensquilla.sandbox.policy import build_policy
|
|
from opensquilla.sandbox.types import SecurityLevel
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
policy = build_policy(
|
|
SecurityLevel.STANDARD,
|
|
"shell.exec",
|
|
tmp_path,
|
|
SandboxSettings(
|
|
sandbox=True,
|
|
security_grading=True,
|
|
backend="windows_default",
|
|
network_default="none",
|
|
),
|
|
trusted=True,
|
|
)
|
|
node_root = tmp_path / "Program Files" / "nodejs"
|
|
git_root = tmp_path / "Program Files" / "Git"
|
|
node_root.mkdir(parents=True)
|
|
(git_root / "cmd").mkdir(parents=True)
|
|
|
|
(node_root / "npm.cmd").write_text("@echo off\r\n", encoding="utf-8")
|
|
(git_root / "cmd" / "git.exe").write_text("", encoding="utf-8")
|
|
|
|
request = SimpleNamespace(
|
|
cwd=tmp_path,
|
|
action_kind="shell.exec",
|
|
policy=policy,
|
|
reason="",
|
|
session_id="s1",
|
|
run_mode="trusted",
|
|
)
|
|
|
|
async def _fake_gate_action(**kwargs):
|
|
return object(), policy, request
|
|
|
|
async def _fake_preflight(*args, **kwargs):
|
|
return None
|
|
|
|
backend_requests = []
|
|
|
|
async def _fake_run_backend(request, *, runtime=None):
|
|
backend_requests.append(request)
|
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="", backend_notes=())
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
|
|
monkeypatch.setattr(shell, "preflight_subprocess_managed_network", _fake_preflight)
|
|
monkeypatch.setattr(shell, "_run_backend_with_managed_network", _fake_run_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.CLI,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(
|
|
(
|
|
"npm view lodash version && "
|
|
"git ls-remote https://github.com/opensquilla/opensquilla.git HEAD"
|
|
),
|
|
workdir=str(tmp_path),
|
|
env={"PATH": f"{node_root}{os.pathsep}{git_root / 'cmd'}"},
|
|
)
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "ok" in result
|
|
assert backend_requests
|
|
mount_paths = {mount.host_path for mount in backend_requests[0].policy.mounts}
|
|
assert node_root not in mount_paths
|
|
assert git_root not in mount_paths
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_exec_command_uses_shared_path_envelopes(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.sandbox.config import SandboxSettings
|
|
from opensquilla.sandbox.policy import build_policy
|
|
from opensquilla.sandbox.types import SecurityLevel
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
policy = build_policy(
|
|
SecurityLevel.STANDARD,
|
|
"shell.exec",
|
|
tmp_path,
|
|
SandboxSettings(
|
|
sandbox=True,
|
|
security_grading=True,
|
|
backend="windows_default",
|
|
network_default="none",
|
|
),
|
|
trusted=True,
|
|
)
|
|
request = SimpleNamespace(
|
|
cwd=tmp_path,
|
|
action_kind="shell.exec",
|
|
policy=policy,
|
|
reason="",
|
|
session_id="s1",
|
|
run_mode="trusted",
|
|
)
|
|
|
|
async def _fake_gate_action(**kwargs):
|
|
return object(), policy, request
|
|
|
|
async def _fake_preflight(*args, **kwargs):
|
|
return None
|
|
|
|
backend_called = False
|
|
|
|
async def _fake_run_backend(request, *, runtime=None):
|
|
nonlocal backend_called
|
|
backend_called = True
|
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="", backend_notes=())
|
|
|
|
def _blocked_write_envelope(*args, **kwargs):
|
|
return {
|
|
"status": "blocked",
|
|
"reason": "sandbox_path",
|
|
"message": "shared path envelope blocked this command",
|
|
}
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
|
|
monkeypatch.setattr(shell, "preflight_subprocess_managed_network", _fake_preflight)
|
|
monkeypatch.setattr(shell, "_run_backend_with_managed_network", _fake_run_backend)
|
|
monkeypatch.setattr(shell, "_sandbox_write_path_access_envelope", _blocked_write_envelope)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command('where opensquilla 2>nul || echo "missing"')
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "shared path envelope blocked this command" in result
|
|
assert backend_called is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auto_host_exec_batch_requests_write_path_access_before_host(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
workspace = tmp_path / "workspace"
|
|
workspace.mkdir()
|
|
outside = tmp_path / "outside.txt"
|
|
command = f"where winget; Set-Content -Path {outside} -Value hi"
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
host_calls: list[str] = []
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append(command)
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
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,
|
|
workspace_dir=str(workspace),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(command, workdir=str(workspace))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
payload = json.loads(result)
|
|
assert payload["status"] == "approval_required"
|
|
assert payload["access"] == "rw"
|
|
assert payload["path"] == str(outside.resolve(strict=False))
|
|
assert host_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_auto_host_exec_batch_blocks_protected_metadata_write_before_host(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.gateway.approval_queue import reset_approval_queue
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
workspace = tmp_path / "workspace"
|
|
protected = workspace / ".git"
|
|
protected.mkdir(parents=True)
|
|
target = protected / "config"
|
|
command = f"where winget; Set-Content -Path {target} -Value hi"
|
|
|
|
_configure_approval_queue(monkeypatch, tmp_path, "prompt")
|
|
runtime = _windows_runtime()
|
|
host_calls: list[str] = []
|
|
|
|
async def fake_host(command, **kwargs):
|
|
host_calls.append(command)
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host)
|
|
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,
|
|
workspace_dir=str(workspace),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(command, workdir=str(workspace))
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
reset_approval_queue()
|
|
|
|
payload = json.loads(result)
|
|
assert payload["status"] == "blocked"
|
|
assert payload["reason"] == "protected_metadata"
|
|
assert payload["resolved_path"] == str(target.resolve(strict=False))
|
|
assert host_calls == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_exec_command_merges_shell_active_mounts(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.sandbox.config import SandboxSettings
|
|
from opensquilla.sandbox.policy import build_policy
|
|
from opensquilla.sandbox.types import SecurityLevel
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
policy = build_policy(
|
|
SecurityLevel.STANDARD,
|
|
"shell.exec",
|
|
tmp_path,
|
|
SandboxSettings(
|
|
sandbox=True,
|
|
security_grading=True,
|
|
backend="windows_default",
|
|
network_default="none",
|
|
),
|
|
trusted=True,
|
|
)
|
|
request = SimpleNamespace(
|
|
cwd=tmp_path,
|
|
action_kind="shell.exec",
|
|
policy=policy,
|
|
reason="",
|
|
session_id="s1",
|
|
run_mode="trusted",
|
|
)
|
|
|
|
async def _fake_gate_action(**kwargs):
|
|
return object(), policy, request
|
|
|
|
async def _fake_preflight(*args, **kwargs):
|
|
return None
|
|
|
|
backend_requests = []
|
|
|
|
async def _fake_run_backend(request, *, runtime=None):
|
|
backend_requests.append(request)
|
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="", backend_notes=())
|
|
|
|
mounted = tmp_path / "external"
|
|
mounted.mkdir()
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
|
|
monkeypatch.setattr(shell, "preflight_subprocess_managed_network", _fake_preflight)
|
|
monkeypatch.setattr(shell, "_run_backend_with_managed_network", _fake_run_backend)
|
|
monkeypatch.setattr(
|
|
shell,
|
|
"_active_sandbox_mounts",
|
|
lambda: [{"path": str(mounted), "access": "rw"}],
|
|
)
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command("Write-Output ok")
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "ok" in result
|
|
assert backend_requests
|
|
assert any(mount.host_path == mounted for mount in backend_requests[0].policy.mounts)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_exec_command_blocks_runtime_readonly_write_target(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.sandbox.config import SandboxSettings
|
|
from opensquilla.sandbox.policy import build_policy
|
|
from opensquilla.sandbox.types import SecurityLevel
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
policy = build_policy(
|
|
SecurityLevel.STANDARD,
|
|
"shell.exec",
|
|
tmp_path,
|
|
SandboxSettings(
|
|
sandbox=True,
|
|
security_grading=True,
|
|
backend="windows_default",
|
|
network_default="none",
|
|
),
|
|
trusted=True,
|
|
)
|
|
request = SimpleNamespace(
|
|
cwd=tmp_path,
|
|
action_kind="shell.exec",
|
|
policy=policy,
|
|
reason="",
|
|
session_id="s1",
|
|
run_mode="trusted",
|
|
)
|
|
runtime_root = tmp_path / "runtime-src"
|
|
runtime_root.mkdir()
|
|
target = runtime_root / "__write_should_not_happen__.txt"
|
|
|
|
async def _fake_gate_action(**kwargs):
|
|
return object(), policy, request
|
|
|
|
async def _fake_preflight(*args, **kwargs):
|
|
return None
|
|
|
|
async def _fake_run_backend(request, *, runtime=None):
|
|
return SimpleNamespace(returncode=0, stdout="ok\n", stderr="", backend_notes=())
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "gate_action", _fake_gate_action)
|
|
monkeypatch.setattr(shell, "preflight_subprocess_managed_network", _fake_preflight)
|
|
monkeypatch.setattr(shell, "_run_backend_with_managed_network", _fake_run_backend)
|
|
monkeypatch.setattr(shell, "_windows_runtime_readonly_roots", lambda: (runtime_root,))
|
|
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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="trusted",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(f'echo "test" > "{target}"')
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
payload = json.loads(result)
|
|
assert payload["reason"] == "runtime_readonly"
|
|
assert payload["resolved_path"] == str(target)
|
|
|
|
|
|
def test_shell_blocks_runtime_python_environment_bootstrap(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime_root = tmp_path / "runtime-venv"
|
|
runtime_root.mkdir()
|
|
|
|
monkeypatch.setattr(shell, "_runtime_readonly_roots", lambda runtime=None: (runtime_root,))
|
|
monkeypatch.setattr(shell, "full_host_access_active", lambda: False)
|
|
|
|
payload = shell._runtime_readonly_shell_block(
|
|
"exec_command",
|
|
"python -m ensurepip --upgrade",
|
|
str(tmp_path),
|
|
runtime=SimpleNamespace(backend=SimpleNamespace(name="bubblewrap")),
|
|
)
|
|
|
|
assert payload is not None
|
|
assert payload["reason"] == "runtime_readonly"
|
|
assert payload["runtime_operation"] == "python -m ensurepip"
|
|
assert payload["readonly_root"] == str(runtime_root)
|
|
|
|
|
|
def test_shell_blocks_runtime_python_package_install(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime_root = tmp_path / "runtime-venv"
|
|
runtime_python = runtime_root / "bin" / "python"
|
|
runtime_python.parent.mkdir(parents=True)
|
|
runtime_python.write_text("", encoding="utf-8")
|
|
|
|
monkeypatch.setattr(shell, "_runtime_readonly_roots", lambda runtime=None: (runtime_root,))
|
|
monkeypatch.setattr(shell, "full_host_access_active", lambda: False)
|
|
|
|
payload = shell._runtime_readonly_shell_block(
|
|
"exec_command",
|
|
f"{runtime_python} -m pip install requests",
|
|
str(tmp_path),
|
|
runtime=SimpleNamespace(backend=SimpleNamespace(name="bubblewrap")),
|
|
)
|
|
|
|
assert payload is not None
|
|
assert payload["reason"] == "runtime_readonly"
|
|
assert payload["runtime_operation"] == "python -m pip install"
|
|
|
|
|
|
def test_shell_allows_explicit_project_venv_package_install(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime_root = tmp_path / "runtime-venv"
|
|
project_python = tmp_path / "project" / ".venv" / "bin" / "python"
|
|
project_python.parent.mkdir(parents=True)
|
|
project_python.write_text("", encoding="utf-8")
|
|
|
|
monkeypatch.setattr(shell, "_runtime_readonly_roots", lambda runtime=None: (runtime_root,))
|
|
monkeypatch.setattr(shell, "full_host_access_active", lambda: False)
|
|
|
|
payload = shell._runtime_readonly_shell_block(
|
|
"exec_command",
|
|
f"{project_python} -m pip install requests",
|
|
str(tmp_path),
|
|
runtime=SimpleNamespace(backend=SimpleNamespace(name="bubblewrap")),
|
|
)
|
|
|
|
assert payload is None
|
|
|
|
|
|
def test_shell_allows_explicit_project_venv_ensurepip(monkeypatch, tmp_path) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime_root = tmp_path / "runtime-venv"
|
|
project_python = tmp_path / "project" / ".venv" / "bin" / "python"
|
|
project_python.parent.mkdir(parents=True)
|
|
project_python.write_text("", encoding="utf-8")
|
|
|
|
monkeypatch.setattr(shell, "_runtime_readonly_roots", lambda runtime=None: (runtime_root,))
|
|
monkeypatch.setattr(shell, "full_host_access_active", lambda: False)
|
|
|
|
payload = shell._runtime_readonly_shell_block(
|
|
"exec_command",
|
|
f"{project_python} -m ensurepip --upgrade",
|
|
str(tmp_path),
|
|
runtime=SimpleNamespace(backend=SimpleNamespace(name="bubblewrap")),
|
|
)
|
|
|
|
assert payload is None
|
|
|
|
|
|
def test_shell_blocks_windows_runtime_python_environment_bootstrap(monkeypatch) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime_root = Path(r"X:\workspace\.venv")
|
|
|
|
monkeypatch.setattr(shell, "_runtime_readonly_roots", lambda runtime=None: (runtime_root,))
|
|
monkeypatch.setattr(shell, "full_host_access_active", lambda: False)
|
|
|
|
payload = shell._runtime_readonly_shell_block(
|
|
"exec_command",
|
|
r"X:\workspace\.venv\Scripts\python.exe -m ensurepip --upgrade",
|
|
r"X:\workspace",
|
|
runtime=SimpleNamespace(backend=SimpleNamespace(name="windows_default")),
|
|
)
|
|
|
|
assert payload is not None
|
|
assert payload["reason"] == "runtime_readonly"
|
|
assert payload["runtime_operation"] == "python -m ensurepip"
|
|
assert payload["readonly_root"] == str(runtime_root)
|
|
|
|
|
|
def test_shell_allows_windows_project_venv_ensurepip(monkeypatch) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime_root = Path(r"X:\workspace\.venv")
|
|
|
|
monkeypatch.setattr(shell, "_runtime_readonly_roots", lambda runtime=None: (runtime_root,))
|
|
monkeypatch.setattr(shell, "full_host_access_active", lambda: False)
|
|
|
|
payload = shell._runtime_readonly_shell_block(
|
|
"exec_command",
|
|
r"X:\workspace\.tmp\proj\.venv\Scripts\python.exe -m ensurepip --upgrade",
|
|
r"X:\workspace",
|
|
runtime=SimpleNamespace(backend=SimpleNamespace(name="windows_default")),
|
|
)
|
|
|
|
assert payload is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_windows_exec_command_full_host_access_skips_runtime_readonly_block(
|
|
monkeypatch,
|
|
tmp_path,
|
|
) -> None:
|
|
from opensquilla.tools.builtin import shell
|
|
|
|
runtime = _windows_runtime()
|
|
runtime_root = tmp_path / "runtime-src"
|
|
runtime_root.mkdir()
|
|
target = runtime_root / "__full_host_should_reach_host__.txt"
|
|
host_calls = []
|
|
|
|
async def _fake_run_host_shell_command(*args, **kwargs):
|
|
host_calls.append((args, kwargs))
|
|
return "exit_code=0\nhost-ok\n"
|
|
|
|
monkeypatch.setattr(shell, "get_runtime", lambda: runtime)
|
|
monkeypatch.setattr(shell, "_windows_runtime_readonly_roots", lambda: (runtime_root,))
|
|
monkeypatch.setattr(shell, "_run_host_shell_command", _fake_run_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,
|
|
workspace_dir=str(tmp_path),
|
|
session_key="s1",
|
|
run_mode="full",
|
|
)
|
|
)
|
|
try:
|
|
result = await shell.exec_command(f'echo "test" > "{target}"')
|
|
finally:
|
|
current_tool_context.reset(token)
|
|
|
|
assert "host-ok" in result
|
|
assert host_calls
|