# 隔离舱 v0.2.0 架构说明 | 项目 | 值 | |---|---| | 插件 / 核心版本 | `0.2.0` | | Host 构建号 | `2` | | 侧车协议 | `1` | | SQLite schema | `1` | | 支持的 DSH | `0.2.1+` | | 本地验证宿主 | 桌面版 `0.2.1-rc.2`(Windows x64) | 本文说明 v0.2.0 的分层、进程边界、状态所有权、关键设计决策与验证边界。 跨层字段级契约见 [`PROTOCOL.md`](PROTOCOL.md),升级步骤见 [`MIGRATION.md`](MIGRATION.md), 用户文档见 [`../README.md`](../README.md) 与 [`../使用教程.md`](../使用教程.md)。 ## 1. 目标与非目标 ### 目标 1. **主会话零自动痕迹**:隔离任务不向主会话写消息、不进入主会话模型上下文、不产生自动状态提示。 2. **系统级沙箱**:隔离任务的可写范围限定在用户手动指定的沙箱根目录内;沙箱外只读。 3. **任务会话使用官方界面**:任务是一个真实的 DSH 普通根会话,由官方会话面板渲染,而不是自绘聊天。 4. **业务状态与文件操作在 Rust 内核**:SQLite、任务状态机、队列、超时、路径策略、文件预览、导出/撤销、迁移与诊断由侧车负责。 5. **可恢复、可审计、可清理**:宿主重启后可恢复任务会话;所有返回/导出仍需用户手动确认。 ### 非目标 - 不重写 DSH 的 Agent Loop、模型协议、工具注册或会话日志。 - 不替换官方会话/输入框组件,也不在插件内维护第二套对话转写。 - 不把 DSH 的模型工具调用沙箱重实现一遍;模型工具仍由 DSH 文件/命令沙箱强制。 - 不兼容 DSH 0.1.7;v0.1.3 是 0.1.x 的最后版本。 ## 2. 三层结构 ``` ┌──────────────────────────── DSH Web / Desktop 页面 ───────────────────────────┐ │ 官方会话面板 (main key = conversation) 隔离舱面板 (main key = isolation-pod) │ │ ├─ 官方消息流 / 工具卡片 / 输入框 ├─ 任务 / 新建 / 配置 / 导出 / 诊断 │ │ ├─ conversation.input.dock ← 任务操作条 └─ 打开任务时调用 uiWorkspace.openSession │ │ └─ conversation.composer ← 未恢复时的只读恢复卡 │ └───────────────────────────────────┬──────────────────────────────────────────────┘ │ /isolation-pod/api(激活期令牌) ┌───────────────────────────────────▼───────────────────────────────────────────────┐ │ JS Host 薄壳(Cordis 插件,lib/) │ │ ├─ DSH 适配:agents / sessions / sessionQuery / sandboxPolicy / fs / presets │ │ ├─ Agent 生命周期:create / resume / setup / guard / probe / return │ │ ├─ HTTP + token + 状态缓存 + 诊断 │ │ └─ 只持有进程内句柄;持久业务状态全部在侧车 │ └───────────────┬───────────────────────────────────────────────┬────────────────────┘ │ stdio NDJSON JSON-RPC │ DSH 服务 / 会话日志 ┌───────────────▼───────────────────────────────┐ ┌───────────▼────────────────────┐ │ Rust 侧车(isolation-pod-core) │ │ DSH Session Log(唯一事实来源) │ │ ├─ SQLite WAL:config/tasks/exports/audit │ │ 官方会话面板直接读取并渲染 │ │ ├─ 队列 / 超时 / 租约 / 状态机 │ └────────────────────────────────┘ │ ├─ 沙箱路径策略 / 文件清单与预览 │ │ ├─ 导出 / 冲突 / BLAKE3 / 备份 / 撤销 │ │ ├─ v0.1.3 迁移 / 诊断日志 │ │ └─ 无网络监听;stdio only │ └───────────────────────────────────────────────┘ ``` ### 2.1 职责边界 | 层 | 负责 | 不负责 | |---|---|---| | Rust 侧车 | 配置校验与持久化、任务状态机、FIFO 队列、每轮超时、租约、任务目录、文件清单/预览、导出/撤销/备份、v0.1.3 迁移、诊断 | DSH Agent 创建、模型调用、工具调用、官方会话渲染 | | JS Host | Cordis 生命周期、DSH 服务调用、Agent create/resume/setup、工具 guard、沙箱自检、主会话上下文读取、返回注入、HTTP/令牌、任务摘要缓存 | 跨会话持久状态、文件复制/删除、路径规范化、任务队列 | | DSH | Agent Loop、会话日志、官方 UI、工具注册、文件/命令沙箱、审批策略、Agent preset | 隔离舱业务状态 | | Client | 任务面板、任务操作条、恢复卡片、配置/导出/诊断视图;打开官方会话 | 业务状态事实来源、转写复制 | ### 2.2 进程与线程 - Host 半运行在 DSH 宿主进程内(Cordis 插件),由 DSH 的 Web 服务暴露 `/isolation-pod/*`。 - 侧车是由 Host 通过 `node:child_process.spawn` 拉起的独立子进程;stdin/stdout 走协议, stderr 进入 Host 的诊断环和 `core.log`。 - 侧车主线程顺序处理 JSON-RPC;一个独立的截止时间线程负责 `deadline.expired` 与 强制超时。SQLite 使用 WAL;没有网络监听。 - 页面 Client 半通过 HTTP 与 Host 通信;打开任务时通过 `uiWorkspace.openSession(sessionId)` 切到官方 `conversation` 面板。会话转写由 DSH 会话日志渲染,插件不复制。 ## 3. 状态所有权 | 数据 | 所有者 | 说明 | |---|---|---| | 配置、任务状态、队列、超时、租约、导出记录、审计 | Rust SQLite | 唯一持久事实来源 | | 任务会话事件、工具卡片、思考过程、完整转写 | DSH Session Log | 官方会话面板读取;侧车只保存摘要/状态 | | 进程内 Agent 句柄、任务缓存、诊断环、HTTP token | JS Host | 不落盘;Host 重启后由侧车状态重建 | | UI 选中项、表单草稿、目标路径偏好 | Client | 不构成授权;导出/返回仍需确认 | **会话日志优先原则**:任务的“执行过程”不在隔离舱数据库里复制。`lastText` 只是 最近一条助手文本的摘要(≤ 4000 字),用于列表与返回预览;完整记录以 DSH 会话日志为准。 ## 4. Agent 生命周期 ### 4.1 新建任务 1. Client 调 `startTask {description, subdir?, mainSessionId}`。 2. Host 校验非空描述、绝对沙箱根、未使用的 preset;生成 taskId 与 sessionId,取得当前模型选择。 3. Host 调侧车 `task.reserve`。侧车: - 校验/创建沙箱根与 `//`; - 占用并发槽,或按 FIFO 入队; - 写入任务记录,返回 `SandboxPolicy`。 4. 若 `startNow:true`,Host 在沙箱根 cwd 上创建**普通根会话**: - 不传 `origin:'subagent'`,不传 parentAgent; - `meta.cwd = sandboxRoot`,`meta.agentPreset = resolvedPresetId`; - `agentOptions` 使用宿主当前模型选择。 5. `setup` 窗口(Agent 首次发布、首轮之前)依次: - append `sandbox/mode {mode}`; - append `approval/policy {policy:'never'}`; - 挂载 preset; - 安装 scoped `tools.guard`; - 注入 `isolation-pod/rules` 系统提示段:沙箱根、任务目录、允许工具、只读主会话上下文; - 捕获真实工具面。 6. Host 执行沙箱自检(DSH 策略 + 越界写探针),失败则 dispose Agent、标记任务失败。 7. Host 以**一条干净的用户消息** `任务:` 启动首轮;不把规则/上下文塞进用户消息。 8. 客户端在 `openTask` 后打开官方会话;后续消息由官方输入框直接进入同一个 live Agent。 ### 4.2 恢复 - 宿主重启后,侧车把 `queued/starting/running` 任务标记 `interrupted`,清除旧租约。 - 用户打开任务时,若 `autoResumeOnOpen` 为 true,Host 调 `task.reattach` 获取新租约, 再用 `agents.resume` 重建 Agent 并重新安装隔离 setup;不自动发消息。 - 恢复失败则仍打开历史会话,官方 composer 由 priority −5 的只读恢复卡片接管; 用户可点击「恢复隔离会话」重试,输入框在恢复前不会把消息发给未隔离的 Agent。 - 官方 UI 自己恢复一个已存在的任务会话时,Host 的全局 `agent/created` 安全网会在首轮前 补齐 sandbox/approval/guard/系统提示段,并尽量 `task.reattach` 接管其状态跟踪。 ### 4.3 续聊与租约 - 任务完成后 Agent 仍可保持 live;官方输入框可继续发消息。 - 侧车在任务终结时**保留租约**;同租约的 `turn/start` 会把任务从 terminal 拉回 `running`。 - Host 每 15 秒心跳;租约超过 30 秒无心跳即不可 reattach。 - 停止/取消通过 DSH `agent.cancel` + 侧车 `task.cancel`;超时由侧车 `deadline.expired` 触发 Host 取消。 - 运行中/排队中的任务重启后标记 `interrupted`;旧 Agent 句柄不会跨进程恢复。 ### 4.4 主会话识别(重要) 任务会话本身是普通根会话,cwd = 沙箱根。它会成为官方主视图;但这**不是**用户的主会话。 隔离舱在所有涉及“主会话”的地方都做区分: - 新建任务时由 Client 显式传入 `mainSessionId`; - Client 选择主会话时排除 id 属于任务的会话、排除 `origin:'subagent'`,回退到最近的非 pod 主会话; - 返回主会话使用任务记录的 `mainSessionId`,不使用当前 `mainView`; - 沙箱位置告警只与**用户主会话**的 cwd 比较;Host 若收到 pod 会话 ID,会解析回任务的 `mainSessionId`,subagent 的 cwd 不参与判断。因此“任务会话的 cwd = 沙箱根”不会误报 “沙箱在用户工作区内”。 ## 5. 任务状态、队列、超时 ``` queued ──dispatch──> starting ──setup──> running ──probe──> first turn ──turn/end──> completed │ │ │ ├─cancel────────────> cancelled ├─timeout──> timeout └─restart───────────> interrupted ├─error────> failed └─cancel───> cancelled terminal ──官方输入框 follow-up / task.turnStarted──> running ``` - 并发:`maxConcurrent` 统计 `starting+running`;超限且 `queueEnabled` 时进入 FIFO 队列。 - 超时:`timeoutMs` 是**每轮**上限(30s–2h),不是整个任务。侧车发出 `deadline.expired` 后 给 Host 10 秒结算;Host 另有 `timeoutMs+30s` 看门狗。 - 租约:Host 进程 UUID + 序号,避免旧 Host 句柄覆盖新进程的状态;`STALE_LEASE` 会拒绝写入。 - 重启:`initialize` 把进行中/排队任务标 `interrupted`,把运行中导出标 `interrupted`; 不会自动重放任务。 - 清理:`record` / `files` / `all`;运行中的任务先取消;沙箱根永远受保护。 `maxTasks`/`retentionDays` 在启动时清理终态任务记录;`maxExports` 在启动时与每次导出后清理导出记录;`retentionDays` 不删文件。 ## 6. 隔离与安全边界 | 层 | 手段 | 效果 | |---|---|---| | 模型工具文件写入 | DSH 文件沙箱(`sandbox/mode=workspace-write`,session cwd=沙箱根) | 沙箱外写由宿主硬拒绝(`FS_SANDBOX_DENIED`);这是权威边界 | | 审批 | `approval/policy=never` | 隔离环境内不弹审批;需要审批的动作失败关闭 | | 工具白名单 | 侧车任务快照 + `tools.guard` | 白名单外调用被拒绝;preset 缺失不会清空白名单 | | 补充路径检查 | Host 同步 guard(write/edit 的绝对/相对路径) | 尽早拒绝明显越界;不替代 DSH 沙箱 | | 越界写探针 | 沙箱内写必须成功;沙箱外写必须被拒绝 | 自检失败任务不启动;`skipped-no-parent` 仅限盘符根 | | 侧车文件操作 | Rust 路径归一化、canonicalize、符号链接/联接点拒绝 | 清单/预览/导出/清理不越界 | | 导出 | 绝对目标、沙箱外目标、每次确认、BLAKE3、备份/冲突检测 | 用户手动导出;复制可撤销;不自动延续权限 | | 返回主会话 | 用户编辑预览 + 确认 + 单条 user 消息 ≤8000 字 | 唯一主动写主会话的入口 | | HTTP | 每次激活随机 token、索引注入、bootstrap 浏览器认证 | 未授权页面不能调用本机 API | | 侧车 | 无网络、stdio、参数化 SQL、协议限额、日志不写文件内容 | 最小攻击面 | **已知且有意保留的边界**: - DSH 的 `workspace-write` 允许平台临时目录写入;系统提示要求临时文件写在沙箱内,但 Host 不能用同步手段阻止 shell 写 TEMP。模型工具文件写入仍受 DSH 沙箱约束。 - 官方会话提供的 **Fork** 会创建一个新的普通会话,该会话不受隔离舱 setup 约束。任务操作条 会显示提醒;不要把 Fork 出的会话当作隔离会话。 - 任务会话是普通根会话,可能出现在 DSH 会话搜索/历史中;它不在任何 workspace 列表里, 也不会向主会话写入内容。 - 旧 v0.1.3 迁移任务没有可恢复的 Agent,只保留只读执行记录。 - 官方二进制未做代码签名;SmartScreen/Gatekeeper 可能提示。Linux/macOS 产物由 CI 构建, 本地只在 Windows x64 上做过手工面板验证。 ## 7. 持久化与迁移 - 数据库:`/isolation-pod/pod.db`(SQLite WAL,schema 1)。 - 表:`meta`、`config`、`tasks`、`task_audit`、`legacy_log`、`exports`、`export_items`。 - 写入:SQLite WAL;单条语句原子,状态转移与审计按顺序写入(不是跨语句事务);启动时完整性检查;损坏库不会被静默删除。 - 配置:单行 `config`;任务/导出的容量上限与保留期在启动时执行。 - v0.1.3 的 `isolation-pod.json` 在首次启动时导入;成功后重命名为 `isolation-pod.json.bak`,旧日志进入 `legacy_log`,旧任务标记 `legacy:true`。 - 导出记录带 BLAKE3 哈希与撤销备份目录;旧记录没有哈希/备份,撤销不会校验也无法恢复被覆盖文件。 - 数据库只应由侧车写入;停机后可用只读 SQLite 工具检查。 ## 8. 模块地图 ``` core/ src/main.rs CLI (--version/--check/--help) and stdio loop entry src/lib.rs public module surface + run() src/protocol.rs NDJSON framing, limits, error codes, version constants src/config.rs config defaults, clamping, validation src/store.rs SQLite schema, migrations, CRUD, pruning src/task.rs Task model, statuses, summary JSON, reason mapping src/sandbox.rs path normalization/containment, task dirs, safe file ops src/files.rs listing/stat/read, UTF-8/UTF-16, binary detection, preview caps src/export.rs export planning, copy/move, conflicts, hashes, backups, undo src/migration.rs v0.1.3 JSON importer + legacy log src/diagnostics.rs rotating logs, rings, levels src/api.rs JSON-RPC dispatch, lifecycle, queue, deadline ticker tests/integration.rs real-binary lifecycle/migration/sandbox tests lib/ index.js Cordis plugin entry, lifecycle, duplicate-activation guard core-client.js binary discovery, spawn, NDJSON, restart/backoff, stderr ring state.js task cache/index, live handles, diagnostics, revision agent-runtime.js create/resume, setup, guard, probe, turn observation api.js /status, /bootstrap, /api, sandbox verdict, compat/presets, returnToMain paths.js synchronous path policy for the tool guard version.js plugin/protocol/schema/build constants client.js built Client bundle (from src/client) src/client/ TypeScript/React sources for the panel, dock, composer card scripts/ build-client, build-core, fetch-core, check-version, package test/ host (stub DSH + fake/real sidecar), client, core e2e docs/ PROTOCOL / ARCHITECTURE / MIGRATION ``` ## 9. 构建、测试与验证 ### 9.1 构建 - Client:`pnpm run build:client` → `lib/client.js`(esbuild;external 仅 `react` / `react/jsx-runtime`;`window.__ModuleLoader__.load` 格式)。 - 侧车:`node scripts/build-core.mjs`(Cargo release,复制到 `lib/bin/-/`,校验 `--version`,Windows 检查 MinGW 运行期 DLL)。 - 工具链:Node 24(构建/测试)、Rust stable ≥ 1.85(本地 1.99.1 GNU)、 Windows 上优先 MSVC 或 GNU,并使用 `+crt-static` 链接选项;C/C++ 构建器供 `rusqlite bundled` 使用。 - `pnpm-workspace.yaml` 的 `allowBuilds` 只用于本地/CI 的 esbuild 安装,不随包分发。 ### 9.2 测试矩阵 | 层 | 命令 | 覆盖 | |---|---|---| | Rust 单元/集成 | `cargo test --manifest-path core/Cargo.toml --locked` | 协议、配置、任务、沙箱、文件、导出/撤销、迁移、DB、真实二进制生命周期 | | Host | `node --test "test/host/**/*.test.mjs"` | 桩 DSH 上下文、假侧车协议、真实侧车跨层用例、鉴权、setup 事件形状、guard、探针、resume/reattach、队列、返回、清理、重启 | | Client | `node --test "test/client/**/*.test.mjs"` | 纯函数、主会话选择、SSR 渲染、错误映射、令牌恢复、构建/槽位 smoke | | 类型 | `tsc --noEmit -p src/client/tsconfig.json` | Client 严格类型 | | 二进制 e2e | `node --test "test/core/**/*.test.mjs"` | 真实侧车:生命周期、文件/导出、队列、重启、迁移、打包路径 | | 元数据 | `node scripts/check-version.mjs` | 版本/协议/schema/locale/patch/workflow/客户端 bundle 一致性 | ### 9.3 本地验证结果(2026-10-17,Windows x64) | 项目 | 结果 | |---|---| | Rust 质量门 | `cargo fmt --check` 通过;`cargo clippy --all-targets -- -D warnings` 通过;`cargo test --locked`:**51 lib + 4 integration** 通过 | | Host 测试 | **64/64** 通过(含 3 个真实侧车用例) | | Client 测试 | **41/41** 通过;`tsc --noEmit` 通过 | | Client 构建 | `lib/client.js` ≈204.9 KiB,externals 仅 `react` + `react/jsx-runtime` | | 侧车 e2e | **4/4** 通过(真实二进制、临时 `DSH_HOME`/DB) | | 发布二进制 | `lib/bin/win32-x64/isolation-pod-core.exe`,1,902,592 B,SHA-256 `a34a701da220fc5c1f6b74eb027ac167c22c9284e0ff460684ccd9a42d442701`;PE 导入仅系统 DLL(无 libgcc/libwinpthread/VCRUNTIME) | | 打包 | `dsh-isolation-pod-0.2.0-windows-x64.zip`(zip 内含时间戳,哈希随重建变化;以 `SHA256SUMS.txt` 为准),裸侧车哈希与 zip 内一致,解压后可执行 | | 元数据 | `check-version` 通过;`fetch-core --verify-only` 通过 | | 真机面板 | Windows 桌面版 0.2.1-rc.2 手工走查;人工验收清单见 `使用教程.md` 附录 C(非逐条记录);Linux/macOS 产物由 CI 构建,未做本机手工验证 | > `BUILD` 从 1 升到 2 对应主会话误报修复与预览字节上限接线;协议/schema 未变。 ## 10. 关键设计决策 1. **普通根会话而不是 subagent**:DSH 0.2.1 要求 subagent 会话有 durable parent address; 无父 subagent 无法通过普通 session 路由打开或发消息。普通根会话可被官方 `uiWorkspace.openSession` 打开、可重启恢复、可续聊,同时不向主会话写入任何内容。 2. **不加入 workspace**:任务会话不进入任何 workspace,因此不出现在侧边栏的 workspace 列表; 它只通过隔离舱面板打开。 3. **会话日志是唯一事实来源**:插件不复制完整转写;官方会话面板直接渲染 DSH 日志。 4. **Rust 侧车而不是 napi 插件**:无 Node ABI 绑定、崩溃隔离、跨平台二进制;代价是独立的 stdio 协议和二进制分发。 5. **Host 保留 Agent 句柄、侧车保留租约**:terminal 任务仍可继续使用同一个 Agent;租约在 terminate 时保留,只有重启/失败/删除/reattach 才清除。 6. **探针严格优先**:沙箱外写入被允许时永远失败;只有结果不明确时才由 `warn` 模式放宽。 7. **导出备份而不是盲目删除**:覆盖前备份目标文件;有哈希且 `exportVerify` 开启时撤销才校验; 移动模式的撤销只在源路径空闲时由核心尝试,UI 不把它作为普通能力。 8. **主会话选择与任务会话分离**:任务会话 cwd 是沙箱根,不能参与“当前工作区”告警; Client 与 Host 双层解析,避免误报。 ## 11. 相关文档 - [`README.md`](../README.md):安装、使用、配置、HTTP、数据布局、限制与差异。 - [`使用教程.md`](../使用教程.md):从安装到验收的分步操作。 - [`PROTOCOL.md`](PROTOCOL.md):侧车/Host/Client 的字段级契约。 - [`MIGRATION.md`](MIGRATION.md):v0.1.3 → v0.2.0 迁移与回滚。 - [`../CHANGELOG.md`](../CHANGELOG.md):版本历史。 - [`../NOTICE.md`](../NOTICE.md):第三方依赖与许可。