dsh-perm-gate 0.2.1-beta.5 → 1.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 +0 -28
- package/CHANGELOG.ko.md +0 -28
- package/CHANGELOG.md +7 -39
- package/INSTALL.ja.md +7 -14
- package/INSTALL.ko.md +7 -13
- package/INSTALL.md +8 -15
- package/INSTALL.zh.md +7 -13
- package/README.ja.md +13 -22
- package/README.ko.md +14 -23
- package/README.md +19 -26
- package/README.zh.md +12 -17
- package/cordis.patch.yml +3 -15
- package/lib/client.js +117 -7
- package/lib/client.js.map +1 -1
- package/lib/runtime.d.ts +5 -22
- package/lib/runtime.js +8 -57
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
> **Compatibility note:**
|
|
15
|
+
> **Compatibility note:** v1.0.0 ships `ja` / `ko` dictionaries, but official DSH exposes
|
|
16
16
|
> only `zh` / `en` through `LocaleRuntime` (`LOCALE_IDS = ["zh", "en"]`). On stock DSH,
|
|
17
17
|
> selecting `ja` / `ko` fails with `locale "<id>" is not registered`. Use a DSH fork that
|
|
18
18
|
> updates `LOCALE_IDS` (locale-settings.ts) and `LOCALES` labels (client/index.ts), then
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
> **private** method of the user-approval service on both, so it is read behind a
|
|
38
38
|
> `typeof` probe and degrades to “policy unknown” when absent or throwing.
|
|
39
39
|
|
|
40
|
-
Version **0.
|
|
40
|
+
Version **1.0.0** — see the [Changelog](./CHANGELOG.md).
|
|
41
41
|
|
|
42
42
|
A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.
|
|
43
43
|
|
|
@@ -72,11 +72,10 @@ cross-plugin version coupling.
|
|
|
72
72
|
malformed rules, and source-hash compile caching.
|
|
73
73
|
- **Audit** — every decision is logged as an `{ignorable:true}` event with its `callId`; the
|
|
74
74
|
model-visible reason matches the recorded outcome.
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
settings row both show it under the product label 自动审查, with no icon.
|
|
75
|
+
- **Permissive tier** — an **independent approval mode** (separate from read-only, full-access
|
|
76
|
+
and whitelist tiers) that is neither "auto-approve" nor blanket trust. Front-end exposes a
|
|
77
|
+
**single switch** (`permissive`); the four backend strategies are **combinable** and driven
|
|
78
|
+
by plugin settings — still fail-closed against P0.
|
|
80
79
|
- **Sandbox-escalation auto-answer** (`trustEscalation`) — a sandbox escalation is asked from
|
|
81
80
|
*inside* the shell / pwsh / edit tool body, after `tools/pre-execute`, so the gate never saw
|
|
82
81
|
it and a call it auto-allowed still prompted you to approve the widening. With this strategy
|
|
@@ -149,18 +148,12 @@ permissions:
|
|
|
149
148
|
A command entry `word#flag` matches the command word (`word`) with the modifier `recursive` or
|
|
150
149
|
`force` — so `rm#recursive` matches `rm -rf`, `env rm -rf`, and `sh -c "rm -rf /"`.
|
|
151
150
|
|
|
152
|
-
##
|
|
151
|
+
## Permissive mode
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
Read Only / Workspace Write / Full access / Whitelist. It is **not**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
The picker label is a **host-supplied product string**, not a per-locale dictionary entry: DSH
|
|
160
|
-
0.1.2 renders a plugin tier's `name:` verbatim on both permission surfaces (the General-settings
|
|
161
|
-
default row and the composer picker) and only supplies its own localized labels for the three
|
|
162
|
-
built-in values, so `cordis.patch.yml` ships the Chinese label for every session. The tier draws
|
|
163
|
-
**no icon** — the composer renders glyphs only for the built-in values.
|
|
153
|
+
Permissive is an **independent approval tier** in the DSH permission picker, parallel to
|
|
154
|
+
Read Only / Workspace Write / Full access / Whitelist. It is **not** "auto-approval" and never
|
|
155
|
+
mints blanket authority: it only narrows or widens the seam *before* the human/LLM step while
|
|
156
|
+
P0 hard-deny stays monotonic and non-negotiable.
|
|
164
157
|
|
|
165
158
|
In `cordis.yml`:
|
|
166
159
|
|
|
@@ -201,7 +194,7 @@ prompt asking you to approve the sandbox widening.
|
|
|
201
194
|
the host's `callId`, which the escalation request repeats) and answers the escalation
|
|
202
195
|
`allowed-once` itself. It applies only when *all* hold:
|
|
203
196
|
|
|
204
|
-
- the
|
|
197
|
+
- the Permissive tier is on and `trustEscalation` is on;
|
|
205
198
|
- the request carries a `callId` the gate cleared, with a matching tool name;
|
|
206
199
|
- the reason is a recognized escalation naming `workspace-write` or `danger-full-access`.
|
|
207
200
|
|
|
@@ -260,18 +253,18 @@ channel existed), with `tools/result` settling the same ask as a fallback when t
|
|
|
260
253
|
correlate it. Approvals report the post-approval learning progress (`n`/threshold), and the notice
|
|
261
254
|
strip labels all three terminal states.
|
|
262
255
|
|
|
263
|
-
The tier
|
|
264
|
-
|
|
256
|
+
The Permissive tier keeps its shield icon in the permission picker — on both the
|
|
257
|
+
menu item and the collapsed picker trigger.
|
|
265
258
|
|
|
266
259
|
### A selectable session tier
|
|
267
260
|
|
|
268
261
|
`cordis.patch.yml` adds a `permissive` preset (`sandbox: workspace-write`, `approval: ask`, name
|
|
269
|
-
|
|
262
|
+
**Permissive**) between Workspace Write and Full access. The DSH bundle patch replaces the whole
|
|
270
263
|
`permission.config.presets` map rather than merging per key, so the file also restates the three
|
|
271
264
|
built-ins (`read-only` / `workspace-write` / `danger-full-access`, from
|
|
272
265
|
`@deepseek-ai/dsh-base/cordis.patch.yml`); `test/patch-presets.spec.ts` pins that key set. So the
|
|
273
|
-
session permission picker offers
|
|
274
|
-
|
|
266
|
+
session permission picker offers Permissive as an independent selectable approval tier, not an
|
|
267
|
+
"auto-approval" mode.
|
|
275
268
|
|
|
276
269
|
The gate is active **only in the tiers listed in `gatePresets`** (default `['permissive']`, the
|
|
277
270
|
tier this plugin adds). In every other tier — Read Only, Workspace Write, Full access,
|
|
@@ -286,11 +279,11 @@ session's effective approval policy is `never`.
|
|
|
286
279
|
|
|
287
280
|
### Configurable in the UI
|
|
288
281
|
|
|
289
|
-
The tier is also adjustable at runtime from **Settings → Plugins →
|
|
282
|
+
The tier is also adjustable at runtime from **Settings → Plugins → Permissive approval tier**
|
|
290
283
|
(a `settings.plugins.tab` page rendered by the plugin's browser client): one switch toggles
|
|
291
284
|
`permissive`, and four toggles edit the backend `permissiveStrategies`. The host reads the
|
|
292
285
|
namespace live, so a change applies to the next tool call without a restart. This is an
|
|
293
|
-
independent approval class, NOT
|
|
286
|
+
independent approval class, NOT the DSH "auto-approval" mode.
|
|
294
287
|
|
|
295
288
|
## CLI
|
|
296
289
|
|
package/README.zh.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
> **兼容性说明:**
|
|
15
|
+
> **兼容性说明:** v1.0.0 自带 `ja` / `ko` 字典,但官方 DSH 的 `LocaleRuntime`
|
|
16
16
|
> 只暴露 `zh` / `en`(`LOCALE_IDS = ["zh", "en"]`)。在原版 DSH 上选择 `ja` / `ko`
|
|
17
17
|
> 会报 `locale "<id>" is not registered`。请使用更新了 `LOCALE_IDS`
|
|
18
18
|
> (locale-settings.ts)与 `LOCALES` 标签(client/index.ts)的 DSH fork 并重新构建。
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
> 线上都是 user-approval 服务的**私有**方法,因此通过 `typeof` 探测读取,缺失
|
|
36
36
|
> 或抛错时降级为「策略未知」。
|
|
37
37
|
|
|
38
|
-
版本 **0.
|
|
38
|
+
版本 **1.0.0** —— 变更见 [Changelog](./CHANGELOG.md)。
|
|
39
39
|
|
|
40
40
|
一个**单一自足、确定性优先、fail-closed** 的 DeepSeek Harness 权限门插件。
|
|
41
41
|
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
- **会话放行** — 精确的 `(工具, 规范化 fingerprint)` grant,带 `TTL` + `maxUses`;换目标绝不复用。子代理继承但不可自授。
|
|
59
59
|
- **纯函数规则引擎** — glob/regex 编译 + ReDoS 上限、坏规则 loud fail、按源内容哈希缓存。
|
|
60
60
|
- **审计** — 每次决策写为 `{ignorable:true}` 事件并带 `callId`;模型可见理由与记录一致。
|
|
61
|
-
-
|
|
61
|
+
- **Permissive 档位** — 一个**独立审批模式**(区别于只读、完全权限与白名单档),既不是"自动审批",也不授予泛化权限。前端只暴露**一个开关**(`permissive`),后台四个审批策略**可组合**、由插件设置决定——仍对 P0 保持 fail-closed。
|
|
62
62
|
- **沙箱提权自动答复**(`trustEscalation`)— 沙箱提权是从 shell / pwsh / edit 工具**体内部**(`tools/pre-execute` 之后)发出的,所以门禁从未见过它,一个它自动放行的调用仍会弹出确认。开启后,门禁以 `callId` 精确匹配已放行调用并直接答复。
|
|
63
63
|
|
|
64
64
|
## 安装
|
|
@@ -88,17 +88,12 @@ dsh plugin --profile web add dsh-perm-gate
|
|
|
88
88
|
|
|
89
89
|
规则示例:见 [examples/permissions.example.yaml](./examples/permissions.example.yaml)。
|
|
90
90
|
|
|
91
|
-
##
|
|
91
|
+
## Permissive 档位
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
Permissive 是权限下拉框里一个**独立审批档**,与 Read Only / Workspace Write / Full access /
|
|
94
|
+
Whitelist 平行。它**不叫"自动审批"**、也不授予泛化权限:只会在人类/LLM 接缝**之前**收窄或放宽决策,
|
|
95
95
|
P0 硬拒绝始终单调且不可协商。
|
|
96
96
|
|
|
97
|
-
下拉框里的名字是**宿主提供的产品名**,不是逐语言的字典项:DSH 0.1.2 对插件档位在**两个**权限界面上
|
|
98
|
-
(通用设置默认档行、输入栏权限选择器)都原样渲染补丁里的 `name:`,只给三个内置档提供自己的本地化
|
|
99
|
-
标签,因此 `cordis.patch.yml` 直接写中文名,对所有会话一致。该档位**不画图标**——选择器的图标只按
|
|
100
|
-
三个内置值取。
|
|
101
|
-
|
|
102
97
|
`cordis.yml`:
|
|
103
98
|
|
|
104
99
|
```yaml
|
|
@@ -134,7 +129,7 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
|
|
|
134
129
|
`trustEscalation` 填补这个缺口。门禁记住每个它正面向上放行的调用(以宿主 `callId` 为键,提权
|
|
135
130
|
请求会重复该值),并在本处自行答复 `allowed-once`。它仅在**全部满足**时适用:
|
|
136
131
|
|
|
137
|
-
-
|
|
132
|
+
- Permissive 档位开启且 `trustEscalation` 开启;
|
|
138
133
|
- 请求携带门禁放行的 `callId`,且工具名匹配;
|
|
139
134
|
- 原因为已知的提权,指明 `workspace-write` 或 `danger-full-access`。
|
|
140
135
|
|
|
@@ -146,11 +141,11 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
|
|
|
146
141
|
### 权限下拉里可选档位
|
|
147
142
|
|
|
148
143
|
`cordis.patch.yml` 在 DSH 的 `permission.config.presets` 里新增了 `permissive` preset
|
|
149
|
-
(`sandbox: workspace-write`、`approval: ask`、名称
|
|
150
|
-
|
|
144
|
+
(`sandbox: workspace-write`、`approval: ask`、名称 **Permissive**),位于 Workspace Write 与
|
|
145
|
+
Full access 之间。DSH 的 bundle patch 对这个 map 是**整表替换**而非逐键合并,所以该文件还必须重述三个内置档
|
|
151
146
|
(`read-only` / `workspace-write` / `danger-full-access`,取自
|
|
152
147
|
`@deepseek-ai/dsh-base/cordis.patch.yml`);`test/patch-presets.spec.ts` 固定了这份键集合。因此会话权限
|
|
153
|
-
|
|
148
|
+
下拉里会出现 Permissive 这个**独立可选审批档**,而不是"auto-approval"档。
|
|
154
149
|
|
|
155
150
|
门禁**只在 `gatePresets` 列出的档位里生效**(默认 `['permissive']`,即本插件新增的那一档)。在其余任何档位
|
|
156
151
|
(Read Only、Workspace Write、Full access、`custom`)里,门禁的判定流程**完全不运行**:不放行、不弹审批、
|
|
@@ -162,7 +157,7 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
|
|
|
162
157
|
|
|
163
158
|
### 在 UI 里可配置
|
|
164
159
|
|
|
165
|
-
该档位也可在运行时从 **设置 → 插件 →
|
|
160
|
+
该档位也可在运行时从 **设置 → 插件 → Permissive 审批档** 调整(插件浏览器端渲染的
|
|
166
161
|
`settings.plugins.tab` 页面):一个开关切换 `permissive`,四个开关编辑后台
|
|
167
162
|
`permissiveStrategies`。host 端 live 读取该命名空间,改动对下一条工具调用即时生效,无需重启。
|
|
168
163
|
这是一个独立审批类,**不是** DSH 的"auto-approval"档。
|
|
@@ -187,7 +182,7 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
|
|
|
187
182
|
调用文本命中任一关键词(大小写不敏感子串)即直接拒绝,且先于白名单 / 授权 / LLM。黑名单在设置
|
|
188
183
|
卡片中按列表查看与增删(预置条目带标签,可一键恢复预置);未设置或为空时应用预置列表——黑名单
|
|
189
184
|
不会静默关闭。
|
|
190
|
-
|
|
185
|
+
Permissive 档在权限选择器中保留盾形图标——菜单项与折叠触发按钮均有图标。
|
|
191
186
|
|
|
192
187
|
|
|
193
188
|
## CLI(独立 dry-run)
|
package/cordis.patch.yml
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
# dsh-perm-gate profile patch.
|
|
2
2
|
#
|
|
3
3
|
# Two jobs:
|
|
4
|
-
# 1) Add the plugin's own "
|
|
4
|
+
# 1) Add the plugin's own "Permissive" session approval tier to the DSH
|
|
5
5
|
# permission picker, and restate the built-in tiers it must not drop.
|
|
6
6
|
# 2) Insert the plugin itself (host half) into the profile.
|
|
7
7
|
#
|
|
8
|
-
# The tier's DISPLAY NAME is the Chinese product label, matching every official
|
|
9
|
-
# 0.1.2 permission label (仅可查看 / 工作区内修改 / 完全权限) in a zh session. Both
|
|
10
|
-
# permission surfaces — the General-settings default row and the composer
|
|
11
|
-
# picker — render a host-supplied name verbatim unless the value is one of the
|
|
12
|
-
# three built-ins, so a single `name:` string is what both show; neither surface
|
|
13
|
-
# is localized per-dictionary for plugin-contributed tiers. The machine value
|
|
14
|
-
# stays `permissive`: it is the key `gatePresets` matches and the value the
|
|
15
|
-
# permission projection reports, so only the label text is user-facing.
|
|
16
|
-
#
|
|
17
|
-
# The tier carries no icon. The built-in glyphs the composer draws are keyed by
|
|
18
|
-
# the three built-in values, and a plugin tier deliberately has none.
|
|
19
|
-
#
|
|
20
8
|
# The DSH bundle patch REPLACES the whole `permission.config.presets` map (it
|
|
21
9
|
# does not merge per key), so this file MUST restate every tier that should
|
|
22
10
|
# survive. The authoritative built-in set is
|
|
@@ -43,8 +31,8 @@
|
|
|
43
31
|
permissive:
|
|
44
32
|
sandbox: workspace-write
|
|
45
33
|
approval: ask
|
|
46
|
-
name:
|
|
47
|
-
description:
|
|
34
|
+
name: Permissive
|
|
35
|
+
description: Independent approval tier — safe in-scope ops auto-allow, dangerous crossings ask (approval strengths tunable under Settings → Plugins → Permissive approval tier).
|
|
48
36
|
danger-full-access:
|
|
49
37
|
sandbox: danger-full-access
|
|
50
38
|
approval: never
|
package/lib/client.js
CHANGED
|
@@ -12,9 +12,9 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
const NS = "dsh-perm-gate";
|
|
13
13
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
14
14
|
const zh = {
|
|
15
|
-
"card.title": "
|
|
16
|
-
"card.description": "
|
|
17
|
-
"card.permissive": "
|
|
15
|
+
"card.title": "Permissive 审批档",
|
|
16
|
+
"card.description": "独立审批模式档位:与只读 / 完全权限 / 白名单平行。前端只暴露一个开关;后台三个审批策略可组合、由此处设置决定,仍对 P0 硬拒绝保持 fail-closed。",
|
|
17
|
+
"card.permissive": "启用 Permissive 档位",
|
|
18
18
|
"card.permissiveHint": "关闭时门禁行为与此前完全一致。",
|
|
19
19
|
"card.strategies": "后台审批策略(至少一个生效;可组合)",
|
|
20
20
|
"card.strategy.trustAutoAllow": "trustAutoAllow — 作用域内安全操作自动放行,危险/未知转 ask(中间档基线)",
|
|
@@ -121,9 +121,9 @@ window.__ModuleLoader__.load({
|
|
|
121
121
|
};
|
|
122
122
|
/** English dictionary (keys mirror zh). */
|
|
123
123
|
const en = {
|
|
124
|
-
"card.title": "
|
|
125
|
-
"card.description": "Independent approval tier, parallel to read-only / full-access / whitelist
|
|
126
|
-
"card.permissive": "Enable
|
|
124
|
+
"card.title": "Permissive approval tier",
|
|
125
|
+
"card.description": "Independent approval mode tier, parallel to read-only / full-access / whitelist. The front-end exposes a single switch; the three backend approval strategies are combinable via this panel and still fail-closed against P0 hard-deny.",
|
|
126
|
+
"card.permissive": "Enable Permissive tier",
|
|
127
127
|
"card.permissiveHint": "When off, the gate behaves exactly as before.",
|
|
128
128
|
"card.strategies": "Backend approval strategies (at least one effective; combinable)",
|
|
129
129
|
"card.strategy.trustAutoAllow": "trustAutoAllow — safe in-scope ops auto-allow; dangerous/unknown ask (baseline middle tier)",
|
|
@@ -1417,6 +1417,115 @@ window.__ModuleLoader__.load({
|
|
|
1417
1417
|
});
|
|
1418
1418
|
}
|
|
1419
1419
|
//#endregion
|
|
1420
|
+
//#region src/client/permission-icon.ts
|
|
1421
|
+
/**
|
|
1422
|
+
* Permission-picker icon decoration for the Permissive tier.
|
|
1423
|
+
*
|
|
1424
|
+
* dsh-auto-mode injects a shield glyph onto its "Auto" permission menu item
|
|
1425
|
+
* via a `data-*` attribute + a CSS mask. This module does the same for the
|
|
1426
|
+
* "Permissive" preset so the two approval tiers look consistent in the
|
|
1427
|
+
* dropdown. It only annotates the item; the preset itself comes from
|
|
1428
|
+
* `cordis.patch.yml` (the `permission.config.presets` table).
|
|
1429
|
+
*
|
|
1430
|
+
* Pure DOM + CSS, no @deepseek-ai value imports (client bundle purity).
|
|
1431
|
+
*/
|
|
1432
|
+
const PLUGIN_ID = "dsh-perm-gate";
|
|
1433
|
+
const ICON_ATTRIBUTE = "data-dsh-perm-gate-icon";
|
|
1434
|
+
/** Labels of the Permissive preset item, current + future localized variants. */
|
|
1435
|
+
const PERMISSIVE_LABELS = /* @__PURE__ */ new Set([
|
|
1436
|
+
"Permissive",
|
|
1437
|
+
"Permissive 审批档",
|
|
1438
|
+
"Permissive 审批档位"
|
|
1439
|
+
]);
|
|
1440
|
+
/** Same shield glyph as dsh-auto-mode so the two approval tiers match visually. */
|
|
1441
|
+
const ICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\"><path d=\"M8.21.9l6.58 2.47v3.64c0 4.99-3.74 7.2-6.58 8.29C5.36 14.21 1.62 12 1.62 7.01V3.37L8.21.9Z\" fill=\"none\" stroke=\"black\" stroke-width=\"1.32\" stroke-linejoin=\"round\"/><path d=\"M8.75 3.65 5.95 8.2h2.08l-.78 4.15 2.82-4.9H8.12l.63-3.8Z\" fill=\"black\"/></svg>";
|
|
1442
|
+
function iconStyles() {
|
|
1443
|
+
const mask = `url("data:image/svg+xml,${encodeURIComponent(ICON_SVG)}")`;
|
|
1444
|
+
return `
|
|
1445
|
+
[${ICON_ATTRIBUTE}]::before {
|
|
1446
|
+
content: "";
|
|
1447
|
+
display: inline-block;
|
|
1448
|
+
flex: 0 0 auto;
|
|
1449
|
+
width: 16px;
|
|
1450
|
+
height: 16px;
|
|
1451
|
+
background-color: var(--dsw-alias-label-tertiary, currentColor);
|
|
1452
|
+
-webkit-mask-image: ${mask};
|
|
1453
|
+
mask-image: ${mask};
|
|
1454
|
+
-webkit-mask-position: center;
|
|
1455
|
+
mask-position: center;
|
|
1456
|
+
-webkit-mask-repeat: no-repeat;
|
|
1457
|
+
mask-repeat: no-repeat;
|
|
1458
|
+
-webkit-mask-size: contain;
|
|
1459
|
+
mask-size: contain;
|
|
1460
|
+
}
|
|
1461
|
+
[${ICON_ATTRIBUTE}='trigger']::before {
|
|
1462
|
+
width: 14px;
|
|
1463
|
+
height: 14px;
|
|
1464
|
+
margin-right: 6px;
|
|
1465
|
+
}
|
|
1466
|
+
`;
|
|
1467
|
+
}
|
|
1468
|
+
/** The permission dropdown menu (lists the permission presets as menuitems). */
|
|
1469
|
+
function isPermissionMenu(menu) {
|
|
1470
|
+
return menu.matches("[role=\"menu\"]") && Array.from(menu.querySelectorAll("button[role=\"menuitem\"]")).length >= 4;
|
|
1471
|
+
}
|
|
1472
|
+
/** Whether one menuitem is the Permissive preset. */
|
|
1473
|
+
function isPermissiveMenuItem(element) {
|
|
1474
|
+
if (!element.matches("button[role=\"menuitem\"]")) return false;
|
|
1475
|
+
const label = (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1476
|
+
if (!PERMISSIVE_LABELS.has(label)) return false;
|
|
1477
|
+
return element.closest("[role=\"menu\"]") !== null;
|
|
1478
|
+
}
|
|
1479
|
+
/** The collapsed permission-picker trigger (opens the presets menu). */
|
|
1480
|
+
function isPermissionTrigger(element) {
|
|
1481
|
+
return element.matches("button[aria-haspopup=\"menu\"]") && PERMISSIVE_LABELS.has((element.textContent ?? "").replace(/\s+/g, " ").trim());
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Annotate the Permissive permission menuitems with the icon attribute, plus
|
|
1485
|
+
* the collapsed picker trigger when its label names the Permissive preset.
|
|
1486
|
+
* Unknown DOM shapes simply never match, so decoration stays conservative.
|
|
1487
|
+
*/
|
|
1488
|
+
function decorate(document) {
|
|
1489
|
+
for (const marked of document.querySelectorAll(`[${ICON_ATTRIBUTE}]`)) if (!isPermissiveMenuItem(marked) && !isPermissionTrigger(marked)) marked.removeAttribute(ICON_ATTRIBUTE);
|
|
1490
|
+
for (const menu of document.querySelectorAll("[role=\"menu\"]")) {
|
|
1491
|
+
if (!isPermissionMenu(menu)) continue;
|
|
1492
|
+
for (const item of menu.querySelectorAll("button[role=\"menuitem\"]")) if (isPermissiveMenuItem(item)) item.setAttribute(ICON_ATTRIBUTE, "menu");
|
|
1493
|
+
}
|
|
1494
|
+
for (const trigger of document.querySelectorAll("button[aria-haspopup=\"menu\"]")) if (isPermissionTrigger(trigger)) trigger.setAttribute(ICON_ATTRIBUTE, "trigger");
|
|
1495
|
+
}
|
|
1496
|
+
/** Install the Permissive tile icon into the permission picker; returns the disposer. */
|
|
1497
|
+
function installPermissivePermissionIcon(document) {
|
|
1498
|
+
for (const existing of document.querySelectorAll("style[data-plugin]")) if (existing.getAttribute("data-plugin") === PLUGIN_ID) existing.remove();
|
|
1499
|
+
const style = document.createElement("style");
|
|
1500
|
+
style.dataset.plugin = PLUGIN_ID;
|
|
1501
|
+
style.dataset.pluginCss = `${PLUGIN_ID}/permission-icon`;
|
|
1502
|
+
style.textContent = iconStyles();
|
|
1503
|
+
document.head.appendChild(style);
|
|
1504
|
+
let active = true;
|
|
1505
|
+
let queued = false;
|
|
1506
|
+
const scan = () => {
|
|
1507
|
+
if (!active || queued) return;
|
|
1508
|
+
queued = true;
|
|
1509
|
+
queueMicrotask(() => {
|
|
1510
|
+
queued = false;
|
|
1511
|
+
if (active) decorate(document);
|
|
1512
|
+
});
|
|
1513
|
+
};
|
|
1514
|
+
decorate(document);
|
|
1515
|
+
const observer = new MutationObserver(scan);
|
|
1516
|
+
observer.observe(document.documentElement, {
|
|
1517
|
+
childList: true,
|
|
1518
|
+
characterData: true,
|
|
1519
|
+
subtree: true
|
|
1520
|
+
});
|
|
1521
|
+
return () => {
|
|
1522
|
+
active = false;
|
|
1523
|
+
observer.disconnect();
|
|
1524
|
+
for (const marked of document.querySelectorAll(`[${ICON_ATTRIBUTE}]`)) marked.removeAttribute(ICON_ATTRIBUTE);
|
|
1525
|
+
style.remove();
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
//#endregion
|
|
1420
1529
|
//#region src/client/feed.ts
|
|
1421
1530
|
/** Fetch decision events for one session; `since > 0` returns only newer ones. */
|
|
1422
1531
|
async function fetchEvents(sessionId, since) {
|
|
@@ -1500,7 +1609,7 @@ window.__ModuleLoader__.load({
|
|
|
1500
1609
|
"deny-keyword": "黑名单关键词",
|
|
1501
1610
|
default: "默认策略",
|
|
1502
1611
|
ask: "默认策略",
|
|
1503
|
-
permissive: "
|
|
1612
|
+
permissive: "Permissive 放行",
|
|
1504
1613
|
classifier: "LLM 裁决",
|
|
1505
1614
|
"llm-safe": "LLM 判定安全",
|
|
1506
1615
|
"llm-learned": "LLM 学习放行",
|
|
@@ -2466,6 +2575,7 @@ window.__ModuleLoader__.load({
|
|
|
2466
2575
|
zh,
|
|
2467
2576
|
en
|
|
2468
2577
|
}), "dsh-perm-gate: dictionaries");
|
|
2578
|
+
ctx.effect(() => installPermissivePermissionIcon(globalThis.document), "dsh-perm-gate: permission icon");
|
|
2469
2579
|
ctx.slots.inject("conversation.input.dock", function* () {
|
|
2470
2580
|
yield ctx.slots.register({
|
|
2471
2581
|
name: "conversation.input.dock",
|