# dsh-auto-approval [English](README.md) | [中文](README.zh.md) 给 DeepSeek Harness 加**第四档权限**:在输入栏的权限下拉里选 **Automode**,会话就以完全权限运行,唯一的安全闸门是一个 LLM 分类器——它替你回答每一次判定,**不再弹审批**。 权限模型保持 3 + 1:官方三档沙箱,外加一档全托管。选其它 preset 即等于关掉本插件,没有第二个开关。 > **0.2.0 是对 0.1.x 的重写。** 开关从插件设置改成了权限 preset:装好、选 **Automode**,就完事。升级见 [从 0.1.x 升级](#从-01x-升级)。 ## 演示 ![Automode:多步任务无人值守跑完,被拒的调用出现在决策表里](docs/demo.gif) 录屏内容:选 **Automode**,发一条 prompt(建目录 → 写文件 → 读回确认 → 执行 `echo danger_test`)。文件那几步全程无人值守通过(`L1-deep` / `whitelist`),最后那条命令被硬规则拦下(`L0-deny`);chip 弹窗同时显示两条判定和累计计数。 ## 工作原理 ```text 模型要调工具 │ ├─ preset ≠ automode ──────────────► 完全不接管(官方行为) │ └─ preset = automode │ ├─ L0 规则(硬底线)────────► deny (rm -rf /、curl | sh、自毁命令 …) ├─ 免检工具 / bash 前缀 ────► allow └─ L1 分类器 ──────────────► allow | deny (fail-closed:超时/解析失败/无模型 → deny) ``` - **L0** —— 正则 deny 规则 + 自毁护栏 + 免检工具/命令前缀白名单,确定性判定,不调模型。 - **L1** —— 拿「最近一条真实用户消息 + 裸工具调用」给分类器。两个 backend:**`llm`**(默认)走聊天模型的两阶段 prompt(fast 单 token 过滤 → 命中才走 deep CoT);**`jev`** 向 TypeSafe System One 发一次 HTTP 请求拿回类型化概率,判定就是阈值比较,没有文本解析。两个 backend 下**分类器都永远看不到工具输出**,所以被注入的内容没法把它骗成 allow。 - **fail-closed** —— 超时、解析失败、没有模型,一律 deny。 automode preset 写的是和「完全权限」同一组旋钮(完全权限 + 审批 `never`),所以其它通道也不会再问人;两者的区别就是本插件这道闸门。官方 preset 服务会记住最后选中的名字,所以下拉里能区分你选的是哪一档。 ## 安装 ```sh dsh plugin --profile web add dsh-auto-approval ``` > **刚发新版?** pnpm 11 默认拒绝发布不足 24 小时的版本(`minimumReleaseAge`,供应链保护),所以发布当天 `add dsh-auto-approval` 会解析到**上一个版本**。要么装精确版本(`dsh plugin --profile web add dsh-auto-approval@0.2.0`),要么在 profile 的 `pnpm-workspace.yaml` 里加: > > ```yaml > minimumReleaseAgeExclude: > - dsh-auto-approval > ``` 然后在输入栏旁的权限下拉里选 **Automode**(或 `/permission automode`)。第一次选会在浏览器里弹一次性提示,说明这档的取舍(官方那个「Enable Full access?」确认硬编码在 `danger-full-access` 键上,自定义 preset 不会触发)。之后预设选择器旁边会出现 `Auto` 胶囊:累计放行/拦截计数,点开是决策表。 源码方式:clone 后 `pnpm install && pnpm run build`,再 `dsh plugin --profile web add link:/<路径>`。 ## 从 0.1.x 升级 0.2.0 保留了包名、`auto-approval:` 配置段和所有配置项,升级不需要改配置。变的是: | | 0.1.x | 0.2.0 | |---|---|---| | 开关 | 插件设置 `enabled` + UI 里的 switch | **Automode** 权限 preset(没有第二个开关) | | 包 | `dsh-auto-approval` + `dsh-client-ui-auto-approval` | `dsh-auto-approval`(host + 浏览器半边同一个包) | | 沙箱 | 看当前 preset;DSH 自己的升级询问照旧弹给用户 | 完全权限 + 审批 `never`,完全不弹窗 | | L1 未配置 | 全部放行(橡皮图章) | 白名单外一律拒绝 | ```sh # 1. 卸掉旧的伴侣包(浏览器半边现在就在主包里) dsh plugin --profile web remove dsh-client-ui-auto-approval # 2. 升级 pnpm --dir "$DSH_HOME/profiles/web" up dsh-auto-approval # 3. 重启 dsh,然后在权限下拉里选 Automode ``` 如果 `settings.yaml` 里没配分类器(`classifierFastProvider` / `classifierFastModel`),automode 现在会 fail-closed——配上它,否则只有免检工具和白名单命令能跑。 ## 配置 `$DSH_HOME/settings.yaml`,热重载: ```yaml auto-approval: denyPatterns: - 'rm\s+(-[a-z]*[fr][a-z]*\s+)*/\s*$' - 'curl\s+[^|]*\x7c\s*(ba)?sh' autoApproveTools: [read, write, edit, glob, grep, ls] bashCommandPrefixes: [ls, pwd, git status, git diff, pnpm test] classifierFastProvider: deepseek-official classifierFastModel: deepseek-v4-flash classifierDeepProvider: deepseek-official classifierDeepModel: deepseek-v4-pro classifierGuidance: '只读命令和跑测试优先放行。' ``` 所有键都可选。`denyPatterns` / `autoApproveTools` / `bashCommandPrefixes` 是**整体替换**默认值(YAML 数组不合并),要保留的默认项得自己写全。 不配分类器就没有 L1,此时 automode **fail-closed**:只有免检工具和白名单命令能跑,其余一律拒绝。这是故意的——没有分类器的会话不是安全兜底,默认放行只会让这道闸门变成橡皮图章。 ### Jev backend(TypeSafe System One) `classifierBackend: jev` 把两阶段 LLM prompt 换成一次 [Jev](https://docs.typesafe.ai/api) 类型化概率调用。Jev 不生成文本:一次请求对同一个帧(最近用户消息 + tool 名 + 参数)回答五个问题、返回校准概率,判定就是代码里的一次阈值比较——整条文本解析链路(VERDICT 行、截断收尾、token 预算)不复存在。一次往返约 300ms。 ```sh export TYPESAFE_API_KEY=<你的密钥> # 推荐方式,见下方警告 ``` ```yaml auto-approval: classifierBackend: jev # 可选: jevModel: jev-latest # 默认值;响应带实际版本号,记进日志供追溯 jevAllowThreshold: 0.9 # 默认值;必须落在开区间 (0, 1) ``` 超时复用 `classifierTimeoutMs`,不新增键。不要把 `classifierBackend: jev` 和 `classifierFast*`/`classifierDeep*` 路由同时配置——歧义配置在加载期直接 throw(fail-loud)。密钥从 `jevApiKey` 解析,缺省回退 `TYPESAFE_API_KEY` 环境变量。**警告:`jevApiKey` 写进 settings.yaml 就是明文落盘——优先用环境变量。** 密钥永不出现在日志、审计事件或 deny reason 里。 **一个闸门,四个证人。** 一次请求问五个问题,只有 `clearly_safe` 参与判定:`noul ≥ jevAllowThreshold` 放行,否则拒绝。`destructive`、`exfiltration`、`beyond_scope`、`impact` **只记录、不拦**——它们的合理阈值必须在你自己的真实 session 上量出来(分类器阈值跨数据集不迁移是普遍教训),先读几十条真实判定日志里的信号分布,再决定要不要升格为闸门。每条 Jev 判定都会在文件日志里写一行,含五个信号数值、`usage.input_tokens` 和实际回答的模型版本号。 **速率与「不重试」的取舍。** jev-1.13 限额 1200 请求/分钟、250k token/秒;输出 token 免费,输入计费(撰文时 $0.042/MTok)。闸门永不重试:429/529(或任何失败)直接拒绝这次调用,不给工具流水线加尾部延迟。撞到限额的表现是「automode 拒绝并告诉你」,而不是「automode 挂起」。 ## 权限与数据 | 面 | 本插件做什么 | |---|---| | 读 | 待判定的工具调用参数;session log(仅用于取最近一条真实用户消息作为分类器意图) | | 写 | `$DSH_HOME/logs/auto-approval.log`——本机 JSONL 审计文件,best-effort;写失败只记 warn | | 网络 | 仅当配置了 L1。`llm` backend:用户消息 + 工具调用发给所配置的模型服务。`jev` backend(默认关闭):**最近一条真实用户消息**(截断到 4000 字)、**tool 名**、**参数 JSON**(截断到 8000 字)发往 `https://api.typesafe.ai/v1/systemone`。两个 backend 都**永不发送 tool 输出**——这是注入防线,不是巧合 | | 执行 | 不执行任何东西。不起子进程、不走 shell、不改审计文件以外的文件 | | 拦截 | `tools/pre-execute`(prepend)+ 单调 `ctx.tools.guard()` deny 守卫,两者都按会话 preset 门控 | | 失败边界 | L1 超时 / 解析失败 / 无模型 → **deny**;配置非法在加载时 throw(fail-loud);settings 服务缺失回退 composition entry 配置 | ## 兼容性 只跟官方最新版走。已在 `@deepseek-ai/dsh` **0.1.2-rc.1** 和 **0.1.5-rc.2** 上实测通过(一次性 `DSH_HOME`:安装 → 启动 → 真实 tool call 判定)。旧版本不保证。 bundle patch 会整体重述官方 preset 表,所以官方基础层新增 preset 时这个文件也要跟着更新。 ## 开发 ```sh pnpm install pnpm run typecheck pnpm run test pnpm run build # lib/index.js(host)+ lib/client.js(browser) ``` ## License BSD-3-Clause