Skip to content

Commit daf7c64

Browse files
committed
扬了 sys 控制
1 parent ef036d0 commit daf7c64

2 files changed

Lines changed: 2 additions & 320 deletions

File tree

pytests/test_plugin_runtime.py

Lines changed: 2 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,142 +1298,12 @@ def test_loader_requires_sdk_plugin_to_override_on_unload(self, tmp_path):
12981298
assert "test.demo-plugin" in loader.failed_plugins
12991299
assert "on_unload" in loader.failed_plugins["test.demo-plugin"]
13001300

1301-
def test_isolate_sys_path_preserves_plugin_dirs(self):
1302-
import builtins
1303-
import importlib
1304-
1305-
from src.plugin_runtime.runner import runner_main
1306-
1307-
plugin_root = os.path.normpath("/tmp/maibot-plugin-root")
1308-
original_import = builtins.__import__
1309-
original_import_module = importlib.import_module
1310-
original_path = list(sys.path)
1311-
original_meta_path = list(sys.meta_path)
1312-
1313-
try:
1314-
if plugin_root in sys.path:
1315-
sys.path.remove(plugin_root)
1316-
1317-
runner_main._isolate_sys_path([plugin_root])
1318-
1319-
assert plugin_root in sys.path
1320-
finally:
1321-
builtins.__import__ = original_import
1322-
importlib.import_module = original_import_module
1323-
sys.path[:] = original_path
1324-
sys.meta_path[:] = original_meta_path
1325-
1326-
def test_isolate_sys_path_blocks_disallowed_src_imports(self):
1327-
import builtins
1328-
import importlib
1329-
1330-
from src.plugin_runtime.runner import runner_main
1331-
1332-
original_import = builtins.__import__
1333-
original_import_module = importlib.import_module
1334-
original_path = list(sys.path)
1335-
original_meta_path = list(sys.meta_path)
1336-
sys.modules.pop("src.forbidden_demo", None)
1337-
1338-
try:
1339-
runner_main._isolate_sys_path([])
1340-
plugin_globals = {
1341-
"__name__": "_maibot_plugin_demo",
1342-
"__package__": "_maibot_plugin_demo",
1343-
"importlib": importlib,
1344-
}
1345-
1346-
with pytest.raises(ImportError, match="不允许导入主程序模块"):
1347-
exec('importlib.import_module("src.forbidden_demo")', plugin_globals)
1348-
finally:
1349-
builtins.__import__ = original_import
1350-
importlib.import_module = original_import_module
1351-
sys.path[:] = original_path
1352-
sys.meta_path[:] = original_meta_path
1353-
sys.modules.pop("src.forbidden_demo", None)
1354-
1355-
def test_isolate_sys_path_blocks_preloaded_runtime_modules(self):
1356-
import builtins
1357-
import importlib
1358-
1359-
from src.plugin_runtime.runner import runner_main
1360-
1361-
original_import = builtins.__import__
1362-
original_import_module = importlib.import_module
1363-
original_path = list(sys.path)
1364-
original_meta_path = list(sys.meta_path)
1365-
1366-
try:
1367-
runner_main._isolate_sys_path([])
1368-
plugin_globals = {
1369-
"__name__": "_maibot_plugin_demo",
1370-
"__package__": "_maibot_plugin_demo",
1371-
"importlib": importlib,
1372-
}
1373-
1374-
with pytest.raises(ImportError, match="rpc_client"):
1375-
exec('importlib.import_module("src.plugin_runtime.runner.rpc_client")', plugin_globals)
1376-
finally:
1377-
builtins.__import__ = original_import
1378-
importlib.import_module = original_import_module
1379-
sys.path[:] = original_path
1380-
sys.meta_path[:] = original_meta_path
1381-
1382-
def test_isolate_sys_path_keeps_legacy_logger_import_available(self):
1383-
import builtins
1384-
import importlib
1385-
1386-
from src.plugin_runtime.runner import runner_main
1387-
1388-
original_import = builtins.__import__
1389-
original_import_module = importlib.import_module
1390-
original_path = list(sys.path)
1391-
original_meta_path = list(sys.meta_path)
1392-
1393-
try:
1394-
runner_main._isolate_sys_path([])
1395-
plugin_globals = {
1396-
"__name__": "_maibot_plugin_demo",
1397-
"__package__": "_maibot_plugin_demo",
1398-
"importlib": importlib,
1399-
}
1400-
1401-
exec('logger_module = importlib.import_module("src.common.logger")', plugin_globals)
1402-
logger_module = plugin_globals["logger_module"]
1403-
assert callable(logger_module.get_logger)
1404-
finally:
1405-
builtins.__import__ = original_import
1406-
importlib.import_module = original_import_module
1407-
sys.path[:] = original_path
1408-
sys.meta_path[:] = original_meta_path
1409-
1410-
def test_isolate_sys_path_keeps_runtime_imports_working(self):
1411-
import builtins
1412-
import importlib
1413-
1414-
from src.plugin_runtime.runner import runner_main
1415-
1416-
original_import = builtins.__import__
1417-
original_import_module = importlib.import_module
1418-
original_path = list(sys.path)
1419-
original_meta_path = list(sys.meta_path)
1420-
1421-
try:
1422-
runner_main._isolate_sys_path([])
1423-
1424-
uds_module = importlib.import_module("src.plugin_runtime.transport.uds")
1425-
assert hasattr(uds_module, "UDSTransportClient")
1426-
finally:
1427-
builtins.__import__ = original_import
1428-
importlib.import_module = original_import_module
1429-
sys.path[:] = original_path
1430-
sys.meta_path[:] = original_meta_path
1431-
14321301
@pytest.mark.asyncio
14331302
async def test_async_main_removes_sensitive_runtime_env_vars(self, monkeypatch):
14341303
from src.plugin_runtime.runner import runner_main
14351304

14361305
captured = {}
1306+
original_path = list(sys.path)
14371307

14381308
class FakeRunner:
14391309
def __init__(
@@ -1457,7 +1327,6 @@ async def run(self) -> None:
14571327
monkeypatch.setenv(runner_main.ENV_PLUGIN_DIRS, "/tmp/plugins")
14581328
monkeypatch.setenv(runner_main.ENV_EXTERNAL_PLUGIN_IDS, '{"demo.plugin":"1.0.0"}')
14591329
monkeypatch.setattr(runner_main, "_install_shutdown_signal_handlers", lambda callback: None)
1460-
monkeypatch.setattr(runner_main, "_isolate_sys_path", lambda plugin_dirs: None)
14611330
monkeypatch.setattr(runner_main, "PluginRunner", FakeRunner)
14621331

14631332
await runner_main._async_main()
@@ -1466,6 +1335,7 @@ async def run(self) -> None:
14661335
assert captured["session_token"] == "secret-token"
14671336
assert captured["plugin_dirs"] == ["/tmp/plugins"]
14681337
assert captured["external_available_plugins"] == {"demo.plugin": "1.0.0"}
1338+
assert sys.path == original_path
14691339

14701340

14711341
# ─── Host-side ComponentRegistry 测试 ──────────────────────

src/plugin_runtime/runner/runner_main.py

Lines changed: 0 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,191 +1183,6 @@ def request_capability(self) -> RPCClient:
11831183
return self._rpc_client
11841184

11851185

1186-
# ─── sys.path 隔离 ────────────────────────────────────────
1187-
1188-
1189-
def _isolate_sys_path(plugin_dirs: List[str]) -> None:
1190-
"""清理 sys.path,限制 Runner 子进程只能访问标准库、SDK 和插件目录。
1191-
1192-
同时阻止插件代码直接导入主程序内部 ``src.*`` 模块,并清理可直接从
1193-
``sys.modules`` 摸到的高权限叶子模块,避免绕过 SDK / capability 边界。
1194-
"""
1195-
from importlib import util as importlib_util
1196-
from types import ModuleType
1197-
1198-
import builtins
1199-
import importlib
1200-
import sysconfig
1201-
1202-
# 保留: 标准库路径 + site-packages(含 SDK 和依赖)
1203-
stdlib_paths = set()
1204-
for key in ("stdlib", "platstdlib", "purelib", "platlib"):
1205-
if path := sysconfig.get_path(key):
1206-
stdlib_paths.add(os.path.normpath(path))
1207-
1208-
runtime_paths = set(stdlib_paths)
1209-
if os.name == "nt":
1210-
# Windows 的部分平台扩展模块和依赖会通过 <prefix>/DLLs 暴露在 sys.path 中。
1211-
for prefix in {sys.prefix, sys.exec_prefix, sys.base_prefix, sys.base_exec_prefix}:
1212-
if prefix:
1213-
runtime_paths.add(os.path.normpath(os.path.join(prefix, "DLLs")))
1214-
1215-
allowed = set()
1216-
for p in sys.path:
1217-
norm = os.path.normpath(p)
1218-
# 保留标准库和 site-packages
1219-
if any(norm.startswith(runtime_path) for runtime_path in runtime_paths):
1220-
allowed.add(p)
1221-
# 保留 site-packages(第三方库 + SDK)
1222-
if "site-packages" in norm or "dist-packages" in norm:
1223-
allowed.add(p)
1224-
1225-
# 添加插件目录
1226-
plugin_dir_paths = [os.path.normpath(d) for d in plugin_dirs]
1227-
for d in plugin_dir_paths:
1228-
allowed.add(d)
1229-
1230-
preserved_paths = [p for p in sys.path if p in allowed]
1231-
for extra_path in plugin_dir_paths:
1232-
if extra_path not in preserved_paths:
1233-
preserved_paths.append(extra_path)
1234-
sys.path[:] = preserved_paths
1235-
1236-
# 仅为旧版插件兼容层保留极小的 src.* 可见面:
1237-
# - src.plugin_system.*: 通过 maibot_sdk.compat 导入钩子重定向
1238-
# - src.common.logger: 仓库内仍有少量旧插件沿用该日志入口
1239-
allowed_src_exact_modules = frozenset(
1240-
{
1241-
"src",
1242-
"src.common",
1243-
"src.common.logger",
1244-
"src.common.logger_color_and_mapping",
1245-
}
1246-
)
1247-
allowed_src_prefixes = ("src.plugin_system",)
1248-
plugin_module_prefix = "_maibot_plugin_"
1249-
1250-
def _is_allowed_src_module(fullname: str) -> bool:
1251-
"""判断给定 src.* 模块是否在 Runner 允许列表中。"""
1252-
if fullname in allowed_src_exact_modules:
1253-
return True
1254-
return any(fullname == prefix or fullname.startswith(f"{prefix}.") for prefix in allowed_src_prefixes)
1255-
1256-
def _resolve_requester_name(import_globals: Any = None) -> str:
1257-
"""解析当前导入请求的发起模块名。"""
1258-
if isinstance(import_globals, dict):
1259-
for key in ("__name__", "__package__"):
1260-
value = import_globals.get(key)
1261-
if isinstance(value, str) and value:
1262-
return value
1263-
1264-
frame = inspect.currentframe()
1265-
try:
1266-
current = frame.f_back if frame is not None else None
1267-
while current is not None:
1268-
module_name = current.f_globals.get("__name__", "")
1269-
if not isinstance(module_name, str) or not module_name:
1270-
current = current.f_back
1271-
continue
1272-
if module_name == __name__ or module_name.startswith("importlib"):
1273-
current = current.f_back
1274-
continue
1275-
return module_name
1276-
return ""
1277-
finally:
1278-
del frame
1279-
1280-
def _is_plugin_import_request(import_globals: Any = None) -> bool:
1281-
"""判断当前导入是否由插件模块直接发起。"""
1282-
requester_name = _resolve_requester_name(import_globals)
1283-
return requester_name.startswith(plugin_module_prefix)
1284-
1285-
def _format_block_message(fullname: str) -> str:
1286-
"""构造统一的拒绝导入错误信息。"""
1287-
return (
1288-
f"Runner 子进程不允许导入主程序模块: {fullname}。"
1289-
"请改用 maibot_sdk 或 src.plugin_system 兼容层提供的接口。"
1290-
)
1291-
1292-
def _iter_requested_src_modules(name: str, fromlist: Any) -> List[str]:
1293-
"""展开本次导入请求涉及的 src.* 模块名。"""
1294-
requested_modules = [name]
1295-
if not name.startswith("src") or not fromlist:
1296-
return requested_modules
1297-
1298-
for item in fromlist:
1299-
if not isinstance(item, str) or not item or item == "*":
1300-
continue
1301-
requested_modules.append(f"{name}.{item}")
1302-
return requested_modules
1303-
1304-
def _assert_plugin_import_allowed(name: str, import_globals: Any = None, fromlist: Any = ()) -> None:
1305-
"""在插件发起导入时校验目标 src.* 模块是否允许访问。"""
1306-
if not _is_plugin_import_request(import_globals):
1307-
return
1308-
1309-
for requested_module in _iter_requested_src_modules(name, fromlist):
1310-
if not requested_module.startswith("src"):
1311-
continue
1312-
if _is_allowed_src_module(requested_module):
1313-
continue
1314-
raise ImportError(_format_block_message(requested_module))
1315-
1316-
def _detach_module_from_parent(fullname: str, module: ModuleType) -> None:
1317-
"""从父模块上移除已清理模块的属性引用。"""
1318-
parent_name, _, child_name = fullname.rpartition(".")
1319-
if not parent_name or not child_name:
1320-
return
1321-
1322-
parent_module = sys.modules.get(parent_name)
1323-
if parent_module is None:
1324-
return
1325-
if getattr(parent_module, child_name, None) is module:
1326-
with contextlib.suppress(AttributeError):
1327-
delattr(parent_module, child_name)
1328-
1329-
# 仅清理已加载的叶子模块,保留包对象给 Runner 自己的延迟导入和相对导入使用。
1330-
existing_src_modules = sorted(
1331-
(
1332-
(module_name, module)
1333-
for module_name, module in list(sys.modules.items())
1334-
if module_name == "src" or module_name.startswith("src.")
1335-
),
1336-
key=lambda item: item[0].count("."),
1337-
reverse=True,
1338-
)
1339-
for module_name, module in existing_src_modules:
1340-
if _is_allowed_src_module(module_name) or hasattr(module, "__path__"):
1341-
continue
1342-
_detach_module_from_parent(module_name, module)
1343-
sys.modules.pop(module_name, None)
1344-
1345-
# ``import`` 语句与 ``importlib.import_module`` 走的是不同入口,因此两边都需要兜底。
1346-
builtins_module = cast(Any, builtins)
1347-
original_import = getattr(builtins_module, "__maibot_runner_original_import__", builtins.__import__)
1348-
builtins_module.__maibot_runner_original_import__ = original_import
1349-
1350-
def _guarded_import(name: str, globals: Any = None, locals: Any = None, fromlist: Any = (), level: int = 0) -> Any:
1351-
if level == 0:
1352-
_assert_plugin_import_allowed(name, import_globals=globals, fromlist=fromlist)
1353-
return original_import(name, globals, locals, fromlist, level)
1354-
1355-
cast(Any, _guarded_import).__maibot_runner_plugin_import_guard__ = True
1356-
builtins.__import__ = _guarded_import
1357-
1358-
importlib_module = cast(Any, importlib)
1359-
original_import_module = getattr(importlib_module, "__maibot_runner_original_import_module__", importlib.import_module)
1360-
importlib_module.__maibot_runner_original_import_module__ = original_import_module
1361-
1362-
def _guarded_import_module(name: str, package: Optional[str] = None) -> Any:
1363-
resolved_name = importlib_util.resolve_name(name, package) if name.startswith(".") else name
1364-
_assert_plugin_import_allowed(resolved_name)
1365-
return original_import_module(name, package)
1366-
1367-
cast(Any, _guarded_import_module).__maibot_runner_plugin_import_guard__ = True
1368-
importlib.import_module = _guarded_import_module
1369-
1370-
13711186
# ─── 进程入口 ──────────────────────────────────────────────
13721187

13731188

@@ -1392,9 +1207,6 @@ async def _async_main() -> None:
13921207
logger.warning("外部依赖插件版本映射格式非法,已回退为空映射")
13931208
external_plugin_ids = {}
13941209

1395-
# sys.path 隔离: 只保留标准库、SDK 包、插件目录
1396-
_isolate_sys_path(plugin_dirs)
1397-
13981210
runner = PluginRunner(
13991211
host_address,
14001212
session_token,

0 commit comments

Comments
 (0)