dsh-perm-gate 2.1.2 → 4.0.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/CHANGELOG.ja.md +82 -0
- package/CHANGELOG.ko.md +80 -0
- package/CHANGELOG.md +225 -0
- package/INSTALL.ja.md +15 -9
- package/INSTALL.ko.md +16 -10
- package/INSTALL.md +17 -9
- package/INSTALL.zh.md +13 -8
- package/README.ja.md +17 -5
- package/README.ko.md +18 -6
- package/README.md +33 -3
- package/README.zh.md +11 -3
- package/docs/baseline-guard.md +56 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +2 -2
- package/examples/permissions.example.yaml +16 -1
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +646 -95
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +11 -0
- package/lib/compiler.js +16 -0
- package/lib/config.d.ts +73 -12
- package/lib/config.js +56 -14
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +52 -2
- package/lib/evaluate.js +76 -2
- package/lib/events.d.ts +91 -2
- package/lib/events.js +220 -0
- package/lib/index.js +111 -26
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +17 -3
- package/lib/rule-chain.js +13 -22
- package/lib/rule-dims.d.ts +35 -0
- package/lib/rule-dims.js +36 -0
- package/lib/rule.d.ts +36 -1
- package/lib/rule.js +56 -8
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +121 -7
- package/lib/runtime.js +204 -25
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/package.json +13 -5
package/lib/client.js
CHANGED
|
@@ -16,10 +16,11 @@ window.__ModuleLoader__.load({
|
|
|
16
16
|
"card.description": "独立审批档位:与只读 / 完全权限 / 白名单平行,在会话权限下拉中显示为「自动审查」。前端只暴露一个开关;后台四个审批策略可组合、由此处设置决定,仍对 P0 硬拒绝保持 fail-closed。",
|
|
17
17
|
"card.permissive": "启用自动审查档位",
|
|
18
18
|
"card.permissiveHint": "关闭时门禁行为与此前完全一致。",
|
|
19
|
+
"card.scopeNote": "档位作用域:本门禁仅在会话权限档位为 %scope 时生效。其他档位下整个门禁停用(含 P0 硬拒绝),由所选档位自身的策略接管;停用时会写入一条 stand-down 事件,并在输入框上方常驻一条提示条。",
|
|
19
20
|
"card.strategies": "后台审批策略(至少一个生效;可组合)",
|
|
20
21
|
"card.strategy.trustAutoAllow": "trustAutoAllow — 作用域内安全操作自动放行,危险/未知转 ask(中间档基线)",
|
|
21
22
|
"card.strategy.alwaysConfirm": "alwaysConfirm — 一律逐次 ask;审批面板「允许控件」含两个扩展按钮:「本会话重复允许该类」(会话语限次 grant)与「允许所有类型」(命令词持久迁入 rulesFile 白名单)",
|
|
22
|
-
"card.strategy.llmAssist": "llmAssist — 自定义 LLM 按风险分级裁决:safe 自动放行;deletion/credential/remote/system/bulk
|
|
23
|
+
"card.strategy.llmAssist": "llmAssist — 自定义 LLM 按风险分级裁决:safe 自动放行;deletion/credential/remote/system/bulk 硬风险维持人工确认(分类器永不拒绝,只负责升级判断);neutral 进入确认学习;失败/超时回退人工",
|
|
23
24
|
"card.strategy.trustEscalation": "trustEscalation — 沙箱升级免确认:本门禁已放行的调用,其工具内部发起的沙箱提权(shell / pwsh / edit 的 sandbox_permissions)由门禁直接批准,不再弹窗",
|
|
24
25
|
"card.strategy.trustEscalationHint": "沙箱提权审批发生在 tools/pre-execute 之后、工具体内部,门禁原本看不到它,因此「LLM 判安全已自动放行」的调用仍会弹窗要求你批准提权。开启后,仅对门禁确实放行过的那一次调用(按 callId 精确匹配)免确认,其余一律照旧走人工;目标模式非 workspace-write / danger-full-access 或理由无法识别时同样回退人工。关闭后提权保持人工把关。",
|
|
25
26
|
"card.readonly": "只读",
|
|
@@ -88,8 +89,6 @@ window.__ModuleLoader__.load({
|
|
|
88
89
|
"card.llmPresetHost": "DSH 模型组(选择后自动切换为会话模式)",
|
|
89
90
|
"card.networkEnabled": "网络拦截(networkEnabled)",
|
|
90
91
|
"card.networkEnabledHint": "开启后启动本地代理拦截子进程出站连接,按规则放行/阻断。关闭则零行为变更。",
|
|
91
|
-
"card.watch": "规则热重载(watch)",
|
|
92
|
-
"card.watchHint": "开启后自动监听规则文件变更并即时生效,无需手动 reload。",
|
|
93
92
|
"card.networkRebindNote": "切换后立即生效(自动重绑,无需重载插件)。",
|
|
94
93
|
"card.networkInjectEnv": "注入代理环境变量(networkInjectEnv)",
|
|
95
94
|
"card.networkInjectEnvHint": "开启后为子进程写入 HTTP(S)_PROXY / ALL_PROXY(并清空 NO_PROXY)。关闭则只启动监听、不改变任何进程环境。",
|
|
@@ -102,6 +101,7 @@ window.__ModuleLoader__.load({
|
|
|
102
101
|
"history.tag.manualApproved": "人工通过",
|
|
103
102
|
"history.tag.manualRejected": "人工拒绝",
|
|
104
103
|
"history.tag.manualCancelled": "人工取消",
|
|
104
|
+
"history.tag.standDown": "门禁停用",
|
|
105
105
|
"history.learnedProgress": "学习 %n/%t",
|
|
106
106
|
"history.snapshots": "diff 快照",
|
|
107
107
|
"history.clearSession": "仅清本会话",
|
|
@@ -124,7 +124,41 @@ window.__ModuleLoader__.load({
|
|
|
124
124
|
"history.diffChipTitle": "查看该文件改动对比",
|
|
125
125
|
"history.diffStats": "+%a / -%r 行变更 · %c 行未变",
|
|
126
126
|
"history.diffGone": "(文件当前已不存在)",
|
|
127
|
-
"history.diffHidden": "%n 行未修改"
|
|
127
|
+
"history.diffHidden": "%n 行未修改",
|
|
128
|
+
"card.dryRun": "规则测试",
|
|
129
|
+
"card.dryRunHint": "用当前生效的规则判定一次调用,不执行任何工具、不写入任何规则。改规则前先在这里验一遍。",
|
|
130
|
+
"card.dryRunTool": "工具名",
|
|
131
|
+
"card.dryRunCommand": "命令 / 参数",
|
|
132
|
+
"card.dryRunCommandPlaceholder": "git push --force origin main",
|
|
133
|
+
"card.dryRunTest": "测试",
|
|
134
|
+
"card.dryRunRunning": "判定中…",
|
|
135
|
+
"card.dryRunEmpty": "尚未测试。填入工具名与命令,点「测试」查看会怎么裁决。",
|
|
136
|
+
"card.dryRunVerdict": "裁决",
|
|
137
|
+
"card.dryRunAllow": "放行",
|
|
138
|
+
"card.dryRunAsk": "人工确认",
|
|
139
|
+
"card.dryRunDeny": "拒绝",
|
|
140
|
+
"card.dryRunRule": "命中规则",
|
|
141
|
+
"card.dryRunRuleNone": "无规则命中,走 defaultAction:%d",
|
|
142
|
+
"card.dryRunDimensions": "约束维度",
|
|
143
|
+
"card.dryRunReason": "原因",
|
|
144
|
+
"card.dryRunNote": "「命中规则」来自规则层。若裁决由更早的硬拒绝或关键词黑名单产生,则不会有规则序号——那种情况下列出的维度仅供参考。",
|
|
145
|
+
"card.dryRunNoRules": "(当前加载了 0 条规则:检查 rulesFile 路径)",
|
|
146
|
+
"card.dryRunFail": "测试失败:",
|
|
147
|
+
"card.dryRunStale": "路由不存在(插件未重启?)",
|
|
148
|
+
"card.rules": "权限 YAML",
|
|
149
|
+
"card.rulesHint": "门禁此刻实际加载的规则文件原文。只读——这里改不了任何东西。要改规则,用下面的白名单,或直接编辑该文件。",
|
|
150
|
+
"card.rulesRefresh": "刷新",
|
|
151
|
+
"card.rulesLoading": "读取中…",
|
|
152
|
+
"card.rulesPath": "文件",
|
|
153
|
+
"card.rulesNone": "未配置规则文件",
|
|
154
|
+
"card.rulesCounts": "规则",
|
|
155
|
+
"card.rulesDefault": "defaultAction",
|
|
156
|
+
"card.rulesStats": "%b 字节 · %l 行",
|
|
157
|
+
"card.rulesMissing": "文件不存在。门禁会退到内置默认。",
|
|
158
|
+
"card.rulesTruncated": "文件过大,仅显示前 512 KB。",
|
|
159
|
+
"card.rulesError": "加载失败:",
|
|
160
|
+
"card.rulesStale": "路由不存在(插件未重启?)",
|
|
161
|
+
"card.rulesNote": "此处只读:面板无法修改该文件,白名单段是唯一会写入它的地方。"
|
|
128
162
|
};
|
|
129
163
|
/** English dictionary (keys mirror zh). */
|
|
130
164
|
const en = {
|
|
@@ -132,10 +166,11 @@ window.__ModuleLoader__.load({
|
|
|
132
166
|
"card.description": "Independent approval tier, parallel to read-only / full-access / whitelist, shown as 自动审查 in the session permission picker. The front-end exposes a single switch; the four backend approval strategies are combinable via this panel and still fail-closed against P0 hard-deny.",
|
|
133
167
|
"card.permissive": "Enable the 自动审查 tier",
|
|
134
168
|
"card.permissiveHint": "When off, the gate behaves exactly as before.",
|
|
169
|
+
"card.scopeNote": "Preset scope: this gate only acts while the session permission preset is %scope. Under any other preset the whole gate stands down — P0 hard-deny included — and the selected preset's own policy takes over. A stand-down records one event and leaves a sticky strip above the input.",
|
|
135
170
|
"card.strategies": "Backend approval strategies (at least one effective; combinable)",
|
|
136
171
|
"card.strategy.trustAutoAllow": "trustAutoAllow — safe in-scope ops auto-allow; dangerous/unknown ask (baseline middle tier)",
|
|
137
172
|
"card.strategy.alwaysConfirm": "alwaysConfirm — every crossing asks; the approval \"allow controls\" get two extended buttons: \"repeat-allow this type this session\" (bounded session grant) and \"allow every occurrence\" (persist the command word into the rulesFile whitelist)",
|
|
138
|
-
"card.strategy.llmAssist": "llmAssist — a custom LLM grades risk: safe auto-allows; deletion/credential/remote/system/bulk hard risks
|
|
173
|
+
"card.strategy.llmAssist": "llmAssist — a custom LLM grades risk: safe auto-allows; deletion/credential/remote/system/bulk hard risks keep the human ask (the classifier never denies — it only escalates); neutral enters confirm-learning; failure/timeout falls back to the human",
|
|
139
174
|
"card.strategy.trustEscalation": "trustEscalation — sandbox escalation without a prompt: for a call this gate already allowed, the sandbox widening a shell / pwsh / edit tool raises from inside itself is approved here instead of asking you",
|
|
140
175
|
"card.strategy.trustEscalationHint": "A sandbox escalation is asked from inside the tool body, after tools/pre-execute, so the gate never saw it — a call the LLM graded safe and the gate auto-allowed still prompted you to approve the widening. With this on, only the exact call the gate allowed (matched by callId) skips the prompt; everything else goes to the human as before, as does any unrecognized reason or a target other than workspace-write / danger-full-access. Turn it off to keep widening human-gated.",
|
|
141
176
|
"card.readonly": "Read-only",
|
|
@@ -204,8 +239,6 @@ window.__ModuleLoader__.load({
|
|
|
204
239
|
"card.llmPresetHost": "DSH model groups (selecting one switches to the session receiver)",
|
|
205
240
|
"card.networkEnabled": "Network interception (networkEnabled)",
|
|
206
241
|
"card.networkEnabledHint": "When on, starts a local proxy that intercepts outbound subprocess connections and applies allow/deny rules. Off = zero behavior change.",
|
|
207
|
-
"card.watch": "Rule hot-reload (watch)",
|
|
208
|
-
"card.watchHint": "When on, automatically watches rule files for changes and applies them instantly without manual reload.",
|
|
209
242
|
"card.networkRebindNote": "Takes effect immediately — the proxy rebinds automatically, no plugin reload needed.",
|
|
210
243
|
"card.networkInjectEnv": "Inject proxy environment (networkInjectEnv)",
|
|
211
244
|
"card.networkInjectEnvHint": "Writes HTTP(S)_PROXY / ALL_PROXY for subprocesses (and clears NO_PROXY). Off = the listener runs but no process environment is changed.",
|
|
@@ -218,6 +251,7 @@ window.__ModuleLoader__.load({
|
|
|
218
251
|
"history.tag.manualApproved": "Approved",
|
|
219
252
|
"history.tag.manualRejected": "Rejected",
|
|
220
253
|
"history.tag.manualCancelled": "Cancelled",
|
|
254
|
+
"history.tag.standDown": "Gate off",
|
|
221
255
|
"history.learnedProgress": "learned %n/%t",
|
|
222
256
|
"history.snapshots": "diff snapshots",
|
|
223
257
|
"history.clearSession": "Clear this session",
|
|
@@ -240,7 +274,41 @@ window.__ModuleLoader__.load({
|
|
|
240
274
|
"history.diffChipTitle": "View this file’s change comparison",
|
|
241
275
|
"history.diffStats": "+%a / -%r lines changed · %c unchanged",
|
|
242
276
|
"history.diffGone": " (file no longer exists)",
|
|
243
|
-
"history.diffHidden": "%n unmodified lines"
|
|
277
|
+
"history.diffHidden": "%n unmodified lines",
|
|
278
|
+
"card.dryRun": "Rule test",
|
|
279
|
+
"card.dryRunHint": "Judge one call against the rules in force. Runs no tool and writes no rule — check a rule here before you change it.",
|
|
280
|
+
"card.dryRunTool": "Tool",
|
|
281
|
+
"card.dryRunCommand": "Command / arguments",
|
|
282
|
+
"card.dryRunCommandPlaceholder": "git push --force origin main",
|
|
283
|
+
"card.dryRunTest": "Test",
|
|
284
|
+
"card.dryRunRunning": "Judging…",
|
|
285
|
+
"card.dryRunEmpty": "Nothing tested yet. Fill in a tool and command, then press Test to see how it would be judged.",
|
|
286
|
+
"card.dryRunVerdict": "Verdict",
|
|
287
|
+
"card.dryRunAllow": "allow",
|
|
288
|
+
"card.dryRunAsk": "ask",
|
|
289
|
+
"card.dryRunDeny": "deny",
|
|
290
|
+
"card.dryRunRule": "Matched rule",
|
|
291
|
+
"card.dryRunRuleNone": "no rule matched; defaultAction: %d",
|
|
292
|
+
"card.dryRunDimensions": "Dimensions",
|
|
293
|
+
"card.dryRunReason": "Reason",
|
|
294
|
+
"card.dryRunNote": "\"Matched rule\" comes from the rule layer. When the verdict was produced by an earlier hard-deny or by the keyword blacklist there is no rule index — the dimensions listed are then informational only.",
|
|
295
|
+
"card.dryRunNoRules": "(0 rules loaded — check the rulesFile path)",
|
|
296
|
+
"card.dryRunFail": "Test failed: ",
|
|
297
|
+
"card.dryRunStale": "route missing (plugin not restarted?)",
|
|
298
|
+
"card.rules": "Permissions YAML",
|
|
299
|
+
"card.rulesHint": "The rules file the gate is loading right now, verbatim. Read-only — nothing here can change it. To change rules, use the allowlist below or edit the file directly.",
|
|
300
|
+
"card.rulesRefresh": "Refresh",
|
|
301
|
+
"card.rulesLoading": "Reading…",
|
|
302
|
+
"card.rulesPath": "File",
|
|
303
|
+
"card.rulesNone": "no rules file configured",
|
|
304
|
+
"card.rulesCounts": "Rules",
|
|
305
|
+
"card.rulesDefault": "defaultAction",
|
|
306
|
+
"card.rulesStats": "%b bytes · %l lines",
|
|
307
|
+
"card.rulesMissing": "The file does not exist. The gate falls back to its built-in defaults.",
|
|
308
|
+
"card.rulesTruncated": "File is large; showing the first 512 KB only.",
|
|
309
|
+
"card.rulesError": "Load failed: ",
|
|
310
|
+
"card.rulesStale": "route missing (plugin not restarted?)",
|
|
311
|
+
"card.rulesNote": "Read-only: this panel cannot modify the file. The allowlist section is the only place that writes to it."
|
|
244
312
|
};
|
|
245
313
|
//#endregion
|
|
246
314
|
//#region src/deny-defaults.ts
|
|
@@ -309,6 +377,19 @@ window.__ModuleLoader__.load({
|
|
|
309
377
|
}
|
|
310
378
|
];
|
|
311
379
|
//#endregion
|
|
380
|
+
//#region src/preset.ts
|
|
381
|
+
/**
|
|
382
|
+
* Presets in which the gate is active. It owns both 自动审查 tiers — the plain
|
|
383
|
+
* one (file sandbox kept) and the full-access one (sandbox restriction lifted,
|
|
384
|
+
* same approval behaviour) — so a user can have the gate without inheriting a
|
|
385
|
+
* sandbox that breaks `git` / Cygwin tools. `'*'` makes it global (every
|
|
386
|
+
* preset, including the hard-deny layer).
|
|
387
|
+
*
|
|
388
|
+
* Lives in this dependency-free module (not `config.ts`) so the browser half can
|
|
389
|
+
* render the scope without pulling schemastery into the client bundle.
|
|
390
|
+
*/
|
|
391
|
+
const DEFAULT_GATE_PRESETS = ["permissive", "permissive-full"];
|
|
392
|
+
//#endregion
|
|
312
393
|
//#region src/client/sediment.tsx
|
|
313
394
|
/**
|
|
314
395
|
* Learning-sediment management section — the settings-card face of verdict
|
|
@@ -320,7 +401,6 @@ window.__ModuleLoader__.load({
|
|
|
320
401
|
* learning.json store the gate reads); actions POST back. Best-effort: a
|
|
321
402
|
* missing route renders the empty state, never an error.
|
|
322
403
|
*/
|
|
323
|
-
const POLL_MS$2 = 5e3;
|
|
324
404
|
const itemStyle = {
|
|
325
405
|
display: "flex",
|
|
326
406
|
alignItems: "center",
|
|
@@ -350,6 +430,8 @@ window.__ModuleLoader__.load({
|
|
|
350
430
|
};
|
|
351
431
|
function SedimentSection({ t }) {
|
|
352
432
|
const [body, setBody] = (0, react.useState)(null);
|
|
433
|
+
/** Latest loader, so a mutation can refresh the list without owning the fetch. */
|
|
434
|
+
const reloadRef = (0, react.useRef)(() => {});
|
|
353
435
|
(0, react.useEffect)(() => {
|
|
354
436
|
let alive = true;
|
|
355
437
|
const load = () => {
|
|
@@ -359,11 +441,18 @@ window.__ModuleLoader__.load({
|
|
|
359
441
|
if (alive) setBody({});
|
|
360
442
|
});
|
|
361
443
|
};
|
|
444
|
+
reloadRef.current = load;
|
|
362
445
|
load();
|
|
363
|
-
const
|
|
446
|
+
const onVisible = () => {
|
|
447
|
+
if (document.visibilityState === "visible") load();
|
|
448
|
+
};
|
|
449
|
+
window.addEventListener("focus", onVisible);
|
|
450
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
364
451
|
return () => {
|
|
365
452
|
alive = false;
|
|
366
|
-
|
|
453
|
+
reloadRef.current = () => {};
|
|
454
|
+
window.removeEventListener("focus", onVisible);
|
|
455
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
367
456
|
};
|
|
368
457
|
}, []);
|
|
369
458
|
const post = (payload) => {
|
|
@@ -371,6 +460,8 @@ window.__ModuleLoader__.load({
|
|
|
371
460
|
method: "POST",
|
|
372
461
|
headers: { "content-type": "application/json" },
|
|
373
462
|
body: JSON.stringify(payload)
|
|
463
|
+
}).then(() => {
|
|
464
|
+
reloadRef.current();
|
|
374
465
|
}).catch(() => {});
|
|
375
466
|
};
|
|
376
467
|
const threshold = body?.threshold ?? 3;
|
|
@@ -559,6 +650,67 @@ window.__ModuleLoader__.load({
|
|
|
559
650
|
const [bulkOpen, setBulkOpen] = (0, react.useState)(false);
|
|
560
651
|
const [healthBusy, setHealthBusy] = (0, react.useState)(false);
|
|
561
652
|
const [healthResult, setHealthResult] = (0, react.useState)(null);
|
|
653
|
+
const [dryTool, setDryTool] = (0, react.useState)("shell");
|
|
654
|
+
const [dryCommand, setDryCommand] = (0, react.useState)("");
|
|
655
|
+
const [dryBusy, setDryBusy] = (0, react.useState)(false);
|
|
656
|
+
const [dryReport, setDryReport] = (0, react.useState)(null);
|
|
657
|
+
const [dryError, setDryError] = (0, react.useState)(null);
|
|
658
|
+
const runDryRunTest = () => {
|
|
659
|
+
const tool = dryTool.trim() === "" ? "shell" : dryTool.trim();
|
|
660
|
+
const command = dryCommand.trim();
|
|
661
|
+
setDryBusy(true);
|
|
662
|
+
setDryError(null);
|
|
663
|
+
setDryReport(null);
|
|
664
|
+
fetch("/api/dsh-perm-gate/dry-run", {
|
|
665
|
+
method: "POST",
|
|
666
|
+
headers: { "content-type": "application/json" },
|
|
667
|
+
body: JSON.stringify({
|
|
668
|
+
tool,
|
|
669
|
+
args: command === "" ? {} : { command }
|
|
670
|
+
})
|
|
671
|
+
}).then(async (r) => {
|
|
672
|
+
const text = await r.text();
|
|
673
|
+
if (r.status === 404) throw new Error(t("card.dryRunStale"));
|
|
674
|
+
try {
|
|
675
|
+
return JSON.parse(text);
|
|
676
|
+
} catch {
|
|
677
|
+
throw new Error(text.trim().slice(0, 120) !== "" ? text.trim().slice(0, 120) : `HTTP ${r.status}`);
|
|
678
|
+
}
|
|
679
|
+
}).then((res) => {
|
|
680
|
+
if (res.ok === true && res.result !== void 0) setDryReport(res.result);
|
|
681
|
+
else setDryError(res.error ?? t("card.dryRunFail"));
|
|
682
|
+
}).catch((e) => {
|
|
683
|
+
setDryError(String(e?.message ?? e));
|
|
684
|
+
}).finally(() => {
|
|
685
|
+
setDryBusy(false);
|
|
686
|
+
});
|
|
687
|
+
};
|
|
688
|
+
const [rulesBody, setRulesBody] = (0, react.useState)(null);
|
|
689
|
+
const [rulesBusy, setRulesBusy] = (0, react.useState)(false);
|
|
690
|
+
const [rulesError, setRulesError] = (0, react.useState)(null);
|
|
691
|
+
const loadRules = () => {
|
|
692
|
+
setRulesBusy(true);
|
|
693
|
+
setRulesError(null);
|
|
694
|
+
fetch("/api/dsh-perm-gate/rules", { headers: { "cache-control": "no-cache" } }).then(async (r) => {
|
|
695
|
+
const text = await r.text();
|
|
696
|
+
if (r.status === 404) throw new Error(t("card.rulesStale"));
|
|
697
|
+
try {
|
|
698
|
+
return JSON.parse(text);
|
|
699
|
+
} catch {
|
|
700
|
+
throw new Error(text.trim().slice(0, 120) !== "" ? text.trim().slice(0, 120) : `HTTP ${r.status}`);
|
|
701
|
+
}
|
|
702
|
+
}).then((res) => {
|
|
703
|
+
if (res.ok === true && res.result !== void 0) setRulesBody(res.result);
|
|
704
|
+
else setRulesError(res.error ?? t("card.rulesError"));
|
|
705
|
+
}).catch((e) => {
|
|
706
|
+
setRulesError(String(e?.message ?? e));
|
|
707
|
+
}).finally(() => {
|
|
708
|
+
setRulesBusy(false);
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
(0, react.useEffect)(() => {
|
|
712
|
+
loadRules();
|
|
713
|
+
}, []);
|
|
562
714
|
const receiverSource = (value.classifierSource ?? "custom") === "host" ? "host" : "custom";
|
|
563
715
|
const [receiverInfo, setReceiverInfo] = (0, react.useState)(null);
|
|
564
716
|
const [receiverNonce, setReceiverNonce] = (0, react.useState)(0);
|
|
@@ -696,6 +848,10 @@ window.__ModuleLoader__.load({
|
|
|
696
848
|
style: hintStyle,
|
|
697
849
|
children: t("card.permissiveHint")
|
|
698
850
|
}),
|
|
851
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
852
|
+
style: hintStyle,
|
|
853
|
+
children: t("card.scopeNote").replace("%scope", ((value.gatePresets ?? []).length > 0 ? value.gatePresets : [...DEFAULT_GATE_PRESETS]).join(" / "))
|
|
854
|
+
}),
|
|
699
855
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
700
856
|
style: sectionStyle,
|
|
701
857
|
children: [
|
|
@@ -1115,6 +1271,325 @@ window.__ModuleLoader__.load({
|
|
|
1115
1271
|
}) : null
|
|
1116
1272
|
]
|
|
1117
1273
|
}),
|
|
1274
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1275
|
+
style: sectionStyle,
|
|
1276
|
+
children: [
|
|
1277
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1278
|
+
style: labelStyle,
|
|
1279
|
+
children: t("card.dryRun")
|
|
1280
|
+
}),
|
|
1281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1282
|
+
style: hintStyle,
|
|
1283
|
+
children: t("card.dryRunHint")
|
|
1284
|
+
}),
|
|
1285
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1286
|
+
style: {
|
|
1287
|
+
display: "flex",
|
|
1288
|
+
flexWrap: "wrap",
|
|
1289
|
+
gap: "8px",
|
|
1290
|
+
alignItems: "flex-end",
|
|
1291
|
+
margin: "6px 0"
|
|
1292
|
+
},
|
|
1293
|
+
children: [
|
|
1294
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1295
|
+
style: {
|
|
1296
|
+
display: "flex",
|
|
1297
|
+
flexDirection: "column",
|
|
1298
|
+
gap: "4px",
|
|
1299
|
+
flex: "0 0 120px"
|
|
1300
|
+
},
|
|
1301
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1302
|
+
style: fieldLabelStyle,
|
|
1303
|
+
children: t("card.dryRunTool")
|
|
1304
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1305
|
+
type: "text",
|
|
1306
|
+
value: dryTool,
|
|
1307
|
+
disabled: readonly,
|
|
1308
|
+
placeholder: "shell",
|
|
1309
|
+
onChange: (e) => {
|
|
1310
|
+
setDryTool(e.target.value);
|
|
1311
|
+
},
|
|
1312
|
+
onKeyDown: (e) => {
|
|
1313
|
+
if (e.key === "Enter") runDryRunTest();
|
|
1314
|
+
},
|
|
1315
|
+
style: controlStyle
|
|
1316
|
+
})]
|
|
1317
|
+
}),
|
|
1318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1319
|
+
style: {
|
|
1320
|
+
display: "flex",
|
|
1321
|
+
flexDirection: "column",
|
|
1322
|
+
gap: "4px",
|
|
1323
|
+
flex: "1 1 260px",
|
|
1324
|
+
minWidth: 0
|
|
1325
|
+
},
|
|
1326
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1327
|
+
style: fieldLabelStyle,
|
|
1328
|
+
children: t("card.dryRunCommand")
|
|
1329
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1330
|
+
type: "text",
|
|
1331
|
+
value: dryCommand,
|
|
1332
|
+
disabled: readonly,
|
|
1333
|
+
placeholder: t("card.dryRunCommandPlaceholder"),
|
|
1334
|
+
onChange: (e) => {
|
|
1335
|
+
setDryCommand(e.target.value);
|
|
1336
|
+
},
|
|
1337
|
+
onKeyDown: (e) => {
|
|
1338
|
+
if (e.key === "Enter") runDryRunTest();
|
|
1339
|
+
},
|
|
1340
|
+
style: controlStyle
|
|
1341
|
+
})]
|
|
1342
|
+
}),
|
|
1343
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1344
|
+
type: "button",
|
|
1345
|
+
disabled: readonly || dryBusy,
|
|
1346
|
+
onClick: runDryRunTest,
|
|
1347
|
+
style: {
|
|
1348
|
+
...controlStyle,
|
|
1349
|
+
flex: "0 0 auto",
|
|
1350
|
+
cursor: readonly || dryBusy ? "default" : "pointer"
|
|
1351
|
+
},
|
|
1352
|
+
children: dryBusy ? t("card.dryRunRunning") : t("card.dryRunTest")
|
|
1353
|
+
})
|
|
1354
|
+
]
|
|
1355
|
+
}),
|
|
1356
|
+
dryError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1357
|
+
style: {
|
|
1358
|
+
...hintStyle,
|
|
1359
|
+
color: "var(--dsw-alias-label-error, #c0392b)"
|
|
1360
|
+
},
|
|
1361
|
+
children: [t("card.dryRunFail"), dryError]
|
|
1362
|
+
}),
|
|
1363
|
+
dryReport === null ? dryError === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1364
|
+
style: hintStyle,
|
|
1365
|
+
children: t("card.dryRunEmpty")
|
|
1366
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1367
|
+
style: {
|
|
1368
|
+
display: "flex",
|
|
1369
|
+
flexDirection: "column",
|
|
1370
|
+
gap: "4px",
|
|
1371
|
+
padding: "8px 10px",
|
|
1372
|
+
borderRadius: "6px",
|
|
1373
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1374
|
+
background: "var(--dsw-alias-bg-surface, transparent)",
|
|
1375
|
+
fontSize: "12px",
|
|
1376
|
+
lineHeight: "18px"
|
|
1377
|
+
},
|
|
1378
|
+
children: [
|
|
1379
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1380
|
+
style: {
|
|
1381
|
+
display: "flex",
|
|
1382
|
+
alignItems: "center",
|
|
1383
|
+
gap: "8px"
|
|
1384
|
+
},
|
|
1385
|
+
children: [
|
|
1386
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1387
|
+
style: fieldLabelStyle,
|
|
1388
|
+
children: t("card.dryRunVerdict")
|
|
1389
|
+
}),
|
|
1390
|
+
(() => {
|
|
1391
|
+
const verdict = dryReport.verdict ?? "ask";
|
|
1392
|
+
const colour = verdict === "deny" ? "var(--dsw-alias-label-error, #c0392b)" : verdict === "allow" ? "var(--dsw-alias-label-success, #2e7d32)" : "var(--dsw-alias-label-warning, #b26a00)";
|
|
1393
|
+
const text = verdict === "deny" ? t("card.dryRunDeny") : verdict === "allow" ? t("card.dryRunAllow") : t("card.dryRunAsk");
|
|
1394
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
1395
|
+
style: { color: colour },
|
|
1396
|
+
children: text
|
|
1397
|
+
});
|
|
1398
|
+
})(),
|
|
1399
|
+
(dryReport.ruleCount ?? 0) === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1400
|
+
style: { color: "var(--dsw-alias-label-secondary, inherit)" },
|
|
1401
|
+
children: t("card.dryRunNoRules")
|
|
1402
|
+
})
|
|
1403
|
+
]
|
|
1404
|
+
}),
|
|
1405
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1406
|
+
style: {
|
|
1407
|
+
display: "flex",
|
|
1408
|
+
gap: "8px",
|
|
1409
|
+
overflowWrap: "anywhere"
|
|
1410
|
+
},
|
|
1411
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1412
|
+
style: fieldLabelStyle,
|
|
1413
|
+
children: t("card.dryRunRule")
|
|
1414
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1415
|
+
style: {
|
|
1416
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1417
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1418
|
+
},
|
|
1419
|
+
children: dryReport.ruleLayer?.ruleIndex === void 0 ? t("card.dryRunRuleNone").replace("%d", dryReport.defaultAction ?? "ask") : `#${dryReport.ruleLayer.ruleIndex} (${dryReport.ruleLayer.action ?? "?"})`
|
|
1420
|
+
})]
|
|
1421
|
+
}),
|
|
1422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1423
|
+
style: {
|
|
1424
|
+
display: "flex",
|
|
1425
|
+
gap: "8px",
|
|
1426
|
+
overflowWrap: "anywhere"
|
|
1427
|
+
},
|
|
1428
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1429
|
+
style: fieldLabelStyle,
|
|
1430
|
+
children: t("card.dryRunDimensions")
|
|
1431
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1432
|
+
style: {
|
|
1433
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1434
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1435
|
+
},
|
|
1436
|
+
children: (dryReport.ruleLayer?.matchedDimensions ?? []).length === 0 ? "—" : (dryReport.ruleLayer?.matchedDimensions ?? []).join(", ")
|
|
1437
|
+
})]
|
|
1438
|
+
}),
|
|
1439
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1440
|
+
style: {
|
|
1441
|
+
display: "flex",
|
|
1442
|
+
gap: "8px",
|
|
1443
|
+
overflowWrap: "anywhere"
|
|
1444
|
+
},
|
|
1445
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1446
|
+
style: fieldLabelStyle,
|
|
1447
|
+
children: t("card.dryRunReason")
|
|
1448
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1449
|
+
style: { color: "var(--dsw-alias-label-secondary, inherit)" },
|
|
1450
|
+
children: dryReport.reason ?? ""
|
|
1451
|
+
})]
|
|
1452
|
+
}),
|
|
1453
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1454
|
+
style: {
|
|
1455
|
+
...hintStyle,
|
|
1456
|
+
margin: 0
|
|
1457
|
+
},
|
|
1458
|
+
children: t("card.dryRunNote")
|
|
1459
|
+
})
|
|
1460
|
+
]
|
|
1461
|
+
})
|
|
1462
|
+
]
|
|
1463
|
+
}),
|
|
1464
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1465
|
+
style: sectionStyle,
|
|
1466
|
+
children: [
|
|
1467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1468
|
+
style: {
|
|
1469
|
+
display: "flex",
|
|
1470
|
+
alignItems: "center",
|
|
1471
|
+
justifyContent: "space-between",
|
|
1472
|
+
gap: "8px"
|
|
1473
|
+
},
|
|
1474
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1475
|
+
style: labelStyle,
|
|
1476
|
+
children: t("card.rules")
|
|
1477
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1478
|
+
type: "button",
|
|
1479
|
+
disabled: rulesBusy,
|
|
1480
|
+
onClick: loadRules,
|
|
1481
|
+
style: {
|
|
1482
|
+
...controlStyle,
|
|
1483
|
+
flex: "0 0 auto",
|
|
1484
|
+
cursor: rulesBusy ? "default" : "pointer"
|
|
1485
|
+
},
|
|
1486
|
+
children: rulesBusy ? t("card.rulesLoading") : t("card.rulesRefresh")
|
|
1487
|
+
})]
|
|
1488
|
+
}),
|
|
1489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1490
|
+
style: hintStyle,
|
|
1491
|
+
children: t("card.rulesHint")
|
|
1492
|
+
}),
|
|
1493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1494
|
+
style: {
|
|
1495
|
+
display: "flex",
|
|
1496
|
+
gap: "8px",
|
|
1497
|
+
overflowWrap: "anywhere",
|
|
1498
|
+
marginTop: "6px"
|
|
1499
|
+
},
|
|
1500
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1501
|
+
style: fieldLabelStyle,
|
|
1502
|
+
children: t("card.rulesPath")
|
|
1503
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1504
|
+
style: {
|
|
1505
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1506
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1507
|
+
},
|
|
1508
|
+
children: rulesBody?.path !== void 0 && rulesBody.path !== "" ? rulesBody.path : t("card.rulesNone")
|
|
1509
|
+
})]
|
|
1510
|
+
}),
|
|
1511
|
+
rulesError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1512
|
+
style: {
|
|
1513
|
+
...hintStyle,
|
|
1514
|
+
color: "var(--dsw-alias-label-error, #c0392b)"
|
|
1515
|
+
},
|
|
1516
|
+
children: [t("card.rulesError"), rulesError]
|
|
1517
|
+
}),
|
|
1518
|
+
rulesBody !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1519
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1520
|
+
style: {
|
|
1521
|
+
display: "flex",
|
|
1522
|
+
flexWrap: "wrap",
|
|
1523
|
+
gap: "12px",
|
|
1524
|
+
marginTop: "4px"
|
|
1525
|
+
},
|
|
1526
|
+
children: [
|
|
1527
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1528
|
+
style: fieldLabelStyle,
|
|
1529
|
+
children: [t("card.rulesCounts"), " "]
|
|
1530
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1531
|
+
style: {
|
|
1532
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1533
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1534
|
+
},
|
|
1535
|
+
children: `deny ${rulesBody.counts?.deny ?? 0} · allow ${rulesBody.counts?.allow ?? 0} · ask ${rulesBody.counts?.ask ?? 0}`
|
|
1536
|
+
})] }),
|
|
1537
|
+
rulesBody.defaultAction !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1538
|
+
style: fieldLabelStyle,
|
|
1539
|
+
children: [t("card.rulesDefault"), " "]
|
|
1540
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1541
|
+
style: {
|
|
1542
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1543
|
+
color: "var(--dsw-alias-label-primary)"
|
|
1544
|
+
},
|
|
1545
|
+
children: rulesBody.defaultAction
|
|
1546
|
+
})] }),
|
|
1547
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1548
|
+
style: fieldLabelStyle,
|
|
1549
|
+
children: t("card.rulesStats").replace("%b", String(rulesBody.bytes ?? 0)).replace("%l", String(rulesBody.lines ?? 0))
|
|
1550
|
+
})
|
|
1551
|
+
]
|
|
1552
|
+
}),
|
|
1553
|
+
rulesBody.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1554
|
+
style: {
|
|
1555
|
+
...hintStyle,
|
|
1556
|
+
color: "var(--dsw-alias-label-error, #c0392b)"
|
|
1557
|
+
},
|
|
1558
|
+
children: rulesBody.error
|
|
1559
|
+
}),
|
|
1560
|
+
rulesBody.exists === false && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1561
|
+
style: hintStyle,
|
|
1562
|
+
children: t("card.rulesMissing")
|
|
1563
|
+
}),
|
|
1564
|
+
rulesBody.truncated === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1565
|
+
style: hintStyle,
|
|
1566
|
+
children: t("card.rulesTruncated")
|
|
1567
|
+
}),
|
|
1568
|
+
(rulesBody.raw ?? "") !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1569
|
+
style: {
|
|
1570
|
+
margin: "6px 0 0",
|
|
1571
|
+
padding: "8px 10px",
|
|
1572
|
+
maxHeight: "260px",
|
|
1573
|
+
overflow: "auto",
|
|
1574
|
+
borderRadius: "6px",
|
|
1575
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
1576
|
+
background: "var(--dsw-alias-bg-surface, transparent)",
|
|
1577
|
+
font: "500 12px/18px var(--ds-font-family-code, monospace)",
|
|
1578
|
+
color: "var(--dsw-alias-label-primary)",
|
|
1579
|
+
whiteSpace: "pre"
|
|
1580
|
+
},
|
|
1581
|
+
children: rulesBody.raw
|
|
1582
|
+
}),
|
|
1583
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1584
|
+
style: {
|
|
1585
|
+
...hintStyle,
|
|
1586
|
+
margin: 0
|
|
1587
|
+
},
|
|
1588
|
+
children: t("card.rulesNote")
|
|
1589
|
+
})
|
|
1590
|
+
] })
|
|
1591
|
+
]
|
|
1592
|
+
}),
|
|
1118
1593
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1119
1594
|
style: sectionStyle,
|
|
1120
1595
|
children: [
|
|
@@ -1454,7 +1929,7 @@ window.__ModuleLoader__.load({
|
|
|
1454
1929
|
})
|
|
1455
1930
|
]
|
|
1456
1931
|
}),
|
|
1457
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1932
|
+
value.networkEnabled === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1458
1933
|
style: sectionStyle,
|
|
1459
1934
|
children: [
|
|
1460
1935
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -1485,39 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1485
1960
|
})]
|
|
1486
1961
|
})
|
|
1487
1962
|
]
|
|
1488
|
-
}),
|
|
1489
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1490
|
-
style: sectionStyle,
|
|
1491
|
-
children: [
|
|
1492
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1493
|
-
style: labelStyle,
|
|
1494
|
-
children: t("card.watch")
|
|
1495
|
-
}),
|
|
1496
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1497
|
-
style: hintStyle,
|
|
1498
|
-
children: t("card.watchHint")
|
|
1499
|
-
}),
|
|
1500
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1501
|
-
style: {
|
|
1502
|
-
display: "flex",
|
|
1503
|
-
alignItems: "center",
|
|
1504
|
-
gap: "8px",
|
|
1505
|
-
marginTop: "6px"
|
|
1506
|
-
},
|
|
1507
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1508
|
-
type: "checkbox",
|
|
1509
|
-
checked: value.watch !== false,
|
|
1510
|
-
disabled: readonly,
|
|
1511
|
-
onChange: (event) => {
|
|
1512
|
-
scope.set("watch", event.currentTarget.checked);
|
|
1513
|
-
}
|
|
1514
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1515
|
-
style: { fontSize: "13px" },
|
|
1516
|
-
children: value.watch !== false ? "ON" : "OFF"
|
|
1517
|
-
})]
|
|
1518
|
-
})
|
|
1519
|
-
]
|
|
1520
|
-
}),
|
|
1963
|
+
}) : null,
|
|
1521
1964
|
!snapshot.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1522
1965
|
style: {
|
|
1523
1966
|
margin: "8px 0 0",
|
|
@@ -1540,6 +1983,95 @@ window.__ModuleLoader__.load({
|
|
|
1540
1983
|
const body = await res.json();
|
|
1541
1984
|
return Array.isArray(body.events) ? body.events : [];
|
|
1542
1985
|
}
|
|
1986
|
+
/** The host's push route (see src/events.ts); the browser half prefers it to polling. */
|
|
1987
|
+
const STREAM_ROUTE = "/api/dsh-perm-gate/stream";
|
|
1988
|
+
/** Stream attempts before the route is written off as unsupported and polling takes over. */
|
|
1989
|
+
const STREAM_RETRIES = 3;
|
|
1990
|
+
const STREAM_RETRY_MS = 3e3;
|
|
1991
|
+
/**
|
|
1992
|
+
* Degraded path only: a host predating the stream route still gets a servable
|
|
1993
|
+
* strip. Deliberately slower than the old 2 s tick — it now also pauses while
|
|
1994
|
+
* the tab is hidden, so a backgrounded page costs nothing.
|
|
1995
|
+
*/
|
|
1996
|
+
const FALLBACK_POLL_MS = 5e3;
|
|
1997
|
+
/**
|
|
1998
|
+
* Follow one session's decision feed until the returned close is called.
|
|
1999
|
+
*
|
|
2000
|
+
* Prefers the host's `text/event-stream` route, which pushes each decision as
|
|
2001
|
+
* the gate appends it — no timer, no repeated re-read of the event log. When
|
|
2002
|
+
* the stream is unavailable (an older host, a proxy that buffers it) this
|
|
2003
|
+
* degrades to polling the JSON route, gated on page visibility.
|
|
2004
|
+
*
|
|
2005
|
+
* @param sessionId - the session whose decisions to follow.
|
|
2006
|
+
* @param getSince - cursor supplier, read at connect and on every fallback poll.
|
|
2007
|
+
* @param onBatch - receives the backlog, then live batches.
|
|
2008
|
+
* @returns the close: ends the stream and any fallback timer.
|
|
2009
|
+
*/
|
|
2010
|
+
function followEvents(sessionId, getSince, onBatch) {
|
|
2011
|
+
let closed = false;
|
|
2012
|
+
let source = null;
|
|
2013
|
+
let pollTimer = null;
|
|
2014
|
+
let retries = 0;
|
|
2015
|
+
let polls = 0;
|
|
2016
|
+
const stopPolling = () => {
|
|
2017
|
+
if (pollTimer === null) return;
|
|
2018
|
+
clearInterval(pollTimer);
|
|
2019
|
+
pollTimer = null;
|
|
2020
|
+
};
|
|
2021
|
+
const poll = () => {
|
|
2022
|
+
if (closed) return;
|
|
2023
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
2024
|
+
const backlog = polls === 0;
|
|
2025
|
+
polls += 1;
|
|
2026
|
+
fetchEvents(sessionId, getSince()).then((events) => {
|
|
2027
|
+
if (!closed) onBatch({
|
|
2028
|
+
events,
|
|
2029
|
+
backlog
|
|
2030
|
+
});
|
|
2031
|
+
}).catch(() => {});
|
|
2032
|
+
};
|
|
2033
|
+
const startPolling = () => {
|
|
2034
|
+
if (closed || pollTimer !== null) return;
|
|
2035
|
+
polls = 0;
|
|
2036
|
+
poll();
|
|
2037
|
+
pollTimer = setInterval(poll, FALLBACK_POLL_MS);
|
|
2038
|
+
};
|
|
2039
|
+
const connect = () => {
|
|
2040
|
+
if (closed) return;
|
|
2041
|
+
const query = `?sessionId=${encodeURIComponent(sessionId)}&since=${getSince()}`;
|
|
2042
|
+
const next = new EventSource(`${STREAM_ROUTE}${query}`);
|
|
2043
|
+
source = next;
|
|
2044
|
+
next.onopen = () => {
|
|
2045
|
+
retries = 0;
|
|
2046
|
+
stopPolling();
|
|
2047
|
+
};
|
|
2048
|
+
next.onmessage = (message) => {
|
|
2049
|
+
try {
|
|
2050
|
+
const body = JSON.parse(message.data);
|
|
2051
|
+
if (Array.isArray(body.events)) onBatch({
|
|
2052
|
+
events: body.events,
|
|
2053
|
+
backlog: body.backlog === true
|
|
2054
|
+
});
|
|
2055
|
+
} catch {}
|
|
2056
|
+
};
|
|
2057
|
+
next.onerror = () => {
|
|
2058
|
+
if (source === next) source = null;
|
|
2059
|
+
next.close();
|
|
2060
|
+
if (closed) return;
|
|
2061
|
+
retries += 1;
|
|
2062
|
+
startPolling();
|
|
2063
|
+
if (retries <= STREAM_RETRIES) setTimeout(connect, STREAM_RETRY_MS);
|
|
2064
|
+
};
|
|
2065
|
+
};
|
|
2066
|
+
if (typeof EventSource === "undefined") startPolling();
|
|
2067
|
+
else connect();
|
|
2068
|
+
return () => {
|
|
2069
|
+
closed = true;
|
|
2070
|
+
source?.close();
|
|
2071
|
+
source = null;
|
|
2072
|
+
stopPolling();
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
1543
2075
|
/** Resolve the current session id from slot props (top level, nested, or hook). */
|
|
1544
2076
|
function resolveSessionId(props) {
|
|
1545
2077
|
const direct = props?.sessionId;
|
|
@@ -1594,6 +2126,12 @@ window.__ModuleLoader__.load({
|
|
|
1594
2126
|
tag: "CANCELLED",
|
|
1595
2127
|
sticky: false
|
|
1596
2128
|
};
|
|
2129
|
+
case "stand-down": return {
|
|
2130
|
+
color: "var(--dsw-alias-state-warn-label, #b9770e)",
|
|
2131
|
+
bg: "var(--dsw-alias-state-warn-tertiary, rgba(185,119,14,0.14))",
|
|
2132
|
+
tag: "GATE OFF",
|
|
2133
|
+
sticky: true
|
|
2134
|
+
};
|
|
1597
2135
|
default: return {
|
|
1598
2136
|
color: "var(--dsw-alias-state-success-primary, #1e8449)",
|
|
1599
2137
|
bg: "var(--dsw-alias-state-success-tertiary, rgba(30,132,73,0.08))",
|
|
@@ -1623,7 +2161,9 @@ window.__ModuleLoader__.load({
|
|
|
1623
2161
|
"human-approved": "人工通过",
|
|
1624
2162
|
"human-rejected": "人工拒绝",
|
|
1625
2163
|
"human-cancelled": "人工取消",
|
|
1626
|
-
"no-approval-channel": "无审批通道"
|
|
2164
|
+
"no-approval-channel": "无审批通道",
|
|
2165
|
+
"preset-passthrough": "审批策略 never · 已放行",
|
|
2166
|
+
"stand-down": "门禁停用"
|
|
1627
2167
|
};
|
|
1628
2168
|
/** Compact wall-clock rendering of an ISO timestamp. */
|
|
1629
2169
|
function clockTime(iso) {
|
|
@@ -1718,15 +2258,15 @@ window.__ModuleLoader__.load({
|
|
|
1718
2258
|
* dsh-perm-gate (a simplified version of the review-feed pattern demonstrated
|
|
1719
2259
|
* by dsh-approval-gate).
|
|
1720
2260
|
*
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
2261
|
+
* Follows the host's decision feed (`GET /api/dsh-perm-gate/stream`, an SSE
|
|
2262
|
+
* push; see `followEvents` for the older-host polling fallback) and shows the
|
|
2263
|
+
* latest decision above the conversation input: auto-allows (green) and denies
|
|
2264
|
+
* (red) auto-dismiss after a few seconds; an ask (amber) stays until the next
|
|
2265
|
+
* event because it needs the human's attention.
|
|
1725
2266
|
*
|
|
1726
2267
|
* Everything is best-effort: no session id, a missing route, or any fetch
|
|
1727
2268
|
* failure simply means "render nothing". No @deepseek-ai value imports.
|
|
1728
2269
|
*/
|
|
1729
|
-
const POLL_MS$1 = 2e3;
|
|
1730
2270
|
const AUTO_HIDE_MS = 4e3;
|
|
1731
2271
|
/** Localized tag for a manual terminal state; other kinds keep their short tag. */
|
|
1732
2272
|
function manualTagKey(kind) {
|
|
@@ -1747,35 +2287,27 @@ window.__ModuleLoader__.load({
|
|
|
1747
2287
|
setNotice(null);
|
|
1748
2288
|
if (sessionId === null) return;
|
|
1749
2289
|
let alive = true;
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
const last = events[events.length - 1];
|
|
1757
|
-
if (last === void 0 || last.id <= shownIdRef.current) return;
|
|
1758
|
-
shownIdRef.current = last.id;
|
|
1759
|
-
sinceRef.current = last.id;
|
|
1760
|
-
setNotice(last);
|
|
1761
|
-
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
1762
|
-
if (!presentation(last.kind).sticky) hideTimerRef.current = setTimeout(() => {
|
|
1763
|
-
setNotice(null);
|
|
1764
|
-
}, AUTO_HIDE_MS);
|
|
1765
|
-
}).catch(() => {});
|
|
1766
|
-
}, POLL_MS$1);
|
|
1767
|
-
};
|
|
1768
|
-
fetchEvents(sessionId, 0).then((events) => {
|
|
1769
|
-
if (!alive) return;
|
|
1770
|
-
if (events.length > 0) {
|
|
1771
|
-
sinceRef.current = events[events.length - 1]?.id ?? 0;
|
|
2290
|
+
const onBatch = (batch) => {
|
|
2291
|
+
if (!alive || batch.events.length === 0) return;
|
|
2292
|
+
const last = batch.events[batch.events.length - 1];
|
|
2293
|
+
if (last === void 0) return;
|
|
2294
|
+
if (last.id > sinceRef.current) sinceRef.current = last.id;
|
|
2295
|
+
if (batch.backlog) {
|
|
1772
2296
|
shownIdRef.current = sinceRef.current;
|
|
2297
|
+
return;
|
|
1773
2298
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
2299
|
+
if (last.id <= shownIdRef.current) return;
|
|
2300
|
+
shownIdRef.current = last.id;
|
|
2301
|
+
setNotice(last);
|
|
2302
|
+
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
2303
|
+
if (!presentation(last.kind).sticky) hideTimerRef.current = setTimeout(() => {
|
|
2304
|
+
setNotice(null);
|
|
2305
|
+
}, AUTO_HIDE_MS);
|
|
2306
|
+
};
|
|
2307
|
+
const close = followEvents(sessionId, () => sinceRef.current, onBatch);
|
|
1776
2308
|
return () => {
|
|
1777
2309
|
alive = false;
|
|
1778
|
-
|
|
2310
|
+
close();
|
|
1779
2311
|
if (hideTimerRef.current !== null) clearTimeout(hideTimerRef.current);
|
|
1780
2312
|
};
|
|
1781
2313
|
}, [sessionId]);
|
|
@@ -1858,7 +2390,8 @@ window.__ModuleLoader__.load({
|
|
|
1858
2390
|
* Approval-history view — the `conversation.view` face of dsh-perm-gate,
|
|
1859
2391
|
* following the session-scoped review-page pattern demonstrated by
|
|
1860
2392
|
* dsh-approval-gate: a timeline of every gate decision in the current
|
|
1861
|
-
* conversation, newest first,
|
|
2393
|
+
* conversation, newest first, loaded once and then extended by the host's push
|
|
2394
|
+
* feed (see `followEvents`).
|
|
1862
2395
|
*
|
|
1863
2396
|
* Beyond the decision timeline this view owns the review data plane: a snapshot
|
|
1864
2397
|
* inventory bar (with session/all clearing), per-event file chips, and a diff
|
|
@@ -1870,7 +2403,6 @@ window.__ModuleLoader__.load({
|
|
|
1870
2403
|
* routes. Everything is best-effort: no session id or any fetch failure renders
|
|
1871
2404
|
* the empty / error state. No @deepseek-ai value imports.
|
|
1872
2405
|
*/
|
|
1873
|
-
const POLL_MS = 5e3;
|
|
1874
2406
|
/** Tag text per event kind, keyed into the plugin dictionary. */
|
|
1875
2407
|
const TAG_KEYS = {
|
|
1876
2408
|
auto: "history.tag.auto",
|
|
@@ -1879,7 +2411,8 @@ window.__ModuleLoader__.load({
|
|
|
1879
2411
|
learned: "history.tag.learned",
|
|
1880
2412
|
"manual-approved": "history.tag.manualApproved",
|
|
1881
2413
|
"manual-rejected": "history.tag.manualRejected",
|
|
1882
|
-
"manual-cancelled": "history.tag.manualCancelled"
|
|
2414
|
+
"manual-cancelled": "history.tag.manualCancelled",
|
|
2415
|
+
"stand-down": "history.tag.standDown"
|
|
1883
2416
|
};
|
|
1884
2417
|
/** The file name alone (chips show the basename, like the reference page). */
|
|
1885
2418
|
function basename(p) {
|
|
@@ -2283,29 +2816,47 @@ window.__ModuleLoader__.load({
|
|
|
2283
2816
|
setEvents(null);
|
|
2284
2817
|
setError(null);
|
|
2285
2818
|
setStats(null);
|
|
2819
|
+
if (sessionId === null) {
|
|
2820
|
+
setEvents([]);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2286
2823
|
let alive = true;
|
|
2287
|
-
|
|
2824
|
+
let cursor = 0;
|
|
2825
|
+
let closeStream = null;
|
|
2826
|
+
fetchSnapshotStats(sessionId).then((next) => {
|
|
2827
|
+
if (alive) setStats(next);
|
|
2828
|
+
});
|
|
2829
|
+
/**
|
|
2830
|
+
* Decisions landing after the initial load. Both the stream's pushes and the
|
|
2831
|
+
* fallback's ticks carry only events past the cursor, so they merge the same
|
|
2832
|
+
* way; a batch is also the point where the snapshot inventory can have grown.
|
|
2833
|
+
*/
|
|
2834
|
+
const onBatch = (batch) => {
|
|
2835
|
+
if (!alive || batch.events.length === 0) return;
|
|
2836
|
+
for (const ev of batch.events) if (ev.id > cursor) cursor = ev.id;
|
|
2837
|
+
setEvents((current) => {
|
|
2838
|
+
const known = new Set((current ?? []).map((ev) => ev.id));
|
|
2839
|
+
const added = batch.events.filter((ev) => !known.has(ev.id));
|
|
2840
|
+
return added.length === 0 ? current : [...added, ...current ?? []].sort((a, b) => b.id - a.id);
|
|
2841
|
+
});
|
|
2842
|
+
setError(null);
|
|
2288
2843
|
fetchSnapshotStats(sessionId).then((next) => {
|
|
2289
2844
|
if (alive) setStats(next);
|
|
2290
2845
|
});
|
|
2291
|
-
if (sessionId === null) {
|
|
2292
|
-
setEvents([]);
|
|
2293
|
-
return;
|
|
2294
|
-
}
|
|
2295
|
-
fetchEvents(sessionId, 0).then((loaded) => {
|
|
2296
|
-
if (!alive) return;
|
|
2297
|
-
setEvents([...loaded].sort((a, b) => b.id - a.id));
|
|
2298
|
-
setError(null);
|
|
2299
|
-
}).catch((e) => {
|
|
2300
|
-
if (!alive) return;
|
|
2301
|
-
setError(String(e?.message ?? e));
|
|
2302
|
-
});
|
|
2303
2846
|
};
|
|
2304
|
-
|
|
2305
|
-
|
|
2847
|
+
fetchEvents(sessionId, 0).then((loaded) => {
|
|
2848
|
+
if (!alive) return;
|
|
2849
|
+
setEvents([...loaded].sort((a, b) => b.id - a.id));
|
|
2850
|
+
setError(null);
|
|
2851
|
+
cursor = loaded.reduce((max, ev) => ev.id > max ? ev.id : max, 0);
|
|
2852
|
+
}).catch((e) => {
|
|
2853
|
+
if (alive) setError(String(e?.message ?? e));
|
|
2854
|
+
}).finally(() => {
|
|
2855
|
+
if (alive) closeStream = followEvents(sessionId, () => cursor, onBatch);
|
|
2856
|
+
});
|
|
2306
2857
|
return () => {
|
|
2307
2858
|
alive = false;
|
|
2308
|
-
|
|
2859
|
+
closeStream?.();
|
|
2309
2860
|
};
|
|
2310
2861
|
}, [sessionId]);
|
|
2311
2862
|
/** Whether one event has a snapshot covering this file (chip is clickable). */
|