dsh-perm-gate 2.1.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +82 -0
- package/CHANGELOG.ko.md +80 -0
- package/CHANGELOG.md +225 -0
- package/INSTALL.ja.md +15 -9
- package/INSTALL.ko.md +16 -10
- package/INSTALL.md +17 -9
- package/INSTALL.zh.md +13 -8
- package/README.ja.md +17 -5
- package/README.ko.md +18 -6
- package/README.md +33 -3
- package/README.zh.md +11 -3
- package/docs/baseline-guard.md +56 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +2 -2
- package/examples/permissions.example.yaml +16 -1
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +646 -95
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +11 -0
- package/lib/compiler.js +16 -0
- package/lib/config.d.ts +73 -12
- package/lib/config.js +56 -14
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +52 -2
- package/lib/evaluate.js +76 -2
- package/lib/events.d.ts +91 -2
- package/lib/events.js +220 -0
- package/lib/index.js +111 -26
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +17 -3
- package/lib/rule-chain.js +13 -22
- package/lib/rule-dims.d.ts +35 -0
- package/lib/rule-dims.js +36 -0
- package/lib/rule.d.ts +36 -1
- package/lib/rule.js +56 -8
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +121 -7
- package/lib/runtime.js +204 -25
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/package.json +13 -5
package/README.ja.md
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
> メソッドであるため、`typeof` プロブで読み取り、欠落時やエラー時には「ポリシー
|
|
46
46
|
> 不明」にフォールバックします。
|
|
47
47
|
|
|
48
|
-
バージョン **2.1
|
|
48
|
+
バージョン **2.4.1** — 変更履歴は [日本語 changelog](./CHANGELOG.ja.md) を参照。
|
|
49
49
|
|
|
50
50
|
DeepSeek Harness 向けの、単一・自己完結・決定論優先・fail-closed な権限ゲートです。
|
|
51
51
|
|
|
@@ -202,7 +202,16 @@ DSH **自身**の通信 —— 組み込みネットワークツールと LLM
|
|
|
202
202
|
自动审查は権限ピッカーの中で 読み取り専用 / ワークスペース内変更 / 完全権限 /
|
|
203
203
|
許可リストと並ぶ**独立した承認ティア**です。汎用の「自動承認」ではなく、包括的な権限を
|
|
204
204
|
発行することもありません。人間/LLM のシーム**の前**で判定を狭めたり広めたりする
|
|
205
|
-
だけで、P0
|
|
205
|
+
だけで、P0 ハード拒否は**ゲート自身のスコープ内で**単調かつ交渉不能のままです。
|
|
206
|
+
|
|
207
|
+
> **P0 はスコープ付きであり、グローバルではありません。** このゲートはセッションの権限プリセットが
|
|
208
|
+
> `gatePresets`(既定は `permissive` / `permissive-full`)のいずれかである間だけ動作します。それ以外の
|
|
209
|
+
> プリセット —— 読み取り専用 / ワークスペース内変更 / 完全権限 —— では **P0 ハード拒否を含むゲート
|
|
210
|
+
> 全体が停止**し、選択されたティア自身のポリシーがそのセッションを統治します。これは意図的な設計です
|
|
211
|
+
> (設定表の `gatePresets` を参照)が、「P0 は交渉不能」は**ゲートのティア内**で成立するものであり、
|
|
212
|
+
> すべてのティアに跨るものではありません。停止は**沈黙しません**: セッション/プリセットの遷移ごとに
|
|
213
|
+
> `stand-down` イベントを 1 件記録し、ブラウザは入力欄の上に **GATE OFF** の常駐ストリップを表示します。
|
|
214
|
+
> `gatePresets: ['*']` に設定すると P0 は再びグローバルになります。
|
|
206
215
|
|
|
207
216
|
**2 つのバリアントを同梱します。** プリセットの `sandbox` と `approval` は**独立した**ノブであり、
|
|
208
217
|
両者を結び付けると悪いトレードオフを強いられるためです。
|
|
@@ -296,8 +305,10 @@ never` パススルー — は人手に変更なく委譲されるため、将
|
|
|
296
305
|
ゲートが動作するのは **`gatePresets` に列挙したティアの中だけ**です(既定
|
|
297
306
|
`['permissive', 'permissive-full']`、このプラグインが追加する 2 つのティア)。それ以外のティア(Read Only / Workspace Write /
|
|
298
307
|
Full access / `custom`)では、ゲートの判定フローは**一切実行されません**——許可も、ask も、
|
|
299
|
-
拒否も、P0
|
|
300
|
-
|
|
308
|
+
拒否も、P0 ハード拒否も、拒否キーワードの遮断も行いません。選択された
|
|
309
|
+
ティア自身の方針が呼び出しを決めます。ただし**沈黙はしません**: セッション/プリセットの遷移ごとに
|
|
310
|
+
`stand-down` イベントを 1 件だけ記録し(判定ではなく通知です)、ブラウザは入力欄の上に
|
|
311
|
+
**GATE OFF** の常駐ストリップを表示します。`danger-full-access` の定義は「承認プロンプトなしの
|
|
301
312
|
フルアクセス」であり、そこを ask で上書きしても意味がありません(その方針では DSH の承認シームが
|
|
302
313
|
**どの answerer よりも先に** `rejected` を返し、転送された ask はパネルを一度も表示せず
|
|
303
314
|
`the user rejected tool "..."` にしかなりません)。ハード拒否で上書きするのも、ユーザーが選んだ
|
|
@@ -319,7 +330,8 @@ Full access / `custom`)では、ゲートの判定フローは**一切実行
|
|
|
319
330
|
`llmAssist` 有効時、設定された LLM(OpenAI 互換エンドポイントなら任意——`classifierEndpoint` / `classifierModel` / `classifierApiKey` を自分の API に向けてください)が構造化プロトコルで `ask` を 1 件ずつ判定します。**判定はゲートの `tools/pre-execute` ウォーターフォール内、決定がホストへ返る前に行われます**:`safe` はそのまま委譲されるため承認パネルは一切表示されず、本当に判断できない判定だけが人手に回ります。
|
|
320
331
|
|
|
321
332
|
- `safe` → 自動許可(監査ソースは `classifier`)。パネルは表示されません。
|
|
322
|
-
- `risky` + **ハードリスクカテゴリ**(`deletion`、`credential`、`remote`、`system`、`bulk`)→
|
|
333
|
+
- `risky` + **ハードリスクカテゴリ**(`deletion`、`credential`、`remote`、`system`、`bulk`)→ **人間への確認(ask)を維持**します。分類器は**決して拒否しません**。拒否は決定論的レイヤー(P0 ハード拒否、拒否キーワードのブラックリスト、明示的な `deny:` ルール)だけに属するため、誤判定されたカテゴリは常に交渉可能で、異議を唱えられないブロックにはなりません。ハードカテゴリが `neutral` と異なる重要な点は一つだけです: **決して学習されない**ため、承認を繰り返しても自動許可に沈殿しません。
|
|
334
|
+
(モデルの判断を拒否の根拠にした結果は実測済みです: 無害な `git commit -F …` が `remote` と判定され、パネルも再試行用の grant もない自動拒否が発生しました。)
|
|
323
335
|
- `risky:neutral` → `riskLearning` 有効時(設定カード内、既定オフ)、人手で承認され実際に実行された neutral リスクは `tool|カテゴリ` ごとにカウントされ、`riskThreshold`(既定 3)到達かつ新呼び出しの操作指紋(コマンド語+対象の基底名)が確認済みサンプルに一致した場合、**同一操作のみ**自動許可されます。学習沈殿(`riskSediment`、既定オン)を有効にすると、しきい値に達したキーの確認サンプルは**決定論的な許可ルール**になります:指紋の正確一致は LLM を経由せず自動許可——llmAssist オフでも継続し、沈殿ルールは設定カードで確認・管理(終止 / サンプル削除)できます。
|
|
324
336
|
- タイムアウト(`riskTimeoutMs`、既定 20 秒、1 回リトライ)、通信失敗、プロトコル外出力は元の `ask` を維持します。
|
|
325
337
|
|
package/README.ko.md
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
> 모두에서 user-approval 서비스의 **개인** 메소드이므로 `typeof` 프로브를 통해
|
|
44
44
|
> 읽으며, 누락되거나 예외 발생 시 "정책_unknown"으로 후퇴합니다.
|
|
45
45
|
|
|
46
|
-
버전 **2.1
|
|
46
|
+
버전 **2.4.1** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
|
|
47
47
|
|
|
48
48
|
DeepSeek Harness용 단일·자족적·결정론 우선·fail-closed 권한 게이트입니다.
|
|
49
49
|
|
|
@@ -196,8 +196,17 @@ DSH **자신**의 트래픽 —— 내장 네트워크 도구와 LLM 전송 —
|
|
|
196
196
|
|
|
197
197
|
自动审查는 권한 선택기에서 읽기 전용 / 워크스페이스 내 수정 / 완전 권한 / 허용 목록과
|
|
198
198
|
나란한 **독립 승인 티어**입니다. 범용 "자동 승인"이 아니며 포괄적 권한을 발행하지도 않습니다.
|
|
199
|
-
사람/LLM 심 **이전에** 판정을 좁히거나 넓힐 뿐이고, P0 하드 거부는
|
|
200
|
-
불가능하게 유지됩니다.
|
|
199
|
+
사람/LLM 심 **이전에** 판정을 좁히거나 넓힐 뿐이고, P0 하드 거부는 **게이트 자체 범위 안에서**
|
|
200
|
+
단조롭고 협상 불가능하게 유지됩니다.
|
|
201
|
+
|
|
202
|
+
> **P0는 범위가 있으며 전역이 아닙니다.** 이 게이트는 세션 권한 프리셋이 `gatePresets`(기본값
|
|
203
|
+
> `permissive` / `permissive-full`) 중 하나일 때만 동작합니다. 그 밖의 프리셋 —— 읽기 전용 /
|
|
204
|
+
> 워크스페이스 내 수정 / 완전 권한 —— 에서는 **P0 하드 거부를 포함한 게이트 전체가 정지**하고,
|
|
205
|
+
> 선택된 티어 자체의 정책이 그 세션을 통치합니다. 의도된 설계이지만(설정 표의 `gatePresets` 참조),
|
|
206
|
+
> "P0는 협상 불가"는 **게이트의 티어 안에서** 성립하는 것이지 모든 티어에 걸친 것이 아닙니다.
|
|
207
|
+
> 정지는 **침묵하지 않습니다**: 세션/프리셋 전환마다 `stand-down` 이벤트 1건을 기록하고 브라우저는
|
|
208
|
+
> 입력창 위에 **GATE OFF** 상시 표시줄을 띄웁니다. `gatePresets: ['*']`로 설정하면 P0는 다시
|
|
209
|
+
> 전역이 됩니다.
|
|
201
210
|
|
|
202
211
|
**두 가지 변형을 함께 제공합니다.** 프리셋의 `sandbox`와 `approval`은 **독립된** 노브이며, 둘을
|
|
203
212
|
묶으면 나쁜 트레이드오프를 강요받기 때문입니다.
|
|
@@ -294,8 +303,10 @@ never` 패스스루 — 는 사람에 그대로 위임되므로 향후 DSH 변
|
|
|
294
303
|
게이트가 동작하는 범위는 **`gatePresets`에 나열한 티어 안뿐**입니다(기본
|
|
295
304
|
`['permissive', 'permissive-full']`, 이 플러그인이 추가하는 두 티어). 그 밖의 티어(Read Only / Workspace Write / Full access /
|
|
296
305
|
`custom`)에서는 게이트의 판정 흐름이 **전혀 실행되지 않습니다** — 허용도, ask도, 거부도,
|
|
297
|
-
P0 하드 거부도, 거부 키워드
|
|
298
|
-
호출을 결정합니다.
|
|
306
|
+
P0 하드 거부도, 거부 키워드 차단도 하지 않습니다. 선택한 티어의 자체 정책이
|
|
307
|
+
호출을 결정합니다. 다만 **침묵하지는 않습니다**: 세션/프리셋 전환마다 `stand-down` 이벤트를 1건만
|
|
308
|
+
기록하고(판정이 아니라 알림입니다), 브라우저는 입력창 위에 **GATE OFF** 상시 표시줄을 띄웁니다.
|
|
309
|
+
`danger-full-access`의 정의는 "승인 프롬프트 없는 전체 접근"이며, 이를 ask로
|
|
299
310
|
덮어써도 의미가 없습니다(그 정책에서는 DSH 승인 시임이 **어떤 answerer보다도 먼저** `rejected`를
|
|
300
311
|
반환해 전달된 ask는 패널을 한 번도 띄우지 못하고 `the user rejected tool "..."`만 남습니다).
|
|
301
312
|
하드 거부로 덮어쓰는 것 역시 사용자가 고른 티어를 조용히 뒤집는 일입니다.
|
|
@@ -315,7 +326,8 @@ host는 네임스페이스를 live로 읽으므로 변경은 재시작 없이
|
|
|
315
326
|
`llmAssist`가 켜져 있으면 설정된 LLM(OpenAI 호환 엔드포인트라면 무엇이든 — `classifierEndpoint` / `classifierModel` / `classifierApiKey`을 자신의 API로 지정)이 구조화된 프로토콜로 `ask`를 하나씩 판정합니다. **판정은 게이트의 `tools/pre-execute` 워터폴 안에서, 결정이 호스트로 반환되기 전에 이루어집니다**: `safe`는 곧바로 위임되므로 승인 패널이 아예 표시되지 않고, 정말로 판단할 수 없는 판정만 사람에게 전달됩니다.
|
|
316
327
|
|
|
317
328
|
- `safe` → 자동 허용(감사 소스 `classifier`), 패널 없음.
|
|
318
|
-
- `risky` + **하드 위험 카테고리**(`deletion`, `credential`, `remote`, `system`, `bulk`) →
|
|
329
|
+
- `risky` + **하드 위험 카테고리**(`deletion`, `credential`, `remote`, `system`, `bulk`) → **사람 확인(ask)을 유지**합니다. 분류기는 **결코 거부하지 않습니다**. 거부는 결정론적 계층(P0 하드 거부, 거부 키워드 블랙리스트, 명시적 `deny:` 규칙)에만 속하므로, 잘못 판정된 카테고리는 언제나 협상 가능하며 이의를 제기할 수 없는 차단이 되지 않습니다. 하드 카테고리가 `neutral`과 다른 중요한 점은 하나뿐입니다: **결코 학습되지 않으므로** 승인을 반복해도 자동 허용으로 침전되지 않습니다.
|
|
330
|
+
(모델의 판단을 거부 근거로 삼은 결과는 실측되었습니다: 무해한 `git commit -F …`가 `remote`로 판정되어 패널도, 재시도용 grant도 없는 자동 거부가 발생했습니다.)
|
|
319
331
|
- `risky:neutral` → `riskLearning` 활성 시(설정 카드, 기본 꺼짐), 사람이 승인하고 실제 실행된 neutral 위험은 `tool|카테고리` 키로 카운트되며, `riskThreshold`(기본 3) 도달 및 새 호출의 작업 지문(명령어 단어 + 대상 기본 이름)이 확인된 샘플과 일치하면 **동일 작업만** 자동 허용됩니다. 학습 침전(`riskSediment`, 기본 켜짐)을 사용하면 임계값에 도달한 키의 확인 샘플이 **결정론적 허용 규칙**이 됩니다: 지문이 정확히 일치하면 LLM 호출 없이 바로 허용——llmAssist가 꺼져도 유지되며, 침전 규칙은 설정 카드에서 확인·관리(종료 / 샘플 삭제)할 수 있습니다.
|
|
320
332
|
- 시간 초과(`riskTimeoutMs`, 기본 20초, 1회 재시도), 전송 실패, 프로토콜 외 출력은 원래 `ask`를 유지합니다.
|
|
321
333
|
|
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 **
|
|
45
|
+
Version **3.0.0** — see the [Changelog](./CHANGELOG.md).
|
|
46
46
|
|
|
47
47
|
A single, self-sufficient, deterministic-first, fail-closed permission gate for DeepSeek Harness.
|
|
48
48
|
|
|
@@ -229,7 +229,16 @@ proxy liveness, env-injection state, block counters, recent blocks).
|
|
|
229
229
|
自动审查 is an **independent approval tier** in the DSH permission picker, parallel to
|
|
230
230
|
Read Only / Workspace Write / Full access / Whitelist. It is **not** generic "auto-approval" and
|
|
231
231
|
never mints blanket authority: it only narrows or widens the seam *before* the human/LLM step
|
|
232
|
-
while P0 hard-deny stays monotonic and non-negotiable
|
|
232
|
+
while P0 hard-deny stays monotonic and non-negotiable **within the gate's own scope**.
|
|
233
|
+
|
|
234
|
+
> **P0 is scoped, not global.** The gate acts only while the session's permission preset is one
|
|
235
|
+
> of `gatePresets` (default `permissive` / `permissive-full`). Under any other preset — Read Only,
|
|
236
|
+
> Workspace Write, or Full access — the **entire** gate stands down, P0 hard-deny included,
|
|
237
|
+
> because the selected tier's own policy governs that session. This is deliberate (see
|
|
238
|
+
> `gatePresets` in the configuration table), but it means "P0 is non-negotiable" holds *inside*
|
|
239
|
+
> the gate's tiers rather than across every tier. A stand-down is not silent: the gate records one
|
|
240
|
+
> `stand-down` event per session/preset transition and the browser shows a sticky **GATE OFF**
|
|
241
|
+
> strip above the input. Set `gatePresets: ['*']` to make P0 global again.
|
|
233
242
|
|
|
234
243
|
**Two variants ship**, because a preset's `sandbox` and `approval` are independent knobs and
|
|
235
244
|
coupling them forced a bad trade:
|
|
@@ -326,7 +335,13 @@ optionally overridden with `classifierProvider` / `classifierModel`). A **health
|
|
|
326
335
|
|
|
327
336
|
- `safe` → the call is auto-allowed (audited as the `classifier` source); no panel is shown.
|
|
328
337
|
- `risky` + a **hard category** (`deletion`, `credential`, `remote`, `system`, `bulk`) → the call
|
|
329
|
-
|
|
338
|
+
**keeps the human ask**. The classifier **never denies**: the deny path belongs to the
|
|
339
|
+
deterministic layers alone (P0 hard-deny, the deny-keyword blacklist, explicit `deny:` rules), so
|
|
340
|
+
a misgraded category is always negotiable instead of an unappealable block. Hard categories stay
|
|
341
|
+
distinct from `neutral` in one way that matters: they are **never learned**, so repeated
|
|
342
|
+
approvals can never sediment them into an auto-allow.
|
|
343
|
+
(Denying on the model's word was measured live: a benign `git commit -F …` graded `remote`
|
|
344
|
+
produced an auto-deny with no panel and no grant to retry with.)
|
|
330
345
|
- `risky:neutral` → with `riskLearning` enabled (Settings card, off by default), each human
|
|
331
346
|
approval that actually executes (settled via the host's `tools/result` event) counts toward
|
|
332
347
|
a `tool|category` key; once the count reaches `riskThreshold` (default 3) **and** the new
|
|
@@ -393,6 +408,21 @@ The tier is also adjustable at runtime from **Settings → Plugins → 自动审
|
|
|
393
408
|
namespace live, so a change applies to the next tool call without a restart. This is an
|
|
394
409
|
independent approval class, NOT a generic "auto-approval" mode.
|
|
395
410
|
|
|
411
|
+
### Rule test (dry-run)
|
|
412
|
+
|
|
413
|
+
The same page carries a **Rule test** panel: type a tool name and a command, press Test, and the
|
|
414
|
+
gate judges that call against the ruleset it currently has loaded — without running anything and
|
|
415
|
+
without writing any rule. You get the verdict, the matched rule (index and action), the dimensions
|
|
416
|
+
that rule constrains, and the reason.
|
|
417
|
+
|
|
418
|
+
It reports the effective verdict (the whole P0 → P1 → P2 → P3 → P4 chain) *and* the rule layer's own
|
|
419
|
+
answer, which are not the same thing: a P0 hard-deny or a preset deny-keyword fires before the rule
|
|
420
|
+
chain and leaves no rule index behind, so the panel says "no rule matched" rather than naming an
|
|
421
|
+
unrelated rule. A `0 rules loaded` note means the `rulesFile` path resolved to nothing.
|
|
422
|
+
|
|
423
|
+
The panel talks to `POST /api/dsh-perm-gate/dry-run`, which is **read-only by construction** — it
|
|
424
|
+
has no write form at all, so testing a rule can never change it.
|
|
425
|
+
|
|
396
426
|
## CLI
|
|
397
427
|
|
|
398
428
|
Dry-run one call against a rules file (no harness needed):
|
package/README.zh.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
> 线上都是 user-approval 服务的**私有**方法,因此通过 `typeof` 探测读取,缺失
|
|
40
40
|
> 或抛错时降级为「策略未知」。
|
|
41
41
|
|
|
42
|
-
版本 **2.1
|
|
42
|
+
版本 **2.4.1** —— 变更见 [Changelog](./CHANGELOG.md)。
|
|
43
43
|
|
|
44
44
|
一个**单一自足、确定性优先、fail-closed** 的 DeepSeek Harness 权限门插件。
|
|
45
45
|
|
|
@@ -151,7 +151,14 @@ DSH **自身**的网络流量 —— 内建网络工具与 LLM 传输 —— 刻
|
|
|
151
151
|
|
|
152
152
|
自动审查是权限下拉框里一个**独立审批档**,与只读 / 工作区内修改 / 完全权限 / 白名单平行。
|
|
153
153
|
它不是泛化的"自动审批"、也不授予泛化权限:只会在人类/LLM 接缝**之前**收窄或放宽决策,
|
|
154
|
-
P0
|
|
154
|
+
P0 硬拒绝在**本门禁自身的档位作用域内**始终单调且不可协商。
|
|
155
|
+
|
|
156
|
+
> **P0 是档位作用域内的,不是全局的。** 门禁只在会话权限档位属于 `gatePresets`
|
|
157
|
+
> (默认 `permissive` / `permissive-full`)时生效。其他档位 —— 只读、工作区内修改、完全权限
|
|
158
|
+
> —— 下**整个门禁停用,包括 P0 硬拒绝**,因为该档位自身的策略接管了这个会话。这是刻意设计
|
|
159
|
+
> (见配置表的 `gatePresets`),但也就意味着「P0 不可协商」成立于**门禁的档位之内**,而非所有档位。
|
|
160
|
+
> 停用**不是静默的**:每次会话档位切换会记录一条 `stand-down` 事件,浏览器在输入框上方常驻一条
|
|
161
|
+
> **GATE OFF** 提示条。把 `gatePresets` 设为 `['*']` 可让 P0 重新变成全局。
|
|
155
162
|
|
|
156
163
|
**提供两个变体** —— 因为预设的 `sandbox` 与 `approval` 是两根**独立**旋钮,把它们绑死会逼出
|
|
157
164
|
一个糟糕的取舍:
|
|
@@ -252,7 +259,8 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
|
|
|
252
259
|
开启 `llmAssist` 后,接收 LLM(自定义 OpenAI 兼容端点,或 DSH 宿主模型组——见上文)按结构化协议逐条评估 `ask`。**判定发生在门禁的 `tools/pre-execute` 瀑布内部、决策返回宿主之前**:`safe` 直接放行,审批面板根本不会出现;只有真正无法确定的判定才会弹到你面前。
|
|
253
260
|
|
|
254
261
|
- `safe` → 自动放行(审计来源为 `classifier`),不弹面板。
|
|
255
|
-
- `risky` + **硬风险类别**(`deletion`、`credential`、`remote`、`system`、`bulk`)→
|
|
262
|
+
- `risky` + **硬风险类别**(`deletion`、`credential`、`remote`、`system`、`bulk`)→ **维持人工确认(ask)**。分类器**永不拒绝**:拒绝只属于确定性层(P0 硬拒绝、黑名单关键词、显式 `deny:` 规则),所以被误判的类别永远可协商,而不会变成无法申诉的封禁。硬类别与 `neutral` 仅保留一条关键区别:**永不进入学习**,因此反复确认也不可能把它沉淀成自动放行。
|
|
263
|
+
(拿模型的判断当拒绝依据是实测出来的问题:一条无害的 `git commit -F …` 被判 `remote`,直接自动拒绝——没有面板、也没有可重试的授权入口。)
|
|
256
264
|
- `risky:neutral` → 若开启 `riskLearning`(设置卡片内,默认关闭),人工批准且真实执行的 neutral 风险会按 `tool|类别` 计数;计数达到 `riskThreshold`(默认 3)且新调用的操作指纹(命令词 + 目标基名)命中已确认样本时,**同一操作**自动放行。不同目标永不复用该放行。开启学习沉淀(`riskSediment`,默认开)后,满阈值 key 的确认样本会成为**确定性放行规则**:指纹精确命中即直接放行、无需再过 LLM——即使关闭 llmAssist 也继续生效;沉淀规则在设置卡片中可见、可管理(终止学习 / 删除样本)。
|
|
257
265
|
- 超时(`riskTimeoutMs`,默认 20s,重试 1 次)、传输失败与协议外输出均维持原 `ask`——门禁绝不猜测。
|
|
258
266
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Baseline Snapshot: feat/capability-absorption
|
|
2
|
+
|
|
3
|
+
> Captured at T0.3/T0.4 on 2026-09-15
|
|
4
|
+
>
|
|
5
|
+
> **为什么这份快照留在仓库里**:下面那 30 个测试文件是能力吸收(6 维度 / 网络执行面 / 热重载)
|
|
6
|
+
> 落地**之前**的既有覆盖,充当回归护栏——新增行为**不得**靠修改它们来适配。
|
|
7
|
+
> 原位置 `.agents/baseline-snapshot.md` 未入库(`.agents/` 是仓库惯例的本地目录),
|
|
8
|
+
> 2026-09-17 迁到 `docs/` 以便随包分发、可追溯。
|
|
9
|
+
> 注意:仓库当前已有 **40** 个测试文件;本列表是**护栏基线**,不是当前全量清单。
|
|
10
|
+
|
|
11
|
+
## Gate Status
|
|
12
|
+
|
|
13
|
+
| Gate | Status | Notes |
|
|
14
|
+
|------|--------|-------|
|
|
15
|
+
| typecheck | ✅ 0 errors | After pre-existing fix commit |
|
|
16
|
+
| test | ✅ 278/278 | 30 spec files |
|
|
17
|
+
| build | ✅ tsc + tsdown | client.js 99.95 kB |
|
|
18
|
+
| lint | ✅ 0 errors | — |
|
|
19
|
+
|
|
20
|
+
## Pre-existing Fixes (committed)
|
|
21
|
+
|
|
22
|
+
- `src/risk.ts:137-138` — cast `req.args` to `Record<string, unknown>` (was `{}`)
|
|
23
|
+
- `src/runtime.ts:26` — add `import { decomposeShellCommand } from './shell.js'`
|
|
24
|
+
|
|
25
|
+
## Test File List (30 files, DO NOT modify to accommodate new behavior)
|
|
26
|
+
|
|
27
|
+
1. audit.spec.ts
|
|
28
|
+
2. auto-allow-tools.spec.ts
|
|
29
|
+
3. compiler.spec.ts
|
|
30
|
+
4. custom-llm.spec.ts
|
|
31
|
+
5. data-home.spec.ts
|
|
32
|
+
6. deny-keywords.spec.ts
|
|
33
|
+
7. deny-keywords-scope.spec.ts
|
|
34
|
+
8. engine.spec.ts
|
|
35
|
+
9. evaluate.spec.ts
|
|
36
|
+
10. events.spec.ts
|
|
37
|
+
11. feature.spec.ts
|
|
38
|
+
12. grant.spec.ts
|
|
39
|
+
13. host-llm.spec.ts
|
|
40
|
+
14. learning.spec.ts
|
|
41
|
+
15. manual-approval.spec.ts
|
|
42
|
+
16. patch-presets.spec.ts
|
|
43
|
+
17. permissive.spec.ts
|
|
44
|
+
18. pre-execute.spec.ts
|
|
45
|
+
19. preset-scope.spec.ts
|
|
46
|
+
20. review.spec.ts
|
|
47
|
+
21. risk.spec.ts
|
|
48
|
+
22. rule.spec.ts
|
|
49
|
+
23. runtime.spec.ts
|
|
50
|
+
24. runtime-risk.spec.ts
|
|
51
|
+
25. sediment.spec.ts
|
|
52
|
+
26. session-resolution.spec.ts
|
|
53
|
+
27. session-sweep.spec.ts
|
|
54
|
+
28. session-sweep-apply.spec.ts
|
|
55
|
+
29. shell.spec.ts
|
|
56
|
+
30. write-path.spec.ts
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Rules File Format (`permissions` YAML)
|
|
2
|
+
|
|
3
|
+
> **Authority**: the dimension list below is byte-for-byte identical to `VALID_KEYS` in
|
|
4
|
+
> `src/rule.ts:181`. A change to either must be mirrored; `test/rule.spec.ts` and
|
|
5
|
+
> `test/rule-dims.spec.ts` are the runtime guards.
|
|
6
|
+
> Applies to `dsh-perm-gate` **2.4.1** (DSH ≥ 0.1.2-alpha.1).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Location and loading
|
|
11
|
+
|
|
12
|
+
| Item | Description |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Default path | `<dataDir>/rules.yml` (`dataDir` resolved by `config.resolveDataDir`, default `$DSH_HOME/perm-gate/`) |
|
|
15
|
+
| Explicit | `config.rulesFile` |
|
|
16
|
+
| Rule chain | Multi-file chains supported (`rulesChain` / `searchUp` / `fallbackPath` / `badFilePolicy` / `maxRules`) — see `src/rule-chain.ts` |
|
|
17
|
+
| Hot reload | `src/watch.ts` (chokidar) watches the effective files and recompiles the whole chain after a debounce |
|
|
18
|
+
| Parse failure | **fail loud** — invalid rules raise, they never silently stand the gate down |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. Top level
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
defaultAction: ask # optional; used when nothing matches. Default: ask
|
|
26
|
+
deny: # action partitions (arrays)
|
|
27
|
+
- <rule entry>
|
|
28
|
+
allow:
|
|
29
|
+
- <rule entry>
|
|
30
|
+
ask:
|
|
31
|
+
- <rule entry>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**The action IS the partition**: entries carry no `action` field (if present it must agree with its
|
|
35
|
+
partition). Evaluation order is fixed: **deny → allow → ask → defaultAction**, first match wins
|
|
36
|
+
(deny wins over allow). The keyword blacklist runs ahead of `deny`.
|
|
37
|
+
|
|
38
|
+
`defaultAction` accepts `allow` | `ask` | `deny` (default `ask`).
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Entry fields (`VALID_KEYS`)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
tools · command · args · paths · params · absent · agents · when · argv · network · branch
|
|
46
|
+
action · reason · enabled
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Field | Type | Purpose |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `tools` | string / string[] | Tool-name glob |
|
|
52
|
+
| `command` | string / string[] | Command word, with `word#recursive` / `word#force` suffixes |
|
|
53
|
+
| `args` | string / string[] | Argument token scan |
|
|
54
|
+
| `paths` | string / string[] | Workspace-relative path glob |
|
|
55
|
+
| `params` | mapping | Parameter key → value glob list |
|
|
56
|
+
| `absent` | string / string[] | Parameter keys that must NOT be present |
|
|
57
|
+
| `agents` | string / string[] | `main` / `subagent` / `preset:<name>` |
|
|
58
|
+
| `when` | mapping | Environment and platform conditions |
|
|
59
|
+
| `argv` | mapping | Extra argv patterns (`pipeline`) |
|
|
60
|
+
| `network` | mapping | Domain / IP / port / scheme |
|
|
61
|
+
| `branch` | mapping | Git branch / remote / protected branch (git commands only) |
|
|
62
|
+
| `action` | `allow`\|`ask`\|`deny` | Optional; must agree with its partition |
|
|
63
|
+
| `reason` | string | Human-readable rationale (lands in events and the UI) |
|
|
64
|
+
| `enabled` | boolean | `false` skips the entry |
|
|
65
|
+
|
|
66
|
+
**Across dimensions = AND**: every dimension present in one entry must be satisfied.
|
|
67
|
+
**Within a dimension = OR**: any single pattern in the list suffices (`params` / `when` /
|
|
68
|
+
`network` use AND across their sub-keys — see below).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 4. Dimension semantics
|
|
73
|
+
|
|
74
|
+
### 4.1 `tools` — tool name
|
|
75
|
+
```yaml
|
|
76
|
+
- tools: [shell, pwsh, bash]
|
|
77
|
+
```
|
|
78
|
+
Glob match. OR within the list.
|
|
79
|
+
|
|
80
|
+
### 4.2 `command` — command word
|
|
81
|
+
```yaml
|
|
82
|
+
- command: [rm#recursive, rm#force]
|
|
83
|
+
```
|
|
84
|
+
Words come from argv decomposition (`src/shell.ts`). The `#recursive` / `#force` suffixes
|
|
85
|
+
require the corresponding semantics. OR within the list.
|
|
86
|
+
|
|
87
|
+
### 4.3 `args` — argument tokens
|
|
88
|
+
```yaml
|
|
89
|
+
- args: ["--force", "-rf"]
|
|
90
|
+
```
|
|
91
|
+
Scans the decomposed token set. **⚠ Semantics are OR**: any single token matching any single
|
|
92
|
+
pattern matches. It can **not** require "`push` and `--force` together", and it can not tell
|
|
93
|
+
`git push --force origin main` from `git checkout --force main` — use `params` (or the future
|
|
94
|
+
`branch` dimension) when you need that discrimination.
|
|
95
|
+
|
|
96
|
+
### 4.4 `paths` — paths
|
|
97
|
+
```yaml
|
|
98
|
+
- paths: ["src/**", "!src/vendor/**"]
|
|
99
|
+
```
|
|
100
|
+
Workspace-relative globs; `!` negates. OR within the list.
|
|
101
|
+
|
|
102
|
+
### 4.5 `params` — key-targeted matching (**AND over keys**)
|
|
103
|
+
```yaml
|
|
104
|
+
- params:
|
|
105
|
+
command: ["*--force*", "!*--dry-run*"]
|
|
106
|
+
flags.mode: ["production"]
|
|
107
|
+
```
|
|
108
|
+
- Each key's **actual value** must match **at least one** pattern of that key (OR within the list)
|
|
109
|
+
- **Every listed key must be satisfied** (AND across keys)
|
|
110
|
+
- A single `!`-prefixed pattern negates (the value must NOT match the remainder)
|
|
111
|
+
- An empty pattern list only requires the key to exist (any value)
|
|
112
|
+
|
|
113
|
+
This is the main tool for compound conditions — the AND cases `args` can not express.
|
|
114
|
+
|
|
115
|
+
### 4.6 `absent` — keys must be missing (AND)
|
|
116
|
+
```yaml
|
|
117
|
+
- absent: [dry_run, force]
|
|
118
|
+
```
|
|
119
|
+
Every listed parameter key must be absent.
|
|
120
|
+
|
|
121
|
+
### 4.7 `agents` — agent identity
|
|
122
|
+
```yaml
|
|
123
|
+
- agents: ["main", "preset:reviewer"]
|
|
124
|
+
```
|
|
125
|
+
Valid values: `main`, `subagent`, `preset:<name>` (case-insensitive); anything else fails at parse
|
|
126
|
+
time. Candidates come from session headers via `src/agent-identity.ts`. OR within the list.
|
|
127
|
+
|
|
128
|
+
### 4.8 `when` — environment and platform (AND)
|
|
129
|
+
```yaml
|
|
130
|
+
- when:
|
|
131
|
+
env:
|
|
132
|
+
NODE_ENV: ["production"]
|
|
133
|
+
CI: ["true", "1"]
|
|
134
|
+
platform: [linux, win32]
|
|
135
|
+
```
|
|
136
|
+
- `env`: key → allowed values (**OR within a list**, **AND across keys**)
|
|
137
|
+
- `platform`: Node.js `process.platform` values
|
|
138
|
+
- `nodeVersion`: reserved, not yet evaluated
|
|
139
|
+
|
|
140
|
+
### 4.9 `argv` — extra argv patterns
|
|
141
|
+
```yaml
|
|
142
|
+
- argv:
|
|
143
|
+
pipeline: ["curl*|sh", "wget*|bash"]
|
|
144
|
+
```
|
|
145
|
+
`pipeline` matches the **whole pipeline string**: each simple command's **full argv**
|
|
146
|
+
(command word + args + redirect targets) joined by `|`, so `curl https://x.sh | sh`
|
|
147
|
+
yields `curl https://x.sh|sh`.
|
|
148
|
+
|
|
149
|
+
**⚠ The `|` in a pattern is a literal**, so `curl|sh` only matches the argument-less
|
|
150
|
+
adjacent form; covering the common form with arguments requires `curl*|sh`.
|
|
151
|
+
(Historical defect: the match string was built from the command word alone, so
|
|
152
|
+
`curl|sh` matched the harmless `curl|sh` while the genuinely dangerous
|
|
153
|
+
`curl https://x.sh | sh` did not match at all.)
|
|
154
|
+
|
|
155
|
+
### 4.10 `network` — network targets (AND across sub-dimensions)
|
|
156
|
+
```yaml
|
|
157
|
+
- network:
|
|
158
|
+
domains: ["*.internal.corp", "github.com"]
|
|
159
|
+
ips: ["10.0.0.0/8", "192.168.1.1"]
|
|
160
|
+
ports: ["443", "8000-9000"]
|
|
161
|
+
schemes: [https, http]
|
|
162
|
+
```
|
|
163
|
+
- Every **present** sub-dimension must match (AND); inside a sub-dimension, OR
|
|
164
|
+
- `domains` supports globs; `ips` supports CIDR; `ports` supports ranges; `schemes` are protocol names
|
|
165
|
+
|
|
166
|
+
The network dimension acts on two seams: the static URL-candidate check in `tools/pre-execute`,
|
|
167
|
+
and the loopback proxy that intercepts real egress traffic (`network.enabled` is **false by
|
|
168
|
+
default** — opt-in).
|
|
169
|
+
|
|
170
|
+
### 4.11 `branch` — git branch / remote / protected branch (AND across sub-dimensions)
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
- command: [git]
|
|
174
|
+
args: [push]
|
|
175
|
+
branch:
|
|
176
|
+
target: [main, master, "release*"]
|
|
177
|
+
remote: [origin]
|
|
178
|
+
shared: true
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Sub-field | Semantics |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `target` | Branch-name glob (`*` **crosses** `/`, so `release*` matches `release/1.0`) |
|
|
184
|
+
| `remote` | Remote-name glob |
|
|
185
|
+
| `shared` | Require the target branch to be a **protected** branch |
|
|
186
|
+
|
|
187
|
+
**Why this dimension exists**: `args` is OR-over-tokens (§4.3). It can ask "does `--force`
|
|
188
|
+
appear?" but never "does `--force` appear **on a push to a protected branch**?". The decisive
|
|
189
|
+
case is telling two commands with identical tokens apart:
|
|
190
|
+
|
|
191
|
+
- `git push --force origin main` → dangerous, must match
|
|
192
|
+
- `git checkout --force main` → routine, must **not** match
|
|
193
|
+
|
|
194
|
+
Candidates come from the shared command classifier (`src/command-dispatcher.ts` +
|
|
195
|
+
`src/parsers/git.ts`), so `refspec` forms (`HEAD:main`) are already split, a remote name is
|
|
196
|
+
never mistaken for a branch name, and flags are normalized.
|
|
197
|
+
|
|
198
|
+
**Three boundaries worth knowing**:
|
|
199
|
+
|
|
200
|
+
1. **Git commands only.** A non-git command (`rm -rf`, `npm install`, …) satisfies no `branch`
|
|
201
|
+
sub-dimension, so a rule that names a branch simply does not match it (and never throws) —
|
|
202
|
+
those commands are covered by the write-path layer and the blacklist.
|
|
203
|
+
2. **`shared` is static.** It comes from the parser's protected-branch list
|
|
204
|
+
(`PROTECTED_BRANCHES` in `src/parsers/git.ts`: `main` / `master` / `production` / `release` /
|
|
205
|
+
`stable`). It does **not** run `git branch -r --contains` — that would put a subprocess on the
|
|
206
|
+
decision path. Note `shared` and `target` are independent sub-dimensions: with
|
|
207
|
+
`target: ["release*"]`, `release/1.0` satisfies `target` but not `shared` (the protected list
|
|
208
|
+
matches exactly).
|
|
209
|
+
3. **AND across sub-dimensions, OR within each**; any one of several simple commands
|
|
210
|
+
(pipeline / `&&`) may satisfy it.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 5. Complete example
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
defaultAction: ask
|
|
218
|
+
|
|
219
|
+
deny:
|
|
220
|
+
# Credential material in a command (P0 hard-deny also backs this up)
|
|
221
|
+
- tools: [shell, pwsh]
|
|
222
|
+
params:
|
|
223
|
+
command: ["*AWS_SECRET*", "*PRIVATE KEY*"]
|
|
224
|
+
reason: "credential material in command"
|
|
225
|
+
|
|
226
|
+
# Download-and-execute via a pipe
|
|
227
|
+
- argv:
|
|
228
|
+
pipeline: ["curl*|sh", "wget*|bash"]
|
|
229
|
+
reason: "piped download-and-execute"
|
|
230
|
+
|
|
231
|
+
# Push to a protected branch (the distinction `args` can not make:
|
|
232
|
+
# the same tokens on a checkout do not match)
|
|
233
|
+
- command: [git]
|
|
234
|
+
args: [push]
|
|
235
|
+
branch:
|
|
236
|
+
target: [main, master, "release*"]
|
|
237
|
+
shared: true
|
|
238
|
+
reason: "push to a protected branch"
|
|
239
|
+
|
|
240
|
+
# Forced recursive delete in production
|
|
241
|
+
- command: [rm#recursive, rm#force]
|
|
242
|
+
when:
|
|
243
|
+
env:
|
|
244
|
+
NODE_ENV: ["production"]
|
|
245
|
+
reason: "recursive force delete in production"
|
|
246
|
+
|
|
247
|
+
allow:
|
|
248
|
+
# Common read-only queries
|
|
249
|
+
- tools: [read, grep, glob]
|
|
250
|
+
reason: "read-only workspace queries"
|
|
251
|
+
|
|
252
|
+
# Common dev hosts
|
|
253
|
+
- network:
|
|
254
|
+
domains: [github.com, "*.githubusercontent.com"]
|
|
255
|
+
schemes: [https]
|
|
256
|
+
reason: "common dev hosts"
|
|
257
|
+
|
|
258
|
+
ask:
|
|
259
|
+
# Push needs confirmation (note: args is OR, so this only requires `push` to appear)
|
|
260
|
+
- command: [git]
|
|
261
|
+
args: ["push"]
|
|
262
|
+
reason: "git push requires confirmation"
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The same sample ships with the package: `examples/permissions.example.yaml`.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 6. Pairing with the CLI dry-run
|
|
270
|
+
|
|
271
|
+
Dry-run before you go live:
|
|
272
|
+
|
|
273
|
+
```powershell
|
|
274
|
+
# List the compiled rules (default action, rule count, permissive-strategy snapshot)
|
|
275
|
+
dsh-perm-gate --rules permissions.yaml --list
|
|
276
|
+
|
|
277
|
+
# Evaluate one call (--args is JSON; short values can be inlined)
|
|
278
|
+
dsh-perm-gate --rules permissions.yaml --tool read --args "{\"file_path\":\"src/index.ts\"}"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The output is JSON: `decision` (`allow` / `ask` / `deny`), `reason`, `audited`.
|
|
282
|
+
|
|
283
|
+
**Two gotchas worth knowing**:
|
|
284
|
+
|
|
285
|
+
1. **The blacklist fires before your rules.** The `deny-keyword` layer runs ahead of the `deny:`
|
|
286
|
+
partition, so preset words like `rm -rf` or `push --force` are vetoed before your rules are
|
|
287
|
+
consulted — a `deny-keyword:` prefix in the dry-run output means that layer decided, not your rule.
|
|
288
|
+
2. **Long quoted arguments get re-parsed by PowerShell** (you may see
|
|
289
|
+
`--args must be valid JSON`). Safe options: write the JSON to a file and pass
|
|
290
|
+
`--args (Get-Content -Raw args.json)`, or lean on the dimension coverage in
|
|
291
|
+
`test/rule-dims.spec.ts` and `test/evaluate.spec.ts`.
|
|
292
|
+
|
|
293
|
+
The CLI and the runtime share one decision engine (`evaluate.ts` + `engine.ts`), so its output is
|
|
294
|
+
the real verdict.
|