dsh-perm-gate 2.0.0 → 2.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/INSTALL.ja.md +57 -1
  3. package/INSTALL.ko.md +54 -1
  4. package/INSTALL.md +53 -1
  5. package/INSTALL.zh.md +48 -1
  6. package/README.ja.md +86 -12
  7. package/README.ko.md +83 -12
  8. package/README.md +120 -24
  9. package/README.zh.md +88 -10
  10. package/cordis.patch.yml +34 -2
  11. package/dsh.plugin.json +22 -0
  12. package/lib/agent-identity.d.ts +43 -0
  13. package/lib/agent-identity.js +95 -0
  14. package/lib/client.js +114 -0
  15. package/lib/client.js.map +1 -1
  16. package/lib/compiler.d.ts +39 -0
  17. package/lib/compiler.js +106 -0
  18. package/lib/config.d.ts +95 -2
  19. package/lib/config.js +28 -3
  20. package/lib/engine.js +9 -2
  21. package/lib/evaluate.d.ts +9 -0
  22. package/lib/evaluate.js +189 -0
  23. package/lib/events.d.ts +13 -0
  24. package/lib/events.js +31 -0
  25. package/lib/index.d.ts +1 -1
  26. package/lib/index.js +235 -6
  27. package/lib/network-lifecycle.d.ts +107 -0
  28. package/lib/network-lifecycle.js +180 -0
  29. package/lib/network.d.ts +109 -0
  30. package/lib/network.js +196 -0
  31. package/lib/proxy.d.ts +160 -0
  32. package/lib/proxy.js +529 -0
  33. package/lib/risk.js +25 -3
  34. package/lib/rule-chain.d.ts +39 -0
  35. package/lib/rule-chain.js +222 -0
  36. package/lib/rule-dims.d.ts +139 -0
  37. package/lib/rule-dims.js +194 -0
  38. package/lib/rule.d.ts +25 -0
  39. package/lib/rule.js +15 -1
  40. package/lib/runtime.d.ts +87 -0
  41. package/lib/runtime.js +327 -2
  42. package/lib/session-sweep.d.ts +45 -0
  43. package/lib/session-sweep.js +153 -0
  44. package/lib/shadow.d.ts +34 -0
  45. package/lib/shadow.js +143 -0
  46. package/lib/watch.d.ts +42 -0
  47. package/lib/watch.js +170 -0
  48. package/package.json +8 -2
  49. package/scripts/patch-permission-glyph.mjs +209 -0
package/README.ko.md CHANGED
@@ -26,8 +26,8 @@
26
26
  >
27
27
  > | DSH 버전 | 브랜치 | 버전 | npm 태그 |
28
28
  > | --- | --- | --- | --- |
29
- > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` / `@dsh-0.1.1` |
30
- > | 0.1.2-alpha.1+ (0.1.5-rc.2 포함) | `main` | `2.x` | `@latest` / `@2.x` / `@dsh-0.1.2` |
29
+ > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
30
+ > | 0.1.2-alpha.1+ (0.1.5-rc.2 포함) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x`는 범위) |
31
31
  >
32
32
  > 버전 계열은 **DSH 라인**을 따릅니다(`1.x` = DSH ≤ 0.1.1, `2.x` = DSH 0.1.2+). 메이저
33
33
  > 버전이 서로를 차단하므로 `^1.x` 설치가 `2.x`를 해석하는 일은 없고 그 반대도
@@ -43,7 +43,7 @@
43
43
  > 모두에서 user-approval 서비스의 **개인** 메소드이므로 `typeof` 프로브를 통해
44
44
  > 읽으며, 누락되거나 예외 발생 시 "정책_unknown"으로 후퇴합니다.
45
45
 
46
- 버전 **2.0.0** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
46
+ 버전 **2.1.2** — 변경 내역은 [한국어 changelog](./CHANGELOG.ko.md)를 참고하세요.
47
47
 
48
48
  DeepSeek Harness용 단일·자족적·결정론 우선·fail-closed 권한 게이트입니다.
49
49
 
@@ -84,7 +84,7 @@ DSH 안전 생태계에서는 이 역할이 `dsh-permission-rules` / `dsh-auto-m
84
84
  모드**. 범용 "자동 승인"도 아니고 포괄적 권한 부여도 아닙니다. 프론트는 **단일 스위치**
85
85
  (`permissive`)만 노출하고, 백엔드의 네 가지 전략은 **조합 가능**하며 플러그인
86
86
  설정으로 제어됩니다. P0에 대해서는 여전히 fail-closed입니다.
87
- 권한 선택기와 설정 행 모두 제품명 「自动审查」로 표시하며 아이콘은 그리지 않습니다.
87
+ 권한 선택기와 설정 행 모두 제품명 「自动审查」로 표시합니다. 아이콘은 아래를 참고하세요(내장 티어는 기본 제공, 플러그인 티어는 패치 필요).
88
88
  - **샌드박스 승격 자동 응답** (`trustEscalation`) — 샌드박스 승격은 shell / pwsh /
89
89
  edit 도구의 **내부**(`tools/pre-execute` 이후)에서 발생하므로 게이트가 이를 볼 수
90
90
  없고, 게이트가 자동 허용한 호출에도 확인 프롬프트가 나타납니다. 이 전략을 켜면 게이트가
@@ -114,7 +114,7 @@ dsh plugin --profile web add dsh-perm-gate
114
114
  rulesFile: ./permissions.yaml # 선택. 기본값은 $DSH_HOME/perm-gate/rules.yml
115
115
  dshHome: $DSH_HOME # 보호 대상 검사의 고정 루트
116
116
  defaultAction: ask # allow | ask | deny
117
- gatePresets: [permissive] # 게이트가 활성화되는 티어(기본)
117
+ gatePresets: [permissive, permissive-full] # 게이트가 활성화되는 티어(기본)
118
118
  ```
119
119
 
120
120
  ### 규칙 파일
@@ -144,6 +144,54 @@ permissions:
144
144
 
145
145
  전체 예시: [examples/permissions.example.yaml](./examples/permissions.example.yaml)
146
146
 
147
+ ### 네트워크 정책(선택적 활성화)
148
+
149
+ 로컬 HTTP/CONNECT 프록시입니다. **동일한 규칙 파일**로 **shell 서브프로세스**의 아웃바운드
150
+ 트래픽을 심사하고, 어떤 규칙에도 해당하지 않는 대상에는 승인 경로를 제공합니다. **기본값은
151
+ 비활성** —— 활성화하면 루프백 포트를 바인딩하고 자식 프로세스의 프록시 환경 변수를 다시 쓰기
152
+ 때문에 암묵적으로 켜지지 않습니다.
153
+
154
+ ```yaml
155
+ - id: dsh-perm-gate
156
+ config:
157
+ networkEnabled: false # 마스터 스위치(기본 false)
158
+ networkMode: whitelist # deny-all | whitelist | allow-all
159
+ networkUnlisted: ask # ask | deny —— 미등록 대상 처리
160
+ networkUnattributed: allow # allow | deny —— shell 귀속 없는 트래픽
161
+ networkInjectEnv: true # 자식 프로세스에 HTTP(S)_PROXY / ALL_PROXY 주입
162
+ networkAskTimeoutMs: 120000 # 승인 대기 상한, 초과 시 거부
163
+ networkGrantTtlMs: 1800000 # 승인 1회가 유효한 세션 기간
164
+ ```
165
+
166
+ **단계적 동작**: 허용 규칙이 없으면 어떤 대상도 나갈 수 없습니다. 미등록 대상은 해당 연결을
167
+ 연 shell 명령의 세션에 대해 대화형 승인으로 에스컬레이션되고, 승인하면 그 대상이 해당
168
+ 세션에서 허용됩니다. **`deny` 규칙은 승인으로 올라가지 않습니다** —— 승인은 "어떤 규칙도
169
+ 금지하지 않은 대상"의 도달 범위를 넓힐 수 있지만, 금지하는 규칙을 뒤집을 수는 없습니다.
170
+
171
+ **경계 —— 의존하기 전에 반드시 읽으십시오**: 이 프록시는 **협조형** 정책 계층이며 강제
172
+ 경계가 아닙니다. 프록시 환경 변수를 **읽는** 클라이언트의 트래픽만 볼 수 있습니다.
173
+
174
+ | 클라이언트 | 차단 가능? |
175
+ |------------|------------|
176
+ | `curl`, `wget`, `git`, Go `net/http`, Python `requests` | ✅ |
177
+ | **Node.js `http` / `https` / `fetch`** | ❌ **직접 연결. 프록시가 보지 못함** |
178
+ | Java(`-D` 프록시 미지정), .NET `HttpClient` | ❌ |
179
+ | 원시 소켓, 직접 구현한 TCP | ❌ |
180
+ | DNS, QUIC/HTTP3, 비 HTTP 프로토콜 | ❌ |
181
+ | 리터럴 IP 연결 | ❌ |
182
+
183
+ 따라서 `node -e "require('http').get('http://host/')"` 같은 명령은 **차단되지 않습니다**.
184
+ 이것을 "실수를 막는 가드레일이자 의도를 선언하는 지점"으로 보십시오. 밀폐된 샌드박스가
185
+ 아닙니다.
186
+
187
+ DSH **자신**의 트래픽 —— 내장 네트워크 도구와 LLM 전송 —— 은 의도적으로 대상에서 제외됩니다.
188
+ 이 연결들은 shell 귀속이 없고, `networkUnattributed: allow`(기본값)가 그대로 통과시킵니다.
189
+ 심사하면 호스트가 **스스로를 차단**하게 되며, 이는 놓치는 것보다 훨씬 나쁜 장애입니다.
190
+ 호스트의 클라이언트가 프록시 환경 변수를 읽지 않는다고 확신할 때만 `deny`로 바꾸십시오.
191
+
192
+ 실시간 상태 조회: `GET /api/dsh-perm-gate/network`(모드 / 바인드 / 포트 / 프록시 활성 /
193
+ 환경 주입 상태 / 차단 카운터 / 최근 차단).
194
+
147
195
  ## 自动审查 티어(머신 값 `permissive`)
148
196
 
149
197
  自动审查는 권한 선택기에서 읽기 전용 / 워크스페이스 내 수정 / 완전 권한 / 허용 목록과
@@ -151,11 +199,33 @@ permissions:
151
199
  사람/LLM 심 **이전에** 판정을 좁히거나 넓힐 뿐이고, P0 하드 거부는 단조롭고 협상
152
200
  불가능하게 유지됩니다.
153
201
 
154
- 선택기의 표시 이름은 **호스트가 공급하는 제품명**이며 언어별 사전 항목이 아닙니다. DSH 0.1.2는
202
+ **두 가지 변형을 함께 제공합니다.** 프리셋의 `sandbox`와 `approval`은 **독립된** 노브이며, 둘을
203
+ 묶으면 나쁜 트레이드오프를 강요받기 때문입니다.
204
+
205
+ | 선택기 표시 이름 | 머신 값 | sandbox | approval |
206
+ |------------------|---------|---------|----------|
207
+ | 自动审查 | `permissive` | `workspace-write` | `ask` |
208
+ | 自动审查(고권한) | `permissive-full` | `danger-full-access` | `ask` |
209
+
210
+ 일반 티어는 내장 파일 샌드박스를 유지합니다. 그 샌드박스는 자식 프로세스 시작에 필요한 이름 있는
211
+ 파이프도 거부하므로 `git clone`, MSYS2/Cygwin의 `sh.exe`, ConPTY가 `Win32 error 5` /
212
+ `couldn't create signal pipe`로 실패합니다. 게다가 게이트는 **`gatePresets`에 나열한 티어에서만
213
+ 동작**하므로, 게이트를 쓰려면 이 제한을 받아들여야 했습니다. 「自动审查(고권한)」는 이 결합을
214
+ 풉니다 —— **승인 동작은 동일하게 유지하고 파일 샌드박스 제한만 해제합니다**. 둘 다 기본
215
+ `gatePresets`에 포함되므로 어느 쪽을 선택해도 P0–P4 전체 체인이 동작합니다. 게이트는 프리셋의
216
+ **이름**만 읽고 sandbox 모드는 읽지 않습니다.
217
+
218
+ 선택기의 표시 이름은 **호스트가 공급하는 제품명**이며 언어별 사전 항목이 아닙니다. DSH는
155
219
  플러그인 티어의 `name:`을 두 권한 화면(일반 설정 기본 행과 입력창 선택기)에 그대로 렌더링하고
156
220
  자체 지역화 라벨은 세 가지 내장 값에만 부여하므로, `cordis.patch.yml`이 모든 세션에 중국어
157
- 라벨을 배포합니다. 이 티어는 **아이콘을 그리지 않습니다** —— 선택기의 글리프는 내장 값에만
158
- 연결됩니다.
221
+ 라벨을 배포합니다.
222
+
223
+ **아이콘은 다른 이야기입니다.** 입력창의 글리프 표는 닫혀 있고, 그 표 자체의 주석이 규칙을
224
+ 말합니다: *host-configured names outside the design set get none*. `permissive`는 내장 값이라
225
+ 「自动审查」에는 원래부터 방패+눈 아이콘이 있습니다. 「自动审查(高权限)」가 같은 아이콘을
226
+ 갖는 것은 `npx dsh-perm-gate-patch-glyph`가 그 표에 항목 하나를 추가했기 때문입니다.
227
+ 이 패치는 **호스트** 패키지를 고치므로 DSH 업그레이드마다 사라집니다 ——
228
+ [DSH 업그레이드 후: 입력창 아이콘 패치 재적용](./INSTALL.ko.md)을 참고하세요.
159
229
 
160
230
  `cordis.yml`에서:
161
231
 
@@ -221,8 +291,8 @@ never` 패스스루 — 는 사람에 그대로 위임되므로 향후 DSH 변
221
291
  선택기에는 "auto-approval"이 아니라 **독립적으로 선택 가능한 승인 티어**인
222
292
  「自动审查」가 놓입니다.
223
293
 
224
- 게이트가 동작하는 범위는 **`gatePresets`에 나열한 티어 안뿐**입니다(기본 `['permissive']`,
225
- 이 플러그인이 추가하는 티어). 그 밖의 티어(Read Only / Workspace Write / Full access /
294
+ 게이트가 동작하는 범위는 **`gatePresets`에 나열한 티어 안뿐**입니다(기본
295
+ `['permissive', 'permissive-full']`, 이 플러그인이 추가하는 두 티어). 그 밖의 티어(Read Only / Workspace Write / Full access /
226
296
  `custom`)에서는 게이트의 판정 흐름이 **전혀 실행되지 않습니다** — 허용도, ask도, 거부도,
227
297
  P0 하드 거부도, 거부 키워드 차단도, 감사 이벤트 기록도 하지 않습니다. 선택한 티어의 자체 정책이
228
298
  호출을 결정합니다. `danger-full-access`의 정의는 "승인 프롬프트 없는 전체 접근"이며, 이를 ask로
@@ -261,8 +331,9 @@ host는 네임스페이스를 live로 읽으므로 변경은 재시작 없이
261
331
  카드에서 목록으로 편집할 수 있고(프리셋 항목에는 태그 표시, 원클릭 복원 지원) 미설정·빈 목록 시 프리셋이
262
332
  적용됩니다——블랙리스트가 조용히 꺼지지 않습니다.
263
333
  블랙리스트가 조용히 꺼지는 일은 없습니다.
264
- 이 티어는 권한 선택기에 **아이콘을 표시하지 않습니다** —— 선택기의 글리프는 내장 세 값에만
265
- 연결되고, 플러그인 티어는 텍스트만입니다.
334
+ 「自动审查」와 「自动审查(高权限)」는 둘 다 선택기에 방패+눈 아이콘을 그립니다 — 전자는
335
+ DSH 내장 표에서, 후자는 설치 안내에 적힌 호스트 패치에서 옵니다. 그 패치가 없으면 두 번째
336
+ 티어는 모든 화면에서 텍스트만 표시됩니다. 라벨과 게이트는 영향을 받지 않습니다.
266
337
 
267
338
 
268
339
  ## CLI(독립 실행 dry-run)
package/README.md CHANGED
@@ -25,8 +25,8 @@
25
25
  >
26
26
  > | DSH version | Branch | Version | npm tag |
27
27
  > | --- | --- | --- | --- |
28
- > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` / `@dsh-0.1.1` |
29
- > | 0.1.2-alpha.1+ (incl. 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@2.x` / `@dsh-0.1.2` |
28
+ > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
29
+ > | 0.1.2-alpha.1+ (incl. 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` is a range) |
30
30
  >
31
31
  > The series number tracks the **DSH line** (`1.x` = DSH ≤ 0.1.1, `2.x` = DSH 0.1.2+),
32
32
  > and the majors fence each other: a `^1.x` install never resolves a `2.x` release and
@@ -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.0.0** — see the [Changelog](./CHANGELOG.md).
45
+ Version **2.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
 
@@ -77,11 +77,13 @@ cross-plugin version coupling.
77
77
  malformed rules, and source-hash compile caching.
78
78
  - **Audit** — every decision is logged as an `{ignorable:true}` event with its `callId`; the
79
79
  model-visible reason matches the recorded outcome.
80
- - **自动审查 tier** (`permissive`) — an **independent approval mode** (separate from read-only,
81
- full-access and whitelist tiers) that is neither "auto-approve" nor blanket trust. Front-end
82
- exposes a **single switch** (`permissive`); the four backend strategies are **combinable** and
83
- driven by plugin settings — still fail-closed against P0. The permission picker and the
84
- settings row both show it under the product label 自动审查, with no icon.
80
+ - **自动审查 tier** (`permissive`, plus `permissive-full`) — an **independent approval mode**
81
+ (separate from read-only, full-access and whitelist tiers) that is neither "auto-approve" nor
82
+ blanket trust. Front-end exposes a **single switch** (`permissive`); the four backend strategies
83
+ are **combinable** and driven by plugin settings — still fail-closed against P0. The permission
84
+ picker and the settings row both show it under the product label 自动审查, with no icon. The
85
+ `permissive-full` variant keeps the identical approval behaviour but drops the built-in file
86
+ sandbox, which otherwise denies the named pipes `git clone` / Cygwin / ConPTY need.
85
87
  - **Sandbox-escalation auto-answer** (`trustEscalation`) — a sandbox escalation is asked from
86
88
  *inside* the shell / pwsh / edit tool body, after `tools/pre-execute`, so the gate never saw
87
89
  it and a call it auto-allowed still prompted you to approve the widening. With this strategy
@@ -125,9 +127,24 @@ Add the plugin to `cordis.yml`:
125
127
  rulesFile: ./permissions.yaml # optional; defaults to $DSH_HOME/perm-gate/rules.yml
126
128
  dshHome: $DSH_HOME # root pinned for protected-target checks
127
129
  defaultAction: ask # allow | ask | deny
128
- gatePresets: [permissive] # tiers where the gate is active at all (default)
130
+ gatePresets: [permissive, permissive-full] # tiers where the gate is active (default)
131
+ sessionSweep: true # hourly cleanup of archived/dead sessions' gate data
129
132
  ```
130
133
 
134
+ ### Session sweep
135
+
136
+ On startup and every hour the gate reads DSH's workspace store
137
+ (`$DSH_HOME/storages/workspace.json`, read-only) and classifies every session it
138
+ holds authorization-chain data for. Sessions DSH has archived (`global.archivedSessionIds`)
139
+ or no longer tracks at all have their decision events dropped from
140
+ `$DSH_HOME/perm-gate/events.jsonl` and their pre-change snapshot files deleted
141
+ from `$DSH_HOME/perm-gate/snapshots/` — history the review page can no longer
142
+ reach, for data the harness itself considers gone. Live sessions are untouched,
143
+ unattributable rows (empty session id) are never deleted, and any failure is
144
+ fail-open: the round is skipped and retried an hour later. Set `sessionSweep: false`
145
+ to disable; `workspaceStoreFile` overrides the store path. Restoring an archived
146
+ session does not restore its swept history.
147
+
131
148
  ### Rules file
132
149
 
133
150
  ```yaml
@@ -154,6 +171,59 @@ permissions:
154
171
  A command entry `word#flag` matches the command word (`word`) with the modifier `recursive` or
155
172
  `force` — so `rm#recursive` matches `rm -rf`, `env rm -rf`, and `sh -c "rm -rf /"`.
156
173
 
174
+ ### Network policy (opt-in)
175
+
176
+ A local HTTP/CONNECT proxy that adjudicates the outbound traffic of **shell
177
+ subprocesses** against the same rules file, plus an approval path for targets no
178
+ rule covers. **Off by default** — enabling it binds a loopback port and rewrites
179
+ the proxy environment for child processes, so it is never turned on implicitly.
180
+
181
+ ```yaml
182
+ - id: dsh-perm-gate
183
+ config:
184
+ networkEnabled: false # master switch (default false)
185
+ networkMode: whitelist # deny-all | whitelist | allow-all
186
+ networkUnlisted: ask # ask | deny — unlisted target handling
187
+ networkUnattributed: allow # allow | deny — traffic with no shell attribution
188
+ networkInjectEnv: true # rewrite HTTP(S)_PROXY/ALL_PROXY for children
189
+ networkAskTimeoutMs: 120000 # approval wait before failing closed
190
+ networkGrantTtlMs: 1800000 # how long one approval covers its target
191
+ ```
192
+
193
+ **Tiered behaviour.** Nothing reaches the network without an allow rule.
194
+ An unlisted target is escalated to the interactive approval seam, raised on
195
+ behalf of the shell command that opened the connection; approving widens reach
196
+ for that target for the session. A `deny` rule is **never** escalated —
197
+ approval can widen what an unlisted target may reach, but it can never override
198
+ a rule that says no.
199
+
200
+ **The boundary — read this before relying on it.** The proxy is a *cooperative*
201
+ policy layer, not an enforcement boundary. It only sees traffic from clients
202
+ that read the proxy environment:
203
+
204
+ | Client | Covered? |
205
+ |--------|----------|
206
+ | `curl`, `wget`, `git`, Go `net/http`, Python `requests` | yes |
207
+ | **Node.js `http`/`https`/`fetch`** | **no — connects directly** |
208
+ | Java (without `-D` proxy flags), .NET `HttpClient` | no |
209
+ | Raw sockets, custom TCP | no |
210
+ | DNS, QUIC/HTTP3, non-HTTP protocols | no |
211
+ | Connections to a literal IP | no |
212
+
213
+ So a shell command like `node -e "require('http').get('http://host/')"` is not
214
+ intercepted. Treat this as a guardrail against accidents and a place to state
215
+ intent, not as a hermetic sandbox.
216
+
217
+ DSH's **own** network traffic — the built-in network tools and the LLM
218
+ transport — is deliberately left alone. Those connections carry no shell
219
+ attribution, and `networkUnattributed: allow` (the default) passes them
220
+ through unreviewed: reviewing them would let the host block *itself*, which is
221
+ a worse failure than a missed block. Set `networkUnattributed: deny` only if
222
+ you know your host's clients ignore the proxy environment.
223
+
224
+ Query the live state at `GET /api/dsh-perm-gate/network` (mode, bind, port,
225
+ proxy liveness, env-injection state, block counters, recent blocks).
226
+
157
227
  ## The 自动审查 tier (machine value `permissive`)
158
228
 
159
229
  自动审查 is an **independent approval tier** in the DSH permission picker, parallel to
@@ -161,11 +231,35 @@ Read Only / Workspace Write / Full access / Whitelist. It is **not** generic "au
161
231
  never mints blanket authority: it only narrows or widens the seam *before* the human/LLM step
162
232
  while P0 hard-deny stays monotonic and non-negotiable.
163
233
 
234
+ **Two variants ship**, because a preset's `sandbox` and `approval` are independent knobs and
235
+ coupling them forced a bad trade:
236
+
237
+ | Picker label | Machine value | sandbox | approval |
238
+ |--------------|---------------|---------|----------|
239
+ | 自动审查 | `permissive` | `workspace-write` | `ask` |
240
+ | 自动审查(高权限) | `permissive-full` | `danger-full-access` | `ask` |
241
+
242
+ The plain tier keeps the built-in file sandbox. That sandbox also denies the named pipes a child
243
+ process needs to start, so `git clone`, MSYS2/Cygwin `sh.exe` and ConPTY fail under it with
244
+ `Win32 error 5` / `couldn't create signal pipe`. Because the gate is active **only** in the tiers
245
+ listed in `gatePresets`, wanting the gate meant accepting that restriction. 自动审查(高权限)
246
+ removes the coupling: identical approval behaviour, no file-sandbox restriction. The tier's own
247
+ description states the trade plainly — the workflow is smoother, approvals still apply per call,
248
+ but there is **no system sandbox left as a backstop**. Both are in the
249
+ default `gatePresets`, so either one gives you the full P0–P4 chain — the gate reads the preset
250
+ **name** only, never the sandbox mode.
251
+
164
252
  The picker label is a **host-supplied product string**, not a per-locale dictionary entry: DSH
165
- 0.1.2 renders a plugin tier's `name:` verbatim on both permission surfaces (the General-settings
253
+ renders a plugin tier's `name:` verbatim on both permission surfaces (the General-settings
166
254
  default row and the composer picker) and only supplies its own localized labels for the three
167
- built-in values, so `cordis.patch.yml` ships the Chinese label for every session. The tier draws
168
- **no icon** — the composer renders glyphs only for the built-in values.
255
+ built-in values, so `cordis.patch.yml` ships the Chinese label for every session.
256
+
257
+ The **icon** is a different story. The composer's glyph map is closed, and its own comment states
258
+ the rule: *host-configured names outside the design set get none.* `permissive` is a built-in
259
+ value, so 自动审查 already has a shield+eye glyph; `permissive-full` gets the same glyph only
260
+ because `npx dsh-perm-gate-patch-glyph` adds it to that map. That patch edits a **host**
261
+ package, so it is lost on every DSH upgrade — see
262
+ [After a DSH upgrade](./INSTALL.md#after-a-dsh-upgrade-re-apply-the-composer-glyph-patch).
169
263
 
170
264
  In `cordis.yml`:
171
265
 
@@ -265,8 +359,9 @@ channel existed), with `tools/result` settling the same ask as a fallback when t
265
359
  correlate it. Approvals report the post-approval learning progress (`n`/threshold), and the notice
266
360
  strip labels all three terminal states.
267
361
 
268
- The tier draws no icon in the permission picker: the composer's glyphs are keyed to the three
269
- built-in values, so a plugin-contributed tier is text-only on every surface.
362
+ 自动审查 and 自动审查(高权限) both draw the shield+eye glyph in the picker — the first from DSH's
363
+ built-in map, the second from the host patch the installation guide describes. Without that patch
364
+ the second tier is text-only on every surface; its label and its gating are unaffected.
270
365
 
271
366
  ### A selectable session tier
272
367
 
@@ -278,16 +373,17 @@ built-ins (`read-only` / `workspace-write` / `danger-full-access`, from
278
373
  session permission picker offers 自动审查 as an independent selectable approval tier, not a
279
374
  generic "auto-approval" mode.
280
375
 
281
- The gate is active **only in the tiers listed in `gatePresets`** (default `['permissive']`, the
282
- tier this plugin adds). In every other tier — Read Only, Workspace Write, Full access,
283
- `custom` — the gate's decision flow does not run at all: no allow, no ask, no deny, no P0
284
- hard-deny, no deny-keyword veto, and no audit event. The selected tier's own policy governs the
285
- call, which is the point: `danger-full-access` is defined as "full access without approval
286
- prompts", so overruling it with an ask (unanswerable there — the approval seam rejects before any
287
- answerer runs, producing `the user rejected tool "..."` with no panel) or with a hard-deny would
288
- silently contradict the tier the user chose. `gatePresets: ['*']` makes the gate global again
289
- (hard-deny included); inside an active tier an `ask` is still degraded to passthrough when the
290
- session's effective approval policy is `never`.
376
+ The gate is active **only in the tiers listed in `gatePresets`** (default
377
+ `['permissive', 'permissive-full']`, the two tiers this plugin adds). In every other tier —
378
+ Read Only, Workspace Write, Full access, `custom` — the gate's decision flow does not run at all:
379
+ no allow, no ask, no deny, no P0 hard-deny, no deny-keyword veto, and no audit event. The
380
+ selected tier's own policy governs the call, which is the point: the built-in
381
+ `danger-full-access` is defined as "full access without approval prompts", so overruling it with
382
+ an ask (unanswerable there — the approval seam rejects before any answerer runs, producing
383
+ `the user rejected tool "..."` with no panel) or with a hard-deny would silently contradict the
384
+ tier the user chose. `gatePresets: ['*']` makes the gate global again (hard-deny included);
385
+ inside an active tier an `ask` is still degraded to passthrough when the session's effective
386
+ approval policy is `never` — which is why both 自动审查 tiers declare `approval: ask`.
291
387
 
292
388
  ### Configurable in the UI
293
389
 
package/README.zh.md CHANGED
@@ -24,8 +24,8 @@
24
24
  >
25
25
  > | DSH 版本 | 分支 | 版本号 | npm 标签 |
26
26
  > | --- | --- | --- | --- |
27
- > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` / `@dsh-0.1.1` |
28
- > | 0.1.2-alpha.1+(含 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@2.x` / `@dsh-0.1.2` |
27
+ > | 0.1.0-rc.7 ~ 0.1.1-rc.x | `legacy` | `1.x` | `@legacy` |
28
+ > | 0.1.2-alpha.1+(含 0.1.5-rc.2) | `main` | `2.x` | `@latest` / `@dsh-0.1.2` (`@2.x` 是范围) |
29
29
  >
30
30
  > 版本序列号跟的是 **DSH 线**(`1.x` = DSH ≤ 0.1.1,`2.x` = DSH 0.1.2+),两条大版本互
31
31
  > 相隔离:锁在 `^1.x` 的安装绝不会解析到 `2.x`,反之亦然。`engines.dsh` 表达同样的
@@ -39,7 +39,7 @@
39
39
  > 线上都是 user-approval 服务的**私有**方法,因此通过 `typeof` 探测读取,缺失
40
40
  > 或抛错时降级为「策略未知」。
41
41
 
42
- 版本 **2.0.0** —— 变更见 [Changelog](./CHANGELOG.md)。
42
+ 版本 **2.1.2** —— 变更见 [Changelog](./CHANGELOG.md)。
43
43
 
44
44
  一个**单一自足、确定性优先、fail-closed** 的 DeepSeek Harness 权限门插件。
45
45
 
@@ -62,7 +62,7 @@
62
62
  - **会话放行** — 精确的 `(工具, 规范化 fingerprint)` grant,带 `TTL` + `maxUses`;换目标绝不复用。子代理继承但不可自授。
63
63
  - **纯函数规则引擎** — glob/regex 编译 + ReDoS 上限、坏规则 loud fail、按源内容哈希缓存。
64
64
  - **审计** — 每次决策写为 `{ignorable:true}` 事件并带 `callId`;模型可见理由与记录一致。
65
- - **自动审查档位**(机器值 `permissive`)——一个**独立审批模式**(区别于只读、完全权限与白名单档),既不是"自动审批",也不授予泛化权限。前端只暴露**一个开关**(`permissive`),后台四个审批策略**可组合**、由插件设置决定——仍对 P0 保持 fail-closed。权限下拉框与设置行都按产品名「自动审查」显示,且不带图标。
65
+ - **自动审查档位**(机器值 `permissive`)——一个**独立审批模式**(区别于只读、完全权限与白名单档),既不是"自动审批",也不授予泛化权限。前端只暴露**一个开关**(`permissive`),后台四个审批策略**可组合**、由插件设置决定——仍对 P0 保持 fail-closed。权限下拉框与设置行都按产品名「自动审查」显示;图标见下文(内置档自带,插件档需补丁)。
66
66
  - **沙箱提权自动答复**(`trustEscalation`)— 沙箱提权是从 shell / pwsh / edit 工具**体内部**(`tools/pre-execute` 之后)发出的,所以门禁从未见过它,一个它自动放行的调用仍会弹出确认。开启后,门禁以 `callId` 精确匹配已放行调用并直接答复。
67
67
 
68
68
  ## 安装
@@ -87,21 +87,96 @@ dsh plugin --profile web add dsh-perm-gate
87
87
  rulesFile: ./permissions.yaml # 可选;默认 $DSH_HOME/perm-gate/rules.yml
88
88
  dshHome: $DSH_HOME
89
89
  defaultAction: ask
90
- gatePresets: [permissive] # 门禁生效的档位(默认值)
90
+ gatePresets: [permissive, permissive-full] # 门禁生效的档位(默认值)
91
+ sessionSweep: true # 每小时清理已归档/已删除会话的门禁数据
91
92
  ```
92
93
 
94
+ ### 会话清扫(session sweep)
95
+
96
+ 插件启动时及每小时读取 DSH 的工作区存储(`$DSH_HOME/storages/workspace.json`,只读),
97
+ 对门禁持有授权链数据的每个会话做归类。已被 DSH 归档(`global.archivedSessionIds`)
98
+ 或彻底不存在的会话,其决策事件会从 `$DSH_HOME/perm-gate/events.jsonl` 中移除,
99
+ 其决策前文件快照会从 `$DSH_HOME/perm-gate/snapshots/` 中删除——宿主已视为消失的数据,
100
+ 审查页也不再保留其历史。活跃会话不受影响;无法归属的行(空 sessionId)永不删除;
101
+ 任何失败都 fail-open:本轮跳过,一小时后重试。设 `sessionSweep: false` 关闭;
102
+ `workspaceStoreFile` 可覆盖存储路径。恢复归档会话不会找回已被清扫的历史。
103
+
93
104
  规则示例:见 [examples/permissions.example.yaml](./examples/permissions.example.yaml)。
94
105
 
106
+ ### 网络策略(可选开启)
107
+
108
+ 本地 HTTP/CONNECT 代理,用**同一份规则文件**审查 **shell 子进程**的出站流量,并对无规则
109
+ 覆盖的目标提供审批通道。**默认关闭** —— 开启后会绑定回环端口并改写子进程的代理环境变量,
110
+ 因此绝不隐式启用。
111
+
112
+ ```yaml
113
+ - id: dsh-perm-gate
114
+ config:
115
+ networkEnabled: false # 总开关(默认 false)
116
+ networkMode: whitelist # deny-all | whitelist | allow-all
117
+ networkUnlisted: ask # ask | deny —— 未列出目标的处理方式
118
+ networkUnattributed: allow # allow | deny —— 无 shell 归属的流量
119
+ networkInjectEnv: true # 为子进程改写 HTTP(S)_PROXY / ALL_PROXY
120
+ networkAskTimeoutMs: 120000 # 审批等待上限,超时按拒绝处理
121
+ networkGrantTtlMs: 1800000 # 一次批准的会话有效期
122
+ ```
123
+
124
+ **分层行为**:没有 allow 规则,任何目标都出不去。未列出的目标会升级到交互审批,挂在该
125
+ shell 命令的会话上;批准后该目标在本次会话内放行。**`deny` 规则永不升级为审批** —— 审批
126
+ 只能为「无规则禁止的目标」拓宽可达性,永远不能推翻一条说「不」的规则。
127
+
128
+ **边界 —— 依赖它之前请先读这段**:代理是**协作式**策略层,不是强制边界。它只能看到
129
+ **愿意读代理环境变量**的客户端的流量。
130
+
131
+ | 客户端 | 能拦吗 |
132
+ |--------|--------|
133
+ | `curl`、`wget`、`git`、Go `net/http`、Python `requests` | ✅ |
134
+ | **Node.js `http` / `https` / `fetch`** | ❌ **直连,代理看不到** |
135
+ | Java(未加 `-D` 代理参数)、.NET `HttpClient` | ❌ |
136
+ | 原始 socket、自写 TCP | ❌ |
137
+ | DNS、QUIC/HTTP3、非 HTTP 协议 | ❌ |
138
+ | 连接字面 IP | ❌ |
139
+
140
+ 因此 `node -e "require('http').get('http://host/')"` 这类命令**不会被拦截**。请把它当作
141
+ 「防误操作的护栏 + 声明意图的地方」,而不是密闭沙箱。
142
+
143
+ DSH **自身**的网络流量 —— 内建网络工具与 LLM 传输 —— 刻意不管:这些连接不带 shell 归属,
144
+ 而 `networkUnattributed: allow`(默认)会直接放行。审查它们会导致宿主**把自己拦死**,
145
+ 那比漏拦严重得多。只有在你确定宿主的客户端不读代理环境变量时,才考虑改成 `deny`。
146
+
147
+ 实时状态查询:`GET /api/dsh-perm-gate/network`(模式 / 绑定 / 端口 / 代理存活 / 环境注入
148
+ 状态 / 阻断计数 / 最近阻断)。
149
+
95
150
  ## 自动审查档位(机器值 `permissive`)
96
151
 
97
152
  自动审查是权限下拉框里一个**独立审批档**,与只读 / 工作区内修改 / 完全权限 / 白名单平行。
98
153
  它不是泛化的"自动审批"、也不授予泛化权限:只会在人类/LLM 接缝**之前**收窄或放宽决策,
99
154
  P0 硬拒绝始终单调且不可协商。
100
155
 
101
- 下拉框里的名字是**宿主提供的产品名**,不是逐语言的字典项:DSH 0.1.2 对插件档位在**两个**权限界面上
156
+ **提供两个变体** —— 因为预设的 `sandbox` 与 `approval` 是两根**独立**旋钮,把它们绑死会逼出
157
+ 一个糟糕的取舍:
158
+
159
+ | 下拉框名称 | 机器值 | sandbox | approval |
160
+ |-----------|--------|---------|----------|
161
+ | 自动审查 | `permissive` | `workspace-write` | `ask` |
162
+ | 自动审查(高权限) | `permissive-full` | `danger-full-access` | `ask` |
163
+
164
+ 普通档保留内置文件沙箱。而那个沙箱**同时**拒绝子进程启动所需的命名管道 —— 所以 `git clone`、
165
+ MSYS2/Cygwin 的 `sh.exe`、ConPTY 都会以 `Win32 error 5` / `couldn't create signal pipe` 失败。
166
+ 又因为门禁**只在 `gatePresets` 列出的档位里生效**,想用门禁就必须接受这个限制。
167
+ 「自动审查(高权限)」解开了这个耦合:**审批行为完全相同,但不限制文件沙箱** —— 档位自带的描述已把代价
168
+ 写明:流程更顺畅、审批仍逐次生效,但**不再有系统沙箱兜底**。两者都在默认
169
+ `gatePresets` 里,任选其一都能获得完整的 P0–P4 链路 —— 门禁只读预设的**名字**,从不读 sandbox 模式。
170
+
171
+ 下拉框里的名字是**宿主提供的产品名**,不是逐语言的字典项:DSH 对插件档位在**两个**权限界面上
102
172
  (通用设置默认档行、输入栏权限选择器)都原样渲染补丁里的 `name:`,只给三个内置档提供自己的本地化
103
- 标签,因此 `cordis.patch.yml` 直接写中文名,对所有会话一致。该档位**不画图标**——选择器的图标只按
104
- 三个内置值取。
173
+ 标签,因此 `cordis.patch.yml` 直接写中文名,对所有会话一致。
174
+
175
+ **图标是另一回事。** 输入栏的图标表是闭合的,表自己的注释写明了规则:*host-configured names
176
+ outside the design set get none*。`permissive` 是内置值,所以「自动审查」本来就有盾+眼图标;
177
+ 「自动审查(高权限)」能拿到同一个图标,靠的是 `npx dsh-perm-gate-patch-glyph` 往那张表里
178
+ 加了一项。该补丁改的是**宿主**包,每次 DSH 升级都会丢 —— 见
179
+ [DSH 升级后:重打输入区图标补丁](./INSTALL.zh.md)。
105
180
 
106
181
  `cordis.yml`:
107
182
 
@@ -156,7 +231,8 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
156
231
  `@deepseek-ai/dsh-base/cordis.patch.yml`);`test/patch-presets.spec.ts` 固定了这份键集合。因此会话权限
157
232
  下拉里会出现「自动审查」这个**独立可选审批档**,而不是"auto-approval"档。
158
233
 
159
- 门禁**只在 `gatePresets` 列出的档位里生效**(默认 `['permissive']`,即本插件新增的那一档)。在其余任何档位
234
+ 门禁**只在 `gatePresets` 列出的档位里生效**(默认 `['permissive', 'permissive-full']`,即本插件新增的
235
+ 两个档位)。在其余任何档位
160
236
  (Read Only、Workspace Write、Full access、`custom`)里,门禁的判定流程**完全不运行**:不放行、不弹审批、
161
237
  不拒绝、不执行 P0 硬拒绝、不做黑名单关键词拦截,也不写审计事件——该档位自己的策略说了算。这正是重点所在:
162
238
  `danger-full-access` 的定义就是"全权限、不弹审批",用 ask 去覆盖它毫无意义(该档 `approval: never` 会让审批接缝
@@ -191,7 +267,9 @@ LLM 评定为 `safe` 且门禁自动放行的调用因此仍会弹出确认。
191
267
  调用文本命中任一关键词(大小写不敏感子串)即直接拒绝,且先于白名单 / 授权 / LLM。黑名单在设置
192
268
  卡片中按列表查看与增删(预置条目带标签,可一键恢复预置);未设置或为空时应用预置列表——黑名单
193
269
  不会静默关闭。
194
- 该档位在权限选择器中**不显示图标**:选择器的图标只按三个内置值取,插件新增的档位是纯文字。
270
+ 「自动审查」与「自动审查(高权限)」在选择器里都画盾+眼图标 —— 前者来自 DSH 内置表,后者来自
271
+ 安装指南里描述的那次宿主补丁。没有该补丁时,第二个档位在所有界面上都是纯文字;它的标签与门禁
272
+ 不受影响。
195
273
 
196
274
 
197
275
  ## CLI(独立 dry-run)
package/cordis.patch.yml CHANGED
@@ -14,8 +14,17 @@
14
14
  # stays `permissive`: it is the key `gatePresets` matches and the value the
15
15
  # permission projection reports, so only the label text is user-facing.
16
16
  #
17
- # The tier carries no icon. The built-in glyphs the composer draws are keyed by
18
- # the three built-in values, and a plugin tier deliberately has none.
17
+ # The composer's permission picker draws an icon per preset from a CLOSED map
18
+ # inside @deepseek-ai/dsh-client-ui-conversation (`permissionGlyphs`), whose
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.
19
28
  #
20
29
  # The DSH bundle patch REPLACES the whole `permission.config.presets` map (it
21
30
  # does not merge per key), so this file MUST restate every tier that should
@@ -31,6 +40,24 @@
31
40
  # lost its implementation with that plugin, and this gate is inactive in it
32
41
  # (`gatePresets` defaults to `['permissive']`) — so it only offered a duplicate
33
42
  # tier that looked protected while nothing was.
43
+ #
44
+ # TWO tiers ship, because `sandbox` and `approval` are INDEPENDENT knobs and
45
+ # coupling them forced a bad trade:
46
+ #
47
+ # permissive sandbox: workspace-write approval: ask
48
+ # permissive-full sandbox: danger-full-access approval: ask
49
+ #
50
+ # `permissive` keeps the built-in file sandbox. That sandbox also denies the
51
+ # named pipes a child process needs to start — `git clone`, MSYS2/Cygwin
52
+ # `sh.exe` and ConPTY all fail with `Win32 error 5` under it. Because this gate
53
+ # is only active in the tiers listed in `gatePresets`, a user who wanted the
54
+ # gate had to accept that restriction to get it. `permissive-full` removes the
55
+ # coupling: same approval behaviour, no file-sandbox restriction.
56
+ #
57
+ # The gate itself only reads the preset NAME (see `src/preset.ts`), never the
58
+ # sandbox mode, so both tiers get the full P0–P4 chain. `approval: ask` is
59
+ # required on both — under `approval: never` the DSH seam rejects every ask
60
+ # before any answerer runs, which would degrade this gate's asks to passthrough.
34
61
  - id: permission
35
62
  config:
36
63
  presets:
@@ -45,6 +72,11 @@
45
72
  approval: ask
46
73
  name: 自动审查
47
74
  description: 独立审批档 —— 作用域内安全操作自动放行,危险/未知越界转人工确认(审批强度可在「设置 → 插件 → 自动审查」调整)。
75
+ permissive-full:
76
+ sandbox: danger-full-access
77
+ approval: ask
78
+ name: 自动审查(高权限)
79
+ description: 只由本插件的审批门过滤 —— 文件修改与子进程网络访问不再受沙箱限制,审批仍逐次生效,但没有系统沙箱兜底。
48
80
  danger-full-access:
49
81
  sandbox: danger-full-access
50
82
  approval: never
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "dsh-perm-gate",
3
+ "name": "dsh-perm-gate",
4
+ "version": "2.1.2",
5
+ "description": {
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
+ "zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
8
+ },
9
+ "homepage": "https://github.com/drscrewdriver/dsh-perm-gate",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/drscrewdriver/dsh-perm-gate.git"
13
+ },
14
+ "license": "MIT",
15
+ "engines": {
16
+ "dsh": ">=0.1.2-alpha.1"
17
+ },
18
+ "components": {
19
+ "host": "lib/index.js",
20
+ "client": "lib/client.js"
21
+ }
22
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Agent identity extraction for the `agents` rule dimension.
3
+ *
4
+ * Given a tool execution context, produces zero or more identity candidates
5
+ * that the `agents` dimension can match against. Candidates are produced
6
+ * in priority order: most-specific first.
7
+ *
8
+ * Identity model:
9
+ * - `main` — the primary (top-level) agent session
10
+ * - `subagent` — a delegated child agent (parentAuthorized present)
11
+ * - `preset:<name>` — running under a named permission preset
12
+ *
13
+ * When no identity can be determined, an empty array is returned and the
14
+ * `agents` dimension in rules fail-closes (no match → rule skipped).
15
+ */
16
+ /**
17
+ * The subset of ToolExecutionLike we need. Defined as a permissive interface
18
+ * to avoid a circular import with runtime.ts. Uses `unknown` for event types
19
+ * since we only need to read `.type`/`.kind` and `.preset`/`.tier` properties.
20
+ */
21
+ export interface ExecutionLike {
22
+ readonly parentAuthorized?: boolean;
23
+ readonly agent?: {
24
+ readonly sessionId?: string;
25
+ readonly session?: {
26
+ readonly id?: string;
27
+ readonly events?: readonly unknown[];
28
+ readonly snapshotEvents?: () => readonly unknown[];
29
+ readonly ownEvents?: () => readonly unknown[];
30
+ };
31
+ };
32
+ }
33
+ /**
34
+ * Extract agent identity candidates from a tool execution context.
35
+ *
36
+ * Returns a deduplicated list of identity strings. The `agents` rule
37
+ * dimension matches if ANY candidate matches ANY entry in the rule's
38
+ * `agents` list.
39
+ *
40
+ * @returns candidates in priority order, e.g. `['preset:permissive', 'subagent']`
41
+ * or `['main']`. Empty array = undecidable → agents rules fail-close.
42
+ */
43
+ export declare function extractAgentCandidates(exec: ExecutionLike): string[];