@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 +13 -8
- package/client.js +165 -32
- package/index.js +392 -3
- package/package.json +4 -4
- package/typert.host.js +104 -5
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
|
-
|
|
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.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 审批**
|
|
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 审批」+
|
|
89
|
-
├── typert.host.js # Typert Host manifest(agentApproval
|
|
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
|
|
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,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// <
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
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*="
|
|
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
|
|
223
|
-
//
|
|
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:
|
|
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 =
|
|
527
|
-
|
|
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
|
-
|
|
531
|
-
? "
|
|
532
|
-
|
|
533
|
-
"
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
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
|
-
|
|
1118
|
-
|
|
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.
|
|
4
|
-
"description": "Agent-decided approvals for DeepSeek Harness: a workspace-write base permission mode where an independent approval subagent
|
|
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 *
|
|
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",
|