Files
dsh_shenxian/dsh-server-docs/04-调整方案/52-新用户实例无法启动.md
T
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

5.1 KiB
Raw Blame History

档案 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() 只剥平台段、保留注释,于是:

body = "# Your patch layer …\n# a top-level YAML array …\n# overrides, …\n[]"   // ≠ '[]'
const normalized = body === '' || body === '[]' ? '' : body + '\n\n'            // 判定失效

→ 裸 [] 被原样留下,平台段又追加在其后 → 同一个 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)。