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/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [日本語 changelog](./CHANGELOG.ja.md)
13
13
  - [한국어 changelog](./CHANGELOG.ko.md)
14
14
 
15
- > **Compatibility note:** v0.2.1-beta.5 ships `ja` / `ko` dictionaries, but official DSH exposes
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.2.1-beta.5** — see the [Changelog](./CHANGELOG.md).
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
- - **自动审查 tier** (`permissive`) — an **independent approval mode** (separate from read-only,
76
- full-access and whitelist tiers) that is neither "auto-approve" nor blanket trust. Front-end
77
- exposes a **single switch** (`permissive`); the four backend strategies are **combinable** and
78
- driven by plugin settings — still fail-closed against P0. The permission picker and the
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
- ## The 自动审查 tier (machine value `permissive`)
151
+ ## Permissive mode
153
152
 
154
- 自动审查 is an **independent approval tier** in the DSH permission picker, parallel to
155
- Read Only / Workspace Write / Full access / Whitelist. It is **not** generic "auto-approval" and
156
- never mints blanket authority: it only narrows or widens the seam *before* the human/LLM step
157
- while P0 hard-deny stays monotonic and non-negotiable.
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 自动审查 tier is on and `trustEscalation` is on;
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 draws no icon in the permission picker: the composer's glyphs are keyed to the three
264
- built-in values, so a plugin-contributed tier is text-only on every surface.
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
- **自动审查**) between Workspace Write and Full access. The DSH bundle patch replaces the whole
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 自动审查 as an independent selectable approval tier, not a
274
- generic "auto-approval" mode.
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 a generic "auto-approval" mode.
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
- > **兼容性说明:** v0.2.1-beta.5 自带 `ja` / `ko` 字典,但官方 DSH 的 `LocaleRuntime`
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.2.1-beta.5** —— 变更见 [Changelog](./CHANGELOG.md)。
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
- - **自动审查档位**(机器值 `permissive`)——一个**独立审批模式**(区别于只读、完全权限与白名单档),既不是"自动审批",也不授予泛化权限。前端只暴露**一个开关**(`permissive`),后台四个审批策略**可组合**、由插件设置决定——仍对 P0 保持 fail-closed。权限下拉框与设置行都按产品名「自动审查」显示,且不带图标。
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
- ## 自动审查档位(机器值 `permissive`)
91
+ ## Permissive 档位
92
92
 
93
- 自动审查是权限下拉框里一个**独立审批档**,与只读 / 工作区内修改 / 完全权限 / 白名单平行。
94
- 它不是泛化的"自动审批"、也不授予泛化权限:只会在人类/LLM 接缝**之前**收窄或放宽决策,
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
- - 自动审查档位开启且 `trustEscalation` 开启;
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
- 完全权限之间。DSH 的 bundle patch 对这个 map 是**整表替换**而非逐键合并,所以该文件还必须重述三个内置档
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
- 下拉里会出现「自动审查」这个**独立可选审批档**,而不是"auto-approval"档。
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 "自动审查" session approval tier to the DSH
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": "独立审批档位:与只读 / 完全权限 / 白名单平行,在会话权限下拉中显示为「自动审查」。前端只暴露一个开关;后台四个审批策略可组合、由此处设置决定,仍对 P0 硬拒绝保持 fail-closed。",
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": "自动审查 (Auto review)",
125
- "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.",
126
- "card.permissive": "Enable the 自动审查 tier",
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",