@whaletalk/dsh-codex-collab 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +105 -25
- package/codex-plugin/plugins/dsh/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/plugins/dsh/skills/dsh-collab/SKILL.md +63 -3
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/review.mjs +91 -6
- package/codex-plugin/plugins/dsh/skills/dsh-collab/scripts/task.mjs +443 -39
- package/docs/CODEX_DSH_APPROVAL_DELEGATION_PLAN.md +130 -0
- package/docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md +285 -0
- package/docs/CODEX_DSH_DELIVERY_CONTROL_PLAN.md +81 -0
- package/docs/CODEX_DSH_MODEL_ROUTING_PLAN.md +75 -0
- package/docs/DSH_APPROVAL_DELEGATION.md +142 -0
- package/docs/DSH_DELIVERY_M0_EVIDENCE.md +69 -0
- package/docs/DSH_MODEL_ROUTING_M0_EVIDENCE.md +166 -0
- package/harness/audit-tail.mjs +32 -0
- package/harness/codex-approvals.mjs +1078 -0
- package/harness/codex-associations.mjs +39 -7
- package/harness/dsh-bridge.mjs +1950 -191
- package/harness/model-route-host.mjs +530 -0
- package/harness/model-routing.mjs +500 -0
- package/harness/session-observer.mjs +217 -0
- package/harness/session-target.mjs +161 -6
- package/harness/task-table.mjs +6 -4
- package/mcp/dsh-mcp.mjs +474 -12
- package/package.json +2 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Codex 接手 DSH 单次审批:方案与实施计划
|
|
2
|
+
|
|
3
|
+
状态:执行版;M0 已由原 DSH 对话交付(见 `CODEX_DSH_APPROVAL_M0_EVIDENCE.md`),根代理复核后进入 M1/M2。基线:`383528c` / npm `0.1.9`。
|
|
4
|
+
|
|
5
|
+
## 1. 目标与授权
|
|
6
|
+
|
|
7
|
+
用户要求:让原来的 DSH 项目对话继续实现,使 Codex 在授权范围内接手 DSH 执行时的审批,用户离开电脑后任务仍能继续,并由 Codex 验收。
|
|
8
|
+
|
|
9
|
+
交付目标是 **Codex 保持任务活跃时的无人值守审批闭环**。每次只决定当前真实挂起的一个调用,获准后仍由 DSH 原工具执行。超出授权、证据不足、断线、过期时保持人工接管路径。用户缺席不等于授予全机权限。
|
|
10
|
+
|
|
11
|
+
本轮本机验证范围:`E:/11-Github/dsh-codex-collab` 下的本次实现、文件查看/编辑和必要测试;测试临时文件可用已声明的 `E:/07-Workspace_codex/.tmp`。不把路径名单当作自动批准所有命令的依据,必须判断实际行为与副作用。上一轮发布 npm 的授权不成为本轮通用审批规则。
|
|
12
|
+
|
|
13
|
+
本轮不修改全局权限、`approval/policy`、permission preset 或上游 DSH;不读取凭据、不自动处理登录/2FA;不批准任意命令、不发布 npm、不打 tag。新的审批能力缺省关闭,显式按任务启用;原有无审批委托调用保持兼容。
|
|
14
|
+
|
|
15
|
+
## 2. 当前证据与未知项
|
|
16
|
+
|
|
17
|
+
- `mcp/dsh-mcp.mjs` 现有六工具没有审批查询/决策工具,`waitTask()` 仅等待终态或预算到期。
|
|
18
|
+
- `harness/dsh-bridge.mjs` 已使用公开的 `sessionController`、会话 follow、请求/回合关联;`originThreadId` 是关联信息,不是认证凭据。
|
|
19
|
+
- 本机官方安装包 `D:/Program Files (x86)/DSH/resources/app.asar` 中 `@deepseek-ai/dsh-user-approval/lib/index.js:130–187`:公开 `approval` 服务在有效 open turn 中记录 `approval/asked`,经 `approval/request` waterfall 获取结果,再记录 `approval/decided`;唯一许可结果是 `allowed-once`,还有 rejected/cancelled/unavailable;策略 never 在 waterfall 前拒绝。
|
|
20
|
+
- `approval/asked` 有 `id/toolName/callId?/reason?`;**传给 answerer 的 req 不一定含该 id,也不保证含完整参数**,不得杜撰 API 或用最后一条日志猜对应关系。
|
|
21
|
+
- 同包 `@deepseek-ai/dsh-experimental-auto-review/lib/index.js:433–485` 在 tools/pre-execute 中做检查,可返回 ask;Codex 必须能看到真实动作和原检查原因。对实际不可覆盖的拒绝/never 策略保持原语义。
|
|
22
|
+
- 本轮只读抽取的 SDK 证据位于 `E:/07-Workspace_codex/.tmp/dsh-approval-protocol`;它们仅供核实,不复制成新的运行时依赖。
|
|
23
|
+
- M0 已证实 `ctx.on('approval/request', handler, {prepend:true})`、公开 req 六字段及 `tools/pre-execute`/原生 `tool/call` 参数来源。公开 req 不带参数、cwd、turn/step。PTC 内层映射、worker child 身份和真实组合的 scope/时序仍须 native 验证。
|
|
24
|
+
- 空闲后自动唤醒 Codex 的旧 M0 仍 blocked。本方案不把后台 HTTP 监听、收件箱或 MCP notification 写成“已经唤醒 Codex”。
|
|
25
|
+
|
|
26
|
+
## 3. 方案取舍
|
|
27
|
+
|
|
28
|
+
原则:用户范围优先;单次与真实调用绑定;使用公开扩展点;失联不放行;沿用现有关联/锁/审计能力。
|
|
29
|
+
|
|
30
|
+
考虑的方案:
|
|
31
|
+
|
|
32
|
+
| 方案 | 好处 | 限制/结论 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| A. 正式 approval/request answerer + Codex 活跃循环 | 保持 DSH 工具执行和官方审计,支持逐项判断;与现有 origin/task 相接 | 需要 M0 证明顺序/调用证据和正式挂起请求;选择本方案 |
|
|
35
|
+
| B. 改全局 preset / 全部自动允许 | 实现少 | 越出委托范围、失去逐次判断;不采用 |
|
|
36
|
+
| C. UI 自动点批准 / 写会话日志伪造 decided | 可表面解卡 | 无法证明真实请求与单次语义,绕开正式边界;不采用 |
|
|
37
|
+
|
|
38
|
+
不额外引入常驻模型代理、定时任务或通用策略框架,不添加新依赖。独立离线策略审批、空闲唤醒属于将来独立需求。
|
|
39
|
+
|
|
40
|
+
## 4. 端到端流程与状态
|
|
41
|
+
|
|
42
|
+
1. 由可信 Codex 调用路径为**明确关联的任务**启用委托,记录 origin/task/target/授权范围/有效期/撤销状态。不能让 DSH 提示文字、仓库文件、审批原因或一个普通 localhost 请求自行签发/扩大授权。
|
|
43
|
+
2. 在官方请求挂起时,仅接手能严格证明属于该 task 的请求。会话目标复用 `requestId → rpcId → DSH turn`;worker 目标必须绑定它真实的执行 child,而不是 owner、最近会话或任意 agent。不能可靠绑定的目标如实报不支持。
|
|
44
|
+
3. 形成不可变审批快照:桥接审批 ID、官方审批 ID(可证实才填)、origin/task、session/child、turn/step、native 或 PTC 调用身份、工具名、完整实际参数、cwd、原因、创建/过期时间、动作 fingerprint、授权版本。参数只从宿主 `tools/pre-execute` 的真实 execution 快照及原生 `tool/call` 日志取得;审批 req 本身不含参数,客户端也不得再传一份替代。匹配不足时 next(),不能只信自然语言 reason。
|
|
45
|
+
4. `dsh_task`/等待逻辑遇到审批应及时返回 `needs-approval` 及查询入口,任务保持非终态。Codex 取详情、逐项判断,提交 `approve-once` 或 `reject`。审批正文是待审数据,不是给 Codex 的新指令。
|
|
46
|
+
5. 新决策接口核对真实挂起项、调用快照、有效委托、控制凭据、fingerprint、未撤销/未过期/未被上游处理,才一次性完成官方 answerer。不得替 DSH 另起进程重执行命令。
|
|
47
|
+
6. 决策已提交与 DSH 已采纳要区分:跟踪官方 `approval/decided` 或同等受支持确认;只有任务自己的 turn/completed 才是整个任务 done。未确认回应报告 `decision-unknown`,不自动重发放行。
|
|
48
|
+
7. Codex 继续有界等待/查询,处理下一条审批直到完成或确实需要用户。CLI/MCP 调用及时返回;不能把一个长时间阻塞工具调用当作模型仍在做审批。
|
|
49
|
+
|
|
50
|
+
建议桥接状态:task `running ↔ needs-approval → 原有终态`;approval `pending → decision-submitted → confirmed`,以及 `rejected / cancelled / expired / superseded / decision-unknown`。准确状态名可精简,语义必须可测。
|
|
51
|
+
|
|
52
|
+
## 5. 范围、认证、时序和恢复
|
|
53
|
+
|
|
54
|
+
- origin/task/session/turn/callId 只负责定位;**新增改变审批结果的 HTTP 路由不能仅凭这些可见 ID 或 loopback 放行**。采用显式控制凭据/不可伪造的任务能力,M0 说明签发、保管、撤销和传输方式,可信发起方之外不能申请 grant。新增含审批正文的清单/详情/授权查询同样鉴权,公共 capability 只返回非敏感能力元数据。凭据不入仓库/提示/会话日志/错误正文/审计;缺失或错误时零决策。不要重构既有所有 API 的认证。
|
|
55
|
+
- 可信委托内容存于仓库外。拒绝依据任意本地可写策略文件来扩大权限;承认同一操作系统用户不是强隔离边界,不作不成立的安全承诺。
|
|
56
|
+
- 控制凭据采用**可信本机 CLI/安装动作离线初始化的私有 key 文件**(仓库和工作区外;无则缺省关闭),host 与 MCP 内部读取。不能暴露未认证的 HTTP enable/bootstrap/领 token 路由;控制 token 不出现在任何 HTTP 响应或 MCP 工具结果中,也不让模型携带它。任务 grant 必须先验证控制凭据和已绑定任务/明确范围再创建。grantId/版本/指纹是非秘密定位数据,不当作单独认证。轮换/撤销、原子初始化、权限与同用户信任边界写清楚,避免“先凭一个普通 localhost 请求领控制凭据”的循环授权。
|
|
57
|
+
- **认证等价物同样保密**:若服务端把 `sha256(key)` 直接当可接受的 header,完整 hash 就是 bearer 凭据,不能以“只是 hash”为由写入 grant/日志/返回模型。最小方案可让私有 raw key 仅在 MCP/CLI 内部通过认证 header 传输,持久 grant 只保留不可用于该协议登录的单向摘要;不必为了“raw key 绝不上线”发明会泄漏等价密钥的协议。审计默认只记身份和动作指纹,不能用不可靠的正则摘要宣称保证去掉 JSON/命令中的所有秘密。
|
|
58
|
+
- approve-once 不是 session 级许可。原检查/用户策略 never 仍拒绝;未关联请求、不在授权任务的其他审批调用原有 next(),不拦截不吞掉。
|
|
59
|
+
- M0 观察到 UI 的 `PendingApproval.delegate()` 及 `isDelegation→next()`,但根代理复核同文件 `ApprovalPanel` 只渲染“拒绝/允许一次”。**不声称存在用户可点击的“委托”按钮**。已在旧 UI 等待的请求不自动接手;新请求依赖提前安装的 prepend handler。内部异常在安全边界未作决定时交原 next(),不通过 throw 吞掉人工链。
|
|
60
|
+
- 批准需由 Codex 结合真实内容判断;命令前缀、工具名、路径命中、测试名或 reason 中写“安全”均不足以自动放行。脚本副作用、重定向、符号链接/junction、路径穿越和外部网络行为必须纳入判断;缺少证据交人工。
|
|
61
|
+
- 使用有界活跃租约(建议 60 秒,可配置)和审批 TTL;只有通过认证且匹配该委托的 Codex 调用可以续租。旧公开 status 查询、DSH 自己的事件和未认证心跳不能续租。失联或租约到期时不作 approve,通过正式 next/取消机制交还原人工处理链或标记需人工;不得永久挂住 waterfall。等待审批期间应明确处理现有 quiet/total 预算,不能一边保留有效租约、一边误收尾成完成或丢弃活任务。
|
|
62
|
+
- 处理 req.signal、父任务取消/完成、并发请求、插件卸载、人工或其他 answerer 已完成、过期后延迟响应;清理定时器/监听器,一条请求最多完成一次。
|
|
63
|
+
- 决策幂等:同一 key+相同请求/决定只返回原记录;同 key 异文冲突拒绝;不同 key 也不能重复完成已解决的请求。fingerprint 改变/身份歧义则拒绝,需重新审查新请求。
|
|
64
|
+
- 审计记录在用户数据目录,复用 `fs-lock.mjs` 等现有实现,保留 actor=codex-delegate、grant版本、绑定和决定摘要。完整敏感参数不默认落盘;只保留验收需要的脱敏内容/摘要。审计失败不得悄悄放行。
|
|
65
|
+
- 重启可恢复归属和审计;内存 callback 丢失的审批不伪称仍可批准,不依据旧 JSONL 再执行。只有正式当前状态可重新确认同一挂起请求时才恢复可答状态,否则 unknown/需人工。旧 R1 未完成任务恢复 unknown 的语义保持。
|
|
66
|
+
|
|
67
|
+
## 6. 产物与接口建议
|
|
68
|
+
|
|
69
|
+
沿现有风格做小改动,可调整命名但避免大框架:
|
|
70
|
+
|
|
71
|
+
- `harness/codex-approvals.mjs`:绑定/不可变请求/短期委托/状态/幂等/审计,复用现有锁。
|
|
72
|
+
- `harness/dsh-bridge.mjs`:惰性检查官方能力、绑定真实 task、注册/解除正式 answerer,添加局部审批路由和 capability。
|
|
73
|
+
- `mcp/dsh-mcp.mjs`:`dsh_approvals`(待审清单/详情/有界等待)、`dsh_approval_decide`(一次性批准/拒绝);`dsh_task/status` 返回 needs-approval 时不报告终态。
|
|
74
|
+
- CLI 脚本与 `skills/dsh-collab/SKILL.md`:来源显式、最小授权、处理审批后继续原任务,不能因 needs-approval 重发原派活。
|
|
75
|
+
- capabilities 明确 reporting:正式审批可用/原因、需显式启用、active-only、`canWakeIdle:false`;不支持版本/缺凭据/旧 gateway 时零执行、明示不支持。
|
|
76
|
+
- 文档 `docs/DSH_APPROVAL_DELEGATION.md`、README;测试新增专门模块/集成/E2E,保持已有六工具兼容与包契约。
|
|
77
|
+
- 新增工具时同步 tarball/MCP 工具数/CI 断言,不靠删除旧断言或误写固定六工具通过。若发布版本号改变,package、Codex manifest、MCP version 三处一致;本轮不实际发布。
|
|
78
|
+
|
|
79
|
+
## 7. 阶段计划与停止条件
|
|
80
|
+
|
|
81
|
+
### M0:正式接口与真实上下文门槛
|
|
82
|
+
|
|
83
|
+
在仓库中写证据:官方 SDK 版本、精确注册 API/顺序、req 字段、官方结果语义、action 获取和 native/PTC 身份规则、权限/next 行为、控制授权方案、真实 canary 接入方法。证据不足的能力报 false。没有受支持的正式 answerer 或不能证明调用身份时,停在明确阻塞及已有可验证部分,不能造 API 或改上游。
|
|
84
|
+
|
|
85
|
+
### M1:请求与授权核心
|
|
86
|
+
|
|
87
|
+
实现快照、显式任务委托、控制认证、单次状态机、幂等、TTL/租约、最小持久审计与撤销;先给有意义的单元/并发/恢复测试。
|
|
88
|
+
|
|
89
|
+
### M2:接入网关与 Codex 工具
|
|
90
|
+
|
|
91
|
+
正式 hook、关联、needs-approval 返回、查询/决定、CLI 和技能中的继续循环、能力上报;旧路径回归。未实现的 worker/PTC 或旧请求接管清楚报告,不把不支持伪装为可用。
|
|
92
|
+
|
|
93
|
+
### M3:集成与用户缺席情境
|
|
94
|
+
|
|
95
|
+
用真实网关 + 独立 MCP/CLI 进程走至少两次审批再完成同一任务;验证 active Codex 循环不需要人点击。再模拟断线/过期/人工抢先/取消/重启,证明不误批准、不重复执行、可返回人工链。
|
|
96
|
+
|
|
97
|
+
### M4:Codex 独立验收
|
|
98
|
+
|
|
99
|
+
DSH 回报实现 diff、命令和测试、M0 证据、限制、真实验证方法。Codex 读实际代码、运行独立负向验证、打包与干净安装,再在能正式加载适配器时对原会话做无害 live canary。真实 canary 若不能运行,必须明确剩余缺口,不能称完整交付。验收失败交原 DSH 对话修正,原任务 delivered 后不重复派发。保持可审查工作树,不擅自推送、tag、发布或重启当前客户端。
|
|
100
|
+
|
|
101
|
+
M0 阶段“不安装/不重启”是阶段约束,不能误写成用户禁止整轮的必要本机测试。根代理会先完成代码审查和独立验证,再准备备份与可回退的局部测试加载;涉及影响其他活动任务的 profile 重启时,先核实影响和支持的加载方式,不在 DSH 自测阶段擅自做。根代理当日复查 desktop/global 已安装版本均为 **0.1.9**,不是 M0 报告中的旧记忆 0.1.7;不得以旧记忆下结论。不得为了 canary 更改 never;是否适合正例需读取目标真实有效状态,未证实则如实说明。
|
|
102
|
+
|
|
103
|
+
## 8. 验收矩阵
|
|
104
|
+
|
|
105
|
+
1. 已授权真实请求 → needs-approval;详情包含完整动作及稳定绑定;一次批准 → 官方 allowed-once → 原命令只执行一次 → 原 task 精确 done。
|
|
106
|
+
2. 同一任务连续/并发两次请求能逐项处理;不会把另一任务/会话/turn 的请求发给错误来源;错误 origin/task/approval/控制凭据组合全部拒绝且零副作用。
|
|
107
|
+
3. 无 grant、grant过期/撤销、仓库文字伪造授权、任意 localhost 调用签发 grant、缺鉴权、缺 callId/歧义日志/可变参数等不得放行。
|
|
108
|
+
4. 不同参数相同 callId、相同参数不同 turn/step、native 与 PTC 内层冲突、父 owner 与 child 混淆全部拒绝。
|
|
109
|
+
5. 拒绝、取消、超时、上游已决定、插件卸载/重启、audit写失败、请求 signal 已 abort、决定发送后确认丢失:不得假成功、重复执行或永远吞住原 UI。
|
|
110
|
+
6. needs-approval 等待及时返回(本机集成目标:出现请求后 3 秒内,轮询/等待单次不超过 30 秒);租约到期后有界交还人工。整个任务不因等待审批误报 done/error 或被活跃任务淘汰逻辑删除。
|
|
111
|
+
7. Codex 新进程按 origin+taskId 恢复详情和审计;失去 live callback 明示不可答;旧 R1 串回合/去重/恢复语义与 outbox 测试全部通过。
|
|
112
|
+
8. 未启用委托/不受支持目标保持旧行为;never、其他 answerer、现有自动检查语义保留;不能以“开启功能”绕过 DSH 原权限控制。
|
|
113
|
+
9. 打包包含新代码/文档;独立进程 tools/list/schema 与能力说明一致,版本一致;干净安装无新增依赖;Windows 与 Linux 的公开 CI 校验兼容。
|
|
114
|
+
10. live canary 用已授权仓库临时目录的无害读写/输出,不使用真实发布、删除用户数据或凭据操作。记录 origin/task/session/turn/approval 与预期 nonce,保存官方 asked/decided 和任务结果证据;没有人点击仍继续才算通过无人值守情境。
|
|
115
|
+
|
|
116
|
+
## 9. 预演风险与缓解
|
|
117
|
+
|
|
118
|
+
- 串错请求或被文字诱导扩大授权:不变动作+严格多层绑定+独立控制认证+原始参数审查;重点负向测试。
|
|
119
|
+
- Codex 已离线但桥接仍等待:租约、TTL、正式 next/取消、状态与 callback 分离;不能声称 idle wake。
|
|
120
|
+
- 决定已采纳但回应丢失,重发导致重复:原生一次性结果、幂等键、decided 证据、decision-unknown 与不自动重试放行。
|
|
121
|
+
|
|
122
|
+
测试分层:单元覆盖身份/状态/审计;集成走真实 HTTP/MCP/CLI 与可控正式 hook;E2E 覆盖 native/PTC 中已证实支持范围;live 用实际 DSH。观测仅记绑定、阶段、摘要、耗时,不能记录控制凭据或完整敏感命令。
|
|
123
|
+
|
|
124
|
+
## 10. 分工与本轮执行方式
|
|
125
|
+
|
|
126
|
+
采用用户指定的现有 DSH「dsh-codex-collab 项目」会话;Codex 来源为当前发起对话。真实 ID 由本次工具参数与仓库外关联记录携带,不写成发布默认目标或授权名单。DSH 负责实现与自测,Codex 负责独立审查/验收,不新建 DSH 会话。独立原生 reviewer 可审方案或 diff,不改 DSH 的实现文件。
|
|
127
|
+
|
|
128
|
+
本轮按 App 可用的直接方案/评审/DSH 交付方式执行,不启动 OMX tmux、Goal 或额外常驻服务。插件实现不加入调度器。根代理为用户已授权的“DSH 做完后由 Codex 验收”建立仅此对话的临时 App 自动跟进,每 10 分钟检查修正交付,验收完成后停止;该任务级跟进不改变插件 `canWakeIdle:false`,也不能替代通用审批流程的活跃租约。计划不是运行授权凭据;实际 grant 只能由可信发起路径按用户本轮范围创建。
|
|
129
|
+
|
|
130
|
+
原生 researcher/critic 角色启动分别被当前账户的模型可用性拒绝(gpt-5.4-mini / gpt-5.5),没有取得原生评审结论;不宣称通过 Ralplan 共识门槛。替代方式为 DSH 独立审查本方案/实现、Codex 独立检查 DSH 的实现与验收。
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Codex 单次审批委托 · M0 工程审查与官方接口取证
|
|
2
|
+
|
|
3
|
+
- 日期:2026-10-15
|
|
4
|
+
- 仓库:`E:/11-Github/dsh-codex-collab`(基线 `383528c` / npm `0.1.9`)
|
|
5
|
+
- 审查对象:`docs/CODEX_DSH_APPROVAL_DELEGATION_PLAN.md`(本文件只读它,未改动)
|
|
6
|
+
- 本轮动作:**只读取证 + 写本文件**。未改任何生产代码、未安装、未改权限/preset/never、未重启客户端、未读凭据或用户其它会话存储。
|
|
7
|
+
- 证据来源(全部只读):本机官方安装包 `D:/Program Files (x86)/DSH/resources/app.asar`;提取/扫描工具在 `E:/07-Workspace_codex/.tmp/dsh-approval-protocol/`(`scan-approval.mjs`,只读 asar,产物写 `.tmp`)。**不把任何 SDK 文件复制成运行时依赖。**
|
|
8
|
+
|
|
9
|
+
## 0. 结论摘要
|
|
10
|
+
|
|
11
|
+
| # | M0 必答项 | 结论 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| 1 | hook 注册 API/优先级/不吞 next/仅本任务生效 | **可实施**:`ctx.on('approval/request', cb, {prepend:true})`(boolean 简写亦可),注册序执行、不调 `next()` 即否决全链、默认 next=`unavailable`;靠"严格谓词 + 始终 `next()`"不吞人工与其它 answerer |
|
|
14
|
+
| 2 | req 字段/参数/turn·step·callId/官方 id/worker child | **req 无参数**(官方类型只有 agent/toolName/callId?/reason?/displayReason?/signal?);参数须经 `tools/pre-execute`(`callId`+`arguments`)或日志 `tool/call`(`turn/step/callId/arguments`)按 `callId` 关联;官方 id **answerer 永远拿不到**(请求内 `randomUUID()`),只能事后观测日志,**worker child 绑定未证明 → 报不支持** |
|
|
15
|
+
| 3 | 单次 allow 与取消/超时/decided/卸载/已在 UI 等待 | 单次与"由那一次调用消费"是官方语义 ✓;官方**无超时**(UI 可无限等);已在 UI 等待的请求**不能自动接手**——只能人点 UI"委托"或我们 prepend 抢先;卸载由 cordis fiber 自动摘监听(fiber 卸载移除,链上数组副本仍安全) |
|
|
16
|
+
| 4 | 控制授权信任链 | 必需:**控制凭据 + 任务能力**双层;`origin/task/reason/loopback` 一律是定位/文案,不是认证;凭据不入仓库/提示/日志/工具结果;审计只存哈希 |
|
|
17
|
+
| 5 | 无重启/不改 preset 的 live canary | **本轮做不到**(需新宿主插件代码 → 装包+重启 profile);替代边界 = `.tmp` 内用**真实抽出的 cordis/user-approval/scope/sandbox** 跑 native harness;未来真 canary 的加载前提与替代验证在 §3.5 |
|
|
18
|
+
| 6 | 方案必修建议 + M1 拆分 | 6 条必修(§3.6,其中"参数来自正式 req"是**必须改**的错误假设);M1 文件拆分见 §3.6 |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. 取证方法与证据等级
|
|
23
|
+
|
|
24
|
+
- **[V 已验证-官方源码]**:直接读到 asar 内官方 JS 的对应行,语义由代码或 JSDoc 明确写出。
|
|
25
|
+
- **[I 强推断]**:由已验证代码结构必然推出,但未在运行时观察过。
|
|
26
|
+
- **[U 未验证]**:M0 无法证明,需 M1 native harness 或 live canary。
|
|
27
|
+
- 行号引用格式:`包名/lib/文件.js:行`(asar 内路径,形如 `dsh/node_modules/@deepseek-ai/<pkg>/...`)。
|
|
28
|
+
|
|
29
|
+
关键提取物(已留在 `.tmp` 供 Codex 独立复核):
|
|
30
|
+
`dsh-user-approval__lib__index.js`、`dsh-experimental-auto-review__lib__index.js`、`dsh-api-session-controller__lib__index.js`;本轮回执新增扫描脚本 `scan-approval.mjs`(含 `--range`/`--list` 模式)。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. 官方审批接缝(逐项证据)
|
|
35
|
+
|
|
36
|
+
### 2.1 服务时序与结果语义 [V]
|
|
37
|
+
|
|
38
|
+
`@deepseek-ai/dsh-user-approval/lib/index.js`:
|
|
39
|
+
|
|
40
|
+
- **L30–35** `OUTCOMES = ["allowed-once","rejected","cancelled","unavailable"]`;**L37** `APPROVAL_POLICIES = ["ask","never"]`。
|
|
41
|
+
- **L128–144 `request(req)`**:
|
|
42
|
+
1. `session = req.agent.session`(L129)→ **`req.agent.session.id` 是从 req 自身可得的严格会话身份**;
|
|
43
|
+
2. `hasOpenTurn(session)` 不满足即 **throw**(L130,错误文本说明审计对必须被 turn 包住);
|
|
44
|
+
3. `id = ApprovalRequestId(randomUUID())`(L131)→ **官方审批 id 在服务内部生成,answerer 永远拿不到它**;
|
|
45
|
+
4. `session.append("approval/asked", {id, toolName, callId?, reason?})`(L132–137);
|
|
46
|
+
5. `outcome = await this.decide(req, session)`(L138)——**先记 asked,再进 waterfall**(→ 答复者被调用时 `approval/asked` 已在日志里,可事后观测到官方 id);
|
|
47
|
+
6. `session.append("approval/decided", {id, outcome})`(L139–142)→ `outcome` 是唯一可信的采纳证据。
|
|
48
|
+
- **L172–189 `decide(req, session)`**:
|
|
49
|
+
- `signal?.aborted` → `'cancelled'`(L174);
|
|
50
|
+
- `effectivePolicy === 'never'` → `'rejected'`(L175),**在 waterfall 之前**;
|
|
51
|
+
- 否则 `ctx.waterfall(scopeTarget(req.agent, req.agent), "approval/request", req, () => Promise.resolve("unavailable"))`(L176)→ **默认 next 返回 `unavailable`(fail closed)**;
|
|
52
|
+
- 非法返回值或 throw → `'unavailable'`(L176);存在 `signal` 时与之赛跑,先到先得(L177–188)。
|
|
53
|
+
- **L49–56 `hasOpenTurn`**:从 `session.seq-1` 倒序找最近的 `turn/start`(遇 `turn/end` 即止)。
|
|
54
|
+
- 包描述(`package.json:3`):*"one-shot permission decisions … over the approval/request waterfall, **fail-closed by default**"*。
|
|
55
|
+
|
|
56
|
+
**含义**:官方契约只有"这一个动作"的一次性许可;链上任何不回 `next()` 的答复者都会**连内置 `unavailable` 一起否决**,因此我们的答复者必须自己负责"不接管时放行"。
|
|
57
|
+
|
|
58
|
+
### 2.2 公开类型与签名(req 有什么、没有什么)[V]
|
|
59
|
+
|
|
60
|
+
`dsh-tool-cordis/lib/types/api-catalog.js`:
|
|
61
|
+
|
|
62
|
+
- **L4475–4476 `ApprovalRequestEvent`**:`{ agent, toolName, callId?, reason?, displayReason?: {en, [locale]}, signal? }`
|
|
63
|
+
- **L4471–4472 `ApprovalRequest`**:`extends ApprovalRequestEvent`,**字段完全相同**(无参数、无 cwd、无 turn/step)
|
|
64
|
+
- **L3852–3854 官方签名**:
|
|
65
|
+
`'approval/request'(this: Scoped<Agent>, req: ApprovalRequestEvent, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>`
|
|
66
|
+
- **L7552 `ToolExecutionInput`**:`{ callId, rootCallId?, name, schema?, arguments, … }`(**参数在这里**)
|
|
67
|
+
- **L7544 `ToolExecution`**:`extends ToolExecutionInput` + `{ rootCallId, token }`
|
|
68
|
+
- **L4236–4238 `tools/pre-execute` 签名**:`(this: Scoped<ToolRuntime>, exec: ToolExecution, next) => Promise<PreToolDecision>`
|
|
69
|
+
- **L6444 `SessionEventMap`**(同一文件):
|
|
70
|
+
- `'tool/call': { turn, step, callId, name, arguments: string }`
|
|
71
|
+
- `'tool/result': { turn, step, message, error? }`
|
|
72
|
+
- `'turn/start' | 'turn/end {turn, reason}' | 'step/start {turn,step}' | 'step/end {turn,step}'`
|
|
73
|
+
|
|
74
|
+
**结论**:**审批 req 里没有参数**;参数与 turn/step 有两条合法来源(见 §3.2)。
|
|
75
|
+
|
|
76
|
+
### 2.3 注册 API、顺序与优先级 [V]
|
|
77
|
+
|
|
78
|
+
`@deepseek-ai/cordis/lib/index.js`:
|
|
79
|
+
|
|
80
|
+
- **L371–380 `on(name, listener, options)`**:`options` 是对象时透传;**boolean 简写即 `prepend`**(L372);`fiber.assertActive()`;文档 L363–364:"**listener 在 fiber 卸载时自动移除**;fiber 已 dispose 则抛 `INACTIVE_EFFECT`"。
|
|
81
|
+
- **L335–345 `register`**:`options.prepend ? "unshift" : "push"` → **prepend = 数组最前 = 第一个执行**;返回 disposer(effect 标签可诊断)。
|
|
82
|
+
- **L317–325 `waterfall`**:`cbs = dispatch(...)`;`next = () => (cbs.shift() ?? inner)(...args)` → **按注册顺序执行**;JSDoc L307–312:"listeners run outermost-first;**不调用 `next()` 的监听者否决整条链,包括内置行为**";最后一个参数是内置 next。
|
|
83
|
+
- **L258–264 `dispatch`**:先取 `thisArg`(= `scopeTarget(req.agent, req.agent)`),再按 `thisArg[Context.filter]` 过滤 hooks,然后 `map(callback)`(**顺序保持**,是数组副本,shift 不污染注册表)。
|
|
84
|
+
- **`dsh-scope/lib/invariant.js:22`**:`"approval/request": (args) => args[0]["agent"]` → **该事件的 scope 键是请求的 agent**。
|
|
85
|
+
|
|
86
|
+
**回答 M0 #1(可实施部分)**:
|
|
87
|
+
- 注册:`ctx.on("approval/request", handler, { prepend: true })`(挂在宿主 root ctx,用 `ctx.effect(() => disposer)` 生命周期管理)。
|
|
88
|
+
- 优先级:prepend 能保证我们**先于**桌面 UI/ACP 拿到请求;不 prepend 则排在已注册者之后。
|
|
89
|
+
- 不吞 next:**谓词不成立一律 `return next()`**;内部异常也 `catch → return next()`(**不要 throw**:`decide` 的 `.then(_, () => 'unavailable')` 会把 throw 变成 `unavailable`,那会**跳过人工 UI**,比放行更糟)。
|
|
90
|
+
- 仅本任务生效:谓词 = 控制凭据 + 有效任务 grant + **`req.agent.session.id` 等于受托会话** + `req.callId` 存在 + pre-execute 快照存在且指纹一致(见 §3.4)。[I] root 注册会被 scope 过滤保留(ACP/UI 均以 root 注册且能收到请求,实证支持 root hook 可达 [I]);`agent.ctx` 范围注册可作加固,但相对 UI 的顺序仍取决于注册序,故**不依赖它,只依赖严格谓词**。
|
|
91
|
+
|
|
92
|
+
### 2.4 已存在的答复者完整清单与"UI 委托"路径 [V]
|
|
93
|
+
|
|
94
|
+
全 asar 扫描 `approval/request`:**只有两个答案者**(服务侧 `waterfall` 调用点、类型目录、invariant、remote 声明除外):
|
|
95
|
+
|
|
96
|
+
1. **ACP 适配器** `dsh-acp/lib/index.js:1116–1139`:
|
|
97
|
+
```js
|
|
98
|
+
ctx.on("approval/request", (request, next) => {
|
|
99
|
+
const record = ownedRecord(request.agent);
|
|
100
|
+
if (record === void 0 || request.callId === void 0) return next(); // 不是我的会话/无 callId → 放行
|
|
101
|
+
… conn.request(session.requestPermission, {toolCall:{toolCallId:callId}, options:[allow-once, reject-once]})
|
|
102
|
+
.then(({outcome}) => outcome.outcome === "cancelled" ? "cancelled"
|
|
103
|
+
: outcome.optionId === "allow-once" ? "allowed-once" : "rejected");
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
⇒ **官方参考实现也在无 `callId` 时放行 `next()`**;只给 `allow-once`/`reject-once` 两个选项。
|
|
107
|
+
2. **桌面 UI** `dsh-client-ui-approval/lib/client.js:355–357` → `answerApproval`(**L284–310**):
|
|
108
|
+
- `ctx.sessions.scopeOf(owner)` 无会话 → **`return next()`**(L286);
|
|
109
|
+
- 构造 `PendingApproval({toolName, callId?, reason?, displayReason?, signal?})` 并 `await pending.result`(L301)——**等人工选择**;
|
|
110
|
+
- **L303:`if (pending.isDelegation(error)) return await next()`** ⇒ UI 存在"委托"**内部协议路径**(`registerPendingInteraction` 回调在 L294–297 调 `pending.delegate()`)。**修正(Codex 复查)**:同文件渲染层 L118–134 只渲染"拒绝 / 允许一次",**没有已证实的"委托"按钮**——不能把内部协议路径说成人工可点的 UI 按钮;
|
|
111
|
+
- `finally { remove(); completed.resolve() }`(L306–309)。
|
|
112
|
+
- 注册方式 `ctx.remote.$on("approval/request", …)`(L355)⇒ UI 的 hook 经远程通道在宿主 root ctx 注册(早于我们的插件时排在前)。
|
|
113
|
+
- 另有快捷键:`approval.allow`=Enter、`approval.reject`=Esc(L322–341)。
|
|
114
|
+
3. 声明面:`dsh-api-remotes/lib/types/remote-events.js:14` `{ event: 'approval/request', mode: 'waterfall' }`。
|
|
115
|
+
|
|
116
|
+
**回答 M0 #3 中"已在 UI 挂起的旧请求能否接手"**:
|
|
117
|
+
- **不能自动接手**。waterfall 是**每次 `request()` 调用时现场组合**的链条;UI 在链上时它 `await pending.result`,**下游(我们)在它调用 `next()` 之前根本不会被调用**。
|
|
118
|
+
- 合法接手途径只有两条:
|
|
119
|
+
1. 我们**注册在 UI 之前**(`prepend`),对该请求直接决定 → UI 根本不弹(无人值守目标需要这条);
|
|
120
|
+
2. UI 走到其**内部 delegate 协议路径**(`isDelegation → next()`)把请求交棒 → 轮到我们(渲染层无已证实按钮,仅协议可达)。
|
|
121
|
+
- 若 UI 是等待方且无人触发其内部 delegate 路径,请求会**一直挂着**(官方无超时,见 §3.3)。
|
|
122
|
+
|
|
123
|
+
### 2.5 真实 ask 形状(沙箱升级路径)与 reason 的可信度 [V]
|
|
124
|
+
|
|
125
|
+
`dsh-sandbox/lib/index.js`:
|
|
126
|
+
|
|
127
|
+
- **L16–20 JSDoc**:通道是结构型 `EscalationAsk`;**"工具层拥有 agent、call id 和 tool name,并 `close over ctx.approval.request(...)` 把闭包下传"**——即 ask 由工具层发起,参数由工具层提供。
|
|
128
|
+
- **L99–123 `approveEscalation`**(唯一权威顺序,fail-closed):
|
|
129
|
+
- L101:`mode === effectiveMode` → 不需要 ask;
|
|
130
|
+
- L102:非严格更宽 → throw;
|
|
131
|
+
- L103–104:**无 approval 服务或无 agent → throw(fail closed)**;
|
|
132
|
+
- **L105–115 真实 ask**:
|
|
133
|
+
```js
|
|
134
|
+
await approval.approver.request({
|
|
135
|
+
agent, toolName, callId,
|
|
136
|
+
reason: `escalate sandbox to ${mode}: ${justification}`,
|
|
137
|
+
displayReason: { en: …, zh: … },
|
|
138
|
+
...approval.signal ? { signal: approval.signal } : {}
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
- L116–122 结果消费:`allowed-once` → 返回 mode(**由"那一次调用"消费**);`rejected/cancelled/unavailable` → 各自 throw,任务立即失败(fail closed)。
|
|
142
|
+
- 配对校验 `validateEscalationArgs`(L51–55):`sandbox_permissions` 与 `justification` 必须成对且非空。
|
|
143
|
+
- **`reason`/`justification` 来自模型写的文本**(`dsh-tools/lib/types/ptc.js:62`:"Reason this complete program needs wider access, **shown to the user**")⇒ **永远不可作为授权依据**(方案 §5 已要求,证据坐实)。
|
|
144
|
+
|
|
145
|
+
### 2.6 PTC 与 native 走同一道 pre-execute 门 [V]
|
|
146
|
+
|
|
147
|
+
`dsh-tools/lib/types/ptc.js`:
|
|
148
|
+
|
|
149
|
+
- **L61–62**:PTC 工具声明 `sandbox_permissions`(枚举 `ESCALATION_TARGETS`)+ `justification` → **PTC 同样会触发升级 ask**(走 §2.5 同一 `approveEscalation`)。
|
|
150
|
+
- **L81**:*"A sandbox escalation approves **this complete program for one execution only**. **Nested tools retain their own policies and approvals.**"*
|
|
151
|
+
- **L71**:时间预算"includes **nested tool and approval waits**"。
|
|
152
|
+
- **L345 / L504**:*"ordered **pre-execute**"、"**pre-execute waits for this resolution, as under the native**"* ⇒ **PTC 与 native 共用同一道 `tools/pre-execute` 门**,参数快照对两者同样可用 [V 结构 / U 运行时]。
|
|
153
|
+
|
|
154
|
+
**含义**:`tools/pre-execute` 快照钩子对 native 与 PTC 都能拿到 `callId + name + arguments`;但 **PTC 的 callId 嵌套/`rootCallId` 关系与 ask 的对应尚未运行验证 [U]** → M1 前按"参数可捕获、答复者绑定待证"报告。
|
|
155
|
+
|
|
156
|
+
### 2.7 日志字段 → turn/step/参数的关联路径 [V]
|
|
157
|
+
|
|
158
|
+
- `approval/asked` 持久字段:`disposition(["id","toolName"], ["callId","reason"])`(`dsh-session-format-v0-to-v1/lib/index.js:34-35`,`dsh-session-persistence-jsonl/lib/worker.cjs:6666` 同);`approval/decided`: `["id","outcome"]`(同处 :35/:6667)⇒ **日志里没有参数**。
|
|
159
|
+
- `tool/call` 持久事件带 `{turn, step, callId, name, arguments}`(§2.2 L6444)⇒ **`approval/asked.callId → tool/call` 即可取到 turn/step/参数**(参数为序列化 string)。
|
|
160
|
+
- 时序 [V]:`approval/asked` 在 `decide()` 之前 append(§2.1)⇒ **答复者被调用时,asked 已在日志里**;`tool/call` 是否恒早于工具体内 ask ⇒ [U,M1 native 确认]。
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 3. 六项必答结论
|
|
165
|
+
|
|
166
|
+
### 3.1 hook 注册:API、优先级、不吞 next、仅本任务生效
|
|
167
|
+
|
|
168
|
+
- **API**:`ctx.on("approval/request", (req, next) => …, { prepend: true })`(boolean 简写 `true` 等价)[V];用 `ctx.effect(() => disposer)` 管理卸载 [V]。
|
|
169
|
+
- **优先级**:`prepend → unshift → 数组首位 → 最先执行**;否则按注册序(晚注册者靠后)。桌面 UI 经 `ctx.remote.$on` 注册、ACP 在其插件 apply 时注册,均可能早于我们 ⇒ **要无人值守必须 prepend** [V 结构]。
|
|
170
|
+
- **不吞人工 next / 其它 answerer**(三条硬规):
|
|
171
|
+
1. 谓词不成立 → `return next()`(含:无 grant、非受托会话、`callId` 缺失、无 pre-execute 快照、指纹不符、grant 过期/撤销、凭据缺失);
|
|
172
|
+
2. **内部异常一律 `catch → return next()`**,不 throw(throw 会被 `decide` 归一成 `unavailable`,**跳过 UI**)[V 推论];
|
|
173
|
+
3. 永不写 `approval/asked|decided`(自己记日志 = 伪造 decided,方案 §3 C 已否决)、永不 `setApprovalPolicy/setPolicy`(那是 `dsh-permission-presets` 的职责,`dsh-permission-presets/lib/index.js:223`)。
|
|
174
|
+
- **仅对显式关联 task 生效**:谓词以 **`req.agent.session.id`** 为会话身份(从 req 自身取得 [V §2.1]),必须 ∈ 当前持有有效 grant 的受托会话集合;再叠加 origin/task/grant/凭据/指纹。**只靠 scope 注册不够**(root 注册也会收到请求),**只靠谓词又已足够** ⇒ 谓词是唯一信任点,必须有负向测试。
|
|
175
|
+
|
|
176
|
+
### 3.2 req 字段、参数绑定、官方 id、worker child
|
|
177
|
+
|
|
178
|
+
| 问题 | 结论(等级) |
|
|
179
|
+
|---|---|
|
|
180
|
+
| req 真实字段 | `agent, toolName, callId?, reason?, displayReason?, signal?`(无参数/无 cwd/无 turn·step)[V §2.2] |
|
|
181
|
+
| 实际参数从哪来 | ① **`tools/pre-execute` 快照**:`{callId, name, arguments, rootCallId}`,执行前捕获、结构化、native/PTC 同门 [V §2.6];② **日志 `tool/call`** `{turn, step, callId, arguments:string}` 按 `asked.callId` 关联 [V §2.7]。②是①的重启后备,但两者都必须**按 callId 精确匹配**,不得按"最近一次"猜 [V 方案 §4.3] |
|
|
182
|
+
| turn/step 绑定 | 取自 `tool/call.turn/step`;实时可用 `hasOpenTurn` 同款倒序规则(与本仓 R1 已实现的请求→turn 关联一致)[V] |
|
|
183
|
+
| 官方 id 映射 | **answerer 永远拿不到 id**(`randomUUID()` 在 `request()` 内)[V];因 `asked` 先于 `decide` append,可**观测日志**按 `toolName+callId+openTurn` 匹配到官方 id,但这是**推断不是契约** ⇒ 审计里标 `officialId: observed|omitted`,**映射失败就填 omitted,不阻塞决定**(方案 §4.3 "可证实才填"保持,补上实现口径)[V+I] |
|
|
184
|
+
| `callId` 缺失 | 官方类型可选;ACP 与我们都必须 `return next()` [V §2.4] ⇒ 该请求**不支持委托**,报 `unsupported`,不猜 |
|
|
185
|
+
| worker child 绑定 | **未证明 [U]**:worker 目标的执行体是连续子代理,bridge 只有 `childId` 与 owner agent;**没有已验证的 `childId → Agent` 取回路径**,更没有"req.agent 即该 child"的证据。⇒ **M0 判定:worker 目标报不支持**(capabilities 置 false),除非 M1 先证明;session 目标可用 `req.agent.session.id` 严格匹配 [V]。这与方案 §4.2 "证明不了的目标如实报不支持"一致 |
|
|
186
|
+
| PTC | 参数快照可用(同门 pre-execute)[V];**ask↔PTC callId 对应未验证 [U]** ⇒ 先报"参数可捕获、绑定待证" |
|
|
187
|
+
|
|
188
|
+
### 3.3 单次 allow、取消、超时、decided、卸载、UI 已挂起
|
|
189
|
+
|
|
190
|
+
- **单次 [V]**:`allowed-once` 是唯一许可;`approveEscalation` 返回值"consumed by the one call that asked";包描述 "grants apply only to the requested action";PTC 升级"one execution only"。⇒ **没有"批准后续同类调用"的官方语义**,我们的 grant 也不能扩大到未请求动作(方案 §4.5 一致)。
|
|
191
|
+
- **取消 [V]**:`signal.aborted → 'cancelled'`;与 answer 赛跑先到先得。父任务取消/turn 结束传入 signal 即生效;我们 answerer 必须监听 `req.signal` 及时停手并 `next()` 或退出。
|
|
192
|
+
- **超时 [V 缺口]**:**官方没有超时**。UI 可无限等待;我们 answerer 若挂住则整条 ask 挂住。⇒ 方案 §5 的租约/TTL 必须由**我们自己**实现:到期 → `return next()`(交还下游/UI 人工链),下游也没有则得 `unavailable`(fail closed)。**到期绝不能返回 `allowed-once`**,也不能靠 throw(会跳过 UI)。
|
|
193
|
+
- **官方已决定 [V]**:同一次 `request()` 链内只会有一个答复者返回值(链式短路)⇒ 不存在双决定;风险只在**桥接侧状态**(我们以为 pending,官方已 `decided`,例如人工在 UI 回答或 signal 取消)。⇒ 监听 `approval/decided`(含 `asked.id`)做 `superseded/confirmed` 收敛,**决定已提交但未见 decided → `decision-unknown`,不重发放行**(方案 §4.6/§5 保持)。
|
|
194
|
+
- **卸载 [V]**:`ctx.on` 的监听在 **fiber 卸载时自动移除**(cordis L363–364);`dispatch` 已把 callback 数组**复制**,已进入链条的请求仍会被调用 [I]。⇒ 卸载语义:**新请求不再到达我们**(安全);在途回调须在 dispose 后**只 next()/或完成已提交的决定**,并撤销在途 grant(M1 用 `ctx.effect` disposer 收口)。
|
|
195
|
+
- **已在 UI 等待的旧请求 [V]**:**不能自动接手**——要么 prepend 抢先(先于 UI 拿到请求),要么 UI 自己走内部 delegate 协议路径把请求交棒(§2.4;渲染层无已证实按钮)⇒ 方案 §4.1/§5 必须写明这条限制(见 §3.6 修正 #6)。
|
|
196
|
+
- **多请求并发 [I]**:链是每次调用独立组合的;同一时刻多个 ask = 多条链并行,各由自己的 signal 赛跑 ⇒ 需按 (session, callId) 分别建快照,一条一个结论。
|
|
197
|
+
|
|
198
|
+
### 3.4 最小可实施的控制授权信任链
|
|
199
|
+
|
|
200
|
+
威胁模型要点(均被本轮证据坐实):
|
|
201
|
+
- 循环地址**不是认证**:本仓既有路由只用 `isLoopback`(读类接口);方案 §5 已禁新增审批路由仅凭 loopback。
|
|
202
|
+
- `origin/task/sessionId` 是**可见定位符**(会出现在提示、日志、工具结果里),任何本地进程可构造;**不能自签 grant、不能当批准**。
|
|
203
|
+
- `reason` 是**模型写的文本**(§2.5),绝不作为授权输入。
|
|
204
|
+
- `ctx.on('approval/request')` 是**进程全局**接缝,同用户内无隔离。
|
|
205
|
+
- 官方把审计权只交给 `ApprovalService`,答复者只有"返回值"这一条路。
|
|
206
|
+
|
|
207
|
+
推荐链(**签发 → 保管 → 校验 → 撤销**,全部宿主侧,客户端不可伪造):
|
|
208
|
+
|
|
209
|
+
1. **控制凭据(control credential)—— 离线初始化(Codex 修正已并入实现)**
|
|
210
|
+
- 签发:**只由受信任的本机 CLI/安装动作离线初始化**(`dsh-task --approval-key-init`),在**仓库与工作区之外**的用户私有目录原子生成 key 文件(temp+link 语义"只在不存在时创建",`--approval-key-rotate` 才显式轮换且轮换=撤销全部旧 grant)。
|
|
211
|
+
- 缺 key ⇒ **整条审批能力默认关闭**(`capabilities.approvals.enabled=false`,审批路由 401,委托派活零执行拒绝)。
|
|
212
|
+
- **不存在任何未认证的 HTTP enable/bootstrap/领 token 路由**:enable/清单/详情/决定/撤销全部要求 `X-Dsh-Control`;桥接内部读 key 后只传 **SHA-256**(`sha256:<hex>`),**raw token 不出现在任何 HTTP 响应、MCP 工具输入/结果、会话日志、审计正文或错误正文**(审计只记 `credHash` 前缀)。
|
|
213
|
+
- 传输:审批相关路由经 `X-Dsh-Control` 头(**不放 query**,避免进日志/历史);错误正文只回凭据类 reason,不回显收到的值。
|
|
214
|
+
2. **任务能力(task grant)**
|
|
215
|
+
- 签发:控制凭据校验通过 **且** taskId 存在 **且** origin 与该任务登记来源一致 **且** 用户已声明授权范围(显式条目,无通配)才签发。
|
|
216
|
+
- 结构:`{grantId, taskId, originThreadId, scope, sessionIds, version, issuedAt, expiresAt, nonce, credHash, status}`,**不可变**(改范围=新版本新 grant,旧版立即 revoke),落用户数据目录 JSONL(复用 `harness/fs-lock.mjs`)。
|
|
217
|
+
- 租约:默认 60s(方案 §5)。**只有"通过凭据校验且匹配该 grant/task/session 的调用"能续租**(清单/详情/决定接口);旧的公开 status 查询、DSH 自己的事件、任意本地 heartbeat **都不经续租路径** ⇒ 不能靠它们让租约永续,也不能因为 MCP/CLI 在轮询就宣称模型一直活跃(等待本身有界,见 §3.3)。
|
|
218
|
+
3. **决定时校验(唯一真正产生批准的时刻)**:控制凭据 + grant 存在/未撤销/未过期/版本匹配 + **实时桥接快照**(`req.agent.session.id` + `callId` + 参数指纹 SHA-256)全部匹配 + `req.signal` 未 abort + **审计 append 成功(写失败=不决定,fail closed)** ⇒ 才 `return "allowed-once"`。任何一项失败 → `return next()`(人工仍可处理)或按语义 `rejected`(仅当 grant 明确拒绝)。
|
|
219
|
+
4. **撤销**:显式 revoke(grant.status=`revoked`)、grant 过期、任务终态自动失效、控制凭据轮换(删 key → 校验全部失败)。每次决定都重查,不缓存。
|
|
220
|
+
5. **边界诚实声明**:同一操作系统用户仍可读取 `control.key` ⇒ 本链挡的是**网页/远程/提示注入/可见 ID 伪造**与**事后重放**(nonce+指纹+单次状态机),**不是**同用户恶意进程的强隔离。方案 §5 已承诺不作不成立的安全承诺 ⇒ 保持。
|
|
221
|
+
|
|
222
|
+
> 不采用:仅 loopback、仅 origin/task、`dryRun` 探测、提示文字、仓库文件、`reason` 里写"安全"作为授权(全部被 §5 禁止,且本轮证据说明它们都是可伪造输入)。
|
|
223
|
+
|
|
224
|
+
### 3.5 无重启/不改 preset 的 live canary 可行性
|
|
225
|
+
|
|
226
|
+
**结论:本轮做不到真正的 live canary,且不伪造。** 理由:
|
|
227
|
+
|
|
228
|
+
- 新答复者是**宿主侧插件代码**(`dsh-bridge.mjs` 经 `cordis.patch.yml` 以 bundle 装载);替换包内容**不会热重载**(本仓 README 已证:需重启 profile 才跑新 JS 模块);client-plugin HMR 只覆盖"客户端插件 + dev watcher",不适用宿主 bundle。
|
|
229
|
+
- 现装 DSH desktop 插件与 Codex 全局安装:**0.1.9**(Codex 于本轮当日重读文件确认;M0 撰写时引用的 0.1.7 为旧记忆,已作废。安装/加载验证仍由 Codex 在审查后统一处理,本轮未装、未重启)。
|
|
230
|
+
- 读取已有会话的 `approval/asked` 日志做观察 = 读用户其它会话存储 ⇒ **本轮禁止**。
|
|
231
|
+
|
|
232
|
+
**替代验证边界(可实施、且不碰运行中的系统)**:
|
|
233
|
+
1. **Native harness(M1 主证据)**:在 `.tmp` 内用**只读抽出的真实模块**(`@deepseek-ai/cordis`、`dsh-user-approval`、`dsh-scope`、`dsh-sandbox`、必要时 `dsh-tools`)搭最小宿主,装载**我们的 handler**,真实调用 `ApprovalService.request()`/`approveEscalation()`,断言:prepend 抢先、非关联请求 `next()` 链到达 UI 替身、`never` 在 answerer 之前拒绝、signal 中断 → cancelled、卸载后新请求不再到我们、在途请求 dispose 行为、内部异常 → next 而非 unavailable、单次消费、指纹不符拒绝。方法论与 dsh-memory 已验收的 native 测试一致。
|
|
234
|
+
2. **已做的官方侧只读取证**(本文件 §2)+ 未来 M3/M4 的**真实 canary**(需用户授权安装+重启)。
|
|
235
|
+
3. **未来真 canary 的加载前提**(写清楚,供 M3/M4 执行):
|
|
236
|
+
- 用户授权安装更新 bundle + **重启 profile 进程**;
|
|
237
|
+
- 目标会话 `approval/policy` 为 `ask`(**默认值**;`never` 会先于 answerer 拒绝,canary 必然被拒——注意本机当前注入的系统提示即为 never 语句,说明存在 never 会话 [V]);
|
|
238
|
+
- **不改**全局 preset / `approval/policy` / never(走 `dsh-permission-presets` 之外的手动路径,本方案不调 `setPolicy`);
|
|
239
|
+
- 委托经用户显式启用(控制凭据在场);
|
|
240
|
+
- canary 动作 = 授权仓库内**临时目录**的无害读写(要求 `workspace-write` 升级,正触发一次真实 ask),带预期 nonce;
|
|
241
|
+
- 证据 = 官方 `approval/asked`(含预期 toolName/callId)+ `approval/decided{allowed-once}` + 任务完成 + 我方审计,**无人点击**;
|
|
242
|
+
- 不涉及发布、删除用户数据、凭据操作、空闲唤醒(`canWakeIdle:false` 保持)。
|
|
243
|
+
|
|
244
|
+
### 3.6 对方案的必修建议(6 条)与 M1 文件拆分
|
|
245
|
+
|
|
246
|
+
**必修**:
|
|
247
|
+
|
|
248
|
+
1. **参数来源必须改写**(方案 §4.3/§2 目前写"参数来自正式 req/执行日志"):`ApprovalRequest` **不含参数**。改为:参数只来自 ① 宿主侧 `tools/pre-execute` 快照(`callId→{name,arguments,rootCallId}`,执行前、结构化、native/PTC 同门),② 日志 `tool/call` 按 `asked.callId` 关联的序列化 `arguments`+`turn/step`;**决定接口绝不接受客户端提交的参数副本**;`callId` 缺失或两侧都不匹配 → 不支持委托(`next()`)。
|
|
249
|
+
2. **"注册顺序与公开优先级未证实"应删除**,换成已证事实(§2.3)+ M1 native 确认项(真实 profile 中 UI/ACP/我们的相对顺序只作确认,不作假设)。
|
|
250
|
+
3. **启用路径具体化**(§4.1 "可信 Codex 调用路径"):写出控制凭据的一次性签发流程与"不得靠 loopback/ID/提示文字"的机检点;`grantVersion` 随授权范围变更而增,旧版即刻 revoke。
|
|
251
|
+
4. **worker 目标能力如实置 false**(§4.2):M0 未证明 `childId→Agent`;capabilities 里写 `session: supported, worker: unsupported(reason), ptc: params-only`;PTC 的 callId 嵌套绑定列为 M1 native 待证项。
|
|
252
|
+
5. **官方 id 口径**(§4.3):`officialId` 只能事后观测(`asked` 先于 `decide` append),审计字段 `observed|omitted`,**不得因缺官方 id 而阻塞决定,也不得自己写 asked/decided**。
|
|
253
|
+
6. **把"UI 委托"写进流程与风险表**(§4/§9):无人值守依赖 prepend;有人在场时仅当 UI 走到其**内部** delegate 协议路径才会转交(渲染层无已证实按钮);**已在 UI 等待的请求不可自动接管**;官方无超时 ⇒ 我方租约到期必须 `next()` 交还,不能 hang、不能 throw、不能凭超时批准。
|
|
254
|
+
|
|
255
|
+
**M1 文件拆分(可实施、无新依赖)**:
|
|
256
|
+
|
|
257
|
+
| 文件 | 职责 | 关键导出 |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `harness/codex-approvals.mjs`(新,纯逻辑) | grant 签发/撤销/租约、`callId` 快照表、指纹计算、单次状态机(`pending→decision-submitted→confirmed` 及 `rejected/cancelled/expired/superseded/decision-unknown`)、幂等键、审计 JSONL(复用 `fs-lock.mjs`)、控制凭据哈希校验 | `openApprovalStore()`, `issueGrant()`, `revokeGrant()`, `snapshotCall()`, `verifyDecisionKey()`, `appendAudit()` |
|
|
260
|
+
| `harness/dsh-bridge.mjs`(改,接线) | `ctx.effect` 注册 prepend answerer(谓词+`next()` 兜底)、`tools/pre-execute` 快照钩子、`/api/dsh-bridge/approvals`(list/detail/decide,`X-Dsh-Control` 校验)、任务状态加 `needs-approval`、capabilities | `approvals` capability、`originBinding` 不变 |
|
|
261
|
+
| `mcp/dsh-mcp.mjs`(改) | 新工具 `dsh_approvals`(清单/详情/有界等待)、`dsh_approval_decide`(`approve-once`/`reject`,带凭据);`dsh_task_status` 见 `needs-approval` 不报终态 | **CI 工具数断言必须同步改**(现断言"恰好 6",计划 §6 已要求同步而非删除) |
|
|
262
|
+
| `codex-plugin/.../task.mjs` + `SKILL.md`(改) | `--approvals`/`--approve`/`--reject`;来源显式;needs-approval 后继续**同一** task,不重发派活 | 退出码新增 `needs-approval` 映射 |
|
|
263
|
+
| 测试 | `test/codex-approvals.test.mjs`(状态机/幂等/审计/租约/撤销)、`test/approval-native.test.mjs`(抽出真实 cordis+user-approval+sandbox 驱动)、`test/approval-e2e.test.mjs`(真实 HTTP+MCP+CLI 两次审批)、`test/dsh-bridge.test.mjs` 回归 | 保持既有 117 项与包契约 |
|
|
264
|
+
| 文档 | `docs/DSH_APPROVAL_DELEGATION.md`(新)+ 本证据文件 + README 简述 | 不改 M0/M1 计划原文 |
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## 4. 未验证 / 需 M1 native harness 确认(诚实清单)
|
|
269
|
+
|
|
270
|
+
1. 真实 profile 中 UI、ACP、我们三者相对注册顺序(prepend 是否足够,需要实跑确认)。
|
|
271
|
+
2. root 注册 vs `agent.ctx` 注册在 `Context.filter` 下的保留集(推断 root 可达,未实跑)。
|
|
272
|
+
3. `resolveAgent` 取回的对象与 `req.agent` 的同一性(更稳妥的判据已改用 `req.agent.session.id`,见 §3.1)。
|
|
273
|
+
4. `tool/call` 是否恒早于工具体内 ask(若否,参数须只靠 pre-execute 快照)。
|
|
274
|
+
5. PTC 的 callId/rootCallId 与 ask 的对应关系。
|
|
275
|
+
6. fiber 卸载后**在途**链条回调的实际行为(数组副本推断 [I],需实跑)。
|
|
276
|
+
7. 同一请求被多个答案者并发到达时的 `dispatch` 实测顺序(prepend 挤压 UI 的真实时序)。
|
|
277
|
+
8. 官方 id 从日志观测的匹配在"同 turn 同 toolName 多调用"下的消歧(需 `callId` 必须存在的强制检查)。
|
|
278
|
+
|
|
279
|
+
## 5. 本轮边界声明
|
|
280
|
+
|
|
281
|
+
- 只读:官方 asar、方案文件;**未改生产代码、未改 `.codex-plugin`、未动 `approval/preset/never`、未重启客户端/DSH、未发布、未打 tag、未推送**。
|
|
282
|
+
- 未读取凭据/登录文件/用户其它会话存储(观察受限,见 §3.5)。
|
|
283
|
+
- 新增文件:**仅本文件** `docs/CODEX_DSH_APPROVAL_M0_EVIDENCE.md`;方案文件 `docs/CODEX_DSH_APPROVAL_DELEGATION_PLAN.md` 原样保留未改。
|
|
284
|
+
- 工具产物:`E:/07-Workspace_codex/.tmp/dsh-approval-protocol/scan-approval.mjs`(只读扫描器,非运行时依赖)。
|
|
285
|
+
- 空闲自动唤醒:仍 **blocked**(`canWakeIdle:false`);本方案不声称 MCP/收件箱/HTTP 监听能唤醒模型。
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Codex 决定 DSH 指令排队或插入:方案与实施计划
|
|
2
|
+
|
|
3
|
+
状态:执行方案。用户明确允许 Codex 按任务关系决定新指令等待当前任务完成,或中途插入;沿用原 DSH 项目对话实施,Codex 独立验收。前一审批功能已通过159项回归及独立真实 DSH profile 验证;它是本次必须保留的未提交基线。
|
|
4
|
+
|
|
5
|
+
## 目标与用户行为
|
|
6
|
+
|
|
7
|
+
- `queue`:新任务先被可靠接收,当前任务完成后再执行;忙不等于错误。每个任务拥有独立 taskId、来源、请求回合和结果。
|
|
8
|
+
- `steer`:补充或纠正当前任务,在 DSH 官方支持的下一步边界插入。补充拥有唯一回执 ID、父任务和自身输入证据;最终执行结果归父任务,不伪装成独立回答。
|
|
9
|
+
- 由 Codex 在工具调用时明确选择模式。默认 queue;用户指定的方式优先。已获本次用户授权后,Codex 可在同一目标/任务范围内选择 steer,不必每次询问。
|
|
10
|
+
- `wait` 只控制调用方是否等待返回,与 DSH 指令投递模式分开。不得把“不等待工具响应”当作已经排队,也不得把“插入”解释成取消/杀掉正在执行的工具。
|
|
11
|
+
|
|
12
|
+
## 当前代码与官方证据
|
|
13
|
+
|
|
14
|
+
1. 现有 MCP/CLI 已有 queue/steer,但 `dsh-bridge.mjs` 对来源绑定的 steer 直接 `origin-steer-unsupported`,对同会话任何在途任务直接 `session-busy`。
|
|
15
|
+
2. `sessionTargets` 以sessionId为键,只能保存一个观察记录;不能直接删除busy检查,否则后来的记录覆盖原任务并破坏审批归属。
|
|
16
|
+
3. 当前单request tracker遇到本身rpcId就绑定其所在turn。同turn两个rpcId会共享turn/end及输出,不能声明它们有两个独立结果。Root独立只读子代理还复现了重放缺陷:B首遍AFTER,重读完整日志变AFTER|BEFORE,原因是绑定前忽略的事件未记去重。
|
|
17
|
+
4. 本机官方 `@deepseek-ai/dsh-api-session-controller/lib/index.js:850–883`:prompt保留requestId为user source.rpcId;mode steer调用agent.steer,否则followup;接受响应只表示接收。
|
|
18
|
+
5. 官方 `dsh-agent-loop/lib/index.js:800–813`:followup写next-turn,steer写next-step;没有在这里cancel/abort正在执行的工具。`claim():103–109` 会取全部next-step,**next-turn每次只取一条**。优先复用官方原生队列,不另建持久消息代理或数据库。
|
|
19
|
+
6. 官方prompt内部存在异步resolve/admission,尚未证明提供原子的expected-turn条件。必须处理父回合恰好结束的竞态;不能把帮助里的mode字段当作原子保证。
|
|
20
|
+
|
|
21
|
+
## 设计约束
|
|
22
|
+
|
|
23
|
+
### A. 真队列与唯一执行结果归属
|
|
24
|
+
|
|
25
|
+
- 优先调用已证实的原生queue,每次独立requestId。支持同一session多个来源明确的在途任务,以taskId保存各自观察记录;状态/停止事件只按各自rpcId→turn/end结算。
|
|
26
|
+
- 若底层版本不能证明逐条next-turn,则在M0如实报告或用最小串行调度适配,不偷换成同turn多任务共同done。
|
|
27
|
+
- 同session的投递接收顺序需可靠;有必要只给短暂prompt admission加锁,不用锁阻塞完整执行,也不全局串行不同session。
|
|
28
|
+
- 旧的无来源/不可精确关联模式继续保持保护;不得为新session能力全局移除worker/reviewer桶忙锁。
|
|
29
|
+
- “已接受/排队中/正在执行/等待审批/完成/未确认”分开,向Codex及时回显真实状态。队列位置不确定时不猜包含人类输入在内的全局名次。
|
|
30
|
+
- 全会话running/idle事件只触发重新观察,不能把所有B/C标成running或done;每个任务开始和结算仍需自身rpcId/turn证据。
|
|
31
|
+
- SDK已接受的queued指令网关重启后仍可能执行;沿用unknown+resend:false,不因恢复丢内存而重新投递。
|
|
32
|
+
|
|
33
|
+
### B. steer作为明确父任务的补充回执
|
|
34
|
+
|
|
35
|
+
- 继续使用显式 `deliver:'steer'`,增加明确父任务定位(建议parentTaskId,必要时expectedTurn)。父任务必须属于相同origin、profile、session,且有真实当前开放回合证据。不得按“最近任务/最老任务”推断父任务。
|
|
36
|
+
- 回执独立taskId/requestId、parentTaskId、requestedMode、receiptState、requestSeq/observedTurn等证据持久化。接收不等于被模型读入,更不等于已执行完成。
|
|
37
|
+
- 同父任务支持多次补充,各自rpcId可确认。回执查询指向父任务最终结果,声明resultScope=parent-task或同等语义,不能把父任务完整输出再冒充补充的独立结果。
|
|
38
|
+
- steer的wait只等待自身接收/官方观察回执(单次有界,不超过30s),不复用“独立任务等最终结果”的循环;要执行结果时显式查询父task。回执只消费自己的user/message,不累计该共享回合的助手文本。
|
|
39
|
+
- 持久化分别保存“回执已观察”与“父执行状态”;重启父task变unknown不抹去回执已观察的事实,已观察回执也不能证明父任务已经done。未观察但SDK已接受仍不重发。
|
|
40
|
+
- 若父任务仍排队、已终态、来源错误、回合不明,执行前拒绝且零prompt。跨来源指令不能改变另一来源正在运行的父任务。
|
|
41
|
+
- 对父任务刚结束的竞态:若能执行前阻止则拒绝;若官方已经接受但实际落入另一回合,保留真实回执/observedTurn并明确未确认或偏离目标,不宣称插入成功,不静默改模式,不重发。不能要求不存在的SDK原子接口,更不能取消用户回合来制造插入。
|
|
42
|
+
- 不支持对不可证明父任务的非托管/冷会话任意插入;明确报不支持。以后可另做有证据的扩展。
|
|
43
|
+
|
|
44
|
+
### C. 保留已通过的审批隔离
|
|
45
|
+
|
|
46
|
+
- 多个queued任务共用session后,审批归属只能由**当前真实open turn对应的唯一queue执行任务**确定。steer回执绝不成为审批owner、结果owner或独立lease owner;即使S1/S2与A同turn,也只有A是执行owner。旧代码sessionTargets.get(sessionId)等单记录假设须逐一替换,不能改成任选一条活task。
|
|
47
|
+
- 排队后B的grant不能批准正在跑的A;A完成撤销/结算不得清掉B的pending;只关闭对应task的挂起项。
|
|
48
|
+
- 收口只删除自己的观察记录;pending同时匹配sessionId/taskId,必要时grantId。回执收口不触碰父pending。需验证A迟到的decided、B新ask和S1已观察交叠仍不串线。
|
|
49
|
+
- steer回执不另建、更不扩大父任务grant;原始授权范围仍由Codex按真实动作判断。新的指令文本本身不是新权限。
|
|
50
|
+
- 插入不会自动批准/拒绝旧pending,也不改全局never/preset。若工具正等审批,下一步插入可能要等该真实工具结束;状态要准确,不能谎称插入已立即执行。
|
|
51
|
+
- 长排队的lease/TTL仍真实有效、由匹配的活跃认证调用维持;过期不自动复活/重新签发。queued状态纳入活任务计数、防淘汰及客户端等待。
|
|
52
|
+
|
|
53
|
+
### D. 客户端、能力与兼容
|
|
54
|
+
|
|
55
|
+
- HTTP/MCP/CLI/DSH原生collab工具及项目技能统一契约,保留`--steer`兼容,补显式父任务参数;默认queue。
|
|
56
|
+
- 客户端先只读核验新投递能力(不能靠0.1.9版本号猜)。旧网关无法处理新父任务/回执语义时零派活拒绝,不能忽略新字段后继续执行。
|
|
57
|
+
- origin/parent/turn只是定位。新增有来源steer会改变现有任务,复用既有离线控制key的内部HTTP鉴权(MCP/CLI自动带头,不让模型传key);缺凭据零插入。旧的非委托queue保持原行为,无来源legacy路径不能用于绕过新的父任务保护。不要另造领token或通用权限框架。
|
|
58
|
+
- 人类给出任务后,Codex选择:独立后续工作或依赖前一结果→queue;同一父任务范围内的纠错/补充→steer。服务器不通过关键词猜急迫程度,不新增常驻模型审批者。
|
|
59
|
+
- 继续显式传当前origin;每次响应严格回显,等待/详情/结果不串来源。保存父任务/回执关系和事件证据,异常或断线不自动重发。
|
|
60
|
+
|
|
61
|
+
## 阶段与产物
|
|
62
|
+
|
|
63
|
+
1. **D0官方契约证据**:写`docs/DSH_DELIVERY_M0_EVIDENCE.md`,核实原生队列顺序、steer消费时机、父回合竞态、source.rpcId和SDK支持能力。用真实SDK组件做可复现契约测试;不冒充完整Desktop。若证据支持,继续实施,无需等人工确认。
|
|
64
|
+
2. **D1状态/存储**:最小改造多任务观察及不可改投的父/回执关系;修复新场景所暴露的回放顺序/去重问题;保留旧R1unknown和精确结果语义。
|
|
65
|
+
3. **D2接线**:gateway、MCP、CLI、native工具、技能和使用文档同步;审批选取与task粒度收口正确。
|
|
66
|
+
4. **D3自测交付**:按下面矩阵做行为回归,重跑原159项,不以改断言取消隔离边界。原先“所有origin steer拒绝”的断言改为“无有效父/无能力拒绝;有效父按回执协议接受”。保持可审查diff,不提交/推送/发包。
|
|
67
|
+
5. **D4Root独立验收**:读相对本次冻结基线的真实差异;独立负例、完整回归、pack/干净安装,再在独立临时真实DSH profile中完成queue与steer正负例。只有最终产物实际通过才验收;回归失败交原对话修。
|
|
68
|
+
|
|
69
|
+
## 必测矩阵
|
|
70
|
+
|
|
71
|
+
- A运行中queue B/C:即时接收并可查询、按官方FIFO独立回合执行;各自task/RPC/结果不覆盖,A失败或审批时B不误结算。
|
|
72
|
+
- A运行中steer S1/S2:不取消A;官方日志出现各自rpcId且同父turn;父结果唯一,补充回执准确关联,重读/快照/重复事件不乱序或重复。
|
|
73
|
+
- 另一origin/profile/session、已结束/排队父任务、错误expectedTurn、未知父任务、旧网关:零投递或在已接收竞态时如实未确认,不偷读别人的结果。
|
|
74
|
+
- 既有A审批挂起、B排队且有grant、A被steer:B授权不能决定A;插入不绕过审批;父任务收口不影响B的回调/状态。
|
|
75
|
+
- 不同session并行;worker/reviewer忙锁不退化;queued任务不被淘汰。队列项/回执若不能逐请求安全撤回则明确不支持,不能用session cancel撤它或取消父任务。保留原单任务force-cancel;多任务情况下只在能证明不会误取消B/C时允许取消当前A,否则拒绝这个不安全组合。A已结束而B刚运行时不可cancel B来回应旧A的取消请求。
|
|
76
|
+
- 接收后确认丢失/网关重启、steer落错回合、客户端断线:持久关系可查,回调不伪恢复,不自动重发,不通过最近助手文本判完成。
|
|
77
|
+
- CLI和MCP都实测queue/steer,不只测协议字段/计数。最终真实profile证明A未完成时新queue被接受,steer在同turn被消费,queue在后续turn执行;记录官方事件、唯一nonce和准确结果。
|
|
78
|
+
|
|
79
|
+
## 执行边界
|
|
80
|
+
|
|
81
|
+
生产实现由原DSH项目会话负责,Root负责方案与独立验收。先备份当前未提交的159测试审批基线,不能用HEAD恢复覆盖它。只用精确edit/write改UTF-8文本,禁止PowerShell文本管道重编码。不开新编码会话,不加依赖,不读/复制凭据,不改上游SDK,不改全局权限/preset或原never,不重启已有活动客户端,不commit/push/tag/npm发布。必要真机验收仅用独立临时profile,验证后清理自建profile/key,保留证据。自动跟进若用于本次长任务须在验收完成后停止,不能声称插件已能idle wake。
|