- 百工域 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 个预存环境失败不变)
498 lines
16 KiB
Python
498 lines
16 KiB
Python
"""百工域应用服务:能力目录 / 授契与行契 / 请命 / 调用全链 / 行动账。
|
|
|
|
调用全链(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
|
|
]
|