diff --git a/cheap-worker/cheap_service_app.py b/cheap-worker/cheap_service_app.py index a7460943..55946055 100644 --- a/cheap-worker/cheap_service_app.py +++ b/cheap-worker/cheap_service_app.py @@ -365,6 +365,13 @@ def build_cheap_app(*, title: str = SERVICE_TITLE) -> "FastAPI": from ctx_compress_patch import apply_ctx_compress_patch # noqa: PLC0415 apply_ctx_compress_patch() + # 工具面封口:2.0.2 Service 路把 workspace 内建(Bash/Edit/Glob/Grep/Read/Write)无条件并入 agent + # 工具面 → 便宜档六工具写白名单被内建 Write/Bash 旁路 + M3 拿内建 Bash 相对路径死圈(80009/80011 + # 生产实证)。本进程只跑便宜档 agent,进程内关内建作用域恰好;tier2 服务独立进程不受影响。 + # 【钉 agentscope==2.0.2,升级必须复核】纪律同 m3_stream_patch(不改 venv 本体)。 + from ws_builtin_tools_patch import apply_ws_builtin_tools_patch # noqa: PLC0415 + apply_ws_builtin_tools_patch() + storage_params = infra_config.redis_params(for_message_bus=False) bus_params = infra_config.redis_params(for_message_bus=True) # cheap 独立 workspace 根(与 tier2 的 _service-workspaces 分开;本线生成产物另落 game-runtime/games/amgen-)。 diff --git a/cheap-worker/tests/test_ws_builtin_tools_patch.py b/cheap-worker/tests/test_ws_builtin_tools_patch.py new file mode 100644 index 00000000..d587db98 --- /dev/null +++ b/cheap-worker/tests/test_ws_builtin_tools_patch.py @@ -0,0 +1,45 @@ +"""ws_builtin_tools_patch 单测:补丁后 LocalWorkspace 内建工具为空、幂等、六工具白名单路不受影响。 +跑:cheap-worker/.venv/bin/python -m pytest cheap-worker/tests/test_ws_builtin_tools_patch.py -v +""" +import asyncio +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/ +import _bootstrap # noqa: E402,F401 仅加 sys.path(import 时不取 key、不触网) +from ws_builtin_tools_patch import apply_ws_builtin_tools_patch # noqa: E402 + + +def test_patch_empties_builtin_tools(tmp_path): + # 补丁前:2.0.2 硬编码六内建(Bash/Edit/Glob/Grep/Read/Write);补丁后:空表。 + from agentscope.workspace import LocalWorkspace + + apply_ws_builtin_tools_patch() + ws = LocalWorkspace(workspace_id="t", workdir=str(tmp_path)) + assert asyncio.run(ws.list_tools()) == [], "补丁后内建工具面必须为空(写白名单不被旁路)" + + +def test_patch_idempotent(): + # 重复 apply 不再包层(标记位短路),且行为不变。 + from agentscope.workspace import LocalWorkspace + + apply_ws_builtin_tools_patch() + fn1 = LocalWorkspace.list_tools + apply_ws_builtin_tools_patch() + assert LocalWorkspace.list_tools is fn1, "幂等:第二次 apply 不应替换函数对象" + + +def test_cheap_six_tools_unaffected(tmp_path): + # 六工具经 build_toolkit(extra_agent_tools 工厂路)产出,不走 workspace.list_tools —— 补丁不动它。 + # 提取方式与生产同源(service.app._extract_function_tools,从 Toolkit.tool_groups 收)。 + apply_ws_builtin_tools_patch() + from cheap_toolkit import CheapSession, build_toolkit + from service.app import _extract_function_tools + + session = CheapSession(game_id="99999") + tools = _extract_function_tools(build_toolkit(session)) + names = {getattr(t, "name", getattr(getattr(t, "func", None), "__name__", "")) for t in tools} + # 至少核心读写与收尾工具在席(名字集按 cheap_toolkit 现约定,松断言防脆)。 + assert any("read" in n for n in names), f"六工具应含读工具,实际:{names}" + assert any("write" in n for n in names), f"六工具应含写工具,实际:{names}" + assert any("finish" in n for n in names), f"六工具应含 finish,实际:{names}" diff --git a/cheap-worker/ws_builtin_tools_patch.py b/cheap-worker/ws_builtin_tools_patch.py new file mode 100644 index 00000000..e75f470a --- /dev/null +++ b/cheap-worker/ws_builtin_tools_patch.py @@ -0,0 +1,54 @@ +"""ws_builtin_tools_patch.py — 便宜档 Service 关 workspace 内建工具(Bash/Edit/Glob/Grep/Read/Write)。 + +【坐实根因(2026-07-04 cutover S2 生产实证)】agentscope 2.0.2 Service 路 build_toolkit +(app/_service/_toolkit.py:107)把 workspace.list_tools() 的六内建(workspace/_local_workspace.py +:670-679 硬编码,无配置开关)无条件并入 agent 工具面,再拼 extra_agent_tools(便宜档六工具)。后果两条: +① 写边界击穿:便宜档护城河 = 六工具白名单(write 越界拒 + basename L1_FIXED 拒,I1 fail-closed), + 内建 Write/Edit/Bash 不走白名单、可写工作区任意路径(Bash 甚至可越 workdir 触仓外)——白名单被旁路; +② 失败噪声/死圈:内建 Bash cwd=Service 工作区(非 repo 根),M3 惯性用相对路径 `ls game-runtime/...` + 连败(80009 四连败触旧版退化签名 stuck 熔断 ¥2.6 作废;80011 复现同款失败指纹,签名富化打印抓获)。 +CLI 路(cheap_run)从来只有六工具;Service 归并(配置控制面阶段一②)带进了这个面,per-task review +未抓到,live 工具失败观测抓获——「跨路径封口」教训再 +1(tech-decisions §12 同款)。 + +【修法】cheap Service 进程内 monkeypatch LocalWorkspace.list_tools → 返回 []。作用域正确性:本进程 +(cheap_service_app)只跑便宜档 agent,进程内全局补丁 = 恰好全体便宜档 agent;tier2 服务是独立进程, +不受影响。skills/MCP 装载(list_skills / MCP 发现)不动;Planning/Team/Schedule 工具面收窄另评 +(工单 h;其成本风险已有 ¥ 两段式硬地板兜上界)。 + +【钉 agentscope==2.0.2,升级必须复核此补丁】运行时 monkeypatch,绝不改 venv/site-packages 文件本体。 +依赖两个 2.0.2 契约:① _toolkit.py build_toolkit 以 `workspace.list_tools()` 为内建来源; +② LocalWorkspace.list_tools 为 async 无参方法。升级 agentscope 后必须复核上述两点并重跑 +tests/test_ws_builtin_tools_patch.py(版本漂移时 apply 响亮 warning、但仍应用——不应用等于开着白名单旁路)。 +""" + +from __future__ import annotations + + +def apply_ws_builtin_tools_patch() -> None: + """把 LocalWorkspace.list_tools 换成恒返回空表(便宜档 Service 进程专用,幂等)。""" + import agentscope # noqa: PLC0415 + from agentscope.workspace import LocalWorkspace # noqa: PLC0415 + + ver = getattr(agentscope, "__version__", "?") + if ver != "2.0.2": + # 版本漂移:响亮告警但仍应用(不应用 = 内建 Bash/Write 旁路写白名单,比契约漂移更危险)。 + print( + f"[ws-builtin-patch] WARNING: agentscope 版本 {ver} != 钉住的 2.0.2," + "list_tools 契约可能已变,升级复核未做——补丁仍应用,请尽快复核。", + flush=True, + ) + + if getattr(LocalWorkspace.list_tools, "_cheap_ws_patched", False): + return # 幂等:重复 apply 不再包一层 + + async def _no_builtin_tools(self): # noqa: ANN001 —— 与 2.0.2 原签名一致(async 无参) + # 便宜档:内建工具全关(写边界只认六工具白名单;根因与作用域见模块头注释)。 + return [] + + _no_builtin_tools._cheap_ws_patched = True # type: ignore[attr-defined] + LocalWorkspace.list_tools = _no_builtin_tools + print( + "[ws-builtin-patch] 已关 workspace 内建工具(Bash/Edit/Glob/Grep/Read/Write)——" + "便宜档写边界只认六工具白名单(钉 2.0.2,升级必须复核)。", + flush=True, + )