@duke-dsh-plugins/dsh-agent-approval 1.5.0 → 1.7.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,6 +26,7 @@
26
26
  | ⛔ 风险即拒绝 | 破坏性 / 不可逆 / 越界(含修改操作系统或其他应用数据)/ 理由与实际命令不符 → 直接 `reject`;仅"安全、可逆、与任务相符、理由诚实"才 `approve`——项目自身的安装/部署脚本写其文档指定路径属任务所需 |
27
27
  | 🔒 Fail-closed | 审批 Agent 启动失败、超时(可配 30s–600s)、结果不合法 → 一律按拒绝处理,绝不静默放行 |
28
28
  | ⚙️ 审批模型可配置 | 设置页选择 Provider + Model,不选则固定用 **Harness 默认模型**(不跟随请求会话,口径稳定);选择与超时**持久保存**,重启不丢 |
29
+ | ⚡ TypeSafe Jev 决策模型后端 | 审批模型可选 **TypeSafe Jev**(System One 结构化决策模型):审批时直连其 API,用类型化问题(Choice/Noul)毫秒级返回带校准概率的裁决;置信度低于阈值按 fail-closed 处理,审计理由由概率合成(需在设置页填 API Key,或设 `TYPESAFE_API_KEY`) |
29
30
  | 📋 审计记录(随会话) | 会话窗口顶部的**「审批」标签页**(轨迹旁)查看本会话全部审批:结论 / 风险等级 / 模型 / 耗时 / 理由;悬停看完整理由与**精确工具参数**;审批 Agent 的会话 id 可回溯完整推理;已批准行可一键**「加白」**存为放行规则。记录存在**会话存储目录内的独立文件**——随会话恢复,删除会话即随之删除 |
30
31
  | 🔁 可逆开关 | 权限菜单「Agent 审批」预设、`/agent-approval on\|off` 命令两条等价路径;关闭时**恢复开启前的权限旋钮** |
31
32
 
@@ -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.5.0/dsh-agent-approval-1.5.0.tgz
65
+ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-approval/releases/download/v1.7.0/dsh-agent-approval-1.7.0.tgz
64
66
  ```
65
67
 
66
68
  重启 DSH 后:设置面板出现 **Agent 审批** 页;`/permission` 菜单出现第四项 **Agent 审批**。
@@ -77,7 +79,7 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-app
77
79
  1. **开启**:在 `/permission` 菜单选 **Agent 审批**,或输入 `/agent-approval on`。
78
80
  2. **自动裁决**:之后该会话里的提权请求(例如命令被沙箱拒绝后带 `sandbox_permissions` 的重试)不再弹窗,由审批 Agent 在后台裁决并放行/拒绝。
79
81
  3. **审计**:会话窗口顶部的**「审批」标签页**(轨迹旁)查看本会话的审批记录;悬停"审批理由"看完整理由与工具参数;已批准行可「加白」存为放行规则。记录存在会话存储目录内的独立文件,删除会话即随之删除;v1.4 的旧全局记录用 `node scripts/migrate-records.mjs` 一次性迁移(`--dry-run` 预览)。
80
- 4. **配置**:设置 → **Agent 审批** 设置审批模型(不选则用 Harness 默认模型)、审批超时与放行/拒绝规则。
82
+ 4. **配置**:设置 → **Agent 审批** 设置审批模型(Harness 默认模型、指定 Provider/Model,或 **TypeSafe Jev**——选 Jev 后在下方卡片填 API Key 并按需调整 Endpoint / 模型版本 / 置信度阈值)、审批超时与放行/拒绝规则。
81
83
  5. **关闭**:菜单切回其他预设,或 `/agent-approval off`,恢复开启前的沙箱模式与审批策略。
82
84
 
83
85
  ## 目录结构
@@ -85,10 +87,11 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-agent-app
85
87
  ```
86
88
  dsh-agent-approval/
87
89
  ├── index.js # Host 半:AgentApprovalService(审批瀑布抢占 + spawn 审批 Agent + 审计)
88
- ├── client.js # Client 半:设置页「Agent 审批」+ 输入框开关 UI bundle
89
- ├── typert.host.js # Typert Host manifest(agentApproval 6 个方法的描述)
90
+ ├── client.js # Client 半:设置页「Agent 审批」+ 会话「审批」审计标签页 bundle
91
+ ├── typert.host.js # Typert Host manifest(agentApproval 9 个 Remote 方法的描述)
90
92
  ├── cordis.patch.yml # dsh bundle patch(挂载行 + permission 预设表覆盖)
91
93
  ├── scripts/patch-glyph.mjs # 可选:权限菜单图标补丁(标准安装不自动执行)
94
+ ├── scripts/check-typert-manifest.mjs # npm run check 用:双代 Typert codec 契约冒烟
92
95
  ├── .github/workflows/ # GitHub Actions 发布
93
96
  ├── AGENTS.md # 面向 AI agent 的开发指南(含踩坑)
94
97
  └── LICENSE # MIT
@@ -97,11 +100,13 @@ dsh-agent-approval/
97
100
  ## 开发
98
101
 
99
102
  ```bash
100
- npm run check # node --check index.js client.js typert.host.js
103
+ npm run check # node --check 全部脚本 + 双代 Typert 契约冒烟(scripts/check-typert-manifest.mjs)
101
104
  dsh plugin --profile web add /path/to/dsh-agent-approval # 安装/重装到本机 DSH profile
102
105
  npm run patch:glyph # 可选:权限菜单图标
103
106
  ```
104
107
 
108
+ **兼容性**:支持 DSH 0.1.5-rc.3 ~ 0.1.7-rc.1。v1.7.0 起 Typert manifest / Client Remote 描述符的每个 codec 同时携带 `schema`(≤0.1.5 的 zod 契约)与 `create()` 工厂(0.1.7 的新契约),任一宿主代际都能注册;旧版本(≤1.6.0)在 0.1.7 上会被 typert-loader 以 "has no create() factory" 拒绝,Remote 全部失效。
109
+
105
110
  详见 [AGENTS.md](AGENTS.md)——记录了 DSH 正式插件(Host/Client/Typert 三件套)的完整机制、审批瀑布 prepend 抢占与结构化子代理裁决的踩坑。
106
111
 
107
112
  ## License
package/client.js CHANGED
@@ -12,10 +12,11 @@
12
12
  * deleted. Rows offer the one-click「加白」rule shortcut.
13
13
  *
14
14
  * 2. A "Agent 审批" page in the Settings panel (`settings.section`):
15
- * approval model picker (provider + model, or the harness default),
16
- * judge timeout setting (fail-closed), the list of sessions with the
17
- * mode enabled (session-list title + workspace), and the allow/deny
18
- * rule table.
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.
19
20
  *
20
21
  * Session-level on/off lives in the /permission menu (the "Agent 审批"
21
22
  * preset, registered by the package's cordis.patch.yml bundle patch) and the
@@ -91,7 +92,7 @@ window.__ModuleLoader__.load({
91
92
  draws the same shield + AI-star glyph patch-glyph.mjs used, as a
92
93
  currentColor mask so hover/selected/disabled colors all follow the shell.
93
94
  Scope guard: only menus that already render the official glyph set
94
- (sibling rows carry span._itemIcon_*) qualify — the composer /permission
95
+ (sibling rows carry the Menu primitive's itemIcon span) qualify — the composer /permission
95
96
  menu does; the settings PermissionRow dropdown (settings.general 权限 row,
96
97
  portaled to <body>) renders NO icons for any preset, so an icon there
97
98
  would be an uninvited extra and is deliberately left unmarked. */
@@ -155,17 +156,20 @@ window.__ModuleLoader__.load({
155
156
  // inside button[role=menuitem]; an icon-less row starts at the label.
156
157
  // Glyph-set guard: only mark our row in menus where the official
157
158
  // presets already render their permissionGlyphs (sibling rows carry
158
- // span[class*="_itemIcon_"] — the CSS-modules build keeps the source
159
- // class name as a substring). The composer /permission menu
160
- // qualifies; the settings PermissionRow dropdown (portaled to
161
- // <body>, no item icons for any preset) does NOT, so the glyph no
162
- // longer leaks into the settings page. (The selected-row checkmark
163
- // svg is class "_check_", not "_itemIcon_", so it cannot fake the
164
- // guard.)
159
+ // the Menu primitive's icon span). The class-name substring is
160
+ // "itemIcon" (v1.7.0, widened from "_itemIcon_"): CSS-modules compiles
161
+ // the source name differently across host generations
162
+ // (`_itemIcon_<hash>_` vs `<hash>_itemIcon`), and the Menu moved from
163
+ // dsh-client-ui-conversation to dsh-client-ui-permission-presets in
164
+ // 0.1.7-rc.1. The composer /permission menu qualifies; the settings
165
+ // PermissionRow dropdown (portaled to <body>, no item icons for any
166
+ // preset) does NOT, so the glyph no longer leaks into the settings
167
+ // page. (The selected-row checkmark svg is class "_check_", not
168
+ // "itemIcon", so it cannot fake the guard.)
165
169
  const menus = document.querySelectorAll('[role="menu"]');
166
170
  const glyphMenus = [];
167
171
  for (let i = 0; i < menus.length; i++) {
168
- if (menus[i].querySelector('span[class*="_itemIcon_"]') !== null) glyphMenus.push(menus[i]);
172
+ if (menus[i].querySelector('span[class*="itemIcon"]') !== null) glyphMenus.push(menus[i]);
169
173
  }
170
174
  const items = document.querySelectorAll('[role="menu"] button[role="menuitem"]');
171
175
  for (let i = 0; i < items.length; i++) {
@@ -219,22 +223,25 @@ window.__ModuleLoader__.load({
219
223
  // client assembly mounts only the official namespaces, so a plugin must
220
224
  // mount its own. Mirrors the invocations in typert.host.js (id,
221
225
  // service/namespace/method). zod is not requirable in the browser module
222
- // loader, so codecs use passthrough schemas — the runtime contract only
223
- // requires typeSymbol + schema.parse().
226
+ // loader, so codecs use passthrough schemas. The wire contract spans two
227
+ // host generations (v1.7.0): the client Remote registry validates
228
+ // `codec.schema.parse` on DSH ≤ 0.1.5-rc.3 but a `codec.create()` factory
229
+ // on 0.1.7-rc.1+ ("strict codec has no create() factory" kills the mount),
230
+ // so every codec carries BOTH fields over the same passthrough schema.
224
231
  const passthrough = () => ({ parse: (v) => v });
232
+ const strictCodec = (typeSymbol) => {
233
+ const schema = passthrough();
234
+ return { mode: "strict", typeSymbol, schema, create: () => schema };
235
+ };
225
236
  const param = (typeSymbol) => [
226
237
  {
227
238
  name: "request",
228
239
  wire: "request",
229
240
  source: "json",
230
- codec: { mode: "strict", typeSymbol, schema: passthrough() },
241
+ codec: strictCodec(typeSymbol),
231
242
  },
232
243
  ];
233
- const result = (typeSymbol) => ({
234
- mode: "strict",
235
- typeSymbol,
236
- schema: passthrough(),
237
- });
244
+ const result = (typeSymbol) => strictCodec(typeSymbol);
238
245
  const CLIENT_REMOTE = {
239
246
  package: "dsh-agent-approval",
240
247
  descriptors: [
@@ -256,6 +263,15 @@ window.__ModuleLoader__.load({
256
263
  parameters: param("dsh-agent-approval#AgentApprovalSetModelRequest"),
257
264
  result: result("dsh-agent-approval#AgentApprovalSetModelResult"),
258
265
  },
266
+ {
267
+ id: "dsh-agent-approval#agentApproval/setJevConfig",
268
+ service: "agentApproval",
269
+ namespace: "agentApproval",
270
+ method: "setJevConfig",
271
+ invocation: { kind: "direct" },
272
+ parameters: param("dsh-agent-approval#AgentApprovalSetJevRequest"),
273
+ result: result("dsh-agent-approval#AgentApprovalSetJevResult"),
274
+ },
259
275
  {
260
276
  id: "dsh-agent-approval#agentApproval/setApprovalTimeout",
261
277
  service: "agentApproval",
@@ -413,6 +429,16 @@ window.__ModuleLoader__.load({
413
429
  const setModel = modelSlot[1];
414
430
  const timeoutSlot = React.useState("");
415
431
  const setTimeoutDraft = timeoutSlot[1];
432
+ // Jev backend drafts (edited in the Jev card shown when the judge
433
+ // provider is the synthetic "typesafe" entry).
434
+ const jevKeySlot = React.useState("");
435
+ const setJevKey = jevKeySlot[1];
436
+ const jevEndpointSlot = React.useState("");
437
+ const setJevEndpoint = jevEndpointSlot[1];
438
+ const jevModelSlot = React.useState("");
439
+ const setJevModel = jevModelSlot[1];
440
+ const jevConfSlot = React.useState("0.5");
441
+ const setJevConf = jevConfSlot[1];
416
442
  const noteSlot = React.useState("");
417
443
  const note = noteSlot[0];
418
444
  const setNote = noteSlot[1];
@@ -431,6 +457,11 @@ window.__ModuleLoader__.load({
431
457
  setProvider(s.model.provider);
432
458
  setModel(s.model.model);
433
459
  setTimeoutDraft(String(s.timeoutMs));
460
+ // A not-yet-restarted old host sends no `jev` field — keep drafts.
461
+ setJevKey(s.jev && typeof s.jev.apiKey === "string" ? s.jev.apiKey : "");
462
+ setJevEndpoint(s.jev && typeof s.jev.endpoint === "string" ? s.jev.endpoint : "");
463
+ setJevModel(s.jev && typeof s.jev.model === "string" ? s.jev.model : "");
464
+ setJevConf(s.jev && typeof s.jev.confidence === "number" ? String(s.jev.confidence) : "0.5");
434
465
  })
435
466
  .catch((e) => setNote("无法读取状态:" + (e && e.message ? e.message : String(e))));
436
467
  };
@@ -458,6 +489,28 @@ window.__ModuleLoader__.load({
458
489
  })
459
490
  .catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
460
491
  };
492
+ const jevKey = jevKeySlot[0];
493
+ const jevEndpoint = jevEndpointSlot[0];
494
+ const jevModel = jevModelSlot[0];
495
+ const jevConf = jevConfSlot[0];
496
+ const saveJev = () => {
497
+ if (typeof remote.setJevConfig !== "function") {
498
+ setNote("Host 半未更新(缺少 setJevConfig):请重装本插件并重启 DSH。");
499
+ return;
500
+ }
501
+ const conf = Number(jevConf);
502
+ if (!Number.isFinite(conf) || conf <= 0 || conf >= 1) {
503
+ setNote("置信度阈值必须是 0–1 之间的小数(如 0.5)");
504
+ return;
505
+ }
506
+ remote
507
+ .setJevConfig({ apiKey: jevKey, endpoint: jevEndpoint, model: jevModel, confidence: conf })
508
+ .then(() => {
509
+ refresh();
510
+ setNote("Jev 配置已保存");
511
+ })
512
+ .catch((e) => setNote("保存失败:" + (e && e.message ? e.message : String(e))));
513
+ };
461
514
  const saveTimeout = () => {
462
515
  const parsed = Number(timeoutDraft);
463
516
  if (!Number.isFinite(parsed)) {
@@ -520,19 +573,30 @@ window.__ModuleLoader__.load({
520
573
  .catch(() => {});
521
574
  };
522
575
 
576
+ // The synthetic "typesafe" provider routes judging through the
577
+ // TypeSafe Jev HTTP API — it is not part of the harness directory.
523
578
  const providerOptions = [{ id: "", name: "默认(Harness 默认模型)" }].concat(
579
+ [{ id: "typesafe", name: "TypeSafe Jev(决策模型·直连 API)" }],
524
580
  dir ? dir.providers : [],
525
581
  );
526
- const modelOptions = [{ provider: "", id: "", name: "默认(Harness 默认模型)" }].concat(
527
- dir && dir.models ? dir.models.filter((m) => m.provider === provider) : [],
528
- );
582
+ const modelOptions =
583
+ provider === "typesafe"
584
+ ? [
585
+ { provider: "typesafe", id: "jev-latest", name: "jev-latest(跟随最新版本)" },
586
+ { provider: "typesafe", id: "jev-1.13.0", name: "jev-1.13.0(锁定版本)" },
587
+ ]
588
+ : [{ provider: "", id: "", name: "默认(Harness 默认模型)" }].concat(
589
+ dir && dir.models ? dir.models.filter((m) => m.provider === provider) : [],
590
+ );
529
591
  const defaultHint =
530
- dir && dir.defaultSelection
531
- ? "未配置时使用 Harness 默认模型;当前默认路由:" +
532
- dir.defaultSelection.provider +
533
- " / " +
534
- dir.defaultSelection.model
535
- : "未配置时使用 Harness 默认模型路由";
592
+ provider === "typesafe"
593
+ ? "当前审批判定直连 TypeSafe Jev API,不经过 Harness 模型路由;下方 Jev 配置在该模式下生效。"
594
+ : dir && dir.defaultSelection
595
+ ? "未配置时使用 Harness 默认模型;当前默认路由:" +
596
+ dir.defaultSelection.provider +
597
+ " / " +
598
+ dir.defaultSelection.model
599
+ : "未配置时使用 Harness 默认模型路由";
536
600
 
537
601
  return h(
538
602
  "div",
@@ -568,7 +632,7 @@ window.__ModuleLoader__.load({
568
632
  value: provider,
569
633
  onChange: (e) => {
570
634
  setProvider(e.target.value);
571
- setModel("");
635
+ setModel(e.target.value === "typesafe" ? "jev-latest" : "");
572
636
  },
573
637
  },
574
638
  providerOptions.map((p) =>
@@ -586,7 +650,7 @@ window.__ModuleLoader__.load({
586
650
  className: "aapr-select",
587
651
  value: model,
588
652
  onChange: (e) => setModel(e.target.value),
589
- disabled: dir === null,
653
+ disabled: dir === null && provider !== "typesafe",
590
654
  },
591
655
  modelOptions.map((m) =>
592
656
  h("option", { key: m.provider + "/" + m.id, value: m.id }, m.id === "" ? m.name : m.name + "(" + m.id + ")"),
@@ -597,6 +661,75 @@ window.__ModuleLoader__.load({
597
661
  ),
598
662
  h("div", { className: "aapr-muted" }, defaultHint),
599
663
  ),
664
+ provider === "typesafe"
665
+ ? h(
666
+ "div",
667
+ { className: "aapr-card" },
668
+ h("h3", null, "TypeSafe Jev 配置"),
669
+ h(
670
+ "div",
671
+ { className: "aapr-muted" },
672
+ "Jev 是结构化决策模型(System One):审批时直连 TypeSafe API,不创建审批子会话,毫秒级返回带校准概率的裁决。审计「理由」由概率分布合成(Jev 本身不生成文字);置信度低于阈值时按 fail-closed 处理(记 unavailable,不批准也不记拒绝)。对中文任务上下文的准确率略低于英语。API Key 明文保存在本机 config.json;留空时使用环境变量 TYPESAFE_API_KEY。",
673
+ ),
674
+ h(
675
+ "div",
676
+ { className: "aapr-row" },
677
+ h(
678
+ "label",
679
+ null,
680
+ "API Key:",
681
+ h("input", {
682
+ className: "aapr-input",
683
+ type: "password",
684
+ placeholder: "TYPESAFE_API_KEY",
685
+ value: jevKey,
686
+ onChange: (e) => setJevKey(e.target.value),
687
+ }),
688
+ ),
689
+ h(
690
+ "label",
691
+ null,
692
+ "模型:",
693
+ h("input", {
694
+ className: "aapr-input",
695
+ placeholder: "jev-latest",
696
+ value: jevModel,
697
+ onChange: (e) => setJevModel(e.target.value),
698
+ }),
699
+ ),
700
+ ),
701
+ h(
702
+ "div",
703
+ { className: "aapr-row" },
704
+ h(
705
+ "label",
706
+ null,
707
+ "Endpoint:",
708
+ h("input", {
709
+ className: "aapr-input aapr-input-wide",
710
+ placeholder: "https://api.typesafe.ai/v1/systemone",
711
+ value: jevEndpoint,
712
+ onChange: (e) => setJevEndpoint(e.target.value),
713
+ }),
714
+ ),
715
+ h(
716
+ "label",
717
+ null,
718
+ "置信度阈值:",
719
+ h("input", {
720
+ className: "aapr-input",
721
+ type: "number",
722
+ step: "0.05",
723
+ min: "0.01",
724
+ max: "0.99",
725
+ value: jevConf,
726
+ onChange: (e) => setJevConf(e.target.value),
727
+ }),
728
+ ),
729
+ h(ui.Button, { variant: "primary", size: "sm", onClick: saveJev }, "保存"),
730
+ ),
731
+ )
732
+ : null,
600
733
  h(
601
734
  "div",
602
735
  { className: "aapr-card" },
package/index.js CHANGED
@@ -31,6 +31,11 @@
31
31
  * `callId`) plus the asker's stated reason. A rejection must name the
32
32
  * concrete, credible risk the operation creates (destructive /
33
33
  * irreversible / out-of-scope / dishonest); vague unease is approved.
34
+ * v1.6.0: the judge can alternatively be the TypeSafe Jev "System One"
35
+ * decision model (synthetic provider id `typesafe`) — a direct HTTP
36
+ * call that answers typed Choice/Noul questions with calibrated
37
+ * probabilities; a confidence below the configured gate resolves
38
+ * fail-closed like any other fault (see `_judgeWithJev`).
34
39
  *
35
40
  * 3. FAIL CLOSED — any infrastructure fault, timeout, malformed verdict, or
36
41
  * cancellation maps to the fail-closed approval outcomes
@@ -104,6 +109,60 @@ const RECORDS_SIDECAR = "agent-approval.jsonl";
104
109
  const DATA_DIR = join(process.env.DSH_HOME || join(homedir(), ".dsh"), "agent-approval");
105
110
  const CONFIG_FILE = join(DATA_DIR, "config.json");
106
111
 
112
+ /**
113
+ * v1.6.0: the TypeSafe Jev judge backend. Jev is a "System One" decision
114
+ * model (https://api.typesafe.ai/v1/systemone): it does not generate text —
115
+ * it answers typed questions (Choice / Score / Noul) over one `state` with
116
+ * calibrated probability distributions in ~70–500ms. That is exactly the
117
+ * approval-verdict shape, so instead of spawning a judge subagent through
118
+ * the harness model registry (Jev is not a chat route and cannot appear in
119
+ * `llm.listProviders()`), the Host half calls its HTTP API directly when the
120
+ * configured judge provider is the synthetic `typesafe` id. Fail-closed is
121
+ * preserved end to end: any transport fault, non-200, malformed answer, or a
122
+ * confidence below the configured gate resolves `unavailable` — never a
123
+ * grant, and (below the gate) not a recorded rejection either.
124
+ */
125
+ const JEV_PROVIDER = "typesafe";
126
+ const JEV_DEFAULT_MODEL = "jev-latest";
127
+ const JEV_DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
128
+ const JEV_DEFAULT_CONFIDENCE = 0.5;
129
+
130
+ /**
131
+ * The typed questions sent to Jev. Policy lives in the criteria descriptions
132
+ * (Jev reads instructions literally and injects domain knowledge only through
133
+ * state + criteria); the wording mirrors the subagent judge prompt's APPROVE
134
+ * conditions / REJECT list / 误杀治理 so both backends judge to the same
135
+ * standard. `decision` and `riskLevel` are Choices (discrete options +
136
+ * probabilities + confidence); `concreteRisk` is a Noul probe whose
137
+ * probability is folded into the synthesized audit rationale.
138
+ */
139
+ const JEV_QUESTIONS = {
140
+ decision: {
141
+ type: "choice",
142
+ instructions: "Should this one-time sandbox-escalation request be approved?",
143
+ criteria: {
144
+ approve:
145
+ "Yes, approve once. The operation is plausibly safe, non-destructive and reversible; it is clearly required by or consistent with the user's task; the stated reason honestly matches the actual arguments; and granting it once cannot leak secrets or cause irreversible system-wide changes. The project's own documented install/build/deploy steps that place the project's own files into the tool's own documented directories, and reading tool-owned config or logs needed for the task, count as task-scoped.",
146
+ reject:
147
+ "No, reject. The operation is destructive or irreversible (mass deletion, disk formatting, registry/service/system-wide changes), exfiltrates credentials or secrets, touches resources unrelated to the task, modifies the operating system or other applications' data, hides intent behind encoded or obfuscated content, the stated reason does not match the actual arguments, or there is a concrete, credible risk — something it would destroy, leak, or change beyond the user's task.",
148
+ },
149
+ },
150
+ riskLevel: {
151
+ type: "choice",
152
+ instructions: "How risky is the requested operation?",
153
+ criteria: {
154
+ low: "Routine and easily reversible: reading files, or writing within the project workspace that can be regenerated or undone.",
155
+ medium: "Awkward to undo or touches more than the immediate task outputs, but not destructive and not security-sensitive.",
156
+ high: "Destructive, irreversible, system-wide, or touching credentials, secrets, or other applications' data.",
157
+ },
158
+ },
159
+ concreteRisk: {
160
+ type: "noul",
161
+ instructions:
162
+ "Does this specific operation create a concrete, credible risk — destroying data, leaking secrets or credentials, or changing the operating system, other applications, or resources beyond the user's task? Answer false when the operation is task-scoped and reversible; vague unease or an unfamiliar command is NOT a risk.",
163
+ },
164
+ };
165
+
107
166
  /**
108
167
  * The structured verdict the judge subagent MUST produce. Constrained to the
109
168
  * JSON-Schema subset `assertObjectJsonSchema` enforces for subagent outputs
@@ -225,6 +284,7 @@ export class AgentApprovalService extends TypertRemoteService {
225
284
  async [Service.init]() {
226
285
  markRemoteMethod(this, "getState", "getState");
227
286
  markRemoteMethod(this, "setModel", "setModel");
287
+ markRemoteMethod(this, "setJevConfig", "setJevConfig");
228
288
  markRemoteMethod(this, "setApprovalTimeout", "setApprovalTimeout");
229
289
  markRemoteMethod(this, "toggle", "toggle");
230
290
  markRemoteMethod(this, "addRule", "addRule");
@@ -234,6 +294,18 @@ export class AgentApprovalService extends TypertRemoteService {
234
294
 
235
295
  /** Judge model override; empty strings = use the harness default route. */
236
296
  this._model = { provider: "", model: "" };
297
+ /**
298
+ * TypeSafe Jev direct backend settings (used when `_model.provider` is
299
+ * the synthetic `typesafe` id). The API key lives in plaintext on this
300
+ * machine only (config.json, same trust domain as the rest of the
301
+ * settings); an empty key falls back to the TYPESAFE_API_KEY env var.
302
+ */
303
+ this._jev = {
304
+ apiKey: "",
305
+ endpoint: JEV_DEFAULT_ENDPOINT,
306
+ model: JEV_DEFAULT_MODEL,
307
+ confidence: JEV_DEFAULT_CONFIDENCE,
308
+ };
237
309
  /** Judge timeout in ms (clamped); a timeout resolves fail-closed. */
238
310
  this._timeoutMs = DEFAULT_TIMEOUT_MS;
239
311
  /** sessionId -> { prevSandbox?: string, prevApproval?: string } */
@@ -693,6 +765,7 @@ export class AgentApprovalService extends TypertRemoteService {
693
765
  _persistConfig() {
694
766
  const body = JSON.stringify({
695
767
  model: { provider: this._model.provider, model: this._model.model },
768
+ jev: this._jevShape(),
696
769
  timeoutMs: this._timeoutMs,
697
770
  rules: this._rules,
698
771
  });
@@ -719,6 +792,18 @@ export class AgentApprovalService extends TypertRemoteService {
719
792
  ) {
720
793
  this._model = { provider: cfg.model.provider, model: cfg.model.model };
721
794
  }
795
+ if (cfg.jev && typeof cfg.jev === "object") {
796
+ if (typeof cfg.jev.apiKey === "string") this._jev.apiKey = cfg.jev.apiKey;
797
+ if (typeof cfg.jev.endpoint === "string" && cfg.jev.endpoint !== "") {
798
+ this._jev.endpoint = cfg.jev.endpoint;
799
+ }
800
+ if (typeof cfg.jev.model === "string" && cfg.jev.model !== "") {
801
+ this._jev.model = cfg.jev.model;
802
+ }
803
+ if (typeof cfg.jev.confidence === "number" && Number.isFinite(cfg.jev.confidence)) {
804
+ this._jev.confidence = Math.min(0.99, Math.max(0.01, cfg.jev.confidence));
805
+ }
806
+ }
722
807
  if (typeof cfg.timeoutMs === "number" && Number.isFinite(cfg.timeoutMs)) {
723
808
  this._timeoutMs = Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.floor(cfg.timeoutMs)));
724
809
  }
@@ -892,6 +977,10 @@ export class AgentApprovalService extends TypertRemoteService {
892
977
  * records display — "p/m" = selected, "default(p/m)" = harness default.
893
978
  */
894
979
  _judgeRoute() {
980
+ if (this._model.provider === JEV_PROVIDER) {
981
+ const model = this._jevEffective().model;
982
+ return { provider: JEV_PROVIDER, model: model, label: "jev(" + model + ")" };
983
+ }
895
984
  if (this._model.provider !== "" && this._model.model !== "") {
896
985
  return {
897
986
  provider: this._model.provider,
@@ -957,7 +1046,13 @@ export class AgentApprovalService extends TypertRemoteService {
957
1046
  return "allowed-once";
958
1047
  }
959
1048
 
960
- // 3. The model judge.
1049
+ // 3. The judge. The TypeSafe Jev backend is a direct HTTP call (no
1050
+ // subagent, no harness model route); anything else spawns the judge
1051
+ // child through the `spawn` provider as before.
1052
+ if (this._model.provider === JEV_PROVIDER) {
1053
+ return this._judgeWithJev(session, req, argsRaw, base, trustKey);
1054
+ }
1055
+
961
1056
  const route = this._judgeRoute();
962
1057
 
963
1058
  let run;
@@ -1059,6 +1154,276 @@ export class AgentApprovalService extends TypertRemoteService {
1059
1154
  return "unavailable";
1060
1155
  }
1061
1156
 
1157
+ // ---- the TypeSafe Jev direct backend ---------------------------------------
1158
+
1159
+ /**
1160
+ * Effective Jev settings with env fallback and clamping applied. The key
1161
+ * may come from config.json or the TYPESAFE_API_KEY environment variable;
1162
+ * an absent key keeps the backend selected but every judgment resolves
1163
+ * `unavailable` (fail closed) until one is configured.
1164
+ */
1165
+ _jevEffective() {
1166
+ const key = String(this._jev.apiKey || process.env.TYPESAFE_API_KEY || "").trim();
1167
+ const endpoint = String(this._jev.endpoint || "").trim() || JEV_DEFAULT_ENDPOINT;
1168
+ const model = String(this._jev.model || "").trim() || JEV_DEFAULT_MODEL;
1169
+ let confidence = Number(this._jev.confidence);
1170
+ if (!Number.isFinite(confidence)) confidence = JEV_DEFAULT_CONFIDENCE;
1171
+ confidence = Math.min(0.99, Math.max(0.01, confidence));
1172
+ return { key: key, endpoint: endpoint, model: model, confidence: confidence };
1173
+ }
1174
+
1175
+ /** Owned plain copy of the Jev settings for the wire and config.json. */
1176
+ _jevShape() {
1177
+ return {
1178
+ apiKey: String(this._jev.apiKey || ""),
1179
+ endpoint: String(this._jev.endpoint || JEV_DEFAULT_ENDPOINT),
1180
+ model: String(this._jev.model || JEV_DEFAULT_MODEL),
1181
+ confidence: Number(this._jev.confidence) || JEV_DEFAULT_CONFIDENCE,
1182
+ };
1183
+ }
1184
+
1185
+ /**
1186
+ * The `state` sent to Jev: the same ground truth the subagent judge sees
1187
+ * (workspace, exact arguments, stated reason, first + recent genuine user
1188
+ * messages), as a named object — the shape TypeSafe recommends. All values
1189
+ * are pre-truncated strings so the 32K state budget is respected.
1190
+ */
1191
+ _jevStateOf(session, req, argsRaw) {
1192
+ let cwd = "";
1193
+ try {
1194
+ if (session.header && typeof session.header.cwd === "string") cwd = session.header.cwd;
1195
+ } catch (e) {
1196
+ /* header access is best-effort */
1197
+ }
1198
+ const task = this._recentUserContext(session);
1199
+ return {
1200
+ workspace: cwd || "(unknown)",
1201
+ tool: String(req.toolName),
1202
+ statedReason:
1203
+ typeof req.reason === "string" && req.reason !== "" ? trunc(req.reason, 300) : "(none)",
1204
+ toolArguments: argsRaw === undefined ? "(not available)" : trunc(argsRaw, 4000) || "(empty)",
1205
+ firstUserMessage: task.first !== "" ? task.first : "(no user messages available)",
1206
+ recentUserMessages: task.recent,
1207
+ };
1208
+ }
1209
+
1210
+ /**
1211
+ * Judge one escalation through the Jev HTTP API (state + typed questions →
1212
+ * calibrated probability distributions). Mirrors `_judge`'s spawn-path
1213
+ * contract exactly — rules and the session trust cache have already run —
1214
+ * and every abnormal shape resolves fail-closed:
1215
+ * - no API key / transport fault / non-200 / malformed answer → `unavailable`
1216
+ * - request cancelled mid-flight → `cancelled`
1217
+ * - overall timeout (the same `this._timeoutMs` budget) → `unavailable`
1218
+ * - confidence below the configured gate → `unavailable` (the model is
1219
+ * not sure enough to decide: never a grant, and not a recorded
1220
+ * rejection either — the v1.4.0 误杀治理 applies symmetrically)
1221
+ * Jev does not generate text, so the audit rationale is synthesized from
1222
+ * the returned distributions; the served model version (`body.model`,
1223
+ * which resolves aliases like jev-latest) is what the audit displays.
1224
+ */
1225
+ async _judgeWithJev(session, req, argsRaw, base, trustKey) {
1226
+ const cfg = this._jevEffective();
1227
+ if (cfg.key === "") {
1228
+ this._record(session, {
1229
+ ...base,
1230
+ outcome: "unavailable",
1231
+ riskLevel: "-",
1232
+ model: "jev(" + cfg.model + ")",
1233
+ rationale: "Jev backend selected but no API key configured (Settings → Agent 审批, or the TYPESAFE_API_KEY environment variable)",
1234
+ });
1235
+ return "unavailable";
1236
+ }
1237
+ if (typeof fetch !== "function") {
1238
+ this._record(session, { ...base, outcome: "unavailable", riskLevel: "-", model: "jev(" + cfg.model + ")", rationale: "fetch is unavailable in this runtime" });
1239
+ return "unavailable";
1240
+ }
1241
+
1242
+ const startedAt = Date.now();
1243
+ const controller = new AbortController();
1244
+ const signal = req.signal;
1245
+ const onAbort = () => controller.abort();
1246
+ if (signal && typeof signal.addEventListener === "function") {
1247
+ signal.addEventListener("abort", onAbort, { once: true });
1248
+ }
1249
+
1250
+ let winner;
1251
+ try {
1252
+ const state = this._jevStateOf(session, req, argsRaw);
1253
+ winner = await Promise.race([
1254
+ this._jevRequest(cfg, state, controller.signal)
1255
+ .then((body) => ({ kind: "result", body: body }))
1256
+ .catch((error) => ({
1257
+ kind: "fault",
1258
+ error: error,
1259
+ aborted: error && error.name === "AbortError",
1260
+ })),
1261
+ (signal
1262
+ ? new Promise((resolve) => {
1263
+ if (signal.aborted) {
1264
+ resolve(true);
1265
+ return;
1266
+ }
1267
+ signal.addEventListener("abort", () => resolve(true), { once: true });
1268
+ })
1269
+ : Promise.resolve(false)
1270
+ ).then((v) => ({ kind: "aborted", aborted: v })),
1271
+ this.ctx.timeout(this._timeoutMs).then(() => ({ kind: "timeout" })),
1272
+ ]);
1273
+ } finally {
1274
+ if (signal && typeof signal.removeEventListener === "function") {
1275
+ signal.removeEventListener("abort", onAbort);
1276
+ }
1277
+ // Whether we lost the race to timeout/cancel or the request already
1278
+ // settled, closing the transport is always safe.
1279
+ try {
1280
+ controller.abort();
1281
+ } catch (e) {
1282
+ /* controller abort never blocks the outcome */
1283
+ }
1284
+ }
1285
+ const durationMs = Date.now() - startedAt;
1286
+
1287
+ if (winner.kind === "result") {
1288
+ return this._jevVerdict(session, winner.body, cfg, base, trustKey, durationMs);
1289
+ }
1290
+ if (winner.kind === "aborted") {
1291
+ this._record(session, { ...base, outcome: "cancelled", riskLevel: "-", model: "jev(" + cfg.model + ")", rationale: "request cancelled while Jev was judging" });
1292
+ return "cancelled";
1293
+ }
1294
+ if (winner.kind === "timeout") {
1295
+ this._record(session, {
1296
+ ...base,
1297
+ outcome: "unavailable",
1298
+ riskLevel: "-",
1299
+ model: "jev(" + cfg.model + ")",
1300
+ rationale: "Jev request timed out after " + String(this._timeoutMs) + "ms (fail closed)",
1301
+ });
1302
+ return "unavailable";
1303
+ }
1304
+ if (winner.aborted) {
1305
+ this._record(session, { ...base, outcome: "cancelled", riskLevel: "-", model: "jev(" + cfg.model + ")", rationale: "request cancelled while Jev was judging" });
1306
+ return "cancelled";
1307
+ }
1308
+ this._record(session, { ...base, outcome: "unavailable", riskLevel: "-", model: "jev(" + cfg.model + ")", rationale: "Jev request failed: " + errText(winner.error) });
1309
+ return "unavailable";
1310
+ }
1311
+
1312
+ /** The single POST to the System One endpoint; resolves the parsed body. */
1313
+ async _jevRequest(cfg, state, abortSignal) {
1314
+ const response = await fetch(cfg.endpoint, {
1315
+ method: "POST",
1316
+ headers: {
1317
+ "Authorization": "Bearer " + cfg.key,
1318
+ "Content-Type": "application/json",
1319
+ },
1320
+ body: JSON.stringify({
1321
+ state: state,
1322
+ model: cfg.model,
1323
+ questions: JEV_QUESTIONS,
1324
+ }),
1325
+ signal: abortSignal,
1326
+ });
1327
+ if (!response.ok) {
1328
+ let detail = "";
1329
+ try {
1330
+ detail = trunc(String(await response.text()), 200);
1331
+ } catch (e) {
1332
+ /* body read is best-effort */
1333
+ }
1334
+ throw new Error("HTTP " + String(response.status) + (detail !== "" ? " " + detail : ""));
1335
+ }
1336
+ const body = await response.json();
1337
+ if (!body || typeof body !== "object") throw new Error("response body is not an object");
1338
+ return body;
1339
+ }
1340
+
1341
+ /**
1342
+ * Map a Jev response to the same outcomes the subagent path produces.
1343
+ * Returns the waterfall outcome string; records the audit line itself.
1344
+ */
1345
+ _jevVerdict(session, body, cfg, base, trustKey, durationMs) {
1346
+ const served = typeof body.model === "string" && body.model !== "" ? body.model : cfg.model;
1347
+ const label = "jev(" + served + ")";
1348
+ const answers = body.answers && typeof body.answers === "object" ? body.answers : {};
1349
+ const decision = answers.decision && typeof answers.decision === "object" ? answers.decision : undefined;
1350
+ const risk = answers.riskLevel && typeof answers.riskLevel === "object" ? answers.riskLevel : undefined;
1351
+ const probe = answers.concreteRisk && typeof answers.concreteRisk === "object" ? answers.concreteRisk : undefined;
1352
+
1353
+ const choice = decision && (decision.choice === "approve" || decision.choice === "reject") ? decision.choice : undefined;
1354
+ const confidence = decision ? Number(decision.confidence) : NaN;
1355
+ const probabilities = decision && decision.probabilities && typeof decision.probabilities === "object" ? decision.probabilities : {};
1356
+ const riskChoice =
1357
+ risk && (risk.choice === "low" || risk.choice === "medium" || risk.choice === "high") ? risk.choice : undefined;
1358
+ const probeNoul = probe ? Number(probe.noul) : NaN;
1359
+
1360
+ // Any missing or out-of-shape answer is fail-closed, not guessed.
1361
+ if (
1362
+ choice === undefined ||
1363
+ !Number.isFinite(confidence) ||
1364
+ confidence < 0 ||
1365
+ confidence > 1 ||
1366
+ riskChoice === undefined ||
1367
+ !Number.isFinite(probeNoul)
1368
+ ) {
1369
+ this._record(session, {
1370
+ ...base,
1371
+ durationMs: durationMs,
1372
+ outcome: "unavailable",
1373
+ riskLevel: "-",
1374
+ model: label,
1375
+ rationale: "Jev returned no valid verdict shape (decision/riskLevel/concreteRisk incomplete)",
1376
+ });
1377
+ return "unavailable";
1378
+ }
1379
+
1380
+ // Confidence gate: below the threshold the model is not sure enough to
1381
+ // decide at all — never a grant, never a recorded rejection.
1382
+ if (confidence < cfg.confidence) {
1383
+ this._record(session, {
1384
+ ...base,
1385
+ durationMs: durationMs,
1386
+ outcome: "unavailable",
1387
+ riskLevel: riskChoice,
1388
+ model: label,
1389
+ rationale:
1390
+ "Jev confidence " + confidence.toFixed(2) + " is below the gate " + cfg.confidence.toFixed(2) + " (decision draft: " + choice + ") — fail closed",
1391
+ });
1392
+ return "unavailable";
1393
+ }
1394
+
1395
+ const pApprove = Number(probabilities.approve);
1396
+ const pReject = Number(probabilities.reject);
1397
+ const rationale =
1398
+ "Jev 决策=" + choice +
1399
+ "(置信度 " + confidence.toFixed(2) +
1400
+ (Number.isFinite(pApprove) && Number.isFinite(pReject)
1401
+ ? ",p approve/reject " + pApprove.toFixed(2) + "/" + pReject.toFixed(2)
1402
+ : "") +
1403
+ ");风险=" + riskChoice +
1404
+ ";具体风险概率=" + probeNoul.toFixed(2) +
1405
+ "。Jev 为结构化决策模型,不生成文字,本理由由概率分布合成。";
1406
+
1407
+ base.durationMs = durationMs;
1408
+ const approved = choice === "approve";
1409
+ this._record(session, {
1410
+ ...base,
1411
+ outcome: approved ? "allowed-once" : "rejected",
1412
+ riskLevel: riskChoice,
1413
+ model: label,
1414
+ rationale: trunc(rationale, 600),
1415
+ });
1416
+ if (approved && trustKey !== undefined) {
1417
+ let set = this._trusted.get(session.id);
1418
+ if (set === undefined) {
1419
+ set = new Set();
1420
+ this._trusted.set(session.id, set);
1421
+ }
1422
+ set.add(trustKey);
1423
+ }
1424
+ return approved ? "allowed-once" : "rejected";
1425
+ }
1426
+
1062
1427
  // ---- Remote API ------------------------------------------------------------
1063
1428
 
1064
1429
  /**
@@ -1100,6 +1465,7 @@ export class AgentApprovalService extends TypertRemoteService {
1100
1465
  ok: true,
1101
1466
  value: {
1102
1467
  model: { provider: this._model.provider, model: this._model.model },
1468
+ jev: this._jevShape(),
1103
1469
  timeoutMs: this._timeoutMs,
1104
1470
  enabledSessions: this._sessionInfos(),
1105
1471
  rules: this._rulesSnapshot(),
@@ -1114,12 +1480,35 @@ export class AgentApprovalService extends TypertRemoteService {
1114
1480
  async setModel(request) {
1115
1481
  const provider = request && typeof request.provider === "string" ? request.provider : "";
1116
1482
  const model = request && typeof request.model === "string" ? request.model : "";
1117
- this._model =
1118
- provider !== "" && model !== "" ? { provider, model } : { provider: "", model: "" };
1483
+ if (provider === JEV_PROVIDER) {
1484
+ // The Jev backend ignores the harness route table; an unset model just
1485
+ // means the latest alias.
1486
+ this._model = { provider: JEV_PROVIDER, model: model !== "" ? model : JEV_DEFAULT_MODEL };
1487
+ } else {
1488
+ this._model =
1489
+ provider !== "" && model !== "" ? { provider, model } : { provider: "", model: "" };
1490
+ }
1119
1491
  this._persistConfig();
1120
1492
  return { ok: true, value: { model: { provider: this._model.provider, model: this._model.model } } };
1121
1493
  }
1122
1494
 
1495
+ /**
1496
+ * Set the TypeSafe Jev backend settings (only provided fields change).
1497
+ * `confidence` is the gate below which Jev's answer is not trusted and the
1498
+ * outcome resolves fail-closed; clamped to [0.01, 0.99]. Persisted.
1499
+ */
1500
+ async setJevConfig(request) {
1501
+ const r = request && typeof request === "object" ? request : {};
1502
+ if (typeof r.apiKey === "string") this._jev.apiKey = r.apiKey.trim();
1503
+ if (typeof r.endpoint === "string") this._jev.endpoint = r.endpoint.trim();
1504
+ if (typeof r.model === "string") this._jev.model = r.model.trim();
1505
+ if (typeof r.confidence === "number" && Number.isFinite(r.confidence)) {
1506
+ this._jev.confidence = Math.min(0.99, Math.max(0.01, r.confidence));
1507
+ }
1508
+ this._persistConfig();
1509
+ return { ok: true, value: { jev: this._jevShape() } };
1510
+ }
1511
+
1123
1512
  /** Set the judge timeout (clamped to [MIN, MAX] milliseconds). Persisted. */
1124
1513
  async setApprovalTimeout(request) {
1125
1514
  const raw = request && typeof request.timeoutMs === "number" ? request.timeoutMs : 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@duke-dsh-plugins/dsh-agent-approval",
3
- "version": "1.5.0",
4
- "description": "Agent-decided approvals for DeepSeek Harness: a workspace-write base permission mode where an independent approval subagent judges every sandbox escalation (risky operations are rejected), with a configurable approval model and a per-session audit trail in the conversation window's 审批 tab.",
3
+ "version": "1.7.0",
4
+ "description": "Agent-decided approvals for DeepSeek Harness: a workspace-write base permission mode where an independent approval judge (an isolated subagent, or the TypeSafe Jev decision model via direct API) evaluates every sandbox escalation (risky operations are rejected), with a configurable approval model and a per-session audit trail in the conversation window's 审批 tab.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/MoonlitDropOfBlood/dsh-agent-approval.git"
@@ -36,13 +36,13 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7"
39
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7 || ^0.1.7-rc.1"
40
40
  },
41
41
  "dependencies": {
42
42
  "zod": "^4.4.3"
43
43
  },
44
44
  "scripts": {
45
45
  "patch:glyph": "node scripts/patch-glyph.mjs",
46
- "check": "node --check index.js && node --check client.js && node --check typert.host.js && node --check scripts/patch-glyph.mjs"
46
+ "check": "node --check index.js && node --check client.js && node --check typert.host.js && node --check scripts/patch-glyph.mjs && node --check scripts/check-typert-manifest.mjs && node scripts/check-typert-manifest.mjs"
47
47
  }
48
48
  }
package/typert.host.js CHANGED
@@ -12,6 +12,17 @@
12
12
  *
13
13
  * Result schemas are STRICT: every Host return value must match exactly
14
14
  * (fields present, types correct), or the gateway validation fails.
15
+ *
16
+ * Codec wire format spans two host generations (v1.7.0): DSH ≤ 0.1.5-rc.3
17
+ * validates and parses through `codec.schema` (a zod v4 instance — the
18
+ * typert-loader checked `"_zod" in schema`, the gateway called
19
+ * `codec.schema.parse`), while DSH 0.1.7-rc.1+ replaced that contract with a
20
+ * `codec.create()` FACTORY (the loader now rejects any codec "with no create()
21
+ * factory"; the gateway calls `codec.create().parse`). Every codec below
22
+ * therefore carries BOTH fields: `schema` keeps 0.1.5 loading, `create` keeps
23
+ * 0.1.7 loading, and both generations parse through the same zod instance.
24
+ * Removing either field breaks one generation at startup (the typert-loader
25
+ * throw is why 0.1.7 refused this manifest before v1.7.0).
15
26
  */
16
27
 
17
28
  import { z } from "zod";
@@ -41,6 +52,15 @@ const modelRouteSchema = z
41
52
  })
42
53
  .readonly();
43
54
 
55
+ const jevConfigSchema = z
56
+ .object({
57
+ apiKey: z.string(),
58
+ endpoint: z.string(),
59
+ model: z.string(),
60
+ confidence: z.number(),
61
+ })
62
+ .readonly();
63
+
44
64
  const enabledSessionSchema = z
45
65
  .object({
46
66
  id: z.string(),
@@ -63,6 +83,7 @@ const ruleSchema = z
63
83
  const stateValueSchema = z
64
84
  .object({
65
85
  model: modelRouteSchema,
86
+ jev: jevConfigSchema,
66
87
  timeoutMs: z.number(),
67
88
  enabledSessions: z.array(enabledSessionSchema).readonly(),
68
89
  rules: z.array(ruleSchema).readonly(),
@@ -75,6 +96,12 @@ const setModelValueSchema = z
75
96
  })
76
97
  .readonly();
77
98
 
99
+ const setJevValueSchema = z
100
+ .object({
101
+ jev: jevConfigSchema,
102
+ })
103
+ .readonly();
104
+
78
105
  const setTimeoutValueSchema = z
79
106
  .object({
80
107
  timeoutMs: z.number(),
@@ -152,6 +179,7 @@ function okResult(valueSchema) {
152
179
 
153
180
  const stateResultSchema = okResult(stateValueSchema);
154
181
  const setModelResultSchema = okResult(setModelValueSchema);
182
+ const setJevResultSchema = okResult(setJevValueSchema);
155
183
  const setTimeoutResultSchema = okResult(setTimeoutValueSchema);
156
184
  const toggleResultSchema = okResult(toggleValueSchema);
157
185
  const addRuleResultSchema = okResult(rulesValueSchema);
@@ -166,6 +194,13 @@ const _agentApproval_setModel_parameter_0$schema = z.object({
166
194
  model: z.string(),
167
195
  });
168
196
 
197
+ const _agentApproval_setJevConfig_parameter_0$schema = z.object({
198
+ apiKey: z.string(),
199
+ endpoint: z.string(),
200
+ model: z.string(),
201
+ confidence: z.number(),
202
+ });
203
+
169
204
  const _agentApproval_setApprovalTimeout_parameter_0$schema = z.object({
170
205
  timeoutMs: z.number(),
171
206
  });
@@ -206,6 +241,7 @@ export const TYPERT = {
206
241
  mode: "strict",
207
242
  typeSymbol: "dsh-agent-approval#AgentApprovalStateResult",
208
243
  schema: stateResultSchema,
244
+ create: () => stateResultSchema,
209
245
  },
210
246
  sourceLocation: { file: "index.js", line: 1, column: 1 },
211
247
  },
@@ -224,6 +260,7 @@ export const TYPERT = {
224
260
  mode: "strict",
225
261
  typeSymbol: "dsh-agent-approval#AgentApprovalSetModelRequest",
226
262
  schema: _agentApproval_setModel_parameter_0$schema,
263
+ create: () => _agentApproval_setModel_parameter_0$schema,
227
264
  },
228
265
  },
229
266
  ],
@@ -231,6 +268,34 @@ export const TYPERT = {
231
268
  mode: "strict",
232
269
  typeSymbol: "dsh-agent-approval#AgentApprovalSetModelResult",
233
270
  schema: setModelResultSchema,
271
+ create: () => setModelResultSchema,
272
+ },
273
+ sourceLocation: { file: "index.js", line: 1, column: 1 },
274
+ },
275
+ {
276
+ id: "dsh-agent-approval#agentApproval/setJevConfig",
277
+ service: "agentApproval",
278
+ namespace: "agentApproval",
279
+ method: "setJevConfig",
280
+ invocation: { kind: "direct" },
281
+ parameters: [
282
+ {
283
+ name: "request",
284
+ wire: "request",
285
+ source: "json",
286
+ codec: {
287
+ mode: "strict",
288
+ typeSymbol: "dsh-agent-approval#AgentApprovalSetJevRequest",
289
+ schema: _agentApproval_setJevConfig_parameter_0$schema,
290
+ create: () => _agentApproval_setJevConfig_parameter_0$schema,
291
+ },
292
+ },
293
+ ],
294
+ result: {
295
+ mode: "strict",
296
+ typeSymbol: "dsh-agent-approval#AgentApprovalSetJevResult",
297
+ schema: setJevResultSchema,
298
+ create: () => setJevResultSchema,
234
299
  },
235
300
  sourceLocation: { file: "index.js", line: 1, column: 1 },
236
301
  },
@@ -249,6 +314,7 @@ export const TYPERT = {
249
314
  mode: "strict",
250
315
  typeSymbol: "dsh-agent-approval#AgentApprovalSetTimeoutRequest",
251
316
  schema: _agentApproval_setApprovalTimeout_parameter_0$schema,
317
+ create: () => _agentApproval_setApprovalTimeout_parameter_0$schema,
252
318
  },
253
319
  },
254
320
  ],
@@ -256,6 +322,7 @@ export const TYPERT = {
256
322
  mode: "strict",
257
323
  typeSymbol: "dsh-agent-approval#AgentApprovalSetTimeoutResult",
258
324
  schema: setTimeoutResultSchema,
325
+ create: () => setTimeoutResultSchema,
259
326
  },
260
327
  sourceLocation: { file: "index.js", line: 1, column: 1 },
261
328
  },
@@ -274,6 +341,7 @@ export const TYPERT = {
274
341
  mode: "strict",
275
342
  typeSymbol: "dsh-agent-approval#AgentApprovalToggleRequest",
276
343
  schema: _agentApproval_toggle_parameter_0$schema,
344
+ create: () => _agentApproval_toggle_parameter_0$schema,
277
345
  },
278
346
  },
279
347
  ],
@@ -281,6 +349,7 @@ export const TYPERT = {
281
349
  mode: "strict",
282
350
  typeSymbol: "dsh-agent-approval#AgentApprovalToggleResult",
283
351
  schema: toggleResultSchema,
352
+ create: () => toggleResultSchema,
284
353
  },
285
354
  sourceLocation: { file: "index.js", line: 1, column: 1 },
286
355
  },
@@ -299,6 +368,7 @@ export const TYPERT = {
299
368
  mode: "strict",
300
369
  typeSymbol: "dsh-agent-approval#AgentApprovalAddRuleRequest",
301
370
  schema: _agentApproval_addRule_parameter_0$schema,
371
+ create: () => _agentApproval_addRule_parameter_0$schema,
302
372
  },
303
373
  },
304
374
  ],
@@ -306,6 +376,7 @@ export const TYPERT = {
306
376
  mode: "strict",
307
377
  typeSymbol: "dsh-agent-approval#AgentApprovalRulesResult",
308
378
  schema: addRuleResultSchema,
379
+ create: () => addRuleResultSchema,
309
380
  },
310
381
  sourceLocation: { file: "index.js", line: 1, column: 1 },
311
382
  },
@@ -324,6 +395,7 @@ export const TYPERT = {
324
395
  mode: "strict",
325
396
  typeSymbol: "dsh-agent-approval#AgentApprovalRemoveRuleRequest",
326
397
  schema: _agentApproval_removeRule_parameter_0$schema,
398
+ create: () => _agentApproval_removeRule_parameter_0$schema,
327
399
  },
328
400
  },
329
401
  ],
@@ -331,6 +403,7 @@ export const TYPERT = {
331
403
  mode: "strict",
332
404
  typeSymbol: "dsh-agent-approval#AgentApprovalRulesResult",
333
405
  schema: removeRuleResultSchema,
406
+ create: () => removeRuleResultSchema,
334
407
  },
335
408
  sourceLocation: { file: "index.js", line: 1, column: 1 },
336
409
  },
@@ -349,6 +422,7 @@ export const TYPERT = {
349
422
  mode: "strict",
350
423
  typeSymbol: "dsh-agent-approval#AgentApprovalSessionRecordsRequest",
351
424
  schema: _agentApproval_sessionRecords_parameter_0$schema,
425
+ create: () => _agentApproval_sessionRecords_parameter_0$schema,
352
426
  },
353
427
  },
354
428
  ],
@@ -356,6 +430,7 @@ export const TYPERT = {
356
430
  mode: "strict",
357
431
  typeSymbol: "dsh-agent-approval#AgentApprovalSessionRecordsResult",
358
432
  schema: sessionRecordsResultSchema,
433
+ create: () => sessionRecordsResultSchema,
359
434
  },
360
435
  sourceLocation: { file: "index.js", line: 1, column: 1 },
361
436
  },
@@ -370,6 +445,7 @@ export const TYPERT = {
370
445
  mode: "strict",
371
446
  typeSymbol: "dsh-agent-approval#AgentApprovalDirectoryResult",
372
447
  schema: directoryResultSchema,
448
+ create: () => directoryResultSchema,
373
449
  },
374
450
  sourceLocation: { file: "index.js", line: 1, column: 1 },
375
451
  },
@@ -390,17 +466,25 @@ export const TYPERT = {
390
466
  kind: "method",
391
467
  name: "getState",
392
468
  signature: "@Remote('getState') async getState(): Promise<AgentApprovalStateResult>",
393
- summary: "Snapshot for the Settings page (model route, timeout, enabled sessions, rules).",
469
+ summary: "Snapshot for the Settings page (model route, Jev backend, timeout, enabled sessions, rules).",
394
470
  jsDoc:
395
- "/**\n * Return the judge model route, timeout, enabled sessions (id + session-list title + workspace cwd), and the rule table.\n * @returns success or a business failure.\n */",
471
+ "/**\n * Return the judge model route, the TypeSafe Jev backend settings, timeout, enabled sessions (id + session-list title + workspace cwd), and the rule table.\n * @returns success or a business failure.\n */",
396
472
  },
397
473
  {
398
474
  kind: "method",
399
475
  name: "setModel",
400
476
  signature: "@Remote('setModel') async setModel(request: AgentApprovalSetModelRequest): Promise<AgentApprovalSetModelResult>",
401
- summary: "Set the judge model route (empty strings = inherit the requesting session's).",
477
+ summary: "Set the judge model route (empty strings = inherit the requesting session's; provider 'typesafe' selects the Jev backend).",
478
+ jsDoc:
479
+ "/**\n * Set provider/model used by the approval judge; empty strings clear the override. The synthetic provider 'typesafe' routes judging through the TypeSafe Jev HTTP API instead of a harness subagent.\n * @param request - { provider, model }.\n * @returns the stored route.\n */",
480
+ },
481
+ {
482
+ kind: "method",
483
+ name: "setJevConfig",
484
+ signature: "@Remote('setJevConfig') async setJevConfig(request: AgentApprovalSetJevRequest): Promise<AgentApprovalSetJevResult>",
485
+ summary: "Set the TypeSafe Jev backend settings (API key, endpoint, model, confidence gate; persisted).",
402
486
  jsDoc:
403
- "/**\n * Set provider/model used by the approval subagent; empty strings clear the override.\n * @param request - { provider, model }.\n * @returns the stored route.\n */",
487
+ "/**\n * Update the Jev backend settings used when the judge provider is 'typesafe'. Only provided fields change; confidence (the fail-closed gate) is clamped to [0.01, 0.99].\n * @param request - { apiKey, endpoint, model, confidence }.\n * @returns the stored Jev settings.\n */",
404
488
  },
405
489
  {
406
490
  kind: "method",
@@ -462,6 +546,21 @@ export const TYPERT = {
462
546
  declaration:
463
547
  "export interface AgentApprovalModelRoute {\n readonly provider: string;\n readonly model: string;\n}",
464
548
  },
549
+ {
550
+ name: "AgentApprovalJevConfig",
551
+ declaration:
552
+ "export interface AgentApprovalJevConfig {\n readonly apiKey: string;\n readonly endpoint: string;\n readonly model: string;\n readonly confidence: number;\n}",
553
+ },
554
+ {
555
+ name: "AgentApprovalSetJevRequest",
556
+ declaration:
557
+ "export interface AgentApprovalSetJevRequest {\n readonly apiKey: string;\n readonly endpoint: string;\n readonly model: string;\n readonly confidence: number;\n}",
558
+ },
559
+ {
560
+ name: "AgentApprovalSetJevResult",
561
+ declaration:
562
+ "export type AgentApprovalSetJevResult = { ok: true; value: { readonly jev: AgentApprovalJevConfig } } | { ok: false; error: { code: string; message?: string } };",
563
+ },
465
564
  {
466
565
  name: "AgentApprovalEnabledSession",
467
566
  declaration:
@@ -480,7 +579,7 @@ export const TYPERT = {
480
579
  {
481
580
  name: "AgentApprovalStateValue",
482
581
  declaration:
483
- "export interface AgentApprovalStateValue {\n readonly model: AgentApprovalModelRoute;\n readonly timeoutMs: number;\n readonly enabledSessions: readonly AgentApprovalEnabledSession[];\n readonly rules: readonly AgentApprovalRule[];\n}",
582
+ "export interface AgentApprovalStateValue {\n readonly model: AgentApprovalModelRoute;\n readonly jev: AgentApprovalJevConfig;\n readonly timeoutMs: number;\n readonly enabledSessions: readonly AgentApprovalEnabledSession[];\n readonly rules: readonly AgentApprovalRule[];\n}",
484
583
  },
485
584
  {
486
585
  name: "AgentApprovalSessionRecordsRequest",