来源:[PR #7782 review r4413108889](https://github.com/makecindy/cindy/pull/7782#discussion_r4413108889)。源码调用链核对成立;尚未实现修复或运行并发复现。原 PR 按明确文件范围登记本 issue,不把关闭讨论当作缺口消失。 ## 问题 自主本机 `schedule_resume` 的许可确认展示并绑定已存定义,但最后一次 Host 快照复核发生在调用 scheduler handler 前。`Scheduler.resume(id)` 在 `serializeScheduleMutation` 队列中调用 `resumeUnlocked`,后者读取并激活届时最新定义,没有校验本次获准的定义。 确定的时序: 1. 已暂停调度为命令 A。 2. 另一个写入经 `updateFromCurrent(id, async buildPatch)` 进入同一 per-schedule 队列,异步稳定化/构造命令 B,尚未提交。 3. 用户允许恢复 A;Host 的锁外快照检查仍读到 A,因此通过。 4. `schedule_resume` handler 调用 `scheduler.resume(id)`,排在更新后。 5. 前方更新提交 B;`resumeUnlocked` 读到 B,将其激活。后续触发执行 B,用户实际确认的是 A。 证据入口: - `apps/desktop/src/main/maker-ipc/taskActionHost.ts`:`scope.assertCurrent` 的保存定义复核。 - `packages/lizi-mcps/src/scheduler/resume.ts`:`scheduler.resume(id)` 未传批准定义/guard。 - `packages/maker-scheduler/src/engine/scheduler.ts`:`updateFromCurrent` 的异步 buildPatch 与 `resume`/`resumeUnlocked` 的同队列先后关系。 ## 修复约束 将批准定义或验证 guard 传到 scheduler,在同一串行 mutation 边界内、读取当前值之后且激活之前验证。普通无 guard 的 UI/既有调用保持兼容;不能通过恢复自主白名单、禁用恢复或破坏现有时间计算来规避。guard 不应重新取得同一条调度 mutation 锁,否则会死锁。锁外重复查询和只覆盖 Agent 调用方的互斥都不够。 ## 最小验收 利用可控 Promise 阻塞真实 `updateFromCurrent` 的 buildPatch,不用真实延时:在 A 仍持久化时让许可复核成功并排入 resume,再释放 B 更新。修复后旧许可必须失效且调度保持暂停,B 不执行。另验证无并发恢复、对 B 的新许可恢复、拒绝、普通旧调用兼容及 resume 的 nextFireAt/interval 语义保留。 该问题已登记但未修复;不得据此宣称 PR #7782 已达到完整的“确认与最终 mutation 原子绑定”。
Activity
dash-s-cindy commentedon Oct 29, 2026
判断
更新:提报者补充了基于
updateFromCurrent异步构建与resume排队的确定并发时序及可控 Promise 验收方案。定时调度从暂停恢复为活跃状态时,确保系统最终激活的执行指令与用户在授权卡上确认的定义严格一致,是保障伙伴自主权限契约闭环的核心安全边界。进一步核对发现,现有的 Host 准入层快照复核(
taskActionHost.ts)发生在调用 scheduler handler 之前,处在调度引擎的串行修改锁(serializeScheduleMutation)之外。若前序更新已进入同一 schedule 队列排队构造新定义,宿主锁外复核仍会读取旧定义并放行,随后队列内的resumeUnlocked无条件读取并激活新定义,导致未经用户复核的指令被直接激活(TOCTOU)。该缺口跨越调度引擎与 MCP 接口,且提报者已给出清晰的调用时序与最小验证思路,值得推进。行动
首要步骤(核心调度与 MCP 开发):
packages/maker-scheduler/src/engine/scheduler.ts中扩展resume与resumeUnlocked方法,支持可选守卫参数(如opts?: { assertCurrent?: (current: Schedule) => void | Promise<void> });serializeScheduleMutation的串行临界区内部、读取当前存储行之后且更新status: 'active'之前执行assertCurrent复核;若目标定义的关键字段与期望快照不一致,抛出异常中断恢复并维持原有的paused状态;守卫严禁重入同一 schedule 锁以防死锁,未传opts时对存量 UI 及既有调用保持完全兼容;packages/lizi-mcps/src/scheduler/resume.ts与apps/desktop/src/main/maker-ipc/taskActionHost.ts之间打通快照上下文,将授权卡捕获的已批准定义指纹或守卫透传给scheduler.resume;packages/maker-scheduler/src/__tests__/scheduler.test.ts中补充用例:利用可控 Promise 阻塞updateFromCurrent的buildPatch,模拟在定义 A 仍持久化时排入带 A 断言的resume,随后释放 B 更新,验证resume抛错阻断、调度保持paused且 B 不执行;同时验证无并发恢复、新许可恢复、拒绝、普通旧调用兼容及nextFireAt/intervalMs时间重算语义保留。opts的普通恢复保持完全兼容。后续集成(仅当首要步骤引擎接口就绪且 PR fix(teammates): 以少数风险确认替代自主白名单并显示授权状态 #7782 授权卡合并后,桌面端开发):
关键证据与技术依据
apps/desktop/src/main/maker-ipc/taskActionHost.ts的scope.assertCurrent仅在准入层检查,未持有目标调度的串行队列;而packages/maker-scheduler/src/engine/scheduler.ts:1588的resume通过serializeScheduleMutation(id)串行排队,其内部的resumeUnlocked仅无条件await this.get(id)后将状态写为active,未接收任何获准定义凭据。updateFromCurrent(id, async buildPatch)(L1379)已在serializeScheduleMutation锁内执行异步回调;当buildPatch在途未提交时,锁外查询仍为旧状态 A,放行后的resume紧随其后排队;一旦前序提交 B,后续resumeUnlocked便直接激活未经批准的 B。相关记录