@goodandready/dsh-approval-gate 0.1.26 → 0.1.27

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/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
- statusClean: "Clean (all calls approved)",
37
- statusWarnings: "Warnings (confirmations requested)",
38
- statusBlocked: "Blocked operations detected",
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
- statusClean: "安全(全部放行)",
61
- statusWarnings: "提示(已请求人工确认)",
62
- statusBlocked: "拦截(存在违规操作)",
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
- return h("div", { key: `hunk_${hIdx}` },
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" }, 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 (e) {
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
- const badgeClass = `ag-badge ag-badge-${evt.verdict}`;
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: badgeClass }, evt.verdict)
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: () => openDiff(evt) }, "Diff")
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: "@goodandready/dsh-approval-gate",
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/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);