# dsh-auto-classifier — DeepSeek Harness 的 auto（自主模式）权限分类器

[English](README.md) | 中文

DeepSeek Harness（DSH）的类 Claude Code Auto Mode 权限分类器。在 `read-only / workspace-write / danger-full-access` 之外新增第四个权限预设 **auto（Autonomous / 自主模式）**：工具调用自动分类——危险操作在真正执行前被拦截，安全操作正常放行，沙箱升级由分类器自动裁决，无需人工盯着审批弹窗。

## 工作机制

host 平面插件。两个 `{ prepend: true }` 监听器抢在浏览器应答者之前裁决，只在会话权限预设为 `auto` 时生效；其它会话保持原生交互行为（处理函数调用 `next()` 放行）：

| 扩展点 | 作用 |
|---|---|
| `tools/pre-execute`（工具预执行瀑布） | 每次工具调用都能看到工具名与完整参数：危险命令（删系统目录、格式化、改注册表、`git reset --hard` / force push、访问凭据等）在执行前被拒绝 |
| `approval/request`（审批瀑布） | 沙箱升级（`sandbox_permissions`）由分类器自动放行/拒绝——auto 会话内无浏览器弹窗 |
| git 快照 | 放行高风险升级前对工作区自动 checkpoint（`git add -A && git commit`，可节流）；`auto_snapshot` 工具可随时手动打点 |
| systemPrompt section | 注入自主模式纪律：风险分级、git 救援、禁止无限重试、需要人决策时发邮件并停止 |
| web 控制页（v0.1.5，v0.1.7 起中英双语，v0.1.10 起模型配置，**v0.1.14 起显式凭证**） | **设置 → 插件 → Auto Classifier · 自动分类器**：实时配置开关（LLM 裁判、写内容审查、严格默认、裁判阶段、默认决策，手机式拨钮）、裁判模型配置（**提供方 / Base URL / API Key / 模型** 四个字段——填写的凭证会被保存，裁判**直接 HTTPS 调用**该提供方，支持 OpenAI 兼容与 Anthropic；**测试**按钮用表单里填写的真实凭证实时 ping，**保存**持久化；模式徽标显示 Direct · 直连 / Service · 服务）、会话统计、最近拒绝列表——标签中英双语（EN · 中文）。浏览器半边经 `exports["./client"]` + `dsh.client` 出货；静态 bundle 的 client 没有 `host.call`（那是动态插件机制），所以页面用 `fetch` 拉 host 半在 `webServer` 上注册的同源路由（v0.1.6 起用 `ctx.inject(['webServer'])` 延迟注册——该服务在 webStartup 之后才挂载）：`GET /dsh-auto-classifier/status`、`GET /dsh-auto-classifier/denials`、`POST /dsh-auto-classifier/set`（仅白名单键）、`GET/POST /dsh-auto-classifier/model`（独立裁判模型端点，provider/baseURL/apiKey/model 持久化，API key **绝不回传**、只回掩码）、`POST /dsh-auto-classifier/model/test`（用表单填写的凭证实时 ping，20s 上限）。开关与模型设置立即生效并跨重启持久化（config.json） |

## 规则引擎（Claude Code 风格，工具作用域）

- **规则语法**：`Tool(pattern)`——如 `pwsh(^git\s+commit\b)`、`write,edit(C:\Windows)`；工具名可逗号并列；无括号的裸正则作用于所有工具。pattern 是大小写不敏感正则。
- **字段投影（防误伤）**：命令类工具（pwsh/bash/run_code）只扫 `command/code` 字段（匹配前先清洗：剥 `FOO=bar` 环境变量前缀、`> / >>` 重定向）；路径类工具（write/edit/read）只扫 `file_path`；其它工具不扫描——文件内容/代码文本里提到危险词不算数。审批请求扫 `reason` 理由句。
- **判定顺序**：deny 规则（最高优先）→ 工作区内结构放行（仅 write/edit/read）→ allow 规则 → LLM 裁判（可选）→ pwsh 严格默认 → `defaultDecision`。
- **工作区内文件操作不经过裁判**（Claude Code Tier-1，v0.1.3 起）：`write`/`edit`/`read` 的目标解析后位于会话工作区（`agent.session.header.cwd`）内时确定性放行——项目内编辑可经版本控制复审，且文件沙箱本身已限定写入范围。deny 规则仍优先执行，因此受保护目标（`.dsh` 配置、凭据、系统目录、`.git/config|hooks` 元数据）照旧被拦。
- **默认 deny 表**（参考 Claude Code soft_deny）：破坏性文件操作、磁盘/引导/系统服务、关机重启、账号/服务篡改、注册表写入、git push / force-push / reset --hard、外部代码执行（`curl|bash`、`iwr|iex`）、提权（sudo/runas/gsudo）、网络监听、系统级包安装、凭据文件、受保护路径直写、DSH/shell 配置文件篡改。
- **默认 allow 表**：常规 git（不含 push）/ 包管理器 / 语言运行 / gh / dsh / winget/scoop/choco / PowerShell cmdlet。
- **LLM 裁判**（`llmJudge: true`）：规则未命中时由模型做语义判定。对齐 Claude Code 生产版分类器（完整参考提示词见仓库根目录 `request-body-gid___axonhub_Request_1316.txt`）：
  - **两阶段**（`judgeStages: "both"`，默认）：快速单 token 过滤器（廉价、倾向拦截——Claude 实测 FPR 8.5%）仅在标记时进入链式思考复审（FPR 降到 0.4%）。`"fast"` / `"thinking"` 只跑单阶段。
  - **reasoning-blind 输入**：裁判只看到会话最近的用户消息（授权证据）+ 最近工具调用（仅名称与投影目标，结果与内容一律剥离）+ 工作区事实（工作区根、解析后路径、是否在工作区内、是否 git 仓库）。助手文本与工具输出**一律剥离**，agent 无法说服裁判放行、恶意文件/网页内容也无法操纵裁判。
  - **HARD / SOFT 分级**（v0.1.4 起）：HARD（外部代码执行、不可逆销毁、未授权持久化、安全削弱、提权、网络监听、密钥外泄）无条件拦截、用户意图不可解除；SOFT（工作区外写入、项目外包管理、git push、共享状态删除、大规模变更）仅在**用户点名动作+危险参数**时解除——提问不是同意（"可以修吗"≠"去做"）、被拦后用户重申=知情强化同意、显式边界持续到用户解除。范围外事项（烂代码、错误答案、死循环、浪费 token、瞬时失败重试）永不拦截。
  - **suspectedBypass**（v0.1.4 起）：会话内曾拒绝的目标换工具重试、或路径被嵌入命令，会给裁判打上疑似绕行标记（`bypassWindow` 条最近拒绝参与匹配）——只是信号不是自动拒绝，裁判结合用户意图证据裁决。
  - **scanWriteContent**（v0.1.4 起，默认 `false`）：开启后，write/edit 内容命中 deny 模式时**写入当下**即送裁判审查（Claude 的 WRITTEN FILE EXECUTION），不再走工作区结构放行。
  - 失败降级到 `defaultDecision`。仅用于 pre-execute 路径——升级请求用规则 + 默认决策（底层命令已在 pre-execute 筛过）。
- **拒绝文案**（`denyMessage`）：分类器拒绝返回固定卡夫卡式裁决——"法的门前站着一个守门人。今天守门人说：还不到时候……"——说明这是分类器裁决（不是沙箱拒绝，`sandbox_permissions` 是错误通道）、应另寻入口、换工具会被重新分类。可自定义或置空。
- **拒绝日志**：每次分类器拒绝都会追加到 `~/.dsh/auto-classifier/denials.jsonl`（超过 1 MB 轮转）——重启不丢的 "Recently denied" 复核轨迹；`auto_status` 会打印路径。
- **denial 上限**：连续 3 次 / 累计 20 次（同 Claude Code `denialTracking`）——触发后硬停并提示通知用户。DSH 本身没有邮件能力：提示会指向 [dsh-notify-skill](https://github.com/PAKIKNOWLEDGE/dsh-notify-skill) 邮件插件（同样收录于 awesome-dsh-plugin 列表）或用户自行配置的通知渠道。
- 所有决策经 `ctx.logger` 记录；`auto_status` 工具可查最近 20 条与上限计数。

## 安装（web profile）

```sh
# 1. 打包并加入 profile 依赖与 bundles（物理 tarball，勿用 link:）
#    cd dsh-auto-classifier && npm pack --cache <工作区内路径>   # workspace-write 沙箱下 npm 默认缓存目录写不进
#    package.json dependencies:  "dsh-auto-classifier": "file:C:/.../dsh-auto-classifier-0.1.1.tgz"
#    package.json dsh.profile.bundles: 追加 "dsh-auto-classifier"
cd ~/.dsh/profiles/web
pnpm add "dsh-auto-classifier@file:C:/.../dsh-auto-classifier-0.1.1.tgz" --force

# 2. 校验组合配置（不启动服务）
dsh --profile web --dump-config   # 应出现 auto-classifier 行与 4 个 presets

# 3. 重启 dsh web，然后在会话的权限选择器里切到 auto（或 /permission auto）
```

> 插件的 `cordis.patch.yml` 作为 bundle patch 自动注入行——不要在 profile/home 层手动 insert 同 id（duplicate loader entry 会让 web 启动失败）。改源码后重装：`npm pack` → `pnpm add ... --force`（刷新 lockfile integrity）。

## 更新

插件的 `cordis.patch.yml` 随包分发，注入的行随包自动更新——无需手动改任何 profile/home 层。优先用官方 `dsh plugin` 形式（转发 pnpm **并**按已装状态调和 `dsh.profile.bundles`：新版本才声明 dsh.bundle 的包会自动激活进层栈，重复 add 已列出的包也不会产生重复条目）：

```sh
dsh plugin --profile web update dsh-auto-classifier              # 尊重 ^0.1.x 范围
# 或精确指定版本（npmmirror 同步慢时带官方源）：
dsh plugin --profile web add "dsh-auto-classifier@0.1.3" --registry=https://registry.npmjs.org/

dsh --profile web --dump-config                                 # 验证 exit=0 + auto-classifier 行在
```

然后重启 dsh web。`auto_status` 显示实际生效配置——0.1.3+ 的 `llmJudge` 行会带 `stages: both`。在 profile 目录内裸跑 `pnpm update` 也能升版本，但不会执行 bundles 调和（原地升版本没问题）。若之前装的是本地 `file:...tgz`，需先 `dsh plugin --profile web remove dsh-auto-classifier` 再按 registry 装，避免二次注入。

也可以直接从 npm 安装：`dsh plugin --profile web add dsh-auto-classifier`。

## 配置（`cordis.patch.yml` 的 `auto-classifier` 行）

| 键 | 默认 | 说明 |
|---|---|---|
| `presetName` | `auto` | 分类器在哪个权限预设下生效 |
| `defaultDecision` | `allow` | 规则未命中且无裁判时的默认决策（`deny` = 失败关闭） |
| `llmJudge` | `false` | 是否启用模型语义裁判（每次未命中调用一次，耗 token） |
| `judgeProvider` / `judgeBaseURL` / `judgeApiKey` / `judgeModel` | 空 | **显式裁判凭证**（v0.1.14 起），在 web 控制页填写。当 API key + 模型 + base URL 非空时，裁判**直接 HTTPS 调用**该提供方（OpenAI 兼容 `/chat/completions`，提供方/地址为 Anthropic 时走 `/messages`），完全不依赖 harness 的 llm 注册表或配置文件凭证。`judgeApiKey` 为空则回退到下方 `llmProvider`/`llmModel`。 |
| `llmProvider` / `llmModel` | `deepseek-official` / `deepseek-v4-flash` | 经 harness llm 服务的裁判模型（未配置直连凭证时的回退） |
| `judgeStages` | `both` | `both` = 快速过滤 + 命中才思考复审；`fast` / `thinking` = 只跑单阶段 |
| `denyMessage` | 卡夫卡守门人文案 | 分类器拒绝时追加的固定文案（说明 sandbox_permissions 通道不适用）；置空禁用 |
| `hardDenyMessage` | 硬边界提示 | deny 规则（HARD）命中时追加的句子——建议退出 auto 模式人工审查 |
| `judgeMaxUserMessages` / `judgeUserMessageChars` | `3` / `400` | 裁判可见的最近用户消息条数（截断）作为意图证据 |
| `judgeToolHistory` | `6` | 裁判可见的最近工具调用条数（仅名称+目标，剥内容）作会话上下文 |
| `judgeFastMaxTokens` / `judgeThinkingTimeoutMs` | `8` / `30000` | 阶段一输出上限 / 阶段二中止超时 |
| `scanWriteContent` / `writeContentScanChars` | `false` / `2000` | 开启后 write/edit 内容命中 deny 模式即送裁判（内容截断到此长度） |
| `bypassWindow` | `5` | 参与 suspectedBypass 匹配的最近拒绝条数 |
| `pwshStrict` | `true` | 命令类工具未命中规则时走裁判/严格默认（比路径工具保守，对齐 Claude Code 对 PowerShell 的默认人工） |
| `denyPatterns` / `allowPatterns` | 内置表 | `Tool(pattern)` 正则数组，覆盖/追加规则 |
| `denialLimitConsecutive` / `denialLimitTotal` | `3` / `20` | 拒绝上限，触发后硬停提示 |
| `gitSnapshotOnAllow` | `true` | 放行高风险升级前是否自动 git 快照 |
| `gitSnapshotIntervalMs` | `30000` | 每会话快照最小间隔 |

## License

MIT
