mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 23:41:58 +00:00
3e311ec551
整合(3c-integrate) - 公共层:跨天时长(formatSpan / formatOffset / formatLapse)、本机署名(lib/actor)、检索分类色令牌、 数据源目录(useDatasources)收进公共层;失败说明能一次列出多个写坏的自定义工具,查询超时指到数据源的查询时限 - 管理页收尾:设置页接入统一的离开前确认;检查脚本不再把沙箱里的真实值打进输出 - 画布 ↔ 右栏:点出具印章、点节点能跳到右栏对应位置;「定位」直接聚焦到出问题的字段 - 回答流 / 问数据 / 记录页:失败信息带「去哪修」且不再被翻译两遍;协作团队的结局按执行截取; 等审批时断流重连、而运行已在空档里跑完的,补全事件 扫尾(3c-sweep) - 术语统一:开始运行 / 继续运行、工作流最大步数;未保存的运行统一叫「未保存的工作流」(后端新运行同步) - 沙箱报错不再带 Python 异常类名;新增 check-tokens,未定义的 CSS 变量为 0,两个旧别名迁移后删除 检查脚本(3c-checks) - check-ui 跟上新界面(运行列表按 data 属性找行),加上令牌与选中态的回归断言,输出里的名字打码 - 新增 check-all:按顺序跑全部检查、汇总退出码,--repeat 列出偶发项 - 单节出错只记这一节失败、不再带崩整个脚本;外壳遥测浮层「重新检测」时焦点不再掉到 body 网页工具与 fake-ip - Clash / mihomo 一类代理开 TUN + fake-ip 时,查什么都先拿到 198.18 段的假地址,被当成内网拦截。 新增 AGENTLAB_HTTP_TOOL_FAKE_IP_RANGES(默认空):落在段里的解析结果改向公共 DNS(DoH)核实真实地址, 公网放行、内网照拦、核实不了按拦截处理。DoH 默认先问境外的,境内公共 DNS 会对被封锁的域名返回污染结果 - 列表型环境变量接受逗号分隔(原来注释说逗号分隔、实际只认 JSON 数组);域名解析改为异步,不再阻塞事件循环 验证(沙箱):tsc 干净,pytest 866 passed(两遍),检查脚本全绿。
112 lines
4.6 KiB
Python
112 lines
4.6 KiB
Python
"""沙箱起不来时的报错:说原因,不带 Python 异常类名(SPEC §3.1)。
|
|
|
|
四个后端起进程、连 VM 失败时以前都写 f"{type(e).__name__}: {e}",代码节点再把它
|
|
拼进首行,界面上的红字就成了「代码执行失败(exit=0):FileNotFoundError: [Errno 2]
|
|
No such file or directory: …」。原始异常留在日志里,排查时照样找得到。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import re
|
|
import types
|
|
|
|
import pytest
|
|
|
|
from app.engine.context import NodeContext, NodeError, RunContext
|
|
from app.engine.nodes.tools import run_code
|
|
from app.engine.schema import GraphNode, GraphSpec
|
|
from app.sandbox import interpreter
|
|
from app.sandbox.bubblewrap_sandbox import BubblewrapSandbox
|
|
from app.sandbox.local_sandbox import LocalSandbox
|
|
from app.sandbox.microvm_sandbox import MicroVMSandbox
|
|
from app.sandbox.seatbelt_sandbox import SeatbeltSandbox
|
|
|
|
_CLASS_NAME = re.compile(r"\b[A-Z]\w*(?:Error|Exception)\b")
|
|
|
|
|
|
def _broken_interpreter(tmp_path) -> str:
|
|
"""一个存在、可执行,但 shebang 指向不存在的解释器的文件:which 找得到,exec 起不来。"""
|
|
fake = tmp_path / "python-broken"
|
|
fake.write_text("#!/nonexistent/interpreter\n")
|
|
fake.chmod(0o755)
|
|
return str(fake)
|
|
|
|
|
|
def test_a_code_node_whose_interpreter_cannot_start_says_why(tmp_path, monkeypatch) -> None:
|
|
import app.engine.nodes.tools as tools
|
|
|
|
monkeypatch.setattr(interpreter, "python_argv", lambda: [_broken_interpreter(tmp_path)])
|
|
local = LocalSandbox()
|
|
|
|
class _Manager:
|
|
async def run(self, code, *, backend=None, **kwargs): # noqa: ANN001, ANN003, ANN201
|
|
return await local.run(code, **kwargs)
|
|
|
|
monkeypatch.setattr(tools, "sandbox_manager", _Manager())
|
|
node = GraphNode(id="calc", type="code",
|
|
data={"label": "计算", "config": {"language": "python", "code": "print(1)",
|
|
"isolation": "fast"}})
|
|
ctx = NodeContext(node=node, run=RunContext(run_id="r1", thread_id="t1",
|
|
spec=GraphSpec(nodes=[node], edges=[])))
|
|
|
|
with pytest.raises(NodeError) as caught:
|
|
asyncio.run(run_code({"vars": {}}, ctx))
|
|
text = str(caught.value)
|
|
assert text.startswith("代码执行失败"), text
|
|
assert not _CLASS_NAME.search(text), text
|
|
|
|
|
|
async def _boom(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
|
raise PermissionError(13, "Permission denied", "/usr/bin/sandbox-exec")
|
|
|
|
|
|
@pytest.mark.parametrize("backend", [LocalSandbox, SeatbeltSandbox, BubblewrapSandbox])
|
|
def test_a_process_that_cannot_spawn_has_no_class_name(backend, monkeypatch) -> None:
|
|
if backend is not LocalSandbox:
|
|
monkeypatch.setattr(backend, "available", staticmethod(lambda: True))
|
|
monkeypatch.setattr(asyncio, "create_subprocess_exec", _boom)
|
|
|
|
result = asyncio.run(backend().run("print(1)"))
|
|
assert not result.ok
|
|
assert result.error and not _CLASS_NAME.search(result.error), result.error
|
|
|
|
|
|
def _microvm(monkeypatch) -> MicroVMSandbox:
|
|
async def nothing(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
|
return None
|
|
|
|
monkeypatch.setattr(MicroVMSandbox, "available", staticmethod(lambda: True))
|
|
monkeypatch.setattr(MicroVMSandbox, "_reap_idle", nothing)
|
|
monkeypatch.setattr(MicroVMSandbox, "_destroy", nothing)
|
|
return MicroVMSandbox()
|
|
|
|
|
|
def test_a_microvm_that_cannot_boot_has_no_class_name(monkeypatch) -> None:
|
|
async def no_vm(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
|
raise RuntimeError("虚拟化框架拒绝了这次启动")
|
|
|
|
vm = _microvm(monkeypatch)
|
|
monkeypatch.setattr(MicroVMSandbox, "_lease", no_vm)
|
|
result = asyncio.run(vm.run("print(1)"))
|
|
assert result.error and result.error.startswith("microVM 启动失败"), result.error
|
|
assert not _CLASS_NAME.search(result.error), result.error
|
|
|
|
|
|
def test_a_microvm_that_drops_mid_exec_has_no_class_name(monkeypatch) -> None:
|
|
async def write(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
|
return None
|
|
|
|
async def gone(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
|
raise ConnectionResetError(54, "Connection reset by peer")
|
|
|
|
lease = types.SimpleNamespace(vm=types.SimpleNamespace(fs=types.SimpleNamespace(write=write, mkdir=write),
|
|
exec=gone))
|
|
|
|
async def leased(*args, **kwargs): # noqa: ANN002, ANN003, ANN202
|
|
return lease
|
|
|
|
vm = _microvm(monkeypatch)
|
|
monkeypatch.setattr(MicroVMSandbox, "_lease", leased)
|
|
result = asyncio.run(vm.run("print(1)"))
|
|
assert result.error and not _CLASS_NAME.search(result.error), result.error
|