Files
hunjing/backend/app/domains/baigong/api.py
T
chenxin 684261f4b4 M5:百工与司命——能力注册表、授契行契请命、禁域裁决与观察标证
- 百工域 domains/baigong:capabilities(统一能力契约:执行类型 builtin/qiankun/mcp/http + 四维风险标注 + 来源版本)、mandates(授契 grant / 行契 charter 预算时间窗,撤销立即生效)、tool_calls(净化摘要 + 观察标证);请命复用 pending_confirmations 加 expires_at/brief_json(超时=拒绝执行 HJ-1302,六要素说明)
- 司命裁决器 siming.py:四维取最高档(能力/数据/后果/情境);禁域为代码常量不可配置(递归删根/格式化/写裸设备/fork 炸弹、file:// 协议、密钥材料外发)——专项测试断言恶意标注与篡改对象仍拒 HJ-1301
- 执行链:禁域→裁决(HJ-1303 未授契拒且双留痕)→请命→执行(乾坤经 SandboxClient HMAC;MCP 经 call_tool;内置无副作用)→净化标证(观察非事实,敏感遮蔽+体量截断)→行动账
- MCP 登记/启停/刷新自动派生与撤下 capabilities(博闻登记接入点);内置能力种子(echo/datetime/qiankun.shell/web.fetch)
- 端点:/capabilities /mandates /petitions /tool-calls + /tools 兼容投影;前端百工页四区(能力目录四维标注/授契行契/请命批准拒绝/行动账观察标证)
- 退役:modules/safety|skills|tool_runtime|agent、application/tool_runtime、services/skills|safety、tool_policies/skills/model_endpoint_overrides 表(迁移 006)、前端 SkillManager/ToolsConsole/旧 tools 页
- 验收(test_baigong_siming 四用例):未授契被拒留痕、请命超时不执行、禁域不可关闭、结果为观察不入忆;后端 454、前端 220、BFF 4、神识 5 包全绿(2 个预存环境失败不变)
2026-08-27 19:17:21 +08:00

180 lines
5.9 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(契约词典 §5.9 pivot=capability_registry)。
- 能力目录与调用:GET/POST /capabilities、POST /capabilities/{id}/invoke;
- 授契与行契:GET/POST /mandates、DELETE /mandates/{id}(撤销立即生效);
- 请命:GET /petitions、POST /petitions/{id}/approve|deny(超时=拒绝执行 HJ-1302);
- 行动账:GET /tool-calls(净化摘要+观察标证)。
"""
from datetime import datetime
from fastapi import APIRouter, Depends, Request, status
from pydantic import BaseModel, Field
from app.api.v1.deps import get_current_user
from app.domains.baigong import service
from app.schemas.auth import CurrentUser
cap_router = APIRouter(prefix="/capabilities", tags=["baigong"])
mandate_router = APIRouter(prefix="/mandates", tags=["baigong"])
petition_router = APIRouter(prefix="/petitions", tags=["baigong"])
ledger_router = APIRouter(prefix="/tool-calls", tags=["baigong"])
class InvokeRequest(BaseModel):
params: dict = Field(default_factory=dict, description="能力参数。")
run_id: str | None = None
character_id: str | None = None
trace_id: str | None = None
confirmed: bool = Field(default=False, description="请命批准后的当次执行回执。")
class CreateCapabilityRequest(BaseModel):
name: str = Field(min_length=1, max_length=120)
description: str = ""
executor_type: str = Field(description="builtin / qiankun / mcp / http")
executor_ref: str = ""
params_schema: dict = Field(default_factory=dict)
risk_dims: dict = Field(default_factory=dict)
data_scope: str = ""
destinations: str = ""
consent_mode: str = "allow"
source: str = "user_custom"
class GrantMandateRequest(BaseModel):
capability_id: str
kind: str = Field(default="grant", description="grant 授契 / charter 行契")
character_id: str | None = None
scope: dict = Field(default_factory=dict)
budget_total: int = Field(default=1, ge=0)
expires_at: datetime | None = None
# ── 能力目录 ──
@cap_router.get("")
async def list_capabilities(user: CurrentUser = Depends(get_current_user)) -> list[dict]:
return await service.list_capabilities(user.owner_scope)
@cap_router.post("", status_code=status.HTTP_201_CREATED)
async def create_capability(
payload: CreateCapabilityRequest, user: CurrentUser = Depends(get_current_user)
) -> dict:
import json
from app.db.session import AsyncSessionLocal
from app.domains.baigong.models import Capability
if payload.executor_type not in ("builtin", "qiankun", "mcp", "http"):
from app.core.errors import AppError
raise AppError("invalid_request", "executor_type 须为 builtin/qiankun/mcp/http")
async with AsyncSessionLocal() as session:
cap = Capability(
name=payload.name,
owner_scope=user.owner_scope,
description=payload.description,
executor_type=payload.executor_type,
executor_ref=payload.executor_ref,
params_schema_json=json.dumps(payload.params_schema, ensure_ascii=False),
risk_dims_json=json.dumps(payload.risk_dims, ensure_ascii=False),
data_scope=payload.data_scope,
destinations=payload.destinations,
consent_mode=payload.consent_mode,
source=payload.source,
)
session.add(cap)
try:
await session.commit()
except Exception as exc: # noqa: BLE001
from app.core.errors import AppError
await session.rollback()
raise AppError("invalid_request", f"能力登记失败(名称重复?):{exc}") from None
return {"id": cap.id, "name": cap.name, "status": "registered"}
@cap_router.post("/{capability_id}/invoke")
async def invoke_capability(
capability_id: str,
payload: InvokeRequest,
request: Request,
user: CurrentUser = Depends(get_current_user),
) -> dict:
return await service.invoke(
request,
capability_id,
payload.params,
user=user,
run_id=payload.run_id,
character_id=payload.character_id,
trace_id=payload.trace_id,
confirmed=payload.confirmed,
)
# ── 授契与行契 ──
@mandate_router.get("")
async def list_mandates(user: CurrentUser = Depends(get_current_user)) -> list[dict]:
return await service.list_mandates(user.owner_scope)
@mandate_router.post("", status_code=status.HTTP_201_CREATED)
async def grant_mandate(
payload: GrantMandateRequest, user: CurrentUser = Depends(get_current_user)
) -> dict:
return await service.grant_mandate(
user.owner_scope,
capability_id=payload.capability_id,
kind=payload.kind,
character_id=payload.character_id,
scope=payload.scope,
budget_total=payload.budget_total,
expires_at=payload.expires_at,
)
@mandate_router.delete("/{mandate_id}", status_code=status.HTTP_204_NO_CONTENT)
async def revoke_mandate(
mandate_id: str, user: CurrentUser = Depends(get_current_user)
) -> None:
await service.revoke_mandate(user.owner_scope, mandate_id)
# ── 请命 ──
@petition_router.get("")
async def list_petitions(user: CurrentUser = Depends(get_current_user)) -> list[dict]:
return await service.list_petitions()
@petition_router.post("/{petition_id}/approve")
async def approve_petition(
petition_id: str, user: CurrentUser = Depends(get_current_user)
) -> dict:
return await service.resolve_petition(petition_id, approved=True)
@petition_router.post("/{petition_id}/deny")
async def deny_petition(petition_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
return await service.resolve_petition(petition_id, approved=False)
# ── 行动账 ──
@ledger_router.get("")
async def list_tool_calls(
user: CurrentUser = Depends(get_current_user), limit: int = 100
) -> list[dict]:
return await service.list_tool_calls(user.owner_scope, limit=limit)
routers = [cap_router, mandate_router, petition_router, ledger_router]