dsh-tool-guard
preset 无关的宿主平面工具屏蔽插件:不绑定任何 preset,在统一的插件层按规则拦下不该被模型看见或调用的工具。
三平面架构——system-prompt/assemble 呈示层过滤让模型「看不见」被屏蔽工具,tools.guard 执行层硬否决在调用真正到达时兜底拒绝,WebUI 配置面(/dsh-tool-guard RPC)负责改名单并即时热更前两层。
快速上手
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)。文档一律不写死某台机器的家目录。
当前形态:web profile bundle 单点挂载(主线)——宿主半与浏览器半由同一条链同源装载:
~/.dsh/profiles/web/package.json的dependencies加一行"dsh-tool-guard": "link:$PKG";- 同文件
dsh.profile.bundles数组追加"dsh-tool-guard"; - 在
~/.dsh/profiles/web跑一次pnpm install(生成node_modules/dsh-tool-guard软链); - 重启 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 的「负样本复现」一节)。
上述死法以及本包全部装载/热更链路,改动后一律先在独立沙盒实例复验(同版本二进制 + 独立
DSH_HOME+ 端口 3085),启动/停止/验收清单与实测陷阱见 docs/DEV-SANDBOX.md。
home 层全局挂载(回滚预案,仅受约束时可用):前提有二——移除包内 dsh.client(放弃 WebUI 配置面),且删除包内 cordis.patch.yml 或把本 bundle 从 bundles 数组摘除(防双挂);满足后在 ~/.dsh/cordis.patch.yml 顶层列表插入:
- 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()):
DSH_TOOL_GUARD_LOG—— 全路径覆盖,最高优先级;$DSH_HOME/dsh-tool-guard.log—— 跟随宿主 home(独立沙盒实例据此天然隔离,不会往生产灌);~/.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 缺席还是鉴权原语缺席)。
开发
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。
No comments yet. Be the first to write one.