@duke-dsh-plugins/dsh-agent-approval 1.7.2 → 1.8.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/README.md +18 -15
- package/client.js +240 -11
- package/cordis.patch.yml +12 -6
- package/index.js +1003 -59
- package/package.json +2 -2
- package/typert.host.js +105 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" color="#4D6BFE"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></svg>
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h3 align="center">DeepSeek Harness
|
|
5
|
+
<h3 align="center">DeepSeek Harness 自动审批权限插件</h3>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="https://img.shields.io/badge/DSH-Plugin-4D6BFE?style=flat" alt="DSH plugin">
|
|
@@ -14,21 +14,22 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
为 [DeepSeek Harness(DSH)](https://github.com/deepseek-ai/deepseek-harness) Web UI 打造的
|
|
17
|
+
为 [DeepSeek Harness(DSH)](https://github.com/deepseek-ai/deepseek-harness) Web UI 打造的 **自动审批**权限插件:当内置的权限选项(`workspace-write + ask` / `danger-full-access + never`)不能满足需求时,为会话开启第三种模式——**以 workspace-write 为基线,提权请求交由独立审批 Agent 裁决,有风险就拒绝**。
|
|
18
18
|
|
|
19
19
|
## 功能
|
|
20
20
|
|
|
21
21
|
| 功能 | 说明 |
|
|
22
22
|
|---|---|
|
|
23
|
-
| 🛡 **权限菜单第四项** | `/permission` 菜单新增
|
|
23
|
+
| 🛡 **权限菜单第四项** | `/permission` 菜单新增 **自动审批** 预设;选中即开启,切到其他预设自动关闭,跨重启保持 |
|
|
24
24
|
| 🤖 新权限模式 | 开启后:沙箱基线固定 `workspace-write`,审批策略切到 `ask`(内部接管),**不再弹人工审批** |
|
|
25
|
-
| 🤖
|
|
25
|
+
| 🤖 自动裁决(默认 LLM 直连) | 每次提权请求由**一次 LLM 直连调用**裁决(v1.8.0 起默认):与子代理同一套审批人格 / 提示词 / 结构化裁决 `{decision, riskLevel, rationale}`,但**不创建审批子会话**(零上下文污染);设置页可切回「隔离子代理」(一次性 `spawn` 子代理:独立会话、零工具、只读材料) |
|
|
26
|
+
| 🕵️ 自动审查(逐调用,实验) | 可选预设(需先在设置页启用 Jev):**Full access 基线**,每个工具调用(含 PTC 内层)执行前经 Jev 判定一次,风险调用**直接拒绝、body 不执行、不转人工**(fail-closed);规则表与会话内信任先行短路降噪;`/agent-review on\|off` 或菜单「自动审查」开启 |
|
|
26
27
|
| ⛔ 风险即拒绝 | 破坏性 / 不可逆 / 越界(含修改操作系统或其他应用数据)/ 理由与实际命令不符 → 直接 `reject`;仅"安全、可逆、与任务相符、理由诚实"才 `approve`——项目自身的安装/部署脚本写其文档指定路径属任务所需 |
|
|
27
28
|
| 🔒 Fail-closed | 审批 Agent 启动失败、超时(可配 30s–600s)、结果不合法 → 一律按拒绝处理,绝不静默放行 |
|
|
28
29
|
| ⚙️ 审批模型可配置 | 设置页选择 Provider + Model,不选则固定用 **Harness 默认模型**(不跟随请求会话,口径稳定);选择与超时**持久保存**,重启不丢 |
|
|
29
30
|
| ⚡ TypeSafe Jev 决策模型后端 | 审批模型可选 **TypeSafe Jev**(System One 结构化决策模型):审批时直连其 API,用类型化问题(Choice/Noul)毫秒级返回带校准概率的裁决;置信度低于阈值按 fail-closed 处理,审计理由由概率合成(需在设置页填 API Key,或设 `TYPESAFE_API_KEY`) |
|
|
30
31
|
| 📋 审计记录(随会话) | 会话窗口顶部的**「审批」标签页**(轨迹旁)查看本会话全部审批:结论 / 风险等级 / 模型 / 耗时 / 理由;悬停看完整理由与**精确工具参数**;审批 Agent 的会话 id 可回溯完整推理;已批准行可一键**「加白」**存为放行规则。记录存在**会话存储目录内的独立文件**——随会话恢复,删除会话即随之删除 |
|
|
31
|
-
| 🔁 可逆开关 |
|
|
32
|
+
| 🔁 可逆开关 | 权限菜单「自动审批」预设、`/agent-approval on\|off` 命令两条等价路径;关闭时**恢复开启前的权限旋钮** |
|
|
32
33
|
|
|
33
34
|
## 工作原理
|
|
34
35
|
|
|
@@ -39,7 +40,8 @@
|
|
|
39
40
|
工具请求提权(sandbox_permissions / 人工 ask)
|
|
40
41
|
└─ ctx.approval.request() → approval/request 瀑布
|
|
41
42
|
└─ 本插件 prepend 抢占(先于人工弹窗 answerer)
|
|
42
|
-
├─ (默认)
|
|
43
|
+
├─ (默认)LLM 直连裁决(一次 stream 调用 · 同套人格/提示词/裁决格式 · 零子会话)
|
|
44
|
+
├─ (可选)spawn 审批 Agent(独立会话 · 零工具 · 结构化裁决 · 不会递归审批)
|
|
43
45
|
└─ (或)直连 TypeSafe Jev 决策模型(state + 类型化问题 → 概率化裁决 · 亚秒级)
|
|
44
46
|
├─ approve → allowed-once(该次放行)
|
|
45
47
|
├─ reject → rejected(风险操作,最终拒绝)
|
|
@@ -55,17 +57,17 @@
|
|
|
55
57
|
|
|
56
58
|
### 标准安装(推荐)
|
|
57
59
|
|
|
58
|
-
本插件是**标准 DSH bundle**:`package.json` 声明 `dsh.bundle.patch`,包内 `cordis.patch.yml` 同时完成两件事——`- insert:` 挂载插件本身,`- id: permission` 把
|
|
60
|
+
本插件是**标准 DSH bundle**:`package.json` 声明 `dsh.bundle.patch`,包内 `cordis.patch.yml` 同时完成两件事——`- insert:` 挂载插件本身,`- id: permission` 把 **自动审批** 预设注册进 `/permission` 菜单。用官方 `dsh plugin` 命令安装:
|
|
59
61
|
|
|
60
62
|
```bash
|
|
61
63
|
# 本地开发:pnpm 软链到本仓库,改代码即生效(无需重新复制)
|
|
62
64
|
dsh plugin --profile web add /path/to/dsh-agent-approval
|
|
63
65
|
|
|
64
66
|
# 正式发布:从 GitHub Release tarball 安装(资产名为 npm pack 的 scope 形式)
|
|
65
|
-
dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-approval/releases/download/v1.
|
|
67
|
+
dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-approval/releases/download/v1.8.0/duke-dsh-plugins-dsh-agent-approval-1.8.0.tgz
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
重启 DSH 后:设置面板出现
|
|
70
|
+
重启 DSH 后:设置面板出现 **自动审批** 页;`/permission` 菜单出现第四项 **自动审批**。
|
|
69
71
|
|
|
70
72
|
> **可选:权限菜单图标**。菜单图标硬编码在官方 `dsh-client-ui-conversation` 的 `permissionGlyphs` 映射里(无公开注册口),标准安装不会补它——不跑下面的命令只是**菜单项没有图标**,预设与功能不受影响。想让菜单项带盾牌图标,装完再跑一次(幂等;DSH 升级重装原版 bundle 后重跑即可):
|
|
71
73
|
> ```bash
|
|
@@ -76,19 +78,20 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-app
|
|
|
76
78
|
|
|
77
79
|
## 使用
|
|
78
80
|
|
|
79
|
-
1. **开启**:在 `/permission` 菜单选
|
|
81
|
+
1. **开启**:在 `/permission` 菜单选 **自动审批**,或输入 `/agent-approval on`。
|
|
80
82
|
2. **自动裁决**:之后该会话里的提权请求(例如命令被沙箱拒绝后带 `sandbox_permissions` 的重试)不再弹窗,由审批 Agent 在后台裁决并放行/拒绝。
|
|
81
83
|
3. **审计**:会话窗口顶部的**「审批」标签页**(轨迹旁)查看本会话的审批记录;悬停"审批理由"看完整理由与工具参数;已批准行可「加白」存为放行规则。记录存在会话存储目录内的独立文件,删除会话即随之删除;v1.4 的旧全局记录用 `node scripts/migrate-records.mjs` 一次性迁移(`--dry-run` 预览)。
|
|
82
|
-
4. **配置**:设置 →
|
|
83
|
-
5.
|
|
84
|
+
4. **配置**:设置 → **自动审批** 设置审批模型(Harness 默认模型、指定 Provider/Model,或 **TypeSafe Jev**——选 Jev 后在下方卡片填 API Key 并按需调整 Endpoint / 模型版本 / 置信度阈值)、**裁决方式**(LLM 直连 / 隔离子代理)、审批超时与放行/拒绝规则。
|
|
85
|
+
5. **自动审查(可选)**:设置页启用 Jev 后,`/permission` 菜单出现 **自动审查**(或执行 `/agent-review on`)——该会话切到 Full access 基线,**每个工具调用**执行前经 Jev 判定,风险调用直接拒绝(不转人工);关闭恢复原旋钮。
|
|
86
|
+
6. **关闭**:菜单切回其他预设,或 `/agent-approval off`,恢复开启前的沙箱模式与审批策略。
|
|
84
87
|
|
|
85
88
|
## 目录结构
|
|
86
89
|
|
|
87
90
|
```
|
|
88
91
|
dsh-agent-approval/
|
|
89
|
-
├── index.js # Host 半:AgentApprovalService(审批瀑布抢占 +
|
|
90
|
-
├── client.js # Client
|
|
91
|
-
├── typert.host.js # Typert Host manifest(agentApproval
|
|
92
|
+
├── index.js # Host 半:AgentApprovalService(审批瀑布抢占 + LLM 直连/子代理/Jev 裁决 + 逐调用审查 + 审计)
|
|
93
|
+
├── client.js # Client 半:设置页「自动审批」+ 会话「审批」审计标签页 bundle
|
|
94
|
+
├── typert.host.js # Typert Host manifest(agentApproval 10 个 Remote 方法的描述)
|
|
92
95
|
├── cordis.patch.yml # dsh bundle patch(挂载行 + permission 预设表覆盖)
|
|
93
96
|
├── scripts/patch-glyph.mjs # 可选:权限菜单图标补丁(标准安装不自动执行)
|
|
94
97
|
├── scripts/check-typert-manifest.mjs # npm run check 用:双代 Typert codec 契约冒烟
|
package/client.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* session — restored with it after a restart, gone when the session is
|
|
12
12
|
* deleted. Rows offer the one-click「加白」rule shortcut.
|
|
13
13
|
*
|
|
14
|
-
* 2. A "
|
|
14
|
+
* 2. A "自动审批" page in the Settings panel (`settings.section`):
|
|
15
15
|
* approval model picker (provider + model, the harness default, or the
|
|
16
16
|
* TypeSafe Jev direct HTTP backend with its API key / endpoint /
|
|
17
17
|
* confidence-gate settings), judge timeout setting (fail-closed), the
|
|
18
18
|
* list of sessions with the mode enabled (session-list title +
|
|
19
19
|
* workspace), and the allow/deny rule table.
|
|
20
20
|
*
|
|
21
|
-
* Session-level on/off lives in the /permission menu (the "
|
|
21
|
+
* Session-level on/off lives in the /permission menu (the "自动审批"
|
|
22
22
|
* preset, registered by the package's cordis.patch.yml bundle patch) and the
|
|
23
23
|
* /agent-approval command — deliberately NO composer chip: a second toggle
|
|
24
24
|
* beside the permission menu it belongs to was redundant.
|
|
@@ -98,6 +98,15 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
would be an uninvited extra and is deliberately left unmarked. */
|
|
99
99
|
[data-dsh-agent-approval-perm-item]::before,
|
|
100
100
|
[data-dsh-agent-approval-perm-trigger]::before{content:'';flex:none;width:16px;height:16px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8 3.2L9.1 5.9L11.8 7L9.1 8.1L8 10.8L6.9 8.1L4.2 7L6.9 5.9Z' fill='black'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8 3.2L9.1 5.9L11.8 7L9.1 8.1L8 10.8L6.9 8.1L4.2 7L6.9 5.9Z' fill='black'/%3E%3C/svg%3E") center/contain no-repeat}
|
|
101
|
+
|
|
102
|
+
/* 自动审查 (agent-review) menu row: same shield-glyph treatment as the
|
|
103
|
+
自动审批 row (same glyph-set guard), and hidden outright while the Jev gate
|
|
104
|
+
is closed (body[data-...-review-gate="off"], set from getState()
|
|
105
|
+
.reviewAvailable) — the mode cannot run without the Jev judge, so the host
|
|
106
|
+
would bounce the selection back anyway. */
|
|
107
|
+
[data-dsh-agent-approval-review-gate="off"] [data-dsh-agent-approval-review-item]{display:none}
|
|
108
|
+
[data-dsh-agent-approval-review-item]::before{content:'';flex:none;width:16px;height:16px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8 3.2L9.1 5.9L11.8 7L9.1 8.1L8 10.8L6.9 8.1L4.2 7L6.9 5.9Z' fill='black'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z' stroke='black' stroke-width='1.31831' stroke-linejoin='round'/%3E%3Cpath d='M8 3.2L9.1 5.9L11.8 7L9.1 8.1L8 10.8L6.9 8.1L4.2 7L6.9 5.9Z' fill='black'/%3E%3C/svg%3E") center/contain no-repeat}
|
|
109
|
+
.aapr-mode-review{margin-left:6px;font-size:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;padding:0 4px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
101
110
|
`;
|
|
102
111
|
|
|
103
112
|
// ---- Settings nav icon --------------------------------------------------
|
|
@@ -106,7 +115,12 @@ window.__ModuleLoader__.load({
|
|
|
106
115
|
// paints a generic gear for every external section. Mark only this
|
|
107
116
|
// plugin's localized nav row so the CSS above can replace the fallback
|
|
108
117
|
// gear; the disposer clears the marker for HMR / plugin disable.
|
|
109
|
-
|
|
118
|
+
// Settings nav icon row text === settings.section label === permission
|
|
119
|
+
// preset display name (the two stay identical by convention — see
|
|
120
|
+
// AGENTS.md): 自动审批 as of v1.8.0 (renamed from "Agent 审批").
|
|
121
|
+
const SETTINGS_LABEL = "自动审批";
|
|
122
|
+
/** The agent-review preset display name (permission menu row). */
|
|
123
|
+
const REVIEW_LABEL = "自动审查";
|
|
110
124
|
const SETTINGS_NAV_MARKER = "data-dsh-agent-approval-settings-nav";
|
|
111
125
|
|
|
112
126
|
function registerSettingsNavIcon(label) {
|
|
@@ -217,6 +231,62 @@ window.__ModuleLoader__.load({
|
|
|
217
231
|
};
|
|
218
232
|
}
|
|
219
233
|
|
|
234
|
+
// ---- 自动审查 menu row + Jev gate visibility -----------------------------
|
|
235
|
+
// Marks the agent-review preset's /permission menu row (same guarded
|
|
236
|
+
// marking as the 自动审批 row) and hides it while the Jev gate is closed:
|
|
237
|
+
// the review mode cannot run without the Jev judge, so offering the row
|
|
238
|
+
// would only bounce the user back (the host-side gate fallback keeps them
|
|
239
|
+
// safe either way). Gate snapshot comes from getState().reviewAvailable.
|
|
240
|
+
const REVIEW_ITEM_MARKER = "data-dsh-agent-approval-review-item";
|
|
241
|
+
const REVIEW_GATE_ATTR = "data-dsh-agent-approval-review-gate";
|
|
242
|
+
|
|
243
|
+
function setReviewGate(available) {
|
|
244
|
+
try {
|
|
245
|
+
document.body.setAttribute(REVIEW_GATE_ATTR, available ? "on" : "off");
|
|
246
|
+
} catch (e) {
|
|
247
|
+
/* body may be unavailable during teardown */
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function registerReviewMenuItem(label) {
|
|
252
|
+
let disposed = false;
|
|
253
|
+
const sync = function () {
|
|
254
|
+
if (disposed) return;
|
|
255
|
+
const currentLabel = String(label).trim();
|
|
256
|
+
if (currentLabel.length === 0) return;
|
|
257
|
+
const menus = document.querySelectorAll('[role="menu"]');
|
|
258
|
+
const glyphMenus = [];
|
|
259
|
+
for (let i = 0; i < menus.length; i++) {
|
|
260
|
+
if (menus[i].querySelector('span[class*="itemIcon"]') !== null) glyphMenus.push(menus[i]);
|
|
261
|
+
}
|
|
262
|
+
const items = document.querySelectorAll('[role="menu"] button[role="menuitem"]');
|
|
263
|
+
for (let i = 0; i < items.length; i++) {
|
|
264
|
+
const button = items[i];
|
|
265
|
+
const text = button.textContent ? button.textContent.trim() : "";
|
|
266
|
+
const menu = button.closest('[role="menu"]');
|
|
267
|
+
if (text === currentLabel && menu !== null && glyphMenus.indexOf(menu) !== -1) {
|
|
268
|
+
button.setAttribute(REVIEW_ITEM_MARKER, "");
|
|
269
|
+
} else {
|
|
270
|
+
button.removeAttribute(REVIEW_ITEM_MARKER);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
sync();
|
|
275
|
+
const observer = new MutationObserver(sync);
|
|
276
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
277
|
+
return function () {
|
|
278
|
+
disposed = true;
|
|
279
|
+
observer.disconnect();
|
|
280
|
+
const marked = document.querySelectorAll("[" + REVIEW_ITEM_MARKER + "]");
|
|
281
|
+
for (let i = 0; i < marked.length; i++) marked[i].removeAttribute(REVIEW_ITEM_MARKER);
|
|
282
|
+
try {
|
|
283
|
+
document.body.removeAttribute(REVIEW_GATE_ATTR);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
/* teardown only */
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
220
290
|
// ---- Client Remote contribution -------------------------------------------
|
|
221
291
|
// The browser-side `remote.agentApproval` service only exists after this
|
|
222
292
|
// module mounts its namespace via ctx.remote.$mount(): dsh-api-remotes'
|
|
@@ -263,6 +333,24 @@ window.__ModuleLoader__.load({
|
|
|
263
333
|
parameters: param("dsh-agent-approval#AgentApprovalSetModelRequest"),
|
|
264
334
|
result: result("dsh-agent-approval#AgentApprovalSetModelResult"),
|
|
265
335
|
},
|
|
336
|
+
{
|
|
337
|
+
id: "dsh-agent-approval#agentApproval/setJudgeMode",
|
|
338
|
+
service: "agentApproval",
|
|
339
|
+
namespace: "agentApproval",
|
|
340
|
+
method: "setJudgeMode",
|
|
341
|
+
invocation: { kind: "direct" },
|
|
342
|
+
parameters: param("dsh-agent-approval#AgentApprovalSetJudgeModeRequest"),
|
|
343
|
+
result: result("dsh-agent-approval#AgentApprovalSetJudgeModeResult"),
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "dsh-agent-approval#agentApproval/setReviewDefault",
|
|
347
|
+
service: "agentApproval",
|
|
348
|
+
namespace: "agentApproval",
|
|
349
|
+
method: "setReviewDefault",
|
|
350
|
+
invocation: { kind: "direct" },
|
|
351
|
+
parameters: param("dsh-agent-approval#AgentApprovalSetReviewDefaultRequest"),
|
|
352
|
+
result: result("dsh-agent-approval#AgentApprovalSetReviewDefaultResult"),
|
|
353
|
+
},
|
|
266
354
|
{
|
|
267
355
|
id: "dsh-agent-approval#agentApproval/setJevConfig",
|
|
268
356
|
service: "agentApproval",
|
|
@@ -348,9 +436,27 @@ window.__ModuleLoader__.load({
|
|
|
348
436
|
// the official permissionGlyphs map; no public registration seam).
|
|
349
437
|
ctx.effect(() => registerPermissionGlyphIcon(SETTINGS_LABEL));
|
|
350
438
|
|
|
439
|
+
// Mark the 自动审查 menu row (same glyph-set guard) and hide it while
|
|
440
|
+
// the Jev gate is closed; the same snapshot feeds the Settings card.
|
|
441
|
+
ctx.effect(() => registerReviewMenuItem(REVIEW_LABEL));
|
|
442
|
+
|
|
351
443
|
// ctx.get() reads the service without the property-accessor inject guard.
|
|
352
444
|
const remote = ctx.get("remote.agentApproval");
|
|
353
445
|
|
|
446
|
+
// Jev gate snapshot (also flips the 自动审查 menu row's visibility).
|
|
447
|
+
// Guarded: a degraded/old remote surface fails the gate closed.
|
|
448
|
+
if (remote && typeof remote.getState === "function") {
|
|
449
|
+
remote
|
|
450
|
+
.getState()
|
|
451
|
+
.then((res) => {
|
|
452
|
+
const s = pick(res);
|
|
453
|
+
setReviewGate(s.reviewAvailable === true);
|
|
454
|
+
})
|
|
455
|
+
.catch(() => setReviewGate(false));
|
|
456
|
+
} else {
|
|
457
|
+
setReviewGate(false);
|
|
458
|
+
}
|
|
459
|
+
|
|
354
460
|
// ---- helpers -----------------------------------------------------------
|
|
355
461
|
|
|
356
462
|
/**
|
|
@@ -411,7 +517,7 @@ window.__ModuleLoader__.load({
|
|
|
411
517
|
}
|
|
412
518
|
|
|
413
519
|
// NOTE: no composer chip anymore. The mode lives in the /permission
|
|
414
|
-
// menu as the "
|
|
520
|
+
// menu as the "自动审批" preset (same place users switch Read-only /
|
|
415
521
|
// Workspace Write / Full access); a second toggle beside that menu was
|
|
416
522
|
// redundant. Enabling surfaces: the menu, the /agent-approval command,
|
|
417
523
|
// and this Settings page (whose session chips can also disable one).
|
|
@@ -427,6 +533,16 @@ window.__ModuleLoader__.load({
|
|
|
427
533
|
const setProvider = providerSlot[1];
|
|
428
534
|
const modelSlot = React.useState("");
|
|
429
535
|
const setModel = modelSlot[1];
|
|
536
|
+
// Judge invocation mode (v1.8.0): "llm" (default, one direct stream
|
|
537
|
+
// call — no subagent session) / "subagent" (isolated judge child).
|
|
538
|
+
const judgeModeSlot = React.useState("llm");
|
|
539
|
+
const judgeMode = judgeModeSlot[0];
|
|
540
|
+
const setJudgeMode = judgeModeSlot[1];
|
|
541
|
+
// v1.8.0: global default — fresh sessions auto-enter 自动审查
|
|
542
|
+
// (Jev-gated); shown as the 逐调用审查 switch when Provider = Jev.
|
|
543
|
+
const reviewDefaultSlot = React.useState(false);
|
|
544
|
+
const reviewDefault = reviewDefaultSlot[0];
|
|
545
|
+
const setReviewDefault = reviewDefaultSlot[1];
|
|
430
546
|
const timeoutSlot = React.useState("");
|
|
431
547
|
const setTimeoutDraft = timeoutSlot[1];
|
|
432
548
|
// Jev backend drafts (edited in the Jev card shown when the judge
|
|
@@ -456,6 +572,12 @@ window.__ModuleLoader__.load({
|
|
|
456
572
|
setState(s);
|
|
457
573
|
setProvider(s.model.provider);
|
|
458
574
|
setModel(s.model.model);
|
|
575
|
+
// A not-yet-restarted old host sends no `judgeMode` — default.
|
|
576
|
+
setJudgeMode(s.judgeMode === "subagent" ? "subagent" : "llm");
|
|
577
|
+
// Gate snapshot also hides/shows the 自动审查 menu row.
|
|
578
|
+
setReviewGate(s.reviewAvailable === true);
|
|
579
|
+
// A not-yet-restarted old host sends no `reviewDefault` — off.
|
|
580
|
+
setReviewDefault(s.reviewDefault === true);
|
|
459
581
|
setTimeoutDraft(String(s.timeoutMs));
|
|
460
582
|
// A not-yet-restarted old host sends no `jev` field — keep drafts.
|
|
461
583
|
setJevKey(s.jev && typeof s.jev.apiKey === "string" ? s.jev.apiKey : "");
|
|
@@ -489,6 +611,40 @@ window.__ModuleLoader__.load({
|
|
|
489
611
|
})
|
|
490
612
|
.catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
|
|
491
613
|
};
|
|
614
|
+
const saveJudgeMode = (mode) => {
|
|
615
|
+
if (typeof remote.setJudgeMode !== "function") {
|
|
616
|
+
setNote("Host 半未更新(缺少 setJudgeMode):请重装本插件并重启 DSH。");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
remote
|
|
620
|
+
.setJudgeMode({ mode: mode })
|
|
621
|
+
.then(() => {
|
|
622
|
+
setJudgeMode(mode);
|
|
623
|
+
setNote(
|
|
624
|
+
mode === "subagent"
|
|
625
|
+
? "裁决方式已保存:隔离子代理(每次裁决创建一个审批子会话)"
|
|
626
|
+
: "裁决方式已保存:LLM 直连(不创建子会话,零上下文污染)",
|
|
627
|
+
);
|
|
628
|
+
})
|
|
629
|
+
.catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
|
|
630
|
+
};
|
|
631
|
+
const saveReviewDefault = (on) => {
|
|
632
|
+
if (typeof remote.setReviewDefault !== "function") {
|
|
633
|
+
setNote("Host 半未更新(缺少 setReviewDefault):请重装本插件并重启 DSH。");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
remote
|
|
637
|
+
.setReviewDefault({ on: on })
|
|
638
|
+
.then(() => {
|
|
639
|
+
setReviewDefault(on);
|
|
640
|
+
setNote(
|
|
641
|
+
on
|
|
642
|
+
? "逐调用审查已开启:新开会话自动进入自动审查(Jev 判定每个工具调用)"
|
|
643
|
+
: "逐调用审查已关闭:新会话按默认预设开启",
|
|
644
|
+
);
|
|
645
|
+
})
|
|
646
|
+
.catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
|
|
647
|
+
};
|
|
492
648
|
const jevKey = jevKeySlot[0];
|
|
493
649
|
const jevEndpoint = jevEndpointSlot[0];
|
|
494
650
|
const jevModel = jevModelSlot[0];
|
|
@@ -604,13 +760,13 @@ window.__ModuleLoader__.load({
|
|
|
604
760
|
h(
|
|
605
761
|
"div",
|
|
606
762
|
{ className: "aapr-card" },
|
|
607
|
-
h("h3", null, "
|
|
763
|
+
h("h3", null, "自动审批权限"),
|
|
608
764
|
h(
|
|
609
765
|
"div",
|
|
610
766
|
{ className: "aapr-muted" },
|
|
611
767
|
"一种新的权限模式:以 workspace-write 为基线沙箱;当工具请求提权(更宽的沙箱)时,由一个独立的审批 Agent 评估风险——安全、可逆、与任务相符的操作自动批准,破坏性、不可逆、越界或理由不符的操作直接拒绝。",
|
|
612
768
|
h("br", null),
|
|
613
|
-
"在输入框 /permission
|
|
769
|
+
"在输入框 /permission 菜单选择「自动审批」预设,或执行命令 /agent-approval on|off 为会话开启;每个会话的审批审计记录在该会话窗口顶部的「审批」标签页(轨迹旁),随会话保存。",
|
|
614
770
|
),
|
|
615
771
|
note !== "" ? h("div", { className: "aapr-muted" }, note) : null,
|
|
616
772
|
),
|
|
@@ -660,6 +816,34 @@ window.__ModuleLoader__.load({
|
|
|
660
816
|
h(ui.Button, { variant: "primary", size: "sm", onClick: saveModel }, "保存"),
|
|
661
817
|
),
|
|
662
818
|
h("div", { className: "aapr-muted" }, defaultHint),
|
|
819
|
+
provider !== "typesafe"
|
|
820
|
+
? h(
|
|
821
|
+
"div",
|
|
822
|
+
{ className: "aapr-row" },
|
|
823
|
+
h(
|
|
824
|
+
"label",
|
|
825
|
+
null,
|
|
826
|
+
"裁决方式:",
|
|
827
|
+
h(
|
|
828
|
+
"select",
|
|
829
|
+
{
|
|
830
|
+
className: "aapr-select",
|
|
831
|
+
value: judgeMode,
|
|
832
|
+
onChange: (e) => saveJudgeMode(e.target.value),
|
|
833
|
+
},
|
|
834
|
+
h("option", { value: "llm" }, "LLM 直连(默认,不创建子会话)"),
|
|
835
|
+
h("option", { value: "subagent" }, "隔离子代理(legacy,创建审批子会话)"),
|
|
836
|
+
),
|
|
837
|
+
),
|
|
838
|
+
)
|
|
839
|
+
: null,
|
|
840
|
+
provider !== "typesafe"
|
|
841
|
+
? h(
|
|
842
|
+
"div",
|
|
843
|
+
{ className: "aapr-muted" },
|
|
844
|
+
"LLM 直连与隔离子代理是同一套审批人格、提示词与裁决格式({decision, riskLevel, rationale}),仅调用方式不同:前者一次直连模型调用完成裁决、不启动审批子代理(零上下文污染);后者每次裁决创建一个独立子会话(v1.8.0 前的唯一行为)。TypeSafe Jev 后端不受此设置影响。",
|
|
845
|
+
)
|
|
846
|
+
: null,
|
|
663
847
|
),
|
|
664
848
|
provider === "typesafe"
|
|
665
849
|
? h(
|
|
@@ -730,6 +914,46 @@ window.__ModuleLoader__.load({
|
|
|
730
914
|
),
|
|
731
915
|
)
|
|
732
916
|
: null,
|
|
917
|
+
provider === "typesafe"
|
|
918
|
+
? h(
|
|
919
|
+
"div",
|
|
920
|
+
{ className: "aapr-card" },
|
|
921
|
+
h("h3", null, "自动审查(逐调用审查)"),
|
|
922
|
+
h(
|
|
923
|
+
"div",
|
|
924
|
+
{ className: "aapr-row" },
|
|
925
|
+
h(
|
|
926
|
+
"label",
|
|
927
|
+
null,
|
|
928
|
+
"逐调用审查:",
|
|
929
|
+
h(
|
|
930
|
+
"select",
|
|
931
|
+
{
|
|
932
|
+
className: "aapr-select",
|
|
933
|
+
value: reviewDefault ? "on" : "off",
|
|
934
|
+
disabled: state !== null && state.reviewAvailable !== true,
|
|
935
|
+
onChange: (e) => saveReviewDefault(e.target.value === "on"),
|
|
936
|
+
},
|
|
937
|
+
h("option", { value: "on" }, "开(新会话自动审查每个工具调用)"),
|
|
938
|
+
h("option", { value: "off" }, "关(新会话按默认预设)"),
|
|
939
|
+
),
|
|
940
|
+
),
|
|
941
|
+
),
|
|
942
|
+
h(
|
|
943
|
+
"div",
|
|
944
|
+
{ className: "aapr-muted" },
|
|
945
|
+
"开启后,新开会话自动进入自动审查:以 danger-full-access 为基线,每个工具调用(含 PTC 内层调用,外层 run_code 传输除外)执行前经 Jev 判定一次——风险调用直接拒绝、body 不执行、不转人工(fail-closed,拒绝即最终结论)。规则表与会话内信任缓存先行短路降噪;命中拒绝规则、Jev 判拒、低置信、超时、网络故障一律拒绝该调用(工具卡片显示 AGENT_REVIEW_DENIED 详情与风险理由)。",
|
|
946
|
+
h("br", null),
|
|
947
|
+
"已存在的会话不受此开关影响,可用 /permission 菜单「自动审查」或 /agent-review on|off 单独切换。审计逐调用记录在「审批」标签页(工具列标注「逐调用」)。",
|
|
948
|
+
h("br", null),
|
|
949
|
+
state === null
|
|
950
|
+
? "状态加载中…"
|
|
951
|
+
: state.reviewAvailable === true
|
|
952
|
+
? "当前状态:Jev 判定可用。"
|
|
953
|
+
: "当前状态:Jev 判定不可用(缺 API Key),开关已禁用,新会话不会自动审查。",
|
|
954
|
+
),
|
|
955
|
+
)
|
|
956
|
+
: null,
|
|
733
957
|
h(
|
|
734
958
|
"div",
|
|
735
959
|
{ className: "aapr-card" },
|
|
@@ -754,7 +978,7 @@ window.__ModuleLoader__.load({
|
|
|
754
978
|
state === null
|
|
755
979
|
? h("div", { className: "aapr-muted" }, "加载中…")
|
|
756
980
|
: state.enabledSessions.length === 0
|
|
757
|
-
? h("div", { className: "aapr-muted" }, "
|
|
981
|
+
? h("div", { className: "aapr-muted" }, "当前没有会话开启自动审批/自动审查。")
|
|
758
982
|
: h(
|
|
759
983
|
"div",
|
|
760
984
|
{ className: "aapr-row" },
|
|
@@ -783,7 +1007,7 @@ window.__ModuleLoader__.load({
|
|
|
783
1007
|
: null,
|
|
784
1008
|
h(
|
|
785
1009
|
"button",
|
|
786
|
-
{ onClick: () => disableSession(sid), title: "
|
|
1010
|
+
{ onClick: () => disableSession(sid), title: "关闭该会话的自动审批/自动审查" },
|
|
787
1011
|
"✕",
|
|
788
1012
|
),
|
|
789
1013
|
);
|
|
@@ -918,9 +1142,9 @@ window.__ModuleLoader__.load({
|
|
|
918
1142
|
{ className: "aapr-view-head" },
|
|
919
1143
|
h("h3", { className: "aapr-view-title" }, "审批审计(本会话)"),
|
|
920
1144
|
enabled === true
|
|
921
|
-
? h("span", { className: "aapr-state-on" }, "●
|
|
1145
|
+
? h("span", { className: "aapr-state-on" }, "● 自动审批/自动审查已开启")
|
|
922
1146
|
: enabled === false
|
|
923
|
-
? h("span", { className: "aapr-state-off" }, "○
|
|
1147
|
+
? h("span", { className: "aapr-state-off" }, "○ 自动审批/自动审查未开启")
|
|
924
1148
|
: null,
|
|
925
1149
|
h(ui.Button, { variant: "ghost", size: "sm", onClick: () => setTick(tick + 1) }, "刷新"),
|
|
926
1150
|
note !== "" ? h("span", { className: "aapr-muted" }, note) : null,
|
|
@@ -957,7 +1181,12 @@ window.__ModuleLoader__.load({
|
|
|
957
1181
|
"tr",
|
|
958
1182
|
{ key: String(r.at) + "-" + String(i) },
|
|
959
1183
|
h("td", null, fmtTime(r.at)),
|
|
960
|
-
h(
|
|
1184
|
+
h(
|
|
1185
|
+
"td",
|
|
1186
|
+
null,
|
|
1187
|
+
String(r.toolName),
|
|
1188
|
+
r.mode === "review" ? h("span", { className: "aapr-mode-review" }, "逐调用") : null,
|
|
1189
|
+
),
|
|
961
1190
|
h("td", { className: r.outcome === "allowed-once" ? "aapr-ok" : "aapr-no" }, OUTCOME_LABEL[r.outcome] || String(r.outcome)),
|
|
962
1191
|
h("td", null, RISK_LABEL[r.riskLevel] || String(r.riskLevel || "-")),
|
|
963
1192
|
h("td", null, String(r.model)),
|
package/cordis.patch.yml
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# dsh bundle patch: inserts this plugin into a profile's layer stack and
|
|
2
|
-
# registers the
|
|
2
|
+
# registers the 自动审批 / 自动审查 presets in the permission menu.
|
|
3
3
|
- insert:
|
|
4
4
|
- id: agent-approval
|
|
5
5
|
name: '@duke-dsh-plugins/dsh-agent-approval'
|
|
6
6
|
|
|
7
|
-
# --- dsh-agent-approval: register the
|
|
7
|
+
# --- dsh-agent-approval: register the 自动审批 / 自动审查 presets in the permission menu ---
|
|
8
8
|
# Restates the full presets table because a patch REPLACES the `permission`
|
|
9
9
|
# row's whole config; re-sync with dsh-base when upgrading DSH. Declaration
|
|
10
|
-
# order IS menu order:
|
|
11
|
-
# danger-full-access (above Full access).
|
|
10
|
+
# order IS menu order: 自动审批 and 自动审查 sit between workspace-write and
|
|
11
|
+
# danger-full-access (above Full access). The `agent-review` key avoids the
|
|
12
|
+
# reserved `auto`/`custom` preset names (0.1.7+ throws on those).
|
|
12
13
|
- id: permission
|
|
13
14
|
name: '@deepseek-ai/dsh-permission-presets'
|
|
14
15
|
config:
|
|
@@ -22,8 +23,13 @@
|
|
|
22
23
|
agent-approval:
|
|
23
24
|
sandbox: workspace-write
|
|
24
25
|
approval: ask
|
|
25
|
-
name:
|
|
26
|
-
description: workspace-write base;
|
|
26
|
+
name: 自动审批
|
|
27
|
+
description: workspace-write base; every sandbox escalation is judged automatically, risky ones are rejected.
|
|
28
|
+
agent-review:
|
|
29
|
+
sandbox: danger-full-access
|
|
30
|
+
approval: ask
|
|
31
|
+
name: 自动审查
|
|
32
|
+
description: Full access base; every tool call is reviewed by the Jev judge before execution, risky calls are rejected with no human fallback.
|
|
27
33
|
danger-full-access:
|
|
28
34
|
sandbox: danger-full-access
|
|
29
35
|
approval: never
|