"""工具定义:ToolSpec 描述工具元数据与请求构造方式,ToolResult 为执行产出。 字段口径对齐 docs/architecture/service-boundaries.md(工具定义字段、风险分级、 权限策略)。工具的实际执行有两条路径: - 沙箱路径:``build_request`` 产出沙箱 ExecRequest,由 SandboxClient 执行(文件/shell 类)。 - 直接执行路径:``executor`` 非 None 时由 runtime 注入 http_client、密钥等上下文直接执行 (HTTP 类工具如 fetch_url,不走沙箱)。 本模块是纯领域数据结构(无 IO):``ExecRequest`` 仅在类型注解中引用(``TYPE_CHECKING``), 运行时不依赖 ``integrations``;实际 ExecRequest 的构造在 ``application/tool_runtime/``。 """ from __future__ import annotations from collections.abc import Awaitable, Callable from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Protocol from app.modules.safety.governor import PermissionPolicy if TYPE_CHECKING: # 仅类型注解用:build_request 产出沙箱 ExecRequest,但本模块运行时不依赖 integrations # (保持 modules 零 IO 依赖);实际构造在 application/tool_runtime/builtins。 from app.integrations.sandbox.models import ExecRequest ToolInput = dict[str, Any] RequestBuilder = Callable[[ToolInput], "ExecRequest"] # 前向引用 ToolResult(定义在下方),避免循环依赖;TYPE_CHECKING 下解析为真实类。 ToolExecutor = Callable[[ToolInput, dict[str, Any] | None], Awaitable["ToolResult"]] class ToolRuntimeLike(Protocol): """Agent loop 所需的最小工具运行时契约。""" async def execute( self, tool: str, input: ToolInput, *, run_id: str | None = None, character_id: str | None = None, trace_id: str | None = None, confirmed: bool = False, context: dict[str, Any] | None = None, ) -> ToolResult: ... @dataclass(frozen=True) class ToolResult: """工具执行产出(映射沙箱 ExecResponse 或 executor 直接产出)。""" ok: bool exit_code: int | None stdout: str stderr: str duration_ms: int timeout: bool truncated: bool trace_id: str @dataclass(frozen=True) class ToolSpec: """工具规格:名称、风险、权限策略与请求构造器/执行器。 执行路径二选一: - ``executor`` 非 None:直接执行,跳过沙箱(HTTP 类工具);``build_request`` 此时可为占位。 - ``executor`` 为 None:走沙箱路径,用 ``build_request`` 构造 ExecRequest。 """ name: str description: str risk_level: str # L0–L4 permission_policy: PermissionPolicy build_request: RequestBuilder = field(default_factory=lambda: _placeholder_builder) default_timeout_ms: int = 30_000 side_effect: bool = True audit_required: bool = True # 是否按 run/trace 隔离工作目录(文件类工具):runtime 会把 workdir 改写为 # ``/``,防跨 run 信息泄漏与文件名冲突;run_key 经 sanitize 防路径注入。 isolated_workspace: bool = False # 非 None 时走直接执行路径(HTTP 类工具),跳过沙箱;context 由 runtime 注入。 executor: ToolExecutor | None = None # 工具入参 JSON Schema(供 LLM 生成正确参数),None 表示无参数。 input_schema: dict[str, Any] | None = None def _placeholder_builder( _input: ToolInput, ) -> ExecRequest: # pragma: no cover - 仅占位,executor 工具不走此路径 """executor 工具的 build_request 占位:正常不应被调用(runtime 见 executor 非 None 会跳过)。""" raise NotImplementedError("该工具使用 executor 直接执行,不应调用 build_request")