"""安全 API(api-spec §16)。 audit-events 按人物/枢过滤查询审计时间线;confirmations 的 approve/deny 负责 高风险工具确认:通过 ConfirmationRegistry 更新工具确认状态唤醒 Agent 循环。 """ from __future__ import annotations from fastapi import APIRouter, Depends, HTTPException, Query, Response, status from app.api.v1.deps import get_confirmation_registry, get_optional_tool_runtime from app.application.agent.confirmation import ConfirmationRegistry from app.application.tool_runtime.runtime import ToolRuntime from app.schemas.admin import AuditEntry from app.schemas.common import Page from app.schemas.safety import ( CreateToolPolicyRequest, SafetyPolicyItem, ToolPolicyResponse, UpdateToolPolicyRequest, ) from app.services import audit as audit_service from app.services import governance as governance_service router = APIRouter(prefix="/safety", tags=["safety"]) # —— §16 审计事件查询 —— @router.get("/audit-events", response_model=Page[AuditEntry]) async def list_audit_events( character_id: str | None = Query(default=None, description="按人物过滤。"), pivot: str | None = Query(default=None, description="按枢过滤(契约字典 §5.9)。"), page: int = Query(default=1, ge=1), page_size: int = Query(default=50, ge=1, le=200), ) -> Page[AuditEntry]: return await audit_service.query_audit_events( character_id=character_id, pivot=pivot, page=page, page_size=page_size ) # —— §16 工具策略治理 —— async def _refresh_governor(runtime: ToolRuntime | None) -> None: """策略写操作后刷新 ToolRuntime 的司命决策器;tool_runtime 未就绪则跳过(重启后从 db 加载)。""" if runtime is not None: await runtime.reload_governor() @router.get("/policies", response_model=list[SafetyPolicyItem]) async def list_safety_policies() -> list[SafetyPolicyItem]: return await governance_service.list_safety_policies() @router.post("/policies", response_model=ToolPolicyResponse, status_code=status.HTTP_201_CREATED) async def create_safety_policy( payload: CreateToolPolicyRequest, runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> ToolPolicyResponse: response = await governance_service.create_tool_policy(payload) await _refresh_governor(runtime) return response @router.patch("/policies/{tool}", response_model=ToolPolicyResponse) async def update_safety_policy( tool: str, payload: UpdateToolPolicyRequest, runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> ToolPolicyResponse: response = await governance_service.update_tool_policy(tool, payload) await _refresh_governor(runtime) return response @router.delete("/policies/{tool}", status_code=status.HTTP_204_NO_CONTENT) async def delete_safety_policy( tool: str, runtime: ToolRuntime | None = Depends(get_optional_tool_runtime), ) -> Response: await governance_service.delete_tool_policy(tool) await _refresh_governor(runtime) return Response(status_code=status.HTTP_204_NO_CONTENT) # —— §16 高风险工具确认 —— def _not_found(confirmation_id: str) -> HTTPException: return HTTPException( status_code=404, detail={ "error": {"code": "not_found", "message": f"确认不存在或已被消费: {confirmation_id}"} }, ) def _cancelled(confirmation_id: str) -> dict[str, str]: return {"status": "cancelled", "confirmation_id": confirmation_id} @router.post("/confirmations/{confirmation_id}/approve") async def approve( confirmation_id: str, registry: ConfirmationRegistry = Depends(get_confirmation_registry), ) -> dict[str, bool | str]: """批准工具执行。通过 ConfirmationRegistry 唤醒等待的 Agent 循环。""" from sqlalchemy import select from app.db.session import AsyncSessionLocal from app.models.safety import PendingConfirmationRecord async with AsyncSessionLocal() as session: result = await session.execute( select(PendingConfirmationRecord).where(PendingConfirmationRecord.id == confirmation_id) ) record = result.scalar_one_or_none() if record is None or record.run_id is None: raise _not_found(confirmation_id) # 更新 resolution 并唤醒进程内 Event ok = await registry.resolve( record.run_id, approved=True, confirmation_id=confirmation_id, ) if not ok: raise _not_found(confirmation_id) return {"status": "resolved", "approved": True, "run_id": record.run_id} @router.post("/confirmations/{confirmation_id}/deny") async def deny( confirmation_id: str, registry: ConfirmationRegistry = Depends(get_confirmation_registry), ) -> dict[str, str]: """拒绝工具执行。通过 ConfirmationRegistry 通知 Agent 循环。""" from sqlalchemy import select from app.db.session import AsyncSessionLocal from app.models.safety import PendingConfirmationRecord async with AsyncSessionLocal() as session: result = await session.execute( select(PendingConfirmationRecord).where(PendingConfirmationRecord.id == confirmation_id) ) record = result.scalar_one_or_none() if record is None or record.run_id is None: raise _not_found(confirmation_id) ok = await registry.resolve( record.run_id, approved=False, confirmation_id=confirmation_id, ) if not ok: raise _not_found(confirmation_id) return {"status": "cancelled", "confirmation_id": confirmation_id}