Skip to content
dsh.fish
Bundle

dsh-tool-guard

Preset-agnostic global tool masking for DeepSeek Harness — presentation-layer filtering plus execution-layer guard veto, with a WebUI editor and self-protection gate.

Source
Tisitan
License
MIT
Updated
Updated 3 days ago

Readme

# dsh-tool-guard

preset 无关的宿主平面工具屏蔽插件:不绑定任何 preset,在统一的插件层按规则拦下不该被模型看见或调用的工具。

三平面架构——`system-prompt`/`assemble` 呈示层过滤让模型「看不见」被屏蔽工具,`tools.guard` 执行层硬否决在调用真正到达时兜底拒绝,WebUI 配置面(`/dsh-tool-guard` RPC)负责改名单并即时热更前两层。

## 快速上手

```bash
git clone https://github.com/Tisitan/dsh-tool-guard.git
cd dsh-tool-guard && npm install && npm test        # 94 个用例,含浏览器半产物构建
```

装进 DSH:把本仓库路径以 `link:` 形式加进 `~/.dsh/profiles/web/package.json` 的 `dependencies`
与 `dsh.profile.bundles`,在该目录跑一次 `pnpm install`,重启 DSH——细节、回滚与「为什么不能
双挂」见[部署与回滚](#部署与回滚)。之后在 DSH Web 设置页 →「工具屏蔽」卡片改名单,保存即时热更。
`read` / `write` / `edit` / `bash` 与保留传输 `run_code` 由自我保护闸兜住,不可屏蔽也拦不掉。

## 挂载方式

- 全局:web profile bundle 单点挂载(现主线,见「部署与回滚」——带 `dsh.client` 的插件禁止双 loader 来源,home 层绝对路径挂载仅在移除 client 半后可用);
- 局部:写入某个 preset 的 `agent.cordis.yml`(局部挂载不占全局 loader 来源)。

代码层对两种挂载无差别:entry 不假设自己挂在宿主平面,所有钩子都经注入的 `ctx` 原样注册——挂在哪一层,就在哪一层生效。

entry 声明 `inject: ['tools', 'settings']`:执行层 guard 注册依赖 tools 服务,命名空间注册与 `denyTools` 读取依赖 settings 服务;cordis 保证 apply 调用前依赖已启动,未启动则 fiber 保持 INACTIVE、apply 根本不执行。宿主平面与 agent 平面的 ctx 均具备这两个服务,两类挂载都满足。

配置面另起一枚子 fiber(`ctx.inject(['connection', 'webServer'])`):它是「两个服务都挂齐了再动手」的点火条件。headless/CLI profile 没有 `webServer`,子 fiber 不点火、RPC 通道整体缺席,而呈示层与执行层的屏蔽语义照常工作——那种场景就走下面的手改 yaml 路径。

## 部署与回滚

> 占位符约定:`$PKG` = 本仓库的绝对路径(例:`$HOME/work/dsh-tool-guard`);`$D` = 沙盒 `DSH_HOME`(见 [docs/DEV-SANDBOX.md](docs/DEV-SANDBOX.md))。文档一律不写死某台机器的家目录。

**当前形态:web profile bundle 单点挂载(主线)**——宿主半与浏览器半由同一条链同源装载:

1. `~/.dsh/profiles/web/package.json` 的 `dependencies` 加一行
   `"dsh-tool-guard": "link:$PKG"`;
2. 同文件 `dsh.profile.bundles` 数组追加 `"dsh-tool-guard"`;
3. 在 `~/.dsh/profiles/web` 跑一次 `pnpm install`(生成 `node_modules/dsh-tool-guard` 软链);
4. 重启 DSH。

装载链:bundles 点名 → 包 `package.json` 的 `dsh.bundle.patch` 指到包内 `cordis.patch.yml` → insert 行一次挂齐 `lib/index.js` 宿主半与浏览器半。缺环现象各不同:bundle 链整个没装则屏蔽根本不生效(`~/.dsh/dsh-tool-guard.log` 无 `apply begin` 行);链在但浏览器半异常则面板缺席而屏蔽正常(看日志 `rpc` 段位)。

**血泪规则**:带 `dsh.client` 的插件在全局只允许**一个** loader 来源——home 层 `~/.dsh/cordis.patch.yml` 若再以绝对路径 insert 本包,即构成双 loader 来源,宿主**启动即死**(实测报错原文:`client-modules: package dsh-tool-guard resolves from multiple active Loader sources: …; remove one entry`)。不带 `dsh.client` 的纯宿主插件才适合 home 层绝对路径全局挂载。两道闸分开报错:home 层沿用同一 `id` 时会**先**撞 `duplicate loader entry id: <id>`(id 冲突闸),换了 id 才轮到上面那记 loader 来源闸——报错串不同,排障别认错(两种死法的复现步骤与报错原文都记在 [docs/DEV-SANDBOX.md](docs/DEV-SANDBOX.md) 的「负样本复现」一节)。

> 上述死法以及本包全部装载/热更链路,改动后一律先在独立沙盒实例复验(同版本二进制 + 独立 `DSH_HOME` + 端口 3085),启动/停止/验收清单与实测陷阱见 [docs/DEV-SANDBOX.md](docs/DEV-SANDBOX.md)。

home 层全局挂载(回滚预案,仅受约束时可用):前提有二——**移除包内 `dsh.client`(放弃 WebUI 配置面)**,且**删除包内 `cordis.patch.yml` 或把本 bundle 从 bundles 数组摘除**(防双挂);满足后在 `~/.dsh/cordis.patch.yml` 顶层列表插入:

```yaml
- insert:
    - id: dsh-tool-guard
      name: $PKG/lib/index.js
```

`name` 写仓库内入口的绝对路径即可——宿主 boot(dsh-app-boot `anchorInsertedPluginNames`)会把绝对路径锚定成 file URL 直接 ESM import,**无需 symlink/junction、无需写入任何 node_modules**。`config:` 字段(可选)透传为 `apply(ctx, config)` 的第二参,支持 `deny: [工具名...]` 作为 settings 之外的静态名单源。

回滚(双向):

- **撤 bundle 挂载**:删 `~/.dsh/profiles/web/package.json` 的 dependencies 行与 bundles 条目(备份在同目录 `package.json.bak-YYYYMMDD`,可整文件还原)→ `pnpm install` → 重启;
- **撤 home 层挂载**:删 `~/.dsh/cordis.patch.yml` 的 insert 行/注释体(备份为同名 `.bak-YYYYMMDD`)→ 重启。

插件无持久副作用——所有钩子随 cordis 插件作用域销毁自动摘除,`~/.dsh/settings.yaml` 中 `dsh-tool-guard.denyTools` 名单可保留(不挂载即不生效)。

## 配置方式

两条写路径**语义完全一致**——都过同一条管线:非法名过滤(`sanitizeToolNames`)→ 保护闸剔除(`applyProtection`)→ 空数组转 `unset`(`denyToolsOps`),落盘后由 `settings/updated` 广播触发热更,呈示层与执行层同一份 `denySet` 就地更新(零重注册)。

- **WebUI 面板(主)**:DSH Web 设置页 →「工具屏蔽」卡片。版面(设置页 section 实测只有
  600-760px,宽度就是第一等资源):两列**等宽平分整幅**——`minmax(0, 1fr)` ×2,没有中间
  按钮列;操作按钮下沉到**本列底部操作条**(左列「屏蔽 →」,右列「← 解除」+ 手填输入框 +
  「添加」同行弹性占宽);两列下方是**通栏固定高度描述区**(3-4 行,超出滚动);底部只留
  一行 11px 图例(多选手册 + 不可屏蔽徽记)与一行保存条(保存 + 草稿数/revision + 回执)。
  左列=可屏蔽清单(宿主 `tools.schemas()` 快照,服务端已滤掉保护工具与 `run_code`,带名称
  过滤框),右列=已屏蔽名单(不在当前注册表的条目带「未注册」徽章,名单保留,MCP 重连后即
  被屏蔽)。行内不换行、溢出走省略号(12px 等宽、行高 21px、两侧等高对齐);全名与注释走
  **双通道**——悬停 `title` 给「全名 + 注释摘要」,描述区给全文(可选中复制)。注释来自
  `loadSettings` 的 `descriptions`(与名册单次投影同源下发,宿主侧压平换行、单条封顶 4000
  字符,保护名与保留传输连注释都不出宿主);缺键时描述区直接写「宿主未提供注释(重启宿主后
  上线)」,不让用户以为坏了。多选:裸点=单选(再点同一项清空)、`Ctrl`/`Cmd`+点=加选/摘除、
  `Shift`+点=在当前过滤视图上段选;按钮按选中数改写成「屏蔽 N 项 →」并一次全生效,操作后
  清空选择,选中提示并入描述区首行(含「N 项不在当前过滤结果里,仍会一并生效」)。读写都走
  `/dsh-tool-guard` 通道(`loadSettings` / `saveSettings`),保存携带载入时拿到的
  `revision`:他处(另一页签或手改 yaml)已经写过就回 `conflict` 拒绝并给出「重新载入」,
  不会拿旧快照覆盖别人的新配置。保存后**即时热更**,当前宿主内全部会话的后续装配立即生效。
  两处刻意的保守行为:① 读面失败(RPC 不可用)时**不给编辑器**,只留红字与「重试」——
  读不到现值的表单必然是空的,此时保存等于把真配置整条抹掉;② 通道注册是
  fail-closed 的——拿不到 `connection.requestRejection`(鉴权原语)就干脆不注册
  路由,宁可没有面板也不开一条无鉴权的写面通道。
- **手改 settings.yaml(备)**:编辑 `~/.dsh/settings.yaml` 的
  `dsh-tool-guard.denyTools` 数组,保存后由文件 watch 触发热更,秒级生效。
  无 WebUI(headless/CLI)、RPC 通道未注册、或脚本化批量配置时走这条。
  **硬约束:改完必须在 `dsh-tool-guard.log` 看到 `[hotreload]` 行才算生效**——
  yaml 缩进写错时热更是**完全静默**的(宿主不死、端口照服务、日志零记录,
  沙盒实测坐实),没看到 `[hotreload]` 行先查缩进,别当它已生效。

面板产物构建:`npm run build:client`(esbuild 属 devDependencies 构建链,不进运行时
依赖;`npm test` 已串在测试前跑,产物 `dist/client.js` 是 `__ModuleLoader__` 包装的
单文件,react 经宿主 loader 解析)。客户端与宿主共用 `lib/rules.js` 的纯状态转移函数
(该模块零依赖,esbuild 直接打进 bundle),保护名单与合法名口径前后端同源,不会漂移。

## 日志

生命周期全程写文件日志(段位标签:`apply` / `settings` / `assemble` / `guard` /
`hotreload` / `rpc`),超过 1 MiB 截断重写;写日志失败一律内部吞掉,绝不影响屏蔽语义。

落盘路径三级解析(`lib/log.js` 的 `defaultLogPath()`):

1. `DSH_TOOL_GUARD_LOG` —— 全路径覆盖,最高优先级;
2. `$DSH_HOME/dsh-tool-guard.log` —— 跟随宿主 home(独立沙盒实例据此天然隔离,不会往生产灌);
3. `~/.dsh/dsh-tool-guard.log` —— 默认。

`DSH_TOOL_GUARD_LOG` 与 `DSH_HOME` 的空串/纯空白一律视为未设(对齐宿主 `resolveDshHome` 语义)。
`npm test` 的测试进程用它把日志钉到 tmp,杜绝测试行为污染真机日志。

## 故障恢复

配置平面永不锁死:guard 是进程内机制,而 deny 名单是普通落盘文件——即使保护闸(`PROTECTED_TOOLS`)全失效、deny 名单写错导致会话内自救通道全断,`~/.dsh/settings.yaml` 中 `dsh-tool-guard` 命名空间的 `denyTools` 数组始终可以手工编辑,保存后重启 DSH 即恢复。保护闸在四处口径一致:装配期(`resolveDeny` 出口)、读档(`readDenyTools`)、RPC 写面(`saveSettings`,被剔除的条目回在 `removed` 里让面板明说)、显式保存(`saveDenyTools`)——命中保护名单(read/write/edit/bash)的条目一律剔除并 warn,正常使用中几乎不需要走到手工兜底。

面板打不开或保存报 `conflict` / 红字时,屏蔽语义本身不受影响(前两层照常按现存名单工作),改用上面的手改 yaml 路径即可;具体原因看 `~/.dsh/dsh-tool-guard.log` 的 `rpc` 段位(通道未注册会写明是 webServer 缺席还是鉴权原语缺席)。

## 开发

```bash
npm install          # 只装 devDependencies(esbuild)
npm run build:client # 产出 dist/client.js(浏览器半)
npm test             # 前置构建 + node --test,94 个用例
```

结构:`lib/`(宿主半,纯 ESM 零构建)· `src/client.js`(浏览器半,esbuild 打进 `dist/`)·
`test/`(`node --test`,无需浏览器:宿主服务与 React 均有替身)· `docs/`(沙盒运行手册)。
`lib/rules.js` 是前后端共用的纯函数层——改它等于同时改两端语义,务必先跑测试。
纯客户端改动不需要重启宿主:宿主按请求读盘,改完 `npm run build:client` 后浏览器硬刷新即生效。

## 许可

MIT © Tisitan —— 详见 [LICENSE](LICENSE)。

Install

dsh plugin --profile web add github:Tisitan/dsh-tool-guard#303a819bbac4819ea13d2548c4c80f8484dd2512

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source