# DEV-SANDBOX — dsh 沙盒测试实例运行手册 > 铁律:**凡碰 boot 链路(profile 组合 / bundle 挂载 / loader 装载 / settings 热更)的改动, > 一律先在本文档描述的独立实例里跑通,再上生产。生产 `~/.dsh` 与生产宿主禁止裸改。** > **占位符约定**(本文档刻意不写死任何一台机器的家目录与私有服务名): > `$D` = 沙盒 `DSH_HOME`(例:`$HOME/dsh-dev-home`);`$PKG` = 本仓库绝对路径; > `` = 同机另一枚带 Web 面板的编排插件(只在「它也带 `dsh.client`、 > 它也有这份状态目录」的语境下被提到,与本包构成陷阱 5 的双挂关系); > `mcp-ext` / `<外部 MCP 服务>` / `<内网后端 IP>` = 局域网上那台自建 MCP 后端; > `<项目目录>` / `<工作目录>` = 个人工作树里的中间层目录名;`` = 本地构建的 > prerelease 标签。**端口(生产 3080 / 沙盒 3085)与 pid 保留原值**——它们是这份 > 记录可复现的一部分,也不含任何身份信息。 | 项 | 值 | | --- | --- | | 二进制 | 全局 `dsh` CLI(`command -v dsh` 取实际路径,npm 全局装)。**2026-09-15 升级彩排后 = `0.1.6-alpha.1`**;生产宿主进程仍在内存里跑 `0.1.5-rc.2`(未重启),重启生产后两者再次同版本 | | 沙盒 home | `$D`(`DSH_HOME` 指过来) | | 端口 | **3085**(生产 3080,互不可见) | | 宿主日志 | `dsh-dev-home/host.log` | | 插件日志 | `dsh-dev-home/dsh-tool-guard.log`(**必须**用 `DSH_TOOL_GUARD_LOG` 显式重定向,见陷阱 3) | | 常驻? | **不常驻**。验完即停,需要时再按「启动」一节拉起 | 用同一份已装二进制(而不是 git 重新构建)是刻意的:boot 链路行为与生产一比一零漂移, 沙盒里跑绿了才等于生产会绿。⚠️ 唯一例外是**「共享二进制已升、生产宿主未重启」的窗口期**: 此时沙盒跑的是新版本、生产宿主跑的是内存里的旧版本,沙盒的绿**不代表**生产重启后安全—— 生产侧的判定要以「生产 home 的 preset 副本是否已重铺」为准(见文末「0.1.6 彩排记录」)。 ## 隔离边界(靠什么保证不污染生产) `DSH_HOME` 的优先级是 `配置项 > $DSH_HOME > ~/.dsh`(`@deepseek-ai/dsh-home-paths/lib/index.js` 的 `resolveDshHome`;空串/纯空白视为未设)。设了它之后,`sessions/`、`storages/`、 `profiles/node_modules`(共享 module fallback 符号链接农场,由 `healProfilesModuleFallback` 自动重建)、 的编排状态(`/{board,metrics,orchestration-ledger.json}`,路径一律按 `process.env.DSH_HOME` 派生,见 `/lib/index.js:240`、`lib/usage-aggregator.mjs:64`、 `preset/tools/broker.mjs:166`)全部落在沙盒内。 **唯一逃逸点**:dsh-tool-guard 的文件日志路径写死 `~/.dsh`(见陷阱 3),必须显式覆盖。 ## 启动 ```bash D="$HOME/dsh-dev-home" ss -ltn | grep -q ':3085 ' && { echo "3085 已被占用,先查占用者"; exit 1; } setsid nohup env -u DSH_SESSION_ID -u DSH_WEB_URL -u DSH_SHELL \ DSH_HOME=$D \ DSH_TOOL_GUARD_LOG=$D/dsh-tool-guard.log \ DSH_TELEMETRY_DISABLED=1 \ dsh web --port 3085 --no-open > $D/host.log 2>&1 < /dev/null & sleep 12; ss -ltn | grep 3085; tail -3 $D/dsh-tool-guard.log ``` `setsid` 是硬要求:从代理/工具会话里起的进程若留在原进程组,shell 一收摊就跟着陪葬。 - `-u DSH_SESSION_ID/DSH_WEB_URL/DSH_SHELL`:从当前 dsh 会话里启动沙盒时**必须剥**,否则子 宿主继承会话身份变量(尤其 `DSH_HOME` 会被后面的显式赋值覆盖,但这三个没有理由带着)。 - `--no-open`:不弹浏览器。 - `DSH_TELEMETRY_DISABLED=1`:沙盒不上报。 ## 停止 **只认监听者的 PID**,从 socket 反查,别用 `pgrep -f` 的字符串匹配: ```bash PID=$(ss -ltnp 2>/dev/null | grep ':3085' | grep -o 'pid=[0-9]*' | cut -d= -f2 | sort -u) echo "沙盒宿主 = ${PID:-无}"; kill $PID; sleep 5 ss -ltn | grep ':3085' || echo "3085 已释放" pgrep -a node | grep "port 3085" || echo "无残留 node 进程" ``` - 停干净的判据:**端口不再监听 + 无残留 node 进程**,两者都要看。 - `pgrep -f "…port 3085"` 会把**发起停止命令的那个 shell 自己**也匹配进去(它的 cmdline 里 就带着这串字面量),`head -1` 一旦选中它就是你自杀——实测踩过一次(表现为工具结果里 `[killed by signal: SIGTERM]`,且沙盒宿主反倒没死,得再补一刀)。 - 绝不要 `pkill -f dsh`:那会顺手杀掉生产宿主。 ## 验收清单(沙盒存在的意义) | # | 项 | 通过判据 | | --- | --- | --- | | a | 干净启动 | `host.log` 除 deny-entry 提示外无 error/Error;无 `resolves from multiple active Loader sources`;无 `duplicate loader entry id` | | b | 屏蔽链全绿 | `dsh-tool-guard.log` 依次出现 `[apply] === apply begin`、`[apply] config={} settings: present tools: present`、`[settings] namespace registered`、`[assemble] initial denySet: [...]`、`[guard] guard registered`、`[rpc] rpc channel registered at /dsh-tool-guard`(**无尾斜杠**:webServer 按前缀直配,带斜杠的旧写法已在实测里判死) | | c | 热更 | 往 `dsh-dev-home/settings.yaml` 的 `dsh-tool-guard.denyTools` 追加一项(如 `- sample_tool`)→ 数秒内出现 `[hotreload] hot reload applied ns=dsh-tool-guard kept=N prev=X next=Y`,且 `host.log` 有 `deny list updated: X -> Y`;删掉该行 → 反向 `prev=Y next=X` | | d | 可访问 | 从 `host.log` 取 `token=` → `curl -sL -c /tmp/dv.jar "http://127.0.0.1:3085/?token=$TOKEN"` 返回 200 + ``/`DeepSeek Harness`(直接裸访问 `/` 是 401,`?token=` 会 303 换 cookie) | | e | LLM 链路 | `mkdir -p $D/scratch && cd $D/scratch && DSH_HOME=$D dsh --profile headless "只回复四个字:链路已通"` 有模型输出且 `[exit=0]` | | f | 面板 RPC 形状 | 拿 d 的 cookie 直投 RPC:`curl -s -b /tmp/dv.jar -X POST -H 'content-type: application/json' -d '{"type":"client-request","rpcId":"x","method":"loadSettings","payload":{}}' "http://127.0.0.1:3085/dsh-tool-guard/loadSettings"` → 信封 `server-response` + `result.value` 键集含 `denyTools/roster/descriptions/removed/revision`;`roster` 里**不得**出现 `read/write/edit/bash/run_code`;`descriptions` 与 `roster` 同集合(无孤儿键)、全部单行(宿主侧压平换行、单条封顶 4000 字符)。**只读端点**,验完不动沙盒名单 | | g | 客户端半活读 | 改 `src/client.js` → `npm run build:client`,**不重启宿主**,重取 `/` 里的 `/plugins/??…&rev=…` 合并串再拉一次(体积会随 `rev` 变),在返回体里 grep 新代码独有的串(注意 esbuild 默认 `charset=ascii`,中文全是 `\uXXXX`,要先解码再匹配)。2026-09-15 实测:boot 之后重建的产物立刻出现在下一次响应里 ⇒ **浏览器半按请求读盘,改面板只需硬刷新;改 `lib/` 才需要重启** | > ⚠️ e 项**必须显式带 `DSH_HOME=$D`**:`dsh` 是独立子进程,不继承「启动」一节里那行 > `env DSH_HOME=...`(那只作用于宿主进程),漏了它就等于拿**生产 `~/.dsh`** 跑 headless > ——会话落进生产 `sessions/`,直接踩红线。首建时的那条命令是裸的,2026-09-15 已修正。 验收 b 的 `dead entries` 段列出的是当前不存在的名单条目(生产同款现象,<外部 MCP 服务> 侧工具已改名/下线), 不是故障——呈示层照旧按名单过滤,MCP 重连后即命中。 ## 已知陷阱(都是实测踩出来的) 1. **home 层 `cordis.patch.yml` 不能只剩注释**。纯注释文件解析成 `null`,宿主直接拒起: `dsh: patches .../cordis.patch.yml must be a top-level YAML array of loader patch entries`。 留一个 `[]` 兜底——沙盒的 home 层就是靠末尾这个 `[]` 活着的,别删。 2. **手改 `settings.yaml` 缩进错误 = 热更静默不生效**。宿主对坏 YAML **完全无提示**: 进程活着、端口照服务、`dsh-tool-guard.log` 一条 hotreload 都不写。写前先 `python3 -c "import yaml,sys;yaml.safe_load(open('settings.yaml'))"` 过一遍, 或改完必须看到 `[hotreload]` 行才叫改成功。 3. **日志路径三级解析,启动命令里的显式覆盖仍是唯一稳态** (`lib/log.js` `defaultLogPath()`:`DSH_TOOL_GUARD_LOG` 全路径 > `$DSH_HOME/dsh-tool-guard.log` > `~/.dsh/dsh-tool-guard.log`;两 env 空串/纯空白一律视为未设)。第二级已经堵住 「沙盒往生产日志灌」这个洞,但依赖它等于依赖宿主把 `DSH_HOME` 透传进插件进程; 显式那行覆盖不省——它是这份记录里唯一被实测钉住的路径。 4. **`node_modules` 不能拷**。profile 里的 `link:` 条目是**相对**软链(生产那份是 `../../../../Desktop/<项目目录>/dsh-tool-guard`,从 `~/.dsh/profiles/web/` 上跳四级才落到家), 换基址后同一串相对路径会跳到别处。沙盒必须在新目录里实跑 `pnpm install`;跑完 dev 那份 变成 `../../../../<项目目录>/dsh-tool-guard`——基址不同,跳的级数自然不同。 顺带:`pnpm install` 的结果 `reused 13, downloaded 0`,全走本地 store,秒级完成。 5. **双 loader 来源启动即死(本沙盒的头号复验对象,已两种死法实测)**:带 `dsh.client` 的 包在 home 层 `cordis.patch.yml` 再以绝对路径 insert 一次,即与 bundles 挂载构成冲突—— - insert 的 `id` 与 bundle 挂载同名 → `duplicate loader entry id: dsh-tool-guard`; - insert 的 `id` 换个名 → 生产同款: `client-modules: package dsh-tool-guard resolves from multiple active Loader sources: ... ; remove one entry` (且此前一行还会先打出 `[settings] namespace registration failed: settings namespace "dsh-tool-guard" is already registered`) 两种都**进程直接退出(exit 1)、端口根本不监听**——即"改了 boot 链路后宿主起不来"在 沙盒里的可观测信号就是:`ss -ltn | grep 3085` 空 + `host.log` 有 Error。 ## 重建沙盒(dev home 丢了/污染了) ```bash D="$HOME/dsh-dev-home"; P=$HOME/.dsh mkdir -p $D/profiles/web # 必需四件(凭据/设置/署名/preset)——一次照抄,之后各实例独立演进 cp -a $P/.credentials.yaml $P/settings.yaml $P/.anonymous-user-id $D/ cp -a $P/.agent-presets $D/ # 440K;不拷也行, 的 ensurePresetInstalled 会自举 cp -a $P/profiles/web/pnpm-workspace.yaml $D/profiles/web/ # package.json / cordis.patch.yml 手写:bundles 只留 # @deepseek-ai/dsh-base, @deepseek-ai/dsh-web-app, # @deepseek-ai/dsh-experimental-agent-team-profile, # @deepseek-ai/dsh-experimental-agent-team-web-profile, , dsh-tool-guard # dependencies 里同步只留 agent-team 两条 + /dsh-tool-guard 两条 link: cd $D/profiles/web && pnpm install --prefer-offline ``` 刻意**不拷**:`sessions/`(29M)、`attachments/`(21M)、`tts-audio/`(6.7M)、 `/`(4.4M,生产编排台账与 board)、`storages/`、各类 `.bak-*`、`live2d-companion/`、 `dsh-html-viewer/`。沙盒要的是 boot 链路,不是生产历史。 刻意**不挂**的 bundle(与生产有意的偏差,全部因端口/单实例风险): `dsh-tts-player`、`dsh-html-viewer`、ui-skin 系三条。 因此 profile 层 `cordis.patch.yml` 里的 dsh-skin 托管段一并删掉, 免得产出掩盖真异常的 skipped-patch 噪声;`mcp-ext` 与 `compaction-basic` 保留(<外部 MCP 服务> 与 newapi 都在 `<内网后端 IP>`,实测端口 OPEN,与生产共用同一后端)。 **2026-09-15 变更**:`dsh-live2d-companion` 已加回(link + bundle),为升级彩排的 V4 (`/live2d/game/presets` 的 `defaultId`)取证。它**不是** `dsh.client` 插件,所以不参与 陷阱 5 的单来源约束;profile 层给它写了 `widget: false / pet: false`(生产是 `widget: false / pet: true`)——路由注册与 pet/widget 开关无关(`index.js:915`), 关掉 pet 只是免得彩排期间真弹出一个 Electron 桌宠。 `profiles/headless` 是验收 e 顺手由宿主从内置模板自举出来的(`dsh-base` + `dsh-headless`), 不用手工建——这也是 `dsh --profile ` 的通用行为:目录缺失即按 `PROFILE_TEMPLATES` 生成。 ## 负样本复现(可选,但这是沙盒最硬的证据) 把 `dsh-dev-home/cordis.patch.yml` 末尾的 `[]` 换成下面这段(缩进照抄),再按「启动」拉起: ```yaml - insert: - id: dsh-tool-guard # 与 bundle 挂载同名 → 死法 A;改名 → 死法 B name: $PKG/lib/index.js ``` 验完**立刻还原 `[]`**。两份现场留在 `dsh-dev-home/`:`negtest-sameid.log`(A)、 `negtest-distinct-id.log`(B)。 ## 实测记录(2026-09-15 首建 · 五项验收全绿) ``` # a 干净启动:host.log 除 deny-entry 提示外零 error,仅一行 dsh web: http://127.0.0.1:3085/?token=… # b 屏蔽链(dsh-tool-guard.log,pid=1079648 那次全绿) [apply] === apply begin pid=1079648 === [apply] config={} settings: present tools: present [settings] namespace registered [assemble] initial denySet: [mcp__ext__…, …]; dead entries: … [guard] guard registered [rpc] rpc channel registered at /dsh-tool-guard/ (loadSettings | saveSettings) # c 热更(追加 sample_tool / 还原各一次;插件日志与宿主日志各记一遍) [hotreload] hot reload applied ns=dsh-tool-guard kept=18 prev=17 next=18 [hotreload] hot reload applied ns=dsh-tool-guard kept=17 prev=18 next=17 [dsh-tool-guard] deny list updated: 17 -> 18 # ← host.log [dsh-tool-guard] deny list updated: 18 -> 17 # ← host.log # d curl 200 / 28777 bytes / DeepSeek Harness(裸 / 是 401,?token= 走 303) # e dsh --profile headless → 模型输出「链路已通」+ reasoning,exit 0 ``` 停机后核对生产侧:`~/.dsh` 六件(`.credentials.yaml`/`settings.yaml`/`cordis.patch.yml`/ `profiles/web/{package.json,cordis.patch.yml,cordis.yml}`)sha256 全部 OK, 生产 `~/.dsh/dsh-tool-guard.log` 大小与 mtime 零变化,生产宿主 pid 存活。 ## 实测记录(2026-09-15 第二轮 · DSH 0.1.6-alpha.1 升级彩排 · a–e + V1/V2/V3/V4/V6 全绿) 现场日志(都在 `dsh-dev-home/`):`host-0.1.6-acceptance.log`、`dsh-tool-guard-0.1.6-acceptance.log`、 `negtest-0.1.6-sameid.log`、`negtest-0.1.6-distinct-id.log`、`host-final-acceptance.log`、 `host-autosync-probe.log`。完整报告:`<工作目录>/rehearsal-0.1.6/REHEARSAL-REPORT.md`。 ``` # a/b/d 与首建一致(pid=1692980 那次):host.log 非 deny 行只有 1 行 URL,零 error; # guard 六段位全绿。差别一处:rpc 行尾斜杠没了—— # [rpc] rpc channel registered at /dsh-tool-guard (loadSettings | saveSettings) # (源码仓已含 RPC 修复,link 挂载直接吃到;上面 b 表的 `/dsh-tool-guard/` 是修复前的写法) # e DSH_HOME=$D dsh --profile headless "只回复四个字:链路已通" → 输出「链路已通」,exit 0 # V1 客户端 boot:58 个 client 插件挂在同一 preload,dsh-tool-guard / 各一份(无重复 id); # 合并 bundle /plugins/??… 12,079,571 bytes / 200,内含「工具屏蔽」卡片代码 # V2 preset:修复版 broken=NO;把同一棵树退回旧行(workflow-worker-thread)作 A/B 负样本 # → broken=YES「row "workflow-worker-thread" names a plugin that cannot be resolved」 # live roster(POST /api/agentPresets/list):standard/ptc/minimal/cordis/ 全部 broken=NO # V3 tool-ralph:POST /api/session/create {agentPreset:""} → 挂载成功 # (mountPreset 的 inactiveRows 只要有一行未激活就抛,故成功即证明 preset 里那行 ralph 已激活), # host.log 零 `duplicate loader entry id` / 零 preset-plane 冲突 # V4 live2d:GET /live2d/game/presets → 200,defaultId=standard(沙盒 settings.yaml 无 agent-presets 段, # 落 `config.default`;生产同样没有该段 ⇒ 默认人格是 standard 而非 ,属既有事实不是漂移) # V6 负样本(0.1.6 下双挂仍旧启动即死,约束未被放松): # 死法 A 同 id → duplicate loader entry id: dsh-tool-guard(端口不监听,exit 1) # 死法 B 改名 → client-modules: package dsh-tool-guard resolves from multiple active Loader sources # (前一行仍有 settings namespace "dsh-tool-guard" is already registered) ``` **新发现(写进 runbook,别再踩)**:`` 的 `ensurePresetInstalled` 在**插件 apply 期** 会按 marker 摘要(`版本+preset 树内容 sha16`)自愈已安装副本。实测把沙盒副本退回旧行 + 旧 marker (`0.5.0-.2+3a8033309291f14a`)后直接拉起,`host.log` 自动打出 `[] preset synced to … (v0.5.0-.2+8ab4a81c451e0ff1)`,副本当场换成 `workflow-ptc` 行。⇒ 官方的 `rm -rf .agent-presets/` + 手跑 `ensurePresetInstalled` 是**保险不是必需**(apply 期那次是 `void` 火忘 + 吞错,且与 roster 读取存在竞争),但升级执行段 仍按保险走:多花两秒,换掉一整类竞态。 **共享二进制的窗口期语义**:`npm i -g` 换掉的是磁盘上的全局树,**内存里的旧宿主不受影响** (本轮实测:生产宿主 pid 1644054 跑着 rc.2 的代码,跨过整个全局重装与两小时彩排仍正常服务)。 但**此时重启生产 = 用 0.1.6 的二进制 + 未重铺的 preset 副本开机**——正是本轮要防的坏局面。 升级执行段的顺序不能反:**先重铺 preset,再重启**。 ## 实测记录(2026-09-15 第三轮 · computerUse + browserUse 环境验证 · 沙盒限定) 完整报告:`<工作目录>/computer-use-verify/COMPUTER-USE-VERIFY-REPORT.md`(证据目录同路径)。 现场日志:`dsh-dev-home/host.log`、`dsh-tool-guard.log`、`<工作目录>/computer-use-verify/*.log`。 ``` # 靶场 Xvfb :99(1920x1080x24) + openbox + xclock/xeyes,scrot 截图通路 OK # 装包(web 与 headless 各一次):dsh plugin --profile

add 四个包 # → 全部报 "declares no dsh.bundle — installed as a plain dependency" # → cua 平台二进制走 optionalDependencies 正常落位: # @trycua/cua-driver-linux-x64-gnu/{cua_driver_node_runtime.node 976KB, libcua_driver_sdk.so 42MB} # 挂载:profiles/{web,headless}/cordis.patch.yml 各加一段 insert 四行 # computer-use / browser-use(零 config 的 Service 注册位) # + cua-driver-native / playwright-mcp(mcp: mode=launch, headless=true, # executablePath=/usr/bin/microsoft-edge-stable) # a 干净启动:host.log = 17 行 deny-entry 提示 + 1 行 URL,零 error、零 loader 冲突 # b 六段位全绿(pid=1742041);d curl 200 / DeepSeek Harness # dump-config:四行在册(web:602-612 / headless:357-364) # 工具目录:cua_driver_native__* 59 个 + mcp__playwright-mcp__* 24 个 = 83 个 # 只读验证:list_windows 报 xclock/xeyes;get_window_state 截图以「图片附件」形态 # 落 attachments/v1/objects/(PNG 200x200),zoom 回落 JPEG 200x200,目视确认画面正确 # browserUse:navigate→example.com(Title "Example Domain")+ take_screenshot # → scratch/example-com-readonly-check.png (PNG 1280x720),Edge 进程级取证 10 个 # 生产零接触:六件 sha256 逐字节一致、生产 guard 日志 size/mtime 零变化、宿主 pid 存活 ``` **新踩的坑(都写进 runbook,别再踩)**: 1. **本机 openbox 的默认菜单文件缺失,且系统菜单含危险项**。默认 rc.xml 首选 `/var/lib/openbox/debian-menu.xml`(每次启动必打一条 Openbox-Message), 回落到 `/etc/xdg/openbox/menu.xml`——那份菜单第一项是 Terminal emulator、 倒数第二是 **Restart**、最后是 **Exit**(杀掉 WM)。右键按下即弹菜单, release 一旦被判为选中就会触发。做桌面操控实验前**必须换掉**:在 `$XDG_CONFIG_HOME/openbox/` 放自有 `rc.xml`(`` 指向自有惰性 `menu.xml`, `` 从 200ms 提到 3000ms 以容忍远控点击的 press-release 间隔), 菜单项全部用无 action 的占位项。 2. **本 agent 会话的 `DISPLAY` 是 `:0`(真实桌面)**。启动沙盒/headless 会话时 `DISPLAY=:99` 必须显式带、不可省——漏一次就等于把 cua 的落点对准物理桌面。 这是继 guard 日志路径之后**第二个「不显式覆盖就逃逸出沙盒」的点**。 3. **`get_window_state` 拒绝没有 `_NET_WM_PID` 的窗口**(如 `xeyes`):传 `pid=0` 或真实 pid 都报 `Window target pid …, window_id … is stale or no longer running`, 刷新 `list_windows`(那边 `pid=None`)后依旧。绕法是 `zoom`(纯裁剪截图,不需 pid)。 4. **cua 的 `click`/`right_click` 走的是按窗口寻址的 `XSendEvent` 合成事件** (契约原文见 `libcua_driver_sdk.so` strings:「Routes via XSendEvent (no focus steal)」/ 「X11: routes through XSendEvent ButtonPress/Release」)。**合成事件不触发 X11 被动 按键抓取**,所以往根窗口右键**不会**弹出 openbox 根菜单——工具返回成功、WM 完全没看见。 驱动另有 `delivery_mode` 的 XTEST / XInput2 master pointer 真实注入通道(代价是一次 短暂 focus 交换)。要验「输入真的落地」,别拿 WM 抓取当靶子,改用 `xev` 之类 能观测事件的窗口做靶,或显式切到 XTEST 通道。 5. **别用 `pgrep -af` 做进程取证**。它会把 (a) 运维者自己在 `:3080` 的生产 Edge、 (b) 当前那条 `bash -c` 自己的 cmdline 一起命中。要精确取证就遍历 `/proc/*/exe` 匹配绝对路径,再看 `/proc/PID/cmdline`。 6. **`playwright-mcp` 会往真实 home 写 `~/.cache/ms-playwright/`**(本次是个空目录 `b/`)。 这是 `DSH_HOME` 之外的落点,虽不涉 `~/.dsh` 也不影响 boot,但心里要有数。 7. **`dsh-tool-guard` 的 deny 是精确名匹配、无通配**(`lib/guard.js:16` `deny.has(name)`), 双层闸(呈示层 `filter.js:22` splice + 执行层 `guard.js:21` 硬否决), 且**执行层 fail-open**(抛异常即放行,注释明写「宁漏勿错杀」)。 83 条 computer-use/browser-use 工具的全 deny 清单见 `<工作目录>/computer-use-verify/deny-block-ready.yaml`。 **本机没有 `x11vnc`**(`which x11vnc` 空,安装需 sudo)。想让运维者围观 `:99`, 装好后 `x11vnc -localhost -display :99 -rfbport 5900 -forever -shared`, Remmina → VNC → `localhost:5900`。