收紧策划 continuation 游标防止旧 delivery 越权
新增 planning session latestDelegationId 直接后继校验,阻止旧 delivery 另起分支 保留用户修订周期内质量返工的 user_revision 语义并更新提交门注释 补充 continuation 边界回归与 Fast GDD 技术决策文档
This commit is contained in:
+65
-6
@@ -25,6 +25,25 @@ fn plan_coordinator_error(kind: &str, detail: impl AsRef<str>) -> String {
|
||||
format!("{kind}: {}", detail.as_ref())
|
||||
}
|
||||
|
||||
fn validate_plan_continuation_parent<'a>(
|
||||
latest_delegation_id: &str,
|
||||
delivery: &'a StaticDelegateDeliveryRecord,
|
||||
) -> Result<&'a str, String> {
|
||||
let original_id = delivery.repair_of_delegation_id.as_deref().ok_or_else(|| {
|
||||
plan_coordinator_error(
|
||||
"PLAN_ACTIVE_RUN_EXISTS",
|
||||
"已有 planning session 时不能创建第二条根 delegation",
|
||||
)
|
||||
})?;
|
||||
if latest_delegation_id != original_id {
|
||||
return Err(plan_coordinator_error(
|
||||
"PLAN_NEEDS_RECONCILIATION",
|
||||
"planning continuation 必须直接继承当前 session 的 latest delegation",
|
||||
));
|
||||
}
|
||||
Ok(original_id)
|
||||
}
|
||||
|
||||
fn plan_session_successor_base(previous: &PlanSessionV1) -> Result<PlanSessionV1, String> {
|
||||
let mut next = previous.clone();
|
||||
next.session_revision = previous.session_revision.checked_add(1).ok_or_else(|| {
|
||||
@@ -230,6 +249,45 @@ mod option_label_delimiter_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod planning_continuation_parent_tests {
|
||||
use super::*;
|
||||
|
||||
fn delivery(repair_of_delegation_id: Option<&str>) -> StaticDelegateDeliveryRecord {
|
||||
new_static_delegate_delivery_with_contract(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"supervisor-session",
|
||||
"supervisor-run",
|
||||
"delegate-action",
|
||||
"current-delivery",
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
"planning-session",
|
||||
"planning-run",
|
||||
&[],
|
||||
&[],
|
||||
repair_of_delegation_id,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuation_must_extend_the_session_cursor() {
|
||||
let continuation = delivery(Some("older-delivery"));
|
||||
let error = validate_plan_continuation_parent("current-delivery", &continuation)
|
||||
.expect_err("older delivery must not become the current planning branch");
|
||||
assert!(error.contains("latest delegation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuation_accepts_the_current_session_cursor() {
|
||||
let continuation = delivery(Some("current-delivery"));
|
||||
assert_eq!(
|
||||
validate_plan_continuation_parent("current-delivery", &continuation)
|
||||
.expect("current delivery is a valid continuation"),
|
||||
"current-delivery"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_option_label_matches_answer(label: &str, normalized_answer: &str) -> bool {
|
||||
label == normalized_answer
|
||||
}
|
||||
@@ -584,12 +642,13 @@ pub(crate) fn ensure_plan_session_for_planning_child_task_at_locked(
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
let original_id = delivery.repair_of_delegation_id.as_deref().ok_or_else(|| {
|
||||
plan_coordinator_error(
|
||||
"PLAN_ACTIVE_RUN_EXISTS",
|
||||
"已有 planning session 时不能创建第二条根 delegation",
|
||||
)
|
||||
})?;
|
||||
// `latest_delegation_id` is the planning session's single continuation
|
||||
// cursor. A new child must extend that cursor directly; otherwise a
|
||||
// Supervisor can select an older claimed delivery and make an unrelated
|
||||
// branch look like the current session. Keep this check here, after the
|
||||
// exact-task replay fast path above, so replaying an already projected
|
||||
// child remains idempotent.
|
||||
let original_id = validate_plan_continuation_parent(&previous.latest_delegation_id, &delivery)?;
|
||||
let deliveries = list_static_delegate_deliveries_at(root)?;
|
||||
if static_delegate_lineage_contains_unknown_contract_status(
|
||||
&deliveries,
|
||||
|
||||
@@ -1189,9 +1189,10 @@ fn validate_current_session_cas(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `user_revision` 只证明审批意见,不证明澄清。首次 collecting、澄清续跑和提交前
|
||||
/// 质量返工的 session 都没有 revise/reject `lastDecisionRef`;合法续跑会把该引用
|
||||
/// 带到新的 collecting successor 上。replay 不走这里。
|
||||
/// `user_revision` 只证明审批意见,不证明澄清。首次 collecting、澄清续跑和没有待处理
|
||||
/// 用户修订的普通质量返工 session 都没有 revise/reject `lastDecisionRef`;用户修订周期
|
||||
/// 内的 continuation(包括其质量返工)会把该引用带到新的 collecting successor 上。
|
||||
/// continuation 的直接父边由 planning coordinator 校验,replay 不走这里。
|
||||
fn validate_user_revision_requires_approval_decision(
|
||||
session: &PlanSessionV1,
|
||||
input: &PlanSubmitGddInputV1,
|
||||
|
||||
@@ -24,6 +24,14 @@
|
||||
- 验证方式:首次 collecting 带 invented-confirmation 必须拒绝且不落 GDD;reject continuation 再交 `user_revision` 的 v2 仍成功。
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。
|
||||
|
||||
## 2026-08-28 planning continuation 必须沿当前 delivery 游标推进
|
||||
|
||||
- 背景:`lastDecisionRef.action=revise/reject` 在用户修订后的质量返工中必须继续有效,但仅凭该历史指针无法证明当前 `agent.delegate` 选择的是本次 planning session 的当前分支。
|
||||
- 决策:不新增用户修订授权字段,也不在 `plan.submit_gdd` 重复遍历 approval receipt/GDD lineage。已有 planning session 创建新 child 时,`repairOfDelegationId` 必须直接等于旧 session 的 `latestDelegationId`;不一致即在 Provider 启动前以 `PLAN_NEEDS_RECONCILIATION` 拒绝。合法用户修订及其后质量返工继续保留 `lastDecisionRef`,成功提交新的 GDD 后仍由 submit successor 清理该指针。
|
||||
- 影响范围:`planning_coordinator.rs` continuation 投影门;Fast GDD 技术方案第 8.2 节和提交步骤;不改变静态委派通用返工合同或 `PlanSessionV1` schema。
|
||||
- 验证方式:新增当前游标 continuation 正向/旧 delivery 负向回归;CI 继续验证首次伪造 `user_revision` 拒绝、用户修订后质量返工提交成功及现有澄清/返工 lineage。
|
||||
- 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_coordinator.rs`。
|
||||
|
||||
## 2026-08-27 退款 emergency spool 容量溢出保持可恢复
|
||||
## 2026-08-27 退款 emergency spool 容量溢出保持可恢复
|
||||
|
||||
|
||||
@@ -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 是当前 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`。该闸只作用于新版本 create,同 `submissionId` replay 不重判。唯一固定的 `confirmed + user_freeform + round=0` 是 Runtime 创建的 `initial-request`。
|
||||
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`
|
||||
|
||||
@@ -1161,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` 按本次完整 GDD 快照校验,不与旧 session 内容逐项比较。`round=0` 的非首项决定只能是 `answerSource=default`(默认建议)或 `answerSource=user_revision`(审批修改),分别对应允许的状态集合。`user_revision` 还要求当前 session 已有 `lastDecisionRef.action ∈ {revise, reject}`;没有该引用时不得把未确认项标成审批修改。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 后再比较,不能用新时间制造假冲突。
|
||||
|
||||
Reference in New Issue
Block a user