|
2 | 2 | import os |
3 | 3 | import shlex |
4 | 4 | import uuid |
| 5 | +from pathlib import Path |
5 | 6 |
|
6 | 7 | from pydantic import Field |
7 | 8 | from pydantic.dataclasses import dataclass |
|
14 | 15 | from astrbot.core.computer.computer_client import get_booter |
15 | 16 | from astrbot.core.message.message_event_result import MessageChain |
16 | 17 | from astrbot.core.platform.message_session import MessageSession |
17 | | -from astrbot.core.tools.computer_tools.util import check_admin_permission |
| 18 | +from astrbot.core.tools.computer_tools.util import ( |
| 19 | + check_admin_permission, |
| 20 | + is_local_runtime, |
| 21 | + workspace_root, |
| 22 | +) |
18 | 23 | from astrbot.core.tools.registry import builtin_tool |
19 | | -from astrbot.core.utils.astrbot_path import get_astrbot_temp_path |
| 24 | +from astrbot.core.utils.astrbot_path import ( |
| 25 | + get_astrbot_system_tmp_path, |
| 26 | + get_astrbot_temp_path, |
| 27 | +) |
| 28 | + |
| 29 | + |
| 30 | +def _file_send_allowed_roots(umo: str) -> tuple[Path, ...]: |
| 31 | + return ( |
| 32 | + workspace_root(umo), |
| 33 | + Path(get_astrbot_temp_path()).resolve(strict=False), |
| 34 | + Path(get_astrbot_system_tmp_path()).resolve(strict=False), |
| 35 | + ) |
| 36 | + |
| 37 | + |
| 38 | +def _is_path_within(path: Path, roots: tuple[Path, ...]) -> bool: |
| 39 | + return any(path == root or path.is_relative_to(root) for root in roots) |
| 40 | + |
| 41 | + |
| 42 | +def _is_restricted_local_env(context: ContextWrapper[AstrAgentContext]) -> bool: |
| 43 | + if not is_local_runtime(context): |
| 44 | + return False |
| 45 | + cfg = context.context.context.get_config( |
| 46 | + umo=context.context.event.unified_msg_origin |
| 47 | + ) |
| 48 | + provider_settings = cfg.get("provider_settings", {}) |
| 49 | + require_admin = provider_settings.get("computer_use_require_admin", True) |
| 50 | + return require_admin and context.context.event.role != "admin" |
| 51 | + |
| 52 | + |
| 53 | +def _can_send_local_file( |
| 54 | + context: ContextWrapper[AstrAgentContext], |
| 55 | + local_path: Path, |
| 56 | +) -> bool: |
| 57 | + umo = context.context.event.unified_msg_origin |
| 58 | + allowed_roots = _file_send_allowed_roots(umo) |
| 59 | + if _is_path_within(local_path, allowed_roots): |
| 60 | + return True |
| 61 | + return is_local_runtime(context) and not _is_restricted_local_env(context) |
20 | 62 |
|
21 | 63 |
|
22 | 64 | @builtin_tool |
@@ -85,23 +127,38 @@ async def _resolve_path_from_sandbox( |
85 | 127 | *, |
86 | 128 | component_type: str = "file", |
87 | 129 | ) -> tuple[str, bool]: |
88 | | - path = str(path) |
89 | | - # if the path is relative, check if the file exists in user's local workspace |
| 130 | + path = str(path).strip() |
| 131 | + if not path: |
| 132 | + raise FileNotFoundError(f"{component_type} path is empty") |
| 133 | + |
| 134 | + # Relative host paths are resolved only inside the user's workspace. |
90 | 135 | if not os.path.isabs(path): |
91 | 136 | unified_msg_origin = context.context.event.unified_msg_origin |
92 | 137 | if unified_msg_origin: |
93 | | - from astrbot.core.tools.computer_tools.util import workspace_root |
94 | | - |
95 | 138 | try: |
96 | 139 | ws_path = workspace_root(unified_msg_origin) |
97 | | - ws_candidate = (ws_path / path).resolve() |
| 140 | + ws_candidate = (ws_path / path).resolve(strict=False) |
98 | 141 | if ws_candidate.is_file() and ws_candidate.is_relative_to(ws_path): |
99 | 142 | return str(ws_candidate), False |
100 | 143 | except Exception: |
101 | 144 | pass |
102 | | - # check if the file exists in local environment (only allow absolute paths to prevent traversal) |
103 | | - elif os.path.isfile(path): |
104 | | - return path, False |
| 145 | + else: |
| 146 | + local_candidate = Path(path).expanduser().resolve(strict=False) |
| 147 | + if local_candidate.is_file(): |
| 148 | + if _can_send_local_file(context, local_candidate): |
| 149 | + return str(local_candidate), False |
| 150 | + if is_local_runtime(context): |
| 151 | + allowed = ", ".join( |
| 152 | + str(root) |
| 153 | + for root in _file_send_allowed_roots( |
| 154 | + context.context.event.unified_msg_origin |
| 155 | + ) |
| 156 | + ) |
| 157 | + raise PermissionError( |
| 158 | + "Local file send is restricted for this user. " |
| 159 | + f"Allowed directories: {allowed}. " |
| 160 | + f"Blocked path: {local_candidate}." |
| 161 | + ) |
105 | 162 |
|
106 | 163 | try: |
107 | 164 | sb = await get_booter( |
@@ -221,6 +278,8 @@ async def call( |
221 | 278 | ) |
222 | 279 | except FileNotFoundError as exc: |
223 | 280 | return f"error: {exc}" |
| 281 | + except PermissionError as exc: |
| 282 | + return f"error: {exc}" |
224 | 283 | except Exception as exc: |
225 | 284 | return f"error: failed to build messages[{idx}] component: {exc}" |
226 | 285 |
|
|
0 commit comments