# dsh-manual-approval(手动批准) [English](./README.md) 为 **DeepSeek Harness** 增加第四种权限模式:**手动批准**。任何工具调用——哪怕只是 读文件、`grep`、`glob`、联网搜索——都会先弹出审批卡片。卡片上列出工具名、**每个参数 单独一行**,以及一个可选的反馈输入框;你的决定是这次调用唯一的放行依据。 ## 为什么需要它 官方三种模式各自替你做了取舍: | 模式 | 沙箱 | 审批 | | --- | --- | --- | | 仅可查看 | `read-only` | 按需 | | 可写入工作区 | `workspace-write` | 按需 | | 完全权限 | `danger-full-access` | 从不 | "按需"意味着**由工具自己决定**什么时候问,不问的工具直接执行;"从不"意味着完全不问。 手动批准补上的正是这个缺口:不由任何工具决定,**每一次都由你决定**,并且参数摆在眼前。 ## 手动批准的行为 - **每次调用都要确认。** 插件对手动批准会话中的**所有**工具调用返回 `ask`,且先于其它 pre-execute 监听器作答。 - **模型侧零改动。** 不需要提权参数、不需要额外字段、不需要改变调用方式,工具参数与其它 模式完全一致。 - **不做沙箱。** 手动批准绑定 `danger-full-access`,因此被放行的调用拥有完全执行权限, 包括修改工作区之外的内容。插件内**没有任何平台相关代码**,Linux / macOS / Windows 行为一致。 - **展示参数而非概括。** 细节面板逐个参数一行;shell 工具还会显示模型自己写的那句操作 说明(`description`)。 - **每次调用可选反馈。** 你在反馈框里写的内容会拼接到该次调用的结果里:放行时作为提示, 拒绝时作为拒绝理由——这样你的判断会出现在模型正在处理的那个位置。 - **失败即关闭。** 关闭卡片、没有可用审批通道、审批出错,都意味着调用**不会**执行。 ## 安装 ```sh dsh plugin --profile web add dsh-manual-approval ``` 从本地检出安装: ```sh git clone <本仓库> dsh-manual-approval dsh plugin --profile web add ./dsh-manual-approval dsh --profile web --dump-config # 校验组合 ``` 重启该 profile,然后在权限选择器里选 **手动批准**(或执行 `/permission manual`)。 卸载: ```sh dsh plugin --profile web remove dsh-manual-approval ``` ### 安装会改变什么 本包自带 `cordis.patch.yml`(由 `dsh.bundle.patch` 声明),因此**安装即生效,你无需手改 宿主组合**。补丁做两件事: 1. 重新发布 `permission` 预设表并加入 `manual`,且**放在第一位**,让选择器把它显示在最前; 2. 插入本插件的宿主半。 > **兼容性说明。** 第 1 条为保证表完整而重述了内置预设。若未来 Harness 增加第五种内置 > 预设,需要同步更新该文件。卸载插件时这份补丁会一并移除。 ## 自定义自动放行规则 当权限模式为「手动批准」时,每次调用**先**按你配置的规则列表顺序判断,**第一条匹配的规则决定结果**; 没有任何规则匹配时,才弹出常规审批卡片。 规则 = 若干**条件**(全部满足,逻辑与)+ 一个**动作**。 每个条件选择一个匹配对象和一个正则表达式: | 匹配对象 | 正则作用在哪 | | --- | --- | | 工具名称 | 整个工具名,例如 `^pwsh$` | | 参数名称 | 逐个参数名,例如 `^(command\|cmd)$` | | 参数值 | 逐个参数值,例如 `rm\s+-rf` | 因此写条件**不需要转义 JSON**:`file_path` 匹配的是参数**名**,`C:\Users` 匹配的是路径**值**。 | 动作 | 效果 | | --- | --- | | **放行** | 立即执行,不弹窗 | | **拒绝** | 不执行,理由返回给模型 | | **审批时附加警告** | 仍然问你,但在卡片里用红色显示你写的内容 | | **联网判断** | 把调用 POST 到你的服务,按返回结果决定 | **兜底规则**:一个条件(对象=工具名称,正则=`.*`)+ 动作=放行,放在第一条,就能让手动批准 完全等价于「完全访问」。这是刻意保留的逃生口,也是规则必须可排序的原因。 ### 理由文本与捕获组 规则的理由/警告里可以用 `$1`、`$2`… 或 `${12}` 引用捕获组,`$$` 表示字面量 `$`。捕获组在 **整条规则的所有条件之间全局顺序编号**(条件从上到下、每个正则从左到右),设置界面会在每个 正则旁边直接显示它对应的组号,不必自己数。 捕获值取自把**整次调用**合成的那个 JSON 文档,因为一个被捕获的参数可能来自调用的任何位置: ```json {"cwd":"<会话工作目录>","<工具名称>":{"<参数名称>":"<参数值>", ...}} ``` `cwd` 只在会话确实报告了工作目录时出现,并且固定排在第一位。**没有** `{cwd}` 占位符需要你写: 它就是文档里的一个普通字段,正则照常匹配即可。 所以 `"command":"([^"]+)"` 这样的模式即使所在条件的对象是参数名称,也能捕获到命令文本。 某个模式在该文档上不匹配时,它对应的组取空字符串。 ### 自动拒绝与手写拒绝无法区分 规则的拒绝走的是**和你在审批弹窗里手填拒绝理由完全相同的格式化函数**,因此模型无法分辨某次 拒绝是规则自动产生的还是你现场写的。工具结果里没有任何「自动拒绝」标记。 ### 联网判断 联网判断跑在**宿主进程**里,不在前端。这一点很重要:Node 没有同源策略,所以 `http://127.0.0.1:…`、 纯 `http` 内网服务、或不带 CORS 头的端点都能正常调用;浏览器 `fetch` 会因 CORS 与混合内容失败, 而把请求交给页面去代发等于把宿主变成开放代理。 - 默认 `POST` + `content-type: application/json`;方法、请求头、请求体都可改。 - 请求体默认就是上面的合成 JSON;覆盖时同样可用 `$1`、`$2`…。 - `decisionPath`(必填)与 `reasonPath`(选填)是响应 JSON 的点/方括号路径,例如 `result.decision`、`choices[0].verdict`。 - 决定值只能是 `"approve"`、`"deny"`、`"ask"`,分别对应放行、拒绝、附加警告。 - `reasonPath` 取到的字符串是字面量,**不**经过捕获组替换。 - **任何**一步异常——主机不可达、非 2xx、返回体不是 JSON、路径不存在、取值不是上述三者、超时、 返回体过大——都回退为红色警告并显示具体错误,调用仍然交给你判断。联网判断只能"多问一次", **永远不会失败放行**。 - 写在 URL 里的凭证(`http://user:pass@host/`)会从所有消息、警告与日志中剥离。注意 Node 的 `fetch` 本身会直接拒绝带凭证的 URL,所以请改用请求头传 token。 规则存放在 `/manual-approval/rules.json`,原子写入,每次读取时重新编译。语法错误的规则 会被**保留在磁盘上**并在设置页报出具体原因,而不是被静默丢弃,且在修正前无法启用。另有总开关可 一次性停用全部规则。可直接改用的示例见 [`examples/`](./examples),其中包含针对灾难性递归删除的 警告规则。 ## 私有通道的安全性 反馈框与设置页需要一条客户端→宿主通道,而官方审批结果词汇表只有 4 个固定字符串、装不下文本, 因此本插件新增了一条**私有回环 HTTP 路由**。 这条路由暴露什么、不暴露什么: - 注入到页面里的**只有两个值**:一个**进程级随机 token** 和一条固定路由路径。本插件 **从不**向浏览器发送模型/供应商凭证、API key、环境变量、会话内容或任何 Host 对象。 - 路由**同时要求回环地址与 token**。需要 token 是因为 DSH 的 web 服务器**可能绑定 `0.0.0.0`**;没有它,同网段的其它机器就能伪造审批反馈或改写你的规则。 - 它在宿主与它自己提供的页面之间搬运短字符串和规则文档:不读取 `/manual-approval/` 以外的文件、不碰凭证、不回显任何 Host 状态。 - 联网判断的 URL 与请求头**只存在于宿主**,浏览器完全不知道。 - 调用结束后条目即删,并按时间自动清扫。 若你的组合里没有 `webServer`,插件仍然照常拦截每次调用、规则也照常生效,只是反馈投递与设置页不可用。 ## 手动批准不做出的承诺 - **它不是判定器。** 手动批准不对"这次调用危不危险"发表意见,它只保证有人看过。**不看就 点同意**是这套设计唯一无法阻止的失效方式。 - **弹窗不能替代阅读。** 这里的全部价值就在于点击之前参数已经逐行列好了。 - **提权参数只是可选强制。** 手动批准下 `sandbox_permissions` / `justification` 没有意义。 在插件配置里设 `strictGuard: 'bypass-guard'` 可让仍然携带这两个字段的调用直接被拒。 - **进程外代理不在边界内。** Codex、ACP、SDK 供应商运行自己的工具循环与权限控制。 - **其它宿主插件的直接 Node 文件操作**不经过 `ctx.tools`,因此不受本插件管辖。 ## 配置 全部可选,写在宿主那一行: ```yaml - id: manual-approval name: 'dsh-manual-approval' config: presetName: manual # 触发本策略的预设键 feedbackPath: /dsh-manual-approval/feedback # 反馈用的回环路由 strictGuard: 'off' # 或 'bypass-guard' debug: false # 打印每次决定 ``` ## 设计 接口与决策顺序、以及为"用户自定义自动规则"预留的扩展点,见 [DESIGN.md](./DESIGN.md)。 ## 许可证 MIT