dsh-perm-gate 2.1.2 → 3.0.1
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 +210 -0
- package/INSTALL.ja.md +14 -8
- package/INSTALL.ko.md +15 -9
- package/INSTALL.md +16 -8
- package/INSTALL.zh.md +12 -7
- package/README.ja.md +19 -6
- package/README.ko.md +20 -7
- package/README.md +35 -4
- package/README.zh.md +13 -4
- package/docs/baseline-guard.md +56 -0
- package/docs/plan-rules-to-settings.md +217 -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 +484 -6
- 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 +56 -2
- package/lib/events.js +92 -0
- package/lib/index.js +106 -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 +19 -2
|
@@ -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.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# 规则文件格式规范(`permissions` YAML)
|
|
2
|
+
|
|
3
|
+
> **权威性**:本文件的维度清单与 `src/rule.ts:181` 的 `VALID_KEYS` **逐字一致**。
|
|
4
|
+
> 两者任一变更必须同步;`test/rule.spec.ts` 与 `test/rule-dims.spec.ts` 是运行时护栏。
|
|
5
|
+
> 适用版本:`dsh-perm-gate` **2.4.1**(DSH ≥ 0.1.2-alpha.1)。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 文件位置与加载
|
|
10
|
+
|
|
11
|
+
| 项 | 说明 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 默认路径 | `<dataDir>/rules.yml`(`dataDir` 由 `config.resolveDataDir` 解析,默认 `$DSH_HOME/perm-gate/`) |
|
|
14
|
+
| 显式指定 | `config.rulesFile` |
|
|
15
|
+
| 规则链 | 支持多文件链(`rulesChain` / `searchUp` / `fallbackPath` / `badFilePolicy` / `maxRules`),见 `src/rule-chain.ts` |
|
|
16
|
+
| 热重载 | 由 `src/watch.ts`(chokidar)监听生效规则文件,防抖后链级重编译 |
|
|
17
|
+
| 解析失败 | **fail loud**——规则非法直接报错,绝不静默停用门控 |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. 顶层结构
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
defaultAction: ask # 可选;无任何规则命中时的动作。缺省 = ask
|
|
25
|
+
deny: # 动作分区(数组)
|
|
26
|
+
- <规则条目>
|
|
27
|
+
allow:
|
|
28
|
+
- <规则条目>
|
|
29
|
+
ask:
|
|
30
|
+
- <规则条目>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**动作即分区**:条目本身不带 action 字段(写了必须与所在分区一致)。求值顺序固定为
|
|
34
|
+
**deny → allow → ask → defaultAction**,首次匹配胜出(deny wins over allow)。
|
|
35
|
+
黑名单关键词层排在 deny 之前。
|
|
36
|
+
|
|
37
|
+
`defaultAction` 取值:`allow` | `ask` | `deny`(缺省 `ask`)。
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. 规则条目可用字段(`VALID_KEYS`)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
tools · command · args · paths · params · absent · agents · when · argv · network · branch
|
|
45
|
+
action · reason · enabled
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| 字段 | 类型 | 作用 |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `tools` | 字符串 / 字符串数组 | 工具名 glob |
|
|
51
|
+
| `command` | 字符串 / 字符串数组 | 命令词,支持 `word#recursive` / `word#force` 后缀 |
|
|
52
|
+
| `args` | 字符串 / 字符串数组 | 参数 token 扫描 |
|
|
53
|
+
| `paths` | 字符串 / 字符串数组 | 工作区相对路径 glob |
|
|
54
|
+
| `params` | 映射 | 参数键 → 值 glob 列表 |
|
|
55
|
+
| `absent` | 字符串 / 字符串数组 | 必须**不存在**的参数键 |
|
|
56
|
+
| `agents` | 字符串 / 字符串数组 | `main` / `subagent` / `preset:<name>` |
|
|
57
|
+
| `when` | 映射 | 环境与平台条件 |
|
|
58
|
+
| `argv` | 映射 | 额外 argv 模式(`pipeline`) |
|
|
59
|
+
| `network` | 映射 | 域名 / IP / 端口 / scheme |
|
|
60
|
+
| `branch` | 映射 | git 分支 / 远程 / 保护分支(仅对 git 命令生效) |
|
|
61
|
+
| `action` | `allow`\|`ask`\|`deny` | 可选;写了必须与所在分区一致 |
|
|
62
|
+
| `reason` | 字符串 | 人类可读理由(进入事件与 UI) |
|
|
63
|
+
| `enabled` | 布尔 | 设 `false` 则跳过该条目 |
|
|
64
|
+
|
|
65
|
+
**跨维度逻辑 = AND**:一条规则内出现的**多个维度必须全部满足**;
|
|
66
|
+
**维度内 = OR**:某个维度的列表里任一模式命中即可(`params`/`when`/`network` 的子键为 AND,见各节)。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 4. 逐维语义
|
|
71
|
+
|
|
72
|
+
### 4.1 `tools` — 工具名
|
|
73
|
+
```yaml
|
|
74
|
+
- tools: [shell, pwsh, bash]
|
|
75
|
+
```
|
|
76
|
+
Glob 匹配工具名。列表内 OR。
|
|
77
|
+
|
|
78
|
+
### 4.2 `command` — 命令词
|
|
79
|
+
```yaml
|
|
80
|
+
- command: [rm#recursive, rm#force]
|
|
81
|
+
```
|
|
82
|
+
命令词来自 argv 分解(`src/shell.ts`)。后缀 `#recursive` / `#force` 分别要求递归 / 强制语义。
|
|
83
|
+
列表内 OR。
|
|
84
|
+
|
|
85
|
+
### 4.3 `args` — 参数 token
|
|
86
|
+
```yaml
|
|
87
|
+
- args: ["--force", "-rf"]
|
|
88
|
+
```
|
|
89
|
+
对分解后的 token 集合做扫描。**⚠ 语义为 OR**:任意一个 token 匹配任意一个模式即命中。
|
|
90
|
+
它**无法**要求「`push` 与 `--force` 同时存在」,也无法区分
|
|
91
|
+
`git push --force origin main` 与 `git checkout --force main`——需要这种区分时用 `params` 或(未来的)`branch` 维度。
|
|
92
|
+
|
|
93
|
+
### 4.4 `paths` — 路径
|
|
94
|
+
```yaml
|
|
95
|
+
- paths: ["src/**", "!src/vendor/**"]
|
|
96
|
+
```
|
|
97
|
+
工作区相对路径 glob;`!` 前缀取反。列表内 OR。
|
|
98
|
+
|
|
99
|
+
### 4.5 `params` — 键定向匹配(**AND over keys**)
|
|
100
|
+
```yaml
|
|
101
|
+
- params:
|
|
102
|
+
command: ["*--force*", "!*--dry-run*"]
|
|
103
|
+
flags.mode: ["production"]
|
|
104
|
+
```
|
|
105
|
+
- 每个键的**实际值**必须匹配该键模式列表中的**至少一个**(列表内 OR)
|
|
106
|
+
- **所有列出的键都必须满足**(键之间 AND)
|
|
107
|
+
- 单个 `!` 前缀模式 = 取反(值必须**不**匹配其余部分)
|
|
108
|
+
- 空模式列表 = 该键只需存在(任意值)
|
|
109
|
+
|
|
110
|
+
这是表达复合条件的主要手段(`args` 做不到的 AND 需求在这里做)。
|
|
111
|
+
|
|
112
|
+
### 4.6 `absent` — 键必须缺失(AND)
|
|
113
|
+
```yaml
|
|
114
|
+
- absent: [dry_run, force]
|
|
115
|
+
```
|
|
116
|
+
所列参数键必须**全部不存在**。
|
|
117
|
+
|
|
118
|
+
### 4.7 `agents` — 代理身份
|
|
119
|
+
```yaml
|
|
120
|
+
- agents: ["main", "preset:reviewer"]
|
|
121
|
+
```
|
|
122
|
+
合法取值:`main`、`subagent`、`preset:<name>`(大小写不敏感)。非法值解析期报错。
|
|
123
|
+
候选集由 `src/agent-identity.ts` 从会话 header 提取。列表内 OR。
|
|
124
|
+
|
|
125
|
+
### 4.8 `when` — 环境与平台(AND)
|
|
126
|
+
```yaml
|
|
127
|
+
- when:
|
|
128
|
+
env:
|
|
129
|
+
NODE_ENV: ["production"]
|
|
130
|
+
CI: ["true", "1"]
|
|
131
|
+
platform: [linux, win32]
|
|
132
|
+
```
|
|
133
|
+
- `env`:键 → 允许值列表(**列表内 OR**,**键之间 AND**)
|
|
134
|
+
- `platform`:Node.js `process.platform` 取值
|
|
135
|
+
- `nodeVersion`:保留字段(尚未参与判定)
|
|
136
|
+
|
|
137
|
+
### 4.9 `argv` — 额外 argv 模式
|
|
138
|
+
```yaml
|
|
139
|
+
- argv:
|
|
140
|
+
pipeline: ["curl*|sh", "wget*|bash"]
|
|
141
|
+
```
|
|
142
|
+
`pipeline` 匹配**整条管道字符串**:各简单命令的**完整 argv**(命令词 + 参数 + 重定向目标)以 `|` 连接,
|
|
143
|
+
例如 `curl https://x.sh | sh` 的匹配串是 `curl https://x.sh|sh`。
|
|
144
|
+
|
|
145
|
+
**⚠ 模式里的 `|` 是字面量**,所以 `curl|sh` 只匹配参数恰好为空的紧邻形式;要覆盖带参数的常见写法必须写
|
|
146
|
+
`curl*|sh`。(历史缺陷:匹配串曾只取命令词,`curl|sh` 因此对上的是无害的 `curl|sh`,而真正危险的
|
|
147
|
+
`curl https://x.sh | sh` 反而匹配不到。)
|
|
148
|
+
|
|
149
|
+
### 4.10 `network` — 网络目标(子维度 AND)
|
|
150
|
+
```yaml
|
|
151
|
+
- network:
|
|
152
|
+
domains: ["*.internal.corp", "github.com"]
|
|
153
|
+
ips: ["10.0.0.0/8", "192.168.1.1"]
|
|
154
|
+
ports: ["443", "8000-9000"]
|
|
155
|
+
schemes: [https, http]
|
|
156
|
+
```
|
|
157
|
+
- 出现的**每个子维度都必须匹配**(子维度之间 AND);每个子维度内部 OR
|
|
158
|
+
- `domains` 支持 glob;`ips` 支持 CIDR;`ports` 支持区间;`schemes` 为协议名
|
|
159
|
+
|
|
160
|
+
网络维度有两个生效面:`tools/pre-execute` 的静态 URL 候选检查,以及回环代理拦截真实出站流量
|
|
161
|
+
(`network.enabled` **默认 false**,opt-in)。
|
|
162
|
+
|
|
163
|
+
### 4.11 `branch` — git 分支 / 远程 / 保护分支(子维度 AND)
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
- command: [git]
|
|
167
|
+
args: [push]
|
|
168
|
+
branch:
|
|
169
|
+
target: [main, master, "release*"]
|
|
170
|
+
remote: [origin]
|
|
171
|
+
shared: true
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
| 子字段 | 语义 |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `target` | 分支名 glob(`*` **跨** `/`,所以 `release*` 能匹配 `release/1.0`) |
|
|
177
|
+
| `remote` | 远程名 glob |
|
|
178
|
+
| `shared` | 要求目标分支是**保护分支** |
|
|
179
|
+
|
|
180
|
+
**为什么需要这个维度**:`args` 是 OR 语义(§4.3),它能问「`--force` 出现了吗」,但永远问不出
|
|
181
|
+
「`--force` 是否用在**推送到保护分支**上」。决定性用例是区分两条 token 完全相同的命令:
|
|
182
|
+
|
|
183
|
+
- `git push --force origin main` → 危险,应命中
|
|
184
|
+
- `git checkout --force main` → 日常操作,**不**应命中
|
|
185
|
+
|
|
186
|
+
候选来自统一的命令分类器(`src/command-dispatcher.ts` + `src/parsers/git.ts`),因此
|
|
187
|
+
refspec 形式(`HEAD:main`)已被拆解、远程名不会被误当成分支名、标志位也已归一。
|
|
188
|
+
|
|
189
|
+
**三条必须知道的边界**:
|
|
190
|
+
|
|
191
|
+
1. **只对 git 命令生效**。非 git 命令(`rm -rf`、`npm install`…)不满足任何 `branch` 子维度,
|
|
192
|
+
因此带 `branch` 的规则对它们**不匹配**(不会抛错)——这类命令由写路径层与黑名单层负责。
|
|
193
|
+
2. **`shared` 是静态判定**,来自解析器的保护分支清单
|
|
194
|
+
(`src/parsers/git.ts` 的 `PROTECTED_BRANCHES`:`main` / `master` / `production` / `release` / `stable`)。
|
|
195
|
+
它**不会**执行 `git branch -r --contains` 之类查询——那会把子进程放上门控决策路径。
|
|
196
|
+
注意 `shared` 与 `target` 是**并列且各自独立**的子维度:写了 `target: ["release*"]` 时
|
|
197
|
+
`release/1.0` 满足 `target`,但不满足 `shared`(保护清单是精确匹配)。
|
|
198
|
+
3. **子维度之间 AND,各自内部 OR**;多个简单命令(管道/`&&`)任一满足即可。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 5. 完整示例
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
defaultAction: ask
|
|
206
|
+
|
|
207
|
+
deny:
|
|
208
|
+
# 凭据材料外泄(P0 层另有硬拒绝兜底)
|
|
209
|
+
- tools: [shell, pwsh]
|
|
210
|
+
params:
|
|
211
|
+
command: ["*AWS_SECRET*", "*PRIVATE KEY*"]
|
|
212
|
+
reason: "credential material in command"
|
|
213
|
+
|
|
214
|
+
# 管道下载即执行
|
|
215
|
+
- argv:
|
|
216
|
+
pipeline: ["curl*|sh", "wget*|bash"]
|
|
217
|
+
reason: "piped download-and-execute"
|
|
218
|
+
|
|
219
|
+
# 推送到保护分支(args 做不到的区分:同一批 token 的 checkout 不命中)
|
|
220
|
+
- command: [git]
|
|
221
|
+
args: [push]
|
|
222
|
+
branch:
|
|
223
|
+
target: [main, master, "release*"]
|
|
224
|
+
shared: true
|
|
225
|
+
reason: "push to a protected branch"
|
|
226
|
+
|
|
227
|
+
# 生产环境下的强制删除
|
|
228
|
+
- command: [rm#recursive, rm#force]
|
|
229
|
+
when:
|
|
230
|
+
env:
|
|
231
|
+
NODE_ENV: ["production"]
|
|
232
|
+
reason: "recursive force delete in production"
|
|
233
|
+
|
|
234
|
+
allow:
|
|
235
|
+
# 常见只读查询
|
|
236
|
+
- tools: [read, grep, glob]
|
|
237
|
+
reason: "read-only workspace queries"
|
|
238
|
+
|
|
239
|
+
# 常见开发域名
|
|
240
|
+
- network:
|
|
241
|
+
domains: [github.com, "*.githubusercontent.com"]
|
|
242
|
+
schemes: [https]
|
|
243
|
+
reason: "common dev hosts"
|
|
244
|
+
|
|
245
|
+
ask:
|
|
246
|
+
# 推送需确认(注意:args 是 OR,这里只要求出现 push)
|
|
247
|
+
- command: [git]
|
|
248
|
+
args: ["push"]
|
|
249
|
+
reason: "git push requires confirmation"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
同一份示例也随包分发:`examples/permissions.example.yaml`。
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 6. 与 CLI dry-run 配合
|
|
257
|
+
|
|
258
|
+
改完规则**先干跑再上线**:
|
|
259
|
+
|
|
260
|
+
```powershell
|
|
261
|
+
# 列出已编译规则(默认 ask 分区、规则条数、permissive 策略快照)
|
|
262
|
+
dsh-perm-gate --rules permissions.yaml --list
|
|
263
|
+
|
|
264
|
+
# 判定一次调用(--args 是 JSON;参数较短时直接内联)
|
|
265
|
+
dsh-perm-gate --rules permissions.yaml --tool read --args "{\"file_path\":\"src/index.ts\"}"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
输出为 JSON:`decision`(`allow` / `ask` / `deny`)、`reason`、`audited`。
|
|
269
|
+
|
|
270
|
+
**两个必须知道的坑**:
|
|
271
|
+
|
|
272
|
+
1. **黑名单先于规则命中**。`deny-keyword` 层跑在 `deny:` 分区之前,
|
|
273
|
+
`rm -rf` / `push --force` 这类预设词会在你的规则之前就把调用拦下——
|
|
274
|
+
dry-run 输出里的 `deny-keyword:` 前缀就是它在起作用,不是你的规则。
|
|
275
|
+
2. **带引号的长参数在 PowerShell 下会被二次解析**(可能报 `--args must be valid JSON`)。
|
|
276
|
+
稳妥做法:写进临时文件后用 `--args (Get-Content -Raw args.json)`,
|
|
277
|
+
或直接依赖 `test/rule-dims.spec.ts` 与 `test/evaluate.spec.ts` 的维度覆盖。
|
|
278
|
+
|
|
279
|
+
CLI 与运行时共用同一判定引擎(`evaluate.ts` + `engine.ts`),输出即真实裁决。
|