Files
hunjing/backend/app/api/v1/endpoints/safety.py
T
2026-08-21 16:00:13 +08:00

163 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""安全 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}