zcbot/core/sandbox/tool_runner.py

81 lines
2.6 KiB
Python

"""容器内 fs 工具 helper(DockerExecutor 通过 `docker exec python tool_runner.py` 调用)。
调用约定:
- argv[1] = tool name(read / write / edit / glob / grep)
- stdin = JSON 序列化的 args(用 stdin 而非 argv 是为了不被 shell metachar 切路径,
CJK / 引号 / 路径分隔符全透明传)
- stdout = tool execute 返回的文本(LLM 拿到的)
- exit code = 0 ok / 1 工具内部抛异常 / 2 参数 / unknown tool
base_dir = `os.getcwd()` ── docker exec --workdir /workspace/<wd> 已切到 task 工作目录
user_root = `/workspace` ── bind mount 边界,Tool._display 据此渲相对路径
不依赖任何 zcbot 自家包外的东西,纯用 `tools.fs` 五个 Tool 子类。容器镜像里
`/sandbox/tools/` 是 host repo `tools/` 目录的拷贝(Dockerfile `COPY tools/`)。
"""
from __future__ import annotations
import json
import os
import sys
import traceback
from pathlib import Path
# 镜像里 /sandbox/ 下放了 tools/ 的拷贝,让 import 走 /sandbox/
sys.path.insert(0, "/sandbox")
from tools.fs import EditTool, GlobTool, GrepTool, ReadTool, WriteTool # noqa: E402
TOOLS = {
"read": ReadTool,
"write": WriteTool,
"edit": EditTool,
"glob": GlobTool,
"grep": GrepTool,
}
def main() -> int:
if len(sys.argv) < 2:
print("[Error] tool_runner: missing tool name argv[1]", file=sys.stderr)
return 2
name = sys.argv[1]
if name not in TOOLS:
print(f"[Error] tool_runner: unknown tool: {name}", file=sys.stderr)
return 2
try:
args_raw = sys.stdin.read()
args = json.loads(args_raw) if args_raw.strip() else {}
except json.JSONDecodeError as e:
print(f"[Error] tool_runner: invalid JSON args: {e}", file=sys.stderr)
return 2
cls = TOOLS[name]
tool = cls(base_dir=Path(os.getcwd()), user_root=Path("/workspace"))
try:
result = tool.execute(**args)
except TypeError as e:
print(f"[Error] bad arguments to {name}: {e}", file=sys.stderr)
return 2
except Exception as e:
# 容器内 Tool 抛异常(IO / 权限等)── 落 stderr + 退非 0,DockerExecutor
# 兜底成 ToolResult content;traceback 限 80 行防爆 LLM context
print(f"[Error executing {name}] {type(e).__name__}: {e}", file=sys.stderr)
tb = traceback.format_exc().splitlines()
if len(tb) > 80:
tb = tb[:40] + [f"... ({len(tb) - 40} lines truncated) ..."]
print("\n".join(tb), file=sys.stderr)
return 1
if not isinstance(result, str):
result = str(result)
sys.stdout.write(result)
return 0
if __name__ == "__main__":
sys.exit(main())