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:
1 parent
144415247e
commit
684261f4b4
97 files changed
+3075
-5554
No files matched your search
@@ -0,0 +1,497 @@
|
||||
"""百工域应用服务:能力目录 / 授契与行契 / 请命 / 调用全链 / 行动账。
|
||||
|
||||
调用全链(mandate-and-capability-model.md §1 流程图):
|
||||
禁域(代码常量,最先且不可绕过)→ 司命裁决(授契/行契/四维)→ 请命(超时不执行)
|
||||
→ 隔离执行(乾坤 HMAC / MCP / 内置)→ 净化标证 → 行动账与调用记录落库。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.core.ids import new_confirmation_id
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.domains.baigong import siming
|
||||
from app.domains.baigong.executor import ExecOutcome, Executor
|
||||
from app.domains.baigong.models import Capability, Mandate, ToolCall
|
||||
from app.domains.baigong.sanitize import digest, observation, redact
|
||||
from app.models.safety import AuditEvent, PendingConfirmationRecord
|
||||
from app.schemas.auth import CurrentUser
|
||||
|
||||
|
||||
async def _get_capability(capability_id: str, owner_scope: str) -> Capability:
|
||||
async with AsyncSessionLocal() as session:
|
||||
cap = await session.get(Capability, capability_id)
|
||||
if cap is None or cap.owner_scope != owner_scope:
|
||||
raise AppError("not_found", "能力不存在。", status_code=404)
|
||||
session.expunge(cap)
|
||||
return cap
|
||||
|
||||
|
||||
async def _audit(
|
||||
*,
|
||||
action: str,
|
||||
resource_id: str,
|
||||
decision: str,
|
||||
risk_level: str,
|
||||
reason: str,
|
||||
input_summary: str,
|
||||
output_summary: str,
|
||||
run_id: str | None,
|
||||
character_id: str | None,
|
||||
trace_id: str | None,
|
||||
) -> None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
session.add(
|
||||
AuditEvent(
|
||||
pivot="capability_registry",
|
||||
action=action,
|
||||
actor="baigong",
|
||||
risk_level=risk_level,
|
||||
decision=decision,
|
||||
reason=reason,
|
||||
input_summary=input_summary,
|
||||
output_summary=output_summary,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
resource_type="capability",
|
||||
resource_id=resource_id,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def _record_call(
|
||||
*,
|
||||
capability: Capability,
|
||||
decision: str,
|
||||
mandate_id: str | None,
|
||||
status: str,
|
||||
error_code: str | None,
|
||||
params: dict[str, Any],
|
||||
outcome: ExecOutcome | None,
|
||||
run_id: str | None,
|
||||
character_id: str | None,
|
||||
trace_id: str | None,
|
||||
duration_ms: int,
|
||||
) -> ToolCall:
|
||||
obs = observation(
|
||||
capability_name=capability.name,
|
||||
executor_type=capability.executor_type,
|
||||
output_digest=outcome.output if outcome else "",
|
||||
trace_id=trace_id,
|
||||
decision=decision,
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
async with AsyncSessionLocal() as session:
|
||||
call = ToolCall(
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
capability_id=capability.id,
|
||||
trace_id=trace_id,
|
||||
decision=decision,
|
||||
mandate_id=mandate_id,
|
||||
status=status,
|
||||
error_code=error_code,
|
||||
input_digest=digest(params, limit=500),
|
||||
output_digest=digest(outcome.output if outcome else ""),
|
||||
evidence_json=obs,
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
session.add(call)
|
||||
await session.commit()
|
||||
session.expunge(call)
|
||||
return call
|
||||
|
||||
|
||||
async def invoke(
|
||||
request: Request,
|
||||
capability_id: str,
|
||||
params: dict[str, Any],
|
||||
*,
|
||||
user: CurrentUser,
|
||||
run_id: str | None = None,
|
||||
character_id: str | None = None,
|
||||
trace_id: str | None = None,
|
||||
confirmed: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""能力调用全链。返回 allow 执行结果 / requires_petition 请命 / 拒绝抛错。"""
|
||||
capability = await _get_capability(capability_id, user.owner_scope)
|
||||
|
||||
# ① 禁域最先:任何配置、标注与确认都不能越过
|
||||
siming.check_forbidden(capability, params)
|
||||
|
||||
# ② 司命裁决:授契/行契/四维
|
||||
ruling = await siming.rule(
|
||||
capability,
|
||||
params,
|
||||
owner_scope=user.owner_scope,
|
||||
character_id=character_id,
|
||||
confirmed=confirmed,
|
||||
)
|
||||
|
||||
if ruling.decision == "denied":
|
||||
await _audit(
|
||||
action="capability.denied",
|
||||
resource_id=capability.name,
|
||||
decision="denied",
|
||||
risk_level=ruling.risk_level,
|
||||
reason=ruling.reason,
|
||||
input_summary=digest(params, limit=500),
|
||||
output_summary="",
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
await _record_call(
|
||||
capability=capability,
|
||||
decision="denied_unmandated" if "未授契" in ruling.reason else "denied",
|
||||
mandate_id=None,
|
||||
status="rejected",
|
||||
error_code="HJ-1303",
|
||||
params=params,
|
||||
outcome=None,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
duration_ms=0,
|
||||
)
|
||||
raise AppError("HJ-1303", ruling.reason, status_code=403)
|
||||
|
||||
if ruling.decision == "ask":
|
||||
petition_id = new_confirmation_id()
|
||||
deadline = siming.petition_deadline()
|
||||
brief = {
|
||||
# 请命说明六要素:谁在做/做什么/用什么/去哪里/后果/替代
|
||||
"who": f"character:{character_id or 'user'}",
|
||||
"what": capability.description or capability.name,
|
||||
"capability": capability.name,
|
||||
"executor": capability.executor_type,
|
||||
"destinations": capability.destinations,
|
||||
"consequences": f"风险等级 {ruling.risk_level}",
|
||||
"alternatives": "拒绝后可调整参数或改用低风险能力",
|
||||
"params": digest(params, limit=500),
|
||||
}
|
||||
async with AsyncSessionLocal() as session:
|
||||
session.add(
|
||||
PendingConfirmationRecord(
|
||||
id=petition_id,
|
||||
tool=capability.name,
|
||||
input_json=json.dumps(params, ensure_ascii=False, default=str),
|
||||
risk_level=ruling.risk_level,
|
||||
reason="请命:高风险动作需当次确认",
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
expires_at=deadline,
|
||||
brief_json=json.dumps(brief, ensure_ascii=False),
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
await _record_call(
|
||||
capability=capability,
|
||||
decision="ask",
|
||||
mandate_id=ruling.mandate.id if ruling.mandate else None,
|
||||
status="petition_pending",
|
||||
error_code=None,
|
||||
params=params,
|
||||
outcome=None,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
duration_ms=0,
|
||||
)
|
||||
return {
|
||||
"status": "requires_petition",
|
||||
"petition_id": petition_id,
|
||||
"risk_level": ruling.risk_level,
|
||||
"expires_at": deadline.isoformat(),
|
||||
"brief": brief,
|
||||
}
|
||||
|
||||
# ③ 执行 → 净化标证 → 落库
|
||||
executor: Executor = request.app.state.baigong_executor
|
||||
started = utc_now()
|
||||
try:
|
||||
outcome = await executor.execute(capability, params)
|
||||
except AppError:
|
||||
raise
|
||||
duration_ms = (utc_now() - started).microseconds // 1000 + (
|
||||
outcome.duration_ms if "outcome" in locals() else 0
|
||||
)
|
||||
|
||||
decision = "ask_approved" if confirmed else "allow"
|
||||
call = await _record_call(
|
||||
capability=capability,
|
||||
decision=decision,
|
||||
mandate_id=ruling.mandate.id if ruling.mandate else None,
|
||||
status="ok" if outcome.ok else "failed",
|
||||
error_code=outcome.error_code,
|
||||
params=params,
|
||||
outcome=outcome,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
# 行契预算核销
|
||||
if ruling.mandate is not None and ruling.mandate.budget_total > 0:
|
||||
async with AsyncSessionLocal() as session:
|
||||
mandate = await session.get(Mandate, ruling.mandate.id)
|
||||
if mandate is not None:
|
||||
mandate.budget_used += 1
|
||||
await session.commit()
|
||||
|
||||
await _audit(
|
||||
action="capability.executed",
|
||||
resource_id=capability.name,
|
||||
decision="allow",
|
||||
risk_level=ruling.risk_level,
|
||||
reason=f"mandate={ruling.mandate.id if ruling.mandate else 'none'}",
|
||||
input_summary=call.input_digest,
|
||||
output_summary=call.output_digest,
|
||||
run_id=run_id,
|
||||
character_id=character_id,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
return {
|
||||
"status": "ok" if outcome.ok else "failed",
|
||||
"call_id": call.id,
|
||||
"observation": call.evidence_json,
|
||||
}
|
||||
|
||||
|
||||
async def resolve_petition(petition_id: str, approved: bool) -> dict[str, Any]:
|
||||
"""请命裁决:approve 允许当次执行(客户端以 confirmed=True 重调);超时拒绝。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
record = await session.get(PendingConfirmationRecord, petition_id)
|
||||
if record is None or record.status != "pending":
|
||||
raise AppError("not_found", "请命不存在或已处理。", status_code=404)
|
||||
expires = record.expires_at
|
||||
if expires is not None:
|
||||
if expires.tzinfo is None: # SQLite 回读 naive(开发档)
|
||||
expires = expires.replace(tzinfo=UTC)
|
||||
if expires is not None and expires <= datetime.now(UTC):
|
||||
record.status = "timed_out"
|
||||
record.resolved_at = utc_now()
|
||||
await session.commit()
|
||||
raise AppError("HJ-1302", "请命超时未确认,未执行。", status_code=410)
|
||||
record.status = "approved" if approved else "denied"
|
||||
record.resolved_at = utc_now()
|
||||
await session.commit()
|
||||
return {
|
||||
"status": "resolved" if approved else "cancelled",
|
||||
"approved": approved,
|
||||
"capability": redact(record.tool),
|
||||
"run_id": record.run_id,
|
||||
}
|
||||
|
||||
|
||||
# ── 查询面 ──
|
||||
|
||||
|
||||
async def list_capabilities(owner_scope: str) -> list[dict[str, Any]]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
caps = (
|
||||
(
|
||||
await session.execute(
|
||||
select(Capability).where(Capability.owner_scope == owner_scope)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
mandates = (
|
||||
(
|
||||
await session.execute(
|
||||
select(Mandate).where(
|
||||
Mandate.owner_scope == owner_scope, Mandate.status == "active"
|
||||
)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
mandated_ids = {m.capability_id for m in mandates}
|
||||
return [
|
||||
{
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"description": c.description,
|
||||
"executor_type": c.executor_type,
|
||||
"executor_ref": c.executor_ref,
|
||||
"risk_dims": json.loads(c.risk_dims_json or "{}"),
|
||||
"data_scope": c.data_scope,
|
||||
"destinations": c.destinations,
|
||||
"source": c.source,
|
||||
"version": c.version,
|
||||
"consent_mode": c.consent_mode,
|
||||
"enabled": c.enabled,
|
||||
"mandated": c.id in mandated_ids,
|
||||
}
|
||||
for c in caps
|
||||
]
|
||||
|
||||
|
||||
async def grant_mandate(
|
||||
owner_scope: str,
|
||||
*,
|
||||
capability_id: str,
|
||||
kind: str,
|
||||
character_id: str | None,
|
||||
scope: dict,
|
||||
budget_total: int,
|
||||
expires_at: datetime | None,
|
||||
) -> dict[str, Any]:
|
||||
await _get_capability(capability_id, owner_scope)
|
||||
if kind not in ("grant", "charter"):
|
||||
raise AppError("invalid_request", "kind 须为 grant / charter")
|
||||
async with AsyncSessionLocal() as session:
|
||||
mandate = Mandate(
|
||||
owner_scope=owner_scope,
|
||||
capability_id=capability_id,
|
||||
kind=kind,
|
||||
scope_json=scope,
|
||||
character_id=character_id,
|
||||
budget_total=budget_total,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
session.add(mandate)
|
||||
await session.commit()
|
||||
session.expunge(mandate)
|
||||
return {
|
||||
"id": mandate.id,
|
||||
"capability_id": capability_id,
|
||||
"kind": kind,
|
||||
"budget_total": budget_total,
|
||||
"budget_used": 0,
|
||||
"status": mandate.status,
|
||||
}
|
||||
|
||||
|
||||
async def revoke_mandate(owner_scope: str, mandate_id: str) -> None:
|
||||
"""撤销授契:立即生效(后续裁决不再命中)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
mandate = await session.get(Mandate, mandate_id)
|
||||
if mandate is None or mandate.owner_scope != owner_scope:
|
||||
raise AppError("not_found", "授契不存在。", status_code=404)
|
||||
mandate.status = "revoked"
|
||||
mandate.revoked_at = utc_now()
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def list_mandates(owner_scope: str) -> list[dict[str, Any]]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
rows = (
|
||||
(
|
||||
await session.execute(
|
||||
select(Mandate)
|
||||
.where(Mandate.owner_scope == owner_scope)
|
||||
.order_by(Mandate.granted_at.desc())
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": m.id,
|
||||
"capability_id": m.capability_id,
|
||||
"character_id": m.character_id,
|
||||
"kind": m.kind,
|
||||
"scope": m.scope_json,
|
||||
"budget_total": m.budget_total,
|
||||
"budget_used": m.budget_used,
|
||||
"status": m.status,
|
||||
"granted_at": m.granted_at.isoformat() if m.granted_at else None,
|
||||
"expires_at": m.expires_at.isoformat() if m.expires_at else None,
|
||||
}
|
||||
for m in rows
|
||||
]
|
||||
|
||||
|
||||
async def list_petitions(owner_scope: str | None = None) -> list[dict[str, Any]]:
|
||||
"""待决请命清单(行动账待办)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
rows = (
|
||||
(
|
||||
await session.execute(
|
||||
select(PendingConfirmationRecord)
|
||||
.where(PendingConfirmationRecord.status == "pending")
|
||||
.order_by(PendingConfirmationRecord.created_at.desc())
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
out = []
|
||||
for r in rows:
|
||||
try:
|
||||
brief = json.loads(r.brief_json or "{}")
|
||||
except ValueError:
|
||||
brief = {}
|
||||
out.append(
|
||||
{
|
||||
"id": r.id,
|
||||
"capability": r.tool,
|
||||
"risk_level": r.risk_level,
|
||||
"reason": r.reason,
|
||||
"brief": brief,
|
||||
"run_id": r.run_id,
|
||||
"character_id": r.character_id,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
"expires_at": r.expires_at.isoformat() if r.expires_at else None,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
async def list_tool_calls(owner_scope: str, limit: int = 100) -> list[dict[str, Any]]:
|
||||
"""行动账:调用记录(净化摘要 + 观察标证)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cap_ids = set(
|
||||
(
|
||||
await session.execute(
|
||||
select(Capability.id).where(Capability.owner_scope == owner_scope)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
rows = (
|
||||
(
|
||||
await session.execute(
|
||||
select(ToolCall).order_by(ToolCall.created_at.desc()).limit(limit)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": t.id,
|
||||
"capability_id": t.capability_id,
|
||||
"run_id": t.run_id,
|
||||
"character_id": t.character_id,
|
||||
"trace_id": t.trace_id,
|
||||
"decision": t.decision,
|
||||
"mandate_id": t.mandate_id,
|
||||
"status": t.status,
|
||||
"error_code": t.error_code,
|
||||
"input_digest": t.input_digest,
|
||||
"output_digest": t.output_digest,
|
||||
"evidence": t.evidence_json,
|
||||
"duration_ms": t.duration_ms,
|
||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||
}
|
||||
for t in rows
|
||||
if t.capability_id in cap_ids
|
||||
]
|
||||
Reference in new issue
Block a user