From a6d5c29ad36af65ee5a6dc7cd459536d4d4c5ee5 Mon Sep 17 00:00:00 2001 From: lili Date: Fri, 3 Jul 2026 19:12:35 -0700 Subject: [PATCH] =?UTF-8?q?fix(cheap):=20Service=20=E5=85=B3=20workspace?= =?UTF-8?q?=20=E5=86=85=E5=BB=BA=E5=B7=A5=E5=85=B7(Bash/Edit/Glob/Grep/Rea?= =?UTF-8?q?d/Write)=E2=80=94=E2=80=94=E5=86=85=E5=BB=BA=20Write/Bash=20?= =?UTF-8?q?=E6=97=81=E8=B7=AF=E5=85=AD=E5=B7=A5=E5=85=B7=E5=86=99=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95(=E6=8A=A4=E5=9F=8E=E6=B2=B3=E5=87=BB?= =?UTF-8?q?=E7=A9=BF)+=20M3=2012+=E5=B7=A5=E5=85=B7=E9=9D=A2=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=E9=87=8D=E5=8F=A0=E6=B7=B7=E4=B9=B1=E7=A8=8E(80009=20?= =?UTF-8?q?=E6=AD=BB=E5=9C=88/80011=20=E5=A4=B1=E8=B4=A5=E7=9B=AE=E5=BD=95?= =?UTF-8?q?:Bash=20CWD=20=E6=B7=B7=E4=B9=B1/read=5Ffile=20=E8=A2=AB?= =?UTF-8?q?=E4=B8=B2=20Read=20=E5=8F=82=E6=95=B0/Edit=20=E8=BF=9E=E8=B4=A5?= =?UTF-8?q?,=E7=AD=BE=E5=90=8D=E5=AF=8C=E5=8C=96=E8=A7=82=E6=B5=8B?= =?UTF-8?q?=E5=AE=9E=E8=AF=81)=20(cutover=20S2=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E9=9D=A2=E5=B0=81=E5=8F=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- cheap-worker/cheap_service_app.py | 7 +++ .../tests/test_ws_builtin_tools_patch.py | 45 ++++++++++++++++ cheap-worker/ws_builtin_tools_patch.py | 54 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 cheap-worker/tests/test_ws_builtin_tools_patch.py create mode 100644 cheap-worker/ws_builtin_tools_patch.py 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, + )