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 个预存环境失败不变)
This commit is contained in:
chenxin committed 2026-08-27 19:17:21 +08:00
1 parent 144415247e
commit 684261f4b4
97 files changed
+3075 -5554

No files matched your search

+179
View File
@@ -0,0 +1,179 @@
"""百工与司命 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]