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
68 · 候选池启停的 root 属主污染:根治
- 日期:2026-09-12
- 触发:投放
@dsh-local/[email protected]时,guest 升级失败(EACCES: permission denied, open '.../node_modules/.bin/modlens') - 结论一句话:不停在"把它 chown 回去",而是三层一起做 —— 修根因(候选池启停改为
setpriv降权)、加防复发(每次改插件前自动属主自愈)、清存量(561 项)。 - 状态:✅ 全部完成并验证
TL;DR|结论:门户候选池的
install/uninstall一直以平台进程身份(root)跑 pnpm,装出来的文件属主是 root → 该用户此后任何pnpm add/remove都必然EACCES。 关键:guest profile 下积了 561 个 root 属主项;平台脚本(ensure-biz-plugins.cjs等)用的是 setpriv 正姿,两条路线不同所以这个 bug 长期没暴露。 状态:✅ 已上线(修根因 + 自愈 + 清存量三层)
一、现象与规模
| 用户 | profile 下 root 属主项 | 分布 |
|---|---|---|
| guest(uid 100002) | 561 | .pnpm 555 / @liustack 2 / .bin 2 / dsh-univer-office 1 / 一个 .bak |
| admin(uid 114801) | 5 | 全在 .pnpm |
表现:该用户无法升级或启用任何插件 —— 以用户身份跑 pnpm add 时连旧文件都覆盖不了。
二、根因
src/web/routes/business-plugins.ts 的 install() / uninstall()(改前):
execFileSync('pnpm', ['add', 'file:' + plugin.tgzPath, ...], { cwd: dir, env: pnpmEnv(user.id) })
没有 setpriv 降权 → 以平台进程身份(root)执行 pnpm → 产物属主是 root。
证据吻合:audit_log 记录 guest 在 01:58 通过候选池启用 @liustack/modlens + dsh-univer-office —— 正是这批 .pnpm / @liustack / dsh-univer-office 项的来源。
为什么长期没暴露:平台侧脚本(ensure-biz-plugins.cjs / ensure-portal-entry.cjs)用的是 setpriv --reuid <uid> --regid <uid> --clear-groups 正姿;档案 43 修「平台 root 污染用户工作区」时也只覆盖了脚本那条线。门户候选池是第三条线,没人走过。
三、修复(三层,按"长期最佳"而非"先能跑")
3.1 修根因 —— install() / uninstall() 改为以用户身份执行
新增两个 helper 并替换两处调用:
| helper | 职责 |
|---|---|
uidOf(userId) |
取该用户 uid(读 <root>/home 属主 —— 编排器创建时已 chown,少一处 DB 耦合) |
runPnpmAs(userId, args, cwd) |
setpriv --reuid <uid> --regid <uid> --clear-groups env HOME=<ws> pnpm … |
healOwnership(userId) |
见 §3.2 |
HOME 仍指向 <root>/ws(pnpm 的 store/cache 落在这里,不设会报 ERR_PNPM_UNEXPECTED_STORE)—— 原 pnpmEnv() 因此变成死代码,已删,其说明移到 wsDir() 上。
3.2 防复发 —— 每次改插件前跑一次属主自愈
healOwnership(userId):find <profile> -user root 数一遍,有则 find … -exec chown -h <uid>:<uid> {} +。
- 放在
install()/uninstall()最前面(否则以用户身份跑 pnpm 连旧文件都覆盖不了); - 用
-exec … +而非一次传全部路径 —— 561 条会撞命令行长度上限; - 用
chown -h——.bin/*是指向别处的符号链接,要改链接本身而非其目标; - 命中时写
heal_plugin_ownership审计(含阶段与条数)。
这一层才是"长期"的关键:即便将来别的写入路径又污染了属主,用户下次启停插件时会被自动清理,不必再人工排查一次。
3.3 清存量 —— 手工跑一次同样的清理
对两个用户的 profile 执行与 healOwnership 等价的 find … -exec chown -h … {} +(属主污染是数据错误,修代码不会自动修正已存在的 561 个文件)。
四、验证
| 项 | 结果 |
|---|---|
npm run typecheck / build |
exit 0 |
| 部署 | 备份 .bak-ownfix-20260912 → scp → md5 一致;服务 active + HTTP=200 |
| 清存量 | guest 剩余 0 / admin 剩余 0 |
| 投放 0.2.5 | admin ✅ 0.2.4 → 0.2.5;guest ✅ 0.2.3 → 0.2.5(修复前该步必失败) |
| 平台代码的 setpriv 路径 | ⏳ 待用户下次在「功能管理」里启停插件时自然验证(该路径需用户会话) |
五、附带修复:npm pack 把上一版 tgz 打进了产物
打包 0.2.5 时发现包体从 8.9 KB 涨到 19.3 KB —— 因为目录里上一版留下的 business-plugins-0.2.4.tgz 被一并打进 package/,且会被永久带进用户的 node_modules。
修复:加 .npmignore(*.tgz 等)而不是"每次记得先手工删" —— 同样是"长期最佳优先于补丁"。重打包后 9.5 KB / 4 个文件,与预期一致。
六、红线遵守
- R7:批量
chown限于两个用户的 profile 目录、只改属主不改内容;执行前已在汇报中列出污染清单与规模(561 / 5)。 - R8:重启平台服务(实测有实例在跑时约 90 秒、无实例约 3 秒),已在上一轮汇报中说明影响。