Files
dsh_shenxian/dsh-server-docs/04-调整方案/52-新用户实例无法启动.md
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

103 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 档案 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**(`<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 '^\[\]$' <home>/profiles/web/cordis.patch.yml`),
凡"裸 `[]` + 平台段"同存即为待修;本次实测 admin / guest **均正常**(它们的文件早已被角色 patch 整体重写)。
- **顺序竞态**:`orchestrator.launch()` 里 `ensurePickerProfile()` 是 fire-and-forget(`detached`),
与 `spawnInstance` 并发;脚本本身幂等且现在不会再写出坏文件,故未改并发模型,
但"脚本运行期间实例正在启动"的时序仍值得后续收敛(例如等脚本退出再 spawn)。