@goodandready/dsh-approval-gate 0.1.26 → 0.1.28
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/CHANGELOG.md +229 -218
- package/lib/client.js +252 -67
- package/lib/compat.js +55 -22
- package/lib/index.js +3 -5
- package/lib/learning.js +5 -0
- package/lib/timeline-service.js +114 -33
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// lib/client.js
|
|
2
|
-
// Browser client half for DeepSeek Harness: Approval Gate UI (#74, #75, #82, #86, #60).
|
|
2
|
+
// Browser client half for DeepSeek Harness: Approval Gate UI (#74, #75, #82, #86, #60, #49, #84, #83, #116).
|
|
3
3
|
// Self-registering module for window.__ModuleLoader__.
|
|
4
4
|
// Canonical languages: English (en) and Chinese (zh).
|
|
5
5
|
|
|
@@ -26,16 +26,25 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
viewDiff: "View Diff",
|
|
27
27
|
diffModalTitle: "Unified Diff Viewer",
|
|
28
28
|
close: "Close",
|
|
29
|
-
expandFold: "Click to expand {count} unchanged lines",
|
|
30
|
-
collapseFold: "Collapse unchanged lines",
|
|
31
29
|
autoTimeoutIn: "Auto-decision in {seconds}s",
|
|
32
30
|
timerPaused: "Timer paused",
|
|
33
31
|
pauseTimer: "Pause",
|
|
34
32
|
resumeTimer: "Resume",
|
|
35
33
|
turnSummary: "Round Summary",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
settingsTitle: "Approval Gate Settings",
|
|
35
|
+
settingsSubtitle: "Security rules, path whitelists and learned rules management",
|
|
36
|
+
rulesEditorTitle: "Rules & Whitelists",
|
|
37
|
+
whitelistPaths: "Path Whitelist",
|
|
38
|
+
addPath: "Add Path",
|
|
39
|
+
learnedRulesTitle: "Learned Routine Rules",
|
|
40
|
+
learnedRulesDesc: "Safe operations auto-learned after repeated manual confirmations",
|
|
41
|
+
revoke: "Revoke",
|
|
42
|
+
noLearnedRules: "No learned rules in active session.",
|
|
43
|
+
toastAutoApproved: "Auto-approved: ",
|
|
44
|
+
modeStrict: "Strict",
|
|
45
|
+
modeHybrid: "Hybrid",
|
|
46
|
+
modeAuto: "Auto",
|
|
47
|
+
modeManual: "Manual",
|
|
39
48
|
};
|
|
40
49
|
|
|
41
50
|
const zh = {
|
|
@@ -50,16 +59,25 @@ window.__ModuleLoader__.load({
|
|
|
50
59
|
viewDiff: "查看差异",
|
|
51
60
|
diffModalTitle: "统一差异比对器",
|
|
52
61
|
close: "关闭",
|
|
53
|
-
expandFold: "点击展开 {count} 行未修改代码",
|
|
54
|
-
collapseFold: "折叠未修改代码",
|
|
55
62
|
autoTimeoutIn: "倒计时 {seconds} 秒后自动判定",
|
|
56
63
|
timerPaused: "倒计时已暂停",
|
|
57
64
|
pauseTimer: "暂停",
|
|
58
65
|
resumeTimer: "继续",
|
|
59
66
|
turnSummary: "轮次总结",
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
settingsTitle: "审批网关设置",
|
|
68
|
+
settingsSubtitle: "安全规则、路径白名单及自动学习规则管理",
|
|
69
|
+
rulesEditorTitle: "自定义规则与白名单",
|
|
70
|
+
whitelistPaths: "路径白名单",
|
|
71
|
+
addPath: "添加路径",
|
|
72
|
+
learnedRulesTitle: "已学习规则管理",
|
|
73
|
+
learnedRulesDesc: "经过多次手动确认后自动放行的安全例行操作",
|
|
74
|
+
revoke: "撤回",
|
|
75
|
+
noLearnedRules: "当前会话尚无已学习规则。",
|
|
76
|
+
toastAutoApproved: "已自动放行:",
|
|
77
|
+
modeStrict: "严格",
|
|
78
|
+
modeHybrid: "混合",
|
|
79
|
+
modeAuto: "自动",
|
|
80
|
+
modeManual: "手动",
|
|
63
81
|
};
|
|
64
82
|
|
|
65
83
|
// ----------------------------------------------------------- CSS Styles
|
|
@@ -104,8 +122,6 @@ window.__ModuleLoader__.load({
|
|
|
104
122
|
.ag-diff-num { width: 36px; flex: none; text-align: right; padding-right: 8px; color: var(--dsw-alias-label-tertiary); user-select: none; }
|
|
105
123
|
.ag-diff-sign { width: 16px; flex: none; text-align: center; font-weight: bold; user-select: none; }
|
|
106
124
|
.ag-diff-text { flex: 1; white-space: pre-wrap; word-break: break-all; }
|
|
107
|
-
|
|
108
|
-
/* Hunk Folding #75 */
|
|
109
125
|
.ag-diff-fold { padding: 4px 12px; background: rgba(0,0,0,0.2); border-top: 1px dashed var(--dsw-alias-border-l2); border-bottom: 1px dashed var(--dsw-alias-border-l2); text-align: center; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 11px; margin: 4px 0; }
|
|
110
126
|
.ag-diff-fold:hover { background: rgba(0,0,0,0.3); color: var(--dsw-alias-label-primary); }
|
|
111
127
|
|
|
@@ -113,6 +129,33 @@ window.__ModuleLoader__.load({
|
|
|
113
129
|
.ag-countdown-box { display: flex; align-items: center; gap: 8px; font-size: 11px; color: #eab308; margin-top: 6px; }
|
|
114
130
|
.ag-countdown-badge { background: rgba(234, 179, 8, 0.2); border: 1px solid rgba(234, 179, 8, 0.4); padding: 2px 6px; border-radius: 4px; font-weight: 600; }
|
|
115
131
|
.ag-pause-btn { background: none; border: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); border-radius: 4px; padding: 1px 6px; cursor: pointer; font-size: 10px; }
|
|
132
|
+
|
|
133
|
+
/* Settings Card #49, #84 */
|
|
134
|
+
.ag-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; list-style: none; margin-bottom: 12px; }
|
|
135
|
+
.ag-card-head { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
|
|
136
|
+
.ag-card-title { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
|
|
137
|
+
.ag-card-sub { color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
138
|
+
.ag-card-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 12px 0 16px; }
|
|
139
|
+
.ag-chev { margin-left: auto; flex: none; color: var(--dsw-alias-label-tertiary); transition: transform .16s; font-size: 12px; }
|
|
140
|
+
.ag-chev-open { transform: rotate(180deg); }
|
|
141
|
+
|
|
142
|
+
.ag-sec-title { font-size: 13px; font-weight: 600; color: var(--dsw-alias-label-primary); margin: 12px 0 6px; }
|
|
143
|
+
.ag-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 6px; }
|
|
144
|
+
.ag-table th { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-secondary); font-weight: 500; }
|
|
145
|
+
.ag-table td { padding: 6px 8px; border-bottom: 1px solid var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); }
|
|
146
|
+
.ag-btn-revoke { background: rgba(239, 68, 68, 0.15); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 4px; padding: 2px 8px; cursor: pointer; font-size: 11px; }
|
|
147
|
+
.ag-input { height: 30px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); border-radius: 6px; padding: 0 8px; font-size: 12px; }
|
|
148
|
+
|
|
149
|
+
/* Floating Toast #83 */
|
|
150
|
+
.ag-toast { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: var(--dsw-alias-bg-layer-3); border: 1px solid #22c55e; color: var(--dsw-alias-label-primary); border-radius: 20px; padding: 6px 14px; font-size: 12px; display: flex; align-items: center; gap: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.3); z-index: 9999; animation: agFade .2s ease-in; }
|
|
151
|
+
@keyframes agFade { from { opacity: 0; transform: translate(-50%, 8px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
152
|
+
|
|
153
|
+
/* Toolbar Badge #116 */
|
|
154
|
+
.ag-mode-badge { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 10px; text-transform: uppercase; letter-spacing: 0.5px; cursor: default; }
|
|
155
|
+
.ag-mode-auto { background: rgba(34, 197, 94, 0.2); color: #22c55e; border: 1px solid rgba(34, 197, 94, 0.4); }
|
|
156
|
+
.ag-mode-hybrid { background: rgba(234, 179, 8, 0.2); color: #eab308; border: 1px solid rgba(234, 179, 8, 0.4); }
|
|
157
|
+
.ag-mode-strict { background: rgba(59, 130, 246, 0.2); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.4); }
|
|
158
|
+
.ag-mode-manual { background: rgba(156, 163, 175, 0.2); color: #9ca3af; border: 1px solid rgba(156, 163, 175, 0.4); }
|
|
116
159
|
`;
|
|
117
160
|
|
|
118
161
|
function injectStyles() {
|
|
@@ -154,10 +197,38 @@ window.__ModuleLoader__.load({
|
|
|
154
197
|
);
|
|
155
198
|
}
|
|
156
199
|
|
|
200
|
+
// ----------------------------------------------------------- Floating Toast Component (#83)
|
|
201
|
+
function AutoApprovalToast({ message, onDismiss }) {
|
|
202
|
+
React.useEffect(() => {
|
|
203
|
+
const t = setTimeout(() => { if (onDismiss) onDismiss(); }, 3000);
|
|
204
|
+
return () => clearTimeout(t);
|
|
205
|
+
}, [onDismiss]);
|
|
206
|
+
|
|
207
|
+
if (!message) return null;
|
|
208
|
+
return h("div", { className: "ag-toast", onClick: onDismiss },
|
|
209
|
+
h("span", null, "⚡ Auto-approved:"),
|
|
210
|
+
h("strong", null, message)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ----------------------------------------------------------- Gate Mode Badge Component (#116)
|
|
215
|
+
function GateModeBadge() {
|
|
216
|
+
const [mode, setMode] = React.useState("hybrid");
|
|
217
|
+
|
|
218
|
+
React.useEffect(() => {
|
|
219
|
+
fetch("/dsh-approval-gate/mode")
|
|
220
|
+
.then((r) => r.json())
|
|
221
|
+
.then((d) => { if (d && d.mode) setMode(d.mode); })
|
|
222
|
+
.catch(() => {});
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
const cls = `ag-mode-badge ag-mode-${mode}`;
|
|
226
|
+
return h("span", { className: cls, title: `Approval Gate mode: ${mode}` }, `🛡️ ${mode}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
157
229
|
// ----------------------------------------------------------- Diff Panel Modal (#74, #75)
|
|
158
230
|
function DiffPanelModal({ diffData, onClose }) {
|
|
159
231
|
const [expandedFolds, setExpandedFolds] = React.useState({});
|
|
160
|
-
|
|
161
232
|
if (!diffData) return null;
|
|
162
233
|
const hunks = diffData.hunks || [];
|
|
163
234
|
|
|
@@ -172,8 +243,8 @@ window.__ModuleLoader__.load({
|
|
|
172
243
|
h("button", { className: "ag-diff-btn", onClick: onClose }, "✕ Close")
|
|
173
244
|
),
|
|
174
245
|
h("div", { className: "ag-diff-content" },
|
|
175
|
-
hunks.map((hunk, hIdx) =>
|
|
176
|
-
|
|
246
|
+
hunks.map((hunk, hIdx) =>
|
|
247
|
+
h("div", { key: `hunk_${hIdx}` },
|
|
177
248
|
h("div", { className: "ag-diff-fold", style: { cursor: "default", fontWeight: "600" } }, hunk.header),
|
|
178
249
|
(hunk.items || []).map((item, iIdx) => {
|
|
179
250
|
if (item.type === "fold") {
|
|
@@ -194,26 +265,136 @@ window.__ModuleLoader__.load({
|
|
|
194
265
|
)
|
|
195
266
|
);
|
|
196
267
|
}
|
|
197
|
-
|
|
198
268
|
const isAdd = item.type === "add";
|
|
199
269
|
const isDel = item.type === "del";
|
|
200
270
|
const lineClass = isAdd ? "ag-diff-line ag-diff-line-add" : (isDel ? "ag-diff-line ag-diff-line-del" : "ag-diff-line");
|
|
201
|
-
const sign = isAdd ? "+" : (isDel ? "-" : " ");
|
|
202
|
-
|
|
203
271
|
return h("div", { key: `line_${hIdx}_${iIdx}`, className: lineClass },
|
|
204
272
|
h("span", { className: "ag-diff-num" }, item.oldLine || ""),
|
|
205
273
|
h("span", { className: "ag-diff-num" }, item.newLine || ""),
|
|
206
|
-
h("span", { className: "ag-diff-sign" },
|
|
274
|
+
h("span", { className: "ag-diff-sign" }, isAdd ? "+" : (isDel ? "-" : " ")),
|
|
207
275
|
h("span", { className: "ag-diff-text" }, item.text)
|
|
208
276
|
);
|
|
209
277
|
})
|
|
210
|
-
)
|
|
211
|
-
|
|
278
|
+
)
|
|
279
|
+
)
|
|
212
280
|
)
|
|
213
281
|
)
|
|
214
282
|
);
|
|
215
283
|
}
|
|
216
284
|
|
|
285
|
+
// ----------------------------------------------------------- Settings Card Component (#49, #84)
|
|
286
|
+
function ApprovalGateSettingsCard({ ctx }) {
|
|
287
|
+
const [open, setOpen] = React.useState(false);
|
|
288
|
+
const [learnedRules, setLearnedRules] = React.useState([]);
|
|
289
|
+
const [whitelist, setWhitelist] = React.useState([]);
|
|
290
|
+
const [newPath, setNewPath] = React.useState("");
|
|
291
|
+
|
|
292
|
+
const fetchSettings = React.useCallback(async () => {
|
|
293
|
+
try {
|
|
294
|
+
const lrRes = await fetch("/dsh-approval-gate/learned-rules");
|
|
295
|
+
if (lrRes.ok) {
|
|
296
|
+
const lrData = await lrRes.json();
|
|
297
|
+
setLearnedRules(lrData.rules || []);
|
|
298
|
+
}
|
|
299
|
+
const rRes = await fetch("/dsh-approval-gate/rules");
|
|
300
|
+
if (rRes.ok) {
|
|
301
|
+
const rData = await rRes.json();
|
|
302
|
+
setWhitelist(rData.whitelistPaths || []);
|
|
303
|
+
}
|
|
304
|
+
} catch (_) {}
|
|
305
|
+
}, []);
|
|
306
|
+
|
|
307
|
+
React.useEffect(() => {
|
|
308
|
+
if (open) fetchSettings();
|
|
309
|
+
}, [open, fetchSettings]);
|
|
310
|
+
|
|
311
|
+
const revokeRule = async (key) => {
|
|
312
|
+
try {
|
|
313
|
+
await fetch("/dsh-approval-gate/learned-rules/revoke", {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: { "Content-Type": "application/json" },
|
|
316
|
+
body: JSON.stringify({ key }),
|
|
317
|
+
});
|
|
318
|
+
setLearnedRules((prev) => prev.filter((r) => r.key !== key));
|
|
319
|
+
} catch (_) {}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const addPath = async () => {
|
|
323
|
+
if (!newPath.trim()) return;
|
|
324
|
+
const updated = [...whitelist, newPath.trim()];
|
|
325
|
+
setWhitelist(updated);
|
|
326
|
+
setNewPath("");
|
|
327
|
+
try {
|
|
328
|
+
await fetch("/dsh-approval-gate/rules", {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify({ whitelistPaths: updated }),
|
|
332
|
+
});
|
|
333
|
+
} catch (_) {}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return h("div", { className: "ag-card" },
|
|
337
|
+
h("button", {
|
|
338
|
+
className: "ag-card-head",
|
|
339
|
+
onClick: () => setOpen(!open),
|
|
340
|
+
"aria-expanded": open,
|
|
341
|
+
},
|
|
342
|
+
h("div", null,
|
|
343
|
+
h("div", { className: "ag-card-title" }, "🛡️ Approval Gate"),
|
|
344
|
+
h("div", { className: "ag-card-sub" }, "Rules editor, path whitelists and learned rules manager")
|
|
345
|
+
),
|
|
346
|
+
h("span", { className: `ag-chev ${open ? "ag-chev-open" : ""}` }, "▼")
|
|
347
|
+
),
|
|
348
|
+
open && h("div", { className: "ag-card-body" },
|
|
349
|
+
// Rules & Whitelists Editor (#49)
|
|
350
|
+
h("div", { className: "ag-sec-title" }, "Path Whitelist"),
|
|
351
|
+
h("div", { style: { display: "flex", gap: "6px", marginBottom: "8px" } },
|
|
352
|
+
h("input", {
|
|
353
|
+
className: "ag-input",
|
|
354
|
+
placeholder: "/workspace/temp",
|
|
355
|
+
value: newPath,
|
|
356
|
+
onChange: (e) => setNewPath(e.target.value),
|
|
357
|
+
style: { flex: 1 },
|
|
358
|
+
}),
|
|
359
|
+
h("button", { className: "ag-diff-btn", onClick: addPath }, "+ Add")
|
|
360
|
+
),
|
|
361
|
+
whitelist.length > 0 && h("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px", marginBottom: "12px" } },
|
|
362
|
+
whitelist.map((p, idx) => h("span", { key: idx, className: "ag-badge ag-badge-allow" }, p))
|
|
363
|
+
),
|
|
364
|
+
|
|
365
|
+
// Learned Rules Manager (#84)
|
|
366
|
+
h("div", { className: "ag-sec-title" }, "Learned Rules Manager"),
|
|
367
|
+
learnedRules.length === 0
|
|
368
|
+
? h("div", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: "12px" } }, "No learned rules in active session.")
|
|
369
|
+
: h("table", { className: "ag-table" },
|
|
370
|
+
h("thead", null,
|
|
371
|
+
h("tr", null,
|
|
372
|
+
h("th", null, "Tool"),
|
|
373
|
+
h("th", null, "Command / Target"),
|
|
374
|
+
h("th", null, "Hits"),
|
|
375
|
+
h("th", null, "Action")
|
|
376
|
+
)
|
|
377
|
+
),
|
|
378
|
+
h("tbody", null,
|
|
379
|
+
learnedRules.map((rule) =>
|
|
380
|
+
h("tr", { key: rule.key },
|
|
381
|
+
h("td", null, rule.tool || "bash"),
|
|
382
|
+
h("td", { style: { fontFamily: "monospace" } }, rule.command || rule.key),
|
|
383
|
+
h("td", null, rule.count || 1),
|
|
384
|
+
h("td", null,
|
|
385
|
+
h("button", {
|
|
386
|
+
className: "ag-btn-revoke",
|
|
387
|
+
onClick: () => revokeRule(rule.key),
|
|
388
|
+
}, "Revoke")
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
217
398
|
// ----------------------------------------------------------- Timeline Tab (#82, #86)
|
|
218
399
|
function ApprovalTimelineTab({ ctx }) {
|
|
219
400
|
const [filter, setFilter] = React.useState("all");
|
|
@@ -233,9 +414,7 @@ window.__ModuleLoader__.load({
|
|
|
233
414
|
const cData = await cRes.json();
|
|
234
415
|
setTurnCard(cData.card);
|
|
235
416
|
}
|
|
236
|
-
} catch (
|
|
237
|
-
// ignore network poll errors
|
|
238
|
-
}
|
|
417
|
+
} catch (_) {}
|
|
239
418
|
}, [filter]);
|
|
240
419
|
|
|
241
420
|
React.useEffect(() => {
|
|
@@ -244,30 +423,6 @@ window.__ModuleLoader__.load({
|
|
|
244
423
|
return () => clearInterval(poll);
|
|
245
424
|
}, [fetchTimeline]);
|
|
246
425
|
|
|
247
|
-
const openDiff = (evt) => {
|
|
248
|
-
if (evt.diffMeta) {
|
|
249
|
-
setActiveDiff(evt.diffMeta);
|
|
250
|
-
} else {
|
|
251
|
-
// Dummy sample diff for inspection demonstration
|
|
252
|
-
setActiveDiff({
|
|
253
|
-
filename: evt.command || "command.sh",
|
|
254
|
-
hunks: [{
|
|
255
|
-
header: "@@ -1,3 +1,4 @@",
|
|
256
|
-
items: [
|
|
257
|
-
{ type: "eq", text: "#!/usr/bin/env bash", oldLine: 1, newLine: 1 },
|
|
258
|
-
{ type: "del", text: "rm -rf /tmp/data", oldLine: 2, newLine: null },
|
|
259
|
-
{ type: "add", text: "trash /tmp/data", oldLine: null, newLine: 2 },
|
|
260
|
-
{ type: "fold", count: 8, foldedLines: [
|
|
261
|
-
{ type: "eq", text: "echo 'safe line 1'", oldLine: 3, newLine: 3 },
|
|
262
|
-
{ type: "eq", text: "echo 'safe line 2'", oldLine: 4, newLine: 4 },
|
|
263
|
-
] },
|
|
264
|
-
{ type: "add", text: "echo 'Done safely'", oldLine: null, newLine: 5 },
|
|
265
|
-
]
|
|
266
|
-
}]
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
426
|
return h("div", { className: "ag-view-tab" },
|
|
272
427
|
h("div", { className: "ag-header" },
|
|
273
428
|
h("span", { className: "ag-title" }, "🛡️ Security Gate"),
|
|
@@ -281,41 +436,31 @@ window.__ModuleLoader__.load({
|
|
|
281
436
|
)
|
|
282
437
|
)
|
|
283
438
|
),
|
|
284
|
-
|
|
285
|
-
// TurnCard Summary (#86)
|
|
286
439
|
turnCard && h("div", { className: "ag-turn-card" },
|
|
287
440
|
h("div", { className: `ag-turn-status ag-${turnCard.status}` },
|
|
288
441
|
turnCard.status === "clean" ? "✓ Turn Clean" : (turnCard.status === "blocked" ? "⚠ Operations Blocked" : "ℹ Confirmations Asked")
|
|
289
442
|
),
|
|
290
443
|
h("div", { className: "ag-turn-desc" }, turnCard.summaryText)
|
|
291
444
|
),
|
|
292
|
-
|
|
293
|
-
// Events List (#82)
|
|
294
445
|
h("div", { className: "ag-events-list" },
|
|
295
446
|
events.length === 0
|
|
296
447
|
? h("div", { style: { padding: "20px", textAlign: "center", color: "var(--dsw-alias-label-tertiary)" } }, "No events recorded yet")
|
|
297
|
-
: events.map((evt) =>
|
|
298
|
-
|
|
299
|
-
return h("div", { key: evt.id, className: "ag-event-item" },
|
|
448
|
+
: events.map((evt) =>
|
|
449
|
+
h("div", { key: evt.id, className: "ag-event-item" },
|
|
300
450
|
h("div", { className: "ag-event-head" },
|
|
301
451
|
h("span", { className: "ag-tool-name" }, evt.tool),
|
|
302
|
-
h("span", { className:
|
|
452
|
+
h("span", { className: `ag-badge ag-badge-${evt.verdict}` }, evt.verdict)
|
|
303
453
|
),
|
|
304
454
|
evt.command && h("div", { className: "ag-cmd-snippet" }, evt.command),
|
|
305
455
|
evt.reason && h("div", { style: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px" } }, evt.reason),
|
|
306
|
-
|
|
307
|
-
// Countdown Timer for Ask state (#60)
|
|
308
456
|
evt.verdict === "ask" && evt.countdownSec && h(CountdownBadge, { initialSeconds: evt.countdownSec }),
|
|
309
|
-
|
|
310
457
|
h("div", { className: "ag-event-footer" },
|
|
311
458
|
h("span", { className: "ag-time" }, new Date(evt.timestamp).toLocaleTimeString()),
|
|
312
|
-
h("button", { className: "ag-diff-btn", onClick: () =>
|
|
459
|
+
h("button", { className: "ag-diff-btn", onClick: () => setActiveDiff(evt.diffMeta || { filename: evt.command || "cmd.sh", hunks: [] }) }, "Diff")
|
|
313
460
|
)
|
|
314
|
-
)
|
|
315
|
-
|
|
461
|
+
)
|
|
462
|
+
)
|
|
316
463
|
),
|
|
317
|
-
|
|
318
|
-
// Modal Diff Viewer (#74, #75)
|
|
319
464
|
activeDiff && h(DiffPanelModal, { diffData: activeDiff, onClose: () => setActiveDiff(null) })
|
|
320
465
|
);
|
|
321
466
|
}
|
|
@@ -331,14 +476,14 @@ window.__ModuleLoader__.load({
|
|
|
331
476
|
console.warn("[dsh-approval-gate] client locale registration:", err && err.message);
|
|
332
477
|
}
|
|
333
478
|
|
|
334
|
-
// Register right session panel slot: conversation.view (#82)
|
|
479
|
+
// 1. Register right session panel slot: conversation.view (#82)
|
|
335
480
|
try {
|
|
336
481
|
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
|
337
482
|
ctx.slots.inject("conversation.view", function () {
|
|
338
483
|
return ctx.slots.register(
|
|
339
484
|
{
|
|
340
485
|
name: "conversation.view",
|
|
341
|
-
id:
|
|
486
|
+
id: PKG,
|
|
342
487
|
order: 45,
|
|
343
488
|
label: function () { return "🛡️ Approvals"; },
|
|
344
489
|
},
|
|
@@ -349,6 +494,43 @@ window.__ModuleLoader__.load({
|
|
|
349
494
|
} catch (err) {
|
|
350
495
|
console.warn("[dsh-approval-gate] conversation.view slot registration:", err && err.message);
|
|
351
496
|
}
|
|
497
|
+
|
|
498
|
+
// 2. Register settings card slot: settings.plugin.item (#49, #84)
|
|
499
|
+
try {
|
|
500
|
+
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
|
501
|
+
ctx.slots.inject("settings.plugin.item", function () {
|
|
502
|
+
return ctx.slots.register(
|
|
503
|
+
{
|
|
504
|
+
name: "settings.plugin.item",
|
|
505
|
+
key: NS,
|
|
506
|
+
locale: NS,
|
|
507
|
+
inject: function () { return { ctx }; },
|
|
508
|
+
},
|
|
509
|
+
ApprovalGateSettingsCard
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
console.warn("[dsh-approval-gate] settings.plugin.item slot registration:", err && err.message);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 3. Register toolbar badge slot: toolbar.badge (#116)
|
|
518
|
+
try {
|
|
519
|
+
if (ctx.slots && typeof ctx.slots.inject === "function") {
|
|
520
|
+
ctx.slots.inject("toolbar.badge", function () {
|
|
521
|
+
return ctx.slots.register(
|
|
522
|
+
{
|
|
523
|
+
name: "toolbar.badge",
|
|
524
|
+
id: PKG,
|
|
525
|
+
order: 25,
|
|
526
|
+
},
|
|
527
|
+
GateModeBadge
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
} catch (err) {
|
|
532
|
+
console.warn("[dsh-approval-gate] toolbar.badge slot registration:", err && err.message);
|
|
533
|
+
}
|
|
352
534
|
}
|
|
353
535
|
|
|
354
536
|
module.exports = {
|
|
@@ -357,6 +539,9 @@ window.__ModuleLoader__.load({
|
|
|
357
539
|
DiffPanelModal,
|
|
358
540
|
CountdownBadge,
|
|
359
541
|
ApprovalTimelineTab,
|
|
542
|
+
ApprovalGateSettingsCard,
|
|
543
|
+
AutoApprovalToast,
|
|
544
|
+
GateModeBadge,
|
|
360
545
|
};
|
|
361
546
|
return module.exports;
|
|
362
547
|
},
|
package/lib/compat.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// lib/compat.js
|
|
2
|
-
// Host compatibility checks and polyfills across DeepSeek Harness minor versions (#106).
|
|
2
|
+
// Host compatibility checks and polyfills across DeepSeek Harness minor versions (#106, #184).
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Detects available runtime capabilities and services in the Cordis host context (#106).
|
|
5
|
+
* Detects available runtime capabilities and services in the Cordis host context (#106, #184).
|
|
6
|
+
* Safely accesses properties via ctx.get() and guarded property lookup to avoid
|
|
7
|
+
* throwing in strict Cordis contexts where uninjected property access is forbidden.
|
|
6
8
|
*
|
|
7
9
|
* @param {any} ctx - Cordis / DSH host context
|
|
8
10
|
* @returns {{
|
|
@@ -11,7 +13,7 @@
|
|
|
11
13
|
* hasPreExecuteHook: boolean,
|
|
12
14
|
* hasProvideApi: boolean,
|
|
13
15
|
* hasLocaleService: boolean,
|
|
14
|
-
* harnessVersion: string
|
|
16
|
+
* harnessVersion: string
|
|
15
17
|
* }}
|
|
16
18
|
*/
|
|
17
19
|
export function detectHarnessCapabilities(ctx) {
|
|
@@ -22,39 +24,48 @@ export function detectHarnessCapabilities(ctx) {
|
|
|
22
24
|
hasPreExecuteHook: false,
|
|
23
25
|
hasProvideApi: false,
|
|
24
26
|
hasLocaleService: false,
|
|
25
|
-
harnessVersion:
|
|
27
|
+
harnessVersion: "unknown",
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const
|
|
31
|
+
const safeGet = (name) => {
|
|
30
32
|
try {
|
|
31
33
|
if (typeof ctx.get === "function") return ctx.get(name);
|
|
32
34
|
} catch {
|
|
33
35
|
// ignore
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
try {
|
|
38
|
+
return ctx[name];
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
36
42
|
};
|
|
37
43
|
|
|
38
|
-
const tools =
|
|
39
|
-
const locale =
|
|
44
|
+
const tools = safeGet("tools");
|
|
45
|
+
const locale = safeGet("locale");
|
|
46
|
+
const onFn = safeGet("on");
|
|
47
|
+
const provideFn = safeGet("provide");
|
|
48
|
+
|
|
49
|
+
const rawVersion = safeGet("version");
|
|
40
50
|
|
|
41
51
|
const harnessVersion =
|
|
42
|
-
(typeof
|
|
43
|
-
(typeof
|
|
44
|
-
|
|
52
|
+
(typeof rawVersion === "string" && rawVersion.trim()) ||
|
|
53
|
+
(rawVersion && typeof rawVersion === "object" && typeof rawVersion.version === "string" && rawVersion.version.trim()) ||
|
|
54
|
+
(typeof process.env.DSH_VERSION === "string" && process.env.DSH_VERSION.trim()) ||
|
|
55
|
+
"unknown";
|
|
45
56
|
|
|
46
57
|
return {
|
|
47
58
|
hasContext: true,
|
|
48
59
|
hasToolsService: Boolean(tools),
|
|
49
|
-
hasPreExecuteHook: typeof
|
|
50
|
-
hasProvideApi: typeof
|
|
60
|
+
hasPreExecuteHook: typeof onFn === "function",
|
|
61
|
+
hasProvideApi: typeof provideFn === "function",
|
|
51
62
|
hasLocaleService: Boolean(locale && typeof locale.register === "function"),
|
|
52
63
|
harnessVersion,
|
|
53
64
|
};
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/**
|
|
57
|
-
* Asserts host compatibility and identifies missing optional or required features (#106).
|
|
68
|
+
* Asserts host compatibility and identifies missing optional or required features (#106, #184).
|
|
58
69
|
*
|
|
59
70
|
* @param {any} ctx
|
|
60
71
|
* @param {{ requireTools?: boolean }} [options]
|
|
@@ -80,7 +91,7 @@ export function assertHarnessCompatibility(ctx, options = {}) {
|
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
83
|
-
* Safely provides a service to the host context with backward-compatible polyfill (#106).
|
|
94
|
+
* Safely provides a service to the host context with backward-compatible polyfill (#106, #184).
|
|
84
95
|
*
|
|
85
96
|
* @param {any} ctx
|
|
86
97
|
* @param {string} serviceName
|
|
@@ -90,21 +101,32 @@ export function assertHarnessCompatibility(ctx, options = {}) {
|
|
|
90
101
|
export function provideServicePolyfill(ctx, serviceName, serviceInstance) {
|
|
91
102
|
if (!ctx || typeof ctx !== "object" || !serviceName) return false;
|
|
92
103
|
|
|
93
|
-
|
|
104
|
+
let provideFn = undefined;
|
|
105
|
+
try {
|
|
106
|
+
provideFn = ctx.provide;
|
|
107
|
+
} catch {
|
|
108
|
+
// ignore
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (typeof provideFn === "function") {
|
|
94
112
|
try {
|
|
95
|
-
|
|
113
|
+
provideFn.call(ctx, serviceName, serviceInstance);
|
|
96
114
|
return true;
|
|
97
115
|
} catch {
|
|
98
116
|
// fallback to property assignment below
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
try {
|
|
121
|
+
ctx[serviceName] = serviceInstance;
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
104
126
|
}
|
|
105
127
|
|
|
106
128
|
/**
|
|
107
|
-
* Safely attaches an event hook to the host context with error boundary (#106).
|
|
129
|
+
* Safely attaches an event hook to the host context with error boundary (#106, #184).
|
|
108
130
|
*
|
|
109
131
|
* @param {any} ctx
|
|
110
132
|
* @param {string} event
|
|
@@ -112,12 +134,23 @@ export function provideServicePolyfill(ctx, serviceName, serviceInstance) {
|
|
|
112
134
|
* @returns {(() => void) | null} Unsubscribe dispose function or null
|
|
113
135
|
*/
|
|
114
136
|
export function registerHookPolyfill(ctx, event, handler) {
|
|
115
|
-
if (!ctx || typeof ctx
|
|
137
|
+
if (!ctx || typeof ctx !== "object" || typeof handler !== "function") {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let onFn = undefined;
|
|
142
|
+
try {
|
|
143
|
+
onFn = ctx.on;
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof onFn !== "function") {
|
|
116
149
|
return null;
|
|
117
150
|
}
|
|
118
151
|
|
|
119
152
|
try {
|
|
120
|
-
return
|
|
153
|
+
return onFn.call(ctx, event, handler);
|
|
121
154
|
} catch {
|
|
122
155
|
return null;
|
|
123
156
|
}
|
package/lib/index.js
CHANGED
|
@@ -408,7 +408,7 @@ export function apply(ctx, config) {
|
|
|
408
408
|
};
|
|
409
409
|
provideServicePolyfill(ctx, "approvalGate", gateService);
|
|
410
410
|
const turnCard = new TurnCardTracker();
|
|
411
|
-
const timeline = new TimelineService({ turnCardTracker: turnCard });
|
|
411
|
+
const timeline = new TimelineService({ turnCardTracker: turnCard, learner: engine.learner, engine });
|
|
412
412
|
timeline.registerRoutes(ctx);
|
|
413
413
|
gateService.timeline = timeline;
|
|
414
414
|
gateService.turnCard = turnCard;
|
|
@@ -539,12 +539,10 @@ export function apply(ctx, config) {
|
|
|
539
539
|
return mount();
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
export { SecurityEngine } from "./engine.js";
|
|
543
542
|
export { BandsEngine } from "./bands.js";
|
|
544
543
|
export { CircuitBreaker, CIRCUIT_STATE_ACTIVE, CIRCUIT_STATE_TRIPPED, CIRCUIT_STATE_RESUMED } from "./breaker.js";
|
|
545
544
|
export { SessionGrantStore, DEFAULT_GRANT_TTL_MS, DEFAULT_GRANT_MAX_USES, matchGrantCriteria } from "./grants.js";
|
|
546
545
|
export { canonicalizeCall, canonicalString, hashCall, sortKeys } from "./canonical.js";
|
|
547
|
-
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath } from "./paths.js";
|
|
548
546
|
export { splitSubcommands } from "./tokenizer.js";
|
|
549
547
|
export { redactCredentials, redactUrl } from "./redact.js";
|
|
550
548
|
export { CommandDispatcher, isReviewerAgent, extractFilePath, TOOL_KIND_SHELL, TOOL_KIND_FILE_WRITE, TOOL_KIND_FILE_READ, TOOL_KIND_NETWORK, TOOL_KIND_GENERIC } from "./dispatcher.js";
|
|
@@ -557,7 +555,7 @@ export { RetryExhaustionGuard, DEFAULT_MAX_RETRY_ATTEMPTS } from "./retry-exhaus
|
|
|
557
555
|
export { QualityLedger, MAX_LEDGER_EVENTS_PER_SESSION, CURRENT_SCHEMA_VERSION, SUPPORTED_SCHEMA_VERSIONS, migrateVerdictRecord } from "./ledger.js";
|
|
558
556
|
export { AuditLogger, DEFAULT_AUDIT_LOG_RELATIVE, DEFAULT_MAX_AUDIT_BYTES, DEFAULT_MAX_AUDIT_LINES, DEFAULT_RETAIN_LINES, auditRotateContent } from "./audit-log.js";
|
|
559
557
|
export { BlockCounters, categorizeReason } from "./counters.js";
|
|
560
|
-
export { isCriticalOperation } from "./engine.js";
|
|
558
|
+
export { SecurityEngine, isCriticalOperation } from "./engine.js";
|
|
561
559
|
export { parseYaml, parseScalar, stripComment } from "./yaml-parser.js";
|
|
562
560
|
export { matchConditionValue, evaluateWhen } from "./when-matcher.js";
|
|
563
561
|
export { HotwordsMatcher, DEFAULT_HOTWORDS, extractWords } from "./hotwords.js";
|
|
@@ -565,7 +563,7 @@ export { DeclarativeRulesEngine, BUILTIN_RULES_FILE, WORKSPACE_RULES_REL, USER_R
|
|
|
565
563
|
export { normalizeChatCompletionsUrl, withLlmRetry, sanitizeClassifierInput, truncateClassifierContext, isLowRiskExecution, isShapeRejectError, extractJsonPayload, ClassifierVerdictSchema, validateClassifierResponse, LlmClassifier } from "./classifier.js";
|
|
566
564
|
export { ContractGuard } from "./contract-guard.js";
|
|
567
565
|
export { OverengineeringGuard, DEFAULT_MAX_FILES_PER_SESSION } from "./overengineering.js";
|
|
568
|
-
export { resolveTempRoot, isGlobalTempPath, checkTempIsolation } from "./paths.js";
|
|
566
|
+
export { expandHome, resolveSafePath, isPathTraversal, canonicalizeFsPath, resolveTempRoot, isGlobalTempPath, checkTempIsolation } from "./paths.js";
|
|
569
567
|
export { computePayloadSize, scanPromptInjection } from "./inspect.js";
|
|
570
568
|
export { RISK_BUCKETS, RISK_TAGS, categorizeRiskBucket, extractRiskTags } from "./risk.js";
|
|
571
569
|
export { ClassifierLatencyTracker, DEFAULT_CLASSIFIER_SLA_MS } from "./latency.js";
|
package/lib/learning.js
CHANGED
|
@@ -218,6 +218,11 @@ export class ConfirmationLearner {
|
|
|
218
218
|
return rules;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
revoke(sessionId, key) {
|
|
222
|
+
const sessionMap = this._getSessionMap(sessionId);
|
|
223
|
+
return sessionMap.delete(key);
|
|
224
|
+
}
|
|
225
|
+
|
|
221
226
|
clearSession(sessionId) {
|
|
222
227
|
const key = sessionId || "default";
|
|
223
228
|
this.sessions.delete(key);
|