test: align local sandbox tests with Windows support

This commit is contained in:
RuochenPan
2026-09-07 10:24:08 +08:00
parent 1510035eb6
commit c5481fa7de
5 changed files with 74 additions and 30 deletions
+43 -17
View File
@@ -15,7 +15,7 @@ from mcp.types import CallToolResult, ImageContent
from PIL import Image
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.computer import file_read_utils
from astrbot.core.computer import file_read_utils, local_file_security
from astrbot.core.computer.booters.local import LocalBooter
from astrbot.core.tools.computer_tools import fs as fs_tools
from astrbot.core.tools.computer_tools import util as computer_util
@@ -306,6 +306,7 @@ def _make_epub_bytes(*, chapter_count: int = 1) -> bytes:
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_restricted_local_member_can_read_plugin_provided_skill(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -331,6 +332,7 @@ async def test_restricted_local_member_can_read_plugin_provided_skill(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_restricted_local_member_can_read_builtin_plugin_skill(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -356,6 +358,7 @@ async def test_restricted_local_member_can_read_builtin_plugin_skill(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_restricted_local_member_can_read_plugin_skill_inventory_even_if_plugin_inactive(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -451,6 +454,7 @@ async def test_restricted_local_member_cannot_write_plugin_provided_skill(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_restricted_local_member_cannot_modify_locally_installed_skill(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -494,6 +498,7 @@ async def test_restricted_local_member_cannot_modify_locally_installed_skill(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_local_admin_can_modify_locally_installed_skill(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -543,29 +548,42 @@ async def test_restricted_local_member_rejects_workspace_hardlink_alias(
@pytest.mark.asyncio
async def test_restricted_local_write_fails_closed_without_safe_file_access(
@pytest.mark.parametrize("role", ["member", "admin"])
@pytest.mark.parametrize(
("tool_type", "arguments"),
[
(fs_tools.FileReadTool, {"path": "existing.txt"}),
(fs_tools.FileWriteTool, {"path": "existing.txt", "content": "changed"}),
(fs_tools.FileWriteTool, {"path": "new.txt", "content": "changed"}),
(
fs_tools.FileEditTool,
{"path": "existing.txt", "old": "original", "new": "changed"},
),
],
)
async def test_windows_restricted_file_access_fails_closed(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
role,
tool_type,
arguments,
):
"""Windows rejects restricted reads, creates, writes, and edits without side effects."""
workspace = _setup_local_fs_tools(monkeypatch, tmp_path)
existing_file = workspace / "existing.txt"
existing_file.write_text("original secret\n", encoding="utf-8")
# Select the real Windows rejection branch without changing pathlib's OS view.
monkeypatch.setattr(local_file_security, "os", SimpleNamespace(name="nt"))
def unavailable_file_access(*_args, **_kwargs):
raise RuntimeError("Restricted file access is unavailable.")
monkeypatch.setattr(
fs_tools,
"open_file_in_allowed_roots",
unavailable_file_access,
result = await tool_type().call(
_make_context(role=role),
**arguments,
)
result = await fs_tools.FileWriteTool().call(
_make_context(role="member"),
path="blocked.txt",
content="must not be written\n",
)
assert "Restricted file access is unavailable" in result
assert not (workspace / "blocked.txt").exists()
assert "Race-resistant restricted file access is unavailable" in result
assert "original secret" not in result
assert existing_file.read_text(encoding="utf-8") == "original secret\n"
assert not (workspace / "new.txt").exists()
@pytest.mark.asyncio
@@ -737,6 +755,7 @@ def test_detect_text_encoding_allows_utf8_probe_cut_mid_character():
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_rejects_large_full_text_read_before_local_stream_read(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -760,6 +779,7 @@ async def test_file_read_tool_rejects_large_full_text_read_before_local_stream_r
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_allows_partial_read_for_large_text_file(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -780,6 +800,7 @@ async def test_file_read_tool_allows_partial_read_for_large_text_file(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_returns_image_call_tool_result_for_images(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -801,6 +822,7 @@ async def test_file_read_tool_returns_image_call_tool_result_for_images(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_treats_svg_as_text(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -821,6 +843,7 @@ async def test_file_read_tool_treats_svg_as_text(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_reads_pdf_via_parser(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -843,6 +866,7 @@ async def test_file_read_tool_reads_pdf_via_parser(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_reads_docx_via_parser_and_magic(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -877,6 +901,7 @@ def test_is_epub_bytes_rejects_plain_zip_archive():
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_reads_epub_via_parser_and_magic(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
@@ -899,6 +924,7 @@ async def test_file_read_tool_reads_epub_via_parser_and_magic(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted file access needs POSIX.")
async def test_file_read_tool_stores_long_converted_document_in_workspace(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
+17 -13
View File
@@ -4,6 +4,7 @@ from types import SimpleNamespace
import pytest
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.computer import process_sandbox
from astrbot.core.tools.computer_tools.shipyard_neo.browser import BrowserExecTool
from astrbot.core.tools.computer_tools.shipyard_neo.neo_skills import (
GetExecutionHistoryTool,
@@ -139,22 +140,25 @@ def test_local_permission_policy_denies_disabled_execution():
assert "Local Permission Policies" in error
def test_unavailable_local_sandbox_requires_a_full_trust_policy(monkeypatch):
from astrbot.core.tools.computer_tools import util as computer_util
def unavailable_sandbox():
raise RuntimeError("No Local process sandbox backend is available.")
monkeypatch.setattr(computer_util, "create_process_sandbox", unavailable_sandbox)
@pytest.mark.parametrize("role", ["member", "admin"])
@pytest.mark.parametrize(
("allow_network", "filesystem_scope"),
[(False, "host"), (False, "workspace"), (True, "workspace")],
)
def test_windows_local_execution_requires_a_full_trust_policy(
monkeypatch, role, allow_network, filesystem_scope
):
"""Windows rejects restricted policies and allows explicit full trust."""
monkeypatch.setattr(process_sandbox, "sys", SimpleNamespace(platform="win32"))
restricted = {
"member": {
role: {
"allow_execution": True,
"allow_network": False,
"filesystem_scope": "host",
"allow_network": allow_network,
"filesystem_scope": filesystem_scope,
}
}
full_trust = {
"member": {
role: {
"allow_execution": True,
"allow_network": True,
"filesystem_scope": "host",
@@ -162,11 +166,11 @@ def test_unavailable_local_sandbox_requires_a_full_trust_policy(monkeypatch):
}
_, restricted_error = check_local_execution_permission(
_make_local_run_context("member", restricted),
_make_local_run_context(role, restricted),
"Shell execution",
)
full_policy, full_error = check_local_execution_permission(
_make_local_run_context("member", full_trust),
_make_local_run_context(role, full_trust),
"Shell execution",
)
+7
View File
@@ -288,6 +288,7 @@ def test_process_sandbox_fails_closed_on_unsupported_system(monkeypatch):
process_sandbox.create_process_sandbox()
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
def test_linux_bwrap_command_is_workspace_only_and_clears_environment(
monkeypatch,
tmp_path,
@@ -332,6 +333,7 @@ def test_linux_bwrap_command_is_workspace_only_and_clears_environment(
]
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
def test_linux_bwrap_command_applies_network_and_filesystem_permissions(
monkeypatch,
tmp_path,
@@ -363,6 +365,7 @@ def test_linux_bwrap_command_applies_network_and_filesystem_permissions(
assert "--tmpfs" not in host_without_network
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
def test_process_sandbox_exposes_resource_limits(monkeypatch, tmp_path):
monkeypatch.setattr(local_booter.sys, "platform", "linux")
monkeypatch.setattr(local_booter.shutil, "which", lambda name: f"/usr/bin/{name}")
@@ -782,6 +785,7 @@ def test_linux_read_only_bwrap_grep_cannot_follow_outside_ancestor_symlink(
("darwin", "/usr/bin/sandbox-exec"),
],
)
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
async def test_managed_shell_uses_platform_sandbox(
monkeypatch,
tmp_path,
@@ -901,6 +905,7 @@ async def test_sandboxed_managed_shell_delegates_launch(monkeypatch, tmp_path):
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
async def test_sandboxed_managed_shell_stops_at_output_limit(monkeypatch, tmp_path):
process_stopped = asyncio.Event()
@@ -964,6 +969,7 @@ async def test_sandboxed_managed_shell_stops_at_output_limit(monkeypatch, tmp_pa
("darwin", "/usr/bin/sandbox-exec"),
],
)
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
async def test_local_python_uses_platform_sandbox(
monkeypatch,
tmp_path,
@@ -1003,6 +1009,7 @@ async def test_local_python_uses_platform_sandbox(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Unix sandbox requires POSIX.")
async def test_sandboxed_local_python_caps_returned_output(monkeypatch, tmp_path):
def fake_run(*args, **kwargs):
kwargs["stdout"].write(b"123456")
+5
View File
@@ -1,6 +1,7 @@
import asyncio
import inspect
import json
import os
from unittest.mock import AsyncMock
import pytest
@@ -114,6 +115,7 @@ def test_shell_session_schema_supports_line_writes():
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted execution needs POSIX.")
async def test_local_execute_shell_manages_running_and_closed_results(
monkeypatch,
tmp_path,
@@ -216,6 +218,7 @@ async def test_local_execute_shell_manages_running_and_closed_results(
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted execution needs POSIX.")
async def test_local_shell_tools_fail_closed_without_sender_identity(
monkeypatch,
tmp_path,
@@ -394,6 +397,7 @@ async def test_local_member_shell_is_denied_without_supported_sandbox(monkeypatc
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted execution needs POSIX.")
async def test_shell_session_tool_lists_sessions_for_current_owner(monkeypatch):
from astrbot.core.tools.computer_tools import shell as shell_tools
@@ -453,6 +457,7 @@ async def test_shell_session_tool_lists_sessions_for_current_owner(monkeypatch):
("terminate", "terminate"),
],
)
@pytest.mark.skipif(os.name == "nt", reason="Restricted execution needs POSIX.")
async def test_shell_session_tool_passes_member_identity_to_session_actions(
monkeypatch,
action,
+2
View File
@@ -1,3 +1,4 @@
import os
import platform
from types import SimpleNamespace
from unittest.mock import AsyncMock
@@ -27,6 +28,7 @@ def test_local_python_tool_description_contains_os():
@pytest.mark.asyncio
@pytest.mark.skipif(os.name == "nt", reason="Restricted execution needs POSIX.")
async def test_local_python_tool_uses_session_workspace(tmp_path, monkeypatch):
"""Local Python execution should use the same workspace as local shell."""
tool = LocalPythonTool()