Files
dsh_ai1net_server/dsh-server-docs/04-调整方案/52-新用户实例无法启动.md
T

102 lines
5.1 KiB
Markdown
Raw Normal View 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()` **只剥平台段、保留注释**,于是:
```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)。