fix(cheap): Service 关 workspace 内建工具(Bash/Edit/Glob/Grep/Read/Write)——内建 Write/Bash 旁路六工具写白名单(护城河击穿)+ M3 12+工具面语义重叠混乱税(80009 死圈/80011 失败目录:Bash CWD 混乱/read_file 被串 Read 参数/Edit 连败,签名富化观测实证) (cutover S2 工具面封口)
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-03 19:12:35 -07:00
parent daee094c83
commit a6d5c29ad3
3 changed files with 106 additions and 0 deletions

View File

@ -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-<id>)。

View File

@ -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}"

View File

@ -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,
)