# 多实例插件框架设计方案（受限容器版）· 最终版

> 状态：**已与用户逐一确认全部设计点，待开工实现**
> 目标：解决「调试插件时把自己搞崩」——宿主会话不死，插件试跑可重复、可回收、可取证、验收后入库。

---

## 一、环境体检结论（今日实测，非推测）

### 运行环境
| 项目 | 结果 |
|---|---|
| 容器 | Ubuntu 24.04，aarch64 |
| 运行时 | Python 3.12+ / Node 24+，标准库齐全，零第三方依赖 |

### 容器能力实测

| 能力 | 实测结果 | 结论 |
|---|---|---|
| `unshare`（UTS/NET/MNT/USER namespace） | 全部失败（Operation not permitted） | ❌ 内核级隔离不可用 |
| `chroot` | Function not implemented（系统调用被禁） | ❌ 文件系统虚拟化不可用 |
| bwrap / firejail / docker / podman | 均未安装 | ❌ 常规沙箱工具不可用 |
| capabilities | CapEff=0（零特权） | ❌ 无特权可借 |
| **setpriv 降 uid** | **假降权！** 实例仍以 uid=0 运行，且能读宿主 700 私有文件 | ❌ uid 权限隔离不可靠 |
| rlimit（内存/进程数/fd） | 实测可设置 | ✅ 资源硬约束可用 |
| 子进程/信号/进程组强杀 | 实测可 kill -9 | ✅ 崩溃隔离可用 |
| UNIX socket / FIFO / 管道 | 实测可用 | ✅ IPC 可用 |
| Python 3.12 / Node 24 | 标准库齐全，零第三方依赖 | ✅ 运行时可用 |

**一句话：移动端的 Android seccomp 过滤器把「电脑上那一套内核沙箱」全部封死了，但「进程级隔离 + 资源限额 + 心跳看门狗」这条路线完全可行。**

---

## 二、方案：进程级多实例（supervisor + 实例）

### 架构

```
┌────────────── 宿主 supervisor.py（我直接调用/驱动）──────────────┐
│  • 实例注册表 + 状态机：created → running → crashed → restarted    │
│  • 看门狗：心跳超时 → setsid 建的进程组强杀（kill -- -PGID）      │
│  • 重启策略：指数退避 0.5s/1s/2s/4s，连续 3 次崩溃 → 熔断止损     │
│  • 崩溃取证：收 stderr + exit code + 状态文件，自动汇总报告        │
│  • 每次 spawn 注入 rlimit 硬限制（内存 512MB、CPU 60s、无 core）   │
└──────┬──────────────┬──────────────┬──────────────┘
       spawn          spawn          spawn
  （setsid 独立进程组；独立状态目录；stdout/stderr 重定向落盘）
       ▼              ▼              ▼
  实例 A           实例 B           实例 C
  状态目录：~/project/instances/<id>/（默认，可用 $DSH_INSTANCES_ROOT 覆盖）
    ├── work/       ← 实例专属工作区（读写自由）
    ├── task.json   ← 宿主注入的任务/快照（聊天记录片段、思考链夹具等）
    ├── state.json  ← 心跳/进度/结果（最后写入时间戳）
    ├── stderr.log  ← 崩溃现场（尾部 200 行即故障报告）
    └── result.json ← 试跑产物（宿主读取后归档）

通信约定（零第三方依赖）：
  • 实例 → 宿主：写 state.json + 心跳（每 2s touch 时间戳）
  • 宿主 → 实例：往 work/ 丢任务文件 + 发 SIGUSR1 唤醒
  • 崩溃判据：心跳超时(5s) 或 SIGCHLD 退出码非 0
```

### 为什么这个「多实例」在移动容器上成立（关键论证）

1. **崩溃本质是进程的，不是环境的**：实例 segfault/抛异常/被 OOM-kill，受影响的只有它自己——不需要 namespace，信号传播天然不跨进程。宿主 supervisor 是独立进程，永远活着。
2. **挂死、死循环可回收**：实例用 `setsid` 启动获得独立进程组，看门狗发现心跳停跳后 `kill -9 -- -PGID` 收回，`timeout` 包一层双保险。电脑沙箱在这点上没有额外优势。
3. **资源失控可约束**：rlimit 实测可用——`RLIMIT_AS=512MB` 防内存泄漏拖垮机器，`RLIMIT_CPU=60s` 防死循环烧 CPU，`RLIMIT_NPROC` 防 fork 炸弹。实例崩了，宿主和 GUI 都无恙。
4. **试跑可重复、可离散**：每次试跑一个全新状态目录，崩溃现场落盘为 stderr 尾 200 行 + exit code + 状态文件，我可以直接读证据、改代码、再跑——**我的调试上下文（推理链）始终在宿主会话里，不随实例崩溃丢失**。

### 防御矩阵（全部实测可用）

| 场景 | 手段 | 验证状态 |
|---|---|---|
| 死循环挂死 | timeout + 看门狗进程组强杀 | ✅ 机制已验 |
| 内存泄漏 | RLIMIT_AS=512MB | ✅ rlimit 可设已验 |
| fork 炸弹 | RLIMIT_NPROC | ✅ 可设已验 |
| 异常崩溃 | SIGCHLD 回收 + 退避重启 | ✅ 信号机制天然可用 |
| 现场丢失 | state.json+stderr.log 落盘 | ✅ 文件系统可写 |

---

## 三、实例与宿主的环境等价性（实测已验证）

**关切**：「测试环境 ≠ 生产环境 → 信息不全 → 漏报」是试跑方案经典陷阱。

**设计原则**：实例 = 宿主「能力面」的完整克隆，而不是裁剪版。

| 维度 | 实例与宿主 | 依据 |
|---|---|---|
| 文件系统 | 同一棵（无 chroot） | 实测 chroot 被禁 → 天然共享 |
| 运行时 | 同一 Python 3.12.3 / Node 24（`sys.executable` 相同） | 无隔离，同一解释器 |
| 依赖/标准库 | 同一 import 面（json/resource/fcntl/signal/socket/multiprocessing/http.server/ctypes/sqlite3 全部一致） | 实测探针一致 |
| 工具链 | 同一 PATH、同一二进制（且**缺失的工具两边都同样缺失**） | 实测探针一致 |
| 环境变量 | 同一 PATH/HOME/TERM 等 | 实测探针一致 |
| 接口面 | DSHA 桥（/app/*）同样可达 | 实测探针一致 |
| **会话上下文** | **不复制**（见下） | 设计决定 |

**实测证据**（`docs/equiv_probe.py`）：宿主直跑 vs setsid 独立进程跑，输出 `diff` **完全一致**。

**唯一不克隆的两样东西，恰好都该不克隆**：
1. **我的会话上下文**（推理链/记忆）——插件若依赖宿主上下文，那是坏插件；在实例里测出依赖 → 正是要暴露的缺陷，而不是「信息不全」。
2. **PID/进程身份**——没有插件应该依赖它。

### 聊天记录策略（注入快照，不开放读取）
- 实例不直接读宿主会话存储；宿主把插件需要的内容**按需注入**：
  - 当前任务上下文 → `work/task.json`（完整注入）
  - 相关聊天记录片段 → 快照（宿主裁剪后注入 task.json）
  - 需全量会话的插件（如对话总结类）→ 宿主生成会话快照文件，实例仍只碰快照
- 收益：等价（与真实插件输入一致）、可重现（快照存档可回放复现 bug）、多实例无冲突、防泄漏（实例永远看不见宿主推理链）

### 思考链类插件测试策略（分两层，已实测演示 ✅）
- **逻辑层 → 实例可测**：思考链是格式契约（schema）的数据流。宿主注入**思考链夹具**（格式合规的假思考链，与真实同构），插件在实例中照常解析/改写/输出；宿主验收 = 契约校验 + 前后 diff（格式合法？只改目标？未破坏其他字段？）。实测：`docs/think_plugin.py`，校验全部通过。
- **效果层 → 任何环境都测不了，用金丝雀兜底**：插件先在宿主以 **--dry-run/观察模式**运行，只输出「修改计划」（diff 摘要，不真改），宿主审查计划后放行生效；出问题一键回滚。
- 线上流程 = 夹具测试（实例，自动）→ 金丝雀审查（宿主，人工）→ 生效/回滚。

### 验收终式（一致性测试）
同一插件、同一输入，宿主直跑 vs 实例跑 → 输出 diff 一致才算验收通过；不一致 = 有环境依赖差异，当场暴露。

---

## 四、插件代码生命周期（验证门槛 + 正式区守护）

**原则：未验证的代码永远进不了正式插件目录。**

```
┌────────────────────────────────────────────────────────┐
│ 正式插件目录（已可用版本的家）                            │
│     ↑ 只有「验收通过」的代码能进来，入口只有我（宿主）    │
└────────────────────────────────────────────────────────┘
        ▲ 写入（唯一入口）
        │ 验证通过
┌────────────────────────────────────────────────────────┐
│ 试跑区：instances/<id>/work/                            │
│     ← 每次修改的版本先落在这里，实例在这执行              │
│       ✓ 跑通 → 宿主验收（契约+一致性）→ 写回正式目录      │
│       ✗ 崩了  → 留在试跑区，崩溃现场进 stderr.log         │
└────────────────────────────────────────────────────────┘
```

- **流程保障**：实例的「唯一可写目录」是自身 `work/`，其中没有任何指向正式插件目录的写路径；写入正式目录只有宿主一条途径。
- **一键回滚**：正式目录每版保留备份（或 git 管理），验收后出问题可随时回到上一可用版本。

---

## 五、浏览器观察窗（实测已验证 ✅）

- supervisor 挂只读看板服务，监听 `127.0.0.1:8765`（仅本机，不暴露局域网）
- 移动端浏览器打开 `http://127.0.0.1:8765/` → 实例状态表（running/crashed/restarted、心跳、CPU/内存、备注），**3s 自动刷新**，崩溃行变红
- `/api` 返回 JSON 数据源；**看板不影响实例，实例崩溃如实显示在页面上**
- **悬浮小窗**：浏览器打开后，从屏幕底部上滑停住 → 最近任务 → 点浏览器卡片「小窗」图标 → 悬浮看板，可边做其他事边围观实例
- 实测结论：容器与移动设备共享网络栈，移动端浏览器可直达容器端口 ✅

---

## 五·五、插件挂载进 DSH 启动链的保险（「插件坏 → DSH 打不开」的解法）

**DSH 现状（源码查证）**：DSH 用 Cordis 插件框架；loader 为**事务式更新 + 失败 rollback**（失败时移除新增、恢复旧配置），支持 **isolate 服务隔离**；但启动阶段插件初始化失败会向上 throw，可能终止启动——**必须外挂保险，不改 DSH 源码**。

**三道闸 + 回滚器（全在插件加载链之外）**：

| 闸 | 时机 | 机制 |
|---|---|---|
| ① 验证后才替换 | 替换前 | 实例加载冒烟测试（与 DSH 启动相同的 import+初始化路径）通过 → 才允许 备份→替换→重启 |
| ② 启动回滚器 | 启动时 | `dsh-guard start`：检测启动失败（超时/非0退出）→ 自动回滚 `.bak` → 重试一次 |
| ③ 安全启动模式 | 兜底 | `DSH_PLUGIN_SAFE=1` 或标志文件 → 跳过全部用户插件，只起 DSH 核心，GUI 必开 |

**结论**：最坏情况 = «干净无插件的 DSH»，不是 «打不开»；回滚器/开关是独立脚本，不参与插件加载链，插件带不崩它。DSH 起来后看加载日志 → 定位 → 修复 → 再走闸①。

**默认策略**：重启后宿主先活 → 检查恢复标志/上次状态 → 询问是否恢复；不自动跑插件（试跑是「工作」不是「系统服务」，手动恢复 = 人永远有控制权）。

---

## 六、已知边界（诚实声明，不掩盖）

- **防 bug，不防恶意**：**可以测试陌生人代码**——实例是独立进程，崩溃/死循环不跨进程传染；但实例与宿主同在 uid 0 且无权限隔离（实测 setpriv 假降权），恶意插件理论上能乱写/乱读全盘，测不可信代码请自行评估风险。若要更强的防恶意，移动端内核层面已无路可走。
- **无网络隔离**：netns 不可用，无法限制实例联网（本方案默认离线调试，无需为此强求）。
- **文件系统视图不虚拟化**：chroot 被禁，实例理论上可见整棵文件系统（同样属于「防 bug 不防恶意」边界）。
- **共享挂载**：实例与宿主共享同一挂载空间，目录隔离靠「约定 + 独立状态目录」，不是硬墙。
- **思考链效果层**：无法预先验证「修改思考链对推理质量的实际影响」，只能靠金丝雀 + 回滚兜底。

---

## 七、调试工作流（我每轮迭代做什么）

```
① edit 插件代码（宿主，无损）           ← 改坏也没事，不会进正式区
② supervisor run --plugin <路径> --id test-007
   → 实例以最小环境跑（rlimit 兜底、看门狗护驾）
③ 读 result.json / stderr.log → 定位崩溃现场
④ 验收：契约校验 + 一致性 diff（宿主直跑 vs 实例跑）
   → 通过才写回正式插件目录（旧版留备份）
⑤ 回到 ①（迭代 N 次，宿主会话一句话都没丢）
全程围观：移动端浏览器小窗看板实时显示实例状态
```

---

## 八、默认参数与实现清单

### 默认参数（已确认）
- 单实例：**512MB 内存 / 60s CPU / 无 core dump**
- 并行实例：**3 个**
- 看门狗：心跳超时 5s；重启退避 0.5s/1s/2s/4s；**连续崩溃 3 次 → 熔断止损**
- 看板：`127.0.0.1:8765`，3s 自动刷新

### 实现清单（确认后动工）
| 文件 | 作用 | 状态 |
|---|---|---|
| `supervisor.py` | 宿主：spawn/看门狗/重启/熔断/取证/看板 | 待实现 |
| `instance_runner.py` | 实例骨架：心跳/状态机/任务执行/结果落盘 | 待实现 |
| `docs/demo_web.py` | 看板原型（已验证，改造为 supervisor 数据源） | ✅ 原型已验 |
| `docs/equiv_probe.py` | 环境等价探针（宿主 vs 实例 diff） | ✅ 已验 |
| `docs/think_plugin.py` | 思考链夹具测试演示（含 dry-run 金丝雀） | ✅ 已验 |

### 开工后第一步
实现 supervisor + runner，跑「**故意崩 3 次再自愈**」演示：实例连续崩溃 → 看门狗退避重启 → 第 3 次熔断 → 崩溃现场完整落盘 → 看板如实显示——小窗里全程可见。
