# dsh-verify-isolated 架构与运行机制(图解)
> 包:`@wingsky-1/dsh-verify-isolated` · 源码:`packages/dsh-verify-isolated/` · 版本:0.2.0
> 功能一句话:**DSH 插件开发的隔离环境浏览器验证 skill**——临时 `DSH_HOME` + 独立
> `verify_<随机>` profile 双重隔离,一键拉起隔离 `dsh web`,退出自动清理,
> 不污染正在使用的 `web` profile。
>
> 快速上手(安装 / 使用)见 [包 README](../../packages/dsh-verify-isolated/README.md);
> 本文讲**原理与运行机制**。
---
## 1. 总体设计:宿主空壳 + skill 载体
本插件**没有宿主逻辑**——`src/index.ts` 只导出 `name` 与**空 `apply()`**(满足门禁),
skill 注册完全由 `cordis.patch.yml` 配置的官方 provider 承担:
```mermaid
flowchart LR
subgraph npm["@wingsky-1/dsh-verify-isolated(随包分发)"]
S["skills/dsh-verify-isolated/
SKILL.md + scripts/verify-isolated.mjs"]
P["cordis.patch.yml"]
IDX["lib/index.js(name + 空 apply)"]
end
subgraph loader["dsh loader(profile)"]
BASE["baseUrl 锚定 profile"]
REQ["createRequire(baseUrl).resolve(
'@wingsky-1/dsh-verify-isolated/package.json')"]
DIR["dirname(包根) + /skills"]
end
P -->|"insert: @deepseek-ai/dsh-skill-filesystem
providerName + bundledSkillDir(!!js)"| BASE
BASE --> REQ
REQ --> DIR
DIR -->|"官方 provider 扫描发现"| S
```
- **skill 注册机制**:`cordis.patch.yml` 复用官方 `@deepseek-ai/dsh-skill-filesystem`,
配置 `bundledSkillDir` 为 `!!js` 表达式——从**安装后的 npm 身份**解析包根再 `join
('skills')`(npm 副本 / `link:` 开发态 / 仓库 checkout 三种形态均正确,不以路径拼接
猜测安装位置);官方 provider 扫描 `skills/dsh-verify-isolated/SKILL.md`
(frontmatter `name: dsh-verify-isolated`)并注册,无需自写注册代码;
- **`!!js` 求值环境**:`baseUrl` 是 loader 为 profile 提供的模块解析锚点(patch 注释
为唯一实证来源);`createRequire(baseUrl)` 得到以该锚点为根的 require;
- `package.json` `dsh.bundle.patch → ./cordis.patch.yml`;`files` 含 `skills`——
skill 目录随 npm 包发布。
---
## 2. 一键脚本:四重隔离的流程
`skills/dsh-verify-isolated/scripts/verify-isolated.mjs`(node ≥22 实现,原 bash 版
`verify-isolated.sh` 已随 #517 C8 重写删除,不留 shim;退出码契约
0/1/2/130/143;可选隔离审计 `--audit` 见 §2.1):
```mermaid
sequenceDiagram
autonumber
participant U as 用户 / agent
participant SH as verify-isolated.mjs
participant T as 临时 DSH_HOME(mkdtemp)
participant P as verify_8位随机 profile
participant D as dsh CLI
U->>SH: node verify-isolated.mjs --port 3456 插件包路径
SH->>T: DSH_HOME = mkdtemp(隔离凭据/会话/home 级 patch)
SH->>T: 建默认证据目录 $DSH_HOME/evidence/(B7;--evidence-dir 外部化)
SH->>P: dsh plugin --profile verify_随机 list
(显式初始化 profile,失败即报可操作错误)
SH->>P: node 注入 dsh.profile.bundles:
@deepseek-ai/dsh-base + @deepseek-ai/dsh-web-app
(按名从 dsh 安装目录解析,不走 npm)
SH->>SH: 插件参数归一化内建(C11 语义)
(相对路径按 cwd 绝对化,规避 dsh 当 git URL,#517 C11)
alt 默认(BUILD=1)
SH->>D: 对每个包 pnpm build(确保 lib/ 或 dist/ 产物;--no-build 校验产物 + 陈旧警告)
end
SH->>D: dsh plugin --profile verify_随机 add 包路径
(相对路径基于 cwd 绝对化,包规格原样透传)
SH->>SH: --audit 时 t0 基线快照(lib/audit.mjs scanSnapshot:
挂载 link: symlink 之后、脚本写面前,先扫后写 + 白名单双保险)
SH->>D: dsh --profile verify_随机 --host 127.0.0.1 --port 端口 --no-open
(后台子进程,stdout/stderr 收集到 $DSH_HOME/dsh.log)
SH->>SH: 就绪断言(轮询 HTTP 2xx-4xx + 进程存活,15s 超时)
SH->>T: 就绪后写 $DSH_HOME/verdict.json(B6,0o600,端口三通道 source)
Note over SH: Ctrl+C / SIGTERM → 统一清理(kill dsh → browser quit →
审计(--audit)→ verdict 终态 cleanup → rm -rf DSH_HOME),透传 130/143
```
双重隔离的层次(为什么两层都必要):
| 隔离层 | 做法 | 隔离内容 |
|---|---|---|
| 第一层:临时 `DSH_HOME` | `DSH_HOME=$(mktemp -d)` | 凭据、会话、全部用户数据、home 级 `cordis.patch.yml` |
| 第二层:独立 profile | `verify_<8位随机>`(非 `web`) | 插件组合栈(bundles)、profile 级 patch、插件依赖 |
| 第三层:独立端口 | `--port` 自选/探测空闲端口 | 与主 `dsh web` 及其它验证实例互不冲突 |
| 第四层:独立浏览器实例 | `--browser`(自带 browser-driver.mjs,raw CDP) | 页面/tab/console 完全独立,多会话并行互不可见 |
> **只建独立 profile 不够**——profile 共享 home 级凭据与会话;必须同时把 `DSH_HOME`
> 指向临时目录才与真实环境完全隔绝。验证结束删除临时 DSH_HOME 与 `verify_*` profile。
### 2.1 隔离审计(B4,可选 `--audit`)
判定面 = **预置白名单**(版本化 `WHITELIST_V`,`scripts/lib/audit.mjs` 纯函数:
`scanSnapshot` / `diffAgainstWhitelist` / `checkSymlinkEscape` / `runAudit`)外的
新增/删除/修改(纯 stat 路径级,不读内容;白名单内变化忽略——dsh 重写 settings
是常态;未知顶层路径 → 可疑):
```text
profiles/**、*.json/*.jsonl/*.log(仅顶层)、.credentials.yaml、browser.state、
browser-profile/**(整树白名单 + 跳过深扫)、evidence/**、audit/**、storages/**(dsh 官方存储写面)、
dsh.log、verdict.json
```
**白名单分工**:静态白名单覆盖 dsh 固定写面(`.credentials.yaml` /
`storages/**`)与脚本/官方形态写面;随 dsh 版本漂移的面(如
`profiles/node_modules/**` 官方 bundle link,指向真实 dsh 安装目录、越界但合法)
由 **t0 动态基线**覆盖——就绪后扫描进基线,t0 已存在且目标未变的外部 symlink
(`link:` 挂载点)合法不报。
- **symlink 防逃逸**:快照 lstat 不跟随(不读链接目标内容);`t1` 时**新增的**或
**目标变化**且 resolve 后在**所在扫描根**(`$ISOLATED_HOME` 或
`--audit-extra-dirs` 目录)外的 symlink 报「越界 symlink」(防插件经 symlink
写回主 checkout);`t0` 已存在且目标未变的外部 symlink(`link:` 挂载点,profile
node_modules 全 link: 是挂载机制本身)合法不报。防逃逸优先于白名单——
`profiles/**` 内新增越界 symlink 同样报。
- **时序**:`t0` 基线在**就绪断言成功之后**(dsh 启动期自身写面与官方 bundle link
进基线——语义为「就绪后运行期写面审计」,审计面 = 就绪后、退出前的增量写面;
verdict 中间态在其后写入,先扫后写 + 白名单双保险);审计 diff 插在
settle「kill dsh → browser quit → **审计** → verdict 终态 → rm」;
`--keep` 落 `$ISOLATED_HOME/audit/audit.json`,否则随 `--json` 终态 verdict
输出 `audit` 字段(错误路径错误 JSON 恒带 `audit` 字段与 verdict 对齐);
就绪前退出/超时路径 auditBaseline 为 null → 审计跳过不报。
- **局限**:只扫 `$ISOLATED_HOME` 子树 + `--audit-extra-dirs