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 一律写「远程服务器」。
5.1 KiB
5.1 KiB
档案 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)。