dsh-crwu-workbench 0.0.16 → 0.0.37

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 CHANGED
@@ -7,6 +7,720 @@
7
7
  `cordis_define` + `cordis_run` 装配,版本号用 DSH 的 `pkg-N`);它已在本仓收尾时删除
8
8
  (见 `0.0.1` 一节),下面 `legacy · pkg-43` 及更早的记录是它的历史。
9
9
 
10
+ ## package · 0.0.37 · 2026-09-30 · fix · 沙箱起不了进程不再被说成「脚本运行时不可用」
11
+
12
+ 真机闭环(0.0.36 的控制探测跑出来的):
13
+
14
+ ```text
15
+ 控制探测(纯 PowerShell 命令)**同样失败**(exitCode 3221225794)
16
+ → 连 pwsh 自己都起不来:本机沙箱后端(ACL restricted-token runner)的问题
17
+ ```
18
+
19
+ 也就是说这台机器的 `workspace-write` 下**任何进程都跑不起来**,插件的输出采集兼容层
20
+ (子进程重定向)在原理上就不可能生效 —— 做重定向的那个 pwsh 自己都没起来。
21
+
22
+ - `PythonRuntimeView` 新增 `blockedBySandbox`:启动失败指纹命中**且控制探测同样失败**时为 true。
23
+ - `audit-start` 的门禁第一句据此分流:沙箱坏 → 「本机受限沙箱起不了任何进程(部署侧问题)」;
24
+ 真的缺运行时 / 缺包 → 仍然是「DSH 脚本运行时不可用」。两者处置完全不同,不许混成一句。
25
+
26
+ ## package · 0.0.36 · 2026-09-30 · fix · 受限沙箱里"进程起不来"不再被说成「DSH Python 不可用」
27
+
28
+ 真机现场(Windows,0.0.35 打出来的结构化事实就成了判据):
29
+
30
+ ```text
31
+ DSH 脚本运行时不可用,已终止本次审核:DSH Python 无法执行:没有版本输出
32
+ (exitCode 3221225794 · 沙箱 执行器默认 → workspace-write)
33
+ ```
34
+
35
+ `3221225794 = 0xC0000142 = STATUS_DLL_INIT_FAILED`,而 `denied` / `runnerFailed` 都还是
36
+ false —— DSH 把它当成一次**普通的非零退出**。于是"沙箱里进程起不来"被说成了"Python 不可用",
37
+ 两种处置完全不同的现场长得一模一样。
38
+
39
+ - 新增 `looksLikeProcessStartFailure()`(认 0xC0000142,无符号与 int32 两种视图)+
40
+ **控制探测**:命中该指纹时再跑一条**纯 PowerShell** 命令,据此把两件事分开并给出处置 ——
41
+ ① 控制探测也失败 ⇒ 本机沙箱后端(ACL restricted-token runner)连 pwsh 都起不来,
42
+ **不是**「DSH Python 不可用」,换运行时没用;② 控制探测成功 ⇒ 只有 native 子进程在初始化阶段退出,
43
+ 是那层捕获兼容层还不够。结论进 `runtime.error`,结构化事实进日志。
44
+ 给出的处置走**部署侧**(DSH 版本 / 沙箱 runner / ACL)—— 插件把审核根钉死在 `workspace-write`
45
+ + `approval: never`,且在 auto / 完全权限的父会话下拒绝发起审核,所以"切完全权限"不是退路。
46
+ - 捕获层补上**第三个句柄**:早先只重定向 stdout/stderr,stdin 仍然继承 DSH 的管道 ——
47
+ 而这层兼容层的全部前提是"子进程一个 DSH 句柄都不继承"。现在内层命令写成
48
+ `$null | & 'exe' args > out 2> err`(pwsh 自己建一根空管道给子进程,立刻 EOF)。
49
+ Host 侧、子代理提示词与技能文档三处同步。
50
+
51
+ ## package · 0.0.35 · 2026-09-30 · fix · Windows 原生命令捕获收口 + 技能脚本执行进 Host(协议 25)
52
+
53
+ ### feat · `crwu_run_python_script`:把技能脚本的执行收进 Host(协议 25)
54
+
55
+ 子代理原先自己拼 `& 'python.exe' script.py`,而**模型可见的 pwsh 工具与插件的 `ctx.shell`
56
+ 是同一套 Windows sandbox** —— 于是子代理的 Python 调用照样命中受限沙箱下 native 子进程的
57
+ 管道缺陷(`0xC0000142` / `EACCES`)。提示词里那份 `Invoke-DshPython` 只是"请照做",
58
+ 不保证被执行,也不带退出码与沙箱事实。
59
+
60
+ - 新增 Host Tool `crwu_run_python_script`(宿主操作 `python.script.run`:来源只给 `audit-tool`、
61
+ **不提权**)。模型只提交 `caseDir` / `script`(**案例目录内**的相对路径)/ `scriptArgs`:
62
+ 路径由 Host 用 `joinLocalPath` 拼,绝对路径、`..`、空串一律拒绝;参数逐个作为 argv 传入,
63
+ 不经过任何 shell 解析。
64
+ - 命令经**同一条 `ctx.shell` seam** 出去,所以自动拿到 `windowsCaptureCommand` 那层
65
+ **只在受限沙箱生效**的临时文件捕获 —— 无需模型照做,也无需第二套执行通道。
66
+ - 返回里带 `exitCode` / `stdout` / `stderr` / `truncated` / `timedOut` 与
67
+ `sandbox{requested,resolved,ran,denied,runnerFailed}`,以及本次用的 Python 路径与版本;
68
+ DSH Python 解析不到时如实回 `capability-gap`,**绝不**改成系统解释器。
69
+ - 审核指令改为**首选**这个 Tool,原来的 PowerShell 包装器降级成"Tool 不可用时的退路"。
70
+
71
+ ### fix · Windows 原生命令捕获收敛成唯一入口 + 探测失败带结构化事实
72
+
73
+ - 新增 `windowsCaptureCommand(command, mode)`:**形状**(只有 `shellInvoke()` 产出的
74
+ `& 'exe' args` 才命中那个缺陷)与**沙箱模式**(只有真跑受限沙箱才需要捕获)一起判。
75
+ `crwu` / `dws` / `ossutil` / DSH Python 的每一条命令都经由 `ctx.shell` 那个 seam 走到它,
76
+ 调用点不需要、也不允许自己决定要不要捕获 —— "统一走文件捕获执行器"只有这一处判据。
77
+ - DSH Python 版本探测失败时把**结构化事实**一起带进错误:`exitCode`、请求与实际的沙箱模式、
78
+ 是否被沙箱拒绝、执行器是否没能启动。Windows 受限沙箱的 `0xC0000142` 与"Python 没装"
79
+ 处置完全不同(换采集方式 vs 换运行时),只报一句"无法执行"会把两件事混起来。
80
+
81
+ ### fix · Windows 原生命令捕获不再套在提权调用上(凭据读不到的真因)
82
+
83
+ 现场(用户报):**同一台 Windows 上 `main` 能读到氚云与钉钉凭据,这个分支读不到** ——
84
+ 扫了码也不行,面板显示「未绑定 / 未知 / 未登录」,看起来像"凭据没写进去"。
85
+
86
+ - **根因**:`capturePowerShellNativeCommand` 是按**命令形状**(`& '…'`)无条件套上去的,可它
87
+ 解决的是**受限沙箱**下的 native-child DLL 初始化失败(0xC0000142)。而 `dsh-pwsh-sandbox`
88
+ 的 `execute()` 对 `danger-full-access` 直接 `super.execute()`、**不套 restricted-token runner**
89
+ —— 那个前提根本不存在。于是所有**提权**命令(`crwu h3yun session status` /
90
+ `dws auth status` / `dws doctor` / `ossutil …`,全都是 `danger-full-access`)也被套上了,
91
+ 而捕获必须经过 PowerShell 的**文本层**(`>` → `Out-File`):Windows PowerShell 5.1 默认写
92
+ UTF-16LE,CLI 的 JSON 就成了 `{\0"\0…`,解析失败 → 「未绑定 / 未知」。
93
+ 不套捕获时 native 子进程**直接继承 DSH 的管道句柄**,原始字节原样过去,不经过任何编码
94
+ —— 这正是 `main` 的形状。
95
+ - **为什么 CI 没拦住**:`tests/windows/powershell-contract.test.mjs` 的 Windows provider 用的是
96
+ **pwsh 7**(默认 UTF-8 无 BOM),而且只断言纯 ASCII 载荷 —— 两个条件恰好把这个缺陷挡在视野外。
97
+ 现在补了一条**非 ASCII JSON 逐字节往返**的真 shell 用例,并把「文本层显式钉成 UTF-8 +
98
+ 从字节里剥 BOM」写进两个包装器(Host 侧 + 子代理提示词里那份),让它们即使在 5.1 上也不改字节。
99
+ - **修法**:新增 `nativeCaptureNeeded(mode)` —— 只有**非** `danger-full-access`(真跑受限沙箱,
100
+ 或没声明策略而落到执行器部署默认)才上捕获。提权调用回到 `main` 的命令形状。
101
+
102
+ ### fix · 「授权收据读不出来」不再显示成「需要授权」(协议 24 → 25)
103
+
104
+ 现场(「授权成功、界面永远停在需要授权」,2026-09-30 复查):磁盘上的
105
+ `~/.dsh/crwu-workbench.json` 里躺着一份**合法**收据(`schemaVersion: 1` + 逐字同序的五项能力),
106
+ 而「账号连接」里的氚云与钉钉两行都显示「需要授权」,插件因此拒绝一切本机凭据访问。
107
+
108
+ - **先纠正一条推断**:这不是"读被沙箱拒了"。DSH 的 `dsh-fs-sandbox` 只在 `writeText` /
109
+ `editText` 上做可写根围栏,源码原话是 *"Reads pass through untouched: every mode permits
110
+ reading."* —— 本地实测也印证:会话策略 `workspace-write`(工作区在仓库下)时读
111
+ `~/.dsh/crwu-workbench.json` 一切正常,`env.localAccess.state` 就是 `granted`。
112
+ 所以"读收据要与写对称地声明提权"并不是这里缺的那一块(fs 的读接口也**没有**逐次策略参数)。
113
+ - **真正的缺陷**:`ConfigRead` 只有一个 `ok` 布尔,于是**五种完全不同的事实**在授权层眼里
114
+ 长得一样 —— 文件不存在、位置不是普通文件、路径解析 / stat / 读文本抛错、内容解析不出来。
115
+ 它们全被折叠成 `missing`,界面于是显示「需要授权」并把员工指向"再授权一次";而读失败时
116
+ 写盘(同样是读-改-写)根本落不了盘。现在逐种都有名字(`absent` / `corrupt` / `no-fs` /
117
+ `no-home` / `not-a-file` / `resolve` / `stat` / `read`),授权层把 **`missing`(真的没有
118
+ 收据)** 与 **`unreadable`(读不出来)** 分成两句话。
119
+ - 主目录未知时不再拼 `~/.dsh/crwu-workbench.json` 这个字面量:`ctx.fs.resolve()` 不展开 `~`,
120
+ 它会变成**相对于会话 cwd** 的路径,于是"主目录探不到"会伪装成"从没授权过"。
121
+ - 解析前先剥掉 UTF-8 BOM(**防御性**):`JSON.parse` 见到 U+FEFF 会直接抛错,而在这里抛错
122
+ 会被折叠成"没有授权"。DSH 自带的本地 fs 用 `TextDecoder`(默认剥 BOM),但 fs 后端并不
123
+ 保证都这么做。
124
+ - `env` / 界面:`localAccess.state` 新增 `unreadable`;氚云、钉钉、OSS、iFinD 的占位事实从
125
+ 「需要授权」改为「授权状态读不出来 + 宿主给的原因」,`credentialsConsent` 归 `invalid`,
126
+ issue 的 owner 归 `system`("把这条原因发给维护者,不是重新授权");授权卡不再对被读不出来的
127
+ 收据显示"首次使用请允许一次"那段介绍。协议 +1 的理由就是这条:旧客户端读到 `unreadable`
128
+ 会当成 `missing`,把员工送回那条走不通的路。
129
+
130
+ ### fix · Windows `workspace-write` 下宿主与审核子代理的 Python 原生命令兼容
131
+
132
+ - Host 的统一 `ctx.shell` 执行入口现在只对 Windows PowerShell 原生命令启用临时文件捕获,
133
+ 再按原始 stdout/stderr 字节转发并保留退出码;macOS / Linux 命令字符串保持不变。
134
+ - 审核子代理提示词和 `crwu-audit` 编排契约增加同一套 `Invoke-DshPython` 包装器,材料准备、
135
+ 复核与交付脚本不再直接继承受限 PowerShell 的管道句柄。
136
+ - 该修复不放宽 `workspace-write`,也不回退系统 Python;Windows 原生 PowerShell 合同继续由
137
+ `tests/windows/powershell-contract.test.mjs` 在 Windows CI 验证。
138
+
139
+ ## package · 0.0.34 · 2026-09-30 · fix · 审核根绑到已选工作空间 + 快照写入带会话策略(协议 24)
140
+
141
+
142
+
143
+ 现场:点「AI 审核」后,侧栏里新会话出现在**未分组**下,而不是环境信息里选定的工作空间下。
144
+
145
+ - **根因**:DSH 有两条硬规则把"会话 cwd"同时当成两件事,而我们只满足了一件:
146
+ 1. `SandboxPolicyService.resolve()`:*"**A session cwd is its workspace-write boundary**"* —— 边界就是 cwd;
147
+ 2. `Workspace.attachSession()`:`if (cwd !== this.record.path) throw …` —— 挂到工作空间下要求
148
+ cwd **逐字等于**工作空间路径。
149
+ 我们原先用 `agents.create({ meta: { cwd: 案例目录 } })` 建根,cwd 与工作空间路径不等 →
150
+ `attachSession` 必然抛错 → 只能落「未分组」。而当时那处 `catch` 只往 `notes` 里塞了一句
151
+ "会话已建,但没能挂到工作空间",界面上看不到 —— 所以看起来像"工作空间没定位到"。
152
+ - **正解**(与 DSH 自己创建"工作空间下的新会话"完全同一条口径,也是本插件"与 DeepSeek 讨论"
153
+ 那条链路一直在用的:客户端 `sessions.create({ workspaceId })` →
154
+ `cwd = workspace.path` → `ensureSession` → `attachSession`):
155
+ **审核根的 cwd = 已选工作空间**。子会话继承父会话的 cwd,于是整棵审核树都在工作空间下。
156
+ - **跨案例隔离没有放松,只是换了承担者**:`requireAuditScope` 仍然要求每个案例内 Tool 的
157
+ `caseDir` 与本轮记录的 `casePath` **规范解析后精确相等**(工作空间根、兄弟案例、案例子目录
158
+ 一律拒绝)—— 这条判据不随 cwd 变化,是跨案例的真正边界。文档里把这两件事分开写清楚了:
159
+ **会话边界回答"这个进程能写哪儿",案例 scope 回答"这次审核被允许碰哪个案例"**。
160
+ - 顺带把那个静默降级也修了:`attachSession` 失败不再只进 `notes`(见下条留档)。
161
+
162
+ ### fix · 三个输入快照 JSON 写不进去(`FS_SANDBOX_DENIED`)
163
+
164
+ `mkdir 输入快照` 修好之后,下一步又断在这里:`ctx.fs.writeText(target, content)` **不带策略**,
165
+ 而 `dsh-fs-sandbox` 的 `checkedTarget()` 是
166
+ `const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()` —— 没有会话就落到**部署默认**
167
+ (进程 cwd),于是往案例目录里写文件被自家沙箱拒:
168
+ `cannot write "…": file access denied under workspace-write mode`(`FS_SANDBOX_DENIED`)。
169
+ 审核停在「未创建子代理」。
170
+
171
+ 修法与 0.0.33 的 shell 侧完全对称,并且**只留一个解析口**:
172
+ - 新增 `host/access/caller-policy.ts` 的 `callerSessionPolicy(ctx, session)`(从 Broker 里提出来,
173
+ 现在 shell 与 fs 两侧共用同一处判据);
174
+ - 新增 `case-files.ts` 的 `writeCaseText(...)`:解析目标 → 带策略写入(第 5 个参数)→ **回读核对**;
175
+ 失败按**结构化错误码**归因(`FS_SANDBOX_DENIED` → sandbox,`EACCES`/`EPERM` → permission,
176
+ 其余 infrastructure),不回退到文本猜测;
177
+ - 三个写入点全部改走它:输入快照的三个 JSON(`bootstrap.ts`)、知识库清单(`knowledge.ts`)、
178
+ 钉钉通知幂等状态(`dingtalk.ts`)。
179
+
180
+ ### feat · 审核发起失败**全部留档**(`state.lastAuditFailure`)
181
+
182
+ 真机连挂四次,每次失败详情只存在于那一次 RPC 的返回里,进程一重启就没了 —— 只能靠"复现 + 读代码"
183
+ 倒推。现在 `auditStart` 的**每一条失败路径**(同一个闭包,漏不掉)都会把结构化事实落盘并在宿主日志
184
+ 留一份:阶段(工作空间 / 案例目录 / 审核根会话 / 输入快照 / …)、`errorKind`(照抄工具回报的
185
+ 结构化归因)、Host 算出来的 `caseDir`、`attemptId`、时刻、以及所有 `notes`(含"没能挂到工作空间"
186
+ 这类原先被吞掉的降级)。发起成功时清空。**不含命令原文与凭据**。
187
+
188
+ ### test · 覆盖
189
+
190
+ - `host-audit-root`:**替身照 DSH 的判据拦人** —— `attachSession` 现在会像真实现一样校验
191
+ `cwd === workspace.path`,cwd 写错就抛。用缺陷注入验过:把根的 cwd 改回案例目录,4 条用例立刻变红
192
+ (改之前它们全绿 —— 这正是这个 bug 溜到真机的原因)。
193
+ - `host-case-files`:`writeCaseText` 两个分支(不带会话 → `FS_SANDBOX_DENIED` + 归因 sandbox +
194
+ 一个字都没落盘;带会话 → 写入 + 回读一致 + 请求里带着案例边界)。
195
+ - `host-tools`:集成层的 fs 替身也换成**按策略拦写**的版本,断言三个 JSON 真的落盘、且没有被拒的写入。
196
+ - `host-audit-lifecycle`:失败留档(阶段/归因/案例目录/notes)与"发起成功清空"。
197
+ - 三条都用缺陷注入验过会红。
198
+
199
+ ## package · 0.0.33 · 2026-09-30 · fix · 非特权命令带**调用方会话的策略**(0.0.32 修错了地方)
200
+
201
+ ### fix · 同一个报错又出现了一次:这次定位到真机制
202
+
203
+ 0.0.32 之后真机**原样复现**同一条失败:
204
+
205
+ ```text
206
+ 创建快照目录失败:mkdir: <案例目录>/输入快照: Operation not permitted
207
+ (操作 system.case-file.write · 来源 audit-tool · 解析为 workspace-write · 实际 workspace-write · 沙箱拒绝=是)
208
+ ```
209
+
210
+ - **0.0.32 的修法是无效的**:它把"调用方的 `ctx`"传进 Broker。但 DSH 的执行器
211
+ **不持有会话**(`dsh-sandbox-policy` 原话:*executors and providers remain session-free*)——
212
+ 换 `ctx` 只是换个地方 `ctx.get('shell')`,拿到的还是同一个执行器,请求里没带 `sandboxPolicy`
213
+ 时它照样用**部署默认**(`workspace-write` + 配置兜底根 = 进程 cwd)。
214
+ - **正解**:像 DSH 自带的 bash / fs 那样,**调用方把自己的会话策略算出来放进请求**。
215
+ - `BrokerShellOptions.session`(替换掉 0.0.32 的 `scope`):**不是权限开关**,提权仍只由
216
+ `privileged` 决定;非特权操作由 Broker 用 `ctx.sandboxPolicy.resolve({ session })` 解析,
217
+ 作为 `sandboxPolicy` 随请求下发;
218
+ - `runShell` / `startShell` 新增 `sandboxPolicy` 选项(`shell/run.ts`),请求模式照旧记进诊断
219
+ (`requested` 不再是空串);
220
+ - 案例内的调用点(`crwu_audit_case_bootstrap` / `crwu_audit_knowledge_materialize`)传
221
+ `exec.agent.session`(新助手 `callerSession(exec)`)。
222
+ - **替身也要按策略拦人**:新增 `tests/helpers/fs-sandbox-stub.mjs` —— 复刻"缺策略 → 部署默认 →
223
+ 写被拒",并只拦**写**(DSH 的可写根管的正是写,只读探测照常回话)。
224
+ 以前那种"永远成功"的 shell 替身正是这两个缺陷在单测里全绿的原因。
225
+ - 失败文案现在带**插件版本**(`… · 插件 pkg-0.0.33`):真机排障第一件事是"装的是哪一份产物",
226
+ 而源码检出形态的客户端徽章只显示 `dev`。
227
+
228
+ ### test · 覆盖
229
+
230
+ - 单元(`host-case-files.test.mjs`)与集成(`host-tools.test.mjs`,真调 `crwu_audit_case_bootstrap`)各一条:
231
+ 不带会话 → 复现真机那句话(且**真的**被沙箱拦下);带会话 → 请求里带对边界、快照三个 JSON 落盘。
232
+ - 两条都用**缺陷注入**验过(去掉 Broker 的会话策略、去掉调用点的 `session`,各自立刻变红)。
233
+
234
+ ## package · 0.0.32 · 2026-09-30 · fix · 案例内的非特权命令按**调用者的会话作用域**执行(协议不变)
235
+
236
+ ### fix · 输入快照目录建不出来:`Operation not permitted(沙箱拒绝=是)`
237
+
238
+ 真机验收第一轮就抓到(案例目录本身已经建出来了 —— §一 的修复是生效的):
239
+
240
+ ```text
241
+ 输入快照交接未完成,已终止本次审核(未创建子代理):输入快照交接失败(infrastructure):
242
+ 创建快照目录失败:mkdir: <案例目录>/输入快照: Operation not permitted
243
+ (操作 system.case-file.write · 来源 audit-tool · 解析为 workspace-write · 实际 workspace-write · 沙箱拒绝=是)
244
+ ```
245
+
246
+ - 根因:**Broker 自己持有的是插件级 `ctx`,它没有会话**。非特权命令(案例目录之内的建/删/探测)
247
+ 不带 `sandboxPolicy`,沙箱边界由"执行它那个 ctx"解析出来的会话决定 —— 插件级 ctx 解析到的是
248
+ **部署默认**(`workspace-write` + 配置里的兜底根,即进程 cwd),而不是调用方那条会话的 cwd。
249
+ 案例目录恰恰就是审核根会话的 cwd,于是"写自己所在的目录"被自家沙箱拒了。
250
+ 这与我上一版写下的注释("子会话的沙箱边界本来就是那个目录")不符:那句话只在**命令真的跑在
251
+ 子会话作用域里**时才成立,而经 Broker 执行时并没有。
252
+ - 修法(保持"提权只由操作表决定"这条口径不变):`BrokerShellOptions` 新增 `scope?: Context` ——
253
+ **不是权限开关**,提权仍由 `privileged` 决定、模型与客户端都碰不到它;它只回答"非特权命令落在
254
+ 谁的沙箱解析里",与 `tools/types.ts` 的 `toolContext()` 同一条口径。`ensureDirectory` /
255
+ `removeFileIfExists` / 案例内探测新增 `scope` 透传,`crwu_audit_case_bootstrap` 与
256
+ `crwu_audit_knowledge_materialize` 把**调用者 Agent 的 ctx** 传下来。
257
+ - **协议不变(23)**:操作集、字段与语义都没变,变的是 Host 内部"这条命令按谁的作用域执行"。
258
+ 版本号 +1 只为让你一眼看出装的是修好的那份产物。
259
+
260
+ ### test · 覆盖
261
+
262
+ - 单元:两个 ctx(插件级 / 调用者作用域)各一个 shell,断言案例内命令**只**落在调用者作用域上。
263
+ - 集成(`crwu_audit_case_bootstrap`):③ 不带作用域 → 复现真机那句话(`创建快照目录失败` +
264
+ `system.case-file.write` + `沙箱拒绝=是`,且一个目录都不建);④ 带作用域 → 快照三个 JSON 落盘。
265
+ - 两条都用**缺陷注入**验过:把 `scope` 摘掉(Broker 侧与 bootstrap 侧各一次)立刻变红,还原即绿。
266
+
267
+ ## package · 0.0.31 · 2026-09-30 · feat · 报告讨论会话的受限材料范围(协议 23)
268
+
269
+ ### feat · 讨论会话也能取材料,但只取"登记那一刻的那批附件"
270
+
271
+ - 现场:案例目录创建修好之后,**报告讨论会话**里调 `crwu_h3yun_file_get` 仍然一件材料都取不回来 ——
272
+ 它是一条普通顶层会话,没有审核记录,而附件下载原先只认审核 scope(「调用者不在进行中的审核里」)。
273
+ - 修法**不是**让它复用审核 scope(那是扩大边界:审核 scope 绑着一条正在跑的子会话),
274
+ 而是给它一份**自己的、更窄的**范围:
275
+ - 新增 Host 操作 `discussion-material-open`(协议 23,操作清单 38 → 39);
276
+ - 客户端在 `ensureDiscussion`(新建**或**恢复)之后、发 kickoff 之前调;只提交
277
+ `sessionId` / `seqNo` / `objectId`;
278
+ - Host 负责:拒绝子代理会话 → 用 `caseDirOf` 算出 `<已选工作空间>/<流水号>` →
279
+ **自己按 `objectId` 取一次记录并核对 `ObjectId`/`SeqNo`**(只看附件清单的话,
280
+ `seqNo=A, objectId=B` 会把 B 的白名单登记成 A 的案例目录)→ 重新取一次附件清单 →
281
+ 把这一批 `fileId` 记进**进程内**白名单(12 小时 TTL、最多 32 条、`use()` 推最近使用时刻);
282
+ - 回传 `caseDir` 与附件清单(`fileId` / `fileName` / `localName` / `fileSize` / `nameTotal`)。
283
+ - 工具侧:`requireAuditScope` 之上新增 **`requireMaterialScope`** —— 先看审核子会话 scope
284
+ (有就以它为准,边界更窄),否则看讨论注册表(案例目录与白名单全部来自 Host 记录)。
285
+ `crwu_h3yun_file_get` 改用它;**普通顶层对话仍然拒绝**。
286
+ - 讨论会话**不是氚云查询入口**:`crwu_h3yun_record_get` / `crwu_h3yun_files_list` 对
287
+ **已登记**的讨论会话一律拒绝(`crwu-h3yun-query` 技能依赖的"普通会话可按 objectId 查记录"
288
+ 这条既有能力**保持不变** —— 收窄的只是讨论会话)。
289
+ - 提示词:kickoff 里新增「本次登记的材料」一段(`fileId` ↔ 落盘名成对给出,同名附件标 `同名 N 件`),
290
+ 取数规则改成"落盘名必须用这一段给出的名字"。报告讨论与审核结果分析共用这一段规则,
291
+ 所以**两条会话都登记**(否则分析会话会读到"用下面给出的落盘名"却找不到"下面")。
292
+ - 登记失败时**不发 kickoff**、会话保留、给出可重试的明确文案;不降级去扫本机目录、也不自己查氚云。
293
+ - **续聊也要重新登记**:范围只在 Host 内存里,插件重启或 12 小时后旧会话默认拿不到材料。
294
+ 所以「继续上次聊天」这条路径也会先登记一次(不重新拉文件、不重新读 OSS、不重复注入 Prompt);
295
+ 登记失败**不拦着进会话**(用户要的是继续聊,历史都在),原因写进面板,会话里 Tool 也会给出
296
+ 「请重新登记」的可执行文案。审核结果分析那条会话同样登记(两处共用同一段取数规则)。
297
+
298
+ ### fix · "查不出来"不再被说成"不存在"(错误分类的两处收紧)
299
+
300
+ - 工作空间探测**本身失败**(shell 服务没起来 / 被沙箱拦下 / 输出不可识别)原先被折叠成
301
+ 「所选工作空间不存在」—— 那会把一次宿主侧的拒绝说成"员工选错了目录",让员工去重选一个
302
+ 本来没错的目录(正是 §三 要消灭的误诊)。现在如实报「无法确认所选工作空间是否存在」,
303
+ 并按结构化事实归因(沙箱拒绝 → `sandbox`,其余 → `infrastructure`)。
304
+ - 建目录的结果里补齐**完整诊断事实**:`facts` 增加 `processStarted`(计划里那个 `ran` 布尔),
305
+ 结果新增 `postcondition`(`directory` / `file` / `absent` / `unknown`)——
306
+ "命令跑过了"与"目标真的成了我们要的样子"从此是两个字段。
307
+
308
+ ### test · 覆盖
309
+
310
+ - 注册表:TTL 到期即失效、重复登记刷新白名单与 TTL、`use` 推最近使用、超上限丢最久没用过的。
311
+ - 解析器:已登记会话放行;未登记 / 过期 / 错误的 `caseDir`(工作空间根、兄弟案例、子目录)/
312
+ 不一致的 `seqNo`·`objectId` 一律拒绝;同一 id 两边都命中时**以审核 scope 为准**。
313
+ - 工具:讨论会话能下白名单附件、清单外 `fileId` 与未登记会话零命令拒绝、
314
+ `record_get` / `files_list` 对讨论会话拒绝。
315
+ - 登记 RPC:成功登记(含"文件身份核对不通过就不去取附件")、子代理会话拒绝且零命令、
316
+ 参数不合法与没有工作空间时拒绝且不登记。
317
+ - 客户端:新建与恢复都登记、失败不发 kickoff、抛错变人话、旧宿主(没有这个能力)行为不变。
318
+ - **授权矩阵**逐格钉住(三种调用者 × 三个氚云 Tool):允许的四格真的执行、拒绝的七格零命令。
319
+ - 审核启动:建目录失败时**后续步骤一个都不许发生**(无记录 / 无占用 / 无快照交接 / 无子代理),
320
+ 且错误里带得出操作名与工作空间;探测没结论时不许说成"工作空间不存在"。
321
+ - **产物级**:`npm run smoke:built` 现在也从打包后的 `lib/index.js` 真调一次
322
+ `discussion-material-open`(空参数 → 结构化 `input` 失败、不回路径),证明这条新契约在**装上去的
323
+ 产物**里可达 —— 只查源码"写了没有"证明不了这件事(用缺陷注入验过这条断言会红)。
324
+
325
+ ## package · 0.0.30 · 2026-09-30 · refactor · 配置入口收敛:iFinD 改回可选数据源、账号连接只读、插件不建工作空间
326
+
327
+ ### 目标
328
+
329
+ 一次收敛三件事,职责边界变成:**环境信息**判断基础条件 / **外部数据源**(配置列表里的一步,
330
+ 可选、不带必检星号)展示与配置 / **账号连接**只读取、检查已有凭据 / **工作空间**只选择已有目录。
331
+
332
+ ### refactor · 同花顺 iFinD 从「必需项」改回**可选数据源**
333
+
334
+ - 清单 `ifind.required=false`:不进必需项分母(8 → 7),未就绪只让 `status` 落到 `degraded`
335
+ (与 `ready` 一样**放行**),只关掉 `capabilities.externalData` —— `global` / `auditCore` /
336
+ `delivery` 与报告审核都不受影响。
337
+ - issue 从"双写 global + external-data 的阻塞项"收成**一条非阻塞**项(scope = `external-data`):
338
+ 归属仍按原因分派(未填 / 无效 → user;无权益 → admin;网络 → system),因为"谁来修"三类不同。
339
+ - 环境页顶部改为「基础环境已就绪」,不再出现「还需完成 1 项 / 请完成同花顺 iFinD API-Key 验证」;
340
+ 门禁文案回到通用模板(`hasIfindBlocker` 那条专门话术删除)。
341
+ - 侧栏那枚环境灯的判据改成 `statusProceedable`:`degraded` 也是绿的,
342
+ 否则会出现「红点 + 基础环境已就绪」的自相矛盾。
343
+ - 外部数据源**和别的配置项并排**放在配置列表里,而且**排在最后一步**
344
+ (账号连接 → 阿里云 OSS → 工作空间 → 同花顺 iFinD):那一步的正文就是 iFinD 卡片
345
+ (状态统一为 未配置 / 待验证 / 已配置并验证 / 验证失败 / 数据权限不足 / 暂时不可用)。
346
+ **未配置时直接显示「未配置」**:芯片一律琥珀(红 = "必须处理"),能力清单等已配置后再显示。
347
+ - **基础必检项带红色星号**(`REQUIRED_SETUP_STEPS`:账号连接 / OSS / 工作空间;颜色用
348
+ `--dsw-alias-state-error-primary`),**外部数据源不带** —— 用户口径:必检项用星号表示"必须配置",
349
+ 外部数据不加,否则页面看起来很乱。可选项同时不进"还没做完"的统计(`pickStep` / `allStepsDone`
350
+ 只统计必检项,且默认落点是**最后一个必检项**而不是末位的可选项)。
351
+ - 形态上做过**三次**返工:① **独立的第四个侧栏模块**(被否:「不要单拆一个目录」);
352
+ ② **页级区块**(被否:「也和其他放在一起…否则页面看起来很乱」);③ 放在列表中间且"未配置"报红
353
+ (被否:「同花顺这里直接显示未配置就可以」+「把同花顺放到最后一个」)。最终就是**配置列表里的
354
+ 最后一步**。
355
+
356
+ ### feat · Windows:提醒"以管理员身份运行"(2026-09-30)
357
+
358
+ - 钉钉 CLI(`dws`)在 Windows 上要碰 `<HOME>\.dws`(先抢 `.data.lock`,再写操作系统凭据存储):
359
+ 进程权限不足时它以"锁被占用 / 拒绝访问"结束,**表现却是钉钉登录一直不成功**。
360
+ - 所以环境页在 `env.platform` 命中 `win32` 前缀时给一条**非阻塞**提醒
361
+ (`features/environment/platform-note.ts` 的 `needsWindowsAdminReminder`):
362
+ 「请用「以管理员身份运行」启动 DeepSeek Harness」——挂在**账号连接**那一步。
363
+ - 它**不参与任何门禁**(钉钉登录态是否有效仍由自检结论说了算);非 Windows **一个字都不提**,
364
+ 拿不到平台也不猜。
365
+
366
+ ### refactor · 账号连接只读取、检查已有凭据(协议 21 → 22)
367
+
368
+ - **删除**氚云内置浏览器扫码登录(协议 20 的 `browser-session-bind` + 客户端浏览器视图 + Cookie 读取)
369
+ 与钉钉设备码 / 两阶段登录(协议 21 的 `dws-login-start` / `dws-login-status` + 进度轮询)。
370
+ 一并删除 `H3yunBrowserLogin.tsx` / `DwsLoginCard.tsx` / `login-browser.ts` / `open-url.ts`
371
+ (内置浏览器优先的打开器)、`crwuOperationOf` 的 `h3yun session bind` 映射、`dwsLogin` 的 `--device`。
372
+ - 保留**兼容路径**:`relogin`(`crwu h3yun session login`)与 `dws-login`(`dws auth login`)——
373
+ 本机 CLI 自己拉起**系统浏览器**;面板只触发 + 「重新检查」,不创建浏览器 Tab、不显示二维码 /
374
+ 设备码、不读 Cookie、不轮询进度。CLI 的 `dws auth login --device` 在终端里仍然可用。
375
+ - 冻结操作清单 41 → 38;`WORKBENCH_PROTOCOL` 22(旧界面调这三个操作会 404,必须被明确挡住)。
376
+
377
+ ### refactor · 插件不再为用户创建工作空间根目录
378
+
379
+ - `ensureAuditRoot` 删掉 `ensureDirectory`(`mkdir` / `New-Item`):路径为空 →
380
+ 「尚未选定工作空间,请先选择一个已有目录」;路径不存在 / 是文件 →
381
+ 「已选定的工作空间目录不存在,请重新选择一个已有目录」——两种情况都**拒绝启动**,
382
+ 不静默切到父会话 cwd、也不静默换工作空间。
383
+ - 目录存在但未登记 → **只登记**(`workspaceRegistry.create`,它要求目录已存在)。
384
+ - 客户端 `UiWorkspaceService` 删除 `createDirectory`,`WorkspaceCard` 去掉「新建目录并用作工作空间」
385
+ 与 `pick(true)`,把用户选中的路径原样登记;文案改为「选择已有目录作为工作空间」。
386
+ - 允许自动创建的**只有** `<工作空间>/<流水号>` 这一级案例目录(审核启动的 shell 轨迹里
387
+ 不许出现 `mkdir <工作空间根>`,有测试按真实 trace 断言)。
388
+
389
+ ### fix · 案例目录创建改走本机访问代理(`mkdir: Operation not permitted` 的根因)
390
+
391
+ - 员工实测:发起审核后案例目录**没建出来**,子会话里案例内 Tool 全被拒("调用者不在进行中的审核里"),
392
+ 而 `/Users/<员工>/中瑞世联工作空间` 本身是可写的。根因不是 ACL,也不是授权收据:
393
+ `host/tools/case-files.ts` 的 `ensureDirectory` 直接调 `shell/run.ts` 的裸 `runShell`,
394
+ 于是这条 `mkdir` 拿到的是**部署默认沙箱**(`workspace-write`,边界 = 员工打开面板的那个会话 cwd),
395
+ 写案例目录被沙箱拒绝 —— 员工本人对那个目录其实是有写权限的。
396
+ - 新增三条**具名**本机访问操作,提权归属从此写在操作表里(`host/access/operations.ts`):
397
+ - `system.case-directory.write`:**特权**、来源 `audit-host` —— 只用于创建
398
+ `<员工选定工作空间>/<流水号>` 这一级案例目录,逐次声明
399
+ `danger-full-access` + `workspaceRoot = 已选工作空间`;
400
+ - `system.case-file.write` / `system.case-file.read`:**不提权**、来源 `audit-tool` ——
401
+ 案例目录**之内**的建 / 删 / 只读探测(子会话的沙箱边界本来就是那个目录,能给最小权限就给最小)。
402
+ - 新增来源 `audit-host`(插件自己的审核编排),与 `audit-tool`(审核子代理调 Tool)刻意分开:
403
+ 两者的沙箱位置不同,合并会让"谁在什么沙箱下建目录"重新变成只能从调用点读出来的事实。
404
+ - `ensureCaseDirectory` 的顺序固定:① 只读探测工作空间是否真的存在 → ② 经 Broker 执行 `mkdir`
405
+ → ③ 回读案例目录后置条件。**失败分三类给话**:沙箱未授权 / 被降级(带请求模式、实际模式、操作名、
406
+ 工作空间)、操作系统 ACL 不足(不动权限、不建议 `chmod`)、工作空间不存在(让员工重选)。
407
+ - 后置条件回读不再用 `ctx.fs.stat`,改用 Broker 上的只读路径探测
408
+ (`platform/shell.ts` 的 `pathProbeCommand`:`test -d` / `Test-Path -PathType`,
409
+ **永远以 0 退出、结论只在 stdout**):这样"读不到"与"不存在"能分开 ——
410
+ 前者按基础设施失败如实上报,不会被折叠成"没有这个目录"。
411
+ - 新增静态门禁 B-01e(`host-access-migration.test.mjs`):`case-files.ts` **不许**再出现裸 `runShell`
412
+ 或值导入 `shell/run.ts`,`audit/ops.ts` 必须用 `ensureCaseDirectory`;配套一台
413
+ **真的会拦人**的沙箱替身做回归(`host-case-files.test.mjs`),把 2026-09-30 的现场固定下来。
414
+
415
+ ### fix · 同名附件不再互相覆盖(`广兴建筑v3.zip` ×2)
416
+
417
+ - 一份报告里可以挂着两个**同名但不同**的附件(实测两个 `广兴建筑v3.zip`,`fileId` 不同)。
418
+ 按文件名落盘时第二件会**静默覆盖**第一件 —— 审核只看到一份材料,也答不了"这是重复上传还是两个版本"。
419
+ - 输入快照的 `附件清单.json` 每行新增三列:`localName`(`<主名>__<fileId 前 8 位><扩展名>`,
420
+ 例如 `广兴建筑v3__c8ef13b8.zip`)、`nameIndex` / `nameTotal`(同名附件的序号与总数)。
421
+ 名字**只由 `fileId` 决定**,与下载顺序无关;外部文件名先被收敛成单个路径段
422
+ (`host/h3yun/attachment-name.ts`,纯函数、可单测)。
423
+ - `crwu_h3yun_file_get` 在任何进程之前校验目标名的最后一段**必须带上这件附件自己的标识**,
424
+ 不带就回 `policy` 失败并给出正确的名字;`relativePath` 仍然由模型给,但它再也拼不出
425
+ 两个不同附件落成同一个名字的形态。
426
+ - 审核提示词同步:附件只按清单里的 `localName` 落盘;`nameTotal > 1` 的条目要当成**两件材料**
427
+ 逐件核对,不许只留一件。
428
+
429
+ ### test · 覆盖
430
+
431
+ - 工作空间:不渲染「新建」按钮、`createDirectory` 调用次数为 0、目录不存在 / 是文件 / 未选定时
432
+ 审核启动被拒、存在但未登记时只登记、审核启动不建工作空间根、案例目录仍然创建。
433
+ - 登录:相关模块文件已删 + 面板不存在内置浏览器登录 / 二维码 / 设备码入口与承诺、
434
+ 阻塞文案改为"请在系统浏览器完成登录后重新检查"、有效凭据照常通过、CLI 兼容路径仍在。
435
+ - iFinD:不在环境步骤与必需项分母里、未配置不阻塞环境与审核、区块展示数据源与验证状态、
436
+ 未配置时不产生误导性告警。
437
+
438
+ ## package · 0.0.29 · 2026-09-29 · fix · 启动审核:「没问到运行时」不再拒绝启动(交给子会话解析)
439
+
440
+ ### fix · 案例目录建不出来 / 子会话没有 scope 的那条根因
441
+
442
+ - 员工实测:发起审核后**案例目录没建**、子会话里案例内 Tool 全被拒("不在进行中的审核里"),
443
+ 而能力自检里二进制与 Tool 全 available —— 说明这轮审核根本没成立。
444
+ - 根因:`audit-start` 在创建案例目录**之前**解析 DSH 自带运行时,而它在 `host-background` 里
445
+ 没有会话作用域 → 工具调用必然报错 → 旧实现一律拒绝启动。
446
+ - 现在按 `unresolved` 分开处置:**确实缺失**(载荷无 python / 路径不可用 / 缺必需包)仍然拒绝启动;
447
+ **没能问到**(工具调用失败/超时/报错)**放行**,并把提示词换成新增的
448
+ 「由你在本会话里解析」那一段 —— 子会话有 agent 作用域,自己调一次
449
+ `load_workspace_dependencies` 就能拿到唯一允许的解释器绝对路径;仍然禁止任何查找与降级,
450
+ 取不到就停下并汇报 capability gap。
451
+ - 另记一条**独立阻塞**(不在插件侧、也不该由插件绕过):该机器上 `workspace-write` 下 pwsh
452
+ 每次以 `0xC0000142` 结束、零输出;而审核根/子会话按设计恒为 `workspace-write`,
453
+ 材料准备与交付渲染都走 shell → 需 DSH 侧排查沙箱启动器。
454
+
455
+ ## package · 0.0.28 · 2026-09-29 · refactor · 把 DSH 自带运行时**从环境自检里挪走**
456
+
457
+ ### refactor · 运行时只在 `audit-start` 解析,不再进环境结论
458
+
459
+ - 原因:`load_workspace_dependencies` **需要 agent 作用域**。会话里调它返回完整载荷
460
+ (`python` + `openpyxl` 等),而环境自检跑在 `host-background`、没有会话上下文 —— 调它就是工具报错。
461
+ 两种补救都不成立:当"缺失"会伪造「系统故障」把审核入口关掉(0.0.26 之前);
462
+ 只显示「待复核」则那一行**永远解析不出来**(0.0.26/0.0.27 员工实测仍是「未设置」)。
463
+ - 现在:环境自检**不再调用解析器、不再有这一行、不再进必需项分母(9 → 8)、不影响总状态**。
464
+ - **能力没有放宽**:`audit-start` 仍用审核根 agent 解析运行时,拿不到就终止审核;失败文案区分
465
+ 「没能问到」(可重试)与「确实缺失」(只能由部署方补运行时)。取 agent 的唯一实现是
466
+ `boundParentAgent`(`bind-session` 落盘的父会话),`audit-start` 与它共用。
467
+ - 一并删除:`RuntimeView`、`env.runtime` 分区、`runtimeItem`、`runtime` issue、
468
+ 客户端三行诊断与相关文案键(不留死代码)。解析器本身保留(`audit-start` 要用)。
469
+
470
+ ## package · 0.0.27 · 2026-09-29 · fix · 环境自检用面板绑定的父会话解析 DSH 运行时(真正修掉"未设置")
471
+
472
+ ### fix · 运行时不再显示「未设置」:自检也带上 agent 作用域
473
+
474
+ - 0.0.26 只把"没问到"说准(非阻塞 + 待复核),但运行时**仍然解析不出来** —— 因为根因是
475
+ **自检没有 agent 作用域**:`load_workspace_dependencies` 需要它,不带就是工具报错。
476
+ - 现在自检在调 `pythonRuntime` 之前,用插件状态里 `bind-session` 落盘的 `parentSessionId`
477
+ 取同一个 agent(新增唯一实现 `boundParentAgent`,`audit/spawn.ts`;审核启动用的也是它)。
478
+ 取不到时退回不带 agent 的调用(只报「待复核」,不判缺失)。
479
+ - 实测依据(员工 Windows):同一个工具在会话里(带 agent)返回完整载荷
480
+ (`python` 路径 + `openpyxl 3.1.5` 等),自检上下文报错 —— 差别就是 agent 作用域。
481
+ - 解析成功会进缓存,因此环境页那一行会变成「Python 3.12.x · DSH 自带」,总状态回到 `degraded`/`ready`。
482
+
483
+ ## package · 0.0.26 · 2026-09-29 · fix · 环境自检「没问到」不再伪造成系统故障(运行时待复核)
484
+
485
+ ### fix · `load_workspace_dependencies` 在自检上下文问不到时,不再把审核入口关掉
486
+
487
+ - 员工 Windows 实测:环境页报「发现系统故障」,只有 `DSH Runtime: (未设置)` 一项;
488
+ 而**同一个工具在会话里(带 agent)返回完整载荷**(`python` 路径 + `openpyxl` 等全部必需包)。
489
+ 根因:环境自检跑在 `host-background`、**没有会话 agent**,而该工具需要 agent 作用域;
490
+ 旧实现把"工具报错"一律说成"DSH 自带脚本运行时不可用"——一条系统归属的阻塞项。
491
+ - 现在把两件事分开:`unresolved`(工具不可用 / 抛错 / 超时 / 工具报错 = **没问到**)与
492
+ **真·缺失**(工具成功但载荷没有 python、路径不存在或不是文件、缺必需包)。
493
+ 前者**非阻塞**、页面显示「待复核」并说明"发起审核时会复核";后者照旧阻塞。
494
+ - 真正的门禁不变:`audit-start` 仍带**审核根 agent** 解析运行时,拿不到就终止审核;
495
+ 解析成功会进缓存,所以发起过一次审核之后环境页那一行会变成"已就绪"。
496
+ - 顺带:`SetupItemView` 新增状态词 `unresolved`(「待复核」),客户端按中性语气渲染。
497
+
498
+ ## package · 0.0.25 · 2026-09-29 · refactor · 下掉"插件自指定目录":配置与临时目录都用默认
499
+
500
+ ### refactor · `DWS_CONFIG_DIR` 与 `TMPDIR/TEMP/TMP` 两条自指定通道整体移除
501
+
502
+ - 删除 `src/host/dws/config-dir.ts` 与 `src/host/system/auth-scratch.ts`,以及各调用点的目录注入
503
+ (`dws/run.ts`、`system/ops.ts`、`crwu/run.ts` 的 `scratchDir`、`ops/core.ts`、`apply.ts`、
504
+ 体检/身份/环境自检/审核 Tool)。
505
+ - 登录命令回到 CLI 默认形状:`crwu h3yun session login`、`dws auth login [--device]` 原样执行,
506
+ 配置目录用 `<HOME>/.dws`、临时目录用系统 TEMP —— 与员工自己终端里的用法一致,状态只有一处。
507
+ - **为什么下掉**(三次对照,见 `docs/development-notes.md` §16):同一个 `dws.exe` 在员工自己的
508
+ PowerShell 里能建 `~/.dws`、能打印授权链接;换成插件目录后**仍然**建不出目录;手工预建好目录后
509
+ **仍然**打不开 `.data.lock`;**以管理员身份运行 DSH 一次通过**,而普通权限下连已存在的锁都打不开
510
+ —— 拦的是这台机器对 `dws.exe` 的按程序/按父进程访问控制,换目录解决不了,只留下"两套状态"的代价。
511
+ - 归因文案相应补一句「插件已经不再自指定目录,全部回到 CLI 默认」,两条可执行路径不变
512
+ (以管理员身份完成登录 / 让管理员按程序放行随包发布的 `dws.exe`)。
513
+
514
+ ## package · 0.0.24 · 2026-09-29 · fix · 归因补上复验结论:提权**不是一次性步骤**,影响面不止登录
515
+
516
+ ### fix · 别再让人以为"提权登一次以后就好了"
517
+
518
+ - 员工复验:提权那次登录成功之后,**用普通权限重启 DSH 依旧打不开已存在的锁**
519
+ (`creating config dir for lock` / `opening lock file`)→ 非提权运行时 `dws` 连打开已有文件都被拒。
520
+ - 归因文案补两句:① **不是一次性步骤**(非提权每次都会被拒);② **影响面不止登录** ——
521
+ 同一台机器上凡是依赖 `dws` 的功能(知识库下载、钉钉归档与通知)同样需要提权才能用。
522
+ - 两条长期正解不变:让 IT 按程序放行随插件发布的 `dws.exe`(放行后恢复普通权限运行),
523
+ 或在放行前以提权方式使用 DSH。
524
+
525
+ ## package · 0.0.23 · 2026-09-29 · fix · 登录失败归因纠正:这台机器上**提权才有效**,换目录无效
526
+
527
+ ### fix · 不再把员工指向"切访问模式"
528
+
529
+ - 0.0.22 的归因里写着「切权限不会有用」—— 前半段(切【DSH 访问模式】没用)是对的,
530
+ 但它被写成了"切权限一律没用",而员工当场实测:**以管理员身份运行 DSH 后一次通过**。
531
+ 两条实测事实必须分开说,否则会把下一个人指到错方向。
532
+ - 现在机器支的文案改为:说清事实(请求与实际都已是 `danger-full-access`)+
533
+ 给出两条可执行路径(**以管理员身份运行 DSH 完成一次登录**;长期让 IT 按程序放行 `dws.exe`)+
534
+ 明确写出「**换目录也没用**」(主目录根、插件状态目录、手工预建的插件目录三个位置都试过)+
535
+ 「设备码登录同样要创建这个锁文件,不是绕过办法」。
536
+ - 提权那次登录的凭据写进**员工本人**的操作系统凭据存储(同账户、只是令牌提权),
537
+ 因此受管机器上"提权做一次性登录"是可接受处置。
538
+ - 测试用 `doesNotMatch` / `match` 双向钉住这两点,并逐条用缺陷注入证伪。
539
+
540
+ ## package · 0.0.22 · 2026-09-29 · fix · `dws` 配置目录固定用插件自己的目录(协议 22)
541
+
542
+ ### fix · 钉钉登录不再依赖 `<HOME>/.dws`(那台机器上 DSH 的子进程建不出来)
543
+
544
+ - 员工 Windows 实测:`creating config dir for lock: mkdir C:\Users\<你>\.dws: Access is denied`,
545
+ 而 DSH 的诊断里三种模式全是 `danger-full-access` —— 与 DSH 的文件策略无关。
546
+ - 证据链(详见 `docs/development-notes.md` §16):① 同一个 `dws.exe` 在员工自己的 PowerShell 里
547
+ 能建 `~/.dws` 并打印授权链接;② **`~/.dws` 已存在也照样报同一个错** —— Windows 的
548
+ `CreateDirectory` 先查父目录创建权再查目标是否存在,所以这是**令牌访问范围**问题,不是缺目录;
549
+ ③ DSH 放行它自己的目录树(插件状态文件与登录临时目录都是插件在 DSH 里建的);
550
+ ④ 全新空目录里 `auth status` 仍 `authenticated:true` → **token 在操作系统凭据存储里,换目录不丢登录态**。
551
+ - 现在所有 dws 调用**一律显式**带 `DWS_CONFIG_DIR=<HOME>/.dsh/crwu-workbench/dws-home`
552
+ (显式写进命令,不再赌进程有没有继承环境变量)。
553
+ - **刻意不做**探针、不做"失败后换目录"、不做重试:登录是有副作用的交互,不能靠重试兜;
554
+ 而只读的 `auth status` 在只读路径上可能不碰锁,拿它当判据实测会漏判。
555
+ - 代价如实说:员工自己终端里的 `dws` 与插件用不同配置目录(锁/日志分开;
556
+ 多组织时"当前组织"可能要各选一次)。
557
+
558
+ ## package · 0.0.19 · 2026-09-29 · feat · 两个登录都改走内置浏览器(协议 20 / 21)
559
+
560
+ ### feat · 氚云:面板内嵌 lease 浏览器扫码,读到 h3_token 就交给 CLI 落 OS 凭据存储
561
+
562
+ - 环境页新增「在内置浏览器里扫码登录」:插件在桌面端自建一个宿主的 lease guest
563
+ (`dshDesktop.browser.acquire` → `<webview partition src="about:blank#<lease>">` → `dom-ready` 后导航),
564
+ 扫码后从页面 `document.cookie` 读 `h3_token`,**立刻**经新增操作 `browser-session-bind`(协议 20)
565
+ 交给 `crwu h3yun session bind --token-stdin`。令牌不进命令行、不落盘、不回显、不进日志。
566
+ - 拿不到内置浏览器(Web profile / 旧桌面端)时卡片禁用并给出回退说明;CLI 自己拉浏览器的老路径保留。
567
+ - E1 探针实测(桌面端 0.2.0-rc.2,macOS,钉钉扫码一次):lease 桥可用、`<webview>` 可附着、
568
+ `executeJavaScript` 可读 `document.cookie`、`h3_token` 是未过期 JWT(约 48h)、
569
+ 导航里出现 `www.h3yun.com/entry/login/corp?code=` 回调、`release` 干净。
570
+ 备通道(抓 `?code=` 由 Host 换取)已观测到,暂不启用。
571
+
572
+ ### feat · 钉钉:`dws auth login` 改成后台跑 + 两阶段(协议 21)
573
+
574
+ - 旧形态是同步等 5 分钟再回 stdout 尾巴 —— 授权 URL 到界面时用户早已不在等。现在
575
+ `dws-login-start` 起后台进程并立刻回快照,`dws-login-status` 轮询;界面先把**动作**摆出来
576
+ (打开授权链接 / 复制设备码),再给 CLI 原文。解析是尽力而为,`tail` 始终是原文。
577
+ - **打开在 DSH 内置浏览器**(右侧栏 browser 标签,`openTab`),拿不到服务 / 标签类型未启用 /
578
+ 异步失败才退回系统浏览器 —— 裸 `window.open` 在桌面端等于"跳到 DSH 外面"。
579
+ - 正在跑时再 start 不会起第二个进程(两个 `dws` 会抢同一个 `~/.dws` 锁);
580
+ 5 分钟到点杀掉并如实报超时;插件卸载时杀掉仍在跑的进程。
581
+ - 副作用:所有操作数与协议号随之 +1(41 个操作 / 协议 21),冻结清单与 facade 已同步。
582
+
583
+ ### chore · 新增后台执行通道
584
+
585
+ - `src/host/shell/run.ts` 的 `startShell` + Broker 的 `startShell`:与前台同一条授权/提权判据,
586
+ 用 `execute()` 但不 await `result()`,请求带 `onExpiry: 'none'`。
587
+
588
+ ## package · 0.0.18 · 2026-09-29 · chore · 把 DSH 0.2 线的验证矩阵推进到 `0.2.0-rc.2`
589
+
590
+ ### fix · iFinD 环境检查改为只读用户主动验证结论
591
+
592
+ - 保存 API-Key 或点击「重新验证」时完成一次真实取数,并把脱敏结论随凭据保存。
593
+ - 普通环境检查、刷新和能力门禁只读取最近结论,不再主动请求 iFinD 远程服务。
594
+
595
+ 桌面端 DSH 已升到 `0.2.0-rc.2`。**业务代码零改动** —— 逐包比对证明这条线内没有 API 漂移,
596
+ 要补的是"声明支持"与"真的验过"之间的差:仓库原先只在矩阵里验到 `0.2.0-rc.1`。
597
+
598
+ **逐包比对结论**(2026-09-29,把 16 个插件用到的 `@deepseek-ai/dsh-*` 在 rc.1 / rc.2 各
599
+ `npm pack` 一次,解包后逐文件 diff):
600
+
601
+ | 差异 | 包 |
602
+ | --- | --- |
603
+ | **所有 `.d.ts` 零差异** | 16/16 |
604
+ | 只有 `package.json`(版本号 + 内部依赖表) | 13 个:`dsh-tools` / `dsh-agent` / `dsh-sandbox-policy` / `dsh-user-approval` / `dsh-skill-filesystem` / `dsh-host-webserver` / `dsh-plugin-manager` / `dsh-shell` / `dsh-session` / `dsh-fs` / `dsh-util-values` / `dsh-client-ui-layout` / `dsh-app-boot` |
605
+ | 另有运行时代码小改动 | `dsh-subagent/lib/typert.host.js` 多一条 `user-question-reply` typert 协议声明;`dsh-client-ui-renderer/lib/client.js` 加一个 `useMemo`;`dsh-client-ui-sidebar/lib/client.js` 去掉品牌按钮外层 `Tooltip` 并换版本号 |
606
+
607
+ 据此:把 `src/` 复制进一个只装了 `0.2.0-rc.2` 完整 peer 集的临时工程跑 `tsc --noEmit`,
608
+ **0 error**;插件用到的 slot / Tool / Shell / Sandbox / Subagent API 一个都没变。
609
+
610
+ **本版改了什么**
611
+
612
+ 1. **兼容矩阵的代表版本换成 `0.2.0-rc.2`**:`scripts/check-dsh-compat.mjs` 的 `RUNTIMES`
613
+ 与 `tests/unit/host-package.test.mjs` 的 `SUPPORTED_DSH_RUNTIMES`。一条声明线只放一个
614
+ 代表版本(同线后续 rc 与正式版由 `^0.2.0-rc.1` 的区间语义覆盖),`0.3.x` 仍然明确不在范围内。
615
+ 2. **`devDependencies` 从 `^0.1.7-rc.2` 挪到 `^0.2.0-rc.2`**(15 个,与全部 peer 一一对应):
616
+ 默认的 `npm run typecheck` / `npm test` 现在验的就是员工装到的那条线,而不是旧线。
617
+ `peerDependencies` 与 `engines.dsh` **不动** —— `^0.1.7-rc.2 || ^0.2.0-rc.1` 本来就覆盖 rc.2,
618
+ 0.1.7 线的用户不受影响。
619
+ 3. 文档同步:`README.md` / `README.en.md` 的支持矩阵与兼容段落、`AGENTS.md` §8.6
620
+ (新增「同一条线内的新 rc 也要进验证矩阵」一条)、`docs/development-notes.md` §11
621
+ (新增一行"升级 DSH 到同线新 rc 怎么办"的完整处置步骤)、`docs/windows-acceptance.md`
622
+ 的 DSH 版本栏。
623
+
624
+ **验证**:`npm run check`(含 `version:check` / `config:check` / `skills:check` /
625
+ `skills:cli-guard` / `dws:check` / `typecheck` / 全量单测 / `build` / `smoke:built`)、
626
+ `npm run pack:assert`,以及 `npm run compat:dsh`(0.1.7-rc.2 与 0.2.0-rc.2 两条线各真装一遍)。
627
+
628
+ ## package · 0.0.17 · 2026-09-29 · fix · 登录失败归因分两支 + 登录临时目录由插件指定
629
+
630
+ 员工在 Windows 上点「扫码登录氚云 / 钉钉登录」持续失败。开发者诊断给出的结构化事实是
631
+ `req = res = ran = danger-full-access`、`denied=false`、`runnerFailed=false`、`started=true` ——
632
+ **DSH 没有拦这次调用**;原文却是
633
+
634
+ ```
635
+ create fallback browser profile directory: mkdir C:\Users\51019\AppData\Local\crwu: Access is denied.
636
+ (system TEMP failed: mkdir C:\Users\51019\AppData\Local\Temp\crwu-scan-556823159: Access is denied.)
637
+ ```
638
+
639
+ 两条路径都在 `AppData\Local` 下、都被拒,而同一时刻 `dws auth status` 是 ok(`~/.dws` 可写)。
640
+ 本版据此做两件事,都只动插件侧:
641
+
642
+ 1. **登录的临时目录改由插件指定**(`src/host/system/auth-scratch.ts`):先建
643
+ `<home>/.dsh/crwu-workbench/auth-tmp`,再把 `TMPDIR`/`TMP`/`TEMP` 指过去
644
+ (POSIX `mkdir -p … && TMPDIR=… <cmd>`;PowerShell `New-Item -Force …; $env:TMP=…; & <cmd>`)。
645
+ 依据是员工机器上的事实:系统 TEMP 与用户缓存目录都被拒,而插件状态目录(`<home>/.dsh/…`)可写。
646
+ **不动 `LOCALAPPDATA`**(Chromium 自己的组件目录仍走系统默认);主目录未知时原样返回命令,不伪造。
647
+ `runCrwu` 增加可选 `scratchDir`(只有会就地起浏览器的那条命令用)。
648
+ 2. **归因分两支,不许合成一句**(`LoginAdvice.policyBlocked`):结构化事实说降级 / 拒绝 / 实际受限
649
+ → 「被文件策略挡在工作区之外」,动作是切「完全权限」;**事实干净却照样 `Access is denied`
650
+ → 「被这台机器拒绝(不是 DSH 的文件策略)」**,明说"切权限不会有用",并给退路
651
+ (氚云 `crwu h3yun session bind --token`、钉钉先试「设备码登录(无浏览器时)」)。
652
+ 上一版把两支合成一句"请切完全权限" —— 员工按提示切了,而日志证明根本不是策略问题(误导)。
653
+ 3. 面板「账号连接」的前置说明同步改口径,并回到"设备码登录(**无浏览器时**)"
654
+ (它不是"沙箱挡浏览器"的退路:同样要抢 `<HOME>/.dws` 的锁)。
655
+
656
+ **已证伪**:把两支合成策略支 → 5 条变红;把 scratch 的 env 前缀去掉(只建目录不指过去)→ 2 条变红。
657
+
658
+ ### ① 登录失败归因:两支分开,不许合成一句
659
+
660
+ **症状**:员工第一次用(Windows 与 macOS 都报)时,点「扫码登录氚云」「钉钉登录」浏览器起不来;
661
+ 面板给的却是 CLI 的原话 —— 氚云 `mkdir C:\…\Temp\crwu-scan-1799287186: Access is denied.`
662
+ (看着像 crwu 坏了)、钉钉 `acquiring file lock: …\.dws\.data.lock: Access is denied.`
663
+ (看着像 dws 自己的 bug)。
664
+
665
+ **根因(两条入口同一条边界)**:登录**必须写工作区之外的路径** ——
666
+
667
+ | 入口 | 必须写的东西 |
668
+ | --- | --- |
669
+ | `crwu h3yun session login` | `$TMPDIR`/`%TEMP%` 下的临时浏览器 profile(`crwu-scan-*`),再经 CDP 读会话 |
670
+ | `dws auth login`(含 `--device`) | `<HOME>/.dws/.data.lock`(拿登录态之前先抢文件锁) |
671
+ | 两者收尾 | 操作系统凭据存储(钥匙串 / Credential Manager) |
672
+
673
+ 宿主按完全访问跑、操作系统的 ACL/受限令牌仍然拒绝时报的就是上面那两句;受限沙箱下浏览器即使
674
+ 起来了也会在几毫秒内 renderer 崩溃(CDP 只回 `close 1006` / `Target crashed`)。
675
+
676
+ - **新增纯函数判据** `src/host/system/login-failure.ts`:`loginFailureAdvice()` / `describeLoginAdvice()`。
677
+ 顺序是「结构化事实优先,文本最后」:① `runnerFailed` → ② 提权被降级(`requested !== resolved`)
678
+ → ③ `denied === true` 或实际跑在受限模式 → ④ **事实干净但原文点名了登录必须写的工作区外目标
679
+ 且带拒绝字样**(Windows 实测就是这一形态)。
680
+ - **第 ④ 条是 `sandboxDenialNote()` 的唯一例外**,判据本身变强了:目标不是"任意路径",而是登录
681
+ 流程必须写的那几个已知目标。**目标词与拒绝字样必须同时命中** —— 只命中一个不归因(否则
682
+ `secret not found in keyring` 这种"真没条目 / 钥匙串被锁"会被说成"去切完全权限")。
683
+ - **`dwsLogin` 以前完全不做归因**(只有 `runDws` 做),现在两条登录都返回 `sandboxBlocked` + `advice`,
684
+ 并把「在输入框下方的访问模式里选「完全权限」再点一次」写进 `error`,**原始报错保留**在末尾。
685
+ - **设备码不再是"退路"**:它同样要抢 `~/.dws` 的锁,所以按钮文案改回「设备码登录(无浏览器时)」,
686
+ 归因文案里明说"在这个模式下也走不通"。
687
+ - **界面**:「账号连接」页在登录按钮下方**先说前置条件**(`envLoginSandboxHint`);
688
+ 被归因时不再追加 stdout 尾巴(否则那句「正在打开浏览器窗口…」会把真正的动作挤掉)。
689
+
690
+ **已证伪**:把第 ④ 条整段关掉 → 7 条变红;把「目标词」那一半去掉(只认拒绝字样)→
691
+ 误报用例变红。`cp` 还原后转绿。
692
+
693
+ ### ② 登录的临时目录由插件指定(不再依赖系统 TEMP)
694
+
695
+ 员工在 Windows 上用 0.0.15/0.0.16 再报一次,原文是**两条路径都被拒**:
696
+
697
+ ```
698
+ create fallback browser profile directory: mkdir C:\Users\51019\AppData\Local\crwu: Access is denied.
699
+ (system TEMP failed: mkdir C:\Users\51019\AppData\Local\Temp\crwu-scan-556823159: Access is denied.)
700
+ ```
701
+
702
+ 而同一时刻开发者诊断里 `h3yun.session.login` 是
703
+ `req = res = ran = danger-full-access`、`denied=false`、`runnerFailed=false`、`started=true` ——
704
+ **DSH 没有拦它**,`%TEMP%` 与用户缓存目录却都被这台机器拒绝。两处修正:
705
+
706
+ 1. **临时目录改由插件指定**(`src/host/system/auth-scratch.ts`):登录命令先建
707
+ `<home>/.dsh/crwu-workbench/auth-tmp`,再把 `TMPDIR`/`TMP`/`TEMP` 指过去
708
+ (POSIX `mkdir -p … && TMPDIR=… <cmd>`;PowerShell `New-Item -Force …; $env:TMP=…; …; & <cmd>`)。
709
+ 依据是**这台机器自己的事实**:系统 TEMP 与缓存目录都被拒,而插件状态目录(`<home>/.dsh/…`)
710
+ 是它一直在写的地方。**不动 `LOCALAPPDATA`**(Chromium 自己的组件目录仍走系统默认);
711
+ 主目录未知时**原样返回命令**,不伪造一个没建出来的目录。
712
+ `runCrwu` 增加可选的 `scratchDir`(只有会就地起浏览器的那条命令用)。
713
+ 2. **归因分两支,不许合成一句**:`LoginAdvice.policyBlocked`。
714
+ - `true`(结构化事实:降级 / 拒绝 / 实际受限 / runner 挂)→「被文件策略挡在工作区之外」,
715
+ 动作是切「完全权限」;
716
+ - `false`(事实干净、原文点名登录必须写的目标且带拒绝字样)→「**被这台机器拒绝**(不是 DSH 的
717
+ 文件策略)」,明确写「切权限不会有用」,并给退路:氚云用 `crwu h3yun session bind --token`、
718
+ 钉钉先试「设备码登录(无浏览器时)」。
719
+
720
+ 上一版把两支合成了一句"请切完全权限"——员工按提示切了,而日志证明根本不是策略问题(**误导**)。
721
+
722
+ **已证伪**:把两支合成策略支 → 5 条变红;把 scratch 的 env 前缀去掉(只建目录不指过去)→ 2 条变红。
723
+
10
724
  ## package · 0.0.16 · 2026-09-29 · fix · Windows 氚云扫码临时目录回退
11
725
 
12
726
  - 随包 `crwu.exe` 的 `h3yun session login` 在系统 `%TEMP%` 被 Windows 安全策略拒绝时,
@@ -220,41 +934,6 @@
220
934
  ② `crwu_audit_case_bootstrap` 的 `caseDir` 虽被钉在本轮案例目录,但 `objectId` 仍是提交进来的 ——
221
935
  子会话可以用**别的** objectId 把别人的记录取进自己的案例目录;现在审核子会话调用它一律拒绝。
222
936
 
223
- ### G 段:登录失败归因 ——「登录必须写工作区之外」不再是原文(2026-09-29)
224
-
225
- **症状**:员工第一次用(Windows 与 macOS 都报)时,点「扫码登录氚云」「钉钉登录」浏览器起不来;
226
- 面板给的却是 CLI 的原话 —— 氚云 `mkdir C:\…\Temp\crwu-scan-1799287186: Access is denied.`
227
- (看着像 crwu 坏了)、钉钉 `acquiring file lock: …\.dws\.data.lock: Access is denied.`
228
- (看着像 dws 自己的 bug)。
229
-
230
- **根因(两条入口同一条边界)**:登录**必须写工作区之外的路径** ——
231
-
232
- | 入口 | 必须写的东西 |
233
- | --- | --- |
234
- | `crwu h3yun session login` | `$TMPDIR`/`%TEMP%` 下的临时浏览器 profile(`crwu-scan-*`),再经 CDP 读会话 |
235
- | `dws auth login`(含 `--device`) | `<HOME>/.dws/.data.lock`(拿登录态之前先抢文件锁) |
236
- | 两者收尾 | 操作系统凭据存储(钥匙串 / Credential Manager) |
237
-
238
- 宿主按完全访问跑、操作系统的 ACL/受限令牌仍然拒绝时报的就是上面那两句;受限沙箱下浏览器即使
239
- 起来了也会在几毫秒内 renderer 崩溃(CDP 只回 `close 1006` / `Target crashed`)。
240
-
241
- - **新增纯函数判据** `src/host/system/login-failure.ts`:`loginFailureAdvice()` / `describeLoginAdvice()`。
242
- 顺序是「结构化事实优先,文本最后」:① `runnerFailed` → ② 提权被降级(`requested !== resolved`)
243
- → ③ `denied === true` 或实际跑在受限模式 → ④ **事实干净但原文点名了登录必须写的工作区外目标
244
- 且带拒绝字样**(Windows 实测就是这一形态)。
245
- - **第 ④ 条是 `sandboxDenialNote()` 的唯一例外**,判据本身变强了:目标不是"任意路径",而是登录
246
- 流程必须写的那几个已知目标。**目标词与拒绝字样必须同时命中** —— 只命中一个不归因(否则
247
- `secret not found in keyring` 这种"真没条目 / 钥匙串被锁"会被说成"去切完全权限")。
248
- - **`dwsLogin` 以前完全不做归因**(只有 `runDws` 做),现在两条登录都返回 `sandboxBlocked` + `advice`,
249
- 并把「在输入框下方的访问模式里选「完全权限」再点一次」写进 `error`,**原始报错保留**在末尾。
250
- - **设备码不再是"退路"**:它同样要抢 `~/.dws` 的锁,所以按钮文案改回「设备码登录(无浏览器时)」,
251
- 归因文案里明说"在这个模式下也走不通"。
252
- - **界面**:「账号连接」页在登录按钮下方**先说前置条件**(`envLoginSandboxHint`);
253
- 被归因时不再追加 stdout 尾巴(否则那句「正在打开浏览器窗口…」会把真正的动作挤掉)。
254
-
255
- **已证伪**:把第 ④ 条整段关掉 → 7 条变红;把「目标词」那一半去掉(只认拒绝字样)→
256
- 误报用例变红。`cp` 还原后转绿。
257
-
258
937
  ### 测试
259
938
 
260
939
  新增/扩写的测试文件:`host-access-consent` / `host-access-broker` / `host-access-classify` /