@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 CHANGED
@@ -26,7 +26,8 @@
26
26
  | ⛔ 风险即拒绝 | 破坏性 / 不可逆 / 越界(含修改操作系统或其他应用数据)/ 理由与实际命令不符 → 直接 `reject`;仅"安全、可逆、与任务相符、理由诚实"才 `approve`——项目自身的安装/部署脚本写其文档指定路径属任务所需 |
27
27
  | 🔒 Fail-closed | 审批 Agent 启动失败、超时(可配 30s–600s)、结果不合法 → 一律按拒绝处理,绝不静默放行 |
28
28
  | ⚙️ 审批模型可配置 | 设置页选择 Provider + Model,不选则固定用 **Harness 默认模型**(不跟随请求会话,口径稳定);选择与超时**持久保存**,重启不丢 |
29
- | 📋 审计记录 | 设置页查看最近审批:结论 / 风险等级 / 模型 / 耗时 / 理由;悬停看完整理由与**精确工具参数**;审批 Agent 的会话 id 可回溯完整推理;记录**本地持久化**(重启保留最近 200 条) |
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
- └─ spawn 审批 Agent(独立会话 · 零工具 · 结构化裁决 · 不会递归审批)
42
+ ├─ (默认)spawn 审批 Agent(独立会话 · 零工具 · 结构化裁决 · 不会递归审批)
43
+ └─ (或)直连 TypeSafe Jev 决策模型(state + 类型化问题 → 概率化裁决 · 亚秒级)
42
44
  ├─ approve → allowed-once(该次放行)
43
45
  ├─ reject → rejected(风险操作,最终拒绝)
44
- └─ 超时/故障/取消 → fail-closed(按拒绝处理)
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.2.0/dsh-agent-approval-1.2.0.tgz
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. **审计**:设置 → **Agent 审批** → 审批记录;悬停"审批理由"看完整理由与工具参数。
80
- 4. **配置**:同页设置审批模型(不选则用 Harness 默认模型)与审批超时。
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. A "Agent 审批" page in the Settings panel (`settings.section`):
7
- * - approval model picker (provider + model, or the harness default),
8
- * - judge timeout setting (fail-closed),
9
- * - the list of sessions with the mode enabled (session-list title +
10
- * workspace, so each chip is recognizable),
11
- * - the latest approval audit records (verdict, risk, model, duration,
12
- * rationale; hover for the full rationale + tool arguments).
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/clearRecords",
306
+ id: "dsh-agent-approval#agentApproval/sessionRecords",
281
307
  service: "agentApproval",
282
308
  namespace: "agentApproval",
283
- method: "clearRecords",
309
+ method: "sessionRecords",
284
310
  invocation: { kind: "direct" },
285
- parameters: [],
286
- result: result("dsh-agent-approval#AgentApprovalClearRecordsResult"),
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 = [{ provider: "", id: "", name: "默认(Harness 默认模型)" }].concat(
529
- dir && dir.models ? dir.models.filter((m) => m.provider === provider) : [],
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
- dir && dir.defaultSelection
533
- ? "未配置时使用 Harness 默认模型;当前默认路由:" +
534
- dir.defaultSelection.provider +
535
- " / " +
536
- dir.defaultSelection.model
537
- : "未配置时使用 Harness 默认模型路由";
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-card" },
709
- h("h3", null, "审批记录(最近 50 条,本地持久化保留 200 条,重启不丢)"),
909
+ { className: "aapr-view-inner" },
710
910
  h(
711
911
  "div",
712
- { className: "aapr-row" },
713
- h(ui.Button, { variant: "ghost", size: "sm", onClick: refresh }, "刷新"),
714
- h(ui.Button, { variant: "ghost", size: "sm", onClick: clearRecords }, "清空记录"),
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-wrap" },
720
- h(
721
- "table",
722
- { className: "aapr-table" },
723
- h(
724
- "thead",
725
- null,
726
- h(
727
- "tr",
728
- null,
729
- ["时间", "会话", "工具", "结果", "风险", "模型", "耗时", "审批理由", "规则"].map((t) => h("th", { key: t }, t)),
730
- ),
731
- ),
732
- h(
733
- "tbody",
734
- null,
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
- { key: String(i) + "-" + String(r.at) },
740
- h("td", null, fmtTime(r.at)),
741
- h("td", null, String(r.sessionId)),
742
- h("td", null, String(r.toolName)),
743
- h("td", { className: r.outcome === "allowed-once" ? "aapr-ok" : "aapr-no" }, OUTCOME_LABEL[r.outcome] || String(r.outcome)),
744
- h("td", null, RISK_LABEL[r.riskLevel] || String(r.riskLevel || "-")),
745
- h("td", null, String(r.model)),
746
- h("td", null, String(r.durationMs) + "ms"),
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
- "td",
760
- null,
761
- r.outcome === "allowed-once" && r.args && r.model !== "rule"
762
- ? h(
763
- "button",
764
- {
765
- className: "aapr-whitelist",
766
- onClick: () => whitelistRecord(r),
767
- title: "把该操作存为放行规则(工具 + 参数子串):今后直接放行,不再经过审批模型",
768
- },
769
- "加白",
770
- )
771
- : null,
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
- : h("tr", null, h("td", { colSpan: 9 }, h("span", { className: "aapr-muted" }, "暂无记录"))),
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;