# P2-DESIGN — Pi Agent 兜底能力 · 写操作过确认卡门禁(文件级详细设计) > 设计员:Agent-I · 日期:2026-09-03 · 协调板:`GOAL-P2.md` > 实施员(Agent-J)照本文件施工;验证员(Agent-K)按 §8 测试矩阵与 §7 冒烟规格执行。 > 前置已读并源码核实:GOAL-P2.md、docs/architecture/fallback.md、 > P1-DESIGN.md / P1-IMPL-NOTES.md / P1-SMOKE.md、fallback_lane.py(823 行)/ pi_bridge.py(261 行)、 > harness.py(852 行)/ state/checkpoints.py(201 行)/ state/store.py / tool_runtime.py / > async_jobs.py(204 行)/ workflow.py execute_confirmed(499 起)与 import.commit 分支(836 起)/ > gateway/app.py confirm 端点(3406-3458)与 SSE block 下发(1080-1081)/ importers.py > preview/validate/apply(828-992)/ apps/web/src/chat/ChatPanel.tsx ConfirmCard(748-777)/ > contracts.py UIBlock(41-52)/ tests/golden/test_fallback_lane.py / poc tests/rogue_llm_server.py。 --- ## 0. 现状关键事实(设计依据,均已源码核实) 1. **确认卡冻结机制**(harness.py:606-690):`stage_confirmation` 把 `params` 深拷贝冻结进 pending 记录(`paramsHash` 同时落账),另捕获 `evidenceRefs` / `beforeSnapshot` / `beforeFingerprint`(世界指纹,经 `peek_scoped_store` 只在 scoped store 已加载时捕获, 否则 None)。**确认请求体只带 confirmId/approve/note**(app.py:3406-3418)——批准时 `take_confirmation` 从审批仓取回冻结 params,API 面无法篡改参数。这是计划锁的通道级根基。 2. **beforeFingerprint 只存不比**:全仓库仅 harness.py:653 一处写入,执行端无比对 (rg 实测)。**漂移检测是沉睡机制**——P2 在 fallback 执行分支里自行比对(不改 harness 函数)。 3. **执行通道**(workflow.py:499 `execute_confirmed`):驳回/证据校验(verify_pending_evidence, fail closed)/逐 action 分支;每个写分支同一范式:`_create_checkpoint(store, label=..., reason="auto:")` → apply_* → write_audit(WORLD_WRITE, before_snapshot=cp.pairId, evidence_refs) → store.save()。 P2 全部照此范式施工,一行新机制不造。 4. **回滚机制**(workflow.py:693-706 checkpoint.rollback 分支 + saga.py:775-801): `pair = get_checkpoints().get(pairId)` → `store.restore(pair["world"])`(store.py:169, 深拷贝+落盘+发号器重校准+主数据回写 DB)。**注意**:restore 整体替换世界,执行期间写入 的 auditEvents 会被一起抹掉(既有"时间旅行"语义)——失败回滚的审计必须在 restore 之后补写。 5. **checkpoint 仓**(checkpoints.py):`CheckpointStore.create()` 返回 meta(pairId/label/reason/ createdAt/versionNo,**不含 world**);取完整世界用 `.get(pairId)["world"]`;容量 20 对淘汰最旧。 `get_checkpoints()` 按 (tenant, world_key) 缓存实例——**测试隔离要点**:黄金测试须给 FakeStore 挂 `.checkpoints` 属性或注入 checkpoint_store(saga.py:469-474 已有同款先例: `getattr(self.store, "checkpoints", None) or get_checkpoints()`)。 6. **确认卡呈现**:`AgentReply.blocks` → gateway SSE `{"type":"block"}`(app.py:1080-1081) → ChatPanel `ev.type==='block'` 追加(ChatPanel.tsx:272)→ `ConfirmCard` 组件只读 props 的 `{confirmId,title,summary,power}` 四个字段(756 行),**额外 props 原样穿透不渲染、 不报错**。批准回传 `postConfirm` → `/api/actions/confirm`。结论:**P2 确认卡零前端改动**。 7. **import.commit 现状链路**(S3 的既有写意图):`/api/import/preview`(preview_file → batches:kind/fieldMap/okRows/errors/diagnostics)→ `/api/import/commit` 出卡时**只把 slim batches(kind/sheet/okRows)冻结进 params**(app.py:1690-1697,先例:整批数据冻结进卡参数 是被接受的现状)→ execute_confirmed 836 分支:`apply_import_commit(store.data, next_id, batches)`(importers.py:906,按 kind 分派到 order.upsert/master.material.upsert/flex* 表直写)。 `validate_batch(kind, rows, world, ...)`(importers.py:507)是行级校验的复用点。 8. **async_jobs 真实形态**(204 行 + app.py:4069-4123):JobQueue 机制本身与业务无关, 但 gateway 暴露面是**封闭 kind 白名单**(仅 sensitivity.recompute / montecarlo.recompute), 且提交时对世界做 `copy.deepcopy`——**现有 jobs 全部跑在快照副本上,物理上不写主干**。 这直接决定前置问题 2 的取舍(§5.2)。 9. **P1 围墙**:pi 启动 `--tools read,grep,find,ls`(无 write/edit);守卫扩展 bash/edit/write 全禁(fallback_lane.py:283-324 `_GUARD_TS_TEMPLATE`)。**P2 要产计划文件,必须在 run 目录内 放开写**——守卫模板参数化是本轮对 P1 模块唯一的语义放松(§9.4 评估)。 10. **凭证语义张力**(P1-IMPL-NOTES 遗留 4):真实 pi 无法获知桥侧 callId(凭证是编排器 事后签发),P1 已降级为防伪绊线。P2 写路径的验证依据整体切换到「桥侧事件流」(§5.1)。 11. **意图落点**:intent.py:948 低置信/unknown 一律改写 `assistant.reply`(P1-DESIGN §0.1), workflow.py:2875 分支覆盖 `("assistant.reply", "unknown")` 两个名字。P1 冒烟靠 404 降级 触发,主线 LLM 正常时的落点未验证(P1-SMOKE §6.3)——P2 测试矩阵补齐(§8 T-16/17)。 12. **匿名/测试身份**:`get_identity()` 无 HTTP 上下文时返回 roles=("system",), `_current_identity_has_approval_role` 对 system 放行(harness.py:482)——黄金测试里 `stage_confirmation`/`take_confirmation` 可直接用,无需绑身份(test_approval_store.py 先例)。 13. **execute_confirmed 的其余调用方**(rg 实测):app.py:3414/3439(confirm/confirm-batch)、 mesh.py:549、saga.py:537/628、workflow.py:3191(run_automation_intent 的 G4 受控自动)。 全部是「拿 confirmId 进来」的通用入口——新增 action 分支对它们零影响(§9.2)。 14. **确认卡 TTL**:`APS_APPROVAL_TTL_SECONDS`(默认 24h),过期由 ApprovalStore.decide 判失效返回 None → "该确认卡已失效"。黄金测试可用小 TTL + 操纵 `expiresAtEpoch` (test_approval_store.py:98/351 先例)做确定性过期用例。 15. **P1 简报模板是字符串常量**(pi_bridge.py:231 `_TASK_TEMPLATE`),P2 新增计划简报模板 与旧模板并存——P1 既有用例(test_fallback_lane.py 13 例)断言的路径不受影响(§9.5 逐条核对)。 --- ## 1. 变更清单表 | # | 文件 | 新建/修改 | 改什么(函数级) | 为什么 | 风险 | |---|------|----------|------------------|--------|------| | 1 | `server/agent_core/fallback_lane.py` | 修改(+~520 行) | 见 §2.1:计划解析/校验/指纹、计划确认卡组装、`execute_plan` 执行编排(含回滚)、ASSISTED 第二次 run、守卫模板参数化、FallbackConfig +2 字段。**不改 P1 已有函数的签名与返回语义** | GOAL 交付 2/3/5 的编排主体 | MED(本模块是 P1 新建,存量调用方仅 workflow.py:2882 一处) | | 2 | `server/integrations/pi_bridge.py` | 修改(+~200 行) | TOOL_REGISTRY += `fs_write`(P1,限 work/outbox)/ `aps_invoke`(P2,邮箱协议);`ActionMailbox`(请求扫描/结果写回/凭证签发);`_PLAN_TASK_TEMPLATE` + `render_plan_task_brief()`;计划 schema 常量 | GOAL 交付 5 工具桥写面 | LOW-MED(纯追加;P1 三键注册表查询路径不变) | | 3 | `server/agent_core/fallback_verify.py` | **新建**(~300 行) | 全部,见 §2.3(moduleId: core-fallback-verify) | GOAL 交付 4 diff 验证器 | 新模块,零存量影响 | | 4 | `server/agent_core/harness.py` | 修改(+4 行) | `_POWER_MAP` += `"agent.fallback.execute": "P2"`、`"agent.fallback.execute.highrisk": "P3"`;`_POLICY_DESC` 同步两条。**不动任何函数** | GOAL 交付 1 | LOW(P1 同款纯追加,§9.1) | | 5 | `server/aps_domain/workflow.py` | 修改(+~70 行) | `execute_confirmed` 追加 `if action == "agent.fallback.execute":` 分支(证据校验后、按现状范式:前快照 → 调 fallback_lane.execute_plan → 审计 → save)。**propose 接线点(2875 分支)不动** | GOAL 交付 3/6 执行通道 | MED(热路径纯增量分支,§9.2) | | 6 | `tests/golden/test_fallback_execute.py` | **新建**(~450 行) | §8 测试矩阵 17 例 | GOAL 交付 8 | 新文件 | | 7 | `tests/golden/test_fallback_lane.py` | 修改(±15 行) | `test_write_guard_extension_real_template_format` 按守卫 v2 模板更新断言;新增「readonly 模式逐字节保持 P1 模板」断言 | 守卫模板参数化的同轮修复 | 低 | | 8 | `docs/architecture/harness.md` | 修改 | 权力矩阵 += 2 行(文档同步铁律) | GOAL 交付 10 | 低 | | 9 | `docs/architecture/fallback.md` | 修改 | 增 P2 章节:计划锁/邮箱协议/回滚接线/验证器/注入防线 | GOAL 交付 10 | 低 | | 10 | `docs/CHANGELOG.md` | 修改 | FB-02 条目 | GOAL 交付 10 | 低 | | 11 | `poc/pi-fallback/smoke/smoke_s3_execute.py` | **新建** | §7 冒烟脚本(临时 APS_HOME,不污染 server/data) | GOAL 交付 9 | poc 内,零产品影响 | | 12 | `poc/pi-fallback/tests/rogue_llm_server.py` | 修改(poc 内) | SCENARIOS += 注入剧本(§6.3 冒烟层用例) | GOAL 交付 7 冒烟侧 | poc 内,零产品影响 | **明确不改**:`gateway/app.py`(confirm 通道原样复用)、`contracts.py`(不加 IntentName 成员、 不加 UIBlock type——复用 `confirm-card`)、`shared/schemas/*`、`apps/web/**`(§0.6 零前端改动)、 `tool_runtime.py`(登记检查自然生效)、`async_jobs.py`(§5.2 评估后不复用)、`state/**` (只用公开 API)、`intent.py`、`assistant.py`、`apps/desktop/sidecar.cjs`。 --- ## 2. 计划锁设计 ### 2.1 总体形态:两段式,执行段再分双模式 ``` 提议段(propose run,P1 同步路径沿用,闸 1 = 90s 可配) Pi 在围墙内读 inbox(快照 + 用户文件)→ 写 outbox/plan.json + outbox/artifacts/* → 编排器解析/校验计划 → 合法 → stage_confirmation("agent.fallback.execute") 出卡 确认段(现状,零改动) 人类在对话流确认卡批准 → POST /api/actions/confirm → execute_confirmed 执行段(execute_confirmed 新分支 → fallback_lane.execute_plan) ├─ FROZEN 模式:步骤参数在批准前已全量冻结 → 编排器确定性逐步应用,Pi 不在环 └─ ASSISTED 模式:步骤需执行期自适应 → 拉起第二次 Pi run, Pi 经「动作请求邮箱」逐步请求 → 编排器逐步比对计划锁 → 通过才执行 ``` **设计理由(为什么 FROZEN 是主干)**:S3/S9 的重活(解析客户文件、规范化、字段映射)全部 可在提议段只读完成,产物(规范化批次 JSON)冻结进 artifacts;执行段只是「把冻结产物经 既有 apply_* 写进主干」——秒级、确定性、**偏离在构造上不可能**(Pi 不在环),这是计划锁的 最强形态。ASSISTED 留给 S2 修复类「执行期才知道下一步参数」的场景,逐步比对算法(§2.4) 在其上满足 GOAL「Pi 调用计划外工具或参数越界 → 立即熔断」的原文要求。 ### 2.2 计划草稿结构化 schema(`outbox/plan.json`,planVersion=1) ```json { "planVersion": 1, "scenario": "S3", "goal": "把客户文件 锐扬-9月订单.xlsx 的 37 行订单导入订单池", "steps": [ { "seq": 1, "mode": "frozen", "intent": "import.commit", "summary": "导入订单批 37 行(kind=orders)", "artifactRef": "outbox/artifacts/step1-orders.json", "artifactSha256": "…64hex…", "params": null, "constraints": {"kinds": ["orders"], "maxRows": 500}, "expected": [{"table": "salesOrders", "added": 37}] }, { "seq": 2, "mode": "assisted", "intent": "order.complete", "summary": "把旧单 SO-1001 标记完成", "params": null, "constraints": {"allowedParamKeys": ["orderNo"], "orderNoPrefix": "SO-"}, "expected": [{"table": "salesOrders", "modified": 1}] } ] } ``` 字段规则(校验器 `validate_plan` 逐条强制,任一违反 → 拒绝出卡): | 字段 | 规则 | |------|------| | `planVersion` | 必须 == 1 | | `scenario` | ∈ {"S1","S2","S3","S9"}(本轮开放集) | | `steps` | 1 ≤ len ≤ `MAX_PLAN_STEPS`(=10);seq 从 1 严格连续 | | `intent` | ∈ `FALLBACK_EXECUTABLE_INTENTS`(§2.3 白名单)且 `harness.power_of(intent)` ∈ {P1,P2}——**P3 意图(如 mes.dispatch)出现即整计划拒绝**(execute.highrisk 本轮不开放的执行点) | | `mode` | "frozen" 必须有 params 或 artifactRef+artifactSha256 之一;"assisted" 必须有 constraints | | `params` | frozen 内联参数;canonical JSON 的 sha256 即该步 `paramsDigest` | | `artifactRef` | 必须落在本 run 目录 outbox/artifacts/ 内(resolve + is_relative_to,防 ../) | | `artifactSha256` | 出卡前编排器重算文件内容 sha256 比对——**不等即拒绝出卡**(防 Pi 报假指纹) | | `constraints` | assisted 步的参数边界:`maxRows`(int) / `kinds`(list) / `allowedParamKeys`(list) / 意图专属键(如 orderNoPrefix) | | `expected` | 结构化预期影响 `[{table, added?, removed?, modified?}]`——验证器对账唯一依据(§4);**Pi 散文预期影响不入卡、不入指纹** | **计划指纹**(出卡时冻结): ```python def plan_fingerprint(plan: dict) -> str: """sha256(canonical_json({ "planVersion", "scenario", "steps": [{seq, mode, intent, paramsDigest|artifactSha256, constraints} ...] })) —— canonical = json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=True)。 goal/summary/expected 等展示性字段不入指纹(改了措辞不算偏离;改了动作/参数边界才算)。""" ``` ### 2.3 可执行意图白名单(P2 开放面,fallback_lane 模块内显式表) ```python # Pi 写世界的全部可能路径。未列入的意图 → 计划校验拒绝(不出卡)。 # 每个执行器复用 execute_confirmed 既有分支调用的同一个 apply_* 函数—— # 「Pi 没有新的物理写能力,只有编排既有写意图的能力」的代码级落实。 FALLBACK_EXECUTABLE_INTENTS: dict[str, str] = { "import.commit": "importers.apply_import_commit(S3 主干;frozen 步先过 validate_batch)", "data.import": "intake.apply_import(S9 自然语言批量)", "order.upsert": "orders.apply_order_action(S9/S2 修复)", "order.cancel": "orders.apply_order_action", "order.complete": "orders.apply_order_action", "master.material.upsert": "masterdata.apply_master_action(S3 附带新物料)", } ``` 执行器分发表 `_STEP_APPLIERS: dict[str, Callable[[WorldStore, dict], dict]]`, 每个函数返回 `{"summary": {...}, "auditTarget": {...}}` 供审计与对账。新增意图 = 改这张表 + 登记检查(`harness.power_of` 已是 P2),除此之外无别路。 ### 2.4 execute 逐步比对算法与偏离熔断(ASSISTED 模式) **动作请求邮箱协议**(`aps_invoke` 工具的物理形态——无网络面、无自定义 RPC): - Pi 侧(守卫放行的 write 工具,仅限 outbox/):写 `outbox/actions/-.json`, 内容 `{"seq": N, "intent": "...", "params": {...}}`;然后轮询 read `outbox/actions/-.result.json` 拿结果。 - 编排器侧:`_run_events` 主循环每消费一个事件后调 `ActionMailbox.scan()` 扫新请求文件 (fake runner 测试路径同样走扫描——事件流与邮箱解耦,确定性保证)。 - 每个通过的请求由桥签发 callId 落 calls.jsonl(**桥侧事件流**,§5.1),执行结果摘要写回 result 文件;偏离请求写回 `{"ok": false, "error": "BLOCKED: ..."}` 并触发熔断。 **逐步比对(`check_step_request(plan, request, state) -> None | DeviationError`)**: | # | 检查 | 偏离类型 | |---|------|---------| | 1 | `request.intent` 未登记进 `FALLBACK_EXECUTABLE_INTENTS` 或 `power_of` ∉ {P1,P2} | `plan_deviation:tool` | | 2 | `request.seq` ≠ 下一待执行 seq(乱序/跳步/重复) | `plan_deviation:tool`(含步骤序) | | 3 | `request.intent` ≠ 计划该 seq 步的 intent | `plan_deviation:tool`(计划外工具) | | 4 | 请求总数 > len(plan.steps)(计划外追加步骤) | `plan_deviation:step_count` | | 5 | assisted 步:`params` 键集合 ⊄ `constraints.allowedParamKeys`,或行数 > `maxRows`,或 kind ∉ `kinds`,或意图专属约束(如 orderNoPrefix)不符 | `plan_deviation:params` | | 6 | frozen 步(双保险,理论上编排器自己执行不会偏):重算 paramsDigest/artifactSha256 ≠ 冻结值 | `plan_deviation:digest` | **熔断状态语义**:偏离即 `breaker:plan_deviation(:)`—— 杀 Pi 进程树(沿用 P1 kill_process_tree)→ 执行段整体判 `blocked` → 自动回滚(§3) → confirm 端点返回显式文案: `"兜底执行偏离已批准计划(),已熔断并自动回滚到执行前快照 ,你的数据未留下任何变更。"` 审计:`agent.fallback.execute` result=FAILED + rationale.deviation + 回滚声明(§3.3)。 ### 2.5 出卡前校验顺序(`validate_plan` 全过才 stage) 1. schema 结构(§2.2 表逐条)→ 2. intent 白名单 + power 复查 → 3. artifact 路径圈禁 + sha256 重算 → 4. constraints 合法性(maxRows>0 等)→ 5. 步骤数 ≤ MAX_PLAN_STEPS。 任一失败:不出卡,回复 = P1 失败话术形态(原话术 + 「智能兜底产出的执行计划未通过校验: <原因>,未生成确认卡,你的数据未被改动」)+ FAILED 审计。**非法计划绝不降级成草稿糊弄** (诚实原则);Pi 没写 plan.json 才是 P1 草稿语义(向后兼容)。 --- ## 3. checkpoint 与回滚接线(全用现状 state 机制) ### 3.1 确切调用链 ``` POST /api/actions/confirm(app.py:3406,零改动) → execute_confirmed(store, confirmId, approve=True, actor) [workflow.py:499] → take_confirmation → verify_pending_evidence(现状,零改动) → 新分支 if action == "agent.fallback.execute": [workflow.py 新增] msg = fallback_lane.execute_plan(store, pending, actor=actor, evidence_refs=evidence_refs) # execute_plan 内部: 1) 计划指纹重算 ≠ pending.params.planFingerprint → 显式拒绝(无写入) 2) 世界漂移:pending.beforeFingerprint 非 None 且 world_fingerprint(store.data) ≠ 之 → 显式拒绝(无写入,fail closed) 3) cps = _resolve_checkpoint_store(store) # saga.py:469 同款: # store.checkpoints or get_checkpoints() cp_before = cps.create(store.data, label=f"兜底执行前基线 {run_id}", reason="auto:fallback.execute", conversation_note=f"批准兜底计划 {plan_fingerprint[:12]}") 4) 逐步执行(FROZEN 直接 _apply_step / ASSISTED 第二次 run + 邮箱比对) 步骤事件全程落 run 目录 execution.jsonl(世界外证据,回滚抹不掉) 5a) 全部成功: cp_after = cps.create(store.data, label=f"兜底执行后快照 {run_id}", reason="auto:fallback.execute.post") verify_report = fallback_verify.build_report(...) # §4 → ExecuteResult(ok=True, cp_before, cp_after, report_path) 5b) 任一步异常 / 计划偏离 / 执行段超时熔断: cp_failed = cps.create(store.data, label=f"兜底失败现场 {run_id}", reason="auto:fallback.execute.failed") # 取证留存 pair = cps.get(cp_before["pairId"]) store.restore(pair["world"]) # 现状回滚原语 rolled_ok = world_fingerprint(store.data) == world_fingerprint(pair["world"]) → ExecuteResult(ok=False, status="blocked"|"failed", rolled_back=True, rollback_verified=rolled_ok, ...) → 分支写审计 + store.save()(§3.3) ``` ### 3.2 设计要点 - **前快照在执行批准后才建**(与全部既有 P2 分支一致),不进 stage_confirmation 的 `before_snapshot` 参数——审批窗口内世界可能合法变化,出卡期快照会过时;漂移防护由 `beforeFingerprint` 比对(步骤 2)承担,这才是该字段的设计用途(§0.2:现状只存不比, 本设计在执行端补上比对,**不改 harness 函数**,比对代码在 fallback_lane)。 - **失败现场快照(cp_failed)先于 restore**:部分写入的损坏世界留档取证,回滚本身可被 时间线检视——与 checkpoint.rollback 分支「回滚前先把现在也存档」同款哲学。 - **回滚验证**(方案 F3「diff 为空才算回滚成功」):restore 后重算世界指纹与前快照比对; 不一致 → ExecuteResult.rollback_verified=False,审计与文案**如实声明「回滚校验不一致, 请人工核查」**(不装死)。restore 是深拷贝整体替换,正常路径恒一致,此断言是纯防御纵深。 - **执行期审计的存活问题**(§0.4):步骤级记录**先落世界外证据**(run 目录 execution.jsonl + calls.jsonl),不进 store.data["auditEvents"];成功时由分支批量补写 TOOL 审计进链;失败回滚时 restore 会抹世界,分支在 restore 之后补写一条 FAILED 总账 (rationale 带 execution.jsonl 路径与步骤摘要 digest)——既满足「失败显式」又不破坏 既有回滚语义、不让审计链出现「被回滚的成功写」。 ### 3.3 审计事件(分支侧,成败都写) ```python write_audit(store.data, store.next_id, actor=actor, category="WORLD_WRITE", action="agent.fallback.execute", target={"type": "FALLBACK_RUN", "id": run_id}, power="P2", rationale={"confirmId": confirm_id, "approver": actor, "runId": run_id, "planFingerprint": fp, "stepsExecuted": n, "status": "success|failed|blocked", "deviation": "...(仅 blocked)", "rolledBack": bool, "rollbackVerified": bool, "cpAfter": pairId or None, "verifyReport": str(path or ""), "executionLog": str(exec_jsonl)}, result="SUCCESS" | "FAILED", before_snapshot=cp_before["pairId"], evidence_refs=evidence_refs) store.save() ``` 出卡时另写 `GATE agent.fallback.execute.stage` 审计(import.commit.stage 先例, app.py:1698-1704),rationale 含 confirmId + planFingerprint + 步骤数。 --- ## 4. diff 验证器(fallback_verify.py,新建,moduleId: core-fallback-verify) ```python # ============================================================ # 兜底验证器 v1(moduleId: core-fallback-verify, 可重生 ✅) # 《Pi-Agent兜底能力详细方案》§4.1/§4.5 + GOAL-P2 交付 4: # 执行后 world diff、验证规则(行数/数量对账)、报告生成。 # 铁律:报告里的每个数字都只许来自冻结快照(cp_before/cp_after 的 # world 深拷贝),绝不引用 Pi 报告文本或 Pi 自述。 # ============================================================ _DIFF_TABLES = ("salesOrders", "flexOrders", "materials", "flexMaterials", "flexEquipment", "flexMolds", "flexOperations", "flexRoutings", "flexBom", "productionOrders", "workOrders") def world_diff(before: dict, after: dict) -> dict: """分表 diff:{table: {"added": n, "removed": n, "modified": n, "quantityDelta": float}}。modified 判定 = 同 id/canonical 键条目内容变化; quantityDelta 对含 quantity/stock 字段的表求和(数量对账类规则的数据源)。""" def check_expectations(plan: dict, diff: dict) -> list[dict]: """逐条比对计划 expected(§2.2 结构化字段)与实际 diff。 返回 [{"expect": ..., "actual": ..., "ok": bool} ...];任一 ok=False → 报告 verdict="MISMATCH"(显式,不圆场)。容差 = 0。""" def build_report(run_dir: Path, plan: dict, *, cp_before_id: str, cp_after_id: str, before_world: dict, after_world: dict, checks: list[dict]) -> Path: """生成 outbox/verify-report.md:计划摘要 / 分表 diff 表 / 对账结论 / 证据引用(两个 pairId + 计划指纹)。返回路径。 报告尾固定一行:本报告全部数字来自检查点 与 的冻结快照。""" ``` 验证结果进 execute 成功回复文案(`"对账:salesOrders +37(与计划一致 ✅)"`), verdict=MISMATCH 时**执行本身仍算成功但报告与回复必须显式标注不一致**—— 写已发生且真实,诚实呈现比对结果,由人决定是否用检查点回滚。 --- ## 5. 三个前置问题的解决方案(每个含明确取舍) ### 5.1 凭证语义:从 Pi 自述 → 桥侧真实事件流 **方案**:P2 写路径的验证依据整体切换—— ① 每个写动作的「发生」以编排器在邮箱目录**观察到请求文件**为准(桥侧事件,Pi 无法否认也无法虚构); ② 每个请求由桥签发 callId 落 calls.jsonl(Pi 不知其值,防伪绊线保留); ③ 执行结果与对账数字只来自前后快照 diff(§4 铁律); ④ 「确认」只信 gateway 会话内真实确认卡(confirmId 通道,harness 冻结 params)—— Pi 输出里出现「用户已确认」「管理员同意」一律无效(§6 测试用例 E-5 断言)。 **取舍**:Pi 报告文本中的任何成果声明不参与验证(连「佐证」都不算);代价是报告可读性 与验证完全解耦,换来「LLM 圆场在网关层被物理判失败」(方案 F7)在写路径同样成立。 ### 5.2 90s 同步超时:重活前置 + 执行段双模式预算,async_jobs 评估后不复用 **方案**: ① S3/S9 的重活(大文件解析/规范化)在**提议段**完成并冻结为 artifacts——提议段受既有 闸 1 保护(默认 90s,`APS_FALLBACK_TIMEOUT_SEC` 可调,大文件现场调 300s); ② 执行段 FROZEN 模式 = 冻结产物经 apply_* 确定性写入,**实测量级秒级**,同步无压力; ③ ASSISTED 模式独立预算闸 `APS_FALLBACK_EXEC_TIMEOUT_SEC`(默认 120s)+ 独立步数闸 (默认 40),超时即 `breaker:timeout` → 熔断 → 自动回滚 + 显式声明; ④ 超大数据的兜底策略 = **分段确认**:Pi 产出多个小计划各自出卡(每张卡都是独立 checkpoint 对),而非单卡长任务。 **取舍(为什么不复用 async_jobs.py)**:现有 `/api/jobs` 是封闭 kind 白名单且**全部任务 跑在深拷贝快照上、物理不写主干**(§0.8);把写路径塞进 JobQueue 需要新开「写主干的后台 任务」副作用通道 + 结果回投机制(轮询/消息补写),二者都是本轮边界外的新机制,违背 改动最小化;confirm 是低频人工动作,单次同步 ≤120s 可接受。**如实登记已知边界**: confirm 端点在 async handler 内同步调 execute_confirmed(app.py:3414),ASSISTED 长执行 期间事件循环被占——与 P1 chat 90s 同步预算同族取舍,P3 再评估异步化。 ### 5.3 主线 LLM 正常时的意图落点验证 **方案**:双层覆盖—— ① 黄金测试(确定性):T-16 直驱 `handle_intent(store, s, IntentResult(intent="assistant.reply", ...))` 断言开关开时进兜底出卡路径、关时逐字节原话术(覆盖 §0.11 的真实落点名字);T-17 断言 intent.py 的改写行为——LLM 置信度 <0.5 或产出 unknown 一律改写 assistant.reply (若既有测试已覆盖则改为引用断言,不重复造)。 ② 真实冒烟(Agent-K):主线模型可用时发一句无害新说法(如「帮我把这份客户表格整进来」), 断言 SSE intent 事件 == assistant.reply 且兜底 propose 触发出卡。 **取舍**:不在黄金测试里 mock 整个 LLM 分类器(脆弱且无增量价值);黄金层锁「落点名字 → 兜底分支」的接线,真实层锁「分类器产出该名字」的事实,两层拼起来即全链。 --- ## 6. 注入防线 ### 6.1 隔离标记包裹方案 - **用户原话**:沿用 P1 `<<>>` 包裹(pi_bridge.py:234),计划简报模板 同款保留。 - **注入数据(inbox 快照 / 客户上传文件)**:计划简报新增「数据区」声明—— `<< maxRows(= E-3) | plan_deviation:params + 回滚验证 | | T-13 | `test_assisted_extra_step_trips_breaker` | 第 N+1 请求(= E-4) | plan_deviation:step_count + 回滚 | | T-14 | `test_execution_exception_rolls_back_and_reports` | 执行器抛错(如 apply 对坏数据) | 自动回滚;rollback_verified;FAILED 审计在 restore 之后存活(链里查得到);失败现场快照存在 | | T-15 | `test_confirm_card_expired` | APS_APPROVAL_TTL_SECONDS=1 + 操纵 expiresAtEpoch(test_approval_store.py 先例) | execute_confirmed 返回「已失效」;零写入 | | T-16 | `test_assistant_reply_intent_reaches_fallback_when_flag_on` | IntentResult(intent="assistant.reply") 直驱 handle_intent(§5.3 黄金层) | 开关开 → 兜底触发;开关关 → 原话术逐字节不变 | | T-17 | `test_unregistered_unparseable_llm_output_rewrites_to_assistant_reply` | intent 管线级:低置信/非法产出 | 落点 == assistant.reply(若既有用例已覆盖则本例改为接线断言:workflow 分支同时覆盖两个名字) | | E-1/E-5/E-7 | (见 §6.3,同文件实现) | | | --- ## 9. 爆炸半径分析(rg 实测;标注 LOW/MED/HIGH) ### 9.1 harness.py(+4 行纯追加)—— **LOW** 改动 = `_POWER_MAP`/`_POLICY_DESC` 各 +2 行,不动函数。影响面: | 调用方(rg 实测) | 影响 | |--------|------| | `power_of`/`needs_confirm`/`list_policy` | 两新键可查;门禁管理台权力矩阵多两行(预期内可见变化) | | `tool_runtime.is_registered`(tool_runtime.py:31) | 两新键变为「已登记」——但 IntentName 枚举不含它们,intent 管线永不产出;execute 走 confirmId 通道不经 check_tool;无实际触达路径(P1 同款先例) | | `_approval_role_policy_config`(harness.py:433) | 只在校验 env 配置时读表;新 P2/P3 键可被角色策略引用(功能增强,非破坏) | | 全部 12 个 `stage_confirmation` 调用文件(§0.13 清单) | 零影响(未动该函数) | | `take_confirmation`/`verify_pending_evidence`/`resolve_confirmation_store` | 零影响(通用机制,action 无关) | ### 9.2 workflow.py execute_confirmed(+1 分支)—— **MED** 调用方(rg 实测):app.py:3414/3439(confirm/confirm-batch)、mesh.py:549、saga.py:537/628、 workflow.py:3191(run_automation_intent)。全部是「带 confirmId 进来」的通用入口, 新分支 `action == "agent.fallback.execute"` 精确匹配——**只有 fallback 出的卡会命中**, 既有全部 action 分支逐字节不变。MED 的来源:热路径 + 分支内拉起子进程(ASSISTED)。 缓解:① 分支体整体 try/except 归并显式失败文案(绝不抛出,execute_confirmed 既有调用方 无一做异常隔离——confirm-batch 有,单条 confirm 没有);② T-2/T-14 直接断言成功与 异常两条路径;③ 开关关时该 action 的卡根本不可能存在(出卡点在 fallback 内)。 **降级方案(若评审判 HIGH)**:分支整体收敛为「同步执行 + 全异常归并显式失败 + 自动回滚」, ASSISTED 子进程拉起失败同样走回滚路径——即最坏情况退化为「一次显式失败的确认」, 不可能出现半写入不声明(checkpoint 前置保证)。 ### 9.3 state/(checkpoints.py / store.py)—— **零改动,LOW** 只用公开 API:`CheckpointStore.create/.get`、`WorldStore.restore/.save`。 `get_checkpoints()` 缓存语义不变;测试隔离经注入点(§0.5)不动产品代码。 ### 9.4 fallback_lane.py / pi_bridge.py(P1 自有模块扩展)—— **MED** - `propose_reply` 扩展(计划解析+出卡):开关关第一关 return None 逐字节不变(P1 测试 1 直接守护);开关开时无 plan.json = P1 语义(T-7 守护)。 - **守卫模板参数化**(`write_guard_extension(run_dir, mode=...)`,mode ∈ readonly/plan/execute,默认 readonly = P1 模板逐字节保持):plan/execute 模式放开 write/edit 至 run 目录内 work/+outbox/(inbox 只读、防逃逸不变、bash 仍全禁)。 这是对 P1 围墙的**唯一语义放松**:影响面 = run 目录沙箱内文件,世界写入仍只能走 邮箱→计划锁。`config.tools` 默认值计划/执行 run 用 `"read,grep,find,ls,write,edit"`。 被打破的既有测试:`test_write_guard_extension_real_template_format`——同轮更新(§1 #7)。 - TOOL_REGISTRY += 2 键:`check_tool_registered` 的 P1 查询路径(三键)不受影响(纯追加)。 ### 9.5 chat 块协议 / 契约三端 —— **零改动,LOW** 复用 `confirm-card` 块:contracts.py UIBlock type 枚举、shared/ui_block.schema.json、 apps/web api/types.ts 三端均已有该类型;ConfirmCard 只读四个既有 props(§0.6)。 props 增量字段(可选 `fallbackPlan={runId, fingerprint, stepCount}` 供后续渐进增强) 对前端透明。test_contract_sync 零风险。 ### 9.6 gateway/app.py / async_jobs.py —— **零改动** confirm/confirm-batch 通道原样复用;jobs 不接入(§5.2)。 **全表无 HIGH 项。** 唯一 MED 集中点(execute_confirmed 热路径分支)已给降级方案(§9.2)。 --- ## 10. 明确不做的事(P2 边界外,施工遇到即停) 1. **`agent.fallback.execute.highrisk` 的白名单开放**:登记 P3 但任何含 P3 步骤的计划一律 拒绝出卡;白名单机制、逐字段人工确认是 P3 阶段内容。 2. **S4(沙盒试排新策略)/S6(外部集成故障补录)/S7(运维诊断)** 场景接线。 3. **异步执行与结果回投**:/api/jobs 写类 kind、消息补写、SSE 进度推送(§5.2 已给取舍)。 4. **Pi 进程内自定义 RPC 工具面 / HTTP 桥**(网络监听面);aps_invoke 只有邮箱一种形态。 5. **桌面打包 / sidecar 集成**(sidecar.cjs 一行不动)。 6. **L3 网络层硬化**(loopback 限制)、token 预算闸、junction/8.3 短路径防护 (沿用 P0/P1 已知边界声明)。 7. **分段确认的自动编排**(多计划批量出卡的编排器辅助;§5.2 只承诺「Pi 可产出多个计划 各自出卡」的手工路径)。 8. **不改 contracts.py / shared/schemas / apps/web / tool_runtime.py / async_jobs.py / state/ / intent.py / assistant.py**;不 import poc/ 下任何模块(poc 仅设计参照 + 冒烟脚本宿主)。 9. **fallback 触发率指标 / 治理周报**(方案 F9,P4)。 10. **确认卡 UI 渐进增强**(计划步骤表格化展示等;本轮卡片信息全部走 summary 文本行)。 --- ## 附:P2 新增环境变量 | 变量 | 默认 | 说明 | |------|------|------| | `APS_FALLBACK_EXEC_TIMEOUT_SEC` | `120` | 执行段 ASSISTED run 预算闸(超时→熔断→回滚) | | `APS_FALLBACK_EXEC_MAX_STEPS` | `40` | 执行段工具/请求步数闸 | | `APS_FALLBACK_MAX_PLAN_STEPS` | `10` | 计划步骤数上限(出卡前校验) | (提议段沿用 P1 的 `APS_FALLBACK_TIMEOUT_SEC=90`;大文件现场调大即可,§5.2。)