aps-agent/poc/pi-fallback/P3-DESIGN.md

777 lines
57 KiB
Markdown
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.

# P3-DESIGN — Pi Agent 兜底能力 · 高风险场景 S4/S6/S7 + 白名单治理(文件级详细设计)
> 设计员:Agent-L · 日期:2026-09-03 · 协调板:`GOAL-P3.md`
> 实施员(Agent-M)照本文件施工;验证员(Agent-N)按 §8 测试矩阵与 §9 冒烟规格执行。
> 前置已读并源码核实:GOAL-P3.md、docs/architecture/fallback.md、
> P2-DESIGN.md / P2-IMPL-NOTES.md / P2-VALIDATION.md、
> fallback_lane.py(1735 行:propose_reply 745 / _PLAN_SCENARIOS 1013 / validate_plan 1075 /
> check_step_request 1218 / execute_plan 1514 / _stage_plan_confirmation 1686)、
> fallback_verify.py(190 行)、pi_bridge.py(416 行:TOOL_REGISTRY 38 / PiBridge 98 /
> ActionMailbox 307 / render_plan_task_brief 406)、
> harness.py(878 行:world_fingerprint 102 / _POWER_MAP 143 / _GLOBAL_APPROVAL_ROLES 372 /
> APS_APPROVAL_ROLE_POLICIES 370 / _can_initiate_current 112 / stage_confirmation 632
> (requiredApprovals=2 if P3,683 行)/ consume_execution_grant 810)、
> approval_store.py decide 395-464(P3 双人 SOD + executionGrant)、
> server/auth/(context.py IdentityContext.roles 自由字符串元组;middleware.py 中间件绑定;
> providers.py 207-218 JMS roles 缺省 planner)、feature_flags.py(fail-open + _DEFAULT_OFF_KEYS
> 语义)、workflow.py(execute_confirmed fallback 分支 577-621 / assistant.reply 兜底接线 2920-2931 /
> _flex_simulate_due 1598 / scenario.compare 分支 2517 / mes.dispatch 分支 1003-1023)、
> flex.py(simulate_due 684 / compare_sort_modes 789,均深拷贝沙盒不改主干)、
> mes.py(_get_active_client 20 / mes_connection_status 28 / apply_dispatch 1203 消费
> execution_grant 1250 / apply_report 1408)、mes_http.py(MesHttpError 五码 /
> readiness(probe) 234 / fetch_work_order 341 只读)、wms_events.py、
> automation.py(Gear/Gate 机制,与 S6 无关——见 §0.13)、audit_alerts.py(仅审计链告警)、
> tests/golden/test_approval_role_policies.py / test_harness_p3.py / test_fallback_execute.py、
> PROJECT_OPERATING_RULES.md(§红线 9-16)。
---
## 0. 现状关键事实(设计依据,均已源码核实)
1. **P3 双人审批与执行授权是既有机制**(harness.py:683 + approval_store.py:417-464):
`power == "P3"` 的 action 出卡自动 `requiredApprovals=2`,第二次批准必须不同用户
(SOD_DENIED),终审后签发一次性 `executionGrant`(绑定 confirmId/action/paramsHash,
`consume_execution_grant` 消费,harness.py:810)。mes.dispatch 是唯一在用的 P3 先例
(workflow.py:1003-1023 → mes.apply_dispatch 1250 行消费 grant)。**P3 白名单放行的
highrisk 意图直接继承这套双人+授权语义,一行新机制不造。**
2. **角色体系是自由字符串 + env 配置三层解析**(harness.py:372-375/453-475):
roles 来自 auth provider(JMS 用户 roles codes,缺省 `JMS_AUTH_DEFAULT_ROLE=planner`),
审批能力分 initiate/approve/history 三 capability,解析顺序 =
action 级规则(`APS_APPROVAL_ROLE_POLICIES` JSON env)→ power 级规则 → 全局 env
(`APS_APPROVAL_INITIATOR_ROLES` / `APS_APPROVAL_ROLES` 等,默认
`planner,approver,admin`)。**"运维"不需要新权限模型:它就是一个角色字符串(建议
`ops`),action 级策略把 highrisk 意图的 initiate/approve 圈到 `ops,admin` 即可,
纯配置零代码**(§4.2)。stage 期门禁既有:stage_confirmation:653
`_can_initiate_current` 不满足即 PermissionError。
3. **匿名/测试身份**:`get_identity()` 无 HTTP 上下文返回 roles=("system",),
`_current_identity_has_approval_role` 对 system 放行(harness.py:486)——黄金测试沿用
test_approval_role_policies.py 的 `bind_identity(IdentityContext(roles=(...)))` 模式。
4. **feature_flags 语义对照**(feature_flags.py:8-12):features.json 是 fail-open
「可用性/可见性」开关(损坏回退默认全开、fallback 键例外默认关),**显式声明"不是权限
边界"**。P3 白名单语义必须相反:**fail-closed,任何配置问题 → 全拒**,且它是权限边界
的一部分。两者文件分离、加载器分离、文档各写各的语义(§2.1),绝不复用同一加载函数
(避免语义混淆——GOAL 硬约束)。
5. **fallback 核心符号的外部调用方只有两处**(rg 实测):workflow.py:583
(execute_plan)与 workflow.py:2927(propose_reply)。`FALLBACK_EXECUTABLE_INTENTS` /
`validate_plan` / `check_step_request` / `plan_fingerprint` 无模块外调用——P3 对计划锁
的扩展爆炸半径天然收敛(§9)。
6. **沙盒意图已有成熟先例**:flex.simulate_due(flex.py:684,深拷贝世界试排,P0)、
flex.compare(789,三模式沙盒对比,P0)、scenario.compare(scenario.py,沙盒三策略
并行试排,P1,workflow.py:2517 分支写 ALGO_RUN 审计后 `store.save()`——**注意:save
只落审计,业务世界未变**)、scenario.sensitivity(P1 同形态)。S4 = Pi 编排这些既有
沙盒意图 + 围墙内临时脚本,产物草稿(§5)。
7. **world_fingerprint 排除 append-only 键**(harness.py:102-118,_VOLATILE_WORLD_KEYS
含 auditEvents/logs/mesSyncJournal)——**沙盒执行「零主干写」的验证器断言可以直接用
前后指纹相等**(审计自然增长不误报),S4 断言与 S6 漂移检测都建立在这上面。
8. **MES 故障点的全部显式信号**(mes_http.py):MesHttpError 五码
(TIMEOUT/CONNECT_FAILED/UPSTREAM_ERROR/AUTH_FAILED/NOT_CONFIGURED)、
`status()` 返回 connected=False 不抛错、`readiness(probe=True)` 轻量探测 connectivity
ok/failed。客户端切换点唯一:mes.py:20 `_get_active_client()`(MES_HTTP_BASE_URL 有则
HTTP 否则 stub)——**S6 冒烟的「断连」= 指向不可达地址的 MES_HTTP_BASE_URL +
reset_http_mes_client()**,「恢复」= 重置回 stub,零产品代码改动(§9.1)。
9. **MES 外部状态只读回流口已有**:`HttpMesClient.fetch_work_order(external_wo_id)`
(mes_http.py:341,GET 轮询,只读)+ MockMesClient 同接口——S6 恢复后对账的读侧
(P0)不需要新 HTTP 面(§6.4)。
10. **补录写入口已有**:`mes.apply_report(store, wo_id, progress_pct, finish, actor)`
(mes.py:1408,P1 `mes.report`,workflow.py:2485-2515 既有批量报工先例——逐笔循环
+ 逐笔 try/except + 汇总文案)。S6 补录 = 编排既有 mes.report,不造新写通道。
11. **计划锁现状**(P2 基线):`_PLAN_SCENARIOS=("S1","S2","S3","S9")`(fallback_lane.py:
1013);validate_plan:1101 **硬性拒绝 power ∉ {P1,P2} 的意图**(P3 意图整计划拒绝);
check_step_request:1231 同款(白名单 + power ∈ {P1,P2})。**这两处是 P3 白名单放行的
唯一物理闸口**——改成「power 分级 + 白名单裁决」双闸(§3.3),放行面完全由白名单
文件决定,代码里没有第二个 P3 放行路径。
12. **P3 确认卡零前端改动**(P2-DESIGN §0.6 结论延续):ConfirmCard 只读
confirmId/title/summary/power 四 props;双人审批的 PARTIALLY_APPROVED /
needsSecondConfirm 流程在 app.py confirm 端点与审批仓内闭环,前端确认卡组件不变。
S6 逐笔多卡 = 多个 `confirm-card` 块在同一 AgentReply.blocks 里依次下发,前端逐张
渲染(既有能力,S6 冒烟需实测确认呈现顺序)。
13. **automation.py / audit_alerts.py 与 S6 触发无关**(精读结论):automation.py 是
Gear/受控自动化升级机制(规则触发业务动作),audit_alerts.py 只聚合审计链完整性告警;
**现状没有任何「集成故障告警」子系统**——S6 的故障信号来源只能是 ① 用户在对话里
陈述(触发兜底)+ ② 编排器在 propose 时主动探测 mes status/readiness 注入 inbox
(§6.2)。不新造告警子系统(GOAL 边界外)。
14. **K 轮真实冒烟实测教训(P2-VALIDATION §5,直接吸收进本设计)**:
① 出卡审计落链改变世界指纹——stage 后必须 `refresh_confirmation_world_fingerprint`
对齐「卡片就绪时刻」(P2 已修,P3 新出卡路径照做);
② `contextPolicies` 簿记键会被 chat 管线事后物化——出卡前 `setdefault`(P2 已修);
③ artifactRef 路线对真实 Pi 物理不可用(无 sha256 工具)——**P3 计划一律 params 内联
frozen,不依赖 artifact 指纹路线**(S6 补录清单是编排器生成的结构化文件,不是 Pi 算的
sha256);
④ ASSISTED 真实模型端到端未验——**P3 三场景全部只开放 frozen 步**(§3.4),ASSISTED
仍限 P1/P2 意图。
15. **P2 确认卡的 beforeFingerprint 漂移比对在 execute_plan 步骤 2**(fail closed),
S6 补录卡沿用;S4 沙盒卡不需要漂移保护(不写主干)但仍冻结指纹做「执行期间主干未变」
的副作用断言(§5.3)。
---
## 1. 变更清单表
| # | 文件 | 新建/修改 | 改什么(函数级) | 为什么 | 风险 |
|---|------|----------|------------------|--------|------|
| 1 | `server/agent_core/fallback_highrisk.py` | **新建**(~330 行) | 全部,见 §2/§4:白名单加载器(fail-closed)`load_highrisk_whitelist` / 场景裁决 `scenario_grant` / 意图裁决 `intent_allowed` / 角色判定 `current_identity_roles` · `is_ops_identity` · `roles_for_scenario` / 白名单文档校验 `validate_whitelist_doc`(改白名单出卡前复用) | GOAL 交付 1/5 的治理主体 | 新模块,零存量影响 |
| 2 | `server/agent_core/fallback_lane.py` | 修改(+~420 行) | 见 §3:`_PLAN_SCENARIOS` 扩为 7 场景;`validate_plan` power 闸改「白名单裁决」;`check_step_request` 同步;`propose_reply` 加角色闸 + 场景提示注入简报;`_stage_plan_confirmation` 分流(P2 单卡 / S6 逐笔多卡 / S4 沙盒卡);`execute_plan` 加 S4 沙盒分支与 S6/S7 步执行器登记;`FallbackConfig` +3 字段。**不改 P1/P2 已有函数签名与默认路径语义** | GOAL 交付 1/2/3/4/5 编排 | MED(本模块是 P1/P2 自建,外部调用方仅 workflow.py 两处,§9.4) |
| 3 | `server/integrations/pi_bridge.py` | 修改(+~160 行) | `PiBridge.export_integration_status()`(MES 状态/readiness 注入 inbox);`PiBridge.export_ops_diagnostics()`(日志尾部 + 配置脱敏快照注入 inbox);简报模板 +S4/S6/S7 段落(可用沙盒意图/补录协议/运维边界声明);TOOL_REGISTRY **不加新工具**(读面仍只有 fs_read 消费 inbox——围墙不扩) | GOAL 交付 3/4 的 Pi 读面 | LOW(纯追加;P1/P2 五键注册表查询路径不变) |
| 4 | `server/agent_core/harness.py` | 修改(+4 行) | `_POWER_MAP` += `"agent.fallback.policy.update": "P2"`、`"agent.fallback.ops.config.apply": "P3"`;`_POLICY_DESC` 同步两条。**不动任何函数** | GOAL 交付 1(改白名单登记为 P2 管理动作)/4(S7 改配置 P3) | LOW(P1/P2 同款纯追加,§9.1) |
| 5 | `server/aps_domain/workflow.py` | 修改(+~90 行) | `execute_confirmed` 追加两分支:`agent.fallback.policy.update`(原子写白名单文件 + 重载校验 + 备份,§2.4)与 `agent.fallback.ops.config.apply`(原子写 features.json 类配置 + 回读校验,§7.3)。**propose 接线点(2920 分支)不动** | GOAL 交付 1/4 执行通道 | MED(热路径纯增量分支,§9.2) |
| 6 | `tests/golden/test_fallback_highrisk.py` | **新建**(~520 行) | §8 测试矩阵 21 例 | GOAL 交付 6 | 新文件 |
| 7 | `tests/golden/test_fallback_execute.py` | 修改(±20 行) | T-3 语义更新:P3 意图未入白名单仍整计划拒绝(断言文案对齐新闸);补一例「白名单放行后 P3 意图过校验」的反向断言(或迁入新文件——M 轮自定,以新文件为先) | 白名单闸替换硬拒闸的同轮修复 | 低 |
| 8 | `docs/architecture/harness.md` | 修改 | 权力矩阵 += 2 行(文档同步铁律) | GOAL 交付 8 | 低 |
| 9 | `docs/architecture/fallback.md` | 修改 | 增 P3 章节:白名单 fail-closed/角色矩阵/S4/S6/S7/审计回放 | GOAL 交付 8 | 低 |
| 10 | `docs/CHANGELOG.md` | 修改 | FB-03 条目 | GOAL 交付 8 | 低 |
| 11 | `poc/pi-fallback/smoke/smoke_s6_reconcile.py` | **新建** | §9.1 冒烟脚本(临时 APS_HOME + MES 断连/恢复) | GOAL 交付 7 | poc 内,零产品影响 |
| 12 | `poc/pi-fallback/smoke/smoke_s7_ops.py` | **新建** | §9.2 冒烟脚本(运维只读诊断) | GOAL 交付 7 | poc 内,零产品影响 |
**明确不改**:`gateway/app.py`(confirm/双人审批通道原样复用)、`contracts.py`、
`shared/schemas/*`、`apps/web/**`(§0.12 零前端改动)、`feature_flags.py`(§0.4 语义分离)、
`state/**`(只用公开 API)、`intent.py`、`assistant.py`、`automation.py`/`audit_alerts.py`
(§0.13)、`mes.py`/`mes_http.py`/`wms_events.py`(编排既有函数,一行不改)、
`tool_runtime.py`、`async_jobs.py`、`apps/desktop/sidecar.cjs`。
---
## 2. P3 白名单设计(GOAL 交付 1 + 前置问题 4)
### 2.1 文件形态与 fail-closed 语义
**默认路径**:`path_under_data("fallback-highrisk.json")`(与 features.json 同目录并列),
环境变量覆盖:`APS_FALLBACK_HIGHRISK_PATH`。
**Schema(whitelistVersion=1)**:
```json
{
"whitelistVersion": 1,
"updatedAt": "2026-09-04T10:00:00",
"updatedBy": "admin-1",
"scenarios": {
"S4": {
"enabled": true,
"intents": ["flex.simulate_due", "flex.compare", "scenario.compare", "scenario.sensitivity"],
"roles": ["planner", "admin"]
},
"S6": {
"enabled": true,
"intents": ["mes.report"],
"roles": ["planner", "admin"],
"maxItemsPerRun": 20
},
"S7": {
"enabled": true,
"intents": ["agent.fallback.ops.config.apply", "agent.fallback.policy.update"],
"roles": ["ops", "admin"]
}
}
}
```
字段规则(`validate_whitelist_doc` 逐条强制):
| 字段 | 规则 |
|------|------|
| `whitelistVersion` | 必须 == 1 |
| `scenarios` | 对象;键只允许 ∈ {"S4","S6","S7"}——**未知场景键 → 整个文件判损坏(全拒)**,不做「忽略未知键」(与 features.json 的 unknown 忽略相反,白名单里任何无法解释的内容都按攻击面处理) |
| `scenarios.<S>.enabled` | 必须 bool |
| `scenarios.<S>.intents` | 字符串数组;每个 intent 必须在 harness `_POWER_MAP` 登记(未登记 → 文件判损坏全拒——白名单不能引用不存在的动作,防止「先放行后登记」的倒置) |
| `scenarios.<S>.roles` | 非空字符串数组(小写归一);该场景可见/可发起/可执行的角色集合 |
| `scenarios.<S>.maxItemsPerRun` | 可选正整数(仅 S6 用,缺省 20) |
**fail-closed 裁决表(`load_highrisk_whitelist` 的全部出口)**:
| 情况 | 结果 | 表现 |
|------|------|------|
| 文件缺失 | 全拒 | `{"ok": False, "error": "白名单文件缺失(fail-closed 默认全拒)", "grants": {}}` |
| JSON 解析失败 / 顶层非对象 / version 非法 | 全拒 | error 显式带原因 |
| 未知场景键 / 未知字段 / 类型错误 | 全拒 | error 显式带 offending key |
| intent 未在 _POWER_MAP 登记 | 全拒 | error 显式带 intent 名 |
| 任何异常(含 IO) | 全拒 | error 归并(与 fallback_feature_enabled 的「宁可误关」同款哲学) |
| 全过 | 按文件放行 | `grants` = 归一化后的场景表 |
**加载结果每次调用现读文件(不缓存)**——白名单修改经确认卡落盘后下一次裁决即生效,
无热更新机制(配置文件量级,读盘成本可忽略;黄金测试改文件即改裁决,确定性)。
返回结构同时携带 `path`/`sha256`(裁决时文件指纹),出卡审计 rationale 记录
`whitelistSha256`——**执行时重算比对,出卡后白名单被改 → 执行拒绝**(§3.5,防「出卡时
合法、执行前收窄/扩权」的窗口攻击)。
### 2.2 与 features.json 的语义隔离(GOAL 硬约束的落实)
| 维度 | features.json | fallback-highrisk.json |
|------|--------------|------------------------|
| 语义 | 可用性/可见性(**不是权限边界**) | **权限边界的一部分**(P3 放行唯一依据) |
| 损坏回退 | fail-open(默认全开,fallback 键默认关) | **fail-closed(全拒)** |
| 未知键 | 忽略并列入 unknown | **判损坏全拒** |
| 加载器 | feature_flags.load_feature_flags | fallback_highrisk.load_highrisk_whitelist(新建,互不 import) |
| 修改通道 | 手工编辑文件(现状) | **只能经 `agent.fallback.policy.update` 确认卡**(§2.4) |
### 2.3 粒度取舍(前置问题 4 的回答)
**结论:意图级为骨架 + 场景级为聚合开关 + 角色级为第三维,三层同文件表达。**
- **为什么不是纯场景级**:S6 的补录意图(mes.report,P1)与 S7 的改配置(P3)权力等级
不同、风险不同、角色不同;场景级整开会把「S7 开了」变成「运维场景里一切 P3 动作都开了」,
违背最小放行。
- **为什么不是纯意图级**:计划的 `scenario` 字段(P2 schema 既有)提供了语义归属——
同一个 mes.report 在 S6(断连补录)放行,不等于在 S3(日常导入)里也该被 Pi 编排;
脱离场景的意图级白名单会让「意图全局放行」悄悄扩大 Pi 在普通对话里的编排面。
场景级开关同时是审计叙事单位(白名单文件读起来就是一张「场景治理表」)。
- **代价如实登记**:场景 × 意图的二维表意味着新增场景或新意图要同时动两处(_POWER_MAP
登记 + 白名单放行),流程变长——这是有意的「放行摩擦」,与白名单的治理定位一致。
### 2.4 改白名单 = P2 管理动作(`agent.fallback.policy.update`)
**链路**(全部复用既有通道,零新机制):
```
发起人(任何有 initiate 角色的用户,建议现场管理员操作)在对话说
「把 mes.report 加进 S6 白名单」→ 意图未登记 → fallback 兜底
→ 编排器识别 policy 请求(简报声明 + Pi 产出 policy 计划,见下)
→ 编排器从「当前白名单 + 计划 diff」在服务端再生成完整新白名单文档
(Pi 只产出 diff 声明 {scenario, addIntents[], removeIntents[], enabled?},
文档本体由编排器计算——卡片内容的机器再生成原则,P2-DESIGN §6.2 同款)
→ validate_whitelist_doc(新文档) 全过才出卡(非法新文档拒绝出卡 + 显式原因)
→ stage_confirmation("agent.fallback.policy.update",
params={"document": 新文档, "documentSha256": sha256(canonical),
"beforeSha256": 当前文件 sha256 | null, "diff": 结构化 diff})
→ P2 单卡确认 → execute_confirmed 新分支:
① 重算 beforeSha256 与当前文件比对(漂移 → 拒绝,fail closed)
② 写 tmp 文件 → validate_whitelist_doc(读回) → os.replace 原子替换
③ 旧文件备份 <path>.bak-<ts>(保留最近 5 份,供人工回滚)
④ WORLD_WRITE 审计(target=FALLBACK_POLICY,rationale 含前后 sha256 + diff + approver)
```
- **手工直接编辑文件不被禁止但可见**:裁决器每次现读 + 出卡/执行双端 sha256 比对,
任何绕开确认卡的手改都会使在途卡片执行拒绝,并在下一次裁决的 `updatedBy/updatedAt`
与审计链对不上——治理台可查(文档如实写明「文件层手改是破窗行为,审计可发现」)。
- **P3 白名单里的 `agent.fallback.policy.update` 自身**(S7.intents 含它时):运维场景下
Pi 编排改白名单同样走上面的卡——「改白名单的卡」本身不享受任何豁免(自指无特权)。
---
## 3. 计划锁的 P3 扩展(GOAL 交付 1 的执行点)
### 3.1 场景开放集
`_PLAN_SCENARIOS = ("S1","S2","S3","S9","S4","S6","S7")`——validate_plan 的场景校验不变,
新场景能不能过卡完全由后续闸口决定。
### 3.2 计划 schema 增量(planVersion 仍为 1,向后兼容)
- `scenario` 新值 S4/S6/S7;
- 步骤 `intent` 可为白名单放行的 P0/P3 意图(见 §3.3 闸口);
- S6 计划的 steps 一律 frozen + params 内联(§0.14③:artifact 指纹路线对真实 Pi 不可用,
P3 全面弃用 artifactRef——validate_plan 对 S4/S6/S7 场景的 artifactRef 显式拒绝,
报错文案指明「P3 场景只接受 params 内联」);
- S4 计划 steps 是「沙盒步骤」(§5.2,`mode` 仍填 "frozen",语义 = 编排器直调沙盒函数)。
### 3.3 闸口改造:硬拒 → 白名单裁决(两处的唯一放行路径)
**validate_plan(出卡闸)改造后的意图检查**(替换 fallback_lane.py:1099-1104):
```python
intent = str(step.get("intent") or "")
scenario = plan.get("scenario")
power = power_of(intent)
if scenario in ("S4", "S6", "S7"):
# P3 场景:意图必须 ① 在 FALLBACK_EXECUTABLE_INTENTS 或 FALLBACK_P3_INTENTS 登记
# ② 经白名单 scenario_grant 裁决放行(含 enabled/intents 双条件)
# ③ power 与该场景允许集匹配(S4 只许 P0/P1 沙盒;S6 只许 P1/P2;
# S7 只许白名单内 P2/P3)
highrisk.check_scenario_step(scenario, intent, power, whitelist) # 不过 → PlanError
else:
# P2 语义逐字节保持:登记 + power ∈ {P1,P2}
if intent not in FALLBACK_EXECUTABLE_INTENTS or power not in ("P1", "P2"):
raise PlanError(...)
```
`highrisk.check_scenario_step` 内部:whitelist 非 ok → PlanError(fail-closed 全拒);
场景未 enabled → PlanError(「S6 未在白名单启用」);intent ∉ 场景 intents → PlanError
(「意图 mes.dispatch 未列入 S6 白名单放行集」);power 越场景允许集 → PlanError。
**check_step_request(执行闸,ASSISTED 邮箱比对)同步改造**:同样的裁决函数,偏离类型
沿用 `plan_deviation:tool`。**但 §3.4 决定 P3 场景不开放 ASSISTED**,该闸的改造仅为
防御纵深(邮箱里出现 P3 意图请求时给出白名单裁决而非硬编码 P1/P2 拒)。
### 3.4 P3 场景只开放 frozen 步(明确取舍)
S4 沙盒步骤参数在提议段即可全量确定(产品/数量/策略名);S6 补录逐笔冻结;S7 配置变更
冻结文档——**三个场景都没有「执行期才知道下一步参数」的正当需求**,而 ASSISTED 是 K 轮
唯一未真实端到端验证的路径(§0.14④)。决定:S4/S6/S7 的 step.mode 只允许 "frozen",
出现 "assisted" → 出卡拒绝(显式文案)。收益:P3 全部执行 = 编排器确定性直执,
**Pi 在执行段不在环**——高风险场景拿到计划锁的最强形态(P2-DESIGN §2.1 的设计理由
原样成立)。代价:未来若出现真需自适应的 P3 场景,需单独设计轮。
### 3.5 出卡/执行双端白名单指纹
- 出卡:`params.whitelistSha256` = 裁决时白名单文件 sha256(随计划冻结进卡);
- 执行:execute_plan 步骤 1.5(指纹重算之后、漂移比对之前)重算当前白名单 sha256,
不等 → `denied`(「白名单在审批窗口内已变更,本次未执行任何变更,请重新发起」)。
P2 场景卡无此字段(None)→ 跳过(向后兼容,T-1..T-17 不受影响)。
---
## 4. 角色矩阵与 auth 现状评估(GOAL 交付 5 + 前置问题 1)
### 4.1 auth 现状评估(前置问题 1 的回答)
**结论:server/auth 现有粒度足以表达「运维」,零代码扩展,纯配置。**
依据(§0.2/§0.3):roles 是自由字符串元组,JMS provider 原样透传平台角色 codes;
审批角色三层解析(action 级 `APS_APPROVAL_ROLE_POLICIES` → power 级 → 全局 env)是
既有机制且有黄金测试(test_approval_role_policies.py)。**最小扩展方案**:
1. 角色字符串约定 `ops`(运维),由 JMS 平台侧角色 code 直接下发;无 JMS 的现场用
`JMS_AUTH_DEFAULT_ROLE` 或既有账号体系赋值——**不新造权限模型、不改 auth 任何文件**;
2. highrisk 动作的发起/批准圈定经 action 级策略配置(部署期 env,示例):
```json
APS_APPROVAL_ROLE_POLICIES={"actions":{
"agent.fallback.execute.highrisk":{"initiate":["ops","admin"],"approve":["ops","admin"]},
"agent.fallback.ops.config.apply":{"initiate":["ops","admin"],"approve":["admin"]},
"agent.fallback.policy.update":{"initiate":["admin"],"approve":["admin"]}}}
```
(该配置不进代码库,进部署 runbook / fallback.md 文档示例;黄金测试用 monkeypatch
setenv 同款机制验证,test_approval_role_policies.py:64 先例。)
3. **可见性门禁**(「非运维物理不可见」)在 fallback 编排层:`propose_reply` 识别到
S7 运维意图(简报引导 Pi 声明 scenario,或编排器按白名单 roles 预检)时,
`is_ops_identity()`(roles ∩ 白名单 S7.roles)不满足 → **返回 None(走原话术,
零副作用零审计,与开关关同形态)**——未授权用户连「存在运维入口」都无从得知。
注意:这不是提示「权限不足」,而是物理不触发(GOAL「物理不可见/不可用」原文)。
4. 例外如实登记:`get_identity()` 的 system 放行(harness.py:486)是测试/脚本通道,
黄金测试里直驱 stage 不受角色闸限制——这与 P1/P2 测试模式一致,不算破窗
(HTTP 面永远有真实身份,middleware 绑定)。
### 4.2 角色 × 场景矩阵(可见性 / 可执行性)
图例:✅ 可见可执行(过卡后);👁 只读可见(草稿/诊断产物);⛔ 物理不可见(propose
返回 None,无入口感知);— 该场景对该角色无定义(按 ⛔ 处理)。
| 角色(auth roles) | S1 意图未识别 | S2 执行失败修复 | S3 新格式导入 | **S4 沙盒试排** | S5 ad-hoc 分析 | **S6 集成故障补录** | **S7 运维诊断** | S9 批量处理 | S10 熔断 |
|---|---|---|---|---|---|---|---|---|---|
| 普通用户(viewer/无审批角色) | 👁 草稿报告 | 👁 诊断报告 | ⛔(出卡被拒:initiate 角色不符) | ⛔(白名单 S4.roles 不含) | 👁 报告 | ⛔ | ⛔(入口不可见) | ⛔ | —(系统行为,全员可见失败文案) |
| 计划员(planner,缺省角色) | ✅ P1 草稿 + ✅ P2 计划卡 | ✅ | ✅ | ✅(S4.roles 含 planner;产物仅草稿) | ✅ 只读 | ✅(S6.roles 含 planner;逐笔卡) | ⛔(入口不可见) | ✅ | — |
| 运维(ops) | 👁 | 👁 | ⛔(S3 属计划员域) | ⛔(S4.roles 不含 ops) | 👁 | 👁 可见对账报告;补录卡发起按 S6.roles 配置(默认不含 ops) | ✅(只读诊断 + 白名单内 P3 配置卡;双人审批) | ⛔ | — |
| 管理员(admin) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅(唯一可 approve S7 P3 配置变更的角色,按 §4.1 策略示例) | ✅ | — |
矩阵落点:**白名单 `scenarios.<S>.roles` 是可见性/发起性的唯一数据源**(文件化、
改它要过卡);批准性由 `APS_APPROVAL_ROLE_POLICIES` action 级策略表达(env 配置)。
两张表在 fallback.md P3 章节并排呈现,文档同步铁律适用。
### 4.3 触发闸顺序(propose_reply 内,新增第 1.5 关)
```
闸 1:fallback 开关(既有,关 → None)
闸 1.5(新增):场景-角色预检 —— 编排器先按 query 做轻量场景归类
(关键词命中只是「归类提示」,不是动作升级——红线 §10 的合规边界:
归类只决定「给 Pi 的简报里声明哪些场景可用」与「入口可见性」,
真正的动作仍由计划锁 + 确认卡 + 白名单裁决):
· 命中运维信号(「日志」「配置」「诊断」「运维」+白名单 S7 enabled)且
身份 ∉ S7.roles → return None(物理不可见,§4.1.3)
· 未命中任何 P3 场景信号 → 按 P1/P2 现有路径继续(简报不含 P3 场景段)
闸 2:运行时可用性 / 闸 3:凭证校验(既有)
闸 4(新增出卡侧):validate_plan 白名单裁决(§3.3)
闸 5(stage 既有):_can_initiate_current 角色策略(harness,PermissionError
→ 编排层捕获归并显式失败文案,不抛穿聊天链路)
```
---
## 5. S4 设计:缺算法/策略的沙盒试排(GOAL 交付 2 + 前置问题 2)
### 5.1 增量价值边界(前置问题 2 的回答)
**结论:S4 = Pi 编排既有沙盒意图 + 沙盒外(run 目录内)临时分析脚本,产物草稿。
不自建试排引擎、不发明新排产算法。**
增量价值在三处(与「flex.simulate_due / scenario.compare 已存在」的重复造轮子风险
正面对账):
1. **意图编排**:现有沙盒意图各自独立、靠用户逐个发起;S4 让 Pi 按用户目标(「这个
工艺没有现成策略,帮我试几个思路」)**自动组合** simulate_due(交期探测)→
compare/sensitivity(策略对比)→ 产出横向对比报告。编排逻辑是 LLM 的增量,
试排本身 100% 复用既有函数。
2. **参数探索**:临时脚本(run 目录 work/ 内,fs_write 写入、**不被执行**——见 §5.2
的硬边界)做的是「从快照数据推导候选参数组合」(如按瓶颈工序反推优先级权重),
推导结果作为沙盒意图的**输入参数**冻结进步骤;计算本身仍在产品沙盒函数内发生。
3. **草稿叙事**:跨多次沙盒调用的结果汇总成一份带 KPI 对比基线的草稿报告
(方案 S4 行验证方式「沙盒 KPI 对比基线版本」的落点)。
**明确不做**:Pi 生成的试排脚本**不被任何解释器执行**(无 shell_run、无 python 桥——
围墙内没有代码执行面,P1/P2 皆然,P3 不扩);「临时脚本」的物理形态 = 产物文件
(work/strategy-notes.md、outbox/draft-report.md),其内容供人审阅、供 Pi 自己
在围墙内读写迭代,**唯一起算作用的是编排器直调的既有沙盒函数**。这是 S4 风险的
物理收敛点:LLM 写错脚本的最坏后果 = 草稿报告内容错,世界不动。
### 5.2 执行形态
```
propose(plan 模式,简报含 S4 段:可用沙盒意图清单 + 「产物仅为草稿」声明)
Pi 读 inbox 快照 → 推导候选策略参数 → 产 plan.json:
steps = [frozen × N],intent ∈ 白名单 S4.intents
(flex.simulate_due / flex.compare / scenario.compare / scenario.sensitivity),
params 内联(§0.14③),expected 声明「预期输出类型」而非世界变化
(沙盒步骤无世界 diff,expected 只允许 {"sandboxOutput": "<说明>"} 形态,
validate_plan 对 S4 步的 added/removed/modified 字段显式拒绝)
→ 出卡:action = "agent.fallback.execute"(P2 卡——见 §5.4 的权力取舍),
summary 逐步行由编排器再生成 + 固定尾行「本计划全部步骤在沙盒内执行,
不改动任何正式数据」
execute(execute_plan 新 S4 分支,步骤 3 的 checkpoint 前置之后):
逐步直调既有沙盒函数(workflow._flex_simulate_due / flex.compare_sort_modes /
scenario.compare_scenarios 的领域层函数,不经 intent 管线)→
每步结果写 execution.jsonl + 汇总进 outbox/sandbox-report.md →
**验证器断言**(fallback_verify 新函数 verify_sandbox_no_main_writes):
world_fingerprint(执行前) == world_fingerprint(执行后)
(§0.7:append-only 审计键不影响指纹)且执行期间未产生任何
category=WORLD_WRITE 的审计事件(扫 cp_before..cp_after 区间 auditEvents)→
断言失败 → 按执行失败处理:自动回滚 + 显式文案(防御纵深:沙盒函数理论上
不写主干,断言是把「草稿语义」从约定变成物理判决)
成功:cp_after 建档 + sandbox-report.md 指纹进审计 rationale
```
### 5.3 为什么不跳过 checkpoint
S4 不写主干,理论上不需要回滚锚点;但 ① 断言失败时回滚是兜底语义的一部分(防「沙盒
函数未来被改出副作用」的漂移);② 前后快照对让「零变化」断言可审计复算(验证报告
引用两个 pairId,数字只许来自冻结快照——P2 铁律延续)。成本 = 两次快照拷贝,可忽略。
### 5.4 S4 出卡权力的取舍
S4 全部步骤 power ∈ {P0,P1},理论上可以免卡直通;**但决定仍出 P2 确认卡**,理由:
① 用户要能看到「Pi 打算试哪些策略组合、用什么参数」再放行(S4 是探索性行为,参数
组合本身是决策);② 计划指纹 + 漂移比对 + 执行证据链全部复用,免卡要新开一条
「P1 计划直通」通道,违背改动最小化;③ GOAL S4 行治理策略原文「仅沙盒语义,产物为
草稿版本」——卡的内容就是草稿声明。代价 = 多一次人工确认(低频探索场景,可接受)。
---
## 6. S6 设计:集成故障补录与恢复后对账(GOAL 交付 3 + 前置问题 3)
### 6.1 阶段划分(四段,每段独立可验)
```
段 A 故障确认(P0 只读):编排器探测 → 状态注入 inbox → Pi 诊断报告
段 B 待补录清单(P1 草稿):编排器从世界状态推导候选清单 → Pi 补充/整理 → 清单冻结
段 C 逐笔补录(白名单 + 逐笔确认卡):清单每一项 = 一张独立 P2 卡
段 D 恢复后对账(P0 读侧对账 + 差异报告 + 证据链冻结)
```
### 6.2 故障检测信号来源(§0.13 结论的落实)
不新造告警子系统;三个既有信号源在 propose 时由编排器聚合注入 inbox:
1. `mes_connection_status()`(mes.py:28)——connected=False + error code;
2. `HttpMesClient.readiness(probe=True)`(mes_http.py:234)——connectivity=failed +
lastError{code,message,statusCode}(轻量 ≤3s,propose 同步预算内可承受;
probe 失败成本已含在 P1 简报里,不额外阻断);
3. 世界状态侧投影:`mesLinks`(下发链路)、`workOrders` 进度缺口、
`wmsPending`/`wmsConsumed`(WMS 事件缓冲)——`export_integration_status()`
(pi_bridge 新增)把这三者 + ①② 写成 `inbox/integration-status.md` +
`inbox/pending-sync.json`(结构化),UNTRUSTED_DATA 声明同款包裹。
Pi 侧只做一件事:基于 inbox 判断「是否确为断连场景」并产出段 A 诊断报告
(断连则进入段 B 协议;未断连则如实报告「连接正常,无需补录」——防空转)。
### 6.3 段 B:待补录清单产物形态
- **候选清单由编排器确定性生成**(不从 Pi 文本解析):扫描 workOrders 中
`mesLinks` 已下发但进度长时间未回流/状态停滞的条目 + 用户在对话里附的 MES
导出文件(inbox 客户文件,P2 先例)→ `outbox/backlog-candidates.json`
(编排器写,sha256 可算);
- **Pi 的职责收敛为「整理与补充」**:读候选清单 + MES 导出文件,补齐缺失字段
(外部工单号、实际完工时间、数量),产出 `outbox/backlog.json`(结构化,
schema 固定:`{"items":[{"woId","externalWoId","progressPct"|"finish","qty"?,"note"?}]}`);
- 编排器对 backlog.json 做**逐项校验**(woId 存在、externalWoId 与 mesLinks 对得上、
progressPct ∈ [0,100]、finish 布尔)——任一不过 → 显式失败不出卡(P2 计划校验
同款哲学:非法产物绝不降级糊弄);
- 清单同时渲染 `outbox/backlog.md` 供人审(卡片 summary 逐项列明,机器再生成)。
### 6.4 段 C:逐笔确认卡链路(「逐笔出卡」的落实)
- backlog.json 的**每一项 = 一个单步计划 = 一张独立确认卡**
(action=`agent.fallback.execute`,scenario=S6,step={frozen, intent="mes.report",
params={"woId":..., "progressPct"|"finish":...}});
- `_stage_plan_confirmation` 分流:scenario=S6 → 循环 stage N 张卡(每张独立
confirmId、独立计划指纹、独立 beforeFingerprint),同一 AgentReply.blocks 依次
下发(§0.12 前端逐张渲染);N > `maxItemsPerRun`(白名单字段,默认 20)→
截断并在文案显式声明「本次出卡前 20 笔,剩余 N-20 笔请在批准后重新发起」
(分批确认,P2-DESIGN §5.2④ 的分段策略延续);
- 每张卡的执行 = execute_plan 既有路径(checkpoint 对 + 漂移比对 + 逐步执行 +
diff 验证),步骤执行器复用 `_apply_step` 新增 `mes.report` 分支 →
`mes.apply_report(store, wo_id, ...)`(§0.10,既有函数一行不改);
- **逐笔独立成败**:卡与卡之间无事务关联——第 3 笔批准失败/拒绝不影响第 1/2 笔
已落账结果(补录场景的正确语义:已补的就是事实);每张卡自己的回滚锚点独立;
- 全部卡处理完后用户在对话里说「补录完了」→ 进入段 D 判定(下一轮 propose)。
### 6.5 段 D:恢复后对账(前置问题 3 的回答)
**「恢复后」语义:人工触发,不是事件订阅。** 用户明示(「MES 恢复了,对一下账」)
→ 新一轮 propose → 编排器 probe readiness:**connected/ok 才进入对账协议**;probe 仍
failed → 如实报告「尚未恢复」并停在段 A 形态(不猜、不轮询、不后台监听——
无事件系统,不造一个)。
**对账流程(全部读侧 P0,零写)**:
```
编排器确定对账域(双来源并集,全部可机读):
① 世界内 mesLinks(kind=dispatch 的外部工单号集合)
② 审计链扫描:本断连窗口内 action 属补录集(agent.fallback.execute + mes.report
步骤)的 WORLD_WRITE/TOOL 事件 → 提取 woId/externalWoId + 补录参数
对账域内每个 externalWoId:
orchestrator 直调 fetch_work_order(external_wo_id)(§0.9,只读 P0)
→ 原始响应原文落 run 目录 inbox/external/<id>.json(UNTRUSTED 标记,
文件 sha256 记 reconcile-manifest.json)
比对(fallback_verify 新函数 reconcile_external):
外部状态/进度/数量 vs 世界侧 workOrders 投影 → 三档结论:
MATCH(一致)/ DRIFT(外部与本地不一致,列差异字段)/ MISSING(单边存在)
产物:outbox/reconcile-report.md(机器生成,逐工单一行 + 汇总计数)
+ reconcile-manifest.json(全部外部响应的 sha256 清单 = **证据链冻结点**)
审计:write_audit(category="ALGO_RUN", action="agent.fallback.reconcile",
power="P0", rationale={"domain": n, "match": n, "drift": n, "missing": n,
"manifestSha256": ..., "externalSnapshotDir": ...}) —— 对账结论与其证据指纹进链
差异处置:报告里 DRIFT/MISSING 条目只给「建议动作」文本;任何纠正写操作
→ 回到段 C 逐笔卡(对账本身永远不写,这是它与段 C 的物理分界)
```
**证据链冻结的构成**(三层,逐层可复算):
① 外部响应原文文件(run 目录,sha256 入 manifest)——世界外证据,回滚抹不掉;
② manifest sha256 + 各文件 sha256 进审计 rationale(哈希链内);
③ reconcile-report.md 的数字只许来自 ① 的冻结文件(报告尾固定声明行,
fallback_verify.build_report 同款铁律)。
Pi 在对账段的唯一角色:把机读差异「翻译」成处置建议文本——**建议不进任何执行路径**
(P2-DESIGN §5.1 的信任边界延续)。
---
## 7. S7 设计:现场运维诊断(GOAL 交付 4)
### 7.1 入口判定(「运维人员显式发起」的落实)
- 触发 = 对话内显式运维意图(闸 1.5 的场景信号,§4.3)+ 身份 ∈ 白名单 S7.roles
(默认 ops/admin)——两个条件缺一不可,缺身份 → 物理不可见(§4.1.3);
- 无独立 UI 入口、无新 HTTP 端点(运维入口 = 同一个聊天框 + 角色门禁;
「专属入口」由「非运维角色物理不触发」实现,与 GOAL「不新造权限模型」对齐)。
### 7.2 只读诊断面(P1 语义,围墙零扩张)
`export_ops_diagnostics()`(pi_bridge 新增,编排器在 propose 时调用)把以下写进
inbox(Pi 仍只有 fs_read 一个读工具——**不登记任何新工具**):
| 文件 | 内容 | 脱敏规则 |
|------|------|---------|
| `inbox/ops/logs-tail.md` | server 日志目录最近 N 行(`APS_FALLBACK_OPS_LOG_LINES`,默认 300;多文件各取尾部) | 行级脱敏:匹配 `(?i)(token|secret|password|api[_-]?key|authorization)\s*[:=]\s*\S+` 的值替换为 `***REDACTED***`;JMS/KIMI 等 key 形态(长 hex/base64 ≥24 位出现在 key= 后)同款 |
| `inbox/ops/config-snapshot.md` | features.json 全文 + fallback-highrisk.json 的**裁决结果投影**(非原文,含 path/sha256/error)+ FallbackConfig 公开字段 | 文件内容同样过行级脱敏 |
| `inbox/ops/health.md` | `/api/readiness` 同源 check_readiness 结果 + 审计链完整性(chain.ok/checked)+ audit_alerts.build_alerts 输出 | 无敏感面 |
| `inbox/ops/integrations.md` | mes/wms/sap 各 status() + readiness(probe=False)(**S7 默认不主动 probe**,避免诊断动作本身产生外部副作用;probe 显式由用户在对话要求时才做) | — |
诊断产物 = P1 草稿报告(诊断结论 + 建议人工操作清单)——方案 S7 行降级策略原文
「输出诊断报告 + 建议人工操作」的直通形态。
### 7.3 改配置:白名单 + 确认卡(P3 双人)
S7 白名单 intents 默认两项:
- `agent.fallback.policy.update`(P2,§2.4):白名单自身治理;
- `agent.fallback.ops.config.apply`(**P3**,harness 新登记):受控配置文件变更。
- params 冻结:`{"file": "features.json", "content": <完整新文档>, "contentSha256": ...,
"beforeSha256": ...}`——**整文档替换形态**(不做 patch 语法:patch 是注入面,
整文档可 diff、可 sha256、可人审;文件白名单只允许 `features.json`,
fallback-highrisk.json 只走 policy.update 专属通道);
- 出卡前编排器校验:file ∈ 允许集、新文档过 `load_feature_flags` 同款结构校验
(version/features 段合法——用独立校验函数,不 import feature_flags 的 fail-open
语义,§0.4);
- P3 卡 → **双人审批(两个不同用户)→ executionGrant**(§0.1 既有机制)→
execute_confirmed 新分支:grant 消费 → beforeSha256 漂移比对 → tmp 写 + 回读校验 +
os.replace 原子替换 + .bak 备份 → WORLD_WRITE 审计(含 grantId 引用、前后 sha256、
approver 双人列表);
- 生效语义如实写明:features.json 每次现读(feature_flags 无缓存),**文件替换即生效**;
但「生效范围 = 可见性开关」——fallback 键本身被关只会让兜底不再触发,不会中断
在途审批(审批仓独立)。
### 7.4 审计回放要求(GOAL「全程审计可回放」)
每个 S7 run 的 run 目录追加 `manifest.json`(编排器写):
```json
{"runId": "...", "scenario": "S7", "actor": "ops-zhang",
"whitelistSha256": "...", "artifacts": {"events.jsonl": "sha256:...",
"calls.jsonl": "sha256:...", "orchestrator.log": "sha256:...",
"outbox/report.md": "sha256:..."}, "auditSeqRange": [起, 止]}
```
回放 = manifest 所列文件逐一验 sha256 + 审计链区间事件链式校验(/api/gov/audit
既有 chain.ok 机制覆盖区间校验)。「录屏级」的如实替代:**事件流全量落盘 +
哈希自证**,比录屏更强(可机读、可复算)——文档里如实说明这是事件级回放而非
屏幕录像。
---
## 8. 测试矩阵(tests/golden/test_fallback_highrisk.py,全部确定性)
风格对齐 test_fallback_execute.py:tmp_path + monkeypatch(APS_FALLBACK_DIR /
APS_FEATURES_PATH / APS_CHECKPOINT_PATH / **APS_FALLBACK_HIGHRISK_PATH** 指 tmp)+
FakeStore 挂 `.checkpoints` + fake runner 注入 + bind_identity 绑角色
(test_approval_role_policies.py:45 `_as` 先例)。清空 LLM env。
公共辅助:`_write_whitelist(tmp, doc)`、`_stage_plan(store, plan, roles=(...))`、
`_approve(store, confirm_id, roles=...)`、`_fp(world)`。
| # | 用例名 | 前置 | 断言 |
|---|--------|------|------|
| H-1 | `test_whitelist_missing_file_denies_all` | 白名单文件不存在 | `load_highrisk_whitelist().ok == False`;S6 计划出卡拒绝(显式「白名单文件缺失」);世界零变更 |
| H-2 | `test_whitelist_corrupt_json_denies_all` | 文件写非法 JSON | 全拒 + error 显式;S4/S6/S7 计划均拒绝出卡 |
| H-3 | `test_whitelist_unknown_scenario_key_denies_all` | 文件含 "S99" 键 | **整个文件判损坏全拒**(不是忽略未知键——与 features.json 相反的断言) |
| H-4 | `test_whitelist_unknown_field_denies_all` | scenario 段含未知字段 | 全拒 + error 带 offending key |
| H-5 | `test_whitelist_unregistered_intent_denies_all` | intents 含未在 _POWER_MAP 登记的意图 | 全拒 + error 带 intent 名 |
| H-6 | `test_whitelist_disabled_scenario_refuses_plan` | S6.enabled=false | S6 计划拒绝出卡(「未在白名单启用」);S4 计划不受影响(场景隔离) |
| H-7 | `test_p3_intent_not_in_whitelist_refused` | 白名单 ok 但 S6.intents 不含 mes.dispatch | 含 mes.dispatch 步的 S6 计划整计划拒绝(P2 T-3 语义的白名单版) |
| H-8 | `test_p2_scenarios_unaffected_by_whitelist` | 白名单文件缺失 | S3 计划(P2 语义)照常出卡——**白名单不挡 P2 存量路径**(向后兼容核心断言) |
| H-9 | `test_whitelist_changed_between_stage_and_execute_denied` | 出卡后改白名单文件 | execute_plan denied(「白名单在审批窗口内已变更」);零写入;DENIED 审计 |
| H-10 | `test_s4_sandbox_plan_executes_with_no_main_writes` | S4 白名单放行;fake runner 产合法 S4 计划(flex.simulate_due frozen 步) | 沙盒函数被调(结果进 execution.jsonl);**前后世界指纹相等**;无 WORLD_WRITE 审计(TOOL/ALGO_RUN 除外);sandbox-report.md 生成;卡片含「不改动任何正式数据」尾行 |
| H-11 | `test_s4_plan_with_write_intent_refused` | S4 计划混入 import.commit | 出卡拒绝(S4 只许白名单沙盒意图) |
| H-12 | `test_s4_expected_world_change_fields_refused` | S4 步 expected 含 {"added": 1} | validate_plan 拒绝(沙盒步只许 sandboxOutput 形态) |
| H-13 | `test_s6_backlog_per_item_cards` | S6 白名单放行;backlog.json 3 项 | 同一 AgentReply.blocks 含 **3 张独立 confirm-card**(独立 confirmId/指纹);逐张批准后逐笔落账 + 每张独立 checkpoint 对;第 2 张拒绝不影响 1/3 |
| H-14 | `test_s6_backlog_over_max_items_truncated` | maxItemsPerRun=2,清单 3 项 | 只出 2 卡 + 文案显式声明截断 |
| H-15 | `test_s6_backlog_item_invalid_refused` | backlog 项 woId 不存在 / progressPct=120 | 逐项校验失败 → 不出卡 + 显式原因 |
| H-16 | `test_s6_reconcile_report_and_evidence_frozen` | 模拟恢复(fake fetch_work_order 注入);对账域 2 项(1 MATCH 1 DRIFT) | reconcile-report.md 结论正确;manifest 含外部响应 sha256;ALGO_RUN 审计 rationale 含 manifestSha256;**世界零变更(对账纯读)** |
| H-17 | `test_s6_reconcile_blocked_while_still_down` | probe 仍 failed | 不产对账报告,如实回复「尚未恢复」 |
| H-18 | `test_s7_non_ops_role_invisible` | 白名单 S7.roles=["ops","admin"];bind_identity(roles=("planner",));query 含运维信号 | propose_reply 返回 **None**(走原话术,零 run 目录、零审计——物理不可见的字面断言) |
| H-19 | `test_s7_ops_readonly_diagnostics` | ops 身份 | inbox/ops/ 四文件生成;**脱敏断言**:埋入 `token=abc123secret` 的日志行在产物中呈 `***REDACTED***`;P1 草稿报告语义 |
| H-20 | `test_s7_config_apply_p3_dual_approval_flow` | S7 白名单含 ops.config.apply;合法 features.json 新文档 | 卡 power=P3;第一次批准 → PARTIALLY_APPROVED;同人二批 → SOD_DENIED;**不同用户二批** → executionGrant 签发 → 执行原子替换 + .bak 备份 + 回读校验 + 双人审计 |
| H-21 | `test_policy_update_writes_whitelist_via_card` | policy 计划(diff 声明 addIntents) | 编排器再生成完整文档;P2 卡批准后白名单文件更新;旧文件 .bak 存在;beforeSha256 漂移(出卡后手改文件)→ 执行拒绝;审计含前后 sha256 |
另:test_fallback_execute.py 的 T-3 同轮更新(§1 #7);P1/P2 既有 32 例全部回归
(白名单文件缺失时 P2 路径零影响——H-8 专项守护)。
---
## 9. 真实冒烟脚本设计(GOAL 交付 7)
### 9.1 `poc/pi-fallback/smoke/smoke_s6_reconcile.py`:MES 断连全链路
环境:临时 APS_HOME(不污染 server/data)+ features.json 开 fallback +
**写 fallback-highrisk.json 放行 S6**(冒烟脚本自己造白名单文件——顺带实测
「文件缺失全拒」的对照组)+ 真实 K2.6 key + 真实 server + 真实 pi。
| # | 验收点 | 通过判据 |
|---|--------|---------|
| A | 断连注入:server 启动 env `MES_HTTP_BASE_URL=http://127.0.0.1:<未监听端口>`(§0.8 唯一切换点) | `/api/integrations/mes/readiness` connectivity=failed,error code=MES_HTTP_CONNECT_FAILED |
| B | 造断连前现场:先经既有链路下发 2 张工单到 stub(切回 stub 配置下发后再切断连,或直接种子数据),并造 1 笔「断连期间实际已完工」的待补录事实(用户在对话陈述 + inbox 放 MES 导出 CSV) | 世界内 mesLinks ≥2;导出文件入 inbox |
| C | 对话「MES 连不上了,今天的报工帮我补一下」 | SSE intent=assistant.reply;inbox/integration-status.md 含 connectivity=failed;Pi 产段 A 诊断 + backlog.json |
| D | 逐笔卡 | blocks 内确认卡张数 == backlog 项数;逐张批准 → 每笔落账 + 独立 checkpoint 对;summary 逐笔列明且全部由结构化字段再生成(无 Pi 散文) |
| E | 恢复注入:`reset_http_mes_client()` 切回 stub(冒烟脚本内嵌 shim:重启 server 或环境切换后探活 ok) | readiness connectivity=ok |
| F | 对话「MES 恢复了,对一下账」 | reconcile-report.md 生成;外部响应原文落盘 + manifest sha256;ALGO_RUN 审计含 manifestSha256;DRIFT/MATCH 结论与脚本构造的事实一致 |
| G | 对照组(同脚本 flag):删掉白名单文件重跑 C | 兜底显式失败「白名单文件缺失」,零出卡零变更 |
| H | 收尾:taskkill 后无残留进程;server/data 零污染;git status 干净 | — |
### 9.2 `poc/pi-fallback/smoke/smoke_s7_ops.py`:运维只读诊断
环境同上 + 白名单放行 S7 + 冒烟身份:JMS_AUTH_DEFAULT_ROLE 或 TestClient 绑身份
机制给会话 ops 角色(冒烟脚本用 server 的测试登录路径,与 P2 冒烟同手法)。
| # | 验收点 | 通过判据 |
|---|--------|---------|
| A | ops 身份对话「帮我看下服务日志和配置有没有异常」 | inbox/ops/ 四文件生成;日志尾部含近期行;脱敏生效(预埋 token 行呈 REDACTED) |
| B | 诊断报告 | P1 草稿语义:报告引用 [callId:] 真实存在;世界零变更(指纹前后相等) |
| C | 非 ops 对照:planner 身份发同一句 | 走原话术,run 目录零新增(物理不可见冒烟层复证 H-18) |
| D | (可选,预算允许)config.apply 双人审批链:ops 发起 + 另一 admin 账号二批 | P3 卡双人链走完,features.json 原子替换 + .bak + 审计双人记录 |
### 9.3 冒烟分工与确定性边界
S6/S7 冒烟归 Agent-N;黄金层(§8)已把白名单各形态、角色越权、逐笔卡、双人链
全部确定性锁定——冒烟层的增量只是「真实 server + 真实 pi + 真实 MES 适配器在环」
的接线事实(K 轮 P2 教训:真实子进程才能暴露的缺陷类型——工具事件映射、简报字段
引导——本轮靠 §0.14 已吸收的 K-1/K-2 修复 + 简报模板 S6/S7 段落显式字段名预防)。
---
## 10. 爆炸半径分析(rg 实测;标注 LOW/MED/HIGH)
### 10.1 fallback_lane.py(P1/P2 自有模块扩展)—— **MED**
外部调用方 rg 实测仅两处:workflow.py:583(execute_plan)、workflow.py:2927
(propose_reply)。模块内改动面:
- `propose_reply`:闸 1.5 角色预检插入在开关闸之后——开关关路径第一关 return None
**逐字节不变**(P1 测试守护);未命中 P3 场景信号的路径简报与 P2 相同(P2 T-1..T-17
守护);
- `validate_plan`:P2 场景分支条件原样(H-8 守护),P3 场景走新裁决函数——新代码
路径只在 scenario ∈ {S4,S6,S7} 时触达;
- `_stage_plan_confirmation`:S6 分流为循环出多卡;单卡路径(scenario != S6)
逐字节保持(含 §0.14①② 的指纹对齐与 setdefault——新卡路径同样照做);
- `execute_plan`:S4 沙盒分支插入在 checkpoint 前置之后;P2 双模式路径(FROZEN/
ASSISTED)不进新分支。
**降级方案(若评审判 HIGH)**:P3 场景全部冻结为 frozen 直执(§3.4 已如此),
分支体整体 try/except 归并显式失败 + checkpoint 前置——最坏情况 = 一次显式失败
且已回滚的确认(P2-DESIGN §9.2 同款兜底语义)。
### 10.2 workflow.py execute_confirmed(+2 分支)—— **MED**
调用方(P2-DESIGN §9.2 已实测):app.py confirm/confirm-batch、mesh.py、saga.py、
run_automation_intent——全部「带 confirmId 进来」的通用入口,新分支按 action 精确
匹配,既有分支逐字节不变。文件写类分支(policy.update / config.apply)全异常归并
显式失败文案(绝不抛出——confirm 单条调用方无异常隔离,P2 同款纪律);写文件前
beforeSha256 漂移比对 + tmp+replace + 回读校验,**失败不留下半个文件**。
降级方案:config.apply 执行失败 = 文件未被替换(tmp 未 promote),显式文案 +
FAILED 审计——原子写保证无中间态。
### 10.3 harness.py(+4 行纯追加)—— **LOW**
_POWER_MAP/_POLICY_DESC 各 +2。影响面与 P2 同款(P2-DESIGN §9.1 逐条适用):
list_policy 多两行(管理台可见,预期内);tool_runtime 登记检查自然生效但
IntentName 不含新 action,intent 管线永不产出——唯一触达路径是 fallback 编排出卡
与 execute_confirmed 分支。**额外注意**:新 P3 action 使 stage_confirmation 自动走
requiredApprovals=2(harness.py:683 既有逻辑按 power 分派)——零代码获得双人语义,
test_harness_p3.py 的既有双人用例模式直接复用(H-20)。
### 10.4 pi_bridge.py(纯追加)—— **LOW**
新增两个 export_* 方法 + 简报模板段落;TOOL_REGISTRY 五键不变;handle_fs_read /
handle_fs_write / ActionMailbox / 凭证签发路径零改动(P1/P2 黄金测试守护)。
脱敏函数是纯字符串变换,失败归并(单文件脱敏异常 → 该文件跳过 + orchestrator.log
记一行,不阻断兜底——与 export_snapshot 失败降级同款,fallback_lane.py:791 先例)。
### 10.5 fallback_highrisk.py(新建)—— **零存量影响**
被 fallback_lane(裁决)与 workflow(policy.update 分支的 validate_whitelist_doc)
引用;不 import poc;不 import feature_flags(§0.4 语义隔离的代码级落实)。
### 10.6 state/ / gateway / 前端 / contracts —— **零改动**
checkpoint/审批仓/confirm 通道/ConfirmCard 全部原样复用(§0.1/§0.12)。
mes.py / mes_http.py / wms_events.py / automation.py / audit_alerts.py /
feature_flags.py / intent.py / assistant.py:一行不改。
**全表无 HIGH 项。** MED 集中点(10.1/10.2)均已给降级方案,且全部被 §8 确定性
用例直接覆盖成功/失败双路径。
---
## 11. 明确不做的事(P3 边界外,施工遇到即停)
1. **P4 内容**:兜底质量 golden 集四指标评估、20 例注入防护集扩建(本轮注入防线
沿用 P2 机制 + 白名单 fail-closed,不新造攻击集)、离线降级演练。
2. **P5 内容**:发布门禁、康尼数据集验收、现场 runbook 正文(§4.1 的策略配置示例
只进 fallback.md 文档)。
3. **桌面打包 / sidecar 集成**(sidecar.cjs 一行不动;pi 打包分发是 P5)。
4. **新 HTTP 端点 / 前端组件 / 独立运维 UI**(§7.1:运维入口 = 聊天框 + 角色门禁;
ConfirmCard 四 props 之外零改动)。
5. **集成故障告警子系统 / 事件订阅 / 后台轮询恢复检测**(§0.13/§6.5:恢复是人工
触发,不造事件系统)。
6. **Pi 围墙内代码执行面**(§5.1:临时脚本只作产物不执行;无 shell_run、无 python
桥、无自定义 RPC——aps_invoke 仍只有邮箱一种形态)。
7. **ASSISTED 模式对 P3 场景开放**(§3.4 取舍);artifactRef 路线对 P3 场景开放
(§0.14③,P3 全面 params 内联)。
8. **auth 体系改动**(不新造权限模型:roles 自由字符串 + env 策略既有机制纯配置
复用,server/auth/ 零改动)。
9. **features.json 与白名单文件合并 / 共用加载器**(§0.4/§2.2 语义隔离是硬约束)。
10. **WMS 补录写路径**(S6 本轮只覆盖 MES 报工补录;WMS 侧 wms_events 管线已有
幂等消费语义,断连补录的场景定义与 MES 不同,留待单独设计轮——文档如实登记)。
11. **不改 contracts.py / shared/schemas / apps/web / state/ / tool_runtime.py /
async_jobs.py / gateway/app.py**;不 import poc/ 下任何模块。
---
## 附:P3 新增环境变量
| 变量 | 默认 | 说明 |
|------|------|------|
| `APS_FALLBACK_HIGHRISK_PATH` | `path_under_data("fallback-highrisk.json")` | P3 白名单文件路径(fail-closed) |
| `APS_FALLBACK_OPS_LOG_LINES` | `300` | S7 诊断注入的日志尾部行数 |
| `APS_FALLBACK_S6_MAX_CARDS` | 白名单 `maxItemsPerRun`(缺省 20) | S6 单轮出卡上限(白名单字段优先,env 为全局封顶) |
(角色策略复用既有 `APS_APPROVAL_ROLE_POLICIES` / `APS_APPROVAL_*_ROLES`,不新增。)