102 lines
5.1 KiB
Markdown
102 lines
5.1 KiB
Markdown
# 档案 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)。
|