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

944 lines
31 KiB
Python

from __future__ import annotations
import json
import os
from pathlib import Path
from types import SimpleNamespace
import pytest
from opensquilla.gateway.approval_queue import get_approval_queue, reset_approval_queue
from opensquilla.sandbox.config import SandboxSettings
from opensquilla.sandbox.integration import configure_runtime, reset_runtime
from opensquilla.tools.builtin import code_exec, filesystem, shell
from opensquilla.tools.builtin.code_exec import execute_code
from opensquilla.tools.builtin.shell_policy import PolicyResult
from opensquilla.tools.types import (
CallerKind,
InteractionMode,
ToolContext,
ToolError,
current_tool_context,
)
@pytest.fixture(autouse=True)
def reset_approval_state():
reset_approval_queue()
reset_runtime()
token = current_tool_context.set(
ToolContext(is_owner=True, caller_kind=CallerKind.CLI, session_key="agent:main:test")
)
yield
current_tool_context.reset(token)
reset_approval_queue()
reset_runtime()
def test_audit_command_preserves_long_commands_until_cap() -> None:
command = "rm " + ("x" * 120)
assert shell._audit_command(command) == command
huge = "rm " + ("x" * 5000)
audited = shell._audit_command(huge)
assert len(audited) > 80
assert audited.endswith("...[truncated]")
@pytest.mark.asyncio
async def test_exec_approval_deny_pattern_blocks_shell_command(tmp_path: Path) -> None:
get_approval_queue().set_settings("prompt", deny_patterns=["rm *"])
result = await shell.exec_command("rm target.txt", workdir=str(tmp_path))
payload = json.loads(result)
assert payload["status"] == "approval_denied"
assert payload["command"] == "rm target.txt"
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_exec_approval_deny_pattern_blocks_wrapped_shell_command(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
get_approval_queue().set_settings("prompt", deny_patterns=["rm *"])
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda _command: PolicyResult(allowed=True, reason="", needs_approval=False),
)
async def fail_host_execution(*args: object, **kwargs: object) -> str:
raise AssertionError("denied wrapped command should not run")
monkeypatch.setattr(shell, "_run_host_shell_command", fail_host_execution)
result = await shell.exec_command(
'powershell -NoProfile -Command "rm target.txt"',
workdir=str(tmp_path),
)
payload = json.loads(result)
assert payload["status"] == "approval_denied"
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_exec_approval_deny_pattern_does_not_depend_on_warnlist(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
get_approval_queue().set_settings("prompt", deny_patterns=["git status"])
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda _command: PolicyResult(allowed=True, reason="", needs_approval=False),
)
result = await shell.exec_command("git status", workdir=str(tmp_path))
payload = json.loads(result)
assert payload["status"] == "approval_denied"
assert payload["command"] == "git status"
@pytest.mark.asyncio
async def test_full_host_access_warnlisted_exec_skips_approval_and_uses_host(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.run_mode = "full"
get_approval_queue().set_settings("auto-deny")
calls: list[str] = []
async def fake_host_execution(*args: object, **kwargs: object) -> str:
calls.append("host")
return "exit_code=0\nhost\n"
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host_execution)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert result == "exit_code=0\nhost\n"
assert calls == ["host"]
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_sandbox_disabled_warnlisted_exec_skips_approval_and_uses_host(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.run_mode = "standard"
ctx.workspace_dir = str(tmp_path)
configure_runtime(
SandboxSettings(sandbox=False, security_grading=False),
workspace=tmp_path,
)
get_approval_queue().set_settings("auto-deny")
calls: list[str] = []
async def fake_host_execution(*args: object, **kwargs: object) -> str:
calls.append("host")
return "exit_code=0\nhost\n"
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host_execution)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert result == "exit_code=0\nhost\n"
assert calls == ["host"]
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_warnlisted_exec_allow_pattern_skips_prompt_when_sandbox_enabled(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.workspace_dir = str(tmp_path)
get_approval_queue().set_settings(
"prompt",
allow_patterns=["pip install requests"],
)
configure_runtime(
SandboxSettings(
sandbox=True,
security_grading=True,
backend="noop",
allow_legacy_mode=True,
),
workspace=tmp_path,
)
calls: list[tuple[str, object]] = []
async def fake_gate_action(**kwargs: object) -> tuple[object, object, object]:
calls.append(("gate", kwargs))
policy = SimpleNamespace(network=None)
request = SimpleNamespace(cwd=tmp_path, action_kind="shell.exec", policy=policy)
return object(), policy, request
async def fake_sandbox(request: object, *, runtime: object = None) -> object:
calls.append(("backend", request))
return SimpleNamespace(
returncode=0,
stdout="sandboxed\n",
stderr="",
timed_out=False,
backend_notes=(),
)
monkeypatch.setattr(shell, "gate_action", fake_gate_action)
monkeypatch.setattr(shell, "run_under_backend", fake_sandbox)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert "sandboxed" in result
assert [name for name, _ in calls] == ["gate", "backend"]
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_warnlisted_exec_allow_pattern_runs_without_approval_when_no_sandbox_gate(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
get_approval_queue().set_settings(
"prompt",
allow_patterns=["pip install requests"],
)
calls: list[str] = []
async def fake_host_execution(*args: object, **kwargs: object) -> str:
calls.append("host")
return "exit_code=0\nhost\n"
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host_execution)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert result == "exit_code=0\nhost\n"
assert get_approval_queue().list_pending("exec") == []
assert calls == ["host"]
@pytest.mark.asyncio
async def test_warnlisted_exec_prompt_runs_without_approval_when_no_sandbox_gate(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
get_approval_queue().set_settings("prompt")
calls: list[str] = []
async def fake_host_execution(*args: object, **kwargs: object) -> str:
calls.append("host")
return "exit_code=0\nhost\n"
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host_execution)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert result == "exit_code=0\nhost\n"
assert get_approval_queue().list_pending("exec") == []
assert calls == ["host"]
@pytest.mark.asyncio
async def test_warnlisted_exec_auto_approve_runs_without_approval_when_no_sandbox_gate(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
get_approval_queue().set_settings("auto-approve")
calls: list[str] = []
async def fake_host_execution(*args: object, **kwargs: object) -> str:
calls.append("host")
return "exit_code=0\nhost\n"
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host_execution)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert result == "exit_code=0\nhost\n"
assert get_approval_queue().list_pending("exec") == []
assert calls == ["host"]
@pytest.mark.asyncio
async def test_warnlisted_exec_unattended_runs_without_approval_when_no_sandbox_gate(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
get_approval_queue().set_settings("prompt")
calls: list[str] = []
async def fake_host_execution(*args: object, **kwargs: object) -> str:
calls.append("host")
return "exit_code=0\nhost\n"
monkeypatch.setattr(shell, "_run_host_shell_command", fake_host_execution)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.exec_command("pip install requests", workdir=str(tmp_path))
assert result == "exit_code=0\nhost\n"
assert calls == ["host"]
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_destructive_code_exec_uses_sandbox_gate_when_runtime_enabled(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.workspace_dir = str(tmp_path)
target = tmp_path / "target.txt"
target.write_text("delete me", encoding="utf-8")
configure_runtime(
SandboxSettings(
sandbox=True,
security_grading=True,
backend="noop",
allow_legacy_mode=True,
),
workspace=tmp_path,
)
calls: list[tuple[str, object]] = []
async def fake_gate_action(**kwargs):
calls.append(("gate", kwargs))
policy = SimpleNamespace(network=None)
request = SimpleNamespace(cwd=tmp_path, action_kind="code.exec", policy=policy)
return object(), policy, request
async def fake_sandbox(request: object, *, runtime: object = None) -> object:
calls.append(("backend", request))
return SimpleNamespace(
returncode=0,
stdout="sandboxed\n",
stderr="",
timed_out=False,
backend_notes=(),
)
monkeypatch.setattr(code_exec, "gate_action", fake_gate_action)
monkeypatch.setattr(code_exec, "run_under_backend", fake_sandbox)
result = await execute_code("import os\nos.remove('target.txt')")
payload = json.loads(result)
assert payload["exit_code"] == 0
assert payload["stdout"] == "sandboxed\n"
assert [name for name, _ in calls] == ["gate", "backend"]
hints = calls[0][1]["hints"] # type: ignore[index]
assert hints.high_impact is True
assert target.exists()
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_warnlist_background_process_uses_sandbox_gate_when_runtime_enabled(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.workspace_dir = str(tmp_path)
configure_runtime(
SandboxSettings(
sandbox=True,
security_grading=True,
backend="noop",
allow_legacy_mode=True,
),
workspace=tmp_path,
)
calls: list[tuple[str, object]] = []
class _FakeStream:
async def read(self, size: int) -> bytes:
return b""
class _FakeProcess:
stdout = _FakeStream()
stdin = None
returncode = 0
async def wait(self) -> int:
return 0
async def fake_gate_action(**kwargs):
calls.append(("gate", kwargs))
policy = SimpleNamespace(network=None, network_proxy=None)
request = SimpleNamespace(cwd=tmp_path, action_kind="shell.background", policy=policy)
return object(), policy, request
async def fake_sandbox_spawn(*args: object, **kwargs: object) -> object:
calls.append(("backend", kwargs.get("request")))
return shell._SpawnedBackgroundProcess(process=_FakeProcess()) # type: ignore[arg-type]
monkeypatch.setattr(shell, "gate_action", fake_gate_action)
monkeypatch.setattr(shell, "_spawn_sandboxed_background_process", fake_sandbox_spawn)
monkeypatch.setattr(
shell,
"check_safe_bin",
lambda command: PolicyResult(
allowed=True,
reason=f"command requires approval: {command}",
needs_approval=True,
),
)
result = await shell.background_process("rm target.txt", workdir=str(tmp_path))
assert "status: running" in result
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
assert [name for name, _ in calls] == ["gate", "backend"]
hints = calls[0][1]["hints"] # type: ignore[index]
assert hints.high_impact is True
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_outside_workspace_write_blocks_without_sandbox_path_approval(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
outside = tmp_path / "outside.txt"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.workspace_dir = str(workspace)
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
payload = json.loads(await write_file(str(outside), "ok"))
assert payload["status"] == "blocked"
assert payload["reason"] == "outside_workspace"
assert not outside.exists()
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_workspace_lockdown_blocks_outside_workspace_write_even_with_bypass(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
outside = tmp_path / "outside.txt"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_lockdown = True # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
with pytest.raises(ToolError, match="workspace lockdown"):
await write_file(str(outside), "ok")
assert not outside.exists()
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_workspace_lockdown_allows_configured_scratch_dir_write(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = scratch / "debug.py"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
ctx.workspace_lockdown = True # type: ignore[attr-defined]
result = await filesystem._gate_out_of_workspace_write(
"write_file",
target.resolve(strict=False),
str(target),
None,
)
assert result is None
assert not target.exists()
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_workspace_write_deny_globs_block_file_write(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
target = workspace / "blocked" / "generated.txt"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_write_deny_globs = ["blocked/**"] # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
with pytest.raises(ToolError, match="workspace write deny policy"):
await write_file(str(target), "nope")
assert not target.exists()
assert get_approval_queue().list_pending("exec") == []
@pytest.mark.asyncio
async def test_workspace_write_deny_globs_block_nested_test_file_write(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
target = workspace / "myproj" / "src" / "test" / "java" / "com" / "example"
target = target / "SampleTest.java"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_write_deny_globs = ["**/test/**", "**/*Test.java"] # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
with pytest.raises(ToolError, match="workspace write deny policy"):
await write_file(str(target), "nope")
assert not target.exists()
assert len(get_approval_queue().list_pending("exec")) == 0
@pytest.mark.asyncio
async def test_workspace_write_deny_globs_allow_configured_scratch_write_file(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = scratch / "test_bug.py"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
ctx.workspace_lockdown = True # type: ignore[attr-defined]
ctx.workspace_write_deny_globs = ["**/test_*.py"] # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
result = await write_file(str(target), "print('scratch')\n")
assert result.startswith("Written 17 bytes to ")
assert target.read_text(encoding="utf-8") == "print('scratch')\n"
assert len(get_approval_queue().list_pending("exec")) == 0
@pytest.mark.asyncio
async def test_configured_scratch_dir_blocks_root_debug_workspace_write_file(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = workspace / "debug_regex.php"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
ctx.workspace_lockdown = True # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
with pytest.raises(ToolError, match="configured scratch directory"):
await write_file(str(target), "<?php echo 'debug';\n")
assert not target.exists()
@pytest.mark.asyncio
async def test_configured_scratch_dir_allows_plain_root_test_source_name(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = workspace / "test_api.py"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
ctx.workspace_lockdown = True # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
result = await write_file(str(target), "def test_api():\n assert True\n")
assert "Written 32 bytes" in result
assert str(target) in result
assert target.exists()
@pytest.mark.asyncio
async def test_write_file_blocks_large_workspace_file_fragment_overwrite(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
target = workspace / "src" / "main.c"
target.parent.mkdir()
target.write_text("int keep;\n" * 700, encoding="utf-8")
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
await filesystem.read_file(str(target))
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
with pytest.raises(ToolError, match="write_file refused to overwrite"):
await write_file(str(target), "int replacement_fragment;\n")
assert target.read_text(encoding="utf-8").startswith("int keep;\n")
@pytest.mark.asyncio
async def test_write_file_allows_scratch_file_fragment_overwrite(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = scratch / "debug.c"
target.write_text("int keep;\n" * 700, encoding="utf-8")
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
result = await write_file(str(target), "int replacement_fragment;\n")
assert result.startswith("Written 26 bytes to ")
assert target.read_text(encoding="utf-8") == "int replacement_fragment;\n"
@pytest.mark.asyncio
async def test_write_file_allows_small_workspace_file_overwrite(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
target = workspace / "README.md"
target.write_text("old\n", encoding="utf-8")
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
await filesystem.read_file(str(target))
write_file = filesystem.write_file.__wrapped__.__wrapped__ # type: ignore[attr-defined]
result = await write_file(str(target), "new\n")
assert result.startswith("Written 4 bytes to ")
assert target.read_text(encoding="utf-8") == "new\n"
def test_tool_definitions_include_scratch_guidance_when_configured(tmp_path: Path) -> None:
from opensquilla.tools.registry import get_default_registry
scratch = tmp_path / "scratch"
ctx = ToolContext(is_owner=True, scratch_dir=str(scratch))
tools = get_default_registry().to_tool_definitions(ctx)
descriptions = {tool.name: tool.description for tool in tools}
assert str(scratch) in descriptions["exec_command"]
assert str(scratch) in descriptions["write_file"]
def test_exec_command_description_names_windows_powershell_shell() -> None:
from opensquilla.tools.registry import get_default_registry
ctx = ToolContext(is_owner=True)
tools = get_default_registry().to_tool_definitions(ctx)
descriptions = {tool.name: tool.description for tool in tools}
assert "PowerShell" in descriptions["exec_command"]
assert "cmd /c" in descriptions["exec_command"]
@pytest.mark.asyncio
async def test_workspace_lockdown_blocks_obvious_outside_shell_redirection(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
outside = tmp_path / "outside.txt"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_lockdown = True # type: ignore[attr-defined]
result = await shell.exec_command(f"echo ok > {outside}", workdir=str(workspace))
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "workspace_lockdown"
@pytest.mark.asyncio
@pytest.mark.skipif(os.name != "posix", reason="/dev/null redirection is POSIX-specific")
async def test_workspace_lockdown_allows_dev_null_shell_redirection(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_lockdown = True # type: ignore[attr-defined]
result = await shell.exec_command("printf ok 2>/dev/null", workdir=str(workspace))
assert result == "exit_code=0\nok"
@pytest.mark.asyncio
@pytest.mark.skipif(os.name != "posix", reason="/dev/null redirection is POSIX-specific")
async def test_workspace_lockdown_dev_null_does_not_hide_sensitive_operand(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_lockdown = True # type: ignore[attr-defined]
result = await shell.exec_command(
"cat /dev/sda 2>/dev/null",
workdir=str(workspace),
)
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "sensitive_path"
@pytest.mark.asyncio
async def test_workspace_write_deny_globs_block_shell_redirection(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_write_deny_globs = ["reports/*.txt"] # type: ignore[attr-defined]
result = await shell.exec_command("echo ok > reports/out.txt", workdir=str(workspace))
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "workspace_write_deny"
assert payload["matched_pattern"] == "reports/*.txt"
@pytest.mark.asyncio
@pytest.mark.skipif(os.name != "posix", reason="heredoc redirection is POSIX-specific")
async def test_workspace_write_deny_globs_allow_shell_redirection_after_cd_to_scratch(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = scratch / "test_bug.py"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
ctx.workspace_lockdown = True # type: ignore[attr-defined]
ctx.workspace_write_deny_globs = ["**/test_*.py"] # type: ignore[attr-defined]
result = await shell.exec_command(
f"cd {scratch} && cat > test_bug.py << 'EOF'\nprint('scratch')\nEOF\ncat test_bug.py",
workdir=str(workspace),
)
assert result == "exit_code=0\nprint('scratch')\n"
assert target.read_text(encoding="utf-8") == "print('scratch')\n"
assert not (workspace / "test_bug.py").exists()
@pytest.mark.asyncio
async def test_configured_scratch_dir_blocks_root_test_shell_redirection(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
scratch = tmp_path / "scratch"
workspace.mkdir()
scratch.mkdir()
target = workspace / "test_bug.php"
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.scratch_dir = str(scratch) # type: ignore[attr-defined]
ctx.workspace_lockdown = True # type: ignore[attr-defined]
result = await shell.exec_command(
"cat > test_bug.php << 'EOF'\n<?php echo 'debug';\nEOF\n",
workdir=str(workspace),
)
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "workspace_scratch_artifact"
assert str(scratch) in payload["message"]
assert not target.exists()
@pytest.mark.asyncio
async def test_workspace_write_deny_globs_inspect_shell_stdin(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
ctx = current_tool_context.get()
assert ctx is not None
ctx.interaction_mode = InteractionMode.UNATTENDED
ctx.elevated = "bypass"
ctx.workspace_dir = str(workspace)
ctx.workspace_write_deny_globs = ["reports/*.txt"] # type: ignore[attr-defined]
result = await shell.exec_command(
"sh",
workdir=str(workspace),
stdin="mkdir -p reports\necho secret > reports/out.txt\n",
)
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "workspace_write_deny"
assert payload["matched_pattern"] == "reports/*.txt"
@pytest.mark.asyncio
async def test_bypass_still_blocks_sensitive_shell_targets() -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.elevated = "bypass"
result = await shell.exec_command("rm ~/.ssh/id_rsa")
payload = json.loads(result)
assert payload["status"] == "blocked"
assert payload["reason"] == "sensitive_path"
assert shell.full_host_access_active() is False
@pytest.mark.asyncio
async def test_bypass_does_not_override_safe_bin_hard_denies() -> None:
ctx = current_tool_context.get()
assert ctx is not None
ctx.elevated = "bypass"
with pytest.raises(ToolError, match="command blocked by policy"):
await shell.exec_command("Clear-Disk")