← 返回列表
未验证
面向 DeepSeek Harness 的 Codex 风格智能体自动审批。…
尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/20 · 已提供中文文档
Codex-style agent auto-approval for DeepSeek Harness: an independent reviewer model decides allow/deny on the approval answerer chain, fail-closed, with a rationale for every decision (refusals and allows) in a dedicated Approvals tab. · 替我审批:独立复核模型裁决,fail-closed,每次审批留详细理由。
综合分
29.6
GitHub 分
29.6
用户评分
—
★ Stars
0
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add LAwLi3tCoding/dsh-approval-review该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
信任档位:已验证本站已于 1 天前真实安装成功
- 是什么
- dsh 原生插件 · chat
- 装得上吗
- 本站已真实安装成功(非静态推断)
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 5 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
🟢实装验证通过· 2026/9/25
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/24(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-client-ui-conversation@deepseek-ai/dsh-client-ui-renderer@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-commands@deepseek-ai/dsh-llm@deepseek-ai/dsh-session@deepseek-ai/dsh-session-projection@deepseek-ai/dsh-tools@deepseek-ai/dsh-user-approval@deepseek-ai/schemastery用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成dsh-approval-review
面向 DeepSeek Harness 的 Codex 风格智能体自动审批。 当某个操作越过了沙箱自身无法覆盖的边界时,会由第二个独立的审查模型读取拟议操作并返回裁决——从而让人工处理更少的常规提示。模型判断仍可能出错。每个经过审查的决策都会在专门的 Approvals 标签页中留下完整的理由说明。
本插件实现了 Codex 的
Auto-review 的形态:
交互式审批请求被路由给审查智能体而非人工,
审查者以结构化裁决作答,拒绝会作为推理内容而非单纯的错误交还给
调用模型,并且每轮的拒绝断路器会阻止智能体在升级尝试上陷入循环。
这是审查者的替换,而非权限的授予。 本插件从不放宽
沙箱、从不凭空创建授权,也从不将人类从一项未被配置为可接管的决策中移除。
它不拥有的请求会通过 next() 原样委派。
0.5:Jev 作为可选的审查引擎
reviewer.engine 现在决定由谁审查:原来的 LLM 路由,还是
TypeSafe 的 Jev(System One)。Jev 是一个决策模型——
它以概率而非撰写文字来回答类型化问题——因此一次审查就是一次 HTTP 调用,没有工具循环。在本插件自身的用例集上实测:每次审查约 0.9 秒,而 LLM 审查者需要 4–12 秒,并且与预期策略结果 16/16 一致。裁决下游的一切——风险门控、断路器、缓存、账本、Approvals 标签页——都由两个引擎共享。
使用 Jev:
1. 存储密钥到 harness 解析凭据的位置:harness 凭据设置,或
$DSH_HOME/.env 中的 TYPESAFE_API_KEY=…。本插件先询问凭据存储(其本身会分层叠加启动环境、
托管存储和 .env 文件),再回退到进程环境;它会在每次审查时重新解析,因此轮换后的密钥无需重启即可应用于下一次裁决。
2. 开启引擎,在你的 profile 补丁中。以 id 为目标的覆盖会替换
整行配置,因此请重新声明你仍需要的任何键:
- id: approval-review
config:
reviewer:
engine: jev
jev:
allowEgress: true
3. 重启 harness(引擎在挂载时读取),然后运行
/approval-review status。它会打印引擎、访问模式门控和
模型,因此静默的账本总会有可见的原因。
有两项防护是刻意设置的:allowEgress: true 是必需的,因为 Jev 是
第三方端点,证据包会离开本机;而不符合当前生效引擎的选择会被报告出来,而不是被转发。可从 Approvals 标签页选择任意 Jev 模型或 LLM 路由——该选择会携带其引擎和
仅适用于该会话。完整契约,包括每个引擎能做什么和不能做什么,见审查引擎。
它做什么
| | |
|---|---|
| 官方接缝 | 一个以 prepend: true 注册的 approval/request 应答器,因此它会在人类 UI 应答器之前认领请求,并将其他所有内容委托回链。 |
| 第二模型审查 | 默认的 direct 接收策略、显式证据包和有界的本地只读检查器。可选的 subagent/spawn 提供只读调查,但保留 DSH 预设继承。 |
| 无静默失败 | 崩溃、超时、截断或不符合模式的审查者应答永远不会成为批准:它会产出配置的失败策略,默认为 delegate——请求回到人类链。设置 onReviewerFailure: rejected 以采用失败关闭立场。 |
| 理由传达给模型 | 拒绝的原因会附加到被拒绝的工具结果中,并明确指示不要通过变通方法追求相同结果。允许裁决走同一通道(由 recordAllowedVerdicts 控制):批准结果是封闭词汇表,因此工具结果是插件唯一能持久写入的地方——没有它,卡片可以显示某个操作已运行,但永远无法显示原因。 |
| 风险门控 | 高于 maxAutoAllowRisk 的 allow 裁决不会自动允许;它会委托给人类。 |
| 断路器 | 连续和滚动窗口拒绝阈值,与 Codex 的每回合断路器匹配,默认在触发拒绝被记录后停止宿主回合。 |
| 预算 | 每回合审查者调用上限,因此循环无法无限制地计费审查。 |
| 一次性覆盖 | /approval-review approve [n] 记录一次重试的人类授权。绑定到同一会话、工具和字节完全相同的参数;默认五分钟后过期,重启后不会恢复。审查者仍然做决定。 |
| 第四种访问模式 | 仅可查看 / 工作区内修改 / 完全权限旁边的 替我审批(“替我审批”)条目。它有意与 workspace-write 共享沙箱和批准旋钮——区别在于谁来应答——因此菜单条目本身就是开关。PermissionPresetService.derive() 首先检查已记录的选择,这正是两者能够共存并保持选中的原因。 |
| 裁决缓存 | 默认禁用,因为授权和本地状态可能变化。显式选择加入仅限于没有近期转录的 direct 审查。 |
| 失败预算 | 每回合审查者失败上限,因此损坏的审查者无法在请求等待时被无限制重试。 |
| 批准标签页 | 一个对话标签页,渲染每个请求及其裁决、路由策略、风险、理由、更安全替代建议、审查者路由、计时以及实时预算/断路器状态,外加可用的开/关和一次性批准按钮。 |
0.4 策略与兼容性
风险和用户授权是分开评估的。常规低/中风险操作通常可以通过。高风险需要中/高授权、有界范围且无禁止项;严重风险则被拒绝。升级、工作区外路径和正常凭据认证本质上不属于高风险。缺少证据、操作参数被截断、超时和格式错误的回答默认委派处理。
这不是完整的 Codex Guardian 实现。默认直接审查最多可通过四次只读调用检查本地元数据、目录条目和文本;当证据仍然不足时则委派处理。检查器允许工作区路径以及操作中明确指定的确切外部路径,排除凭据存储,将读取限制为 16 KiB、列表限制为 100 个条目,并且从不执行 shell 命令。可选的子代理仍会继承 DSH 预设。默认断路器会记录工具拒绝,然后取消宿主轮次,同时保留待处理的用户输入。仅覆盖宿主审批/请求事件。自定义 policyText 会替换语义策略,但无法绕过代码级别的严重风险拒绝或高风险授权门控。
升级会更改默认审查器模式、风险上限、断路器操作和缓存设置;显式配置文件覆盖仍然优先。记录的理由保留其原始语言,而 UI 标签遵循当前区域设置。
策略比较与测试证据
安装
安装时无需构建步骤。 仓库携带已构建的打包产物
(lib/),且 package.json 未声明 prepare 脚本,因为 pnpm 会阻止
git 依赖的 prepare/install 构建脚本
(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED),这会导致整个安装失败。
因此 dsh plugin add github:... 可以正常工作,无需在
配置文件的 pnpm-workspace.yaml 中添加 allowBuilds 条目。
只有在从源代码克隆进行开发时才需要构建:pnpm build(并且 prepack
会在发布前自动运行它)。
npm(已发布版本)
dsh plugin --profile add dsh-approval-review
本地检出
dsh plugin --profile add /path/to/dsh-approval-review
git 固定版本
dsh plugin --profile add "github:LAwLi3tCoding/dsh-approval-review#"
然后重启并确认该行已组合:
dsh --profile --dump-config | grep -A6 'id: approval-review'
桌面配置文件由 Electron 应用管理,并会拒绝 dsh plugin;请在那里手动添加
依赖项和 bundle 条目,或通过应用的插件管理器来驱动它。
配置
所有可调项都位于 bundle 的 cordis.patch.yml 行中,因此无需改动代码即可修改。
针对 id 的覆盖会替换整个配置行——
请重新声明你仍然需要的每个键,否则被省略的键会静默恢复为其
schema 默认值。
| 键 | 默认值 | 含义 |
|---|---|---|
| enabled | true | 主开关。false 会挂载插件但不声明任何内容。 |
| enabledByDefault | true | 运行时开关的会话启动默认值。 |
| reviewTools | [''] | 默认审批所有工具请求。 |
| defaultPolicy | ai | 未匹配工具的回退路由策略。 |
| rules | [] | 有序的 {pattern, policy, field?, note?} 正则规则,在工具表之前求值。field 为 reason(默认)、toolName 或 arguments。 |
| reviewer.engine | llm | 由哪个引擎作答:原始 LLM 审查器,或 TypeSafe 的 Jev。参见 审查器引擎。 |
| reviewer.mode | direct | 默认隔离模型调用:不继承父级提示词、历史、技能或记忆。可选的 subagent 可以检查工作区。仅适用于 engine: llm。 |
| reviewer.provider / .model | (继承)* | 审查器路由;未设置时继承调用智能体自身的路由。 |
| reviewer.subagentProvider | spawn | 可选的子智能体后端;spawn 省略父级历史,但仍继承宿主预设。 |
| reviewer.inspectLocalState | true | 在直接模式下启用有界本地检查器。 |
| reviewer.tools | [read, glob, grep] | 审查器子进程的工具允许列表。空列表会回退到只读默认值,而不是父级的全部工具面。 |
| reviewer.timeoutMs | 120000 | 单次审查器调用的硬性截止时间。慢速路由加上推理型审查器可能耗时约 50 秒;在审查中途到期的截止时间会成为失败策略结果(默认委托给人类),而不是裁决。 |
| reviewer.maxTokens | (模型路由默认值) | 可选输出上限;默认省略,以便应用适配器/模型配置。 |
| reviewer.temperature | 0 | 采样温度。 |
| reviewer.policyText | (随附策略) | 替换裁决策略文本。 |
| reviewer.guidance | (无) | 附加在策略之后的额外部署指导。 |
| reviewer.argumentMaxChars | 4000 | 每个字符串参数的上限。 |
| reviewer.argumentsBudgetChars | 16000 | 整个参数文档的上限;0 表示禁用。 |
| context.turns | 2 | 作为证据的先前对话轮次;0 省略近期对话记录;仍会提供选定的原始/最新用户意图。 |
| context.maxChars | 6000 | 对话记录字符预算。 |
| context.includeAssistant | true | 在对话记录中包含助手消息。 |
| context.includeToolActivity | true | 包含工具调用和结果。 |
| maxAutoAllowRisk | high | 高风险还要求中/高授权和有界范围;严重风险始终拒绝。 |
| onRiskExceeded | delegate | 超过该上限时执行 allow / delegate / deny。 |
| onUncertain | delegate | 审查器报告无法决定。 |
| onReviewerFailure | delegate | 审查器崩溃、超时或回答不符合模式。默认委托:无法运行的审查器是基础设施问题,而不是裁决——设置 rejected 以采用故障关闭立场。 |
| budget.maxReviewsPerTurn | 20 | 每个开放回合的审查器调用次数。 |
| budget.onExhausted | delegate | 用尽后为 delegate / deny。 |
| maxFailuresPerTurn | 10 | 请求委托前,每个开放回合的审查器失败次数。 |
| verdictCache.ttlMs | 0 | 默认禁用。仅对直接模式且 context.turns=0 且 inspectLocalState=false 时选择启用;键包含会话、用户证据和模型。 |
| verdictCache.maxEntries | 256 | 最旧淘汰前的缓存指纹数。 |
| circuitBreaker.consecutiveDenials | 3 | 触发断路器的连续拒绝次数。 |
| circuitBreaker.windowDenials | 10 | windowSize 内触发断路器的拒绝次数;0 表示禁用。 |
| circuitBreaker.windowSize | 50 | 滚动窗口大小。 |
| circuitBreaker.action | stop | 记录拒绝后停止宿主回合;delegate / deny 仍可用。 |
| override.ttlMs | 300000 | /approval-review approve 保持可用的时长;0 表示永不过期。 |
| override.maxPending | 10 | 覆盖可处理多少条最近的拒绝。 |
| reasonMaxChars | 2000 | 插件发出的任何原因字符串的长度上限。 |
| feedReasonToModel | true | 将理由追加到被拒绝的工具结果中。 |
| recordAllowedVerdicts | true | 将允许裁决追加到已接受的工具结果中,以便卡片可以显示某个操作为何被允许。每次自动允许的调用会在模型上下文中增加一个简短的标记块。 |
| language | auto | 此插件发出的散文语言:/approval-review 命令输出以及审查器的 reason/suggestion 字段。auto 遵循 harness 语言设置(Settings → General → Language),en/zh 将其固定。按调用解析,因此切换会应用于下一条命令和下一个裁决。边界:decision/risk 枚举保持为英文标记(解析器会验证它们),并且已记录在转录中的文本——较早裁决的散文、较早命令的输出——永远不会被重写。 |
审查器引擎:llm 和 jev
reviewer.engine 选择由谁回答审查。llm(默认)是原始路径:一次模型调用——或一个只读子代理——读取证据包并返回裁决 JSON。jev 将相同的包发送到 TypeSafe 的 System One 端点 并读回类型化答案,决策(命中禁止项、不确定性、有界范围)在代码中应用。裁决下游的一切——风险门、断路器、缓存、账本和卡片——由两个引擎共享。
Jev 从不接触 DSH 的模型路由:它是使用 bearer key 的直接 HTTP 调用,因此不会出现在模型选择器中,也不需要提供程序注册。账本中显示的 typesafe/ 值是插件为其自身审计记录合成的标签,而不是 DSH 路由;/approval-review model 仍然有效,并会覆盖发送给 TypeSafe 的模型名称或版本。
选择器中的一项选择会携带其引擎:typesafe/ 使用 Jev 进行审查,/ 使用该 LLM 路由进行审查,单独的 保留当前已生效的引擎,仅替换模型,而 default 则返回到部署自带的审查器。正因如此,列表中的每一行才有意义——包括将 Jev 部署临时切换回 LLM 进行单次会话,标题栏胶囊和账本都会随之更新。有两条限制:仅当部署已确认出站许可(jev.allowEgress)时,会话才能切换到 Jev;并且 Jev 端点接收的是裸模型名称,因此如果某个值在剥离 typesafe/ 标记后仍包含 /,则会被报告并忽略,而不会被转发。
| 键 | 默认值 | 含义 |
|---|---|---|
| reviewer.jev.endpoint | https://api.typesafe.ai/v1/systemone | 证据 POST 的目标地址。如果数据包不能直接发往上游,请将其指向你自己的网关。 |
| reviewer.jev.model | jev-latest | 模型或别名。响应会报告实际应答的版本(jev-1.13.0),账本记录的正是该版本。 |
| reviewer.jev.apiKeyEnv | TYPESAFE_API_KEY | 保存密钥的环境变量。密钥绝不会从配置中读取,绝不会被记录到日志,也绝不会写入审计记录。 |
| reviewer.jev.timeoutMs | 8000 | 单次 HTTP 调用的截止时间;独立于 reviewer.timeoutMs。 |
| reviewer.jev.permitProbMin | 0.6 | 许可答案被视为 uncertain 的最高概率阈值。 |
| reviewer.jev.prohibitedAt | 0.5 | 四项禁令中任意一项直接拒绝的概率阈值,无论授权情况如何。刻意不对称:错误拒绝的代价低于错误允许。 |
| reviewer.jev.scopeBoundedAt | 0.5 | 范围答案被视为有界的概率阈值。 |
| reviewer.jev.allowEgress | false | 必须为 true 才能挂载该插件。否则加载失败,并指明证据将到达的端点。 |
| reviewer.jev.rubric | {} | 按问题 id 键控的每问题 instructions 覆盖(参见 src/jev-questions.ts)。 |
实际差异如下:
- 每次审查一次 HTTP 调用,无工具循环。 Jev 无法检查本地状态,因此如果某个请求的决定性事实不在证据中,该请求会变为 uncertain,并遵循 onUncertain(默认情况下为人工提示),而不是被调查。reviewer.mode 和 reviewer.inspectLocalState 不适用。
- reason 由代码根据答案组合而成——例如
Denied: prohibition "disclosure of secrets or private data" (0.97); risk critical; authorization unknown; permit 1.00——
使用配置的输出语言。reviewer.policyText 不适用:裁决策略存在于每问题评分标准中。
- 证据数据包会离开本机,这正是 allowEgress 所确认的内容。脱敏机制不变(基于键名,并对无法解析的载荷进行尽力而为的处理);它不会从自由文本中清除机密。
- 密钥首先通过 harness 凭据存储解析,然后才是环境。 将其存储在凭据设置中、$DSH_HOME/.env 中,或在启动前导出:凭据接缝已经对这些来源进行了分层(启动环境 → 托管存储 → 项目 .env → harness-home .env),并按请求重新解析,因此轮换后的密钥无需重启即可应用于下一次判定。通过 GUI 启动的 harness 永远不会运行你的 shell 启动文件,这就是为什么存储——而不是 ~/.zshenv——才是有效的地方。
当 Jev 未配置,或只配置了一半时会发生什么:
| 状态 | 结果 |
|---|---|
| reviewer.engine 保留为 llm(默认值) | LLM 审查器运行。没有请求到达 TypeSafe,不需要密钥,也不会记录 Jev 警告。 |
| engine: jev 但没有 jev.allowEgress: true | 插件拒绝挂载,并指出证据将到达的端点。不会自动审查任何内容。 |
| engine: jev,已确认出站,但无法从存储或环境解析出密钥 | 当未挂载凭据存储时,插件会挂载并在启动时发出警告;如果有存储,则第一次审查会以缺少凭据的消息失败。无论哪种情况,失败都遵循 onReviewerFailure——默认是 delegate,因此请求会到达人工,而不是被静默允许。在 maxFailuresPerTurn 次失败后,该轮次将完全不再咨询审查器。 |
| engine: jev 且 mode: subagent,或非 https 端点 | 插件拒绝挂载。 |
| 密钥被拒绝(401/403)、被限流(429)、5xx、超时、非 JSON 或缺少答案 | 记录为审查器失败;不重试,也不对任何部分答案采取行动。 |
| 会话访问模式不是 reviewerPreset | 按设计,插件不声明任何内容——账本保持为空,/approval-review status 会指出该门禁。 |
在全新安装上启用 Jev
1. 存储密钥到 harness 可以解析的位置:harness 凭据设置,或 $DSH_HOME/.env 中的 TYPESAFE_API_KEY=…。两者都会在每次审查时读取;process.env 是不挂载任何凭据行的部署的回退。
2. 打开引擎,在你的 profile 补丁中。以 id 为目标的覆盖会替换整行配置,因此请重新声明你仍然需要的任何键:
- id: approval-review
config:
reviewer:
engine: jev
jev:
allowEgress: true
3. 重启 harness(引擎在挂载时读取)并运行 /approval-review status:它会打印引擎、门禁和模型,因此静默的账本总是有可见的原因。
在启用之前验证连通性——实时探测只发送合成证据,绝不发送仓库内容:
export TYPESAFE_API_KEY=… # 与插件在审查时解析的密钥相同
npx vitest run tests/jev-live.test.ts tests/jev-policy-live.test.ts
工具策略
- ai — 由本插件的审查器决定。allowed-once 或 rejected。
- human — 使用 next() 委托给应答者链的其余部分:即普通的审批提示。插件绝不会将其短路。
- never — 确定性的 rejected,并带有解释性标记,不调用审查器,也不弹出提示。对某个工具族采取硬禁用立场。
edit 有意不在默认的 reviewTools 中:对现有文件进行原地修改是后果最严重的常规操作,因此在部署另行决定之前,它会保留人工提示。
示例:更严格的部署
- insert:
- id: approval-review
name: dsh-approval-review
config:
reviewTools: ['bash', 'pwsh', 'write', 'edit']
defaultPolicy: human
rules:
- pattern: '(?i)(rm\s+(-[a-z]+\s+)/|git\s+push\s+--force)'
policy: never
note: destructive
- pattern: 'curl|wget|nc\s'
policy: ai
field: arguments
reviewer:
model: ''
timeoutMs: 30000
maxAutoAllowRisk: low
onRiskExceeded: delegate
circuitBreaker: { consecutiveDenials: 2, windowDenials: 5, windowSize: 20, action: deny }
会话命令
/approval-review on|off|status|approve [n]|model [/]
- on / off — 持久化的按会话开关。它能在重启和恢复后保留,因为该开关是从命令自身的会话事件中折叠出来的,而不是保存在内存中。
- status — 生效的开关、本轮的审查器预算、拒绝连续次数、累计计数、断路器是否打开、有多少个一次性覆盖待处理,以及最近的决策。
- approve [n] — 为最近第 n 次拒绝(1 = 最近一次)记录一次性授权。只有同一会话、同一工具且参数字节完全一致时,才能消费它一次。默认五分钟后过期,且不会在重启后保留。审查器仍会应用所有策略禁令。
Approvals 标签页
包的 dsh.client 声明会自动注册浏览器端;只要配置文件提供了会话投影能力,宿主就会注册一个 approvalReview 会话投影。无需额外的补丁行。
Approvals 标签页位于对话视图中,紧邻轨迹 / 上下文 / 费用,并将账本渲染为完整页面:每个请求的工具、裁决、路由策略、风险等级、审查器的理由、可选的安全替代建议、请求者自己的原因、审查器路由与耗时、风险/不确定性标志、可展开的参数视图,以及针对近期拒绝的一次性批准按钮。它还会显示实时预算、拒绝连续次数和断路器状态,以及等效的斜杠命令。
不再有会话头部卡片。 它读取与标签页相同的投影,并将相同的账本渲染到一个弹出层中,位于最拥挤的条带上
会话 chrome——标签页已经全页显示它了,所以那张卡片是重复的。
每一行都会说明这个插件是否对此做出了裁决。 该投影会合并宿主自身的 approval/asked 事件,因此标签页中也包含这个插件从未仲裁过的请求——一个由 defaultPolicy: human 交回的 web_fetch,或一个由 dsh-permission-rules 的网络策略引发的 ask。这些行带有 delegated / hard-disabled 标签以及真实的 policy · policySource(在折叠时从部署配置重新推导),而不是伪装成 ai · unrecorded。缺失的理由同样会按路由策略措辞,因此“无记录”绝不会被报告为“并非由这个插件裁决”。
它列出的是审批请求,而不是工具调用。 沙箱直接允许、从未引发审批的调用永远不会出现;任何确实引发了审批的内容都会出现,无论是由哪个插件引发的。
如果没有投影能力,标签页会报告自身不可用,而应答器不受影响。
访问模式图标
@deepseek-ai/dsh-client-ui-conversation 内的访问模式菜单图标表是一个封闭的设计集合:只有三个内置键拥有盾牌图标,而 permissions 投影只携带 value/name/description,因此无法向宿主请求一个图标。因此第四个条目渲染时没有图标。
src/client/access-mode-glyph.ts 从浏览器端提供它:它用 data-dsh-approval-review-glyph 标记访问模式触发器以及匹配的菜单行,并由插件自有的样式表通过 ::before 和 SVG 蒙版绘制同样的盾牌,上面带有一只眼睛(边界不变——有人在跨越之前看过)。
- 它从不触碰 React 的树:只添加属性,不插入节点,因此子节点协调不受影响。
- 它会让位于内置图标:一旦宿主真正为这个键提供了图标(例如你重新构建了 harness 客户端包),这个垫片会检测到它并移除自己的标记,因此图标绝不会被绘制两次。
- 如果重命名预设却没有更新该文件中的标签列表,图标就干脆不会出现;菜单仍可正常工作。这是一种渐进增强,而不是依赖项。
工作原理
approval/request waterfall (answerer chain)
│
┌───────────┴──────────────────────────────────┐
│ dsh-approval-review answerer (prepended) │
│ · plugin + session switch on? │ no ── next() ──▶ human answerer
│ · risk rules → reviewTools → defaultPolicy │
│ = human? ─────────────────────────────────┼── next() ──▶ human answerer
│ = never? ─────────────────────────────────┼── rejected + marker
│ · circuit breaker open? ────────────────────┼── delegate / deny
│ · per-turn budget spent? ───────────────────┼── delegate / deny
└───────────┬──────────────────────────────────┘
│ ai
▼
┌──────────────────────────────────────────────┐
│ 审查者:一次性调用 / 只读子代理 │
│ · 证据:拟议操作 + 脱敏后的参数 │
│ + 询问原因 + 有界转录, │
│ 以 DATA 围栏标注而非指令 │
│ · 输出:{decision, risk, reason, suggest} │
│ · 超时与请求信号竞速 │
└───────────┬──────────────────────────────────┘
│ 裁决 | 失败(失败策略)
▼
allow ─▶ allowed-once deny ─▶ rejected
└▶ 理由追加到被拒绝的
工具结果(tools/post-execute)
审批结果词汇表是封闭的,因此拒绝无处携带文本。插件改为通过
tools/post-execute 监听器,将理由放到被拒绝的工具结果上。这一单一通道
服务于两个目的:模型读取其被拒绝的原因,而审计账本——它折叠会话日志——
为卡片恢复同一理由。
为什么账本不添加会话事件类型
持久化读取路径拒绝解释包含 harness 自身 KNOWN_SESSION_EVENT_TYPES 之外
事件类型的日志,除非该记录携带信封的 ignorable: true 标记,而
Session.append 无法在任何已发布行上盖上该标记——只有拥有该日志的 harness
才能做到。因此,追加自己的 approvalReview/ 事件的插件会使会话无法恢复。
所以账本不添加事件类型。它折叠宿主已经写入的事件
(approval/asked、approval/decided、tool/call、step/、turn/、
command/run、tool/result)为固定投影,并从被拒绝的工具结果中关联出
审查者的理由。卡片上的每个字段都可仅从日志重建。
安全说明
- 脱敏先结构化,再文本化。 参数对象会被遍历,带密钥的叶子节点在到达
审查者之前被替换;键按词边界匹配,因此 auth 不会脱敏 author。解析
失败的载荷则获得尽力而为的文本清理加长度限制。
- 转录使用相同的脱敏。 它读取与拟议操作部分相同的 tool/call 事件;
由原始参数字符串构建的转录会把另一部分已遮蔽的凭据原样交给审查者。
- 审查者的证据是数据,不是指令。 转录和询问者的原因可能包含仓库控制的
文本(AGENTS.md、被审查的文件、命令输出)。数据/指令边界由代码追加,
不能被 policyText 覆盖。宿主标记的用户意图和精确操作审批是授权证据;
工具输出无法伪造其中任何一项。为分析而引用恶意文本本身不是不安全操作。
- 审查者是只读的。 mode: direct 是一次不持有任何工具的模型调用;
mode: subagent 是一个带有 toolFilter 允许列表的子代理,并且
maxDepth: parentDepth + 1。配置的工具会与
read/glob/grep 取交集,因此配置无法添加写入或执行工具。两种形式都无法写入、执行或委托,因此审查器
即使被攻破也无法提升它所守护的边界。
- 审查器无法递归。 审查器子进程一旦被注册,就会
exists, so its own approval asks are delegated to the human chain instead of
returning to the answerer serving it.
- A reviewer that cannot run asks a human. onReviewerFailure: delegate,
onUncertain: delegate, and maxAutoAllowRisk: high are the shipping
choices: refusing in the model's name would make an infrastructure failure
look like a judgement. Set onReviewerFailure: rejected for fail-closed, where
refusing a safe action costs a retry while approving an unsafe one may be
unrecoverable.
- It is not a security guarantee. It evaluates only the requests the approval
seam raises, and a language model can be wrong, especially in adversarial
contexts. It complements a well-configured sandbox; it does not replace one.
Development
pnpm install
pnpm typecheck # tsc --noEmit
pnpm test # vitest run
pnpm build # tsdown: lib/index.js + lib/client.js
pnpm check # all three
The test suite is layered: pure policy tables, the reviewer packet and verdict
parser, the audit fold, the runtime guards, and an integration layer that mounts
the real ApprovalService and LlmRuntime with a scripted adapter and drives
real approval/request dispatches. The integration layer is what caught a
transcript-path secret leak, so it is the part worth extending first.
License
Apache-2.0.