- 百工域 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 个预存环境失败不变)
180 lines
5.9 KiB
Python
180 lines
5.9 KiB
Python
"""百工与司命 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]
|