@duke-dsh-plugins/dsh-agent-approval 1.4.2 → 1.6.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 +9 -7
- package/client.js +324 -106
- package/index.js +544 -81
- package/package.json +2 -2
- package/typert.host.js +121 -19
package/README.md
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
| ⛔ 风险即拒绝 | 破坏性 / 不可逆 / 越界(含修改操作系统或其他应用数据)/ 理由与实际命令不符 → 直接 `reject`;仅"安全、可逆、与任务相符、理由诚实"才 `approve`——项目自身的安装/部署脚本写其文档指定路径属任务所需 |
|
|
27
27
|
| 🔒 Fail-closed | 审批 Agent 启动失败、超时(可配 30s–600s)、结果不合法 → 一律按拒绝处理,绝不静默放行 |
|
|
28
28
|
| ⚙️ 审批模型可配置 | 设置页选择 Provider + Model,不选则固定用 **Harness 默认模型**(不跟随请求会话,口径稳定);选择与超时**持久保存**,重启不丢 |
|
|
29
|
-
|
|
|
29
|
+
| ⚡ TypeSafe Jev 决策模型后端 | 审批模型可选 **TypeSafe Jev**(System One 结构化决策模型):审批时直连其 API,用类型化问题(Choice/Noul)毫秒级返回带校准概率的裁决;置信度低于阈值按 fail-closed 处理,审计理由由概率合成(需在设置页填 API Key,或设 `TYPESAFE_API_KEY`) |
|
|
30
|
+
| 📋 审计记录(随会话) | 会话窗口顶部的**「审批」标签页**(轨迹旁)查看本会话全部审批:结论 / 风险等级 / 模型 / 耗时 / 理由;悬停看完整理由与**精确工具参数**;审批 Agent 的会话 id 可回溯完整推理;已批准行可一键**「加白」**存为放行规则。记录存在**会话存储目录内的独立文件**——随会话恢复,删除会话即随之删除 |
|
|
30
31
|
| 🔁 可逆开关 | 权限菜单「Agent 审批」预设、`/agent-approval on\|off` 命令两条等价路径;关闭时**恢复开启前的权限旋钮** |
|
|
31
32
|
|
|
32
33
|
## 工作原理
|
|
@@ -38,11 +39,12 @@
|
|
|
38
39
|
工具请求提权(sandbox_permissions / 人工 ask)
|
|
39
40
|
└─ ctx.approval.request() → approval/request 瀑布
|
|
40
41
|
└─ 本插件 prepend 抢占(先于人工弹窗 answerer)
|
|
41
|
-
|
|
42
|
+
├─ (默认)spawn 审批 Agent(独立会话 · 零工具 · 结构化裁决 · 不会递归审批)
|
|
43
|
+
└─ (或)直连 TypeSafe Jev 决策模型(state + 类型化问题 → 概率化裁决 · 亚秒级)
|
|
42
44
|
├─ approve → allowed-once(该次放行)
|
|
43
45
|
├─ reject → rejected(风险操作,最终拒绝)
|
|
44
|
-
└─
|
|
45
|
-
└─
|
|
46
|
+
└─ 超时/故障/低置信/取消 → fail-closed(按拒绝处理)
|
|
47
|
+
└─ 记入审计(会话目录内的独立文件,「审批」标签页可见)
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
- 审批 Agent 只能看到:workspace 路径、**最近的用户消息**(任务上下文)、工具名、提权理由、**精确的工具参数 JSON**(按 `callId` 从会话日志回查)。裁决看"操作 vs 用户任务"的客观对齐,不依赖理由措辞。
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
dsh plugin --profile web add /path/to/dsh-agent-approval
|
|
61
63
|
|
|
62
64
|
# 正式发布:从 GitHub Release tarball 安装
|
|
63
|
-
dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-approval/releases/download/v1.
|
|
65
|
+
dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-approval/releases/download/v1.6.0/dsh-agent-approval-1.6.0.tgz
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
重启 DSH 后:设置面板出现 **Agent 审批** 页;`/permission` 菜单出现第四项 **Agent 审批**。
|
|
@@ -76,8 +78,8 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-app
|
|
|
76
78
|
|
|
77
79
|
1. **开启**:在 `/permission` 菜单选 **Agent 审批**,或输入 `/agent-approval on`。
|
|
78
80
|
2. **自动裁决**:之后该会话里的提权请求(例如命令被沙箱拒绝后带 `sandbox_permissions` 的重试)不再弹窗,由审批 Agent 在后台裁决并放行/拒绝。
|
|
79
|
-
3.
|
|
80
|
-
4.
|
|
81
|
+
3. **审计**:会话窗口顶部的**「审批」标签页**(轨迹旁)查看本会话的审批记录;悬停"审批理由"看完整理由与工具参数;已批准行可「加白」存为放行规则。记录存在会话存储目录内的独立文件,删除会话即随之删除;v1.4 的旧全局记录用 `node scripts/migrate-records.mjs` 一次性迁移(`--dry-run` 预览)。
|
|
82
|
+
4. **配置**:设置 → **Agent 审批** 设置审批模型(Harness 默认模型、指定 Provider/Model,或 **TypeSafe Jev**——选 Jev 后在下方卡片填 API Key 并按需调整 Endpoint / 模型版本 / 置信度阈值)、审批超时与放行/拒绝规则。
|
|
81
83
|
5. **关闭**:菜单切回其他预设,或 `/agent-approval off`,恢复开启前的沙箱模式与审批策略。
|
|
82
84
|
|
|
83
85
|
## 目录结构
|
package/client.js
CHANGED
|
@@ -3,13 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Rendered by the DSH web shell via `window.__ModuleLoader__.load`. Adds:
|
|
5
5
|
*
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* 1. An「审批」tab in the conversation window's view ring
|
|
7
|
+
* (`conversation.view`, right next to 轨迹): the per-session approval
|
|
8
|
+
* audit trail. The Host folds the records out of a sidecar file inside
|
|
9
|
+
* the session's OWN persistence directory
|
|
10
|
+
* (`<sessionDir>/agent-approval.jsonl`), so the audit follows the
|
|
11
|
+
* session — restored with it after a restart, gone when the session is
|
|
12
|
+
* deleted. Rows offer the one-click「加白」rule shortcut.
|
|
13
|
+
*
|
|
14
|
+
* 2. A "Agent 审批" page in the Settings panel (`settings.section`):
|
|
15
|
+
* approval model picker (provider + model, the harness default, or the
|
|
16
|
+
* TypeSafe Jev direct HTTP backend with its API key / endpoint /
|
|
17
|
+
* confidence-gate settings), judge timeout setting (fail-closed), the
|
|
18
|
+
* list of sessions with the mode enabled (session-list title +
|
|
19
|
+
* workspace), and the allow/deny rule table.
|
|
13
20
|
*
|
|
14
21
|
* Session-level on/off lives in the /permission menu (the "Agent 审批"
|
|
15
22
|
* preset, registered by the package's cordis.patch.yml bundle patch) and the
|
|
@@ -57,6 +64,16 @@ window.__ModuleLoader__.load({
|
|
|
57
64
|
.aapr-whitelist{color:var(--dsw-alias-label-secondary)}
|
|
58
65
|
.aapr-whitelist:hover{color:var(--dsw-alias-label-primary)}
|
|
59
66
|
|
|
67
|
+
/* Conversation-window「审批」tab: fills the view area below the tab strip
|
|
68
|
+
with the same token family as the Settings cards, so the audit ledger
|
|
69
|
+
reads native beside 轨迹. */
|
|
70
|
+
.aapr-view{height:100%;overflow:auto;padding:12px 16px 16px;color:var(--dsw-alias-label-primary);font-size:13px}
|
|
71
|
+
.aapr-view-inner{display:flex;flex-direction:column;gap:10px;max-width:980px;margin:0 auto}
|
|
72
|
+
.aapr-view-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
73
|
+
.aapr-view-title{font-size:13px;font-weight:600;margin:0}
|
|
74
|
+
.aapr-state-on{color:var(--dsw-alias-state-success-primary)}
|
|
75
|
+
.aapr-state-off{color:var(--dsw-alias-label-secondary)}
|
|
76
|
+
|
|
60
77
|
/* Settings nav icon: DSH 0.1.x settings.section only projects id/order/
|
|
61
78
|
label, and the settings shell paints a generic gear for every external
|
|
62
79
|
section (client-ui-settings-general's navIcon()). registerSettingsNavIcon
|
|
@@ -240,6 +257,15 @@ window.__ModuleLoader__.load({
|
|
|
240
257
|
parameters: param("dsh-agent-approval#AgentApprovalSetModelRequest"),
|
|
241
258
|
result: result("dsh-agent-approval#AgentApprovalSetModelResult"),
|
|
242
259
|
},
|
|
260
|
+
{
|
|
261
|
+
id: "dsh-agent-approval#agentApproval/setJevConfig",
|
|
262
|
+
service: "agentApproval",
|
|
263
|
+
namespace: "agentApproval",
|
|
264
|
+
method: "setJevConfig",
|
|
265
|
+
invocation: { kind: "direct" },
|
|
266
|
+
parameters: param("dsh-agent-approval#AgentApprovalSetJevRequest"),
|
|
267
|
+
result: result("dsh-agent-approval#AgentApprovalSetJevResult"),
|
|
268
|
+
},
|
|
243
269
|
{
|
|
244
270
|
id: "dsh-agent-approval#agentApproval/setApprovalTimeout",
|
|
245
271
|
service: "agentApproval",
|
|
@@ -277,13 +303,13 @@ window.__ModuleLoader__.load({
|
|
|
277
303
|
result: result("dsh-agent-approval#AgentApprovalRulesResult"),
|
|
278
304
|
},
|
|
279
305
|
{
|
|
280
|
-
id: "dsh-agent-approval#agentApproval/
|
|
306
|
+
id: "dsh-agent-approval#agentApproval/sessionRecords",
|
|
281
307
|
service: "agentApproval",
|
|
282
308
|
namespace: "agentApproval",
|
|
283
|
-
method: "
|
|
309
|
+
method: "sessionRecords",
|
|
284
310
|
invocation: { kind: "direct" },
|
|
285
|
-
parameters:
|
|
286
|
-
result: result("dsh-agent-approval#
|
|
311
|
+
parameters: param("dsh-agent-approval#AgentApprovalSessionRecordsRequest"),
|
|
312
|
+
result: result("dsh-agent-approval#AgentApprovalSessionRecordsResult"),
|
|
287
313
|
},
|
|
288
314
|
{
|
|
289
315
|
id: "dsh-agent-approval#agentApproval/directory",
|
|
@@ -397,6 +423,16 @@ window.__ModuleLoader__.load({
|
|
|
397
423
|
const setModel = modelSlot[1];
|
|
398
424
|
const timeoutSlot = React.useState("");
|
|
399
425
|
const setTimeoutDraft = timeoutSlot[1];
|
|
426
|
+
// Jev backend drafts (edited in the Jev card shown when the judge
|
|
427
|
+
// provider is the synthetic "typesafe" entry).
|
|
428
|
+
const jevKeySlot = React.useState("");
|
|
429
|
+
const setJevKey = jevKeySlot[1];
|
|
430
|
+
const jevEndpointSlot = React.useState("");
|
|
431
|
+
const setJevEndpoint = jevEndpointSlot[1];
|
|
432
|
+
const jevModelSlot = React.useState("");
|
|
433
|
+
const setJevModel = jevModelSlot[1];
|
|
434
|
+
const jevConfSlot = React.useState("0.5");
|
|
435
|
+
const setJevConf = jevConfSlot[1];
|
|
400
436
|
const noteSlot = React.useState("");
|
|
401
437
|
const note = noteSlot[0];
|
|
402
438
|
const setNote = noteSlot[1];
|
|
@@ -415,6 +451,11 @@ window.__ModuleLoader__.load({
|
|
|
415
451
|
setProvider(s.model.provider);
|
|
416
452
|
setModel(s.model.model);
|
|
417
453
|
setTimeoutDraft(String(s.timeoutMs));
|
|
454
|
+
// A not-yet-restarted old host sends no `jev` field — keep drafts.
|
|
455
|
+
setJevKey(s.jev && typeof s.jev.apiKey === "string" ? s.jev.apiKey : "");
|
|
456
|
+
setJevEndpoint(s.jev && typeof s.jev.endpoint === "string" ? s.jev.endpoint : "");
|
|
457
|
+
setJevModel(s.jev && typeof s.jev.model === "string" ? s.jev.model : "");
|
|
458
|
+
setJevConf(s.jev && typeof s.jev.confidence === "number" ? String(s.jev.confidence) : "0.5");
|
|
418
459
|
})
|
|
419
460
|
.catch((e) => setNote("无法读取状态:" + (e && e.message ? e.message : String(e))));
|
|
420
461
|
};
|
|
@@ -442,6 +483,28 @@ window.__ModuleLoader__.load({
|
|
|
442
483
|
})
|
|
443
484
|
.catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
|
|
444
485
|
};
|
|
486
|
+
const jevKey = jevKeySlot[0];
|
|
487
|
+
const jevEndpoint = jevEndpointSlot[0];
|
|
488
|
+
const jevModel = jevModelSlot[0];
|
|
489
|
+
const jevConf = jevConfSlot[0];
|
|
490
|
+
const saveJev = () => {
|
|
491
|
+
if (typeof remote.setJevConfig !== "function") {
|
|
492
|
+
setNote("Host 半未更新(缺少 setJevConfig):请重装本插件并重启 DSH。");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const conf = Number(jevConf);
|
|
496
|
+
if (!Number.isFinite(conf) || conf <= 0 || conf >= 1) {
|
|
497
|
+
setNote("置信度阈值必须是 0–1 之间的小数(如 0.5)");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
remote
|
|
501
|
+
.setJevConfig({ apiKey: jevKey, endpoint: jevEndpoint, model: jevModel, confidence: conf })
|
|
502
|
+
.then(() => {
|
|
503
|
+
refresh();
|
|
504
|
+
setNote("Jev 配置已保存");
|
|
505
|
+
})
|
|
506
|
+
.catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
|
|
507
|
+
};
|
|
445
508
|
const saveTimeout = () => {
|
|
446
509
|
const parsed = Number(timeoutDraft);
|
|
447
510
|
if (!Number.isFinite(parsed)) {
|
|
@@ -463,12 +526,6 @@ window.__ModuleLoader__.load({
|
|
|
463
526
|
.then(refresh)
|
|
464
527
|
.catch(() => {});
|
|
465
528
|
};
|
|
466
|
-
const clearRecords = () => {
|
|
467
|
-
remote
|
|
468
|
-
.clearRecords()
|
|
469
|
-
.then(refresh)
|
|
470
|
-
.catch(() => {});
|
|
471
|
-
};
|
|
472
529
|
|
|
473
530
|
const ruleEffect = ruleEffectSlot[0];
|
|
474
531
|
const ruleTool = ruleToolSlot[0];
|
|
@@ -509,32 +566,31 @@ window.__ModuleLoader__.load({
|
|
|
509
566
|
})
|
|
510
567
|
.catch(() => {});
|
|
511
568
|
};
|
|
512
|
-
// One-click whitelist from an audit row: the recorded args are a
|
|
513
|
-
// PREFIX of the real arguments JSON (the Host truncates at 2000
|
|
514
|
-
// chars), so stripping the truncation marker keeps a valid substring.
|
|
515
|
-
const whitelistRecord = (r) => {
|
|
516
|
-
const args = String(r.args || "").replace(/…\[truncated\]$/, "");
|
|
517
|
-
addRule({
|
|
518
|
-
effect: "allow",
|
|
519
|
-
tool: String(r.toolName),
|
|
520
|
-
match: args,
|
|
521
|
-
note: "来自审计 " + fmtTime(r.at),
|
|
522
|
-
});
|
|
523
|
-
};
|
|
524
569
|
|
|
570
|
+
// The synthetic "typesafe" provider routes judging through the
|
|
571
|
+
// TypeSafe Jev HTTP API — it is not part of the harness directory.
|
|
525
572
|
const providerOptions = [{ id: "", name: "默认(Harness 默认模型)" }].concat(
|
|
573
|
+
[{ id: "typesafe", name: "TypeSafe Jev(决策模型·直连 API)" }],
|
|
526
574
|
dir ? dir.providers : [],
|
|
527
575
|
);
|
|
528
|
-
const modelOptions =
|
|
529
|
-
|
|
530
|
-
|
|
576
|
+
const modelOptions =
|
|
577
|
+
provider === "typesafe"
|
|
578
|
+
? [
|
|
579
|
+
{ provider: "typesafe", id: "jev-latest", name: "jev-latest(跟随最新版本)" },
|
|
580
|
+
{ provider: "typesafe", id: "jev-1.13.0", name: "jev-1.13.0(锁定版本)" },
|
|
581
|
+
]
|
|
582
|
+
: [{ provider: "", id: "", name: "默认(Harness 默认模型)" }].concat(
|
|
583
|
+
dir && dir.models ? dir.models.filter((m) => m.provider === provider) : [],
|
|
584
|
+
);
|
|
531
585
|
const defaultHint =
|
|
532
|
-
|
|
533
|
-
? "
|
|
534
|
-
|
|
535
|
-
"
|
|
536
|
-
|
|
537
|
-
|
|
586
|
+
provider === "typesafe"
|
|
587
|
+
? "当前审批判定直连 TypeSafe Jev API,不经过 Harness 模型路由;下方 Jev 配置在该模式下生效。"
|
|
588
|
+
: dir && dir.defaultSelection
|
|
589
|
+
? "未配置时使用 Harness 默认模型;当前默认路由:" +
|
|
590
|
+
dir.defaultSelection.provider +
|
|
591
|
+
" / " +
|
|
592
|
+
dir.defaultSelection.model
|
|
593
|
+
: "未配置时使用 Harness 默认模型路由";
|
|
538
594
|
|
|
539
595
|
return h(
|
|
540
596
|
"div",
|
|
@@ -548,8 +604,9 @@ window.__ModuleLoader__.load({
|
|
|
548
604
|
{ className: "aapr-muted" },
|
|
549
605
|
"一种新的权限模式:以 workspace-write 为基线沙箱;当工具请求提权(更宽的沙箱)时,由一个独立的审批 Agent 评估风险——安全、可逆、与任务相符的操作自动批准,破坏性、不可逆、越界或理由不符的操作直接拒绝。",
|
|
550
606
|
h("br", null),
|
|
551
|
-
"在输入框 /permission 菜单选择「Agent 审批」预设,或执行命令 /agent-approval on|off
|
|
607
|
+
"在输入框 /permission 菜单选择「Agent 审批」预设,或执行命令 /agent-approval on|off 为会话开启;每个会话的审批审计记录在该会话窗口顶部的「审批」标签页(轨迹旁),随会话保存。",
|
|
552
608
|
),
|
|
609
|
+
note !== "" ? h("div", { className: "aapr-muted" }, note) : null,
|
|
553
610
|
),
|
|
554
611
|
h(
|
|
555
612
|
"div",
|
|
@@ -569,7 +626,7 @@ window.__ModuleLoader__.load({
|
|
|
569
626
|
value: provider,
|
|
570
627
|
onChange: (e) => {
|
|
571
628
|
setProvider(e.target.value);
|
|
572
|
-
setModel("");
|
|
629
|
+
setModel(e.target.value === "typesafe" ? "jev-latest" : "");
|
|
573
630
|
},
|
|
574
631
|
},
|
|
575
632
|
providerOptions.map((p) =>
|
|
@@ -587,7 +644,7 @@ window.__ModuleLoader__.load({
|
|
|
587
644
|
className: "aapr-select",
|
|
588
645
|
value: model,
|
|
589
646
|
onChange: (e) => setModel(e.target.value),
|
|
590
|
-
disabled: dir === null,
|
|
647
|
+
disabled: dir === null && provider !== "typesafe",
|
|
591
648
|
},
|
|
592
649
|
modelOptions.map((m) =>
|
|
593
650
|
h("option", { key: m.provider + "/" + m.id, value: m.id }, m.id === "" ? m.name : m.name + "(" + m.id + ")"),
|
|
@@ -598,6 +655,75 @@ window.__ModuleLoader__.load({
|
|
|
598
655
|
),
|
|
599
656
|
h("div", { className: "aapr-muted" }, defaultHint),
|
|
600
657
|
),
|
|
658
|
+
provider === "typesafe"
|
|
659
|
+
? h(
|
|
660
|
+
"div",
|
|
661
|
+
{ className: "aapr-card" },
|
|
662
|
+
h("h3", null, "TypeSafe Jev 配置"),
|
|
663
|
+
h(
|
|
664
|
+
"div",
|
|
665
|
+
{ className: "aapr-muted" },
|
|
666
|
+
"Jev 是结构化决策模型(System One):审批时直连 TypeSafe API,不创建审批子会话,毫秒级返回带校准概率的裁决。审计「理由」由概率分布合成(Jev 本身不生成文字);置信度低于阈值时按 fail-closed 处理(记 unavailable,不批准也不记拒绝)。对中文任务上下文的准确率略低于英语。API Key 明文保存在本机 config.json;留空时使用环境变量 TYPESAFE_API_KEY。",
|
|
667
|
+
),
|
|
668
|
+
h(
|
|
669
|
+
"div",
|
|
670
|
+
{ className: "aapr-row" },
|
|
671
|
+
h(
|
|
672
|
+
"label",
|
|
673
|
+
null,
|
|
674
|
+
"API Key:",
|
|
675
|
+
h("input", {
|
|
676
|
+
className: "aapr-input",
|
|
677
|
+
type: "password",
|
|
678
|
+
placeholder: "TYPESAFE_API_KEY",
|
|
679
|
+
value: jevKey,
|
|
680
|
+
onChange: (e) => setJevKey(e.target.value),
|
|
681
|
+
}),
|
|
682
|
+
),
|
|
683
|
+
h(
|
|
684
|
+
"label",
|
|
685
|
+
null,
|
|
686
|
+
"模型:",
|
|
687
|
+
h("input", {
|
|
688
|
+
className: "aapr-input",
|
|
689
|
+
placeholder: "jev-latest",
|
|
690
|
+
value: jevModel,
|
|
691
|
+
onChange: (e) => setJevModel(e.target.value),
|
|
692
|
+
}),
|
|
693
|
+
),
|
|
694
|
+
),
|
|
695
|
+
h(
|
|
696
|
+
"div",
|
|
697
|
+
{ className: "aapr-row" },
|
|
698
|
+
h(
|
|
699
|
+
"label",
|
|
700
|
+
null,
|
|
701
|
+
"Endpoint:",
|
|
702
|
+
h("input", {
|
|
703
|
+
className: "aapr-input aapr-input-wide",
|
|
704
|
+
placeholder: "https://api.typesafe.ai/v1/systemone",
|
|
705
|
+
value: jevEndpoint,
|
|
706
|
+
onChange: (e) => setJevEndpoint(e.target.value),
|
|
707
|
+
}),
|
|
708
|
+
),
|
|
709
|
+
h(
|
|
710
|
+
"label",
|
|
711
|
+
null,
|
|
712
|
+
"置信度阈值:",
|
|
713
|
+
h("input", {
|
|
714
|
+
className: "aapr-input",
|
|
715
|
+
type: "number",
|
|
716
|
+
step: "0.05",
|
|
717
|
+
min: "0.01",
|
|
718
|
+
max: "0.99",
|
|
719
|
+
value: jevConf,
|
|
720
|
+
onChange: (e) => setJevConf(e.target.value),
|
|
721
|
+
}),
|
|
722
|
+
),
|
|
723
|
+
h(ui.Button, { variant: "primary", size: "sm", onClick: saveJev }, "保存"),
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
: null,
|
|
601
727
|
h(
|
|
602
728
|
"div",
|
|
603
729
|
{ className: "aapr-card" },
|
|
@@ -703,84 +829,164 @@ window.__ModuleLoader__.load({
|
|
|
703
829
|
h(ui.Button, { variant: "primary", size: "sm", onClick: submitRule }, "添加"),
|
|
704
830
|
),
|
|
705
831
|
),
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* The conversation-window「审批」tab (`conversation.view`, next to
|
|
837
|
+
* 轨迹): this session's audit trail, folded by the Host out of the
|
|
838
|
+
* session's own sidecar storage. Rendered only while the tab is
|
|
839
|
+
* active, so the 10s poll costs nothing otherwise. Session-scoped
|
|
840
|
+
* slot: the runtime hands us the standard `useSession` hook; the
|
|
841
|
+
* snapshot's `sessionId` leaf is the only field we read.
|
|
842
|
+
*/
|
|
843
|
+
function ApprovalAuditView(props) {
|
|
844
|
+
const useSession = props.useSession;
|
|
845
|
+
const session = useSession(function (s) { return s; });
|
|
846
|
+
const sessionId = session && session.sessionId ? String(session.sessionId) : "";
|
|
847
|
+
|
|
848
|
+
const stateSlot = React.useState(null); // { records, enabled } | null
|
|
849
|
+
const state = stateSlot[0];
|
|
850
|
+
const setState = stateSlot[1];
|
|
851
|
+
const noteSlot = React.useState("");
|
|
852
|
+
const note = noteSlot[0];
|
|
853
|
+
const setNote = noteSlot[1];
|
|
854
|
+
const tickSlot = React.useState(0); // manual-refresh counter
|
|
855
|
+
const tick = tickSlot[0];
|
|
856
|
+
const setTick = tickSlot[1];
|
|
857
|
+
|
|
858
|
+
React.useEffect(() => {
|
|
859
|
+
if (sessionId === "") return undefined;
|
|
860
|
+
let alive = true;
|
|
861
|
+
const load = () => {
|
|
862
|
+
if (typeof remote.sessionRecords !== "function") {
|
|
863
|
+
if (alive) setNote("Host 半未更新(缺少 sessionRecords):请重装本插件并重启 DSH。");
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
remote
|
|
867
|
+
.sessionRecords({ sessionId: sessionId })
|
|
868
|
+
.then((res) => {
|
|
869
|
+
if (alive) setState(pick(res));
|
|
870
|
+
})
|
|
871
|
+
.catch((e) => {
|
|
872
|
+
if (alive) setNote("无法读取审批记录:" + (e && e.message ? e.message : String(e)));
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
load();
|
|
876
|
+
const timer = setInterval(load, 10000);
|
|
877
|
+
return () => {
|
|
878
|
+
alive = false;
|
|
879
|
+
clearInterval(timer);
|
|
880
|
+
};
|
|
881
|
+
}, [sessionId, tick]);
|
|
882
|
+
|
|
883
|
+
// One-click whitelist from an audit row: the recorded args are a
|
|
884
|
+
// PREFIX of the real arguments JSON (the Host truncates at 2000
|
|
885
|
+
// chars), so stripping the truncation marker keeps a valid substring.
|
|
886
|
+
const whitelistRecord = (r) => {
|
|
887
|
+
const args = String(r.args || "").replace(/…\[truncated\]$/, "");
|
|
888
|
+
remote
|
|
889
|
+
.addRule({
|
|
890
|
+
effect: "allow",
|
|
891
|
+
tool: String(r.toolName),
|
|
892
|
+
match: args,
|
|
893
|
+
note: "来自审批审计 " + fmtTime(r.at),
|
|
894
|
+
})
|
|
895
|
+
.then(() => setNote("已加白:今后该操作直接放行,不再经过审批模型。"))
|
|
896
|
+
.catch((e) => setNote("加白失败:" + (e && e.message ? e.message : String(e))));
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const records = state !== null && Array.isArray(state.records) ? state.records : [];
|
|
900
|
+
// 倒序展示(最新在最上);Host 仍按时间正序返回,顺序属于视图层。
|
|
901
|
+
const ordered = records.slice().reverse();
|
|
902
|
+
const enabled = state !== null ? !!state.enabled : null;
|
|
903
|
+
|
|
904
|
+
return h(
|
|
905
|
+
"div",
|
|
906
|
+
{ className: "aapr-view" },
|
|
706
907
|
h(
|
|
707
908
|
"div",
|
|
708
|
-
{ className: "aapr-
|
|
709
|
-
h("h3", null, "审批记录(最近 50 条,本地持久化保留 200 条,重启不丢)"),
|
|
909
|
+
{ className: "aapr-view-inner" },
|
|
710
910
|
h(
|
|
711
911
|
"div",
|
|
712
|
-
{ className: "aapr-
|
|
713
|
-
h(
|
|
714
|
-
|
|
912
|
+
{ className: "aapr-view-head" },
|
|
913
|
+
h("h3", { className: "aapr-view-title" }, "审批审计(本会话)"),
|
|
914
|
+
enabled === true
|
|
915
|
+
? h("span", { className: "aapr-state-on" }, "● Agent 审批已开启")
|
|
916
|
+
: enabled === false
|
|
917
|
+
? h("span", { className: "aapr-state-off" }, "○ Agent 审批未开启")
|
|
918
|
+
: null,
|
|
919
|
+
h(ui.Button, { variant: "ghost", size: "sm", onClick: () => setTick(tick + 1) }, "刷新"),
|
|
715
920
|
note !== "" ? h("span", { className: "aapr-muted" }, note) : null,
|
|
716
921
|
),
|
|
717
922
|
h(
|
|
718
923
|
"div",
|
|
719
|
-
{ className: "aapr-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
"
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
state !== null && state.records
|
|
736
|
-
? state.records.map((r, i) =>
|
|
924
|
+
{ className: "aapr-muted" },
|
|
925
|
+
"审批结论随会话保存(会话目录内的独立记录文件):随会话恢复,删除会话即随之删除,最新记录在最上。悬停「审批理由」可查看完整理由与工具参数;「审批会话」前缀可在会话列表中找到审批 Agent 的完整会话记录。",
|
|
926
|
+
),
|
|
927
|
+
sessionId === ""
|
|
928
|
+
? h("div", { className: "aapr-muted" }, "当前没有活动会话。")
|
|
929
|
+
: records.length === 0
|
|
930
|
+
? h("div", { className: "aapr-muted" }, "本会话暂无审批记录。")
|
|
931
|
+
: h(
|
|
932
|
+
"div",
|
|
933
|
+
{ className: "aapr-wrap" },
|
|
934
|
+
h(
|
|
935
|
+
"table",
|
|
936
|
+
{ className: "aapr-table" },
|
|
937
|
+
h(
|
|
938
|
+
"thead",
|
|
939
|
+
null,
|
|
737
940
|
h(
|
|
738
941
|
"tr",
|
|
739
|
-
|
|
740
|
-
h("
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
h(
|
|
748
|
-
"td",
|
|
749
|
-
{
|
|
750
|
-
className: "aapr-cell",
|
|
751
|
-
title:
|
|
752
|
-
(r.rationale || "") +
|
|
753
|
-
(r.args ? "\n\n工具参数:" + r.args : "") +
|
|
754
|
-
(r.childSessionId ? "\n\n审批会话:" + r.childSessionId : ""),
|
|
755
|
-
},
|
|
756
|
-
truncText(r.rationale, 110),
|
|
757
|
-
),
|
|
942
|
+
null,
|
|
943
|
+
["时间", "工具", "结果", "风险", "模型", "耗时", "审批理由", "规则"].map((t) => h("th", { key: t }, t)),
|
|
944
|
+
),
|
|
945
|
+
),
|
|
946
|
+
h(
|
|
947
|
+
"tbody",
|
|
948
|
+
null,
|
|
949
|
+
ordered.map((r, i) =>
|
|
758
950
|
h(
|
|
759
|
-
"
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
951
|
+
"tr",
|
|
952
|
+
{ key: String(r.at) + "-" + String(i) },
|
|
953
|
+
h("td", null, fmtTime(r.at)),
|
|
954
|
+
h("td", null, String(r.toolName)),
|
|
955
|
+
h("td", { className: r.outcome === "allowed-once" ? "aapr-ok" : "aapr-no" }, OUTCOME_LABEL[r.outcome] || String(r.outcome)),
|
|
956
|
+
h("td", null, RISK_LABEL[r.riskLevel] || String(r.riskLevel || "-")),
|
|
957
|
+
h("td", null, String(r.model)),
|
|
958
|
+
h("td", null, String(r.durationMs) + "ms"),
|
|
959
|
+
h(
|
|
960
|
+
"td",
|
|
961
|
+
{
|
|
962
|
+
className: "aapr-cell",
|
|
963
|
+
title:
|
|
964
|
+
(r.rationale || "") +
|
|
965
|
+
(r.args ? "\n\n工具参数:" + r.args : "") +
|
|
966
|
+
(r.childSessionId ? "\n\n审批会话:" + r.childSessionId : ""),
|
|
967
|
+
},
|
|
968
|
+
truncText(r.rationale, 110),
|
|
969
|
+
),
|
|
970
|
+
h(
|
|
971
|
+
"td",
|
|
972
|
+
null,
|
|
973
|
+
r.outcome === "allowed-once" && r.args && r.model !== "rule"
|
|
974
|
+
? h(
|
|
975
|
+
"button",
|
|
976
|
+
{
|
|
977
|
+
className: "aapr-whitelist",
|
|
978
|
+
onClick: () => whitelistRecord(r),
|
|
979
|
+
title: "把该操作存为放行规则(工具 + 参数子串):今后直接放行,不再经过审批模型",
|
|
980
|
+
},
|
|
981
|
+
"加白",
|
|
982
|
+
)
|
|
983
|
+
: null,
|
|
984
|
+
),
|
|
772
985
|
),
|
|
773
986
|
),
|
|
774
|
-
)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
),
|
|
778
|
-
),
|
|
779
|
-
h(
|
|
780
|
-
"div",
|
|
781
|
-
{ className: "aapr-muted" },
|
|
782
|
-
"悬停“审批理由”可查看完整理由与工具参数;“审批会话”前缀可在会话列表中找到审批 Agent 的完整会话记录。「加白」把一条已批准的操作存为放行规则(模型列显示 rule/trust 的行分别来自规则命中与会话内信任缓存)。",
|
|
783
|
-
),
|
|
987
|
+
),
|
|
988
|
+
),
|
|
989
|
+
),
|
|
784
990
|
),
|
|
785
991
|
);
|
|
786
992
|
}
|
|
@@ -792,6 +998,18 @@ window.__ModuleLoader__.load({
|
|
|
792
998
|
AgentApprovalSection,
|
|
793
999
|
),
|
|
794
1000
|
);
|
|
1001
|
+
|
|
1002
|
+
// Conversation entry: the「审批」audit tab in the view ring, right
|
|
1003
|
+
// beside 轨迹 (chat order 0, trajectory order 10, audit order 11).
|
|
1004
|
+
// Session-scoped: renders per conversation with the session's own
|
|
1005
|
+
// records; registration rides the slot ledger so plugin unload
|
|
1006
|
+
// removes the tab.
|
|
1007
|
+
ctx.slots.inject("conversation.view", () =>
|
|
1008
|
+
ctx.slots.register(
|
|
1009
|
+
{ name: "conversation.view", id: "agent-approval-audit", order: 11, label: () => "审批" },
|
|
1010
|
+
ApprovalAuditView,
|
|
1011
|
+
),
|
|
1012
|
+
);
|
|
795
1013
|
}
|
|
796
1014
|
|
|
797
1015
|
exports.apply = apply;
|