dsh-perm-gate 4.1.1 → 4.1.2

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 CHANGED
@@ -5,6 +5,32 @@
5
5
  形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) に従い、
6
6
  このプロジェクトは [Semantic Versioning](https://semver.org/spec/v2.0.0.html) に準拠します。
7
7
 
8
+ ## [4.1.2] - 2026-09-25
9
+
10
+ ### 修正
11
+
12
+ - **入力欄グリフのパッチが DSH 0.1.5 で再び機能します。** `scripts/patch-permission-glyph.mjs`
13
+ は `["permissive",` の直後にエントリを挿入していましたが、そのキーは DSH 0.1.2 がグリフを
14
+ 描いていた値で、0.1.5-rc.2 のバンドルにはどこにも存在しません。そのためパッチは
15
+ `anchor not found` で失敗し、両ティアはテキストのみのままでした。取得元はティアごとに
16
+ `GLYPH_TARGETS` で宣言し、アンカーは**解決**します —— まず `["<key>",`、次にバンドルが使う
17
+ 定数別名(`danger-full-access` は `[FULL_ACCESS,`)。ホストが描かないキーは、Map が実際に
18
+ 持つキーと一緒に報告されます。
19
+ - **UI を提供していない DSH インストールにパッチを当てなくなりました。** 候補は profile
20
+ スコープを先、CLI インストールを次に並べ、同じバンドルを持つ**他の**インストールは黙って
21
+ 見送らず出力に列挙します。このプロジェクトのホストで実測:グローバル CLI インストールが
22
+ ランチャー管理のランタイムを隠していたため、パッチは成功と報告しつつピッカーは
23
+ アイコンなしのままでした。
24
+
25
+ ### 変更
26
+
27
+ - **各ティアは、共有するファイルサンドボックスのグリフを付けます(共通の盾+目ではなく)。**
28
+ 「自动审查」は `workspace-write` のグリフを、「自动审查(高权限)」は `danger-full-access` の
29
+ グリフをコピーするため、アイコンはそのティアが実際に与えるアクセスと一致します。同一アイコンの
30
+ 旧挙動は、`permissive` のグリフを持っていた 0.1.2 の Map に由来します。
31
+ - **2 つのグリフは両方適用するか、両方とも適用しません。** 解決できない取得元キーは何も書かない
32
+ 拒否であり、部分的にパッチされたバンドルにはなりません。
33
+
8
34
  ## [2.4.1] - 2026-09-17
9
35
 
10
36
  ### 修正
package/CHANGELOG.ko.md CHANGED
@@ -5,6 +5,30 @@
5
5
  형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)을 따르며,
6
6
  이 프로젝트는 [Semantic Versioning](https://semver.org/spec/v2.0.0.html)을 준수합니다.
7
7
 
8
+ ## [4.1.2] - 2026-09-25
9
+
10
+ ### 수정됨
11
+
12
+ - **입력창 글리프 패치가 DSH 0.1.5에서 다시 동작합니다.** `scripts/patch-permission-glyph.mjs`는
13
+ `["permissive",` 뒤에 항목을 끼워 넣었지만, 그 키는 DSH 0.1.2가 글리프를 그리던 값이고
14
+ 0.1.5-rc.2 번들에는 어디에도 없습니다. 그래서 패치는 `anchor not found`로 실패했고 두 티어는
15
+ 텍스트만 표시했습니다. 이제 원본은 티어별로 `GLYPH_TARGETS`에 선언하고, 앵커는 **해석**합니다
16
+ —— 먼저 `["<key>",`, 다음으로 번들이 쓰는 상수 별칭(`danger-full-access`는 `[FULL_ACCESS,`).
17
+ 호스트가 그리지 않는 키는 Map이 실제로 가진 키와 함께 보고됩니다.
18
+ - **UI를 제공하지 않는 DSH 설치에는 더 이상 패치하지 않습니다.** 후보는 profile 스코프를
19
+ 먼저, CLI 설치를 다음으로 정렬하고, 같은 번들을 가진 **다른** 설치는 조용히 넘기지 않고
20
+ 출력에 나열합니다. 이 프로젝트의 호스트에서 실측: 전역 CLI 설치가 런처가 관리하는
21
+ 런타임을 가려서, 패치는 성공을 보고했지만 선택기는 아이콘 없이 남아 있었습니다.
22
+
23
+ ### 변경됨
24
+
25
+ - **각 티어는 공유하는 파일 샌드박스의 글리프를 답니다(공통 방패+눈이 아니라).** 「自动审查」는
26
+ `workspace-write` 글리프를, 「自动审查(高权限)」는 `danger-full-access` 글리프를 복사하므로
27
+ 아이콘이 그 티어가 실제로 부여하는 접근 권한과 일치합니다. 동일 아이콘 쌍은 `permissive`
28
+ 글리프를 갖고 있던 0.1.2의 Map에서 온 동작입니다.
29
+ - **두 글리프는 모두 적용되거나 모두 적용되지 않습니다.** 해석할 수 없는 원본 키는 아무것도 쓰지
30
+ 않는 거부이며, 일부만 패치된 번들이 되지 않습니다.
31
+
8
32
  ## [2.4.1] - 2026-09-17
9
33
 
10
34
  ### 수정됨
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.1.2] - 2026-09-25
9
+
10
+ ### Fixed
11
+
12
+ - **The composer glyph patch works on DSH 0.1.5 again.** `scripts/patch-permission-glyph.mjs`
13
+ spliced its entry after `["permissive",` — a key DSH 0.1.2 rendered a glyph for and 0.1.5-rc.2
14
+ no longer contains anywhere in the bundle — so the patch died with `anchor not found` and both
15
+ plugin tiers stayed text-only. The sources are now declared per tier in `GLYPH_TARGETS` and each
16
+ anchor is **resolved**: `["<key>",` first, then the constant alias the bundle uses instead
17
+ (`danger-full-access` is keyed `[FULL_ACCESS,`). A source key this host does not render is
18
+ reported together with the keys the map does carry.
19
+ - **The probe no longer patches a DSH install that is not serving the UI.** Candidate bundles are
20
+ ordered profile scope first, CLI install second, and any *other* install carrying the same
21
+ bundle is listed in the output instead of being silently passed over. Measured on this
22
+ project's own host: a global CLI install shadowed the launcher-managed runtime, so the patch
23
+ reported success while the picker stayed icon-free.
24
+
25
+ ### Changed
26
+
27
+ - **Each tier wears the glyph of the file sandbox it shares, not one shared shield+eye.** 自动审查
28
+ copies the `workspace-write` glyph and 自动审查(高权限) the `danger-full-access` one, so the icon
29
+ matches the access the tier actually grants. The old identical-icon pair came from 0.1.2, whose
30
+ map still keyed a `permissive` glyph that 0.1.5 dropped.
31
+ - **Both glyphs are applied, or neither.** A source key the script cannot resolve is a refusal that
32
+ writes nothing, rather than a half-patched bundle.
33
+
8
34
  ## [2.6.0] - 2026-09-17
9
35
 
10
36
  ### Added
package/INSTALL.ja.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [日本語 changelog](./CHANGELOG.ja.md)
13
13
  - [한국어 changelog](./CHANGELOG.ko.md)
14
14
 
15
- `dsh-perm-gate` バージョン **2.4.1**。判定チェーン、ルールファイル形式、自动审查
15
+ `dsh-perm-gate` バージョン **4.1.2**。判定チェーン、ルールファイル形式、自动审查
16
16
  ティアについては [日本語 README](./README.ja.md) を参照してください。
17
17
 
18
18
  ## 必要条件
@@ -89,13 +89,21 @@ dsh profile reload --profile web
89
89
 
90
90
  ## **DSH** アップグレード後:入力欄アイコンのパッチを再適用
91
91
 
92
- 「自动审查(高权限)」が「自动审查」と同じ盾+目のアイコンを表示するのは、
92
+ 「自动审查」と「自动审查(高权限)」の 2 つのティアにアイコンが付くのは、
93
93
  `scripts/patch-permission-glyph.mjs` が **DSH ホストパッケージ内の閉じた Map** に
94
- その項目を追加したからにすぎません。DSH はプラグイン提供ティアに**設計上アイコンを
95
- 与えません** —— その Map 自身のコメントが *"host-configured names outside the design
96
- set get none"* と述べています。プラグインが影響できる option オブジェクトは
97
- `{value, name, description}` しか運ばないため、代わりに使えるプラグイン側のシームは
98
- 存在しません。
94
+ それらを追加したからにすぎません ——「自动审查」は `workspace-write` の盾+鉛筆を、
95
+ 「自动审查(高权限)」は `danger-full-access` の盾+感嘆符をコピーしており、それぞれが
96
+ 実際に共有するファイルサンドボックスのグリフになります。DSH はプラグイン提供ティアに
97
+ **設計上アイコンを与えません** —— その Map 自身のコメントが *"host-configured names
98
+ outside the design set get none"* と述べています。プラグインが影響できる option
99
+ オブジェクトは `{value, name, description}` しか運ばないため、代わりに使えるプラグイン側の
100
+ シームは存在しません。
101
+
102
+ スクリプトは取得元のキーを**解決**します(`["<key>",` を先に、次にホストが使う定数別名 ——
103
+ 0.1.5 の `danger-full-access` は `[FULL_ACCESS,`)。DSH はこの設計セットを一度変更して
104
+ います —— 0.1.2 には `permissive` のグリフがあり、0.1.5 には `read-only` /
105
+ `workspace-write` / `danger-full-access` しかありません。解決できないキーは、Map が実際に
106
+ 持つキーと一緒に報告されるため、ティアの付け替えは `GLYPH_TARGETS` の 1 行で済みます。
99
107
 
100
108
  このパッチは**ホスト**のファイルを書き換えるので、**DSH のアップグレードや再インストールで
101
109
  失われます**。*本プラグイン*のアップグレードでは失われません。アイコンは元々プラグインの
@@ -114,11 +122,15 @@ dsh profile reload --profile web
114
122
  プラグインが許可なく自分の harness を書き換えてよい理由はありません。適用の有無にかかわらず
115
123
  DSH の動作に影響はなく、ティア自体はそのまま機能します。
116
124
 
117
- このスクリプトは冪等(2 回目は no-op)で、バックアップは 1 度だけ取り、壊れたスライスを
118
- 書き込もうとしません —— 結果に対して `node --check` を実行し、失敗すればバックアップを
119
- 復元します —— したがって DSH アップグレードのたびに無条件で再実行して安全です。
120
- 実行中の `node` バイナリからパッケージを逆算するため、nvm のバージョン変更や
121
- インストール symlink の貼り替えでも壊れません。
125
+ このスクリプトは冪等(2 回目は no-op)で、バックアップは 1 度だけ取り、2 つのグリフは
126
+ 両方適用するか両方とも適用しないか(解決できない取得元キーは拒否であり、部分適用では
127
+ ありません)のいずれかで、壊れたスライスを書き込もうとしません —— 結果に対して
128
+ `node --check` を実行し、失敗すればバックアップを復元します —— したがって DSH
129
+ アップグレードのたびに無条件で再実行して安全です。
130
+ 呼び出し元の profile スコープを先に、CLI インストールを次に探すため、nvm のバージョン
131
+ 変更やインストール symlink の貼り替えでも壊れません。マシンに複数の DSH インストールが
132
+ ある場合、**パッチを当てなかった**方は出力に列挙されます —— 目の前の UI を提供している
133
+ のはそのうち 1 つだけなので、必要ならそのパスを引数で明示してください。
122
134
 
123
135
  **プラグインを再インストールしてもアイコンは戻りません。**
124
136
  `dsh plugin --profile web add …` は profile ディレクトリ内の pnpm に引数を転送し、
@@ -194,10 +206,12 @@ DSH の bundle patch は `permission.config.presets` を key 単位でマージ
194
206
  します。profile を再読込して `cordis.patch.yml` を再適用し、後から読み込まれる
195
207
  プラグインが `presets` を上書きしていないか確認してください。
196
208
 
197
- **「自动审查(高权限)」の入力欄アイコンが消えた。**
209
+ **「自动审查」ティアの入力欄アイコンが消えた。**
198
210
  DSH のアップグレードまたは再インストールが、パッチを当てたホスト bundle を置き換え
199
211
  ました。プラグインを再インストールしても戻りません。
200
- `npx dsh-perm-gate-patch-glyph` を実行して再読込してください。
212
+ `npx dsh-perm-gate-patch-glyph` を実行して再読込してください。取得元キーが無いと
213
+ 報告された場合は DSH がグリフ表を再度変更しています —— エラーが現在のキー一覧を
214
+ 示すので、`GLYPH_TARGETS` の該当ティアを付け替えてください。
201
215
  ティア自体は影響を受けません —— パッチなしでもラベルとゲートは動作します。
202
216
 
203
217
  **ルールファイルがあるのに `--list` が `ruleCount: 0` を返す。**
package/INSTALL.ko.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [日本語 changelog](./CHANGELOG.ja.md)
13
13
  - [한국어 changelog](./CHANGELOG.ko.md)
14
14
 
15
- `dsh-perm-gate` 버전 **2.4.1**. 결정 체인, 규칙 파일 형식, 自动审查 티어는
15
+ `dsh-perm-gate` 버전 **4.1.2**. 결정 체인, 규칙 파일 형식, 自动审查 티어는
16
16
  [한국어 README](./README.ko.md)를 참고하세요.
17
17
 
18
18
  ## 요구 사항
@@ -90,13 +90,21 @@ dsh profile reload --profile web
90
90
 
91
91
  ## **DSH** 업그레이드 후: 입력창 아이콘 패치 재적용
92
92
 
93
- 「自动审查(高权限)」가 「自动审查」와 같은 방패+눈 아이콘을 보여 주는 것은
94
- `scripts/patch-permission-glyph.mjs`가 **DSH 호스트 패키지 안의 닫힌 Map**에 그 항목을
95
- 추가했기 때문일 뿐입니다. DSH는 플러그인이 제공한 티어에 **설계상 아이콘을 주지
93
+ 「自动审查」와「自动审查(高权限)」두 티어에 아이콘이 붙는 것은
94
+ `scripts/patch-permission-glyph.mjs`가 **DSH 호스트 패키지 안의 닫힌 Map**에 두 항목을
95
+ 추가했기 때문입니다 ——「自动审查」는 `workspace-write`의 방패+연필을,
96
+ 「自动审查(高权限)」는 `danger-full-access`의 방패+느낌표를 복사해 각자가 실제로 공유하는
97
+ 파일 샌드박스의 글리프를 갖습니다. DSH는 플러그인이 제공한 티어에 **설계상 아이콘을 주지
96
98
  않습니다** —— 그 Map 자체의 주석이 *"host-configured names outside the design set get
97
99
  none"*이라고 말합니다. 플러그인이 영향을 줄 수 있는 option 객체는
98
100
  `{value, name, description}`만 나르므로, 대신 쓸 수 있는 플러그인 측 이음새가 없습니다.
99
101
 
102
+ 스크립트는 원본 키를 **해석**합니다(`["<key>",`를 먼저, 다음으로 호스트가 쓰는 상수 별칭 ——
103
+ 0.1.5의 `danger-full-access`는 `[FULL_ACCESS,`). DSH는 이 설계 집합을 이미 한 번 바꿨습니다 ——
104
+ 0.1.2에는 `permissive` 글리프가 있었고 0.1.5에는 `read-only` / `workspace-write` /
105
+ `danger-full-access`만 있습니다. 해석할 수 없는 키는 Map이 실제로 가진 키와 함께 보고되므로
106
+ 티어 교체는 `GLYPH_TARGETS` 한 줄이면 됩니다.
107
+
100
108
  이 패치는 **호스트** 파일을 고치므로 **DSH 업그레이드나 재설치에서 사라집니다**.
101
109
  *이 플러그인*을 업그레이드할 때는 사라지지 않습니다. 아이콘은 애초에 플러그인 소유가
102
110
  아니었고, 플러그인 자신의 기여(`cordis.patch.yml`의 `name:` / `description:`)는
@@ -114,10 +122,13 @@ dsh profile reload --profile web
114
122
  플러그인이 허락 없이 자신이 설치된 harness를 다시 쓸 이유는 없습니다. 적용 여부와 무관하게
115
123
  DSH 동작에는 영향이 없고 티어 자체는 그대로 기능합니다.
116
124
 
117
- 이 스크립트는 멱등이며(두 번째 실행은 no-op), 백업은 한 번만 뜨고, 잘못 잘린 조각을
118
- 쓰지 않습니다 —— 결과에 `node --check`를 돌려 실패하면 백업을 복원합니다 —— 따라서
119
- DSH 업그레이드 때마다 무조건 다시 실행해도 안전합니다. 실행 중인 `node` 바이너리에서
120
- 패키지를 역산하므로 nvm 버전 변경이나 설치 symlink 재지정에도 깨지지 않습니다.
125
+ 이 스크립트는 멱등이며(두 번째 실행은 no-op), 백업은 한 번만 뜨고, 두 글리프를 모두
126
+ 적용하거나 모두 적용하지 않으며(해석할 수 없는 원본 키는 거부이지 부분 적용이 아닙니다),
127
+ 잘못 잘린 조각을 쓰지 않습니다 —— 결과에 `node --check`를 돌려 실패하면 백업을 복원합니다
128
+ —— 따라서 DSH 업그레이드 때마다 무조건 다시 실행해도 안전합니다. 호출자의 profile 스코프를 먼저, CLI 설치를 다음으로 탐색하므로 nvm 버전 변경이나 설치
129
+ symlink 재지정에도 깨지지 않습니다. 머신에 DSH 설치가 여러 개면 **패치하지 않은** 쪽이
130
+ 출력에 나열됩니다 —— 눈앞의 UI를 제공하는 것은 그중 하나뿐이므로, 필요하면 그 경로를
131
+ 인수로 명시하세요.
121
132
 
122
133
  **플러그인을 재설치해도 아이콘은 돌아오지 않습니다.**
123
134
  `dsh plugin --profile web add …`는 profile 디렉터리 안의 pnpm으로 인자를 넘기고
@@ -190,11 +201,12 @@ DSH 번들 패치는 `permission.config.presets`를 key 단위로 병합하지
190
201
  합니다. profile을 다시 불러와 `cordis.patch.yml`을 재적용하고, 더 늦게 로드되는
191
202
  플러그인이 `presets`를 덮어쓰지 않는지 확인하세요.
192
203
 
193
- **「自动审查(高权限)」의 입력창 아이콘이 사라졌습니다.**
204
+ **「自动审查」티어의 입력창 아이콘이 사라졌습니다.**
194
205
  DSH 업그레이드나 재설치가 패치해 둔 호스트 번들을 교체했습니다. 플러그인을
195
206
  재설치해도 되돌아오지 않습니다. `npx dsh-perm-gate-patch-glyph`를 실행하고
196
- 다시 불러오세요. 티어 자체는 영향받지 않습니다 —— 패치가 없어도 라벨과 게이트는
197
- 정상 동작합니다.
207
+ 다시 불러오세요. 원본 키가 없다고 보고되면 DSH가 글리프 표를 다시 바꾼 것입니다 ——
208
+ 오류가 현재 키 목록을 알려 주므로 `GLYPH_TARGETS`에서 해당 티어를 다시 지정하세요.
209
+ 티어 자체는 영향받지 않습니다 —— 패치가 없어도 라벨과 게이트는 정상 동작합니다.
198
210
 
199
211
  **규칙 파일이 있는데 `--list`가 `ruleCount: 0`을 표시합니다.**
200
212
  `rulesFile`은 플러그인 디렉터리가 아니라 Harness 프로세스의 CWD에서 해석됩니다.
package/INSTALL.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [日本語 changelog](./CHANGELOG.ja.md)
13
13
  - [한국어 changelog](./CHANGELOG.ko.md)
14
14
 
15
- `dsh-perm-gate` version **2.4.1**. Continue with the [README](./README.md) for the
15
+ `dsh-perm-gate` version **4.1.2**. Continue with the [README](./README.md) for the
16
16
  decision chain, the rules file format and the 自动审查 tier.
17
17
 
18
18
  ## Requirements
@@ -90,12 +90,21 @@ dsh profile reload --profile web
90
90
 
91
91
  ## After a **DSH** upgrade: re-apply the composer glyph patch
92
92
 
93
- The 自动审查(高权限) tier shows the same shield+eye glyph as 自动审查 only because
94
- `scripts/patch-permission-glyph.mjs` added it to a **closed map inside a DSH host
95
- package**. DSH gives host-configured tiers no glyph by design — the map's own
96
- comment reads *"host-configured names outside the design set get none"* — and the
97
- option objects a plugin can influence carry only `{value, name, description}`, so
98
- there is no plugin-side seam to use instead.
93
+ Both 自动审查 tiers draw an icon only because `scripts/patch-permission-glyph.mjs`
94
+ added them to a **closed map inside a DSH host package**: 自动审查 copies the
95
+ `workspace-write` glyph and 自动审查(高权限) the `danger-full-access` one — the
96
+ built-in tier each shares a file sandbox with. DSH gives host-configured tiers no
97
+ glyph by design — the map's own comment reads *"host-configured names outside the
98
+ design set get none"* — and the option objects a plugin can influence carry only
99
+ `{value, name, description}`, so there is no plugin-side seam to use instead.
100
+
101
+ The script resolves each source key instead of assuming its spelling: `["<key>",`
102
+ first, then the constant alias the bundle may use instead (`[FULL_ACCESS,` for
103
+ `danger-full-access`). DSH has already changed this design set once — 0.1.2 keyed a
104
+ `permissive` glyph, 0.1.5 keeps only `read-only` / `workspace-write` /
105
+ `danger-full-access` — and a source key it cannot resolve is reported together with
106
+ the keys the map does carry, so re-pointing a tier is a one-line edit to
107
+ `GLYPH_TARGETS`.
99
108
 
100
109
  That patch edits a **host** file, so a DSH upgrade or reinstall erases it. Upgrading
101
110
  *this plugin* does not: the plugin never owned the glyph, and its own contribution
@@ -112,11 +121,16 @@ the `dsh-perm-gate-patch-glyph` bin — so no source checkout is needed. It is d
112
121
  **not** wired to `postinstall`: it edits a host package, and a plugin must not rewrite its
113
122
  harness uninvited. Running DSH is unaffected either way; the tier works with or without it.
114
123
 
115
- It is idempotent (a second run is a no-op), backs the bundle up once, and refuses to
116
- write a mis-sliced bundle — it runs `node --check` on the result and restores the
117
- backup if that fails — so it is safe to run unconditionally after every DSH upgrade.
118
- It locates the bundle from the running `node` binary, so an nvm version change or a
119
- re-pointed install symlink does not break it.
124
+ It is idempotent (a second run is a no-op), backs the bundle up once, applies both
125
+ glyphs or neither (a source key it cannot resolve is a refusal, not a half-patch),
126
+ and refuses to write a mis-sliced bundle — it runs `node --check` on the result and
127
+ restores the backup if that fails — so it is safe to run unconditionally after every
128
+ DSH upgrade.
129
+ It locates the bundle from the caller's profile scope first and the CLI install second,
130
+ so an nvm version change or a re-pointed install symlink does not break it — and when the
131
+ machine carries more than one DSH install, the ones it did **not** patch are listed in the
132
+ output, because only one of them serves the UI in front of you. Pass that path explicitly
133
+ to patch it.
120
134
 
121
135
  **Reinstalling the plugin does not restore the glyph.** `dsh plugin --profile web
122
136
  add …` forwards to pnpm inside the profile directory and writes the profile's own
@@ -191,11 +205,13 @@ The DSH bundle patch replaces the whole `permission.config.presets` map instead
191
205
  merging per key. Reload the profile so `cordis.patch.yml` is re-applied, and make
192
206
  sure no later plugin overwrites `presets`.
193
207
 
194
- **自动审查(高权限) lost its icon in the composer.**
195
- A DSH upgrade or reinstall replaced the host bundle the glyph was patched into;
208
+ **A 自动审查 tier lost its icon in the composer.**
209
+ A DSH upgrade or reinstall replaced the host bundle the glyphs were patched into;
196
210
  re-running the plugin install will not bring it back. Run
197
- `npx dsh-perm-gate-patch-glyph` and reload. The tier itself is
198
- unaffected — its label and its gating keep working without the patch.
211
+ `npx dsh-perm-gate-patch-glyph` and reload. If it reports that a source key is gone,
212
+ DSH changed the glyph map again: the error names the keys the map now carries, so
213
+ re-point the affected tier in `GLYPH_TARGETS`. The tiers themselves are
214
+ unaffected — their labels and their gating keep working without the patch.
199
215
 
200
216
  **`--list` reports `ruleCount: 0` although my rules file exists.**
201
217
  `rulesFile` is resolved against the process CWD of the harness, not the plugin
package/INSTALL.zh.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [日本語 changelog](./CHANGELOG.ja.md)
13
13
  - [한국어 changelog](./CHANGELOG.ko.md)
14
14
 
15
- `dsh-perm-gate` 版本 **2.4.1**。裁决链、规则文件格式与自动审查档位请见
15
+ `dsh-perm-gate` 版本 **4.1.2**。裁决链、规则文件格式与自动审查档位请见
16
16
  [中文 README](./README.zh.md)。
17
17
 
18
18
  ## 前置条件
@@ -87,11 +87,18 @@ dsh profile reload --profile web
87
87
 
88
88
  ## **DSH** 升级后:重打输入区图标补丁
89
89
 
90
- 「自动审查(高权限)」之所以和「自动审查」一样显示盾+眼图标,只是因为
91
- `scripts/patch-permission-glyph.mjs` 把这一项加进了 **DSH 宿主包里的一张封闭 Map**。
92
- DSH 对插件档位**设计上就不给图标** —— 那张表自己的注释写着 *"host-configured
93
- names outside the design set get none"*;而插件能影响的 option 对象只携带
94
- `{value, name, description}`,所以插件侧没有可用的接缝。
90
+ 两个「自动审查」档位之所以有图标,只是因为 `scripts/patch-permission-glyph.mjs`
91
+ 把它们加进了 **DSH 宿主包里的一张封闭 Map**:「自动审查」复制 `workspace-write` 的
92
+ 盾+铅笔,「自动审查(高权限)」复制 `danger-full-access` 的盾+感叹号 —— 各自对应
93
+ 自己实际共用的文件沙箱。DSH 对插件档位**设计上就不给图标** —— 那张表自己的注释写着
94
+ *"host-configured names outside the design set get none"*;而插件能影响的 option 对象
95
+ 只携带 `{value, name, description}`,所以插件侧没有可用的接缝。
96
+
97
+ 脚本会**解析**源键而不是假定它的写法:先找 `["<key>",`,再找宿主可能改用的常量别名
98
+ (`danger-full-access` 在 0.1.5 里就写作 `[FULL_ACCESS,`)。宿主已经改过一次设计集 ——
99
+ 0.1.2 有 `permissive` 图标,0.1.5 只剩 `read-only` / `workspace-write` /
100
+ `danger-full-access` —— 解析不到的源键会在报错里连同「表里实际有哪些键」一起给出,
101
+ 所以改档位只需改 `GLYPH_TARGETS` 一行。
95
102
 
96
103
  该补丁改的是**宿主**文件,因此 **DSH 升级或重装会把它抹掉**。升级*本插件*不会:
97
104
  图标从来不属于插件,而插件自己的贡献(`cordis.patch.yml` 里的 `name:` /
@@ -108,9 +115,12 @@ dsh profile reload --profile web
108
115
  `postinstall` 上:它改的是宿主包,插件不该未经许可改写自己所在的 harness。有没有它
109
116
  都不影响 DSH 运行,档位本身照常工作。
110
117
 
111
- 脚本是幂等的(重复执行是 no-op),只备份一次,且拒绝写坏切片 —— 它会对结果跑
112
- `node --check`,失败即还原备份 —— 所以每次 DSH 升级后无条件重跑都是安全的。
113
- 它从运行中的 `node` 二进制反推包路径,因此 nvm 换版本或安装软链重指都不会让它失效。
118
+ 脚本是幂等的(重复执行是 no-op),只备份一次,两个图标要么都打要么都不打(源键解析不到
119
+ 即拒绝,不会打出半个补丁),且拒绝写坏切片 —— 它会对结果跑 `node --check`,失败即还原
120
+ 备份 —— 所以每次 DSH 升级后无条件重跑都是安全的。
121
+ 它先探测调用方自己的 profile 作用域,再看 CLI 安装目录,因此 nvm 换版本或安装软链重指都
122
+ 不会让它失效;当机器上存在多份 DSH 安装时,**没有**被打补丁的那些会列在输出里 —— 因为
123
+ 真正服务你面前界面的只有其中一份,需要时把那个路径作为参数显式传入。
114
124
 
115
125
  **重装插件并不会恢复图标。**`dsh plugin --profile web add …` 只是把参数转发给
116
126
  profile 目录里的 pnpm,只写 profile 自己的 `node_modules`;`-w`
@@ -180,9 +190,10 @@ DSH 的 bundle patch 是整体替换 `permission.config.presets`,而非逐 key
180
190
  请重载 profile 让 `cordis.patch.yml` 重新生效,并确认没有更晚加载的插件覆盖了
181
191
  `presets`。
182
192
 
183
- **「自动审查(高权限)」在输入区丢了图标。**
193
+ **某个「自动审查」档位在输入区丢了图标。**
184
194
  DSH 升级或重装替换了被打了补丁的宿主 bundle;重装插件不会把它带回来。执行
185
- `npx dsh-perm-gate-patch-glyph` 后重载即可。档位本身不受影响 ——
195
+ `npx dsh-perm-gate-patch-glyph` 后重载即可。若报错说某个源键不存在,说明宿主又改了
196
+ 图标表 —— 报错会列出表里现有的键,按提示改 `GLYPH_TARGETS` 即可。档位本身不受影响 ——
186
197
  没有补丁,它的标签与门禁照常工作。
187
198
 
188
199
  **规则文件存在但 `--list` 显示 `ruleCount: 0`。**
package/README.ja.md CHANGED
@@ -45,7 +45,7 @@
45
45
  > メソッドであるため、`typeof` プロブで読み取り、欠落時やエラー時には「ポリシー
46
46
  > 不明」にフォールバックします。
47
47
 
48
- バージョン **2.4.1** — 変更履歴は [日本語 changelog](./CHANGELOG.ja.md) を参照。
48
+ バージョン **4.1.2** — 変更履歴は [日本語 changelog](./CHANGELOG.ja.md) を参照。
49
49
 
50
50
  DeepSeek Harness 向けの、単一・自己完結・決定論優先・fail-closed な権限ゲートです。
51
51
 
@@ -93,6 +93,31 @@ DSH の安全まわりのエコシステムでは、この役割が `dsh-permiss
93
93
  オンにすると、ゲートが許可した呼び出しを `callId` で正確に一致させてここで直接回答
94
94
  します。
95
95
 
96
+ ## スクリーンショット
97
+
98
+ **設定カード — 自动审查ティア**:フロントが露出するのは単一スイッチのみで、バックエンドの
99
+ 4 戦略は組み合わせ可能です。`LlmAssist` はセッションの現在のモデルに追従するか、
100
+ Provider / Model を固定でき、ヘルスチェックと裁決学習を備えます:
101
+
102
+ ![設定カード — 自动审查ティア](assets/setting-head.png)
103
+ ![LlmAssist — Provider / Model / API Key](assets/llms.png)
104
+
105
+ **ゲートが実際に読み込んだルール** — `dsh-perm-gate-rules` 設定名前空間から直接読み出した
106
+ 読み取り専用ビューです。パネルからは変更できません:
107
+
108
+ ![組み込みルール(読み取り専用)](assets/setting-rules.png)
109
+
110
+ **権限承認記録** — このセッションのすべての裁決を新しい順に、モデルが見た理由つきで表示します:
111
+
112
+ ![権限承認記録](assets/permlist.png)
113
+
114
+ **裁決ストリップ** — 自動許可は根拠(読み取り専用ツール、または LLM の `safe` 判定)を明示し、
115
+ ゲートの停止も決して無言では起きません:
116
+
117
+ ![ALLOW ストリップ — 読み取り専用ツール](assets/allow1.png)
118
+ ![ALLOW ストリップ — llm-assist が safe](assets/allow2.png)
119
+ ![GATE OFF ストリップ — ゲート停止](assets/gateoff.png)
120
+
96
121
  ## インストール
97
122
 
98
123
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) が導入済みである
@@ -235,9 +260,10 @@ P0–P4 の全チェーンが動作します。ゲートはプリセットの**
235
260
  `cordis.patch.yml` は全セッション共通で中国語ラベルを配布します。
236
261
 
237
262
  **アイコンは別の話です。** 入力欄のグリフ表は閉じており、その表自身のコメントが規則を述べています:
238
- *host-configured names outside the design set get none*。`permissive` は組み込み値なので
239
- 「自动审查」には元から盾+目のアイコンがあります。「自动审查(高权限)」が同じアイコンを得るのは、
240
- `npx dsh-perm-gate-patch-glyph` がその表に 1 項目を追加したからです。このパッチは
263
+ *host-configured names outside the design set get none*。どちらのティアも組み込み値ではないため、
264
+ アイコンは `npx dsh-perm-gate-patch-glyph` がその表に 2 項目を足すことで初めて付きます ——
265
+ 「自动审查」は `workspace-write` の盾+鉛筆を、「自动审查(高权限)」は `danger-full-access` の
266
+ 盾+感嘆符をコピーし、それぞれが実際に共有するファイルサンドボックスのグリフになります。このパッチは
241
267
  **ホスト**パッケージを書き換えるため、DSH アップグレードのたびに失われます ——
242
268
  [DSH アップグレード後:入力欄アイコンのパッチを再適用](./INSTALL.ja.md) を参照してください。
243
269
 
@@ -342,9 +368,10 @@ Full access / `custom`)では、ゲートの判定フローは**一切実行
342
368
  人手に回した `ask` は回答が返るまで追跡されます。受動的な `approval/request` オブザーバが閉じた結果を記録し(`allowed-once` → **承認**、`rejected` → **拒否**、`cancelled` → **キャンセル**、`unavailable` → 拒否=承認チャネルなし)、オブザーバが関連付けできない場合(`callId` 欠落・approval サービスなし・上流リスナーの短絡)は `tools/result` が同じ ask をフォールバックとして解決します。承認時には承認後の学習進捗(`n`/しきい値)を表示し、通知バーも 3 つの終態にラベルを付けます。
343
369
 
344
370
  さらにプリセットの**拒否キーワード黑名単**(dsh-approval-gate の `DEFAULT_DENY_KEYWORDS` を継承:`rm -rf`、`push --force`、`drop table`、`mkfs`、`git reset --hard`、`docker system prune` など)を備え、テキストがキーワードを含む呼び出し(大小文字を区別しない部分一致)は許可リスト / 許可 / LLM より先に拒否します。設定カードでリストとして編集でき(プリセット項目にはタグ付き、ワンクリック復元対応)、未設定・空ならプリセットを適用します——黑名単が静かに無効化されることはありません。
345
- 「自动审查」と「自动审查(高权限)」はどちらもピッカーに盾+目のアイコンを描きます —— 前者は
346
- DSH の組み込み表から、後者はインストールガイドに書かれたホストパッチからです。そのパッチが
347
- ない場合、2 つ目のティアはどのサーフェスでもテキストのみになります。ラベルとゲートは影響を
371
+ 「自动审查」と「自动审查(高权限)」はどちらもピッカーにアイコンを描きます —— インストール
372
+ ガイドに書かれたホストパッチが、それぞれが共有するファイルサンドボックスのティア
373
+ (`workspace-write` と `danger-full-access`)からコピーしたものです。そのパッチが
374
+ ない場合、両ティアはどのサーフェスでもテキストのみになります。ラベルとゲートは影響を
348
375
  受けません。
349
376
 
350
377
 
package/README.ko.md CHANGED
@@ -43,7 +43,7 @@
43
43
  > 모두에서 user-approval 서비스의 **개인** 메소드이므로 `typeof` 프로브를 통해
44
44
  > 읽으며, 누락되거나 예외 발생 시 "정책_unknown"으로 후퇴합니다.
45
45
 
46
- 버전 **2.4.1** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
46
+ 버전 **4.1.2** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
47
47
 
48
48
  DeepSeek Harness용 단일·자족적·결정론 우선·fail-closed 권한 게이트입니다.
49
49
 
@@ -90,6 +90,31 @@ DSH 안전 생태계에서는 이 역할이 `dsh-permission-rules` / `dsh-auto-m
90
90
  없고, 게이트가 자동 허용한 호출에도 확인 프롬프트가 나타납니다. 이 전략을 켜면 게이트가
91
91
  `callId`로 이미 허용한 호출을 정확히 매칭하여 여기서 직접 응답합니다.
92
92
 
93
+ ## 스크린샷
94
+
95
+ **설정 카드 — 自动审查 티어**: 프론트는 단일 스위치만 노출하고, 백엔드의 네 전략은 조합
96
+ 가능합니다. `LlmAssist`는 세션의 현재 모델을 따르거나 Provider / Model을 고정할 수 있으며,
97
+ 헬스 테스트와 판정 학습을 갖춥니다:
98
+
99
+ ![설정 카드 — 自动审查 티어](assets/setting-head.png)
100
+ ![LlmAssist — Provider / Model / API Key](assets/llms.png)
101
+
102
+ **게이트가 실제로 로드한 규칙** — `dsh-perm-gate-rules` 설정 네임스페이스에서 직접 읽은
103
+ 읽기 전용 뷰입니다. 패널에서는 바꿀 수 없습니다:
104
+
105
+ ![내장 규칙(읽기 전용)](assets/setting-rules.png)
106
+
107
+ **권한 승인 기록** — 이 세션의 모든 판정을 최신순으로, 모델이 본 이유와 함께 표시합니다:
108
+
109
+ ![권한 승인 기록](assets/permlist.png)
110
+
111
+ **판정 스트립** — 자동 허용은 근거(읽기 전용 도구 또는 LLM의 `safe` 판정)를 밝히고,
112
+ 게이트 정지도 결코 조용히 일어나지 않습니다:
113
+
114
+ ![ALLOW 스트립 — 읽기 전용 도구](assets/allow1.png)
115
+ ![ALLOW 스트립 — llm-assist safe](assets/allow2.png)
116
+ ![GATE OFF 스트립 — 게이트 정지](assets/gateoff.png)
117
+
93
118
  ## 설치
94
119
 
95
120
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)가 설치되어 있어야
@@ -230,9 +255,10 @@ DSH **자신**의 트래픽 —— 내장 네트워크 도구와 LLM 전송 —
230
255
  라벨을 배포합니다.
231
256
 
232
257
  **아이콘은 다른 이야기입니다.** 입력창의 글리프 표는 닫혀 있고, 그 표 자체의 주석이 규칙을
233
- 말합니다: *host-configured names outside the design set get none*. `permissive`는 내장 값이라
234
- 「自动审查」에는 원래부터 방패+눈 아이콘이 있습니다. 「自动审查(高权限)」가 같은 아이콘을
235
- 갖는 것은 `npx dsh-perm-gate-patch-glyph`가 그 표에 항목 하나를 추가했기 때문입니다.
258
+ 말합니다: *host-configured names outside the design set get none*. 두 티어 모두 내장 값이 아니므로
259
+ 아이콘은 `npx dsh-perm-gate-patch-glyph`가 그 표에 항목 두 개를 넣어야 생깁니다 ——
260
+ 「自动审查」는 `workspace-write`의 방패+연필을, 「自动审查(高权限)」는 `danger-full-access`의
261
+ 방패+느낌표를 복사해 각자가 실제로 공유하는 파일 샌드박스의 글리프를 갖습니다.
236
262
  이 패치는 **호스트** 패키지를 고치므로 DSH 업그레이드마다 사라집니다 ——
237
263
  [DSH 업그레이드 후: 입력창 아이콘 패치 재적용](./INSTALL.ko.md)을 참고하세요.
238
264
 
@@ -343,9 +369,10 @@ host는 네임스페이스를 live로 읽으므로 변경은 재시작 없이
343
369
  카드에서 목록으로 편집할 수 있고(프리셋 항목에는 태그 표시, 원클릭 복원 지원) 미설정·빈 목록 시 프리셋이
344
370
  적용됩니다——블랙리스트가 조용히 꺼지지 않습니다.
345
371
  블랙리스트가 조용히 꺼지는 일은 없습니다.
346
- 「自动审查」와 「自动审查(高权限)」는 둘 다 선택기에 방패+눈 아이콘을 그립니다 — 전자는
347
- DSH 내장 표에서, 후자는 설치 안내에 적힌 호스트 패치에서 옵니다. 그 패치가 없으면 두 번째
348
- 티어는 모든 화면에서 텍스트만 표시됩니다. 라벨과 게이트는 영향을 받지 않습니다.
372
+ 「自动审查」와 「自动审查(高权限)」는 둘 다 선택기에 아이콘을 그립니다 — 설치 안내에 적힌
373
+ 호스트 패치가 각자가 공유하는 파일 샌드박스 티어(`workspace-write`와 `danger-full-access`)에서
374
+ 복사한 것입니다. 그 패치가 없으면 두 티어 모두 모든 화면에서 텍스트만 표시됩니다.
375
+ 라벨과 게이트는 영향을 받지 않습니다.
349
376
 
350
377
 
351
378
  ## CLI(독립 실행 dry-run)
package/README.md CHANGED
@@ -42,7 +42,7 @@
42
42
  > **private** method of the user-approval service on both, so it is read behind a
43
43
  > `typeof` probe and degrades to “policy unknown” when absent or throwing.
44
44
 
45
- Version **2.6.0** — see the [Changelog](./CHANGELOG.md).
45
+ Version **4.1.2** — see the [Changelog](./CHANGELOG.md).
46
46
 
47
47
  A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.
48
48
 
@@ -104,6 +104,31 @@ grants and LLM. It is editable as a list in the settings card (preset entries ar
104
104
  one-click restore brings the preset back); unset or empty applies the preset — the blacklist
105
105
  never silently turns off.
106
106
 
107
+ ## Screenshots
108
+
109
+ **The settings card — the 自动审查 tier.** The front end exposes a single switch; the four back-end
110
+ strategies combine. `LlmAssist` can follow the session's current model or be pinned to a provider /
111
+ model, and carries its own health test and verdict learning:
112
+
113
+ ![Settings card — the 自动审查 tier](assets/setting-head.png)
114
+ ![LlmAssist — provider, model, API key](assets/llms.png)
115
+
116
+ **The rules the gate actually loaded**, read straight from the `dsh-perm-gate-rules` settings
117
+ namespace — the panel is read-only by design, so it can never edit what it shows:
118
+
119
+ ![Built-in rules, read-only](assets/setting-rules.png)
120
+
121
+ **Approval decision log** — every verdict in this session, newest first, with the reason the model saw:
122
+
123
+ ![Approval decision log](assets/permlist.png)
124
+
125
+ **Decision strips** — an auto-allow states its basis (a read-only tool, or an LLM `safe` verdict),
126
+ and a stand-down is never silent:
127
+
128
+ ![ALLOW strip — read-only internal tool](assets/allow1.png)
129
+ ![ALLOW strip — llm-assist risk: safe](assets/allow2.png)
130
+ ![GATE OFF strip — the gate stood down](assets/gateoff.png)
131
+
107
132
  ## Install
108
133
 
109
134
  Requires an existing [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) installation.
@@ -264,9 +289,10 @@ default row and the composer picker) and only supplies its own localized labels
264
289
  built-in values, so `cordis.patch.yml` ships the Chinese label for every session.
265
290
 
266
291
  The **icon** is a different story. The composer's glyph map is closed, and its own comment states
267
- the rule: *host-configured names outside the design set get none.* `permissive` is a built-in
268
- value, so 自动审查 already has a shield+eye glyph; `permissive-full` gets the same glyph only
269
- because `npx dsh-perm-gate-patch-glyph` adds it to that map. That patch edits a **host**
292
+ the rule: *host-configured names outside the design set get none.* Neither tier is a built-in value,
293
+ so both get an icon only because `npx dsh-perm-gate-patch-glyph` adds them to that map — 自动审查
294
+ copies the `workspace-write` glyph, 自动审查(高权限) the `danger-full-access` one, so each tier
295
+ shows the glyph of the file sandbox it actually shares. That patch edits a **host**
270
296
  package, so it is lost on every DSH upgrade — see
271
297
  [After a DSH upgrade](./INSTALL.md#after-a-dsh-upgrade-re-apply-the-composer-glyph-patch).
272
298
 
@@ -374,9 +400,10 @@ channel existed), with `tools/result` settling the same ask as a fallback when t
374
400
  correlate it. Approvals report the post-approval learning progress (`n`/threshold), and the notice
375
401
  strip labels all three terminal states.
376
402
 
377
- 自动审查 and 自动审查(高权限) both draw the shield+eye glyph in the picker — the first from DSH's
378
- built-in map, the second from the host patch the installation guide describes. Without that patch
379
- the second tier is text-only on every surface; its label and its gating are unaffected.
403
+ 自动审查 and 自动审查(高权限) both draw a glyph in the picker — copied from the built-in tiers whose
404
+ file sandbox they share (`workspace-write` and `danger-full-access`) by the host patch the
405
+ installation guide describes. Without that patch both tiers are text-only on every surface; their
406
+ labels and their gating are unaffected.
380
407
 
381
408
  ### A selectable session tier
382
409
 
package/README.zh.md CHANGED
@@ -39,7 +39,7 @@
39
39
  > 线上都是 user-approval 服务的**私有**方法,因此通过 `typeof` 探测读取,缺失
40
40
  > 或抛错时降级为「策略未知」。
41
41
 
42
- 版本 **2.4.1** —— 变更见 [Changelog](./CHANGELOG.md)。
42
+ 版本 **4.1.2** —— 变更见 [Changelog](./CHANGELOG.md)。
43
43
 
44
44
  一个**单一自足、确定性优先、fail-closed** 的 DeepSeek Harness 权限门插件。
45
45
 
@@ -65,6 +65,27 @@
65
65
  - **自动审查档位**(机器值 `permissive`)——一个**独立审批模式**(区别于只读、完全权限与白名单档),既不是"自动审批",也不授予泛化权限。前端只暴露**一个开关**(`permissive`),后台四个审批策略**可组合**、由插件设置决定——仍对 P0 保持 fail-closed。权限下拉框与设置行都按产品名「自动审查」显示;图标见下文(内置档自带,插件档需补丁)。
66
66
  - **沙箱提权自动答复**(`trustEscalation`)— 沙箱提权是从 shell / pwsh / edit 工具**体内部**(`tools/pre-execute` 之后)发出的,所以门禁从未见过它,一个它自动放行的调用仍会弹出确认。开启后,门禁以 `callId` 精确匹配已放行调用并直接答复。
67
67
 
68
+ ## 界面预览
69
+
70
+ **设置卡片 —— 自动审查档位**:前端只暴露一个开关,后台四个策略可组合;`LlmAssist` 可跟随会话当前模型,也可钉住 Provider / Model,并自带健康测试与裁决学习:
71
+
72
+ ![设置卡片 —— 自动审查档位](assets/setting-head.png)
73
+ ![LlmAssist —— Provider / Model / API Key](assets/llms.png)
74
+
75
+ **门禁实际加载的规则**——直接读自 `dsh-perm-gate-rules` 设置命名空间;面板按设计只读,改不了它显示的内容:
76
+
77
+ ![内置规则(只读)](assets/setting-rules.png)
78
+
79
+ **权限审批记录**——本会话每一次裁决,最新在上,附带模型看到的理由:
80
+
81
+ ![权限审批记录](assets/permlist.png)
82
+
83
+ **裁决条**——自动放行会写明依据(只读工具,或 LLM 判定 `safe`);停摆也绝不静默:
84
+
85
+ ![ALLOW 条 —— 只读内置工具](assets/allow1.png)
86
+ ![ALLOW 条 —— llm-assist 判定 safe](assets/allow2.png)
87
+ ![GATE OFF 条 —— 门禁停摆](assets/gateoff.png)
88
+
68
89
  ## 安装
69
90
 
70
91
  需要先安装 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)。
@@ -180,9 +201,10 @@ MSYS2/Cygwin 的 `sh.exe`、ConPTY 都会以 `Win32 error 5` / `couldn't create
180
201
  标签,因此 `cordis.patch.yml` 直接写中文名,对所有会话一致。
181
202
 
182
203
  **图标是另一回事。** 输入栏的图标表是闭合的,表自己的注释写明了规则:*host-configured names
183
- outside the design set get none*。`permissive` 是内置值,所以「自动审查」本来就有盾+眼图标;
184
- 「自动审查(高权限)」能拿到同一个图标,靠的是 `npx dsh-perm-gate-patch-glyph` 往那张表里
185
- 加了一项。该补丁改的是**宿主**包,每次 DSH 升级都会丢 —— 见
204
+ outside the design set get none*。两个档位都不是内置值,图标全靠
205
+ `npx dsh-perm-gate-patch-glyph` 往那张表里补两项 ——「自动审查」复制 `workspace-write` 的
206
+ 盾+铅笔,「自动审查(高权限)」复制 `danger-full-access` 的盾+感叹号,各自对应自己实际共用的
207
+ 文件沙箱。该补丁改的是**宿主**包,每次 DSH 升级都会丢 —— 见
186
208
  [DSH 升级后:重打输入区图标补丁](./INSTALL.zh.md)。
187
209
 
188
210
  `cordis.yml`:
@@ -275,9 +297,9 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
275
297
  调用文本命中任一关键词(大小写不敏感子串)即直接拒绝,且先于白名单 / 授权 / LLM。黑名单在设置
276
298
  卡片中按列表查看与增删(预置条目带标签,可一键恢复预置);未设置或为空时应用预置列表——黑名单
277
299
  不会静默关闭。
278
- 「自动审查」与「自动审查(高权限)」在选择器里都画盾+眼图标 —— 前者来自 DSH 内置表,后者来自
279
- 安装指南里描述的那次宿主补丁。没有该补丁时,第二个档位在所有界面上都是纯文字;它的标签与门禁
280
- 不受影响。
300
+ 「自动审查」与「自动审查(高权限)」在选择器里都画图标 —— 图标由安装指南里描述的那次宿主补丁
301
+ 从各自共用的文件沙箱档位复制而来(`workspace-write` 与 `danger-full-access`)。没有该补丁时,
302
+ 两个档位在所有界面上都是纯文字;它们的标签与门禁不受影响。
281
303
 
282
304
 
283
305
  ## CLI(独立 dry-run)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/cordis.patch.yml CHANGED
@@ -17,14 +17,17 @@
17
17
  # The composer's permission picker draws an icon per preset from a CLOSED map
18
18
  # inside @deepseek-ai/dsh-client-ui-conversation (`permissionGlyphs`), whose
19
19
  # source states the rule: "host-configured names outside the design set get
20
- # none". DSH ships a shield+eye glyph for `permissive`, so 自动审查 has an icon,
21
- # but a second plugin tier has none — it falls back to text in the trigger. The
22
- # map is a closure variable with no plugin-facing seam (the option objects the
23
- # host supplies carry only {value, name, description}), so the only way to give
24
- # BOTH tiers the same glyph is to extend that map.
25
- # `scripts/patch-permission-glyph.mjs` does exactly that, duplicating the
26
- # `permissive` entry under `permissive-full`. It patches a HOST package, so it
27
- # must be re-run after any DSH upgrade or reinstall.
20
+ # none". Neither tier is a built-in value, so DSH renders no glyph for either
21
+ # — the map is a closed closure variable and its only plugin-facing input is
22
+ # {value, name, description} on the option objects, which carries no icon. The
23
+ # only way to give a tier an icon is to add an entry to that map.
24
+ # `scripts/patch-permission-glyph.mjs` does exactly that, copying the glyph of
25
+ # the built-in tier whose file sandbox each of ours shares:
26
+ # permissive <- ["workspace-write", (same sandbox: workspace-write)
27
+ # permissive-full <- [FULL_ACCESS, (same sandbox: danger-full-access)
28
+ # The source key is RESOLVED, not assumed (0.1.2 keyed a `permissive` glyph that
29
+ # 0.1.5 dropped, so a literal anchor died with "anchor not found"). It patches a
30
+ # HOST package, so it must be re-run after any DSH upgrade or reinstall.
28
31
  #
29
32
  # The DSH bundle patch REPLACES the whole `permission.config.presets` map (it
30
33
  # does not merge per key), so this file MUST restate every tier that should
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-perm-gate",
3
3
  "name": "dsh-perm-gate",
4
- "version": "4.1.1",
4
+ "version": "4.1.2",
5
5
  "description": {
6
6
  "en": "Deterministic-first, fail-closed permission gate for DeepSeek Harness 0.1.2+: a P0 hard-deny -> P1 precise session grant -> P2 static allow/deny rule chain -> P3 optional LLM classifier -> P4 ask pipeline over the tools/pre-execute waterfall, with a standalone CLI dry-run evaluator and an auditable decision log. It only vetoes with deny/ask and always delegates otherwise, and unknown or malformed rules fail loud instead of silently standing the gate down.",
7
7
  "zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-perm-gate",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Add `permissive-full` to the DSH composer's permission-glyph map.
3
+ * Give this plugin's own permission tiers the composer glyphs the host will not
4
+ * draw for them.
4
5
  *
5
6
  * WHY THIS EXISTS
6
7
  * ---------------
@@ -9,22 +10,40 @@
9
10
  *
10
11
  * const permissionGlyphs = new Map([
11
12
  * ["read-only", <svg …/>],
12
- * ["permissive", <svg …/>], // the 自动审查 tier's shield+eye
13
13
  * ["workspace-write", <svg …/>],
14
- * ["danger-full-access", <svg …/>],
14
+ * [FULL_ACCESS, <svg …/>], // const FULL_ACCESS = "danger-full-access"
15
15
  * ]);
16
16
  * function permissionGlyph(value) { return permissionGlyphs.get(value); }
17
17
  *
18
18
  * The source comment states the rule: "host-configured names outside the design
19
19
  * set get none." There is no plugin-facing seam — the option objects the host
20
20
  * supplies carry only `{value, name, description}`, and the map itself is a
21
- * closure variable. So a plugin-contributed tier such as `permissive-full`
22
- * renders with no icon in the dropdown and falls back to plain text in the
23
- * trigger, while the built-in `permissive` tier shows the shield+eye.
21
+ * closure variable. So a plugin-contributed tier renders with no icon in the
22
+ * dropdown and falls back to plain text in the trigger.
24
23
  *
25
- * The only way to give our tier the same glyph is to add an entry to that map.
26
- * This script does exactly that: it duplicates the `permissive` entry under the
27
- * `permissive-full` key, so the two tiers of one plugin look alike.
24
+ * The only way to give our tiers the same glyph is to add entries to that map.
25
+ * This script does exactly that — one glyph per tier, copied from the built-in
26
+ * tier whose file sandbox the plugin tier shares:
27
+ *
28
+ * permissive sandbox: workspace-write <- the ["workspace-write",] entry
29
+ * permissive-full sandbox: danger-full-access <- the [FULL_ACCESS,] entry
30
+ *
31
+ * WHY THE SOURCES ARE DECLARED AND THE ANCHOR IS RESOLVED
32
+ * -------------------------------------------------------
33
+ * DSH 0.1.2 shipped four glyphs, `permissive` among them; 0.1.5-rc.2 ships three
34
+ * (`read-only` / `workspace-write` / `danger-full-access`) and the string
35
+ * `permissive` no longer occurs anywhere in the bundle — so a splice anchored on
36
+ * `["permissive",` dies with "anchor not found" on the current host. Two
37
+ * properties keep this working across such a design-set change:
38
+ *
39
+ * 1. the source key is a declaration (`GLYPH_TARGETS`) rather than a literal
40
+ * buried in the splice, so re-pointing a tier is a one-line edit; and
41
+ * 2. the anchor for a key is RESOLVED, not assumed to be a string literal:
42
+ * `["<key>",` first, then the constant alias the bundle may use instead
43
+ * (`[FULL_ACCESS,` for `"danger-full-access"`).
44
+ *
45
+ * When a declared source key is gone, the refusal names the keys the map DOES
46
+ * carry, so the next drift is diagnosed from the error text alone.
28
47
  *
29
48
  * VOLUNTARY, NOT AUTOMATIC
30
49
  * ------------------------
@@ -42,8 +61,11 @@
42
61
  * npx dsh-perm-gate-patch-glyph --check # report only; exit 1 if missing
43
62
  * node scripts/patch-permission-glyph.mjs <path/to/client.js>
44
63
  *
45
- * With no argument it probes the usual global-install locations. The script is
46
- * idempotent (a second run is a no-op), backs the file up once, and refuses to
64
+ * With no argument it probes the profile scopes first and the CLI install second,
65
+ * and lists any other DSH install that carries the same bundle — on a host with
66
+ * both, only one of them serves the UI, so the explicit-path form is the answer
67
+ * when the probe picks the wrong one. The script is idempotent (a second run is a
68
+ * no-op), backs the file up once, applies every target or none, and refuses to
47
69
  * write a bundle it cannot slice correctly, so it can never half-apply.
48
70
  */
49
71
  import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs'
@@ -52,13 +74,23 @@ import { homedir } from 'node:os'
52
74
  import { dirname, join, resolve } from 'node:path'
53
75
  import { pathToFileURL } from 'node:url'
54
76
 
55
- /** The map key whose entry is cloned, and the key it is cloned onto. */
56
- export const SOURCE_KEY = 'permissive'
57
- export const TARGET_KEY = 'permissive-full'
58
-
59
77
  /** Relative path from a package root to the bundle holding the map. */
60
78
  const BUNDLE = join('@deepseek-ai', 'dsh-client-ui-conversation', 'lib', 'client.js')
61
79
 
80
+ /**
81
+ * The glyphs to add: each plugin tier, and the built-in tier it copies.
82
+ *
83
+ * The pairing is by file-sandbox meaning, not by name similarity — `permissive`
84
+ * runs in the same sandbox as `workspace-write`, `permissive-full` in the same
85
+ * sandbox as `danger-full-access` — so the icon a user sees matches the access
86
+ * the tier actually grants. A tier listed here gets that built-in's entry
87
+ * verbatim apart from its key.
88
+ */
89
+ export const GLYPH_TARGETS = [
90
+ { target: 'permissive', source: 'workspace-write' },
91
+ { target: 'permissive-full', source: 'danger-full-access' },
92
+ ]
93
+
62
94
  /** Thrown for every refusal; the CLI turns it into an exit code 1. */
63
95
  export class GlyphPatchError extends Error {}
64
96
 
@@ -66,6 +98,15 @@ export class GlyphPatchError extends Error {}
66
98
  * Candidate bundle paths, most specific first. Pure: no filesystem access, so
67
99
  * the probe order is unit-testable.
68
100
  *
101
+ * Profile scope before CLI-install scope, and that order is load-bearing on a
102
+ * machine with more than one DSH: a profile's UI is served by the profile's own
103
+ * dependency scope, not by whichever global CLI install happens to sit beside the
104
+ * running `node`. Measured on this project's own host — a global install under
105
+ * `dirname(node)` existed and shadowed the profile's bundle under an older probe
106
+ * order, so the patch reported success while the picker stayed icon-free. On a
107
+ * stock single-install machine every candidate is one inode, so the order decides
108
+ * nothing there; it only matters exactly when guessing wrong is silent.
109
+ *
69
110
  * `execDir` is `dirname(process.execPath)`. Deriving from the running node
70
111
  * binary — never from a hardcoded nvm version or install path — is what keeps
71
112
  * the script working across node upgrades and re-pointed install symlinks.
@@ -75,19 +116,111 @@ export class GlyphPatchError extends Error {}
75
116
  */
76
117
  export function candidateBundles({ execDir, cwd, dshHome }) {
77
118
  return [
78
- // 1. `@deepseek-ai` scope hoisted beside the running node binary.
79
- join(execDir, 'node_modules', BUNDLE),
80
- // 2. The same scope nested inside the `dsh` package itself.
81
- join(execDir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', BUNDLE),
82
- // 3. Whatever profile the caller is standing in.
119
+ // 1. Whatever profile the caller is standing in.
83
120
  join(cwd, 'node_modules', BUNDLE),
84
- // 4. The hoisted scope shared by every profile under DSH_HOME.
121
+ // 2. The hoisted scope every profile under DSH_HOME shares.
85
122
  join(dshHome, 'profiles', 'node_modules', BUNDLE),
123
+ // 3. `@deepseek-ai` scope hoisted beside the running node binary.
124
+ join(execDir, 'node_modules', BUNDLE),
125
+ // 4. The same scope nested inside the `dsh` package itself.
126
+ join(execDir, 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', BUNDLE),
86
127
  ]
87
128
  }
88
129
 
89
130
  /**
90
- * Extract the full `["<key>", …]` map entry, by bracket depth.
131
+ * In-page string constants (`const FULL_ACCESS = "danger-full-access"`), as
132
+ * name -> value. First declaration wins, so a later reassignment cannot shadow
133
+ * the spelling the map was built with above it.
134
+ *
135
+ * The bundle may key a map entry by such a name rather than by a literal, which
136
+ * is the whole reason the anchor is resolved instead of concatenated.
137
+ *
138
+ * @param {string} source
139
+ * @returns {Map<string, string>} constant name -> its string value
140
+ */
141
+ export function pageConstants(source) {
142
+ const constants = new Map()
143
+ for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*"([^"]*)"/g)) {
144
+ if (!constants.has(match[1])) constants.set(match[1], match[2])
145
+ }
146
+ return constants
147
+ }
148
+
149
+ /**
150
+ * The `permissionGlyphs` map's text, brackets included.
151
+ *
152
+ * Parsed rather than assumed: the map is a `new Map([ … ])` closure variable, so
153
+ * its extent is found by bracket depth from its `[`. Reading the map (instead of
154
+ * only probing for one anchor inside it) is what lets a refusal report the keys
155
+ * the host actually renders.
156
+ *
157
+ * @param {string} source
158
+ * @returns {string} the map array text, from `[` to the matching `]`
159
+ */
160
+ export function glyphMapText(source) {
161
+ const start = source.search(/permissionGlyphs\s*=\s*new Map\(\s*\[/)
162
+ if (start === -1) throw new GlyphPatchError('permissionGlyphs map not found in this bundle')
163
+ const open = source.indexOf('[', start)
164
+ let depth = 0
165
+ for (let i = open; i < source.length; i += 1) {
166
+ const ch = source[i]
167
+ if (ch === '[' || ch === '(' || ch === '{') depth += 1
168
+ else if (ch === ']' || ch === ')' || ch === '}') {
169
+ depth -= 1
170
+ if (depth === 0) return source.slice(open, i + 1)
171
+ }
172
+ }
173
+ throw new GlyphPatchError('unterminated permissionGlyphs map')
174
+ }
175
+
176
+ /**
177
+ * The preset values the map renders a glyph for — literals as-is, and entries
178
+ * keyed by an in-page constant resolved to that constant's value.
179
+ *
180
+ * @param {string} source
181
+ * @returns {string[]} e.g. `['read-only', 'workspace-write', 'danger-full-access']`
182
+ */
183
+ export function glyphKeys(source) {
184
+ const constants = pageConstants(source)
185
+ const keys = []
186
+ for (const match of glyphMapText(source).matchAll(/\[\s*(?:"([^"]+)"|([A-Za-z_$][\w$]*))\s*,/g)) {
187
+ if (match[1] !== undefined) keys.push(match[1])
188
+ else keys.push(constants.get(match[2]) ?? match[2])
189
+ }
190
+ return keys
191
+ }
192
+
193
+ /**
194
+ * Resolve the anchor text of one map entry, literal first, constant alias second.
195
+ *
196
+ * @param {string} source the bundle's text
197
+ * @param {string} key the preset value, e.g. `danger-full-access`
198
+ * @returns {{ anchor: string, index: number }}
199
+ * @throws {GlyphPatchError} when the host renders no glyph for `key`
200
+ */
201
+ export function resolveAnchor(source, key) {
202
+ for (const anchor of [`["${key}",`, `['${key}',`]) {
203
+ const index = source.indexOf(anchor)
204
+ if (index !== -1) return { anchor, index }
205
+ }
206
+ const name = [...pageConstants(source)].find(([, value]) => value === key)?.[0]
207
+ if (name !== undefined) {
208
+ const anchor = `[${name},`
209
+ const index = source.indexOf(anchor)
210
+ if (index !== -1) return { anchor, index }
211
+ throw new GlyphPatchError(
212
+ `anchor not found: ${anchor} (the alias for "${key}")`,
213
+ )
214
+ }
215
+ throw new GlyphPatchError(
216
+ `this host renders no glyph for "${key}" — the map carries: ` +
217
+ `${glyphKeys(source).map((k) => `"${k}"`).join(', ')}\n` +
218
+ ' Re-point GLYPH_TARGETS at a key the map does carry.',
219
+ )
220
+ }
221
+
222
+ /**
223
+ * Extract the full `["<key>", …]` (or `[CONST, …]`) map entry, by bracket depth.
91
224
  *
92
225
  * The scan starts ON the entry's opening `[` (the one immediately before the
93
226
  * key), so depth returns to zero exactly at that entry's closing `]`. Starting
@@ -98,12 +231,12 @@ export function candidateBundles({ execDir, cwd, dshHome }) {
98
231
  * contain no brackets.
99
232
  *
100
233
  * @param {string} source
101
- * @param {string} key
234
+ * @param {string} anchor the text returned by {@link resolveAnchor}
102
235
  * @returns {string} the entry, brackets included
103
236
  */
104
- export function sliceEntry(source, key) {
105
- const start = source.indexOf(`["${key}",`)
106
- if (start === -1) throw new GlyphPatchError(`anchor not found: ["${key}",`)
237
+ export function sliceEntry(source, anchor) {
238
+ const start = source.indexOf(anchor)
239
+ if (start === -1) throw new GlyphPatchError(`anchor not found: ${anchor}`)
107
240
  let depth = 0
108
241
  for (let i = start; i < source.length; i += 1) {
109
242
  const ch = source[i]
@@ -113,24 +246,41 @@ export function sliceEntry(source, key) {
113
246
  if (depth === 0) return source.slice(start, i + 1)
114
247
  }
115
248
  }
116
- throw new GlyphPatchError(`unterminated entry for ["${key}",`)
249
+ throw new GlyphPatchError(`unterminated entry for ${anchor}`)
117
250
  }
118
251
 
119
252
  /**
120
- * Clone the `permissive` glyph entry onto the `permissive-full` key.
253
+ * Splice every declared glyph into the map.
254
+ *
255
+ * All or nothing: a source key this host does not carry throws before the caller
256
+ * can write anything, so a partially-glyphed bundle is impossible. Idempotent
257
+ * per target — a target already present is left untouched and reported as such.
121
258
  *
122
259
  * @param {string} source the bundle's text
123
- * @returns {{ changed: boolean, source: string, entry: string | null }}
124
- * `changed: false` means the target key was already present (idempotent no-op).
260
+ * @returns {{ changed: boolean, source: string, added: string[], already: string[] }}
125
261
  */
126
262
  export function applyGlyphPatch(source) {
127
- if (source.includes(`["${TARGET_KEY}",`)) return { changed: false, source, entry: null }
128
- const sourceEntry = sliceEntry(source, SOURCE_KEY)
129
- if (!sourceEntry.includes('svg')) {
130
- throw new GlyphPatchError(`the ["${SOURCE_KEY}",] entry does not look like a glyph`)
263
+ let next = source
264
+ const added = []
265
+ const already = []
266
+
267
+ for (const { target, source: sourceKey } of GLYPH_TARGETS) {
268
+ if (next.includes(`["${target}",`)) {
269
+ already.push(target)
270
+ continue
271
+ }
272
+ const { anchor } = resolveAnchor(next, sourceKey)
273
+ const sourceEntry = sliceEntry(next, anchor)
274
+ if (!sourceEntry.includes('svg')) {
275
+ throw new GlyphPatchError(`the ${anchor} entry does not look like a glyph`)
276
+ }
277
+ const clonedEntry = sourceEntry.replace(anchor, `["${target}",`)
278
+ // A function replacement: the entry is spliced verbatim, `$` never expanded.
279
+ next = next.replace(sourceEntry, () => `${sourceEntry},\n\t\t\t${clonedEntry}`)
280
+ added.push(target)
131
281
  }
132
- const clonedEntry = sourceEntry.replace(`["${SOURCE_KEY}",`, `["${TARGET_KEY}",`)
133
- return { changed: true, source: source.replace(sourceEntry, `${sourceEntry},\n\t\t\t${clonedEntry}`), entry: clonedEntry }
282
+
283
+ return { changed: added.length > 0, source: next, added, already }
134
284
  }
135
285
 
136
286
  /** Resolve the bundle to patch, or throw with an explanation. */
@@ -151,8 +301,29 @@ export function findBundle(argv, where) {
151
301
  )
152
302
  }
153
303
 
304
+ /**
305
+ * A warning block naming the other DSH installs that carry this bundle.
306
+ *
307
+ * Reported rather than patched: on a host with a global CLI install and a
308
+ * launcher-managed runtime, only one of them serves the UI in front of the user,
309
+ * and picking the wrong one is invisible — the patch succeeds and the picker
310
+ * stays icon-free. Naming the others turns that into a one-line fix (pass the
311
+ * path explicitly).
312
+ *
313
+ * @param {string[]} others bundle paths that exist but were not chosen
314
+ * @returns {string} an empty string when there is nothing to report
315
+ */
316
+ function describeOtherInstalls(others) {
317
+ if (others.length === 0) return ''
318
+ return ` note: ${others.length} other DSH install(s) carry this bundle — NOT patched:\n` +
319
+ others.map((path) => ` ${path}\n`).join('') +
320
+ ' If the tier still has no icon, the UI is served by one of those; re-run with\n' +
321
+ ' that path as the argument.\n'
322
+ }
323
+
154
324
  function main(argv) {
155
325
  const check = argv.includes('--check')
326
+ const explicit = argv.slice(2).find((arg) => !arg.startsWith('-'))
156
327
  const where = {
157
328
  execDir: dirname(process.execPath),
158
329
  cwd: process.cwd(),
@@ -160,17 +331,27 @@ function main(argv) {
160
331
  }
161
332
 
162
333
  const bundle = findBundle(argv, where)
334
+ const others = explicit === undefined
335
+ ? candidateBundles(where).filter((candidate) => candidate !== bundle && existsSync(candidate))
336
+ : []
337
+ const otherNote = describeOtherInstalls(others)
163
338
  const original = readFileSync(bundle, 'utf8')
164
- const { changed, source: patched } = applyGlyphPatch(original)
339
+ const { changed, source: patched, added, already } = applyGlyphPatch(original)
165
340
 
166
341
  if (!changed) {
167
- process.stdout.write(`patch-permission-glyph: ["${TARGET_KEY}",] already present in\n ${bundle}\n`)
342
+ process.stdout.write(
343
+ `patch-permission-glyph: all glyphs already present in\n ${bundle}\n` +
344
+ ` present: ${already.map((key) => `["${key}",]`).join(' ')}\n` +
345
+ otherNote,
346
+ )
168
347
  return 0
169
348
  }
170
349
  if (check) {
171
350
  process.stdout.write(
172
- `patch-permission-glyph: ["${TARGET_KEY}",] is MISSING from\n ${bundle}\n` +
173
- ' (--check: nothing written; re-run without --check to apply)\n',
351
+ `patch-permission-glyph: MISSING ${added.map((key) => `["${key}",]`).join(' ')} from\n ${bundle}\n` +
352
+ ` present: ${already.length > 0 ? already.map((key) => `["${key}",]`).join(' ') : '(none)'}\n` +
353
+ ' (--check: nothing written; re-run without --check to apply)\n' +
354
+ otherNote,
174
355
  )
175
356
  return 1
176
357
  }
@@ -189,9 +370,11 @@ function main(argv) {
189
370
  }
190
371
 
191
372
  process.stdout.write(
192
- `patch-permission-glyph: added ["${TARGET_KEY}",] to\n ${bundle}\n` +
373
+ `patch-permission-glyph: added ${added.map((key) => `["${key}",]`).join(' ')} to\n ${bundle}\n` +
374
+ already.map((key) => ` already present: ["${key}",]\n`).join('') +
193
375
  ` backup: ${backup}\n` +
194
- ' NOTE: this edits a DSH package — re-run after any DSH upgrade or reinstall.\n',
376
+ ' NOTE: this edits a DSH package — re-run after any DSH upgrade or reinstall.\n' +
377
+ otherNote,
195
378
  )
196
379
  return 0
197
380
  }