# 档案 52 · 新用户实例无法启动 —— 三处连锁缺陷(picker 铺开脚本) - **日期**:2026-09-11 - **状态**:已落地并端到端验证 - **发现方式**:在为档案 51 做复现验证、**新建一个测试用户**时暴露 - **影响面**:**每一位新注册用户**——首次(或第二次)进入实例即进入崩溃循环,最终熔断;页面上表现为 502 / 发消息报错,且**重试与刷新均无效** - **关联**:档案 18 v3(目录选择器收敛)、档案 42(`ro-bind` 不能遮蔽 `/usr`)、档案 20(崩溃熔断) --- ## 一、缺陷链(三处独立缺陷,串起来 = 实例永远起不来) ### D1 · 模板空数组未剥离 → 写出非法 YAML(**直接致命**) dsh 新建 profile 时生成的 `cordis.patch.yml` 模板 = **3 行注释 + `[]`**。 `ensure-workspace-picker.cjs` 的 `stripPlatformSegments()` **只剥平台段、保留注释**,于是: ```js body = "# Your patch layer …\n# a top-level YAML array …\n# overrides, …\n[]" // ≠ '[]' const normalized = body === '' || body === '[]' ? '' : body + '\n\n' // 判定失效 ``` → 裸 `[]` 被原样留下,平台段又追加在其后 → **同一个 YAML 流里出现两个文档却没有 `---`**: ```yaml [] ← 空数组文档 # >>> platform: workspace-scoped-picker … - insert: ← 第二个文档,缺分隔符 ``` dsh 启动即报: ``` Error: dsh: failed to parse overlay …/profiles/web/cordis.patch.yml: YAMLException: end of the stream or a document separator is expected (9:1) → exitCode 1 ``` ### D2 · 先写平台段、后装插件,且装失败不回收(**连锁致命**) 原逻辑:`if (needPatch) 写段` → `if (needInstall) 装插件`,装失败只 `console.log` + `continue`。 于是留下"**段里引用了根本不存在的插件**"的 profile: ``` Error: dsh: plugin tree failed to load: failed to import loader entry workspace-scoped-picker (@dsh-local/workspace-scoped-picker): Cannot find package '@dsh-local/workspace-scoped-picker' ``` ### D4 · `/opt/dsh` 是 `drwx------ root`,用户 uid 读不到产物(**D2 的直接成因**) 脚本注释写着"直接用 artifacts 里的 tgz 绝对路径(root 可读、全局只读)", 但 `/opt/dsh` 是 **700** —— `setpriv` 到用户 uid 后: ``` EACCES: permission denied, open '/opt/dsh/artifacts/workspace-scoped-picker-0.1.4.tgz' ``` → 插件安装**必然失败**(不是偶发)→ 叠上 D2 就变成"段在、插件不在"→ 叠上 D1 就是双重必崩。 --- ## 二、改动点(`poc/workspace-scoped-picker/ensure-workspace-picker.cjs`) | # | 改动 | 目的 | |---|------|------| | D1 | `stripPlatformSegments()` 内 `if (line.trim() === '[]') continue` | 丢掉裸空数组文档行(空数组本就是"无 patch",丢弃永远安全) | | D2 | **顺序反转**:先装插件 → 复查 `node_modules/@dsh-local/workspace-scoped-picker/package.json` → **只有插件确实在位才写平台段**;插件缺失时**反向剥离已有平台段**(自愈) | 绝不让 profile 引用不存在的插件 | | D4 | 安装前把产物**暂存到用户自己的 home**(`/.dsh-stage/…tgz`,`0444` 只读),从那里 `pnpm add` | 用户 uid 可读;**不扩大任何宿主权限**(R5 安全,未改 `/opt/dsh` 权限);只读 0444 杜绝安装源被就地篡改 | --- ## 三、验证结果(全新用户 `poc-fresh`,uid 100004,首次启动) | 检查项 | 修复前 | 修复后 | |--------|--------|--------| | `cordis.patch.yml` | 含裸 `[]` + 平台段 → 非法 YAML | 仅注释 + 平台段(无裸 `[]`) | | 插件 | `ERR_MODULE_NOT_FOUND` | 安装成功 | | 实例状态 | `crash-restart` 循环 → `crash-loop-circuit-open` | `status: running`,`restarts: 0` ✅ | | 实例页 | 502 | `200`,26073 B,自愈脚本注入命中 ✅ | | `POST /api/session/prompt` | 失败 | `200` ✅ | | journal | `YAMLException` / `ERR_MODULE_NOT_FOUND` | 无 ✅ | --- ## 四、红线遵守 - **R2**:只改平台自有脚本 `poc/workspace-scoped-picker/ensure-workspace-picker.cjs`。 - **R5**:**零权限扩大**。D4 刻意选择"暂存到用户自己的 home"而不是 `chmod o+x /opt/dsh` —— 后者会让用户 uid 能穿过 root 私有目录,属于**扩大可见面**,需先确认。 本档案未做任何权限放宽。 - **R4**:全程使用**自建测试账号**(`poc-replay` / `poc-fresh`,注册 → DB 审批 → 用完即删), 未使用 admin / guest 做登录测试。 ## 五、遗留与建议 - **存量体检**:建议对现有用户做一次巡检(`grep -n '^\[\]$' /profiles/web/cordis.patch.yml`), 凡"裸 `[]` + 平台段"同存即为待修;本次实测 admin / guest **均正常**(它们的文件早已被角色 patch 整体重写)。 - **顺序竞态**:`orchestrator.launch()` 里 `ensurePickerProfile()` 是 fire-and-forget(`detached`), 与 `spawnInstance` 并发;脚本本身幂等且现在不会再写出坏文件,故未改并发模型, 但"脚本运行期间实例正在启动"的时序仍值得后续收敛(例如等脚本退出再 spawn)。