dsh-codex-approval 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dsh-codex-approval contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-codex-approval contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,150 +1,172 @@
1
- # dsh-codex-approval
2
-
3
- > **仿照 OpenAI Codex CLI 审批模型的 AI 自动审批插件**,为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)带来 Codex 式的智能审批体验。
4
-
5
- dsh 原生只有两种审批策略:模式级沙箱(`read-only` / `workspace-write` / `danger-full-access`)和一刀切的 `ask`/`never` 策略——**没有命令级规则,也没有 AI 风险评估**。本插件在 dsh 的 `approval/request` 应答者(answerer)seam 上实现了一个完整的自动审批决策链:
6
-
7
- ```
8
- 规则层(Codex approve-always / reject-always 风格)
9
- → AI 审判层(Codex 三级风险 + 三级授权 + risk tolerance)
10
- → 人类兜底(GUI 弹窗)
11
- ```
12
-
13
- ## 仿照 Codex 的什么
14
-
15
- | Codex CLI | 本插件 |
16
- |---|---|
17
- | `--approve-always 'Bash(git diff)'` / `--reject-always` | glob 规则(`Bash(git *)` / `reason:*curl*`),动作 `allow` / `ask` / `deny`,安全优先级 **deny > ask > allow** |
18
- | 工具风险分级 `low` / `medium` / `high` | AI 对每次审批请求输出 `risk: low\|medium\|high`(只读=low、有界修改=medium、破坏/泄密/系统级=high) |
19
- | 三级授权 | AI 输出 `authorization: allow\|ask\|deny`——直接放行 / 交人类 / 禁止 |
20
- | `risk_tolerance` 配置 | `riskTolerance: low\|medium\|high`:AI 判 ask 时按容忍度映射(风险 ≤ 容忍度 → 自动放行) |
21
- | `--permission-mode auto` 的"低风险自动、高风险询问" | 默认 `tolerance: medium`:low/medium 自动放行,high 交人类或由 AI 直接拒绝 |
22
-
23
- ## 决策流程
24
-
25
- ```
26
- approval/request 到达(toolName + callId + reason)
27
- ├─ 1. 参数反查:按 callId 从会话日志恢复完整命令(bash/pwsh 取原始 command)
28
- ├─ 2. 规则层(deny > ask > allow,命中即定,0ms)
29
- │ deny → 直接拒绝(AI 无权覆盖)│ allow → 静默放行 │ ask → 交人类
30
- ├─ 3. AI 审判层(规则未命中时;默认 opencode-go / deepseek-v4-flash)
31
- │ LLM 裁决 {risk, authorization, reason}
32
- │ allow/deny 直接生效;ask 按 riskTolerance 映射
33
- │ AI 报错/超时/输出非法 → failOpen(默认 ask → 人类)
34
- └─ 4. 兜底:fallback(默认 ask → GUI 弹窗)
35
- ```
36
-
37
- 每次决策写入一行 JSONL 审计日志(默认 `~/.dsh/logs/approval.jsonl`):工具名、命令预览、reason、判定来源(rule / ai / ai-error / fallback)、**模式(mode)**、风险、AI 理由、耗时。
38
-
39
- ## 审批模式(v0.2.0)
40
-
41
- 插件提供一个与 dsh 沙箱模式**正交**的审批模式维度,三种模式按需切换:
42
-
43
- | 模式 | 名称 | 行为 | 场景 |
44
- |---|---|---|---|
45
- | 1 | `manual` | **完全旁路**:不决策、不写日志,审批全部交回人类弹窗 | 回归未装插件的原生体验 |
46
- | 2 | `ai`(默认) | 规则 → AI → ask 交人类 | 日常:低风险自动、高风险问人 |
47
- | 3 | `ai-auto` | 规则 → AI → **ask 永不交人类**,按 `mode3OnAsk`(默认 deny)处理 | 全自动操作但又不放心 full access:AI 全权把关,绝不弹窗 |
48
-
49
- **运行时切换**(GUI 斜杠命令,作用于当前会话,持久化到 settings):
50
-
51
- ```
52
- /approval-mode 显示当前模式(覆盖值 + 生效值)
53
- /approval-mode 3 切换为 ai-auto(也接受 ai-auto / 1 / 2 / manual 等)
54
- /approval-mode default 清除会话覆盖,回落到配置默认
55
- ```
56
-
57
- **ai-auto 下 ask 的归宿**(`mode3OnAsk`,默认 `deny`):规则 ask、AI 判 ask 且超容忍度、AI 故障 failOpen=ask、兜底 fallback=ask——全部按此处理,绝不弹窗。⚠️ 若设为 `allow`,AI 无法决定时也会放行高风险操作,**慎用**。
58
-
59
- **模式持久化**:会话覆盖存 `~/.dsh/settings.yaml` 的 `dsh-codex-approval` 命名空间(settings 服务不可用时降级为纯内存,重启丢失)。默认模式由配置 `mode` 字段决定。
60
-
61
- **与 dsh 沙箱模式的关系**:
62
-
63
- | 沙箱模式 | AI 审核是否生效 |
64
- |---|---|
65
- | `read-only` | ✅ 生效——沙箱拒绝写操作,模型可申请升级(`WIDER_MODES` 允许),升级请求照常走审批链 |
66
- | `workspace-write` | ✅ 生效(推荐组合:工作区内自由,越界 AI 把关) |
67
- | `danger-full-access` | ⏸ 不触发——沙箱从不拒绝任何操作,没有升级请求,插件自然空闲 |
68
-
69
- **命令多语言**:`/approval-mode` 的返回文案跟随 dsh 设置的语言(`locale.preference`,中/英)。命令 `description` 在启动时按当时语言注册,运行中切换语言后需重启才更新 description(返回文本每次实时跟随)。
70
-
71
- **npm publish 默认 ask**:内置规则 `Bash(npm publish*) → ask`——agent 执行 `npm publish` 的升级请求**必定弹窗询问人类**,AI 无权自动放行(`ai` 模式下弹窗;`ai-auto` 模式下按 `mode3OnAsk` 处理,默认拒绝)。`npm unpublish` 无规则,由 AI 默认判定(通常判 high 直接拒绝)。
72
-
73
- ## 安装
74
-
75
- ```bash
76
- dsh plugin --profile web add dsh-codex-approval
77
- # 重启 dsh web 生效
78
- ```
79
-
80
- 插件只在目标 profile 注册(推荐 web);qqbot / headless profile 不受影响。
81
-
82
- ## 配置(~/.dsh/profiles/web/cordis.patch.yml)
83
-
84
- ```yaml
85
- - id: dsh-codex-approval
86
- config:
87
- mode: ai # manual | ai | ai-auto(默认 ai)
88
- mode3OnAsk: deny # deny | allow(ai-auto 下 ask 的归宿;默认 deny 安全)
89
- locale: auto # auto | zh | en(命令文案语言;auto=跟随 dsh 设置的语言偏好)
90
- rules:
91
- - match: 'Bash(git status*)' # 命中即自动通过(Codex approve-always)
92
- action: allow
93
- - match: 'Bash(rm -rf /*)' # 危险命令直接拒绝(Codex reject-always)
94
- action: deny
95
- - match: 'reason:*credential*' # 敏感场景强制询问
96
- action: ask
97
- ai:
98
- enabled: true
99
- provider: opencode-go # 与主 agent 同一 provider(成本一致)
100
- model: deepseek-v4-flash # deepseek-chat 官方 API 已弃用
101
- riskTolerance: medium # low | medium | high(仿 Codex risk tolerance)
102
- maxPromptChars: 2000
103
- timeoutMs: 15000
104
- maxTokens: 512 # 含 reasoning 余量
105
- failOpen: ask # AI 故障兜底:ask | deny | allow
106
- fallback: ask # 无规则命中且 AI 关闭时:ask | deny | allow
107
- logFile: ~/.dsh/logs/approval.jsonl
108
- ```
109
-
110
- 不配置即用内置默认:只读命令(git status/diff/log、ls、cat、pwd、which、echo)自动放行,破坏性命令(`rm -rf /`、`rm -rf ~`、`sudo rm`、`shutdown`、`reboot`、`mkfs`)直接拒绝,敏感词(secret/password/credential/token)询问。
111
-
112
- ## 规则语法
113
-
114
- - 匹配对象(任一表面命中即中,大小写不敏感):
115
- - `ToolName(args preview)` 如 `Bash(git status)`(bash/pwsh 为原始命令)
116
- - `reason:<文本>` — 审批 reason(如沙箱升级的 justification)
117
- - 通配:`*` 任意序列、`?` 单字符
118
- - 优先级:**deny > ask > allow**(与列表顺序无关);同优先级内按列表顺序取首个
119
-
120
- ## AI 审判输入/输出
121
-
122
- **输入**:固定系统提示(审批员角色 + risk/authorization 定义 + 只输出 JSON 约束)+ `{"toolName", "command", "reason"}`(命令截断 2000 字符,无其他上下文)。
123
-
124
- **输出**:`{"risk":"low|medium|high","authorization":"allow|ask|deny","reason":"一句话"}`;解析策略:整体 JSON ```json``` 代码块 → 平衡花括号扫描;枚举校验失败按 AI 故障处理。
125
-
126
- ## 安全注意事项
127
-
128
- - **deny 规则永远最先求值**,AI 无权覆盖显式拒绝
129
- - AI 输出只映射为三种结果之一,不存在注入面;命令文本进 prompt 前截断
130
- - AI 调用有超时上限(默认 15s),失败默认交还人类(fail-open,不会静默全拒)
131
- - 审批审计对(approval/asked + approval/decided)由 dsh 审批服务持久化,插件只追加自己的决策日志
132
- - `danger-full-access` 模式下沙箱不拒绝任何操作,审批请求不会发生,插件自然空闲
133
- - 单次 AI 审批成本约 0.3~0.7 分钱(官方价估算),仅规则未命中时产生
134
-
135
- ## 成本
136
-
137
- | 场景 | 单次 Token | 单次成本(官方高峰价) |
138
- |---|---|---|
139
- | 典型(短命令) | ~400-500 | 0.003 元 |
140
- | 最坏(命令 2000 字符) | ~1,500 | ≈ 0.007 元 |
141
-
142
- ## 开发与测试
143
-
144
- ```bash
145
- node --test # 55 个单测:规则匹配 / 参数反查 / AI 裁决解析 / 决策流
146
- ```
147
-
148
- ## License
149
-
150
- MIT
1
+ # dsh-codex-approval
2
+
3
+ > **仿照 OpenAI Codex CLI 审批模型的 AI 自动审批插件**,为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)带来 Codex 式的智能审批体验。
4
+
5
+ dsh 原生只有两种审批策略:模式级沙箱(`read-only` / `workspace-write` / `danger-full-access`)和一刀切的 `ask`/`never` 策略——**没有命令级规则,也没有 AI 风险评估**。本插件在 dsh 的 `approval/request` 应答者(answerer)seam 上实现了一个完整的自动审批决策链:
6
+
7
+ ```
8
+ 规则层(Codex approve-always / reject-always 风格)
9
+ → AI 审判层(Codex 三级风险 + 三级授权 + risk tolerance)
10
+ → 人类兜底(GUI 弹窗)
11
+ ```
12
+
13
+ ## 仿照 Codex 的什么
14
+
15
+ | Codex CLI | 本插件 |
16
+ |---|---|
17
+ | `--approve-always 'Bash(git diff)'` / `--reject-always` | glob 规则(`Bash(git *)` / `reason:*curl*`),动作 `allow` / `ask` / `deny`,安全优先级 **deny > ask > allow** |
18
+ | 工具风险分级 `low` / `medium` / `high` | AI 对每次审批请求输出 `risk: low\|medium\|high`(只读=low、有界修改=medium、破坏/泄密/系统级=high) |
19
+ | 三级授权 | AI 输出 `authorization: allow\|ask\|deny`——直接放行 / 交人类 / 禁止 |
20
+ | `risk_tolerance` 配置 | `riskTolerance: low\|medium\|high`:AI 判 ask 时按容忍度映射(风险 ≤ 容忍度 → 自动放行) |
21
+ | `--permission-mode auto` 的"低风险自动、高风险询问" | 默认 `tolerance: medium`:low/medium 自动放行,high 交人类或由 AI 直接拒绝 |
22
+
23
+ ## 决策流程
24
+
25
+ ```
26
+ approval/request 到达(toolName + callId + reason)
27
+ ├─ 1. 参数反查:按 callId 从会话日志恢复完整命令(bash/pwsh 取原始 command)
28
+ ├─ 2. 规则层(deny > ask > allow,命中即定,0ms)
29
+ │ deny → 直接拒绝(AI 无权覆盖)│ allow → 静默放行 │ ask → 交人类
30
+ ├─ 3. AI 审判层(规则未命中时;默认 opencode-go / deepseek-v4-flash)
31
+ │ LLM 裁决 {risk, authorization, reason}
32
+ │ allow/deny 直接生效;ask 按 riskTolerance 映射
33
+ │ AI 报错/超时/输出非法 → failOpen(默认 ask → 人类)
34
+ └─ 4. 兜底:fallback(默认 ask → GUI 弹窗)
35
+ ```
36
+
37
+ 每次决策写入一行 JSONL 审计日志(默认 `~/.dsh/logs/approval.jsonl`):工具名、命令预览、reason、判定来源(rule / ai / ai-error / fallback)、**模式(mode)**、风险、AI 理由、耗时。
38
+
39
+ ## 审批模式(v0.2.0)
40
+
41
+ 插件提供一个与 dsh 沙箱模式**正交**的审批模式维度,三种模式按需切换:
42
+
43
+ | 模式 | 名称 | 行为 | 场景 |
44
+ |---|---|---|---|
45
+ | 1 | `manual` | **完全旁路**:不决策、不写日志,审批全部交回人类弹窗 | 回归未装插件的原生体验 |
46
+ | 2 | `ai`(默认) | 规则 → AI → ask 交人类 | 日常:低风险自动、高风险问人 |
47
+ | 3 | `ai-auto` | 规则 → AI → **ask 永不交人类**,按 `mode3OnAsk`(默认 deny)处理 | 全自动操作但又不放心 full access:AI 全权把关,绝不弹窗 |
48
+
49
+ **运行时切换**(GUI 斜杠命令,作用于当前会话,持久化到 settings):
50
+
51
+ ```
52
+ /approval-mode 显示当前模式(覆盖值 + 生效值)
53
+ /approval-mode 3 切换为 ai-auto(也接受 ai-auto / 1 / 2 / manual 等)
54
+ /approval-mode default 清除会话覆盖,回落到配置默认
55
+ ```
56
+
57
+ **ai-auto 下 ask 的归宿**(`mode3OnAsk`,默认 `deny`):规则 ask、AI 判 ask 且超容忍度、AI 故障 failOpen=ask、兜底 fallback=ask——全部按此处理,绝不弹窗。⚠️ 若设为 `allow`,AI 无法决定时也会放行高风险操作,**慎用**。
58
+
59
+ **模式持久化**:会话覆盖存 `~/.dsh/settings.yaml` 的 `dsh-codex-approval` 命名空间(settings 服务不可用时降级为纯内存,重启丢失)。默认模式由配置 `mode` 字段决定。
60
+
61
+ **与 dsh 沙箱模式的关系**:
62
+
63
+ | 沙箱模式 | AI 审核是否生效 |
64
+ |---|---|
65
+ | `read-only` | ✅ 生效——沙箱拒绝写操作,模型可申请升级(`WIDER_MODES` 允许),升级请求照常走审批链 |
66
+ | `workspace-write` | ✅ 生效(推荐组合:工作区内自由,越界 AI 把关) |
67
+ | `danger-full-access` | ⏸ 不触发——沙箱从不拒绝任何操作,没有升级请求,插件自然空闲 |
68
+
69
+ **命令多语言**:`/approval-mode` 的返回文案跟随 dsh 设置的语言(`locale.preference`,中/英)。命令 `description` 在启动时按当时语言注册,运行中切换语言后需重启才更新 description(返回文本每次实时跟随)。
70
+
71
+ **npm publish 默认 ask**:内置规则 `Bash(npm publish*) → ask`——agent 执行 `npm publish` 的升级请求**必定弹窗询问人类**,AI 无权自动放行(`ai` 模式下弹窗;`ai-auto` 模式下按 `mode3OnAsk` 处理,默认拒绝)。`npm unpublish` 无规则,由 AI 默认判定(通常判 high 直接拒绝)。
72
+
73
+ ## 拒绝归因反馈(denyFeedback,v0.3.0)
74
+
75
+ **问题**:dsh 的沙箱层把一切审批拒绝硬编码为 "the user rejected..."(`dsh-sandbox` 的 `approveEscalation`),插件 AI 拒绝时主 agent 会误以为用户拒绝了——道歉、停下、或盲目重试,而不是带理由去找更安全路径。
76
+
77
+ **方案**:插件在自身产生拒绝(规则 deny / AI deny / ai-auto 的 mode3 拒绝 / AI 故障 failOpen deny / 兜底 deny)后,把拒绝记录进内存队列;下一次 `agent/pre-step`(模型即将推理)时向消息列表追加一条**更正消息**(`user` 角色 + `source.kind: "plugin"`,机制同 dsh-time-context / dsh-tool-cordis):
78
+
79
+ ```
80
+ [auto-review] The previous action `rm -rf /tmp/x` was denied by the automatic
81
+ approval reviewer (source: deterministic rule) — this was NOT a user rejection.
82
+ Do not pursue this action via workaround or indirect execution; continue with
83
+ a materially safer alternative, or stop and ask the user.
84
+ ```
85
+
86
+ - 被拒工具的 `tool/result` 错误与更正消息出现在同一次模型推理中(紧邻),模型可完成正确归因
87
+ - 用户手动拒绝(GUI 弹窗)不经过插件 answerer,**不会被标记为自动审批拒绝**
88
+ - 模型被拒后立即结束回合时,更正留在队列,**下一回合首步注入**(消息持久化在会话中,重启后不重复注入)
89
+ - 每会话未注入队列上限 `denyFeedbackMax`(默认 3,超限丢最旧)
90
+
91
+ 配置:`denyFeedback: true|false`(默认 true);文案跟随 `locale` 设置(zh/en)。已知边界:源头文案("the user rejected")由 dsh 核心生成,本功能通过紧邻更正消息覆盖模型感知,并非源头级修正。
92
+
93
+ ## 安装
94
+
95
+ ```bash
96
+ dsh plugin --profile web add dsh-codex-approval
97
+ # 重启 dsh web 生效
98
+ ```
99
+
100
+ 插件只在目标 profile 注册(推荐 web);qqbot / headless 等 profile 不受影响。
101
+
102
+ ## 配置(~/.dsh/profiles/web/cordis.patch.yml)
103
+
104
+ ```yaml
105
+ - id: dsh-codex-approval
106
+ config:
107
+ mode: ai # manual | ai | ai-auto(默认 ai)
108
+ mode3OnAsk: deny # deny | allow(ai-auto 下 ask 的归宿;默认 deny 安全)
109
+ locale: auto # auto | zh | en(命令文案语言;auto=跟随 dsh 设置的语言偏好)
110
+ rules:
111
+ - match: 'Bash(git status*)' # 命中即自动通过(Codex approve-always)
112
+ action: allow
113
+ - match: 'Bash(rm -rf /*)' # 危险命令直接拒绝(Codex reject-always)
114
+ action: deny
115
+ - match: 'reason:*credential*' # 敏感场景强制询问
116
+ action: ask
117
+ ai:
118
+ enabled: true
119
+ provider: opencode-go # 与主 agent 同一 provider(成本一致)
120
+ model: deepseek-v4-flash # deepseek-chat 官方 API 已弃用
121
+ riskTolerance: medium # low | medium | high(仿 Codex risk tolerance)
122
+ maxPromptChars: 2000
123
+ timeoutMs: 15000
124
+ maxTokens: 512 # reasoning 余量
125
+ failOpen: ask # AI 故障兜底:ask | deny | allow
126
+ fallback: ask # 无规则命中且 AI 关闭时:ask | deny | allow
127
+ denyFeedback: true # 拒绝后向主 agent 注入归因更正消息(默认 true)
128
+ denyFeedbackMax: 3 # 未注入拒绝队列上限(1-10)
129
+ logFile: ~/.dsh/logs/approval.jsonl
130
+ ```
131
+
132
+ 不配置即用内置默认:只读命令(git status/diff/log、ls、cat、pwd、which、echo)自动放行,破坏性命令(`rm -rf /`、`rm -rf ~`、`sudo rm`、`shutdown`、`reboot`、`mkfs`)直接拒绝,敏感词(secret/password/credential/token)询问。
133
+
134
+ ## 规则语法
135
+
136
+ - 匹配对象(任一表面命中即中,大小写不敏感):
137
+ - `ToolName(args preview)` `Bash(git status)`(bash/pwsh 为原始命令)
138
+ - `reason:<文本>` — 审批 reason(如沙箱升级的 justification)
139
+ - 通配:`*` 任意序列、`?` 单字符
140
+ - 优先级:**deny > ask > allow**(与列表顺序无关);同优先级内按列表顺序取首个
141
+
142
+ ## AI 审判输入/输出
143
+
144
+ **输入**:固定系统提示(审批员角色 + risk/authorization 定义 + 只输出 JSON 约束)+ `{"toolName", "command", "reason"}`(命令截断 2000 字符,无其他上下文)。
145
+
146
+ **输出**:`{"risk":"low|medium|high","authorization":"allow|ask|deny","reason":"一句话"}`;解析策略:整体 JSON → ```json``` 代码块 → 平衡花括号扫描;枚举校验失败按 AI 故障处理。
147
+
148
+ ## 安全注意事项
149
+
150
+ - **deny 规则永远最先求值**,AI 无权覆盖显式拒绝
151
+ - AI 输出只映射为三种结果之一,不存在注入面;命令文本进 prompt 前截断
152
+ - AI 调用有超时上限(默认 15s),失败默认交还人类(fail-open,不会静默全拒)
153
+ - 审批审计对(approval/asked + approval/decided)由 dsh 审批服务持久化,插件只追加自己的决策日志
154
+ - `danger-full-access` 模式下沙箱不拒绝任何操作,审批请求不会发生,插件自然空闲
155
+ - 单次 AI 审批成本约 0.3~0.7 分钱(官方价估算),仅规则未命中时产生
156
+
157
+ ## 成本
158
+
159
+ | 场景 | 单次 Token | 单次成本(官方高峰价) |
160
+ |---|---|---|
161
+ | 典型(短命令) | ~400-500 | ≈ 0.003 元 |
162
+ | 最坏(命令 2000 字符) | ~1,500 | ≈ 0.007 元 |
163
+
164
+ ## 开发与测试
165
+
166
+ ```bash
167
+ node --test # 55 个单测:规则匹配 / 参数反查 / AI 裁决解析 / 决策流
168
+ ```
169
+
170
+ ## License
171
+
172
+ MIT
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-codex-approval bundle layer: insert the plugin row into the profile.
2
- - insert:
3
- - id: dsh-codex-approval
4
- name: dsh-codex-approval
1
+ # dsh-codex-approval bundle layer: insert the plugin row into the profile.
2
+ - insert:
3
+ - id: dsh-codex-approval
4
+ name: dsh-codex-approval
package/enrich.js CHANGED
@@ -1,68 +1,68 @@
1
- /**
2
- * dsh-codex-approval — enrich.js
3
- *
4
- * Best-effort recovery of the full tool-call arguments behind an approval
5
- * request. The approval seam hands answerers only `{ toolName, callId,
6
- * reason }`, but the session log's latest `assistant/message` contains the
7
- * complete `tool-call` content part (id, name, arguments JSON) — so by
8
- * `callId` we can recover e.g. the exact bash command that triggered a
9
- * sandbox escalation, which is what rule matching and the AI judge see.
10
- *
11
- * Everything here is defensive: any shape drift or missing data returns
12
- * null / a degraded preview, never throws.
13
- */
14
-
15
- /**
16
- * Find the parsed tool-call arguments for a callId in a session event list.
17
- * @param events - session.events (or any event array)
18
- * @param callId - the approval request's callId
19
- * @returns the parsed arguments object, or null when unrecoverable.
20
- */
21
- export function findToolCallArgs(events, callId) {
22
- if (!Array.isArray(events) || callId === undefined) return null;
23
- for (let i = events.length - 1; i >= 0; i -= 1) {
24
- const event = events[i];
25
- if (event === null || typeof event !== "object" || event.type !== "assistant/message") continue;
26
- const content = event.data?.message?.content;
27
- if (!Array.isArray(content)) continue;
28
- for (let j = content.length - 1; j >= 0; j -= 1) {
29
- const part = content[j];
30
- if (part === null || typeof part !== "object" || part.type !== "tool-call") continue;
31
- if (part.id !== callId) continue;
32
- try {
33
- return JSON.parse(part.arguments ?? "null");
34
- } catch {
35
- return null;
36
- }
37
- }
38
- }
39
- return null;
40
- }
41
-
42
- /**
43
- * Build the Codex-style args preview used for rule matching and the AI
44
- * prompt: the raw command for bash/pwsh, compact JSON otherwise.
45
- * @param args - parsed tool arguments (or null)
46
- * @param toolName - the tool that was called
47
- * @param maxChars - preview length cap
48
- */
49
- export function argsPreview(args, toolName, maxChars) {
50
- let preview;
51
- if (args !== null && typeof args === "object") {
52
- if ((toolName === "bash" || toolName === "pwsh") && typeof args.command === "string") {
53
- preview = args.command;
54
- } else {
55
- try {
56
- preview = JSON.stringify(args);
57
- } catch {
58
- preview = String(args);
59
- }
60
- }
61
- } else if (args === undefined || args === null) {
62
- preview = "";
63
- } else {
64
- preview = String(args);
65
- }
66
- if (preview.length > maxChars) preview = `${preview.slice(0, maxChars)}…`;
67
- return preview;
68
- }
1
+ /**
2
+ * dsh-codex-approval — enrich.js
3
+ *
4
+ * Best-effort recovery of the full tool-call arguments behind an approval
5
+ * request. The approval seam hands answerers only `{ toolName, callId,
6
+ * reason }`, but the session log's latest `assistant/message` contains the
7
+ * complete `tool-call` content part (id, name, arguments JSON) — so by
8
+ * `callId` we can recover e.g. the exact bash command that triggered a
9
+ * sandbox escalation, which is what rule matching and the AI judge see.
10
+ *
11
+ * Everything here is defensive: any shape drift or missing data returns
12
+ * null / a degraded preview, never throws.
13
+ */
14
+
15
+ /**
16
+ * Find the parsed tool-call arguments for a callId in a session event list.
17
+ * @param events - session.events (or any event array)
18
+ * @param callId - the approval request's callId
19
+ * @returns the parsed arguments object, or null when unrecoverable.
20
+ */
21
+ export function findToolCallArgs(events, callId) {
22
+ if (!Array.isArray(events) || callId === undefined) return null;
23
+ for (let i = events.length - 1; i >= 0; i -= 1) {
24
+ const event = events[i];
25
+ if (event === null || typeof event !== "object" || event.type !== "assistant/message") continue;
26
+ const content = event.data?.message?.content;
27
+ if (!Array.isArray(content)) continue;
28
+ for (let j = content.length - 1; j >= 0; j -= 1) {
29
+ const part = content[j];
30
+ if (part === null || typeof part !== "object" || part.type !== "tool-call") continue;
31
+ if (part.id !== callId) continue;
32
+ try {
33
+ return JSON.parse(part.arguments ?? "null");
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * Build the Codex-style args preview used for rule matching and the AI
44
+ * prompt: the raw command for bash/pwsh, compact JSON otherwise.
45
+ * @param args - parsed tool arguments (or null)
46
+ * @param toolName - the tool that was called
47
+ * @param maxChars - preview length cap
48
+ */
49
+ export function argsPreview(args, toolName, maxChars) {
50
+ let preview;
51
+ if (args !== null && typeof args === "object") {
52
+ if ((toolName === "bash" || toolName === "pwsh") && typeof args.command === "string") {
53
+ preview = args.command;
54
+ } else {
55
+ try {
56
+ preview = JSON.stringify(args);
57
+ } catch {
58
+ preview = String(args);
59
+ }
60
+ }
61
+ } else if (args === undefined || args === null) {
62
+ preview = "";
63
+ } else {
64
+ preview = String(args);
65
+ }
66
+ if (preview.length > maxChars) preview = `${preview.slice(0, maxChars)}…`;
67
+ return preview;
68
+ }