dsh-safety
English | 中文
<p align="center"> <a href="https://github.com/sugarxl/dsh-safety/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="license"></a> <a href="https://github.com/sugarxl/dsh-safety/blob/main/package.json"><img src="https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square" alt="dependencies"></a> <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D22-339933?style=flat-square" alt="node"></a> <a href="https://www.npmjs.com/package/@suagr_xl/dsh-safety"><img src="https://img.shields.io/npm/v/@suagr_xl/dsh-safety?style=flat-square" alt="npm version"></a> <a href="https://www.npmjs.com/package/@suagr_xl/dsh-safety"><img src="https://img.shields.io/npm/dm/@suagr_xl/dsh-safety?style=flat-square" alt="npm downloads"></a> <a href="https://github.com/sugarxl/dsh-safety/releases"><img src="https://img.shields.io/github/v/release/sugarxl/dsh-safety?style=flat-square" alt="github release"></a> <a href="https://github.com/sugarxl/dsh-safety/stargazers"><img src="https://img.shields.io/github/stars/sugarxl/dsh-safety?style=flat-square" alt="stars"></a> <a href="https://github.com/sugarxl/dsh-safety/commits/main"><img src="https://img.shields.io/github/last-commit/sugarxl/dsh-safety?style=flat-square" alt="last commit"></a> <a href="https://github.com/sugarxl/dsh-safety/actions"><img src="https://img.shields.io/github/actions/workflow/status/sugarxl/dsh-safety/test.yml?style=flat-square&label=CI" alt="ci"></a> </p>
<p align="center"> <strong>DeepSeek Harness 文件系统安全护栏:执行前拦截 · 用户审批门禁 · 删除可恢复 · 组合可回滚 · 启动前校验</strong><br> <em>execution-time guard · user-gated approvals · trash-based deletes · composition snapshots · pre-restart checks · standalone CLI</em> </p>
<div align="center">
[是什么](#是什么) · [功能](#功能) · [审批流程](#审批流程) · [安装](#安装) · [快速上手](#快速上手) · [CLI](#cli-参考) · [工具](#模型侧工具以插件方式安装后) · [配置](#配置) · [原理](#原理) · [目录结构](#目录结构) · [测试](#测试) · [故障排查](#故障排查) · [安全](#安全) · [设计](docs/DESIGN.md) · [常见问题](docs/FAQ.md) · [已知限制](docs/KNOWN-LIMITATIONS.md)
</div>
是什么
DeepSeek Harness(DSH)的文件系统安全护栏。它在工具执行边界强制一套三级文件策略,并且关键的是——让模型在删除/改写任何重要文件前先向人提问:
- 破坏性调用在执行之前被拒绝,并附教育式消息:目标是什么、为什么重要、正确的替代路径是什么;
- 每次删除都走可恢复的回收站;
- 插件组合可快照、可事务化回滚;
- 重启前对组合做校验;
- 敏感删除/改写需要一次性、限时的用户审批——模型永远无法自我授权。
零运行时依赖。既可安装为标准的 DSH profile bundle,也附带独立 CLI——DSH 本身无法启动时,恢复层与审批层依然可用。
> 背景——护栏规则源于一次真实生产事故:脚本因 PowerShell $HOME 是只读变量而静默解析错路径,Remove-Item -Recurse -Force 删除了整个引擎运行根目录。该目录能恢复,仅因为它属于可再生成的生成内容;手写文件则会永久丢失。插件将这次事故的教训实现为强制机制,而非文档说明。
功能
- 执行前守卫(
ctx.tools.guard):在工具真正运行之前拒绝破坏性调用。
- 递归删除目录(rm -r/-rf、Remove-Item -Recurse、rd /s、rmdir、shutil.rmtree、fs.rm recursive、require('fs').rmSync…)默认一律拒绝,强制走 safe_delete(回收站、可撤销);cooperative 模式下人类可批准放行一次自由路径的递归 shell 删除。 - write/edit/str_replace_editor 写 protected 区(profile 的 package.json、cordis.patch.yml、cordis.yml、lockfile、node_modules、部署安装目录、home 级补丁/设置)→ 拒绝,除非用户已批准。 - 删除命中 confirm 区(整个 OS 用户主目录、插件源码、agent-preset)→ 拒绝并需已获批的审批。 - run_code 代码体同样被扫描——任意代码执行不能靠"绕过工具边界"把对受保护区的 fs.rmSync/shutil.rmtree 藏起来。 - 变量引用删除也能拦——Remove-Item "$env:USERPROFILE\.dsh\…" 这种展开后才是真实路径的命令,会把引用+尾段与保护标记比对并拒绝。
- 教育式、反绕过的拒绝——被拦时返回:为什么拦、目标是什么、后果是什么、正路是什么;systemPrompt 明确要求模型"被拦就停、不许换姿势绕过、直接问用户";同一目标反复被拦会升级为明确的 STOP 警告。
- 用户审批门禁——
safety_ask发起带因果的结构化请求(是什么/为什么/后果/替代方案);人类通过dsh-safety allow <id>(或dsh-safety delete --force)批准;审批一次性 + 限时 + 全审计 + 跨进程原子锁串行化(web + headless 不会并发丢更新)。模型永远无法自我批准——force:true单独不算数。每个请求还携带系统按真实路径分类计算的后果(systemNote),与模型自述分开呈现——你批准时看到的是系统背书的判定,而非模型不可核验的故事。 safe_delete—— 唯一合法的删除通道。删除=移动进回收站(safety_undo可还原);preview:true先看再删;拒绝文件系统根和自身状态目录;每次删除都进审计日志。- 组合快照 ——
safety_snapshot把整套插件组合(每个 profile 的 manifest/补丁/lockfile、插件package.json+cordis.patch.yml、agent-preset)带 SHA-256 存起来;safety_restore事务化回滚到 last-known-good(现行文件先自动备份,失败的整体回滚不会把组合留成半恢复态)。默认排除含凭据的文件。 - 重启前体检 ——
safety_check检查 UTF-8、乱码检测(错误编码往返,就是"DSH 打不开"的经典原因)、JSON 可解析、跨补丁层重复插件行 id("一行只能在一个层"规则)。 - 审计日志 —— 拦截/审批/删除/快照/回滚全部留痕,
safety_journal/safety_status可查。 - 独立 CLI ——
dsh-safety不依赖 DSH:在你自己终端就能 policy/delete/undo/snapshot/restore/check/approve,DSH 打不开时也能用。
审批流程
审批系统的全部意义在于:模型永远不能批准自己的破坏性调用。模型能设置的标志(force:true)不是确认——只有人类动作(CLI)才是。
模型对 confirm/protected 区发起删除/改写
│
▼
守卫拦截(教育式消息:是什么 / 为什么 / 后果 / 正路)
│
▼
模型调用 safety_ask { path, kind, what, why, consequence, alternative }
│ → 生成请求(系统同时按真实路径分类计算权威后果)、返回 id、写入审计
▼
模型告诉用户:"请批准:dsh-safety allow <id>"
│
▼
用户执行 dsh-safety allow <id> (或:dsh-safety delete --force)
│ → 授予一次性、限时的审批(默认 5 分钟)
▼
模型重试原调用 → 守卫消费该审批并放行
│ (审批已用掉;下一次同目标调用会再次被拦)
▼
全程审计:谁请求、谁批准、何时、何时被消费实际细节:
- 请求:调用被拦时,拒绝消息会提示模型用
safety_ask并附上因果(what/why/consequence/alternative)。请求同时携带系统计算的systemNote——dsh-safety approvals把[system]判定与(model: …)自述分开呈现,让你基于系统分类做知情决策,而非模型的单方陈述。 - 批准:
dsh-safety allow <id>批准模型创建的请求;dsh-safety allow --path <p> --kind delete|write [--recursive]直接创建并批准一个(你就是人类);dsh-safety delete --force在 confirm/protected 路径上也会先授予所需审批再移入回收站。 - 一次性:审批被第一个匹配调用消费(kind 与 target 精确匹配;递归审批按 flag 精确匹配、target 可为空)。用后即失效。
- 限时:已批准的请求在
approvalTtlMs(默认 5 分钟)后过期,需重新批准。 - strict 与 cooperative:
mode: strict(默认)下递归 shell 删除永不可批准——删目录树的唯一方式就是safe_delete(回收站、可撤销);mode: cooperative下人类可用通用递归审批(dsh-safety allow --path … --recursive)放行一次自由路径的递归删除。 - 防循环:模型对同一目标反复重试时,守卫会升级并明确要求它停止、去问用户。
安装
系统要求:已装好 DeepSeek Harness(dsh web 能启动)。npm 安装无额外要求;从仓库安装需要 Node.js >= 22 与 pnpm。
从 npm 安装(推荐)
dsh plugin --profile web add @suagr_xl/dsh-safety # 从官方 npm registry 安装 / install from the official npm registrydsh plugin 会跑 pnpm,并因本包声明了 dsh.bundle 自动把它加进 dsh.profile.bundles。装完重启 dsh web,守卫即生效、safety_* 工具可用。
从仓库安装(开发调试)
git clone https://github.com/sugarxl/dsh-safety.git # 克隆仓库 / clone the repo
cd dsh-safety # 进入目录 / enter the directory
dsh plugin --profile web add link:$(pwd) # 把仓库软链进 profile / symlink the repo into the profile用 link: 是软链(改 lib/ 重启即生效),file: 则是复制快照。dsh plugin 会自动 reconcile 进 bundles。注意:profile 目录不是 pnpm workspace,workspace:* 依赖会回退到 npm 仓库——本插件完全没有运行时依赖(import 只有 Node 内置 + 自己的 safety-core.mjs/state.mjs/audit.mjs),所以裸 link: 安装不需要它自己的 node_modules,也不存在回退问题。
官方安装布局
两种方式都走官方 dsh plugin 机制,装完无需任何手工配置:
$DSH_HOME/profiles/<name>/package.json # 新增依赖 + dsh.profile.bundles / dependency + dsh.profile.bundles
$DSH_HOME/profiles/<name>/node_modules/@suagr_xl/dsh-safety/ # 安装的包本体 / installed packagebundle 层在启动时从包内的 cordis.patch.yml 读取。dsh-safety 这个行 id 只能出现在这一个层(包内文件)——不要再写进 profile 或 home 的 cordis.patch.yml。
验证与卸载
dsh --profile web --dump-config | grep -i dsh-safety # 确认行出现 / row present
dsh-safety check # 重启前体检 / pre-restart gate
# 重启 dsh web / restart dsh web
# 卸载:/ uninstall:
dsh plugin --profile web remove @suagr_xl/dsh-safety
# 重启 dsh web / restart dsh web独立 CLI(不装插件也能用)
npm link # 或直接: node bin/dsh-safety.mjs ... / or directly: node bin/dsh-safety.mjs ...
dsh-safety statusCLI 与插件读写同一个 $DSH_HOME/.dsh-safety 状态目录,DSH 挂了也能 approve/undo/restore。
安装排障
- 装了也重启了,但没生效:要重启整个
dsh web进程,刷新页面不够;用dsh --profile web --dump-config确认行已挂载。 ERR_PNPM_IGNORED_BUILDS:pnpm 拒绝依赖的构建脚本,把提示的包加进 profile 的pnpm-workspace.yamlallowBuilds后重跑。- pnpm 发布年龄门禁装到旧版:pnpm 11 的
minimumReleaseAge会在发布后约 10 天内静默装旧版;在 profile 的pnpm-workspace.yaml加minimumReleaseAgeExclude: ['@suagr_xl/dsh-safety'],再执行dsh plugin --profile web update @suagr_xl/dsh-safety升级。
快速上手
# 1. 查看当前策略分区
dsh-safety policy
# 2. 修改任何组合文件之前,先快照
dsh-safety snapshot before-edit
# 3. 通过安全通道删除(先预览,再执行)
dsh-safety delete path/to/file --preview # free 路径——直接可用
dsh-safety delete path/to/file # 移入回收站(可撤销)
dsh-safety delete path/to/important --force # confirm/protected 区:
# CLI 的 --force 就是人类审批
# 4. 恢复误删
dsh-safety trash
dsh-safety undo <trash-id>
# 5. 启动失败时:先校验,再回滚
dsh-safety check
dsh-safety status # 查看快照 + 待批准请求
dsh-safety restore <snapshot-id> --confirm
# 6. 批准模型创建的审批请求(模型通过 safety_ask 提问后)
dsh-safety approvals
dsh-safety allow <request-id>CLI 参考
dsh-safety status 状态:回收站/快照/审批/日志
dsh-safety delete <path> [--force] [--preview]
dsh-safety trash [--limit N]
dsh-safety undo <id>
dsh-safety snapshot [label] [--exclude a,b]
dsh-safety restore <id> --confirm
dsh-safety check 失败时 exit 1(适合 CI)
dsh-safety journal [n]
dsh-safety policy 当前策略分区
dsh-safety approvals 列出待批准/已批准请求
dsh-safety allow <id> 批准模型创建的请求
dsh-safety allow --path <p> [--kind delete|write] [--recursive] 直接创建并批准一个
dsh-safety revoke <id> 撤销请求
dsh-safety help--home <path> 可覆盖状态根(默认 $DSH_HOME 或 ~/.dsh)。
插件配置的根目录在 cordis 补丁层里,独立 CLI 读不到——delete/policy 因此接受同样的覆盖参数,以便与运行中的守卫对齐:
--write-root <path> 追加 protected(禁写/改/删)根目录
--confirm-root <path> 追加 confirm-delete(仅回收站)根目录
--no-home-confirm 不把整个 OS 主目录设为 confirm 区
--keep-trash=N / --keep-snapshots=N 删除/快照后的保留上限> CLI 是审批流的人侧:dsh-safety delete --force 和 dsh-safety allow 是真实的用户授权(写入状态);模型永远无法自我批准。
模型侧工具(以插件方式安装后)
| 工具 | 作用 |
|---|---|
safe_delete | 回收站式删除(preview / 用户批准 / 可撤销)。force:true 不是用户批准——删除前必须已有获批的审批请求 |
safety_ask | 带因果(是什么/为什么/后果/替代方案)地向用户请求审批;用户通过 dsh-safety allow <id> 批准 |
safety_trash / safety_undo | 列回收站 / 还原条目 |
safety_snapshot / safety_restore | 快照组合 / 回滚(需 confirm:true) |
safety_check | 重启前校验(UTF-8 / 乱码 / JSON / 重复 id) |
safety_journal / safety_status | 审计日志 / 状态(含待批准请求) |
配置
在补丁层覆盖插件行配置(例如 profile 的 cordis.patch.yml):
- id: dsh-safety
config:
blockWriteRoots: ["C:\\extra\\protected"]
confirmDeleteRoots: ["D:\\data"]
snapshotExclude: ["settings.yaml", ".credentials.yaml"]
blockWrites: true
blockShellDestructive: true
audit: true
keepTrash: 200
keepSnapshots: 10
mode: strict # strict | cooperative
approvalTtlMs: 300000 # 已批准请求的有效期(默认 5 分钟)| 字段 | 默认 | 含义 |
|---|---|---|
blockWriteRoots | profile manifest/补丁/lockfile/node_modules、安装目录、home 补丁/设置 | 禁写/改/删 |
confirmDeleteRoots | $HOME、profiles/*、.agent-presets | 禁删(需已获批的审批,仍只进回收站) |
snapshotExclude | ["settings.yaml", ".credentials.yaml"] | 永不复制进快照的文件 |
blockWrites | true | 开/关写保护守卫 |
blockShellDestructive | true | 开/关 shell 删除守卫 |
audit | true | 记录破坏性工具调用 |
keepTrash / keepSnapshots | 200 / 10 | 保留上限 |
mode | strict | strict:递归 shell 删除永不可批准;cooperative:人类可通过审批流授权 |
approvalTtlMs | 300000 | 已批准请求的有效期,过期需重新批准 |
原理
三级策略:
| 级别 | 允许 | 禁止 | 默认覆盖 |
|---|---|---|---|
protected | 读 | 写 / 改 / 删(除非已获用户审批) | profile 的 package.json/cordis.patch.yml/cordis.yml/lockfile/node_modules、安装目录、home 补丁与设置 |
confirm | 读、编辑 | 删(需已获批的审批,仍只进回收站) | 整个 $HOME、插件源码、agent-preset |
free | 读写删 | 递归删(cooperative 模式下可批准) | 普通工作区文件 |
守卫对每次工具调用的判定链:有破坏性动词?→ 是不是递归删除?→ 显式路径是否命中 protected/confirm?→ 变量引用片段($env:X\…、%X%\…、${X}/…)是否展开进受保护区?→ 命令文本是否命中保护标记(~/相对路径形式)?→ run_code 代码体走同一条链 → 存在匹配的已获批审批则放行一次,否则拒绝。
拒绝是教育式的:说明目标是什么、描述它、解释后果(如"改写会让 DSH 启动失败")与正路(safe_delete / safety_ask),并提示模型不要绕过;同一目标反复被拦会升级为明确的 STOP。拒绝会写审计日志并作为错误返回给模型(绝不会导致进程崩溃)。
第二层:挂 fs/write-intent / fs/edit-intent 瀑布,任何途径写 protected 路径都抛 FS_DENIED。
buildPolicy 位于 safety-core.mjs,插件守卫和独立 CLI 共用同一份策略,两套表面永远不会漂移。restoreSnapshot 是事务化的:先备份现行文件、再从快照复制回去,任一阶段失败就整体回滚——失败的恢复永远不会把组合留成半恢复状态。审批状态存放于 $DSH_HOME/.dsh-safety/state.json,守卫、safe_delete 与 CLI 共用。
目录结构
dsh-safety/
├── bin/
│ └── dsh-safety.mjs # 独立 CLI(零依赖)
├── lib/
│ ├── safety-core.mjs # 纯逻辑:策略/守卫/回收站/快照/校验
│ ├── index.js # host 半区:工具、guard、fs 钩子
│ ├── state.mjs # 持久化状态(审批/守卫计数)——已接入 index.js
│ ├── audit.mjs # JSONL 审计日志 + 阈值告警——已接入 index.js
│ ├── policy.mjs # 策略细化工具(符号链接/挂载检测,已导出)
│ └── snapshot-store.mjs # 增量快照工具(baseline/delta,已导出)
├── test/
│ ├── safety.test.mjs # 单测:核心守卫/回收站/快照/校验
│ ├── state.test.mjs # 状态持久化 + 审批生命周期
│ ├── audit.test.mjs # 审计日志 + 告警
│ ├── policy.test.mjs # 策略细化
│ ├── snapshot-store.test.mjs # 增量快照
│ └── harness.mjs # 集成检查(干净检出,零依赖)
├── cordis.patch.yml # bundle 补丁(插入 dsh-safety 行)
├── package.json # dsh.bundle + bin
├── install.ps1 / recover.ps1 # 本地便捷脚本(快照→安装→校验→回滚)
├── README.md / README.zh.md # 文档(中英双语,官方配对)
└── LICENSE / NOTICE / SECURITY.md测试
npm test # 全部单测(核心 + state/audit/policy/snapshot-store)
node test/harness.mjs # 集成检查,干净检出(无需 @deepseek-ai)
npm run check # 每个 lib/bin 模块的语法检查当前规模:68 个单测 + 集成 harness,Windows 与 Linux CI(Node 22/24)全绿。
故障排查
- 改完插件后 DSH 打不开:跑
dsh-safety check找乱码/JSON/重复 id;dsh --profile web --dump-default-config看不带用户层的 bundle 层;dsh-safety restore <id> --confirm回滚快照。 - 守卫拦了合法操作:守卫从不拦读和插件源码编辑;它拦的是
$HOME/插件/配置区的删除并要求人类审批。用safe_delete(可撤销)代替裸rm;confirm/protected 路径上让模型先safety_ask,你用dsh-safety allow <id>批准。 - 需要删除受保护路径:CLI 侧
dsh-safety delete <path> --force——CLI 用户就是人类,--force即真实审批,仍然只进回收站、永不真正删除;模型侧必须有已获批的审批(光force:true不够)。 - 模型被拦后反复换姿势绕过:这正是守卫要阻止的。明确告诉它调用
safety_ask并等待你的批准,或直接拒绝该请求。
安全
完整威胁模型见 [SECURITY.md](SECURITY.md)。要点:守卫拦截的是模型工具调用,不是你在自己终端敲的命令;run_code 扫描是文本级的,动态/混淆代码可能绕过;state.json 里的审批记录可被同进程插件篡改。它是安全网,不是沙箱——真正的隔离请配好 DSH 自带的沙箱/审批,用本插件补 DSH 缺失的恢复层 + 问人层。
License
MIT。集成模式参考 DeepSeek Harness(MIT),见 [NOTICE](NOTICE)。