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.
- package/CHANGELOG.ja.md +108 -0
- package/CHANGELOG.ko.md +104 -0
- package/CHANGELOG.md +236 -0
- package/INSTALL.ja.md +41 -21
- package/INSTALL.ko.md +37 -19
- package/INSTALL.md +46 -23
- package/INSTALL.zh.md +33 -17
- package/README.ja.md +52 -12
- package/README.ko.md +53 -13
- package/README.md +68 -10
- package/README.zh.md +41 -10
- package/assets/allow1.png +0 -0
- package/assets/allow2.png +0 -0
- package/assets/gateoff.png +0 -0
- package/assets/llms.png +0 -0
- package/assets/permlist.png +0 -0
- package/assets/setting-head.png +0 -0
- package/assets/setting-rules.png +0 -0
- package/cordis.patch.yml +11 -8
- 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
- package/scripts/patch-permission-glyph.mjs +225 -42
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# dsh-perm-gate UI 缺口分析与分支级权限可行性
|
|
2
|
+
|
|
3
|
+
> **日期**: 2026-09-16
|
|
4
|
+
> **状态**: 草案
|
|
5
|
+
> **相关讨论**: PerryLink dsh-auto-review 对比分析
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、现状:后端能力完整,前端 UI 缺失
|
|
10
|
+
|
|
11
|
+
### 后端已支持的完整 YAML 规则格式
|
|
12
|
+
|
|
13
|
+
`rules.yml` 支持 10 个维度的分层权限配置:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
permissions:
|
|
17
|
+
deny:
|
|
18
|
+
- tools: [shell, pwsh]
|
|
19
|
+
command: [rm#recursive, rm#force]
|
|
20
|
+
reason: "destructive system command"
|
|
21
|
+
|
|
22
|
+
- args: ["--force"]
|
|
23
|
+
level: deny
|
|
24
|
+
reason: "Force flag detected"
|
|
25
|
+
|
|
26
|
+
allow:
|
|
27
|
+
- command: [grep, glob, read]
|
|
28
|
+
reason: "permissive allowlist"
|
|
29
|
+
|
|
30
|
+
- network:
|
|
31
|
+
domains: [github.com, api.github.com]
|
|
32
|
+
reason: "common dev hosts"
|
|
33
|
+
|
|
34
|
+
ask:
|
|
35
|
+
- command: [git]
|
|
36
|
+
args: ["push"]
|
|
37
|
+
reason: "Git push requires confirmation"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**VALID_KEYS**: `tools`, `command`, `args`, `paths`, `params`, `absent`, `agents`, `when`, `argv`, `network`, `action`, `reason`, `enabled`
|
|
41
|
+
|
|
42
|
+
### 前端 UI 面板实际暴露的内容
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
┌─────────────────────────────────────────────┐
|
|
46
|
+
│ 自动审查 (permissive) 开关 │
|
|
47
|
+
├─────────────────────────────────────────────┤
|
|
48
|
+
│ 策略选项 │
|
|
49
|
+
│ ☑ trustAutoAllow │
|
|
50
|
+
│ ☐ alwaysConfirm │
|
|
51
|
+
│ ☐ llmAssist │
|
|
52
|
+
├─────────────────────────────────────────────┤
|
|
53
|
+
│ 允许列表 (allowlist) │
|
|
54
|
+
│ [简单字符串列表,支持批量 textarea 编辑] │
|
|
55
|
+
│ ⚠️ 只支持前缀匹配,不支持 YAML 分层规则 │
|
|
56
|
+
├─────────────────────────────────────────────┤
|
|
57
|
+
│ 拒绝关键词 (denyKeywords) │
|
|
58
|
+
│ [同样只支持简单字符串列表] │
|
|
59
|
+
└─────────────────────────────────────────────┘
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 缺失的 UI 能力
|
|
63
|
+
|
|
64
|
+
| 功能 | 后端支持 | UI 支持 | 状态 |
|
|
65
|
+
|------|---------|---------|------|
|
|
66
|
+
| YAML 规则编辑器 | ✅ | ❌ | 需开发 |
|
|
67
|
+
| 维度选择器 (tools/command/args/network 等) | ✅ | ❌ | 需开发 |
|
|
68
|
+
| 规则级别选择 (deny/allow/ask) | ✅ | ❌ | 需开发 |
|
|
69
|
+
| 复合条件编辑 (AND/OR 逻辑) | ✅ | ❌ | 需开发 |
|
|
70
|
+
| 规则测试/预览 | ✅ (CLI dry-run) | ❌ | 需开发 |
|
|
71
|
+
| 规则导入/导出 | ❌ | ❌ | 需开发 |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 二、UI 补全计划草案
|
|
76
|
+
|
|
77
|
+
### Phase 1: 最小可行方案 (MVP)
|
|
78
|
+
|
|
79
|
+
**目标**: 在 UI 中暴露完整的 YAML 规则编辑能力
|
|
80
|
+
|
|
81
|
+
#### 1.1 YAML 编辑器集成
|
|
82
|
+
|
|
83
|
+
- 集成 Monaco Editor 或 CodeMirror 支持 YAML 语法高亮
|
|
84
|
+
- 支持 `rules.yml` 文件的实时编辑和保存
|
|
85
|
+
- 添加 YAML 语法校验和错误提示
|
|
86
|
+
- 添加规则格式校验(维度名称、值类型等)
|
|
87
|
+
|
|
88
|
+
#### 1.2 规则模板系统
|
|
89
|
+
|
|
90
|
+
- 提供常用规则模板(git push、文件删除、网络访问等)
|
|
91
|
+
- 支持从模板快速创建规则
|
|
92
|
+
- 模板库可扩展
|
|
93
|
+
|
|
94
|
+
#### 1.3 规则测试功能
|
|
95
|
+
|
|
96
|
+
- 添加"测试规则"按钮
|
|
97
|
+
- 输入命令字符串,预览规则匹配结果
|
|
98
|
+
- 显示哪些维度匹配、哪些不匹配
|
|
99
|
+
- 复用 CLI dry-run 逻辑
|
|
100
|
+
|
|
101
|
+
### Phase 2: 可视化规则编辑器
|
|
102
|
+
|
|
103
|
+
**目标**: 提供图形化的规则创建界面,降低 YAML 编写门槛
|
|
104
|
+
|
|
105
|
+
#### 2.1 维度化规则编辑器
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
┌─────────────────────────────────────────────┐
|
|
109
|
+
│ 新建规则 │
|
|
110
|
+
├─────────────────────────────────────────────┤
|
|
111
|
+
│ 规则级别: [▼ deny] │
|
|
112
|
+
│ 原因: [输入框] │
|
|
113
|
+
├─────────────────────────────────────────────┤
|
|
114
|
+
│ 匹配条件 (AND 逻辑): │
|
|
115
|
+
│ [+ 添加条件] │
|
|
116
|
+
│ │
|
|
117
|
+
│ ┌─────────────────────────────────────┐ │
|
|
118
|
+
│ │ 维度: [▼ command] 模式: [git] │ │
|
|
119
|
+
│ │ 删除 │ │
|
|
120
|
+
│ └─────────────────────────────────────┘ │
|
|
121
|
+
│ │
|
|
122
|
+
│ ┌─────────────────────────────────────┐ │
|
|
123
|
+
│ │ 维度: [▼ args] 模式: [--force] │ │
|
|
124
|
+
│ │ 删除 │ │
|
|
125
|
+
│ └─────────────────────────────────────┘ │
|
|
126
|
+
│ │
|
|
127
|
+
│ ┌─────────────────────────────────────┐ │
|
|
128
|
+
│ │ 维度: [▼ network] 域名: [example.*] │ │
|
|
129
|
+
│ │ 删除 │ │
|
|
130
|
+
│ └─────────────────────────────────────┘ │
|
|
131
|
+
├─────────────────────────────────────────────┤
|
|
132
|
+
│ [预览 YAML] [保存规则] │
|
|
133
|
+
└─────────────────────────────────────────────┘
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### 2.2 规则分组管理
|
|
137
|
+
|
|
138
|
+
- 支持按用途分组(安全规则、工作流规则等)
|
|
139
|
+
- 支持规则启用/禁用开关
|
|
140
|
+
- 支持规则排序(优先级)
|
|
141
|
+
|
|
142
|
+
#### 2.3 规则冲突检测
|
|
143
|
+
|
|
144
|
+
- 检测规则间的冲突(deny vs allow)
|
|
145
|
+
- 提示规则冗余(被更宽泛的规则覆盖)
|
|
146
|
+
- 建议规则优化
|
|
147
|
+
|
|
148
|
+
### Phase 3: 高级功能
|
|
149
|
+
|
|
150
|
+
**目标**: 提供生产级规则管理能力
|
|
151
|
+
|
|
152
|
+
#### 3.1 规则版本控制
|
|
153
|
+
|
|
154
|
+
- 规则变更历史
|
|
155
|
+
- 支持回滚到历史版本
|
|
156
|
+
- 与 Git 集成(rules.yml 版本追踪)
|
|
157
|
+
|
|
158
|
+
#### 3.2 规则导入/导出
|
|
159
|
+
|
|
160
|
+
- 支持从其他 perm-gate 实例导入规则
|
|
161
|
+
- 支持导出规则包
|
|
162
|
+
- 支持从模板库导入
|
|
163
|
+
|
|
164
|
+
#### 3.3 规则审计日志
|
|
165
|
+
|
|
166
|
+
- 记录规则创建/修改/删除操作
|
|
167
|
+
- 记录规则匹配历史
|
|
168
|
+
- 支持审计日志查询
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 三、分支级权限可行性分析
|
|
173
|
+
|
|
174
|
+
### 当前 YAML 能否实现 git push 分支级权限?
|
|
175
|
+
|
|
176
|
+
#### 3.1 args 维度的匹配逻辑
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// evaluate.ts 第 76-81 行
|
|
180
|
+
// args dimension: any token matches any args glob.
|
|
181
|
+
if (rule.args.length > 0) {
|
|
182
|
+
const tokens: string[] = []
|
|
183
|
+
for (const cmd of commands) tokens.push(...cmd.args, ...cmd.redirects)
|
|
184
|
+
tokens.push(...pathCands, ...urlCands)
|
|
185
|
+
if (!tokens.some((tok) => rule.args.some((g) => g.re.test(tok)))) return false
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**关键发现:args 是 OR 逻辑,不是 AND 逻辑。**
|
|
190
|
+
|
|
191
|
+
对于 `git push --force origin main`:
|
|
192
|
+
- tokens = [`push`, `--force`, `origin`, `main`]
|
|
193
|
+
- args 匹配规则:只要**任意一个 token** 匹配**任意一个 glob 模式**,规则就生效
|
|
194
|
+
|
|
195
|
+
#### 3.2 能做什么
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
# ✅ 拦截所有带 --force 的命令(不管是不是 push)
|
|
199
|
+
- args: ["--force"]
|
|
200
|
+
level: deny
|
|
201
|
+
reason: "Force flag detected"
|
|
202
|
+
|
|
203
|
+
# ✅ 拦截所有涉及 main 分支的命令
|
|
204
|
+
- args: ["main"]
|
|
205
|
+
level: deny
|
|
206
|
+
reason: "Main branch target detected"
|
|
207
|
+
|
|
208
|
+
# ✅ 拦截所有 git push 命令
|
|
209
|
+
- command: [git]
|
|
210
|
+
args: ["push"]
|
|
211
|
+
level: confirm
|
|
212
|
+
reason: "Git push requires confirmation"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### 3.3 不能做什么
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
# ❌ 精确匹配 "git push --force origin main" 这个复合操作
|
|
219
|
+
# 因为 args 是 OR 逻辑,无法要求多个 token 同时存在
|
|
220
|
+
|
|
221
|
+
# ❌ 区分 "git push --force origin main" 和 "git checkout --force main"
|
|
222
|
+
# 两者都有 --force 和 main,无法区分
|
|
223
|
+
|
|
224
|
+
# ❌ 动态判断分支是"个人"还是"共享"
|
|
225
|
+
# YAML 是静态 pattern,没有 git 查询能力
|
|
226
|
+
|
|
227
|
+
# ❌ 匹配分支模式(如 release/*、feature/*)
|
|
228
|
+
# args glob 不支持路径段匹配(segments=false)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### 3.4 结论
|
|
232
|
+
|
|
233
|
+
| 能力 | 状态 | 说明 |
|
|
234
|
+
|------|------|------|
|
|
235
|
+
| 拦截 force push | ⚠️ 部分可行 | 只能匹配 `--force` token,无法限定是 push 操作 |
|
|
236
|
+
| 拦截特定分支 | ⚠️ 部分可行 | 只能硬编码分支名,无法判断共享属性 |
|
|
237
|
+
| 复合条件匹配 | ❌ 不可行 | args 是 OR 逻辑,无法要求多个 token 同时存在 |
|
|
238
|
+
| 运行时 git 查询 | ❌ 不可行 | YAML 是静态 pattern,无法调用 git 命令 |
|
|
239
|
+
| 分支模式匹配 | ⚠️ 部分可行 | 可用 `release*` 匹配,但无法匹配路径段 |
|
|
240
|
+
|
|
241
|
+
### 3.5 要实现真正的分支级权限,需要
|
|
242
|
+
|
|
243
|
+
#### 方案 A: 新增 `branch` 维度
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
permissions:
|
|
247
|
+
deny:
|
|
248
|
+
- branch:
|
|
249
|
+
target: "main|master|develop|release/*"
|
|
250
|
+
shared: true # 动态判断是否共享
|
|
251
|
+
reason: "Force push to shared branch"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**实现要求**:
|
|
255
|
+
- 运行时调用 `git branch -r --contains <branch>` 查询分支属性
|
|
256
|
+
- 查询 `git log --format='%ae' <branch>` 获取提交者信息
|
|
257
|
+
- 查询 `git config branch.<name>.remote` 获取远程仓库
|
|
258
|
+
- 缓存查询结果(避免重复调用)
|
|
259
|
+
- 添加超时机制(防止 git 命令卡住)
|
|
260
|
+
|
|
261
|
+
#### 方案 B: 改造 args 为 AND 逻辑
|
|
262
|
+
|
|
263
|
+
```yaml
|
|
264
|
+
permissions:
|
|
265
|
+
deny:
|
|
266
|
+
- args_match: AND # 新语法:要求所有条件同时满足
|
|
267
|
+
- "push"
|
|
268
|
+
- "--force"
|
|
269
|
+
- "origin"
|
|
270
|
+
- "main|master|develop"
|
|
271
|
+
reason: "Force push to shared branch"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**实现要求**:
|
|
275
|
+
- 修改 args 维度的匹配逻辑(从 OR 改为 AND)
|
|
276
|
+
- 添加新语法支持(`args_match: AND`)
|
|
277
|
+
- 保持向后兼容(旧的 `args` 保持 OR 逻辑)
|
|
278
|
+
|
|
279
|
+
#### 方案 C: 新增 `shared-branches` 配置
|
|
280
|
+
|
|
281
|
+
```yaml
|
|
282
|
+
# rules.yml 顶部配置
|
|
283
|
+
shared_branches:
|
|
284
|
+
- main
|
|
285
|
+
- master
|
|
286
|
+
- develop
|
|
287
|
+
- "release/*"
|
|
288
|
+
|
|
289
|
+
permissions:
|
|
290
|
+
deny:
|
|
291
|
+
- command: [git]
|
|
292
|
+
args: ["push", "--force"]
|
|
293
|
+
branch:
|
|
294
|
+
in: shared_branches # 引用预定义列表
|
|
295
|
+
reason: "Force push to shared branch"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**实现要求**:
|
|
299
|
+
- 新增 `shared_branches` 配置项
|
|
300
|
+
- 新增 `branch` 维度,支持 `in` 操作符引用列表
|
|
301
|
+
- 运行时解析分支名,匹配列表中的模式
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 四、建议的实施路径
|
|
306
|
+
|
|
307
|
+
### 短期 (1-2 周)
|
|
308
|
+
|
|
309
|
+
1. **Phase 1 UI 补全**: 集成 YAML 编辑器,暴露完整的规则编辑能力
|
|
310
|
+
2. **文档更新**: 明确说明 YAML 规则的完整能力边界
|
|
311
|
+
3. **临时方案**: 在文档中提供手动配置分支级权限的方法(硬编码分支名)
|
|
312
|
+
|
|
313
|
+
### 中期 (1-2 月)
|
|
314
|
+
|
|
315
|
+
1. **Phase 2 可视化编辑器**: 提供图形化规则创建界面
|
|
316
|
+
2. **args AND 逻辑**: 改造 args 维度,支持复合条件匹配
|
|
317
|
+
3. **规则测试功能**: 支持在 UI 中测试规则匹配结果
|
|
318
|
+
|
|
319
|
+
### 长期 (3-6 月)
|
|
320
|
+
|
|
321
|
+
1. **branch 维度**: 实现运行时 git 查询,支持动态分支判断
|
|
322
|
+
2. **规则版本控制**: 支持规则变更历史和回滚
|
|
323
|
+
3. **规则审计日志**: 记录规则匹配历史,支持审计
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## 五、与 PerryLink dsh-auto-review 的对比总结
|
|
328
|
+
|
|
329
|
+
| 维度 | dsh-perm-gate | dsh-auto-review | 差距分析 |
|
|
330
|
+
|------|--------------|-----------------|----------|
|
|
331
|
+
| 规则格式 | YAML 静态规则 | cordis patch 配置 | 格式不同,无法直接兼容 |
|
|
332
|
+
| 匹配逻辑 | 模式匹配 (pattern) | AI 语义理解 | 各有优势:模式匹配快,语义理解准 |
|
|
333
|
+
| 分支权限 | 不支持动态判断 | 不支持(无分支维度) | 两者都需要新增能力 |
|
|
334
|
+
| UI 面板 | 简化接口,缺失 YAML 编辑器 | 完整配置面板 | 我们需要补全 UI |
|
|
335
|
+
| 配置入口 | settings.yaml | cordis.patch.yml | 入口不同,无法直接融合 |
|
|
336
|
+
| 执行速度 | < 10ms (静态匹配) | 3-60s (AI 审查) | 我们的速度优势明显 |
|
|
337
|
+
|
|
338
|
+
**核心结论**: 两者是互补关系,不是替代关系。perm-gate 的静态规则适合确定性拦截(速度优先),auto-review 的 AI 审查适合模糊场景(精度优先)。分支级权限是两者都缺失的能力,需要各自实现。
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-perm-gate",
|
|
3
3
|
"name": "dsh-perm-gate",
|
|
4
|
-
"version": "2.1
|
|
4
|
+
"version": "2.6.1",
|
|
5
5
|
"description": {
|
|
6
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
7
|
"zh": "面向 DeepSeek Harness 0.1.2+ 的确定性优先、失败即拒的权限闸门:在 tools/pre-execute 瀑布上串起 P0 硬拒绝 → P1 精确会话授权 → P2 静态 allow/deny 规则链 → P3 可选 LLM 语义分类 → P4 ask,并附带独立 CLI 干跑评估器与可审计的决策日志。只以 deny/ask 否决、其余一律放行,规则未知或非法时直接报错,绝不静默失效。"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"engines": {
|
|
16
|
-
"dsh": ">=0.1.2-alpha.1"
|
|
16
|
+
"dsh": ">=0.1.2-alpha.1 <0.2.0-0"
|
|
17
17
|
},
|
|
18
18
|
"components": {
|
|
19
19
|
"host": "lib/index.js",
|
|
@@ -9,8 +9,23 @@ permissions:
|
|
|
9
9
|
reason: no direct remote shell
|
|
10
10
|
- paths: [.dsh/**]
|
|
11
11
|
reason: protect harness metadata
|
|
12
|
+
# `command` matches the decomposed command WORD only (`git`), so the
|
|
13
|
+
# subcommand belongs in `args`. A multi-word entry such as `git pull` can
|
|
14
|
+
# never match — see docs/rules-format.md §4.2.
|
|
15
|
+
- command: [git]
|
|
16
|
+
args: [push]
|
|
17
|
+
branch:
|
|
18
|
+
target: [main, master, "release*"]
|
|
19
|
+
shared: true
|
|
20
|
+
reason: no push to a protected branch
|
|
12
21
|
allow:
|
|
13
|
-
- command: [
|
|
22
|
+
- command: [pnpm, node]
|
|
23
|
+
reason: routine project development
|
|
24
|
+
- command: [git]
|
|
25
|
+
args: [pull]
|
|
26
|
+
reason: routine project development
|
|
27
|
+
- command: [npm]
|
|
28
|
+
args: [run]
|
|
14
29
|
reason: routine project development
|
|
15
30
|
- command: [curl, wget]
|
|
16
31
|
args: ["https://*.example.com/*"]
|
package/lib/allowlist.d.ts
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal face of the DSH settings scope the rules namespace is bound to
|
|
3
|
+
* (`scope.get()` / `scope.update(patch)`; the host merges a patch into the
|
|
4
|
+
* user layer, replacing arrays wholesale).
|
|
5
|
+
*/
|
|
6
|
+
export interface SettingsRulesScope {
|
|
7
|
+
get(): unknown;
|
|
8
|
+
update(patch: object): Promise<unknown> | unknown;
|
|
9
|
+
}
|
|
10
|
+
/** The allow-list command patterns currently in the settings rules document (read-only). */
|
|
11
|
+
export declare function listAllowFromRulesDoc(doc: unknown): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Append one command pattern as a new `allow` entry in the settings rules
|
|
14
|
+
* namespace. When the namespace is still unconfigured (bare defaults) and a
|
|
15
|
+
* file document is supplied, the namespace is seeded from it first — the
|
|
16
|
+
* implicit migration — so the first settings write cannot shadow file-defined
|
|
17
|
+
* deny/ask rules. Returns false when the scope rejects the write.
|
|
18
|
+
*/
|
|
19
|
+
export declare function appendAllowToSettings(scope: SettingsRulesScope, pattern: string, reason?: string, seed?: unknown): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Replace the settings rules namespace's `allow` whitelist with exactly the
|
|
22
|
+
* given command patterns (one allow entry per pattern); empty clears it.
|
|
23
|
+
* Other sections (deny / ask / defaultAction) are preserved — seeded from the
|
|
24
|
+
* file document on the namespace's first write. Returns false when the scope
|
|
25
|
+
* rejects the write.
|
|
26
|
+
*/
|
|
27
|
+
export declare function replaceAllowInSettings(scope: SettingsRulesScope, patterns: readonly string[], reason?: string, seed?: unknown): Promise<boolean>;
|
|
1
28
|
/**
|
|
2
29
|
* Append one command pattern as a new `allow` entry in the rules file.
|
|
3
30
|
* Returns false (leaving the file untouched) on any I/O or shape error.
|
package/lib/allowlist.js
CHANGED
|
@@ -2,11 +2,113 @@
|
|
|
2
2
|
* Durable allowlist (whitelist) writes for dsh-perm-gate.
|
|
3
3
|
*
|
|
4
4
|
* The "allow everywhere" approval action appends a matched command pattern to
|
|
5
|
-
* the rules
|
|
5
|
+
* the rules' `allow` list and reloads, so a human-approved command is
|
|
6
6
|
* persistently whitelisted across sessions — not just granted once.
|
|
7
|
+
*
|
|
8
|
+
* Two storage backs, per the dual-source migration:
|
|
9
|
+
* - **settings** (`appendAllowToSettings` / `replaceAllowInSettings`) — the
|
|
10
|
+
* `dsh-perm-gate-rules` namespace; the primary write path once the settings
|
|
11
|
+
* service is available.
|
|
12
|
+
* - **rules file** (`appendAllowCommand` / `replaceAllowCommands`) — the legacy
|
|
13
|
+
* path, kept as the fallback when no settings scope is wired (and as the
|
|
14
|
+
* migration seed: the first settings write carries the file document along so
|
|
15
|
+
* file-defined deny/ask rules are not shadowed).
|
|
7
16
|
*/
|
|
8
17
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
9
18
|
import { parse, stringify } from 'yaml';
|
|
19
|
+
import { isRulesConfigured } from './config.js';
|
|
20
|
+
function isRecord(v) {
|
|
21
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
22
|
+
}
|
|
23
|
+
/** Accept both the bare rules form and the file's `permissions:`-wrapped form. */
|
|
24
|
+
function unwrapPermissions(seed) {
|
|
25
|
+
if (!isRecord(seed))
|
|
26
|
+
return {};
|
|
27
|
+
return isRecord(seed['permissions']) ? seed['permissions'] : seed;
|
|
28
|
+
}
|
|
29
|
+
/** The file document a first write should migrate into the namespace, or bare defaults. */
|
|
30
|
+
function seedOrDefaults(seed) {
|
|
31
|
+
const root = unwrapPermissions(seed);
|
|
32
|
+
return {
|
|
33
|
+
defaultAction: typeof root['defaultAction'] === 'string' ? root['defaultAction'] : 'ask',
|
|
34
|
+
deny: Array.isArray(root['deny']) ? root['deny'] : [],
|
|
35
|
+
allow: Array.isArray(root['allow']) ? root['allow'] : [],
|
|
36
|
+
ask: Array.isArray(root['ask']) ? root['ask'] : [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function allowEntriesOf(section) {
|
|
40
|
+
const allow = section['allow'];
|
|
41
|
+
return Array.isArray(allow) ? allow : [];
|
|
42
|
+
}
|
|
43
|
+
function commandEntries(patterns, reason) {
|
|
44
|
+
return patterns.map((pattern) => ({ command: [pattern], reason }));
|
|
45
|
+
}
|
|
46
|
+
/** The allow-list command patterns currently in the settings rules document (read-only). */
|
|
47
|
+
export function listAllowFromRulesDoc(doc) {
|
|
48
|
+
const section = unwrapPermissions(doc);
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const entry of allowEntriesOf(section)) {
|
|
51
|
+
if (!isRecord(entry))
|
|
52
|
+
continue;
|
|
53
|
+
const command = entry['command'];
|
|
54
|
+
if (typeof command === 'string') {
|
|
55
|
+
out.push(command);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!Array.isArray(command))
|
|
59
|
+
continue;
|
|
60
|
+
for (const c of command)
|
|
61
|
+
if (typeof c === 'string')
|
|
62
|
+
out.push(c);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Append one command pattern as a new `allow` entry in the settings rules
|
|
68
|
+
* namespace. When the namespace is still unconfigured (bare defaults) and a
|
|
69
|
+
* file document is supplied, the namespace is seeded from it first — the
|
|
70
|
+
* implicit migration — so the first settings write cannot shadow file-defined
|
|
71
|
+
* deny/ask rules. Returns false when the scope rejects the write.
|
|
72
|
+
*/
|
|
73
|
+
export async function appendAllowToSettings(scope, pattern, reason = 'permissive allow-everywhere', seed) {
|
|
74
|
+
try {
|
|
75
|
+
const current = scope.get();
|
|
76
|
+
const section = isRulesConfigured(current) && isRecord(current)
|
|
77
|
+
? { ...current }
|
|
78
|
+
: seedOrDefaults(seed);
|
|
79
|
+
const allow = allowEntriesOf(section);
|
|
80
|
+
allow.push({ command: [pattern], reason });
|
|
81
|
+
section['allow'] = allow;
|
|
82
|
+
await scope.update(section);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Replace the settings rules namespace's `allow` whitelist with exactly the
|
|
91
|
+
* given command patterns (one allow entry per pattern); empty clears it.
|
|
92
|
+
* Other sections (deny / ask / defaultAction) are preserved — seeded from the
|
|
93
|
+
* file document on the namespace's first write. Returns false when the scope
|
|
94
|
+
* rejects the write.
|
|
95
|
+
*/
|
|
96
|
+
export async function replaceAllowInSettings(scope, patterns, reason = 'permissive allowlist', seed) {
|
|
97
|
+
try {
|
|
98
|
+
if (patterns === undefined)
|
|
99
|
+
return false;
|
|
100
|
+
const current = scope.get();
|
|
101
|
+
const section = isRulesConfigured(current) && isRecord(current)
|
|
102
|
+
? { ...current }
|
|
103
|
+
: seedOrDefaults(seed);
|
|
104
|
+
section['allow'] = commandEntries(patterns, reason);
|
|
105
|
+
await scope.update(section);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
10
112
|
/**
|
|
11
113
|
* Append one command pattern as a new `allow` entry in the rules file.
|
|
12
114
|
* Returns false (leaving the file untouched) on any I/O or shape error.
|
package/lib/cli.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseArgs } from 'node:util';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import { createDryRunRuntime, runDryRun } from './dry-run.js';
|
|
14
14
|
function main() {
|
|
15
15
|
const { values } = parseArgs({
|
|
16
16
|
options: {
|
|
@@ -23,11 +23,7 @@ function main() {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
const rulesFile = values.rules ? resolve(values.rules) : undefined;
|
|
26
|
-
const runtime =
|
|
27
|
-
rulesFile,
|
|
28
|
-
caseInsensitivePaths: true,
|
|
29
|
-
permissive: values.permissive || undefined,
|
|
30
|
-
});
|
|
26
|
+
const runtime = createDryRunRuntime({ rulesFile, permissive: values.permissive });
|
|
31
27
|
if (values.list) {
|
|
32
28
|
process.stdout.write(JSON.stringify({
|
|
33
29
|
rulesFile,
|
|
@@ -53,15 +49,16 @@ function main() {
|
|
|
53
49
|
process.exitCode = 2;
|
|
54
50
|
return;
|
|
55
51
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
52
|
+
// `hostView` reproduces this command's historic output exactly: it reports the
|
|
53
|
+
// host-facing decision path (whose `allow` is a bare passthrough with no
|
|
54
|
+
// reason) and its audit count. The panel reports the pure policy verdict
|
|
55
|
+
// instead, because a session-less caller must not inherit the session layers.
|
|
56
|
+
const result = runDryRun({ tool, args, rulesFile, cwd: values.cwd, hostView: true }, runtime);
|
|
57
|
+
const host = result.host;
|
|
61
58
|
process.stdout.write(JSON.stringify({
|
|
62
|
-
decision:
|
|
63
|
-
reason:
|
|
64
|
-
audited:
|
|
59
|
+
decision: host?.verdict ?? result.verdict,
|
|
60
|
+
reason: host?.reason ?? result.reason,
|
|
61
|
+
audited: host?.audited ?? 0,
|
|
65
62
|
}, null, 2) + '\n');
|
|
66
63
|
}
|
|
67
64
|
main();
|