aps-agent/server/agent_core/harness.py

139 lines
8.7 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.

# ============================================================
# 门禁 Harness v1(moduleId: core-harness, 可重生 ✅)
# plan.md §3.3:所有写操作的唯一入口。M1 实现:
# - 权力分级:P0/P1 放行;P2 生成确认卡(人工批准后才执行)
# - 待确认队列:confirmId → 待执行动作(会话内存)
# - 执行/驳回均写审计(§3.6)
# ============================================================
from __future__ import annotations # 前向类型引用
import uuid # 确认卡 ID
from typing import Any, Callable # 类型标注
from server.contracts import UIAction, UIBlock # 确认卡的 UI 块契约
# 待确认动作表:confirmId → {action, params, summary, sessionId}
# M1 用模块级内存(单进程);M2 起随会话树持久化
_pending: dict[str, dict[str, Any]] = {}
# 动作 → 权力等级 映射(封闭表:未登记的动作一律拒绝)
# ⚠ 文档同步铁律(plan.md §12.9):改本表必同步 docs/architecture/harness.md 权力矩阵
_POWER_MAP: dict[str, str] = {
"schedule.run": "P1", # 试排:写草稿版本(沙盒语义)
"schedule.publish": "P2", # 发布:写主干世界状态 → 必须人工确认
"order.upsert": "P2", # 订单新增/编辑:写主干订单池 → 必须人工确认
"order.cancel": "P2", # 订单取消:影响后续排产输入 → 必须人工确认
"order.complete": "P2", # 订单完成:影响后续排产输入 → 必须人工确认
"master.line.upsert": "P2", # 产线编辑/停启用:改后续新排产资源池 → 必须人工确认
"master.material.upsert": "P2", # 物料库存编辑:改齐套检查输入 → 必须人工确认
"master.maintenance.upsert": "P2", # 维保新增/取消:改设备避让窗口 → 必须人工确认
"master.bom.upsert": "P2", # BOM 明细编辑:改齐套需求与采购建议 → 必须人工确认
"master.routing.upsert": "P2", # 工艺步骤编辑:改占槽工时与外协标记 → 必须人工确认
"order.decompose": "P1", # 订单分解:产出采购/委外建议草稿,不碰主数据
"scenario.compare": "P1", # 方案对比:只写 Explore 沙盒(§5.1)
"checkpoint.create": "P1", # 建档:只新增快照,不改世界
"checkpoint.rollback": "P2", # 回滚:整体替换主干世界 → 必须人工确认
"data.reset": "P2", # 重置数据:破坏性 → 必须人工确认
"knowledge.query": "P0", # 知识检索:只读(M3 §8.2)
"report.generate": "P1", # 报告生成:产出文档并入知识库(M3 §9.10 规则3)
"viewport.*": "P0", # 视口命令:纯视图状态
"query.*": "P0", # 查询:只读
}
# 各动作的中文说明(门禁管理台"权力矩阵"页签展示用;与 _POWER_MAP 键集合一致)
_POLICY_DESC: dict[str, str] = {
"schedule.run": "试排:只产草稿版本,不动已发布主干",
"schedule.publish": "发布:写主干并推进订单状态,影响下游执行",
"order.upsert": "新增/编辑销售订单:改后续排产输入",
"order.cancel": "取消销售订单:从后续排产输入中排除",
"order.complete": "完成销售订单:从后续排产输入中排除",
"master.line.upsert": "编辑/停启用产线:改后续新排产的资源池",
"master.material.upsert": "编辑物料库存:改后续新排产的齐套检查",
"master.maintenance.upsert": "新增/取消维保:改设备避让窗口",
"master.bom.upsert": "编辑 BOM 明细:改齐套需求与采购建议输入",
"master.routing.upsert": "编辑工艺步骤:改占槽工时与外协标记",
"order.decompose": "订单分解:按 BOM/工艺产出采购与委外建议(草稿)",
"scenario.compare": "方案对比:多策略试排全程在深拷贝沙盒",
"checkpoint.create": "建档:只新增成对快照文件",
"checkpoint.rollback": "回滚:整体替换主干世界(时间旅行)",
"data.reset": "重置:清空重播种子数据",
"knowledge.query": "知识检索:只读命中知识资产,回答强制带出处",
"report.generate": "报告生成:冻结快照模板化产出,报告入知识库",
"viewport.*": "视口命令:纯前端视图状态(模式/过滤/聚焦/高亮)",
"query.*": "查询:只读(KPI/世界视图)",
}
def list_policy() -> list[dict[str, str]]:
"""权力矩阵投影(门禁管理台数据源;P0 只读,§6.10.2)。
Returns: [{action, power, desc, confirm}] 列表,顺序与 _POWER_MAP 声明一致。
"""
return [{
"action": action, # 动作名(支持前缀通配)
"power": power, # 权力等级 P0-P3
"desc": _POLICY_DESC.get(action, ""), # 中文说明
"confirm": power in ("P2", "P3"), # 是否需要人工确认
} for action, power in _POWER_MAP.items()]
def power_of(action: str) -> str:
"""查动作的权力等级(前缀通配;未登记按最高 P3 拒绝——白名单原则)。"""
if action in _POWER_MAP: # 精确命中
return _POWER_MAP[action]
prefix = action.split(".")[0] + ".*" # 前缀通配(viewport.* / query.*)
return _POWER_MAP.get(prefix, "P3") # 未登记 → P3(默认最严)
def needs_confirm(action: str) -> bool:
"""该动作是否需要人工确认(P2/P3 强制过卡,§3.2 权力边界)。"""
return power_of(action) in ("P2", "P3") # 写主干/外部副作用必须确认
def stage_confirmation(session_id: str, action: str, params: dict[str, Any],
title: str, summary_lines: list[str]) -> UIBlock:
"""把 P2 动作压入待确认队列并生成确认卡 UI 块(动作此刻并未执行)。
权力等级:P1(只登记意图与出卡,不触发任何写)。
"""
confirm_id = uuid.uuid4().hex[:12] # 生成确认 ID(防猜测)
_pending[confirm_id] = { # 登记待确认动作
"sessionId": session_id, "action": action, "params": params,
}
return UIBlock( # 确认卡:前端渲染为审批卡片
blockId=f"confirm-{confirm_id}", # 块 ID 复用确认 ID
type="confirm-card", # 块类型
props={ # 卡片内容
"confirmId": confirm_id, # 前端回传用
"title": title, # 卡片标题
"summary": summary_lines, # 影响面说明(逐行)
"power": power_of(action), # 展示权力等级
"action": action, # 待执行动作名
},
actions=[ # 批准/驳回两个动作
UIAction(actionId="confirm", label="批准执行", power="P2", payload={"confirmId": confirm_id}),
UIAction(actionId="reject", label="驳回", power="P0", payload={"confirmId": confirm_id}),
],
)
def take_confirmation(confirm_id: str) -> dict[str, Any] | None:
"""取出并移除一条待确认动作(一次性令牌语义:批准/驳回后即失效)。"""
return _pending.pop(confirm_id, None) # 不存在返回 None(过期/重放防护)
def list_pending() -> list[dict[str, Any]]:
"""列出全部待确认动作(门禁管理台"待审批"页签的数据源,§6.10.2)。"""
return [{"confirmId": cid, **info} for cid, info in _pending.items()] # 附带令牌一并返回(只读投影)
def guard(action: str, params: dict[str, Any], executor: Callable[[], Any]):
"""P0/P1 动作的直通执行护栏:校验权力等级后执行(P2/P3 禁止走此通道)。
权力等级:随 action 而定;本函数强制 P2/P3 抛错(必须走 stage_confirmation)。
"""
if needs_confirm(action): # P2/P3 误入直通通道
raise PermissionError(f"动作 {action} 为 {power_of(action)},必须经确认卡执行") # 硬拒绝
return executor() # P0/P1 直接执行