修复链路上存在的若干阻塞性漏洞 (#199)
Project CI / Repository checks (push) Successful in 4m0s
Project CI / Frontend tests (push) Successful in 4m34s
Project CI / Backend tests (push) Successful in 6m48s
Project CI / Native shell tests (push) Successful in 16m12s

Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/199
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #199.
This commit is contained in:
2026-08-28 17:09:02 +08:00
committed by 段舒康
parent 7d4c47ec0c
commit e31d8a2a5b
50 changed files with 5118 additions and 1658 deletions
@@ -1482,6 +1482,8 @@ npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-final-reply-transie
V1.43 不放宽 V1.41 的文本型 `game-creator-provider-handoff.v1`,而是为 `requestKind=tool-plan` 增加独立私有账本 `.agent/runtime/tool-plan-handoffs/<agentKey>/<runKey>.json`,schema 固定为 `game-creator-tool-plan-handoff.v1`。同一 Agent/run 账本按 `(loopIteration, repairAttempt)` 单调保存已成功的 `repair-0..N` Provider 响应,每条绑定完整 retry identity、实际物理 `providerRequestId`、真实 request slot/attempt、Provider/model、去除 thinking 后的响应、thinking 归一化哈希/计数、完整 function call envelope、usage、响应指纹和创建时间。账本使用既有 `0600`、原子替换、父目录同步、`.previous` 恢复和写后完整回读;未知字段、乱序/缺口、重复 slot 冲突、超限、危险可执行路径、密钥或配置痕迹一律失败关闭。
当成功响应因 handoff 校验失败而进入 `needs-reconciliation` 时,Runtime 额外在应用私有数据目录的 `diagnostics/provider-reconciliation/<projectHash>/<requestHash>.json` 写入一次本地诊断。该诊断只服务人工排障,不参与恢复、重试或业务状态判断,可保留本次 Provider 响应、tool call arguments 和原始校验错误;项目 `.agent`、Agent DB、公共 event、CLI 与报告只保留安全摘要及该私有诊断的相对引用。诊断文件限制为 1 MiB,使用原子写入;应用配置目录不可用或诊断写入失败时,不改变既有 fail-closed reconciliation 语义。
### 提交、重放与所有权
- 每个 tool-plan 物理请求的顺序固定为:Provider 成功 -> tool-plan handoff 追加并回读 -> 同一实际 requestId lifecycle `completed` -> 解析/格式修复或动作预检。function arguments 只存在于私有 handoff 与后续 pending/action batch。protocol/repair 公共审计共同保存 `agentId/taskId/sessionId/runId/source/loopIteration/repairAttempt/requestSlot/responseFingerprint/providerRequestIdSha256/protocol`;protocol 只额外保存 `functionCallCount/callIdSha256s/functionNames/responseIdSha256/responseIdChars` 和既有 normalization 字段,其中 function names 必须由 catalog 绑定;repair 只额外保存 attempt/maxAttempts、协议错误/preview 哈希与字符数及 `callIdSha256/functionNameSha256`。公共 task、event、Agent DB、CLI 和报告不得保存原始 callId/callIds/responseId/providerRequestId。两类审计都在 Agent DB append 锁内按完整 Agent/task/Session/run/source/slot 身份做全历史 compare-and-append,不能以受限尾部读取替代幂等。
@@ -173,7 +173,7 @@ D9/D10 描述的「manifest ready-task 调度器在 Supervisor 下游启动策
| hydrate read-model schema | `plan-gdd-state-view.v1` |
| GDD 状态 | `draft \| ready_for_approval \| revision_requested \| approved \| rejected \| superseded` |
| 单项决定状态 | `confirmed \| default_pending \| prototype_pending` |
| 回答来源 | `user_option \| user_freeform \| default` |
| 回答来源 | `user_option \| user_freeform \| user_revision \| default` |
| 审计 recordType | `agent.runtime.plan.gdd_decided` |
| planning typed 指纹文本 | `sha256-serde-json-v2:<64 位小写十六进制>` |
| 现役 action/profile binding digest | `<64 位小写十六进制>`,无前缀 |
@@ -402,7 +402,7 @@ Runtime 注入并强校验以下精确结构:
- **轮次计数与上限**:本轮是第几轮由委派链上的 `clarification_round` 派生值决定(沿 `repair_of_delegation_id` 上溯推断,见第 23.5 节),上限 3;不再由 session 自行累加 `roundsUsed`。session 仍是 decisions 数组与 GDD 草稿内容的权威,但**不再是轮次状态机的权威**。
> **随之而来的合同影响 —— 2026-08-13 已全部收口。** 第 3 节注册表的 `plan-decision-checkpoint.v1` 与 request kind、第 8.6 节 `plan-session.v1` 的 `activeQuestion` / `roundsUsed` / `supersededCheckpointHandoffs`、第 9 节的 checkpoint domain 与 `supersededCheckpointProviderRequestIds`、第 12 节的 checkpoint stale 状态机、第 14 节与 activeQuestion 相关的恢复行,均已随本节重写一并删除或改写;第 9.1 节 golden vector 已按新 identity 重新生成(3857 bytes,`a59856de7e…`)。
- 用户明确输入优先于 Agent 默认;默认建议必须标为 `default_pending`,手感、节奏、镜头、可读性或重玩差异等需要验证的结论标为 `prototype_pending`。
- 用户明确输入优先于 Agent 默认;默认建议必须标为 `default_pending`,手感、节奏、镜头、可读性或重玩差异等需要验证的结论标为 `prototype_pending`。审批阶段的用户修改意见使用 `answerSource=user_revision`、`round=0`;Runtime 仅在 session 带 revise/reject 的 `lastDecisionRef` 时接受该来源。
- session revision 1 由 Runtime 先写入固定 `initial-request` 决定:topic=`初始需求`、state=`confirmed`、answerSource=`user_freeform`、round=0、answerSummary 精确等于规范化后的 1~400 scalar 初始用户需求。Provider 不能改写或省略这条来源记录;超过上限的初始输入先要求用户收束,不能截断。
### 5.2 决策卡
@@ -663,7 +663,7 @@ Provider 只能提交设计内容,不能提交或覆盖任何 Runtime 身份
该 input 及所有嵌套类型都使用 `deny_unknown_fields`;`game.platformFacts`、任意 `basis`、`projectId/gddId/version/submissionId/approvalRequestId`、action/run/session identity、时间与任何 fingerprint 一旦出现在 Provider input 中即返回 `PLAN_INVALID_REQUEST`。Runtime 在发出本轮 Provider request 前把当前 `sessionRevision/sessionFingerprint` 绑定进内部执行上下文,在项目锁内验证该 CAS 后,才把 project、GDD、版本、durable action、source/profile、session/run、时间、固定 `platformFacts`、全部 `basis:null` 与 fingerprint 注入 `plan-gdd.v1`。字段数量和文本限制按第 8.3 节对应 durable 字段执行。
input 中必须逐项包含并精确等于 source session 的全部 `decisionsSummary` 和 `prototypeValidationItems`,不得改变决定的 id/topic/state/answerSource/round/answerSummary,也不得改变原型项正文或顺序;每个已提问决定因此具有可验证的 1~3 轮来源。额外 decision 只允许是未提问默认:`default_pending + default + round=0`,且不能为它伪造 prototype item。唯一允许的 `confirmed + user_freeform + round=0` 是 Runtime 创建的固定 `initial-request`,其 answerSummary 精确等于初始用户需求。直接出稿因此可以合法使用 `roundsUsed=0`,但仍至少提交该初始 decision。
input 是当前 GDD 的完整快照,不要求与 source session 的 `decisionsSummary` 和 `prototypeValidationItems` 逐项相等。审批修订可用 `user_revision + round=0` 修改、删除或新增决定;未涉及内容由 Agent 以当前 GDD 为基线保持不变。Runtime 仍校验决定结构、原型项双射、`initial-request` 首项、身份和 CAS。payload 出现 `answerSource=user_revision` 时,当前 session 的 `lastDecisionRef.action` 必须是 `revise` 或 `reject`;首次提交、澄清续跑和没有待处理用户修订的普通质量返工返回 `PLAN_INVALID_REQUEST`。用户修订周期内的质量返工可以继续携带 `user_revision`,但其 planning child 必须直接继承当前 session 的 `latestDelegationId`,不得从旧 delivery 另起分支。该闸只作用于新版本 create,同 `submissionId` replay 不重判。唯一固定的 `confirmed + user_freeform + round=0` 是 Runtime 创建的 `initial-request`。
### 8.3 `plan-gdd.v1`
@@ -1152,15 +1152,7 @@ GDD handler 只能从已验证 batch binding 复制 `sourceSessionRevision/sourc
4. Provider transient failure/物理中断但 session、context 和 request slot 未变时,才沿用同一 base ID 的 attempt 派生规则。已知 retryable transport/upstream failure 先把旧 attempt durable 闭合为 `failed`;Runner/进程恢复只有在 boot/owner/lease 证据证明旧物理请求不再存活且无 handoff/batch 时,才闭合为 `interrupted`。旧终态写入、同步并回读成功后,才能创建 attempt N+1 的新 `started`;不能原地复用同一 providerRequestId,也不能让两个 started attempt 并存。无法证明旧请求已终止时进入 recovery required,不自动重发。每个 attempt 始终有独立 `started → completed|failed|interrupted` lifecycle。
5. 除第 1~2 项明确允许的同 binding `started + ready batch` 崩溃组合,以及上文 delivery 问题落盘/答案绑定的已消费证明外,binding 缺失/损坏、lifecycle 与 batch 不一致、同 revision 下 requestContextFingerprint 漂移、session 不是合法 successor,或 batch 已进入执行/等待状态时返回 `PLAN_NEEDS_RECONCILIATION`。此路径不自动删除、不补默认 binding、不重绑、不重试。
严格 submit input 被 Runtime 以 `PLAN_INVALID_REQUEST`、`PLAN_SESSION_DECISIONS_MISMATCH`(2026-08-21 补,见下)或由该输入导出的候选 GDD `PLAN_SIZE_LIMIT` 拒绝时,当前策划子 run 最多产生 **5 次** `plan.submit_gdd / rejected` observation:前 4 次关闭原 sole-action batch 后可在同一 run 续跑,让 Provider 根据最后一条 observation 修正;第 5 次仍须先完整落盘 rejected observation,再把该 run 终态失败,**不得**请求第 6 次 Provider tool-plan。该分类只针对本次 Provider input / 候选 GDD;读取既有不可变 GDD 或 receipt 时出现同名大小上限、既有 lineage 已达版本上限,或任何其它 durable authority 异常,一律是 `PLAN_NEEDS_RECONCILIATION`,不得消耗 Provider 重试额度。计数是 Runtime state 的 durable、每个 child run 独立的字段,进程重启不能清零;只有新建的策划 child run 才从 0 开始。它不依赖前端、Prompt 文字或 Provider 自报,且普通工具 observation 不计入。
**(2026-08-21)台账逐项比对失败从 `PLAN_SESSION_CAS_CONFLICT` 拆出为 `PLAN_SESSION_DECISIONS_MISMATCH`,并纳入上述可重试分类。** 第 8.2 节「input 必须逐项包含并精确等于 source session 的 `decisionsSummary` 与 `prototypeValidationItems`」这条校验(实现为 `planning_submit.rs` 的 `session_decisions_match_input`)原先与三条真 CAS 判据(`sessionRevision` 溢出、session 已被其它动作推进、Runtime source revision/fingerprint 无效)共用一个错误码,因此被 `plan_submit_error_is_business_rejection` 漏掉,一次不匹配即 `needs-reconciliation` 硬阻断整个策划子 Agent。
两者性质本就不同,按本节自己的判据即可区分:真 CAS 说明 **durable 权威**已变或已坏,重交同一份 input 不可能成功;台账不匹配时权威完好,错的是**本次 Provider input**——策划子 Agent 把决策摘要抄漏、抄错,或多追加了一条非 `default_pending` 决定。后者正是本节划归「本次 Provider input / 候选 GDD」的那一类。
**不变量未放松**:不匹配照样拒绝、照样不产生任何事实,只是拒绝的后果从「叫人核对」变成「回灌 rejected observation 让 Provider 改稿」,仍受同一个 5 次 durable 预算约束,第 5 次照常终态失败。伪造用户确认(追加 `confirmed + user_option`)等第 8.2 节禁止的写法一条都没有变得可行。
**触发这次拆分的实测**:策划子 Agent 连续三次 submit 撞形状层(`PLAN_INVALID_REQUEST`),每次都按回灌的理由改对一部分——机制运转正常;第四次形状终于合法,随即撞上台账比对这一支,直接 `needs-reconciliation`,整条链路零产物收场。即**越接近提交成功越容易撞上不给重试的门**,这与「5 次预算让 Provider 自行收敛」的设计意图直接冲突。
严格 submit input 被 Runtime 以 `PLAN_INVALID_REQUEST` 或由该输入导出的候选 GDD `PLAN_SIZE_LIMIT` 拒绝时,当前策划子 run 最多产生 **5 次** `plan.submit_gdd / rejected` observation:前 4 次关闭原 sole-action batch 后可在同一 run 续跑,让 Provider 根据最后一条 observation 修正;第 5 次仍须先完整落盘 rejected observation,再把该 run 终态失败,**不得**请求第 6 次 Provider tool-plan。该分类只针对本次 Provider input / 候选 GDD;读取既有不可变 GDD 或 receipt 时出现同名大小上限、既有 lineage 已达版本上限,或任何其它 durable authority 异常,一律是 `PLAN_NEEDS_RECONCILIATION`,不得消耗 Provider 重试额度。计数是 Runtime state 的 durable、每个 child run 独立的字段,进程重启不能清零;只有新建的策划 child run 才从 0 开始。它不依赖前端、Prompt 文字或 Provider 自报,且普通工具 observation 不计入。
第 2 项的自动前滚必须与 session successor、batch supersede/cleanup 和 replacement request 的 started 写入都在项目锁内按幂等步骤恢复;任一断点重启后只能继续相同步骤。这样合法 steer 能确定性替换旧输出,而身份污染不会被“自动恢复”掩盖。
@@ -1169,7 +1161,7 @@ GDD handler 只能从已验证 batch binding 复制 `sourceSessionRevision/sourc
main loop 不能把 submit 当成普通 action dispatch:在 durable action identity 建立后、生成普通 command ID 或进入 action executor 前,必须进入 `plan.submit_gdd` 专用分支。该分支重验 exact plan identity,执行下列提交与投影。**(2026-08-14 按 M1B-2 实现边界收口)** 本包只负责校验、定版、写不可变 GDD、重建 index、渲染 `game/fast_gdd.md`、安装 session successor 并终止策划子 run;**不创建 `.agent/planning/pending.json` / `gdd-approval` planning pending,不创建审批卡,也不把 Supervisor 或策划子 run 投影为审批等待**。`gdd-approval` pending 与 Supervisor 等待态属于 `M1C-1`,还要受第 13.0 节 `M1C-2a` 验收取证门约束。原 submit 在进入专用分支前已经建立的 generic `game-creator-pending-action.v5` standalone pending 与 `game-creator-provider-action-batch.v4` action batch 必须原样保留,作为后续 receipt/terminal observation 的同 action 恢复锚点;GDD create 成功不等于该 action 已 observed。
1. 解析第 8.2 节 strict input;在项目锁内重读 project identity、策划子 run 与委派根身份、Provider request 所绑定的 session CAS、canonical GDD 链及原 submit 的 generic v5 standalone pending / v4 batch anchors。不信任 Provider payload 中不存在也不允许出现的版本、时间、平台事实或身份;M1B-2 不读取或创建尚未实现的 approval receipt / planning pending。
2. 验证文本上限、轮次、决定状态和 prototype item 一一对应;`decisions` 必须先逐项等于 source session 的完整决定前缀,前缀之后只允许追加 `state=default_pending + answerSource=default + round=0` 的未提问默认决定,任何伪造为用户已确认的额外决定都按 session CAS 冲突拒绝。Runtime 注入固定 platformFacts 和所有 `basis:null`,以当前 durable actionId/裸 action fingerprint 作为 submission identity。
2. 验证文本上限、轮次、决定状态和 prototype item 一一对应;`decisions` 按本次完整 GDD 快照校验,不与旧 session 内容逐项比较。`round=0` 的非首项决定只能是 `answerSource=default`(默认建议)或 `answerSource=user_revision`(审批修改),分别对应允许的状态集合。`user_revision` 还要求当前 session 已有 `lastDecisionRef.action ∈ {revise, reject}`;没有该引用时不得把未确认项标成审批修改。已有 session 创建新的 planning child 时,`repairOfDelegationId` 必须精确等于旧 session 的 `latestDelegationId`;这条 continuation 游标约束在 Provider 请求前生效,防止旧 delivery 重新成为当前分支。Runtime 注入固定 platformFacts 和所有 `basis:null`,以当前 durable actionId/裸 action fingerprint 作为 submission identity。
3. M1B-2 尚无 receipt writer:只要已有任一 GDD,新的不同 submissionId 就返回 `PLAN_PENDING_GDD_EXISTS`;同 submissionId 只允许按历史 binding replay。`M1C-1` 接入有效 approve/revise/reject receipt 后,才把边界扩为“最新版本已有 receipt 才允许下一版本”。
4. 当前 M1B-2 的首次版本固定为 1;未来版本仍只能取最后一个连续有效版本加一,范围 1~128,不允许缺号或扫描任意文件补号。
5. 新提交由 Runtime 生成并冻结 `approvalRequestId/createdAtUtc`,填充全部 durable identity、source session binding 和时间,计算 GDD fingerprint,以第 10.1 节算法 create-only 发布 `gdd.v{N}.json`。同 submissionId replay 必须先找到并严格读取既有 GDD,复用其中 Runtime 生成的版本、request/time 与 identity 后再比较,不能用新时间制造假冲突。
@@ -1449,7 +1441,6 @@ type PlanGddError = {
| 'PLAN_UNSUPPORTED_KNOWLEDGE_BASIS'
| 'PLAN_CORRUPT_AUTHORITY'
| 'PLAN_SESSION_CAS_CONFLICT'
| 'PLAN_SESSION_DECISIONS_MISMATCH'
| 'PLAN_SESSION_RECOVERY_REQUIRED'
| 'PLAN_NEEDS_RECONCILIATION'
| 'PLAN_DURABILITY_FAILED'