"""工具调用 API(api-spec §15)。 - GET /tools:列出已注册工具策略元数据(从 tool_policies 表,含 executable 派生)。 - POST /tools:注册自定义工具策略(百工枢器用治理声明,记审计)。 - GET /tools/calls/{tool_call_id}:单次工具调用事件详情。 - PATCH /tools/{tool_name}:编辑工具策略元数据(spec §15: risk_level 等)。 - DELETE /tools/{tool_name}:注销自定义工具(builtin 不可删)。 - POST /tools/{tool_name}/invoke:调用工具;高风险返回 requires_confirmation + confirmation_id, 交由 /safety/confirmations/{id}/approve 消费。 - POST /tools/{tool_name}/invoke/stream:流式调用(SSE),实时推送 tool.stdout/tool.stderr 增量与 tool.completed;司命 deny/ask-未确认/沙箱错误分别以 tool.failed / tool.requires_confirmation 结束流。 策略写操作(POST/PATCH/DELETE)后刷新 ToolRuntime 的司命决策器,使变更即时生效; tool_runtime 未就绪时仅写库(重启后从 db 加载)。 """ from __future__ import annotations import json from typing import Any from fastapi import APIRouter, Depends, Response, status from pydantic import BaseModel from sse_starlette.sse import EventSourceResponse from app.api.v1.deps import get_confirmation_store, get_optional_tool_runtime, get_tool_runtime from app.application.tool_runtime.runtime import ToolRuntime from app.db.session import AsyncSessionLocal from app.integrations.sandbox.exceptions import SandboxError from app.models.safety import PendingConfirmationRecord from app.modules.safety.confirmation import ( ConfirmationStore, PendingConfirmation, new_confirmation_id, ) from app.modules.safety.exceptions import PermissionDenied, RequiresConfirmation from app.schemas.safety import ( CreateToolPolicyRequest, ToolCallDetail, ToolPolicyResponse, UpdateToolPolicyRequest, ) from app.schemas.skills import ToolCapability from app.services import governance as governance_service from app.services import skills as skills_service router = APIRouter(prefix="/tools", tags=["tools"]) class InvokeRequest(BaseModel): input: dict[str, Any] run_id: str | None = None character_id: str | None = None trace_id: str | None = None confirmed: bool = False class ToolResultPayload(BaseModel): ok: bool exit_code: int | None = None stdout: str = "" stderr: str = "" duration_ms: int = 0 timeout: bool = False truncated: bool = False trace_id: str = "" class CompletedResponse(BaseModel): status: str = "completed" result: ToolResultPayload class RequiresConfirmationResponse(BaseModel): status: str = "requires_confirmation" confirmation_id: str risk_level: str reason: str async def _sync_confirmation_record(pending: PendingConfirmation) -> None: """把 InMemoryConfirmationStore 的待确认记录同步持久化到 DB。""" async with AsyncSessionLocal() as session: record = PendingConfirmationRecord( id=pending.confirmation_id, tool=pending.tool, input_json=json.dumps(pending.input) if pending.input else "{}", risk_level=pending.risk_level, reason=pending.reason or "", run_id=pending.run_id, character_id=pending.character_id, trace_id=pending.trace_id, status="pending", ) session.add(record) await session.commit() # —— §15 工具策略治理 —— @router.get("", response_model=list[ToolPolicyResponse]) async def list_tools() -> list[ToolPolicyResponse]: return await governance_service.list_tool_policies() @router.get("/capabilities", response_model=list[ToolCapability]) async def list_capabilities( runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> list[ToolCapability]: """统一列举所有能力(builtin / mcp / skill,含来源标记,skills-design §6)。""" return await skills_service.list_all_capabilities(runtime) @router.post("", response_model=ToolPolicyResponse, status_code=status.HTTP_201_CREATED) async def register_tool( payload: CreateToolPolicyRequest, runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> ToolPolicyResponse: response = await governance_service.create_tool_policy(payload) if runtime is not None: await runtime.reload_governor() return response @router.get("/calls/{tool_call_id}", response_model=ToolCallDetail) async def get_tool_call(tool_call_id: str) -> ToolCallDetail: return await governance_service.get_tool_call(tool_call_id) @router.patch("/{tool_name}", response_model=ToolPolicyResponse) async def update_tool( tool_name: str, payload: UpdateToolPolicyRequest, runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> ToolPolicyResponse: response = await governance_service.update_tool_policy(tool_name, payload) if runtime is not None: await runtime.reload_governor() return response @router.delete("/{tool_name}", status_code=status.HTTP_204_NO_CONTENT) async def delete_tool( tool_name: str, runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> Response: await governance_service.delete_tool_policy(tool_name) if runtime is not None: await runtime.reload_governor() return Response(status_code=status.HTTP_204_NO_CONTENT) # —— §15 工具调用 —— @router.post("/{tool_name}/invoke", response_model=None) async def invoke_tool( tool_name: str, body: InvokeRequest, runtime: ToolRuntime = Depends(get_tool_runtime), confirmations: ConfirmationStore = Depends(get_confirmation_store), ) -> CompletedResponse | RequiresConfirmationResponse: try: result = await runtime.execute( tool_name, body.input, run_id=body.run_id, character_id=body.character_id, trace_id=body.trace_id, confirmed=body.confirmed, ) except RequiresConfirmation as exc: pending = await confirmations.create( PendingConfirmation( confirmation_id=new_confirmation_id(), tool=tool_name, input=body.input, risk_level=exc.risk_level or "L2", reason=exc.message, run_id=body.run_id, character_id=body.character_id, trace_id=body.trace_id, ) ) # 同步持久化 PendingConfirmationRecord(供 safety/confirmations 新端点查找) await _sync_confirmation_record(pending) return RequiresConfirmationResponse( confirmation_id=pending.confirmation_id, risk_level=pending.risk_level, reason=pending.reason, ) return CompletedResponse( result=ToolResultPayload( ok=result.ok, exit_code=result.exit_code, stdout=result.stdout, stderr=result.stderr, duration_ms=result.duration_ms, timeout=result.timeout, truncated=result.truncated, trace_id=result.trace_id, ) ) @router.post("/{tool_name}/invoke/stream") async def invoke_tool_stream( tool_name: str, body: InvokeRequest, runtime: ToolRuntime = Depends(get_tool_runtime), ) -> EventSourceResponse: """流式调用工具(SSE)。 事件:``tool.stdout`` / ``tool.stderr``(data 为 ``{"data":""}``,前端解码)、 ``tool.completed``(data 为结束汇总 JSON)。司命 deny → ``tool.failed``; ask 且未确认 → ``tool.requires_confirmation``(前端应先走非流式 invoke 完成 confirmation, 再以 confirmed=True 调用本端点);沙箱错误 → ``tool.failed``。 """ async def event_gen(): try: async for ev in runtime.execute_stream( tool_name, body.input, run_id=body.run_id, character_id=body.character_id, trace_id=body.trace_id, confirmed=body.confirmed, ): if ev.kind in ("stdout", "stderr"): yield {"event": f"tool.{ev.kind}", "data": json.dumps({"data": ev.payload})} elif ev.kind == "done": yield {"event": "tool.completed", "data": json.dumps(ev.payload)} except RequiresConfirmation as exc: yield { "event": "tool.requires_confirmation", "data": json.dumps({"risk_level": exc.risk_level, "reason": exc.message}), } except PermissionDenied as exc: yield { "event": "tool.failed", "data": json.dumps({"error_code": "permission_denied", "error_message": str(exc)}), } except SandboxError as exc: yield { "event": "tool.failed", "data": json.dumps({"error_code": exc.code, "error_message": str(exc)}), } return EventSourceResponse(event_gen())