# dsh-automode ⚡ **面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的 Claude Code 风格自动模式:让 agent 放手自主执行,同时由确定性护栏 + 低成本复审模型把危险操作挡在执行之前。** > 🌐 **简体中文**: [README.zh.md](./README.zh.md) · **English**: [README.md](./README.md) [![npm](https://img.shields.io/npm/v/@log.li/dsh-automode)](https://www.npmjs.com/package/@log.li/dsh-automode) [![npm downloads](https://img.shields.io/npm/dm/@log.li/dsh-automode)](https://www.npmjs.com/package/@log.li/dsh-automode) [![license](https://img.shields.io/npm/l/@log.li/dsh-automode)](./LICENSE) [![GitHub stars](https://img.shields.io/github/stars/log-li/dsh-automode)](https://github.com/log-li/dsh-automode) [![GitHub last commit](https://img.shields.io/github/last-commit/log-li/dsh-automode)](https://github.com/log-li/dsh-automode) [![TypeScript](https://img.shields.io/github/languages/top/log-li/dsh-automode)](https://github.com/log-li/dsh-automode) [![DSH plugin](https://img.shields.io/badge/DSH%20plugin-ecosystem-2ea043)](https://github.com/topics/dsh-plugin) dsh-automode
--- dsh-automode 是护栏插件,介于 agent 与 harness 之间,在**每次工具调用执行前**拦截——硬 `deny` 规则与 curated `allowPaths` 确定性裁决(零 LLM 开销),其余交给两阶段分类器。安全操作全自动放行;危险操作被拦截、改写或转交给你。 ## ✨ 核心特性 - 🛡️ **确定性第一道防线** —— 正则 `deny` 频带在任何 LLM 调用之前硬拒外泄、密钥与系统路径;前缀 glob `allow` 规则零成本放行常规命令。 - ⚡ **零确认白名单** —— `config.allowPaths` 即全信任:其中文件操作与 bash 写命令完全跳过分类器,提权调用经 approval 桥接自动放行(v0.10.0)——不弹窗、不来回。 - 🧠 **省成本的两阶段分类器** —— 约 1 token 的粗筛预判,仅命中项进入结构化复审;相同动作 5 分钟内复用判决缓存。 - 🔁 **熔断器 + 人工兜底** —— 连续 3 次(或累计 20 次)DENY 暂停自动模式并转人工;一次人工决定即恢复并清零。 - 📜 **完整审计轨迹** —— 每次 allow / deny / bridge 决定都追加到 `~/.dsh/auto-mode/decisions.jsonl`。 - 🔌 **原生预设** —— 权限选择器一键开启或 `/auto`;与只读 / workspace-write / danger-full-access 并存。 > ⚠️ **它不是沙箱**。插件运行在 [DSH](https://github.com/deepseek-ai/deepseek-harness) 进程内,蓄意恶意的插件可以做你用户账户能做的任何事。它用于降低不安全的自主工具使用,而不是 OS 安全边界。 ## 📚 目录 - [安装](#安装) - [命令](#命令) - [工作原理](#工作原理) - [规则](#规则) - [配置](#配置) - [日志](#日志) - [架构](#架构) - [兼容性与贡献](#兼容性与贡献) - [许可证](#许可证) ## 安装 ```bash dsh plugin add @log.li/dsh-automode ``` 从本地 checkout: ```bash dsh plugin add ./path/to/dsh-automode ``` 安装后重启 `dsh web`。权限选择器(聊天框左下角)会显示 **Auto mode**,与只读 / workspace-write / danger-full-access 并列。 ### 可选:权限选择器里的 ⚡ 图标 `auto-mode` 预设声明了一个 `icon`,但**原生 DSH 两边都不认预设的 `icon`**:宿主预设 schema 没有 `icon` 字段、`optionOf()` 也不透传,客户端产物还把三个内置 glyph 写死。所以只有额外打上随包提供的补丁,闪电才会显示——补丁给 schema 加上该字段、把它转发给客户端目录、并让客户端学会画这个路径: ```bash # 是你的 DSH profile 名(通常是 web)。用哪个 profile 下的副本都行: # 脚本自己会扫描所有 profile 与全局安装。 node ~/.dsh/profiles//node_modules/@log.li/dsh-automode/patches/dsh-permission-preset-icon.mjs # 或者从 checkout 里跑: node patches/dsh-permission-preset-icon.mjs --dry-run # 先看它会改什么 node patches/dsh-permission-preset-icon.mjs ``` 然后重启 `dsh web`。 **先弄清你在接受什么。** 这个补丁改的是 **`node_modules` 里的文件**(宿主预设 schema 与客户端 bundle),所以它以独立脚本形式提供、而不在安装时自动执行: - **它只是外观。** 闪电渲染与否,`auto-mode` 行为完全一致。不在意就别跑。 - **每次 `npm update @deepseek-ai/dsh` 或重装插件都会把它冲掉** —— 之后重跑一次即可。它不会「打一半」:先做全量锚点预检,锚点对不上的文件会被明确报告(`ANCHORS NOT FOUND`)并**逐字节保持原样**。 - **回滚就是抄回备份。** 每个被改写的文件旁边都先留一份 `<文件名>.pre-dsh-automode-icon.bak`,重跑是空操作。`already patched` 的含义是脚本自己写过那个文件:宿主半边只有在备份加脚本自己的改动能复现当前字节时才这么报,所以陈旧或外来的备份骗不过它。宿主自己就提供 `icon` 透传的,报 `natively supported`,文件逐字节不动。 - **它覆盖 `0.1.7-rc.2` 这一代到 `0.2.x`,两半都打。** 比 `0.1.7` 更早的版本线会被标注后跳过(那几代的权限选择器在另一个包里,锚点不可能命中)。`0.2.1-rc.2` 上两半都还需要打,因为宿主 schema 与客户端都不读预设 `icon`。锚点失配的文件逐字节不动,报告点名是哪一处锚点没中。将来某一代 DSH 自己在宿主半边提供透传时,这一半会报 `natively supported`;等客户端也开始读预设 `icon`,删掉脚本即可。 - `npm run test:patch`(已并入 `npm test`)用合成 fixture 覆盖 apply / 幂等 / dry-run / 锚点失配 / 外来注入拒绝 / 参数错误,以及 `natively supported` 与 `already patched` 的区分(包括陈旧 sidecar 不得冒充脚本自己的产物),并对「判错时一个字节都不写」做字节级断言;本机存在真实 DSH 安装时,还会**拿真实产物的副本**跑一遍(两个补丁产物都过 `node --check`)。 ## 命令 ```text /auto # 把本会话切换到 auto 模式 /auto-status # 显示诊断:preset、审批策略、熔断器状态 ``` ## 工作原理 ```text 工具调用到达 │ ├─ [pre-execute 门](所有工具,第一道防线;auto-mode 会话,以及继承它的子会话) │ ① 只读工具 → 放行(除非命中 deny) │ ② deny 规则(正则)→ 硬拒绝 │ ③ 删除守卫(rm / unlink / shred / git clean -f / find -delete / del / Remove-Item)→ 硬拒绝,改用 `trash` │ ④ allow 规则(前缀 glob)→ 放行 │ ⑤ 工作区内文件操作 → 放行(allowInsideWorkingDirectory) │ ⑥ 升级意图 → 分类器预审 │ ⑦ 其余 → 放行 │ └─ [approval 瀑布] ① 散文 deny 规则 → 拒绝 ② 散文 allow 规则 → 放行 ③ 只读 allowlist → 放行 ④ 裁决缓存命中 → 复用(不二次调用 LLM) ⑤ 分类器(两阶段:one-token 预筛 → 结构化裁决) ⑥ 失败 → fail-closed ``` ![Auto mode 工具调用拦截管线](docs/auto-mode-flow.zh.png) > 🖱️ **可交互版本**:[docs/auto-mode-flow.zh.html](docs/auto-mode-flow.zh.html) —— 平移缩放、关系追踪、暗色模式。图表源数据:[`docs/auto-mode-flow.zh.workflow.json`](docs/auto-mode-flow.zh.workflow.json)。 pre-execute 门拦截**所有**工具调用(包括工作区沙箱内、本来不会触发 approval 瀑布的那些)。approval 瀑布只对真正需要沙箱升级的调用运行。pre-execute 门**仅对 auto-mode 会话生效**;在其他 preset(read-only / workspace-write / danger-full-access)下它是 no-op,不会与你所选沙箱冲突。自 0.18.0 起,门也覆盖**继承 auto mode 的会话**:子代理或 AgentTeams worker 自己的 preset 为空时,只要沿 `parentSession` 链找到一层 auto-mode 祖先,该会话即受管辖。这类会话只跑确定性层——它们身后没有审批答复器,分类器不会被调用——被覆盖的第一次调用会在审计日志写一条 `pre-execute-inherited`。 **永久删除是「直接拒绝」,不是「交给人审」。** `rm`、`unlink`、`shred`、`git rm`、`git clean -f…`、`find … -delete`、`rsync --delete*`、`del`/`rd /s`/`Remove-Item`,以及把它们藏在包装词(`sudo`、`env -u …`、`xargs -I {} …`、`timeout 5 …`、`nohup`、`eval`、`busybox`、`su -c …`)、shell 载荷(`sh -c`、`bash -lc`、`find -exec sh -c`、`npx -c`)、`$( … )`/反引号替换或 Windows 路径前缀后面的写法,都会在执行前被硬拒,拒绝理由让模型改用 `trash`(可恢复,仍在 allow 频带内)。这是确定性频带,所以即使命令发生在工作区内、根本不涉及审批,也照样拦下。两点后果值得知道:`rm -rf node_modules` 同样被拒(改用 `trash`);守卫只看**会被执行**的文本——`sh -c`、`bash -lc`、`eval`、`busybox`、`su -c`、`find -exec`、`npx -c`、`$( … )` / 反引号替换、管道送进 shell 的 heredoc(`cat </cordis.patch.yml - id: auto-mode config: allowPaths: - /tmp/ - /Users//Library/CloudStorage/OneDrive-/Projects//Proposal/ ``` 只有被识别的写命令才会被信任;路径在 symlink 解析后匹配,`/Users//OneDrive - …` 软链与真实 `Library/CloudStorage/…` 路径都可用,且 `~`/`$HOME` 前缀会在匹配前展开(v0.13.0)。**不可恢复删除**(`rm`、`shred`、`unlink`)绝不会进入白名单——自 0.18.0 起由删除守卫直接拒绝;而经 `trash`(freedesktop 回收站)执行的**可恢复删除**可以(v0.13.0)——其目标会被提取,且**每一个**都必须解析到 allowPath 内,硬 `deny` 频带仍最先拒绝针对系统路径的 `trash`/`mv`。白名单目录内的 `git add`/`commit`/`push` 会把**仓库根**解析为写目标,因此对位于 allowPath 下的仓库做提交/推送也会跳过分类器(v0.11.1)。下面的裁决缓存修复仍然重要:即便没有 allowPath,你一旦显式授权某个动作,分类器也会带着你的意图重跑,而不是回放旧的缓存拒绝。 **复合写命令(v0.11.0)**。临时文件→替换的导出三步曲(如 `DIR=…; cp a b_tmp && (trash b; true) && mv b_tmp "$DIR/b"`)现在会按段解析(含 `VAR=…` 赋值跟踪与 `$VAR` 展开),其目标仍能命中 `allowPaths`。`cd ` 是受跟踪的良性导航命令——它更新有效工作目录(使后面的 `git add/commit/push` 据此解析仓库根),且不使快速路径失效(`cd -` 仍不可预测,回退分类器)。快速路径仍有守卫:复合命令若含副作用命令(`kill`、`pkill`、`rm`、`sh`、`bash`、网络/守护进程管理等)、命令替换(`` `…` ``、`$(…)`、`<(…)`)、文件重定向(`>file`、`>>file`、`2>file`)或写/良性集之外的任何命令,一律回退分类器,与之前行为一致。**fd-dup 重定向**(`2>&1`、`>&2`、`>&-`)不是文件写入,不使快速路径失效——所以 `git push … 2>&1 | tail` 仍可被白名单判定。复合内的**只读**工具(`echo`、`ls`、`cat`、`head` 等)随白名单快速路径搭车执行——一旦所有写目标都在白名单内,其副作用不再单独过分类器。写目标按**该段当时**的 cwd 解析,因此 `cd` 会被信任证明如实采纳:`cd` 进信任根内的相对写入仍走快速路径,而 `cd /elsewhere && cp a rel/path` 不再因为把 `rel/path` 按会话目录解析而蒙混过关(v0.15.3——此前这种不一致可被桥接为 `danger-full-access`);展开后仍含未解析 `$VAR` 的目标(如命令内从未赋值的 `$TMPDIR`)则整条不再走快速路径,而不是去证明一个字面量路径。`mkdir` **自 v0.15.3 起不再搭车**:它会创建目录,因此其目标同样加入信任证明(全部须在信任根内)——此前 `git commit … && mkdir -p /elsewhere` 可以仅凭仓库根就被自动信任。`rm` 仍强制回退分类器;而 v0.13.0 起,`trash` 出现在复合命令中时其可恢复删除目标会加入目标集,与普通写目标一样接受白名单判定(全部须在信任根内,否则整体回退分类器)。改写历史的 git 命令(`reset --hard`、`clean`、`rebase`、`merge`)**刻意不**进入白名单信任。 **零确认提权(v0.10.0)**。allowlist 路径意味着**全信任**:请求放宽沙箱(`sandbox_permissions: danger-full-access`)进入 allowlist 路径的调用现在**无需任何确认、不经分类器直接放行**——pre-execute 门已确定性证明所有目标都在 `allowPath` 内,该结论通过调用的 `callId` 传给 approval answerer(审计日志呈现 `curated allowPath` → `approval-bridge` → `decision allowed-once`;工作区内提权则呈现 `workspace in-tree escalation`,v0.14.0)。deny 频带仍最先执行(`~/.ssh/` 等 deny 路径即便在 allowPath 内也硬拒),熔断器也不会被绕过——跳闸期间 allowlist 调用仍走人工。 **不是文件沙箱豁免**。`allowPaths` 只跳过**本插件**的评审——DSH 文件沙箱(会话文件策略)是独立一层,仍然生效。写 **workspace 外**的 allowlist 目录会被沙箱拦截,除非调用带 `sandbox_permissions: danger-full-access`;而该提权对 allowlist 路径经 approval 桥接**零评审自动放行**——所以第一次尝试就直接带上提权即可。 每个 **auto-mode** 会话也会通过系统提示段(`auto-mode:allowlist`)获得这份知识:模型知道 per-profile 的 `allowPaths` 配置在哪、怎么改——动作被拦截时可以给出精确的配置修改建议,且**只有在你明确确认后**才会实际改动。v0.15.3 起该段还携带**提权规则**(原先只写在本 README 里):目标在白名单内时,**首次尝试就带 `sandbox_permissions: danger-full-access` + 一句 justification**——approval 桥接零评审放行,不会排队等人工;不要裸跑、等沙箱拒绝后再升级,也不要因为怕弹窗而回避提权(信任目标不会弹窗)。 ### 权限预设图标 `auto-mode` 预设默认在权限选择器里显示 ⚡ 图标。想换 logo,就覆盖**预设**上的 `icon`(`cordis.patch.yml` 的 `permission` 行,不是 `auto-mode` 自身配置)——一个画在盾牌轮廓内的 SVG 路径: ```yaml - id: permission config: presets: auto-mode: sandbox: workspace-write approval: ask name: Auto mode description: ... icon: '<你的-svg-path-d>' # 默认 bolt:'M9.15 3.4L5.85 8.55H7.95L7.05 12.6L10.45 7.25H8.25L9.15 3.4Z' ``` 图标只是**外观**——无论是否渲染,行为完全一致。它只在打上随包提供的 [`patches/dsh-permission-preset-icon.mjs`](patches/dsh-permission-preset-icon.mjs) 后才显示:补丁给 schema 加上字段、把值转发给客户端、并让客户端学会画这个路径(见[安装](#可选权限选择器里的--图标))。渲染位置是选择器菜单与当前值触发器;设置页的「新会话默认预设」那一行只显示文字。不设 `icon` 即用默认 bolt。 ### 两阶段分类器 分类器用两阶段把 LLM 成本降到最低: 1. **One-token 预筛**(~1 token):让 LLM 只回一个数字(0 = 安全,1 = 需审查)。使用宽松的 token 预算和鲁棒的数字解析,避免 reasoning 模型被饿死,并把 `classifier.reasoningLevel` 作为推理强度传入(`off` = 不推理)。大多数常规动作返回 0 直接跳过阶段 2。**预筛的输入是「动作」而不是「叙述」**(v0.15.1):命令原文(bash)或目标路径(文件工具)在前,agent 的 justification 只作附注——否则一段写得自信的理由就能让一个会离开本机的命令以 0 免审通过。 2. **结构化裁决**:仅当阶段 1 标记时才运行。返回完整 verdict 及理由。 这意味着大多数工具调用只产生 ~1 token 的分类器开销,只有边界情况才 incur 完整分类器成本。 闸门判的是**授权,不是价值**。它不判断一个动作「值不值得做」,只判断**你有没有要求它**。**你才是最终决断者。** - **只读与可恢复操作一律 ALLOW**:GET/HEAD 请求、检视/列举/搜索/状态查询,以及可以安全撤销的本地改动(编辑、临时文件、构建、测试、git 跟踪文件)。 - **你要求过的、有风险的动作可以放行 —— 包括会离开本机的效果**:发版、发布、部署、推送、对外发消息、写别的系统。**风险本身不是拒绝你的理由。** - **只有一条硬底线对任何人都不开**:执行下载来的代码、破坏或移动系统路径、删 docker 卷/容器/镜像、读密钥材料/凭据库/env 文件、把密钥材料写进命令、改 shell 启动文件或 agent 权限配置。这些还会在任何模型介入之前被确定性拦下(见上文 `deny` 频带)。 - **double-check 闭环**:当动作会影响其他人或系统、而你自己的消息**没有明确覆盖这一个动作**时,闸门拒绝,并要求 agent 把**原样命令**拿给你确认(`ask_user_question`)。你的回答就是直接授权,之后该动作可以放行。 - **`` 只计入直接的人类消息**——但有一个刻意例外:用户经 `ask_user_question` 工具给出的答案也是**经由工具的直接人工授权**,会被并入意图窗口 —— 连同**你回答的那个问题**一起(问题明确标注为 agent 所写,好让一句简短的「可以」仍能与你当时看到的命令对上),并进入裁决缓存签名,使「工具型授权」与「打字授权」一样能让旧的 `DENY` 缓存失效。agent 的 justification、仓库文本与工具输出**永远不是**授权:否则「用户意图」就退化成「谁最后说话谁算」。 ### 熔断器 当分类器在会话内连续拒绝 3 个动作、或累计 20 个时,熔断器跳闸,auto 模式暂停。approval 瀑布转交人工 answerer。**任何一次真实人工决策(允许或拒绝该动作)都会恢复 auto 模式并清零所有计数器**——人工参与即打破熔断器要抓的静默连拒循环。若用户取消请求、或没有可用 answerer,熔断器保持跳闸。 在熔断器跳闸的瞬间,插件会注入一段提示,告诉模型在**下一次尝试就直接请求 `danger-full-access` 沙箱升级**(立即弹出人工批准窗口),而不是"先以当前权限试一次 → 命中 denied → 再升级"的多余往返。v0.15.3 起该提示还限定了**为何**会走人工:走人工是**因为 auto 模式已暂停**,而不是提权本身总要人工——正常状态下白名单内目标的提权是零评审自动放行。 分类器失败(超时、解析错误、空响应)**不计入**熔断器。**但缓存命中的拒绝会计入**——完全相同的已拒动作重试(verdict cache 命中)同样增加连续与总计计数,因此重复提权尝试能真正触发熔断器并到达人工审批,而不是永远空转。 ### 拒绝引导与诊断 分类器为**两态(allow / reject),无 ask 层**——不确定的动作直接拒绝(fail-closed):拒绝可重试或升级到用户,误放不可逆,宁拒勿放。 routine 类别(install/build/test/文件编辑/git add/commit/status)只是**倾向基准,不是免检通行证**——分类器必须判断**具体命令与参数**,不能只看类别标签(例如管道下载执行远程代码、未知包安装带任意 postinstall 脚本、写入 secrets、不可逆删除、推送未知 remote)。 拒绝时,提示会回显**审查者的拒绝理由 + 模型自身在工具调用里写的操作解释(justification)**,让模型看清被拒的是什么、如何改造成更安全的形式。随后指示模型尝试更安全方案;若**没有更安全方案存在**,则**停止重试并询问用户明确许可**——被拒绝的动作会一直失败,只有用户明确批准,后续尝试才可能通过(分类器经 `` 权衡用户的最近显式意图)。 每次分类器流失败(抛异常**或** `error` finish chunk)都会写入 DSH 日志(带解析后的路由、effort、底层错误 code/message、模型原始输出),并作为 `classifier-fail` 事件写入 `decisions.jsonl`——反复出现的 `classifier returned no verdict` 直接从审计记录即可诊断。若路由拒绝配置的 `reasoningEffort`(例如只支持 `off` 的路由收到 `low`),调用会先重试不传 effort 再判定失败。模型侧看到的拒绝提示现在会区分**配置性**问题(未配路由 / 路由拒绝配置的 effort——v0.14.0:修复 `classifier.provider/model` 或模型元数据,重试无用)与**瞬时**问题(429 / 5xx / 超时——稍后重试)。 ### 裁决缓存 分类器 verdict 按会话的 **tool + 命令 + 用户意图** 签名缓存。用户最近直接指示——**打字消息与 `ask_user_question` 答案**——会被 hash 进签名,因此一次新的显式授权(新的人类消息或工具型授权)会使旧的缓存 verdict 失效、分类器带着新意图重跑——**用户的授权绝不会被缓存的 `DENY` 吞掉**。在同一意图窗口内,重复动作仍会复用缓存 verdict,不再二次 LLM 调用。pre-execute 门与 approval 路径现在用**同一份命令原文**签名——approval 路径按 `callId` 从会话恢复该工具调用的真实参数(审批 payload 不携带参数),因此提权调用会命中门的裁决,而不是被第二次分类(v0.13.0)。文件工具(write/edit,无 `command` 字段)按**目标目录**签名(v0.14.1):同一目录的批量写入共享一次裁决,不同目录即使理由逐字相同也不会碰撞——**敏感文件名**仍由 deny 频带兜底(每调用照跑)。缓存条目 5 分钟后过期。 ## 日志 所有决策写入 `~/.dsh/auto-mode/decisions.jsonl`(JSONL 格式,append-only,跨重启保留)。每条记录包含: - `at` — ISO 时间戳 - `event` — decision / pre-execute-deny / pre-execute-allow / pre-execute-fileop / pre-execute-bashop / pre-execute-fail-open / classifier-fail / breaker / resume / boot - `outcome` — allowed-once / rejected / cancelled - `tool` — 工具名 - `tier` — deny / allow / classify:monitor / classify:cache / classify:fail / ... - `callId` — 被裁决的精确工具调用(approval 路径 `decision` 事件,v0.14.0)——可与其 pre-execute 记录精确 join,做两阶段审计 - `detail` — 人类可读理由(deny 命中现附命令/目标上下文,v0.14.0) - `sessionId` — 会话标识 用复盘脚本分析日志、检测 allow→reject 矛盾对回归特征、识别规则优化机会(v0.14.0): ```bash node scripts/audit.mjs # 报告 + 矛盾对哨兵 node scripts/audit.mjs --fail-on-pairs --limit 500 # 存在矛盾对即 exit 1——CI/cron 建议配 `--limit` 滚动窗口 ``` ## 贡献者(Contributors) 感谢所有为 @log.li/dsh-automode 做出贡献的人: | 贡献者 | 贡献 | |---|---| | @WSL043 | 💻 权限兼容层 namespace-probe 思路([PR #2](https://github.com/log-li/dsh-automode/pull/2),共同署名) | | @xiaolinziwang | 🐛 bug 报告([issue #1](https://github.com/log-li/dsh-automode/issues/1)) | | @kuhaku9527 | 🐛 bug 报告 + 根因分析([issue #4](https://github.com/log-li/dsh-automode/issues/4)) | 当 auto 模式激活时,插件会"影子化"审批策略的系统提示,让模型看到 "auto" 而不是 "ask"。这告诉模型:工具拒绝来自自动化审查者,而非人类。模型会相应调整重试策略(尝试更小/更安全动作,而不是问用户)。 ## 架构 ```text src/ index.ts 主入口:preset 管理、审批 answerer、熔断器复位、命令、系统提示影子化 config.ts 配置 schema + $defaults 机制 + 内置规则列表 bands.ts 确定性频带引擎(deny 正则 + allow glob) pre-execute.ts pre-execute 门(第一道防线;真实路径信任、分类器预审、熔断跳闸提示) classifier.ts 两阶段分类器(+ 鲁棒解析、推理强度、诊断) rules.ts 分类器散文规则匹配 prompt.ts 分类器提示构造( + 意图加权) cache.ts 裁决缓存(跨强制点共享) breaker.ts 熔断器(3 连续 / 20 总) log.ts 共享 appendDecision JSONL 日志器 ``` ## 兼容性与贡献 **支持的 dsh 版本:`0.1.0-rc.6` – `0.2.x`**(peer 范围 `>=0.1.0-rc.6 <0.3.0-0`),其中 0.2 线实测于 `0.2.1-rc.2`。有两点要说明白: - peer 范围覆盖的是 `0.2.x` 的**正式版**。按 npm 的预发布规则,预发布宿主(包括实测的 `0.2.1-rc.2`)不会落在比较器带 `0.1.0-rc.6` 预发布标识的范围内;原来的上界 `<0.2.0` 对 `0.1.7-rc.2` 也是同样的结果。`dsh plugin add` 底层是 pnpm,它**会**评估该范围:在这种宿主上打一条 unmet peer 警告,但照常安装;npm 式安装遇预发布宿主则以 `ERESOLVE` 硬失败。 - `npm run typecheck` 编译的是本仓 devDependency(`@deepseek-ai/dsh-*` 0.1.0-rc.8),不是你在跑的宿主。把它指向 `0.2.1-rc.2` 的 `.d.ts` 后,tsc 会报两类类型漂移:v3 兼容分支里的 `tool-result`(0.2.x 的 `ContentBlockMap` 已无该分支),以及 `agent/created` 监听器的返回类型。两者都只是编译期问题:该分支在 0.2.x 上不可达,监听器返回 `undefined` 也符合新签名。运行时行为不受影响。 权限事实(预设 / 沙箱 / 审批策略)经两条自动探测的读取路径获取: | dsh 版本 | 权限读取路径 | |---|---| | `0.1.0-rc.6` – `0.1.4.x` | `session.events` 事件日志(`effectivePermissionPreset` 等) | | `≥ 0.1.5-rc.1` | 持久的 `permissions` 会话投影(`ctx.sessionProjections.stateOf`) | dsh `0.1.5-rc.1` 移除了 `session.events` 访问器——没有投影路径时,每个 auto-mode 回合都会在组装系统提示时报 `Cannot read properties of undefined (reading 'length')`(v0.11.2 修复)。插件运行时探测的宿主导出在 `0.2.1-rc.2` 上全部存在:`setApprovalPolicy`、`setSandboxMode`、`PermissionPresetService`(含 `current` / `set` / `derive` / `optionOf`)与服务名 `permissionPresets`。 DSH session format v4(`dsh ≥ 0.1.7-alpha.1`)退役了 catch-all 的 `plugin` 消息来源标识,并拒绝所有 durable 消息槽位上的该形态。注入消息改用本插件自有的 source kind——与 v3 → v4 会话日志迁移把老记录归一到的标识一致,因此两代格式都接受,无需版本探测(v0.15.4 修复)。`0.2.x` 上的会话格式仍是 v4,没有 v5,v3 → v4 迁移也仍被接受。 同一次格式变更还把工具结果从「用户消息包裹」改成了原生 `tool` 消息。还原用户意图时**两种形态都读**,因此你对 `ask_user_question` 的答复在两代格式上都算授权(v0.16.1 修复,在 `0.2.1-rc.2` 上仍然正确)。 - **仅在 macOS 上验证**。已针对 macOS 文件系统、[DeepSeek Harness(DSH)](https://github.com/deepseek-ai/deepseek-harness) 运行时与开发时使用的 DSH 版本做过测试。路径语义——包括 macOS 的 `/tmp` → `/private/tmp` 软链(由 realpath 最近祖先解析处理)与工作区路径信任——**尚未在 Linux / Windows 上验证**,deny 模式与路径匹配在这些平台上可能有差异。 - **发现 bug,或其它平台上有问题?** 欢迎提交 issue 或 PR:[github.com/log-li/dsh-automode](https://github.com/log-li/dsh-automode)。 ## 许可证 [MIT](./LICENSE)