mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-16 15:06:04 +08:00
test: align local sandbox tests with Windows support
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user