dsh-perm-gate 0.2.0 → 0.2.1-beta.3
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 +110 -1
- package/CHANGELOG.ko.md +108 -1
- package/CHANGELOG.md +159 -1
- package/INSTALL.ja.md +3 -3
- package/INSTALL.ko.md +3 -3
- package/INSTALL.md +4 -3
- package/INSTALL.zh.md +3 -3
- package/README.ja.md +34 -11
- package/README.ko.md +42 -13
- package/README.md +82 -17
- package/README.zh.md +48 -13
- package/cordis.patch.yml +19 -12
- package/lib/audit.d.ts +1 -1
- package/lib/classifier.d.ts +2 -0
- package/lib/classifier.js +7 -4
- package/lib/client.js +1786 -53
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +63 -4
- package/lib/config.js +50 -2
- package/lib/deny-defaults.d.ts +12 -0
- package/lib/deny-defaults.js +19 -0
- package/lib/engine.d.ts +13 -1
- package/lib/engine.js +85 -8
- package/lib/evaluate.d.ts +10 -0
- package/lib/evaluate.js +101 -11
- package/lib/events.d.ts +140 -4
- package/lib/events.js +578 -13
- package/lib/host-llm.d.ts +61 -0
- package/lib/host-llm.js +52 -0
- package/lib/index.d.ts +31 -2
- package/lib/index.js +289 -31
- package/lib/learning.d.ts +19 -2
- package/lib/learning.js +47 -5
- package/lib/llm-presets.d.ts +16 -0
- package/lib/llm-presets.js +11 -0
- package/lib/preset.d.ts +30 -0
- package/lib/preset.js +41 -0
- package/lib/receiver-info.d.ts +76 -0
- package/lib/receiver-info.js +70 -0
- package/lib/risk.d.ts +16 -1
- package/lib/risk.js +15 -3
- package/lib/runtime.d.ts +210 -4
- package/lib/runtime.js +589 -33
- package/package.json +6 -5
package/README.ja.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
> **互換性に関する注記:** v0.1.
|
|
15
|
+
> **互換性に関する注記:** v0.2.1-beta.3 は `ja` / `ko` 辞書を同梱しますが、公式 DSH の
|
|
16
16
|
> `LocaleRuntime` が公開するのは `zh` / `en` のみです(`LOCALE_IDS = ["zh", "en"]`)。
|
|
17
17
|
> 素の DSH で `ja` / `ko` を選択すると `locale "<id>" is not registered` になります。
|
|
18
18
|
> `LOCALE_IDS`(locale-settings.ts)と `LOCALES` ラベル(client/index.ts)を更新した
|
|
19
19
|
> DSH fork を使って再ビルドしてください。
|
|
20
20
|
|
|
21
|
-
バージョン **0.1.
|
|
21
|
+
バージョン **0.2.1-beta.3** — 変更履歴は [日本語 changelog](./CHANGELOG.ja.md) を参照。
|
|
22
22
|
|
|
23
23
|
DeepSeek Harness 向けの、単一・自己完結・決定論優先・fail-closed な権限ゲートです。
|
|
24
24
|
|
|
@@ -81,9 +81,10 @@ dsh plugin --profile web add dsh-perm-gate
|
|
|
81
81
|
- id: dsh-perm-gate
|
|
82
82
|
name: dsh-perm-gate
|
|
83
83
|
config:
|
|
84
|
-
rulesFile: ./permissions.yaml #
|
|
84
|
+
rulesFile: ./permissions.yaml # 任意。既定は $DSH_HOME/perm-gate/rules.yml
|
|
85
85
|
dshHome: $DSH_HOME # 保護対象チェックのルート固定
|
|
86
86
|
defaultAction: ask # allow | ask | deny
|
|
87
|
+
gatePresets: [permissive] # ゲートが有効なティア(既定)
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
### ルール ファイル
|
|
@@ -141,18 +142,34 @@ Whitelist と並ぶ**独立した承認ティア**です。「自動承認」で
|
|
|
141
142
|
(`approveRepeat`、有界なセッション許可)と**すべての発生を許可**
|
|
142
143
|
(`approveAllowEverywhere`、コマンド語を `permissions.yaml` の allow ホワイトリストへ
|
|
143
144
|
永続化して再読込)。`llmAssist` は設定済みの実際の LLM
|
|
144
|
-
|
|
145
|
+
(受信先は設定カードで選択:**カスタム API**(`classifierEndpoint` / `classifierModel`。OpenAI 互換 API なら何でも可、Xiaomi MiMo `https://api.xiaomimimo.com/v1` 等のプリセット付き)または**ホストモデルグループ**(DSH の `llm` サービスと現在のモデルグループ、`classifierProvider` / `classifierModel` で上書き可)。**健全性テスト**ボタンで受信 LLM の疎通とレイテンシを確認可能)に
|
|
145
146
|
`ask` の自動判定を委ね、`ask`/エラー時は人手のシームへフォールバックします —
|
|
146
147
|
常に fail-closed です。`permissive` がオフなら、ゲートの挙動は以前と完全に同じです。
|
|
147
148
|
|
|
148
149
|
### 選択可能なセッション ティア
|
|
149
150
|
|
|
150
|
-
`cordis.patch.yml` は DSH の `permission.config.presets`
|
|
151
|
+
`cordis.patch.yml` は DSH の `permission.config.presets` に、Workspace Write と
|
|
151
152
|
Full access の間に `permissive` preset(`sandbox: workspace-write`、`approval: ask`、
|
|
152
|
-
名称 **Permissive**)を追加します。
|
|
153
|
+
名称 **Permissive**)を追加します。DSH の bundle patch はこの map を**全体置換**するため
|
|
154
|
+
(キー単位のマージではありません)、組み込み 3 ティア
|
|
155
|
+
(`read-only` / `workspace-write` / `danger-full-access`、
|
|
156
|
+
`@deepseek-ai/dsh-base/cordis.patch.yml` 由来)も再記載する必要があり、
|
|
157
|
+
`test/patch-presets.spec.ts` がそのキー集合を固定しています。したがって
|
|
153
158
|
セッションの権限ピッカーには「auto-approval」ではなく、**独立して選択できる承認
|
|
154
159
|
ティア**として Permissive が並びます。
|
|
155
160
|
|
|
161
|
+
ゲートが動作するのは **`gatePresets` に列挙したティアの中だけ**です(既定 `['permissive']`、
|
|
162
|
+
このプラグインが追加するティア)。それ以外のティア(Read Only / Workspace Write /
|
|
163
|
+
Full access / `custom`)では、ゲートの判定フローは**一切実行されません**——許可も、ask も、
|
|
164
|
+
拒否も、P0 ハード拒否も、拒否キーワードの遮断も、監査イベントの記録も行いません。選択された
|
|
165
|
+
ティア自身の方針が呼び出しを決めます。`danger-full-access` の定義は「承認プロンプトなしの
|
|
166
|
+
フルアクセス」であり、そこを ask で上書きしても意味がありません(その方針では DSH の承認シームが
|
|
167
|
+
**どの answerer よりも先に** `rejected` を返し、転送された ask はパネルを一度も表示せず
|
|
168
|
+
`the user rejected tool "..."` にしかなりません)。ハード拒否で上書きするのも、ユーザーが選んだ
|
|
169
|
+
ティアを黙って覆すことになります。`gatePresets: ['*']` でゲートを再び全体(ハード拒否を含む)に
|
|
170
|
+
適用できます。有効なティア内では、セッションの実効承認方針が `never` の場合 ask はパススルーに
|
|
171
|
+
降格します。
|
|
172
|
+
|
|
156
173
|
### UI から設定可能
|
|
157
174
|
|
|
158
175
|
このティアは実行時にも **設定 → プラグイン → Permissive 承認ティア** から調整できます
|
|
@@ -164,14 +181,20 @@ Full access の間に `permissive` preset(`sandbox: workspace-write`、`approv
|
|
|
164
181
|
|
|
165
182
|
### リスク判定 llmAssist、裁決学習、イベントフィード
|
|
166
183
|
|
|
167
|
-
`llmAssist` 有効時、設定された LLM(OpenAI 互換エンドポイントなら任意——`classifierEndpoint` / `classifierModel` / `classifierApiKey` を自分の API に向けてください)が構造化プロトコルで `ask` を 1
|
|
184
|
+
`llmAssist` 有効時、設定された LLM(OpenAI 互換エンドポイントなら任意——`classifierEndpoint` / `classifierModel` / `classifierApiKey` を自分の API に向けてください)が構造化プロトコルで `ask` を 1 件ずつ判定します。**判定はゲートの `tools/pre-execute` ウォーターフォール内、決定がホストへ返る前に行われます**:`safe` はそのまま委譲されるため承認パネルは一切表示されず、本当に判断できない判定だけが人手に回ります。
|
|
168
185
|
|
|
169
|
-
- `safe` → 自動許可(監査ソースは `classifier
|
|
170
|
-
- `risky` + **ハードリスクカテゴリ**(`deletion`、`credential`、`remote`、`system`、`bulk`)→
|
|
171
|
-
- `risky:neutral` → `riskLearning` 有効時(設定カード内、既定オフ)、人手で承認され実際に実行された neutral リスクは `tool|カテゴリ` ごとにカウントされ、`riskThreshold`(既定 3
|
|
186
|
+
- `safe` → 自動許可(監査ソースは `classifier`)。パネルは表示されません。
|
|
187
|
+
- `risky` + **ハードリスクカテゴリ**(`deletion`、`credential`、`remote`、`system`、`bulk`)→ **自動拒否**、パネルなし。ハードリスクは自動許可も学習もされません。
|
|
188
|
+
- `risky:neutral` → `riskLearning` 有効時(設定カード内、既定オフ)、人手で承認され実際に実行された neutral リスクは `tool|カテゴリ` ごとにカウントされ、`riskThreshold`(既定 3)到達かつ新呼び出しの操作指紋(コマンド語+対象の基底名)が確認済みサンプルに一致した場合、**同一操作のみ**自動許可されます。学習沈殿(`riskSediment`、既定オン)を有効にすると、しきい値に達したキーの確認サンプルは**決定論的な許可ルール**になります:指紋の正確一致は LLM を経由せず自動許可——llmAssist オフでも継続し、沈殿ルールは設定カードで確認・管理(終止 / サンプル削除)できます。
|
|
172
189
|
- タイムアウト(`riskTimeoutMs`、既定 20 秒、1 回リトライ)、通信失敗、プロトコル外出力は元の `ask` を維持します。
|
|
173
190
|
|
|
174
|
-
学習状態はプラグイン所有の JSON(`$DSH_HOME/perm-gate/learning.json` または `learningFile`)に永続化され、YAML ルールには書き込みません。各決定は `$DSH_HOME/perm-gate/events.jsonl`(または `eventsFile`)に追記され、`GET /api/dsh-perm-gate/events?sessionId=&since=`
|
|
191
|
+
学習状態はプラグイン所有の JSON(`$DSH_HOME/perm-gate/learning.json` または `learningFile`)に永続化され、YAML ルールには書き込みません。各決定は `$DSH_HOME/perm-gate/events.jsonl`(または `eventsFile`)に追記され、`GET /api/dsh-perm-gate/events?sessionId=&since=` で提供されます。ブラウザ側はこれをポーリングし、入力欄の上に最新の決定を通知バーで表示するとともに、会話ビューの「承認記録」タブにセッション内の全判定を新しい順に一覧表示します。
|
|
192
|
+
|
|
193
|
+
各決定が触れたファイルは変更前に(1 イベントあたり ≤5 ファイル、各 ≤256 KB)`$DSH_HOME/perm-gate/snapshots/` へスナップショットされます。「承認記録」タブでは各ファイルチップから行単位の diff(`GET /api/dsh-perm-gate/diff`)を開き、**取り消し**(`POST /api/dsh-perm-gate/revert`)で会話に復元指示を送信できます。スナップショット管理バーはセッション単位または全件で削除できます(`GET /api/dsh-perm-gate/snapshots-stats` / `POST /api/dsh-perm-gate/snapshots-clear`)。
|
|
194
|
+
|
|
195
|
+
人手に回した `ask` は回答が返るまで追跡されます。受動的な `approval/request` オブザーバが閉じた結果を記録し(`allowed-once` → **承認**、`rejected` → **拒否**、`cancelled` → **キャンセル**、`unavailable` → 拒否=承認チャネルなし)、オブザーバが関連付けできない場合(`callId` 欠落・approval サービスなし・上流リスナーの短絡)は `tools/result` が同じ ask をフォールバックとして解決します。承認時には承認後の学習進捗(`n`/しきい値)を表示し、通知バーも 3 つの終態にラベルを付けます。
|
|
196
|
+
|
|
197
|
+
さらにプリセットの**拒否キーワード黑名単**(dsh-approval-gate の `DEFAULT_DENY_KEYWORDS` を継承:`rm -rf`、`push --force`、`drop table`、`mkfs`、`git reset --hard`、`docker system prune` など)を備え、テキストがキーワードを含む呼び出し(大小文字を区別しない部分一致)は許可リスト / 許可 / LLM より先に拒否します。設定カードでリストとして編集でき(プリセット項目にはタグ付き、ワンクリック復元対応)、未設定・空ならプリセットを適用します——黑名単が静かに無効化されることはありません。Permissive ティアは権限ピッカーに盾アイコンを保持します(メニュー項目と折りたたみトリガーの両方)。
|
|
175
198
|
|
|
176
199
|
|
|
177
200
|
## CLI(スタンドアロン dry-run)
|
package/README.ko.md
CHANGED
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
> **호환성 안내:** v0.1.
|
|
15
|
+
> **호환성 안내:** v0.2.1-beta.3은 `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를 사용해
|
|
19
19
|
> 다시 빌드하세요.
|
|
20
20
|
|
|
21
|
-
버전 **0.1.
|
|
21
|
+
버전 **0.2.1-beta.3** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
|
|
22
22
|
|
|
23
23
|
DeepSeek Harness용 단일·자족적·결정론 우선·fail-closed 권한 게이트입니다.
|
|
24
24
|
|
|
@@ -81,9 +81,10 @@ dsh plugin --profile web add dsh-perm-gate
|
|
|
81
81
|
- id: dsh-perm-gate
|
|
82
82
|
name: dsh-perm-gate
|
|
83
83
|
config:
|
|
84
|
-
rulesFile: ./permissions.yaml # 선택.
|
|
84
|
+
rulesFile: ./permissions.yaml # 선택. 기본값은 $DSH_HOME/perm-gate/rules.yml
|
|
85
85
|
dshHome: $DSH_HOME # 보호 대상 검사의 고정 루트
|
|
86
86
|
defaultAction: ask # allow | ask | deny
|
|
87
|
+
gatePresets: [permissive] # 게이트가 활성화되는 티어(기본)
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
### 규칙 파일
|
|
@@ -139,19 +140,36 @@ Permissive는 권한 선택기에서 Read Only / Workspace Write / Full access /
|
|
|
139
140
|
모든 경계에서 승인 패널을 띄우며, 그「허용 컨트롤」에 두 개의 확장 버튼을 추가합니다 —
|
|
140
141
|
**이 세션에서 해당 유형 반복 허용**(`approveRepeat`, 유계 세션 허용)과 **모든 발생
|
|
141
142
|
허용**(`approveAllowEverywhere`, 명령어를 `permissions.yaml`의 allow 허용 목록에
|
|
142
|
-
영속 기록 후 재로드). `llmAssist`는 설정된 실제 LLM(`classifierEndpoint` /
|
|
143
|
-
`classifierModel`, OpenAI 호환 API면 무엇이든
|
|
143
|
+
영속 기록 후 재로드). `llmAssist`는 설정된 실제 LLM(수신처는 설정 카드에서 선택: **사용자 지정 API**(`classifierEndpoint` /
|
|
144
|
+
`classifierModel`, OpenAI 호환 API면 무엇이든 가능, Xiaomi MiMo `https://api.xiaomimimo.com/v1` 등
|
|
145
|
+
프리셋 포함) 또는 **호스트 모델 그룹**(DSH의 `llm` 서비스와 현재 모델 그룹, `classifierProvider` /
|
|
146
|
+
`classifierModel`로 재정의 가능). **상태 테스트** 버튼으로 수신 LLM의 연결과 지연 시간을 확인할 수 있음)에 `ask` 자동 판정을 맡기고,
|
|
144
147
|
`ask`/오류 시 사람의 승인 심으로 폴백합니다 — 항상 fail-closed입니다.
|
|
145
148
|
`permissive`가 꺼져 있으면 게이트는 이전과 완전히 동일하게 동작합니다.
|
|
146
149
|
|
|
147
150
|
### 선택 가능한 세션 티어
|
|
148
151
|
|
|
149
|
-
`cordis.patch.yml`은 DSH의 `permission.config.presets
|
|
150
|
-
Full access
|
|
151
|
-
이름 **Permissive**)을 추가합니다.
|
|
152
|
+
`cordis.patch.yml`은 DSH의 `permission.config.presets`에 Workspace Write와
|
|
153
|
+
Full access 사이의 `permissive` preset(`sandbox: workspace-write`, `approval: ask`,
|
|
154
|
+
이름 **Permissive**)을 추가합니다. DSH의 bundle patch는 이 map을 **전체 교체**하므로
|
|
155
|
+
(키 단위 병합이 아닙니다) 내장 3개 티어
|
|
156
|
+
(`read-only` / `workspace-write` / `danger-full-access`,
|
|
157
|
+
`@deepseek-ai/dsh-base/cordis.patch.yml` 기준)도 다시 기재해야 하며,
|
|
158
|
+
`test/patch-presets.spec.ts`가 그 키 집합을 고정합니다. 따라서 세션 권한
|
|
152
159
|
선택기에는 "auto-approval"이 아니라 **독립적으로 선택 가능한 승인 티어**인 Permissive가
|
|
153
160
|
놓입니다.
|
|
154
161
|
|
|
162
|
+
게이트가 동작하는 범위는 **`gatePresets`에 나열한 티어 안뿐**입니다(기본 `['permissive']`,
|
|
163
|
+
이 플러그인이 추가하는 티어). 그 밖의 티어(Read Only / Workspace Write / Full access /
|
|
164
|
+
`custom`)에서는 게이트의 판정 흐름이 **전혀 실행되지 않습니다** — 허용도, ask도, 거부도,
|
|
165
|
+
P0 하드 거부도, 거부 키워드 차단도, 감사 이벤트 기록도 하지 않습니다. 선택한 티어의 자체 정책이
|
|
166
|
+
호출을 결정합니다. `danger-full-access`의 정의는 "승인 프롬프트 없는 전체 접근"이며, 이를 ask로
|
|
167
|
+
덮어써도 의미가 없습니다(그 정책에서는 DSH 승인 시임이 **어떤 answerer보다도 먼저** `rejected`를
|
|
168
|
+
반환해 전달된 ask는 패널을 한 번도 띄우지 못하고 `the user rejected tool "..."`만 남습니다).
|
|
169
|
+
하드 거부로 덮어쓰는 것 역시 사용자가 고른 티어를 조용히 뒤집는 일입니다.
|
|
170
|
+
`gatePresets: ['*']`는 게이트를 다시 전역(하드 거부 포함)으로 적용합니다. 유효한 티어 안에서는
|
|
171
|
+
세션의 유효 승인 정책이 `never`이면 ask가 패스스루로 강등됩니다.
|
|
172
|
+
|
|
155
173
|
### UI에서 설정 가능
|
|
156
174
|
|
|
157
175
|
이 티어는 실행 중에도 **설정 → 플러그인 → Permissive 승인 티어**에서 조정할 수 있습니다
|
|
@@ -162,14 +180,25 @@ host는 네임스페이스를 live로 읽으므로 변경은 재시작 없이
|
|
|
162
180
|
|
|
163
181
|
### 위험 등급 llmAssist, 판정 학습, 이벤트 피드
|
|
164
182
|
|
|
165
|
-
`llmAssist`가 켜져 있으면 설정된 LLM(OpenAI 호환 엔드포인트라면 무엇이든 — `classifierEndpoint` / `classifierModel` / `classifierApiKey`을 자신의 API로 지정)이 구조화된 프로토콜로 `ask`를 하나씩
|
|
183
|
+
`llmAssist`가 켜져 있으면 설정된 LLM(OpenAI 호환 엔드포인트라면 무엇이든 — `classifierEndpoint` / `classifierModel` / `classifierApiKey`을 자신의 API로 지정)이 구조화된 프로토콜로 `ask`를 하나씩 판정합니다. **판정은 게이트의 `tools/pre-execute` 워터폴 안에서, 결정이 호스트로 반환되기 전에 이루어집니다**: `safe`는 곧바로 위임되므로 승인 패널이 아예 표시되지 않고, 정말로 판단할 수 없는 판정만 사람에게 전달됩니다.
|
|
166
184
|
|
|
167
|
-
- `safe` → 자동 허용(감사 소스 `classifier`)
|
|
168
|
-
- `risky` + **하드 위험 카테고리**(`deletion`, `credential`, `remote`, `system`, `bulk`) →
|
|
169
|
-
- `risky:neutral` → `riskLearning` 활성 시(설정 카드, 기본 꺼짐), 사람이 승인하고 실제 실행된 neutral 위험은 `tool|카테고리` 키로 카운트되며, `riskThreshold`(기본 3) 도달 및 새 호출의 작업 지문(명령어 단어 + 대상 기본 이름)이 확인된 샘플과 일치하면 **동일 작업만** 자동 허용됩니다.
|
|
185
|
+
- `safe` → 자동 허용(감사 소스 `classifier`), 패널 없음.
|
|
186
|
+
- `risky` + **하드 위험 카테고리**(`deletion`, `credential`, `remote`, `system`, `bulk`) → **자동 거부**, 패널 없음. 하드 위험은 자동 허용도 학습도 되지 않습니다.
|
|
187
|
+
- `risky:neutral` → `riskLearning` 활성 시(설정 카드, 기본 꺼짐), 사람이 승인하고 실제 실행된 neutral 위험은 `tool|카테고리` 키로 카운트되며, `riskThreshold`(기본 3) 도달 및 새 호출의 작업 지문(명령어 단어 + 대상 기본 이름)이 확인된 샘플과 일치하면 **동일 작업만** 자동 허용됩니다. 학습 침전(`riskSediment`, 기본 켜짐)을 사용하면 임계값에 도달한 키의 확인 샘플이 **결정론적 허용 규칙**이 됩니다: 지문이 정확히 일치하면 LLM 호출 없이 바로 허용——llmAssist가 꺼져도 유지되며, 침전 규칙은 설정 카드에서 확인·관리(종료 / 샘플 삭제)할 수 있습니다.
|
|
170
188
|
- 시간 초과(`riskTimeoutMs`, 기본 20초, 1회 재시도), 전송 실패, 프로토콜 외 출력은 원래 `ask`를 유지합니다.
|
|
171
189
|
|
|
172
|
-
학습 상태는 플러그인 소유 JSON(`$DSH_HOME/perm-gate/learning.json` 또는 `learningFile`)에 영속화되며 YAML 규칙 파일에는 기록되지 않습니다. 모든 결정은 `$DSH_HOME/perm-gate/events.jsonl`(또는 `eventsFile`)에 추가되고 `GET /api/dsh-perm-gate/events?sessionId=&since=`로 제공됩니다. 브라우저 절반이 이를 폴링하여 입력창 위에 최신 결정을 알림 바로
|
|
190
|
+
학습 상태는 플러그인 소유 JSON(`$DSH_HOME/perm-gate/learning.json` 또는 `learningFile`)에 영속화되며 YAML 규칙 파일에는 기록되지 않습니다. 모든 결정은 `$DSH_HOME/perm-gate/events.jsonl`(또는 `eventsFile`)에 추가되고 `GET /api/dsh-perm-gate/events?sessionId=&since=`로 제공됩니다. 브라우저 절반이 이를 폴링하여 입력창 위에 최신 결정을 알림 바로 표시하고, 대화 보기의「승인 기록」탭에 세션의 모든 판정을 최신 순으로 나열합니다.
|
|
191
|
+
|
|
192
|
+
각 결정이 건드린 파일은 변경 전에(이벤트당 ≤5개 파일, 각 ≤256KB) `$DSH_HOME/perm-gate/snapshots/`에 스냅샷됩니다. 「승인 기록」탭에서는 각 파일 칩을 눌러 줄 단위 diff(`GET /api/dsh-perm-gate/diff`)를 열고, **되돌리기**(`POST /api/dsh-perm-gate/revert`)로 대화에 복원 지시를 보낼 수 있습니다. 스냅샷 관리 바는 세션 단위 또는 전체로 삭제할 수 있습니다(`GET /api/dsh-perm-gate/snapshots-stats` / `POST /api/dsh-perm-gate/snapshots-clear`).
|
|
193
|
+
|
|
194
|
+
사람에게 넘긴 `ask`는 응답이 올 때까지 추적됩니다. 수동적 `approval/request` 옵서버가 닫힌 결과를 기록하며(`allowed-once` → **승인**, `rejected` → **거부**, `cancelled` → **취소**, `unavailable` → 거부=승인 채널 없음), 옵서버가 연관시키지 못하는 경우(`callId` 누락, approval 서비스 없음, 상류 리스너의 단락) `tools/result`가 같은 ask를 폴백으로 정산합니다. 승인 시 승인 후 학습 진행률(`n`/임계값)을 표시하고, 알림 바도 세 가지 종결 상태에 라벨을 붙입니다.
|
|
195
|
+
|
|
196
|
+
또한 프리셋 **거부 키워드 블랙리스트**(dsh-approval-gate의 `DEFAULT_DENY_KEYWORDS` 계승: `rm -rf`,
|
|
197
|
+
`push --force`, `drop table`, `mkfs`, `git reset --hard`, `docker system prune` 등)를 갖추어, 텍스트가
|
|
198
|
+
키워드를 포함하는 호출(대소문자 구분 없는 부분 일치)은 허용 목록/권한/LLM보다 먼저 거부됩니다. 설정
|
|
199
|
+
카드에서 목록으로 편집할 수 있고(프리셋 항목에는 태그 표시, 원클릭 복원 지원) 미설정·빈 목록 시 프리셋이
|
|
200
|
+
적용됩니다——블랙리스트가 조용히 꺼지지 않습니다.
|
|
201
|
+
Permissive 티어는 권한 선택기에서 방패 아이콘을 유지합니다(메뉴 항목과 축소 트리거 모두).
|
|
173
202
|
|
|
174
203
|
|
|
175
204
|
## CLI(독립 실행 dry-run)
|
package/README.md
CHANGED
|
@@ -12,13 +12,29 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
> **Compatibility note:** v0.1.
|
|
15
|
+
> **Compatibility note:** v0.2.1-beta.3 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
|
|
19
19
|
> rebuild.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
> **▼ DSH version compatibility**
|
|
22
|
+
>
|
|
23
|
+
> | DSH version | Load | Settings registration | Host gate | Client half |
|
|
24
|
+
> | --- | --- | --- | --- | --- |
|
|
25
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ✅ | `ctx.settings.register(ns, schema, { base })` | ✅ `tools/pre-execute` identical | ✅ type-only imports |
|
|
26
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ✅ | `register` still present (`installSection` added) | ✅ `tools/pre-execute` identical | ✅ type-only imports |
|
|
27
|
+
>
|
|
28
|
+
> One artifact covers both. Two version-sensitive seams are handled by capability
|
|
29
|
+
> probes rather than version checks: (1) settings registration uses `register`,
|
|
30
|
+
> which exists in every supported version (`installSection` is a 0.1.2 addition,
|
|
31
|
+
> not a replacement); (2) `effectivePolicy` is a **private** method of the
|
|
32
|
+
> user-approval service in **both** versions, so it is read behind a `typeof`
|
|
33
|
+
> probe and degrades to “policy unknown” when absent or throwing. The client
|
|
34
|
+
> bundle value-imports nothing from `@deepseek-ai/*`, so the 0.1.2
|
|
35
|
+
> `dsh-client-runtime` → `dsh-client-store` rename cannot break it.
|
|
36
|
+
|
|
37
|
+
Version **0.2.1-beta.3** — see the [Changelog](./CHANGELOG.md).
|
|
22
38
|
|
|
23
39
|
A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.
|
|
24
40
|
|
|
@@ -63,7 +79,15 @@ cross-plugin version coupling.
|
|
|
63
79
|
- **Verdict learning** — neutral-risk asks that the human approves and that actually execute
|
|
64
80
|
count up; after the threshold, the *exact same operation* (fingerprint-matched) auto-allows.
|
|
65
81
|
- **Decision event feed** — every decision is appended to a JSONL feed and surfaced by the
|
|
66
|
-
browser half as a notice strip above the conversation input
|
|
82
|
+
browser half as a notice strip above the conversation input plus an approval-records tab
|
|
83
|
+
(newest first) in the conversation view.
|
|
84
|
+
|
|
85
|
+
A preset **deny-keyword blacklist** (inherited from dsh-approval-gate's `DEFAULT_DENY_KEYWORDS`:
|
|
86
|
+
`rm -rf`, `push --force`, `drop table`, `mkfs`, `git reset --hard`, `docker system prune`, …) vetoes
|
|
87
|
+
any call whose text contains a keyword — case-insensitive substring, applied before whitelist,
|
|
88
|
+
grants and LLM. It is editable as a list in the settings card (preset entries are tagged, and a
|
|
89
|
+
one-click restore brings the preset back); unset or empty applies the preset — the blacklist
|
|
90
|
+
never silently turns off.
|
|
67
91
|
|
|
68
92
|
## Install
|
|
69
93
|
|
|
@@ -85,9 +109,10 @@ Add the plugin to `cordis.yml`:
|
|
|
85
109
|
- id: dsh-perm-gate
|
|
86
110
|
name: dsh-perm-gate
|
|
87
111
|
config:
|
|
88
|
-
rulesFile: ./permissions.yaml # optional;
|
|
112
|
+
rulesFile: ./permissions.yaml # optional; defaults to $DSH_HOME/perm-gate/rules.yml
|
|
89
113
|
dshHome: $DSH_HOME # root pinned for protected-target checks
|
|
90
114
|
defaultAction: ask # allow | ask | deny
|
|
115
|
+
gatePresets: [permissive] # tiers where the gate is active at all (default)
|
|
91
116
|
```
|
|
92
117
|
|
|
93
118
|
### Rules file
|
|
@@ -149,18 +174,28 @@ exactly as before.
|
|
|
149
174
|
|
|
150
175
|
### Risk-graded llmAssist, verdict learning, and the event feed
|
|
151
176
|
|
|
152
|
-
When `llmAssist` is on, the configured LLM
|
|
153
|
-
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- `
|
|
158
|
-
|
|
177
|
+
When `llmAssist` is on, the configured LLM grades one `ask` at a time with a structured
|
|
178
|
+
protocol. Grading happens **inside the gate's `tools/pre-execute` waterfall, before the decision
|
|
179
|
+
is returned to the host**: a `safe` verdict delegates the call straight through, so the approval
|
|
180
|
+
panel never appears; only a genuinely uncertain verdict reaches you. Two receiver sources are
|
|
181
|
+
selectable in the settings card: a **custom API** (any
|
|
182
|
+
OpenAI-compatible endpoint — `classifierEndpoint` / `classifierModel` / `classifierApiKey`,
|
|
183
|
+
with presets including Xiaomi MiMo `https://api.xiaomimimo.com/v1`), or the **DSH host model
|
|
184
|
+
group** (the session's configured `llm` service, via `agentDefaultModel.currentSelection`,
|
|
185
|
+
optionally overridden with `classifierProvider` / `classifierModel`). A **health test** button
|
|
186
|
+
(`POST /api/dsh-perm-gate/health`) runs one minimal completion and reports latency.
|
|
187
|
+
|
|
188
|
+
- `safe` → the call is auto-allowed (audited as the `classifier` source); no panel is shown.
|
|
189
|
+
- `risky` + a **hard category** (`deletion`, `credential`, `remote`, `system`, `bulk`) → the call
|
|
190
|
+
is **auto-denied** without a panel; hard risks are never auto-allowed and never learned.
|
|
159
191
|
- `risky:neutral` → with `riskLearning` enabled (Settings card, off by default), each human
|
|
160
192
|
approval that actually executes (settled via the host's `tools/result` event) counts toward
|
|
161
193
|
a `tool|category` key; once the count reaches `riskThreshold` (default 3) **and** the new
|
|
162
194
|
call's operation fingerprint (command word + target basename) matches a confirmed sample,
|
|
163
|
-
the exact same operation auto-allows. A different target never reuses that authority.
|
|
195
|
+
the exact same operation auto-allows. A different target never reuses that authority. With sedimentation (`riskSediment`, on by default) a
|
|
196
|
+
threshold-reached key's confirmed samples become **deterministic allow rules**: an exact hit allows
|
|
197
|
+
outright without another LLM call — even with `llmAssist` off — and the sedimented rules are visible
|
|
198
|
+
and manageable (terminate / remove) in the settings card.
|
|
164
199
|
- Timeouts (`riskTimeoutMs`, default 20 s, one retry), transport failures, and off-protocol
|
|
165
200
|
model output leave the ask untouched — the gate never guesses.
|
|
166
201
|
|
|
@@ -169,15 +204,45 @@ Learning state persists to a plugin-owned JSON (`$DSH_HOME/perm-gate/learning.js
|
|
|
169
204
|
`$DSH_HOME/perm-gate/events.jsonl` (or `eventsFile`) and served at
|
|
170
205
|
`GET /api/dsh-perm-gate/events?sessionId=&since=`; the browser half polls it and shows the
|
|
171
206
|
latest decision as a notice strip above the conversation input (asks stay visible until the
|
|
172
|
-
next event)
|
|
207
|
+
next event) and lists the whole session's decisions newest-first in the **Approvals** tab of
|
|
208
|
+
the conversation view.
|
|
209
|
+
|
|
210
|
+
Every decision's affected files are snapshotted before the change lands (≤5 files, ≤256 KB each)
|
|
211
|
+
under `$DSH_HOME/perm-gate/snapshots/`; in the **Approvals** tab each file chip opens a line diff
|
|
212
|
+
(`GET /api/dsh-perm-gate/diff`) with a **revert** action that delivers a restore instruction into
|
|
213
|
+
the conversation (`POST /api/dsh-perm-gate/revert`). A snapshot inventory bar clears them per
|
|
214
|
+
session or entirely (`GET /api/dsh-perm-gate/snapshots-stats` / `POST /api/dsh-perm-gate/snapshots-clear`).
|
|
215
|
+
|
|
216
|
+
An `ask` the gate routes to a human is tracked until the human answers: a passive
|
|
217
|
+
`approval/request` observer records the closed outcome (`allowed-once` → **approved**,
|
|
218
|
+
`rejected` → **rejected**, `cancelled` → **cancelled**, `unavailable` → a denial, since no approval
|
|
219
|
+
channel existed), with `tools/result` settling the same ask as a fallback when the observer cannot
|
|
220
|
+
correlate it. Approvals report the post-approval learning progress (`n`/threshold), and the notice
|
|
221
|
+
strip labels all three terminal states.
|
|
222
|
+
|
|
223
|
+
The Permissive tier keeps its shield icon in the permission picker — on both the
|
|
173
224
|
menu item and the collapsed picker trigger.
|
|
174
225
|
|
|
175
226
|
### A selectable session tier
|
|
176
227
|
|
|
177
|
-
`cordis.patch.yml`
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
228
|
+
`cordis.patch.yml` adds a `permissive` preset (`sandbox: workspace-write`, `approval: ask`, name
|
|
229
|
+
**Permissive**) between Workspace Write and Full access. The DSH bundle patch replaces the whole
|
|
230
|
+
`permission.config.presets` map rather than merging per key, so the file also restates the three
|
|
231
|
+
built-ins (`read-only` / `workspace-write` / `danger-full-access`, from
|
|
232
|
+
`@deepseek-ai/dsh-base/cordis.patch.yml`); `test/patch-presets.spec.ts` pins that key set. So the
|
|
233
|
+
session permission picker offers Permissive as an independent selectable approval tier, not an
|
|
234
|
+
"auto-approval" mode.
|
|
235
|
+
|
|
236
|
+
The gate is active **only in the tiers listed in `gatePresets`** (default `['permissive']`, the
|
|
237
|
+
tier this plugin adds). In every other tier — Read Only, Workspace Write, Full access,
|
|
238
|
+
`custom` — the gate's decision flow does not run at all: no allow, no ask, no deny, no P0
|
|
239
|
+
hard-deny, no deny-keyword veto, and no audit event. The selected tier's own policy governs the
|
|
240
|
+
call, which is the point: `danger-full-access` is defined as "full access without approval
|
|
241
|
+
prompts", so overruling it with an ask (unanswerable there — the approval seam rejects before any
|
|
242
|
+
answerer runs, producing `the user rejected tool "..."` with no panel) or with a hard-deny would
|
|
243
|
+
silently contradict the tier the user chose. `gatePresets: ['*']` makes the gate global again
|
|
244
|
+
(hard-deny included); inside an active tier an `ask` is still degraded to passthrough when the
|
|
245
|
+
session's effective approval policy is `never`.
|
|
181
246
|
|
|
182
247
|
### Configurable in the UI
|
|
183
248
|
|
package/README.zh.md
CHANGED
|
@@ -12,12 +12,26 @@
|
|
|
12
12
|
- [日本語 changelog](./CHANGELOG.ja.md)
|
|
13
13
|
- [한국어 changelog](./CHANGELOG.ko.md)
|
|
14
14
|
|
|
15
|
-
> **兼容性说明:** v0.1.
|
|
15
|
+
> **兼容性说明:** v0.2.1-beta.3 自带 `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 并重新构建。
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
> **▼ DSH 版本适配**
|
|
21
|
+
>
|
|
22
|
+
> | DSH 版本 | 加载 | 设置注册 | 宿主门 | 客户端半 |
|
|
23
|
+
> | --- | --- | --- | --- | --- |
|
|
24
|
+
> | 0.1.0-rc.7 ~ 0.1.1-rc.x | ✅ | `ctx.settings.register(ns, schema, { base })` | ✅ `tools/pre-execute` 一致 | ✅ 仅类型导入 |
|
|
25
|
+
> | 0.1.2-alpha.2+ / 0.1.2-rc.1 | ✅ | `register` 仍保留(另加 `installSection`) | ✅ `tools/pre-execute` 一致 | ✅ 仅类型导入 |
|
|
26
|
+
>
|
|
27
|
+
> 一份产物同时支持两版本。两处版本敏感点都用**能力探测**而非版本号判断:
|
|
28
|
+
> ① 设置注册走 `register`,它在所有目标版本都存在(`installSection` 是 0.1.2 的
|
|
29
|
+
> **新增**而非替代);② `effectivePolicy` 在**两版本**中都是 user-approval 服务的
|
|
30
|
+
> **私有**方法,故只在 `typeof` 探测后调用,缺失或抛错时降级为「策略未知」。
|
|
31
|
+
> 客户端 bundle 对 `@deepseek-ai/*` 无任何值导入,因此 0.1.2 的
|
|
32
|
+
> `dsh-client-runtime` → `dsh-client-store` 改名不会影响它。
|
|
33
|
+
|
|
34
|
+
版本 **0.2.1-beta.3** —— 变更见 [Changelog](./CHANGELOG.md)。
|
|
21
35
|
|
|
22
36
|
一个**单一自足、确定性优先、fail-closed** 的 DeepSeek Harness 权限门插件。
|
|
23
37
|
|
|
@@ -61,9 +75,10 @@ dsh plugin --profile web add dsh-perm-gate
|
|
|
61
75
|
- id: dsh-perm-gate
|
|
62
76
|
name: dsh-perm-gate
|
|
63
77
|
config:
|
|
64
|
-
rulesFile: ./permissions.yaml
|
|
78
|
+
rulesFile: ./permissions.yaml # 可选;默认 $DSH_HOME/perm-gate/rules.yml
|
|
65
79
|
dshHome: $DSH_HOME
|
|
66
80
|
defaultAction: ask
|
|
81
|
+
gatePresets: [permissive] # 门禁生效的档位(默认值)
|
|
67
82
|
```
|
|
68
83
|
|
|
69
84
|
规则示例:见 [examples/permissions.example.yaml](./examples/permissions.example.yaml)。
|
|
@@ -89,7 +104,7 @@ P0 硬拒绝始终单调且不可协商。
|
|
|
89
104
|
llmAssist: false # 先由 LLM 分类裁决;ask/无分类器时回退到人工
|
|
90
105
|
```
|
|
91
106
|
(llmAssist 的真实接收 LLM 在设置页填 `classifierEndpoint` / `classifierModel`,OpenAI 兼容的自定义 API
|
|
92
|
-
|
|
107
|
+
均可。设置页可选择接收来源:**自定义 API**(任何 OpenAI 兼容端点,内置小米 MiMo `https://api.xiaomimimo.com/v1` 等预设)或**宿主模型组**(复用 DSH 会话已配置的 `llm` 服务与当前模型组,可用 `classifierProvider` / `classifierModel` 覆盖);并提供**健康测试**按钮,一键验证接收 LLM 的连通性与延迟。)
|
|
93
108
|
|
|
94
109
|
`trustAutoAllow` 是中间档基线(rule-allow 自动放行)。`alwaysConfirm` 让每次越界都走审批面板,其
|
|
95
110
|
「允许控件」含两个扩展按钮:**本会话重复允许该类**(会话限次 grant,`approveRepeat`)与
|
|
@@ -99,10 +114,20 @@ P0 硬拒绝始终单调且不可协商。
|
|
|
99
114
|
|
|
100
115
|
### 权限下拉里可选档位
|
|
101
116
|
|
|
102
|
-
`cordis.patch.yml`
|
|
117
|
+
`cordis.patch.yml` 在 DSH 的 `permission.config.presets` 里新增了 `permissive` preset
|
|
103
118
|
(`sandbox: workspace-write`、`approval: ask`、名称 **Permissive**),位于 Workspace Write 与
|
|
104
|
-
Full access
|
|
105
|
-
|
|
119
|
+
Full access 之间。DSH 的 bundle patch 对这个 map 是**整表替换**而非逐键合并,所以该文件还必须重述三个内置档
|
|
120
|
+
(`read-only` / `workspace-write` / `danger-full-access`,取自
|
|
121
|
+
`@deepseek-ai/dsh-base/cordis.patch.yml`);`test/patch-presets.spec.ts` 固定了这份键集合。因此会话权限
|
|
122
|
+
下拉里会出现 Permissive 这个**独立可选审批档**,而不是"auto-approval"档。
|
|
123
|
+
|
|
124
|
+
门禁**只在 `gatePresets` 列出的档位里生效**(默认 `['permissive']`,即本插件新增的那一档)。在其余任何档位
|
|
125
|
+
(Read Only、Workspace Write、Full access、`custom`)里,门禁的判定流程**完全不运行**:不放行、不弹审批、
|
|
126
|
+
不拒绝、不执行 P0 硬拒绝、不做黑名单关键词拦截,也不写审计事件——该档位自己的策略说了算。这正是重点所在:
|
|
127
|
+
`danger-full-access` 的定义就是"全权限、不弹审批",用 ask 去覆盖它毫无意义(该档 `approval: never` 会让审批接缝
|
|
128
|
+
**在任何 answerer 运行之前**直接返回 `rejected`,被转发的 ask 只能得到 `the user rejected tool "..."`,面板根本不会
|
|
129
|
+
弹出),用硬拒绝去覆盖它则等于悄悄推翻用户选定的档位。`gatePresets: ['*']` 可让门禁重新全局生效(含硬拒绝层);
|
|
130
|
+
在生效档位内,若会话生效的审批策略为 `never`,ask 仍会降级为放行。
|
|
106
131
|
|
|
107
132
|
### 在 UI 里可配置
|
|
108
133
|
|
|
@@ -113,15 +138,25 @@ Full access 之间——与 Auto 档同一机制。因此会话权限下拉里
|
|
|
113
138
|
|
|
114
139
|
### 风险分级 llmAssist、裁决学习与事件流
|
|
115
140
|
|
|
116
|
-
开启 `llmAssist`
|
|
117
|
-
`classifierModel` / `classifierApiKey` 指向你自己的 API)按结构化协议逐条评估 `ask`:
|
|
141
|
+
开启 `llmAssist` 后,接收 LLM(自定义 OpenAI 兼容端点,或 DSH 宿主模型组——见上文)按结构化协议逐条评估 `ask`。**判定发生在门禁的 `tools/pre-execute` 瀑布内部、决策返回宿主之前**:`safe` 直接放行,审批面板根本不会出现;只有真正无法确定的判定才会弹到你面前。
|
|
118
142
|
|
|
119
|
-
- `safe` → 自动放行(审计来源为 `classifier
|
|
120
|
-
- `risky` + **硬风险类别**(`deletion`、`credential`、`remote`、`system`、`bulk`)→
|
|
121
|
-
- `risky:neutral` → 若开启 `riskLearning`(设置卡片内,默认关闭),人工批准且真实执行的 neutral 风险会按 `tool|类别` 计数;计数达到 `riskThreshold`(默认 3)且新调用的操作指纹(命令词 +
|
|
143
|
+
- `safe` → 自动放行(审计来源为 `classifier`),不弹面板。
|
|
144
|
+
- `risky` + **硬风险类别**(`deletion`、`credential`、`remote`、`system`、`bulk`)→ **自动拒绝**、不弹面板;硬风险永不自动放行、也永不进入学习。
|
|
145
|
+
- `risky:neutral` → 若开启 `riskLearning`(设置卡片内,默认关闭),人工批准且真实执行的 neutral 风险会按 `tool|类别` 计数;计数达到 `riskThreshold`(默认 3)且新调用的操作指纹(命令词 + 目标基名)命中已确认样本时,**同一操作**自动放行。不同目标永不复用该放行。开启学习沉淀(`riskSediment`,默认开)后,满阈值 key 的确认样本会成为**确定性放行规则**:指纹精确命中即直接放行、无需再过 LLM——即使关闭 llmAssist 也继续生效;沉淀规则在设置卡片中可见、可管理(终止学习 / 删除样本)。
|
|
122
146
|
- 超时(`riskTimeoutMs`,默认 20s,重试 1 次)、传输失败与协议外输出均维持原 `ask`——门禁绝不猜测。
|
|
123
147
|
|
|
124
|
-
学习状态持久化在插件自有 JSON(`$DSH_HOME/perm-gate/learning.json` 或 `learningFile`),不写入你的 YAML 规则文件。每次决策都会追加到 `$DSH_HOME/perm-gate/events.jsonl`(或 `eventsFile`),并经 `GET /api/dsh-perm-gate/events?sessionId=&since=` 提供;浏览器端轮询该接口,在输入框上方以提示条展示最新决策(ask
|
|
148
|
+
学习状态持久化在插件自有 JSON(`$DSH_HOME/perm-gate/learning.json` 或 `learningFile`),不写入你的 YAML 规则文件。每次决策都会追加到 `$DSH_HOME/perm-gate/events.jsonl`(或 `eventsFile`),并经 `GET /api/dsh-perm-gate/events?sessionId=&since=` 提供;浏览器端轮询该接口,在输入框上方以提示条展示最新决策(ask 常驻至下一条事件),并在对话视图的「审批记录」页签按时间倒序列出本会话的全部判定。
|
|
149
|
+
|
|
150
|
+
每次决策涉及的文件都会在改动落地前快照(每事件 ≤5 个文件、单文件 ≤256 KB)到 `$DSH_HOME/perm-gate/snapshots/`;「审批记录」页签中每个文件 chip 可点开行级改动对比(`GET /api/dsh-perm-gate/diff`),并可**撤销**该改动——向会话投递恢复指令(`POST /api/dsh-perm-gate/revert`)。快照管理条支持按会话或全量清理(`GET /api/dsh-perm-gate/snapshots-stats` / `POST /api/dsh-perm-gate/snapshots-clear`)。
|
|
151
|
+
|
|
152
|
+
转人工的 `ask` 会被跟踪到人工给出答复为止:一个**被动** `approval/request` 观察者记录封闭结果(`allowed-once` → **人工通过**、`rejected` → **人工拒绝**、`cancelled` → **人工取消**、`unavailable` → 拒绝,因为不存在审批通道);当观察者无法关联该 ask 时(缺 `callId`、无 approval 服务、上游监听者短路),由 `tools/result` 兜底结算同一个 ask。人工通过会显示通过后的学习进度(`n`/阈值),通知条也会为三种终态分别打标。
|
|
153
|
+
|
|
154
|
+
插件还内置一份**预置黑名单关键词**(继承自 dsh-approval-gate 的 `DEFAULT_DENY_KEYWORDS`:
|
|
155
|
+
`rm -rf`、`push --force`、`drop table`、`mkfs`、`git reset --hard`、`docker system prune` 等),
|
|
156
|
+
调用文本命中任一关键词(大小写不敏感子串)即直接拒绝,且先于白名单 / 授权 / LLM。黑名单在设置
|
|
157
|
+
卡片中按列表查看与增删(预置条目带标签,可一键恢复预置);未设置或为空时应用预置列表——黑名单
|
|
158
|
+
不会静默关闭。
|
|
159
|
+
Permissive 档在权限选择器中保留盾形图标——菜单项与折叠触发按钮均有图标。
|
|
125
160
|
|
|
126
161
|
|
|
127
162
|
## CLI(独立 dry-run)
|
package/cordis.patch.yml
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# dsh-perm-gate profile patch.
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# Omitting any of them silently disables that tier on the next profile load.
|
|
7
|
-
# 1) Add an independent "Permissive" session approval tier to the DSH permission
|
|
8
|
-
# picker (same mechanism Auto uses).
|
|
3
|
+
# Two jobs:
|
|
4
|
+
# 1) Add the plugin's own "Permissive" session approval tier to the DSH
|
|
5
|
+
# permission picker, and restate the built-in tiers it must not drop.
|
|
9
6
|
# 2) Insert the plugin itself (host half) into the profile.
|
|
7
|
+
#
|
|
8
|
+
# The DSH bundle patch REPLACES the whole `permission.config.presets` map (it
|
|
9
|
+
# does not merge per key), so this file MUST restate every tier that should
|
|
10
|
+
# survive. The authoritative built-in set is
|
|
11
|
+
# `@deepseek-ai/dsh-base/cordis.patch.yml` (`id: permission`):
|
|
12
|
+
# read-only / workspace-write / danger-full-access
|
|
13
|
+
# Omitting any of them silently drops that tier on the next profile load.
|
|
14
|
+
# `test/patch-presets.spec.ts` pins the exact key set so the two cannot drift.
|
|
15
|
+
#
|
|
16
|
+
# The `auto` tier is deliberately NOT restated. It was contributed by
|
|
17
|
+
# dsh-auto-mode, which is no longer installed: its knobs were identical to
|
|
18
|
+
# `workspace-write`, its "automatic review / one-shot approval" description
|
|
19
|
+
# lost its implementation with that plugin, and this gate is inactive in it
|
|
20
|
+
# (`gatePresets` defaults to `['permissive']`) — so it only offered a duplicate
|
|
21
|
+
# tier that looked protected while nothing was.
|
|
10
22
|
- id: permission
|
|
11
23
|
config:
|
|
12
24
|
presets:
|
|
@@ -21,15 +33,10 @@
|
|
|
21
33
|
approval: ask
|
|
22
34
|
name: Permissive
|
|
23
35
|
description: Independent approval tier — safe in-scope ops auto-allow, dangerous crossings ask (approval strengths tunable under Settings → Plugins → Permissive approval tier).
|
|
24
|
-
auto:
|
|
25
|
-
sandbox: workspace-write
|
|
26
|
-
approval: ask
|
|
27
|
-
name: Auto
|
|
28
|
-
description: Workspace-sandboxed execution with automatic review and one-shot approval for wider access.
|
|
29
36
|
danger-full-access:
|
|
30
37
|
sandbox: danger-full-access
|
|
31
38
|
approval: never
|
|
32
39
|
|
|
33
40
|
- insert:
|
|
34
41
|
- id: dsh-perm-gate
|
|
35
|
-
name: dsh-perm-gate
|
|
42
|
+
name: dsh-perm-gate
|
package/lib/audit.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* in-memory mirror instead of failing.
|
|
10
10
|
*/
|
|
11
11
|
export type AuditOutcome = 'allow' | 'deny' | 'ask' | 'delegated';
|
|
12
|
-
export type DecisionSource = 'hard-deny' | 'grant' | 'rule' | 'default' | 'ask' | 'classifier' | 'permissive';
|
|
12
|
+
export type DecisionSource = 'hard-deny' | 'deny-keyword' | 'grant' | 'rule' | 'default' | 'ask' | 'classifier' | 'permissive';
|
|
13
13
|
export interface AuditEntry {
|
|
14
14
|
readonly kind: 'permissionGate/decision';
|
|
15
15
|
readonly marker: 'ignorable';
|
package/lib/classifier.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ export declare function chatCompletion(cfg: ClassifierConfig, system: string, us
|
|
|
55
55
|
content: string;
|
|
56
56
|
} | {
|
|
57
57
|
ok: false;
|
|
58
|
+
status?: number;
|
|
59
|
+
error?: string;
|
|
58
60
|
}>;
|
|
59
61
|
/** A bounded single-shot task result; `undefined` means "try again". */
|
|
60
62
|
type RetryTask<T> = () => Promise<T | undefined>;
|
package/lib/classifier.js
CHANGED
|
@@ -19,7 +19,7 @@ async function postChat(url, headers, body, signal, nowFetch) {
|
|
|
19
19
|
return { ok: false, status: res.status };
|
|
20
20
|
const text = await res.text();
|
|
21
21
|
if (text.trim() === '')
|
|
22
|
-
return { ok: false };
|
|
22
|
+
return { ok: false, status: res.status };
|
|
23
23
|
// Custom models may answer with plain text instead of the JSON envelope;
|
|
24
24
|
// pass the raw text through — the verdict parser's keyword fallback judges it.
|
|
25
25
|
let content = text;
|
|
@@ -71,10 +71,13 @@ export async function chatCompletion(cfg, system, user, nowFetch = fetch) {
|
|
|
71
71
|
}, controller.signal, nowFetch);
|
|
72
72
|
if (first.ok)
|
|
73
73
|
return first;
|
|
74
|
-
if (first.status === undefined || !SHAPE_REJECT_STATUS.has(first.status))
|
|
75
|
-
return { ok: false };
|
|
74
|
+
if (first.status === undefined || !SHAPE_REJECT_STATUS.has(first.status)) {
|
|
75
|
+
return controller.signal.aborted ? { ok: false, error: 'timeout' } : first;
|
|
76
|
+
}
|
|
76
77
|
const second = await postChat(url, headers, { model, temperature: 0, messages }, controller.signal, nowFetch);
|
|
77
|
-
|
|
78
|
+
if (second.ok)
|
|
79
|
+
return second;
|
|
80
|
+
return controller.signal.aborted ? { ok: false, error: 'timeout' } : second;
|
|
78
81
|
}
|
|
79
82
|
finally {
|
|
80
83
|
clearTimeout(timer);
|