dsh-perm-gate 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.ja.md +82 -0
  2. package/CHANGELOG.ko.md +80 -0
  3. package/CHANGELOG.md +321 -0
  4. package/INSTALL.ja.md +70 -8
  5. package/INSTALL.ko.md +68 -9
  6. package/INSTALL.md +68 -8
  7. package/INSTALL.zh.md +59 -7
  8. package/README.ja.md +103 -16
  9. package/README.ko.md +101 -17
  10. package/README.md +152 -25
  11. package/README.zh.md +99 -12
  12. package/cordis.patch.yml +34 -3
  13. package/docs/baseline-guard.md +56 -0
  14. package/docs/plan-rules-to-settings.md +217 -0
  15. package/docs/rules-format.en.md +294 -0
  16. package/docs/rules-format.md +279 -0
  17. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  18. package/dsh.plugin.json +22 -0
  19. package/examples/permissions.example.yaml +16 -1
  20. package/lib/agent-identity.d.ts +43 -0
  21. package/lib/agent-identity.js +95 -0
  22. package/lib/allowlist.d.ts +27 -0
  23. package/lib/allowlist.js +103 -1
  24. package/lib/cli.js +11 -14
  25. package/lib/client.js +598 -6
  26. package/lib/client.js.map +1 -1
  27. package/lib/command-dispatcher.d.ts +22 -0
  28. package/lib/command-dispatcher.js +97 -0
  29. package/lib/command-semantics.d.ts +62 -0
  30. package/lib/command-semantics.js +6 -0
  31. package/lib/compiler.d.ts +50 -0
  32. package/lib/compiler.js +122 -0
  33. package/lib/config.d.ts +161 -7
  34. package/lib/config.js +78 -11
  35. package/lib/dry-run.d.ts +87 -0
  36. package/lib/dry-run.js +110 -0
  37. package/lib/engine.js +61 -4
  38. package/lib/evaluate.d.ts +9 -0
  39. package/lib/evaluate.js +264 -1
  40. package/lib/events.d.ts +69 -2
  41. package/lib/events.js +123 -0
  42. package/lib/index.d.ts +1 -1
  43. package/lib/index.js +320 -11
  44. package/lib/network-lifecycle.d.ts +107 -0
  45. package/lib/network-lifecycle.js +180 -0
  46. package/lib/network.d.ts +109 -0
  47. package/lib/network.js +196 -0
  48. package/lib/parsers/git.d.ts +43 -0
  49. package/lib/parsers/git.js +404 -0
  50. package/lib/parsers/shell-cmds.d.ts +32 -0
  51. package/lib/parsers/shell-cmds.js +293 -0
  52. package/lib/preset.d.ts +13 -0
  53. package/lib/preset.js +17 -0
  54. package/lib/proxy.d.ts +160 -0
  55. package/lib/proxy.js +529 -0
  56. package/lib/risk.d.ts +17 -3
  57. package/lib/risk.js +19 -6
  58. package/lib/rule-chain.d.ts +39 -0
  59. package/lib/rule-chain.js +213 -0
  60. package/lib/rule-dims.d.ts +174 -0
  61. package/lib/rule-dims.js +230 -0
  62. package/lib/rule.d.ts +60 -0
  63. package/lib/rule.js +69 -7
  64. package/lib/rules-view.d.ts +52 -0
  65. package/lib/rules-view.js +126 -0
  66. package/lib/runtime.d.ts +208 -7
  67. package/lib/runtime.js +433 -27
  68. package/lib/session-sweep.d.ts +45 -0
  69. package/lib/session-sweep.js +153 -0
  70. package/lib/shadow.d.ts +34 -0
  71. package/lib/shadow.js +143 -0
  72. package/lib/shell.d.ts +14 -0
  73. package/lib/shell.js +17 -0
  74. package/lib/watch.d.ts +42 -0
  75. package/lib/watch.js +170 -0
  76. package/package.json +15 -2
  77. package/scripts/patch-permission-glyph.mjs +209 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Git command parser - extracts subcommand, branch, remote, flags, destructiveness.
3
+ * P0 priority - most critical for branch permissions.
4
+ *
5
+ * Handles:
6
+ * - git push/force push detection
7
+ * - git branch operations (delete, rename)
8
+ * - git checkout/switch with branch names
9
+ * - git merge/rebase
10
+ * - git reset (soft/hard/mixed)
11
+ * - git clean (destructive file deletion)
12
+ * - git stash operations
13
+ * - git refspec parsing (HEAD:main)
14
+ * - Remote vs branch name disambiguation
15
+ */
16
+ /** Known protected branch patterns */
17
+ const PROTECTED_BRANCHES = /^(main|master|production|release|stable)$/i;
18
+ /** Known remote names (common defaults) */
19
+ const KNOWN_REMOTES = /^(origin|upstream|fork|origin-\w+)$/i;
20
+ /** Flags that increase destructiveness */
21
+ const DESTRUCTIVE_FLAGS = new Set([
22
+ '--force', '-f',
23
+ '--hard',
24
+ '--delete', '-d', '-D',
25
+ '--amend',
26
+ '--no-backup',
27
+ '-rf',
28
+ '--clean',
29
+ '--reset',
30
+ ]);
31
+ export class GitParser {
32
+ name = 'git';
33
+ priority = 10;
34
+ canParse(command) {
35
+ const trimmed = command.trim();
36
+ // Direct git command
37
+ if (/^git\s+/.test(trimmed))
38
+ return true;
39
+ // Shell-wrapped: sh -c "git ...", bash -c git ..., etc.
40
+ if (/(?:sh|bash|pwsh|powershell)(?:\s+-c\s+['"]?)?.*?\bgit\s+/.test(trimmed))
41
+ return true;
42
+ return false;
43
+ }
44
+ parse(command) {
45
+ const trimmed = command.trim();
46
+ // Extract the git command portion (strip shell wrappers)
47
+ const gitMatch = trimmed.match(/(?:sh|bash|pwsh|powershell)(?:\s+-c\s+['"]?)?(\s+git\s+.+)/);
48
+ const gitPart = gitMatch ? gitMatch[1].trim() : trimmed;
49
+ // Parse git arguments
50
+ const parts = this.parseGitArgs(gitPart);
51
+ if (parts.length < 2)
52
+ return null;
53
+ // Skip 'git' keyword if present
54
+ const startIndex = parts[0] === 'git' ? 1 : 0;
55
+ if (parts.length - startIndex < 1)
56
+ return null;
57
+ const subcommand = parts[startIndex]?.toLowerCase();
58
+ // Get rest of arguments
59
+ const args = parts.slice(startIndex + 1);
60
+ return this.analyzeSubcommand(subcommand, args, trimmed);
61
+ }
62
+ analyzeSubcommand(subcommand, args, rawCommand) {
63
+ const flags = this.extractFlags(args);
64
+ const hasForce = flags.includes('--force') || flags.includes('-f') || flags.includes('--force-with-lease');
65
+ const hasDelete = flags.includes('--delete') || flags.includes('-d') || flags.includes('-D');
66
+ const hasHard = flags.includes('--hard');
67
+ let operation;
68
+ let destructiveness = 1;
69
+ let targets = [];
70
+ let branch;
71
+ let remote;
72
+ let gitOperation = true;
73
+ let targetsSharedBranch = false;
74
+ switch (subcommand) {
75
+ case 'push': {
76
+ remote = this.extractRemote(args);
77
+ branch = this.extractBranch(args);
78
+ targets = [branch, remote].filter(Boolean);
79
+ targetsSharedBranch = this.isProtectedBranch(branch);
80
+ if (hasForce || flags.includes('--force-with-lease')) {
81
+ operation = 'force-push';
82
+ destructiveness = targetsSharedBranch ? 5 : 4;
83
+ }
84
+ else if (hasDelete) {
85
+ // 删除远端分支。`hasDelete` 此前只在 `branch` 分支被读取,push 侧从未
86
+ // 使用——于是 `git push --delete origin main` 落进上面的 else,被判成
87
+ // 普通 push(dest=4)、`DESTRUCTIVENESS_MAP` 里的 `'push-delete': 5`
88
+ // 是**死条目**。删远端分支与 force push 同级:都不可逆地改动共享历史。
89
+ operation = 'push-delete';
90
+ destructiveness = targetsSharedBranch ? 5 : 4;
91
+ }
92
+ else {
93
+ operation = 'push';
94
+ destructiveness = targetsSharedBranch ? 4 : 3;
95
+ }
96
+ break;
97
+ }
98
+ case 'reset': {
99
+ if (hasHard) {
100
+ operation = 'reset-hard';
101
+ destructiveness = 5;
102
+ }
103
+ else if (flags.includes('--soft')) {
104
+ operation = 'reset-soft';
105
+ destructiveness = 2;
106
+ }
107
+ else {
108
+ operation = 'reset-mixed';
109
+ destructiveness = 4;
110
+ }
111
+ targets = this.extractRefs(args);
112
+ break;
113
+ }
114
+ case 'branch': {
115
+ if (hasDelete) {
116
+ const forceDelete = flags.includes('-D');
117
+ branch = this.extractBranchName(args);
118
+ targets = [branch].filter(Boolean);
119
+ targetsSharedBranch = this.isProtectedBranch(branch);
120
+ operation = forceDelete ? 'branch-delete-force' : 'branch-delete';
121
+ destructiveness = targetsSharedBranch ? 5 : (forceDelete ? 3 : 2);
122
+ }
123
+ else if (flags.includes('-m') || flags.includes('--move')) {
124
+ operation = 'branch-rename';
125
+ destructiveness = 2;
126
+ targets = this.extractRefs(args);
127
+ }
128
+ else {
129
+ operation = 'branch-create';
130
+ destructiveness = 3;
131
+ branch = this.extractBranchName(args);
132
+ targets = [branch].filter(Boolean);
133
+ }
134
+ break;
135
+ }
136
+ case 'checkout':
137
+ case 'switch': {
138
+ const isNewBranch = flags.includes('-b') || flags.includes('--create');
139
+ branch = this.extractBranchName(args);
140
+ targets = [branch].filter(Boolean);
141
+ operation = isNewBranch ? 'checkout-new-branch' : 'checkout';
142
+ destructiveness = isNewBranch ? 3 : 2;
143
+ break;
144
+ }
145
+ case 'merge': {
146
+ targets = this.extractRefs(args);
147
+ if (flags.includes('--ff-only')) {
148
+ operation = 'merge-ff-only';
149
+ destructiveness = 3;
150
+ }
151
+ else {
152
+ operation = 'merge';
153
+ destructiveness = 3;
154
+ }
155
+ break;
156
+ }
157
+ case 'rebase': {
158
+ targets = this.extractRefs(args);
159
+ if (flags.includes('-i') || flags.includes('--interactive')) {
160
+ operation = 'rebase-interactive';
161
+ destructiveness = 4;
162
+ }
163
+ else {
164
+ operation = 'rebase';
165
+ destructiveness = 3;
166
+ }
167
+ break;
168
+ }
169
+ case 'clean': {
170
+ if (hasForce || flags.includes('-f')) {
171
+ operation = 'clean-force';
172
+ destructiveness = 5;
173
+ }
174
+ else {
175
+ operation = 'clean';
176
+ destructiveness = 3;
177
+ }
178
+ targets = this.extractRefs(args);
179
+ break;
180
+ }
181
+ case 'cherry-pick': {
182
+ operation = 'cherry-pick';
183
+ destructiveness = 3;
184
+ targets = this.extractRefs(args);
185
+ break;
186
+ }
187
+ case 'revert': {
188
+ operation = 'revert';
189
+ destructiveness = 3;
190
+ targets = this.extractRefs(args);
191
+ break;
192
+ }
193
+ case 'tag': {
194
+ if (hasDelete) {
195
+ operation = 'tag-delete';
196
+ destructiveness = 2;
197
+ targets = this.extractRefs(args);
198
+ }
199
+ else {
200
+ operation = 'tag-create';
201
+ destructiveness = 1;
202
+ targets = this.extractRefs(args);
203
+ }
204
+ break;
205
+ }
206
+ case 'stash': {
207
+ const stashOp = args[0]?.toLowerCase();
208
+ if (stashOp === 'pop' || stashOp === 'apply') {
209
+ operation = 'stash-pop';
210
+ destructiveness = 2;
211
+ }
212
+ else if (stashOp === 'drop') {
213
+ operation = 'stash-drop';
214
+ destructiveness = 2;
215
+ }
216
+ else {
217
+ operation = 'stash-create';
218
+ destructiveness = 1;
219
+ }
220
+ break;
221
+ }
222
+ case 'fetch': {
223
+ remote = this.extractRemote(args);
224
+ targets = [remote].filter(Boolean);
225
+ operation = 'fetch';
226
+ destructiveness = 1;
227
+ break;
228
+ }
229
+ case 'pull': {
230
+ remote = this.extractRemote(args);
231
+ branch = this.extractBranch(args);
232
+ targets = [remote, branch].filter(Boolean);
233
+ operation = 'pull';
234
+ destructiveness = 2;
235
+ break;
236
+ }
237
+ case 'clone': {
238
+ operation = 'clone';
239
+ destructiveness = 1;
240
+ targets = this.extractRefs(args);
241
+ break;
242
+ }
243
+ case 'status':
244
+ case 'log':
245
+ case 'diff':
246
+ case 'show':
247
+ case 'blame':
248
+ case 'ls-files':
249
+ case 'rev-parse':
250
+ case 'describe': {
251
+ operation = subcommand;
252
+ destructiveness = 1;
253
+ break;
254
+ }
255
+ default: {
256
+ // Unknown git subcommand
257
+ operation = subcommand;
258
+ destructiveness = 1;
259
+ gitOperation = true;
260
+ break;
261
+ }
262
+ }
263
+ // Detect destructive flags
264
+ const destructiveFlags = hasForce || hasDelete || hasHard ||
265
+ flags.some(f => DESTRUCTIVE_FLAGS.has(f));
266
+ return {
267
+ family: 'git',
268
+ subcommand,
269
+ operation,
270
+ destructiveness: destructiveness,
271
+ destructiveFlags,
272
+ targets,
273
+ flags,
274
+ requiresConfirmation: destructiveness >= 4,
275
+ compound: false,
276
+ gitOperation,
277
+ branch,
278
+ remote,
279
+ targetsSharedBranch,
280
+ rawCommand: rawCommand,
281
+ };
282
+ }
283
+ parseGitArgs(command) {
284
+ const args = [];
285
+ let current = '';
286
+ let inQuote = false;
287
+ let quoteChar = '';
288
+ let escape = false;
289
+ for (const char of command) {
290
+ if (escape) {
291
+ current += char;
292
+ escape = false;
293
+ continue;
294
+ }
295
+ if (char === '\\') {
296
+ escape = true;
297
+ continue;
298
+ }
299
+ if (inQuote) {
300
+ if (char === quoteChar) {
301
+ inQuote = false;
302
+ continue;
303
+ }
304
+ current += char;
305
+ continue;
306
+ }
307
+ if (char === '"' || char === "'") {
308
+ inQuote = true;
309
+ quoteChar = char;
310
+ continue;
311
+ }
312
+ if (/\s/.test(char)) {
313
+ if (current) {
314
+ args.push(current);
315
+ current = '';
316
+ }
317
+ continue;
318
+ }
319
+ current += char;
320
+ }
321
+ if (current)
322
+ args.push(current);
323
+ return args;
324
+ }
325
+ extractFlags(args) {
326
+ const flags = [];
327
+ for (const arg of args) {
328
+ if (arg.startsWith('-')) {
329
+ // Normalize long flags to lowercase, but preserve case for short flags
330
+ // to distinguish between -d (delete) and -D (force delete)
331
+ if (arg.startsWith('--')) {
332
+ flags.push(arg.toLowerCase());
333
+ }
334
+ else {
335
+ // Short flags: -f, -d, -D, etc. - preserve case
336
+ flags.push(arg);
337
+ }
338
+ }
339
+ }
340
+ return flags;
341
+ }
342
+ extractRemote(args) {
343
+ // First positional arg is usually remote
344
+ for (const arg of args) {
345
+ if (!arg.startsWith('-')) {
346
+ if (KNOWN_REMOTES.test(arg)) {
347
+ return arg;
348
+ }
349
+ // Could be a URL or path
350
+ if (arg.includes('/') || arg.includes('.')) {
351
+ return arg;
352
+ }
353
+ }
354
+ }
355
+ return undefined;
356
+ }
357
+ extractBranch(args) {
358
+ // Branch is usually the last positional arg or after remote
359
+ const positionalArgs = args.filter(a => !a.startsWith('-'));
360
+ if (positionalArgs.length > 1) {
361
+ // Could be "origin main" or "origin:main"
362
+ const last = positionalArgs[positionalArgs.length - 1];
363
+ // Handle refspec (e.g., HEAD:main)
364
+ if (last.includes(':')) {
365
+ return last.split(':')[1];
366
+ }
367
+ return last;
368
+ }
369
+ return positionalArgs[0];
370
+ }
371
+ extractBranchName(args) {
372
+ // For branch create/checkout, find the branch name
373
+ const positionalArgs = args.filter(a => !a.startsWith('-'));
374
+ // Skip -b flag value if present
375
+ for (let i = 0; i < args.length; i++) {
376
+ if (args[i] === '-b' || args[i] === '--create') {
377
+ return args[i + 1];
378
+ }
379
+ }
380
+ return positionalArgs[positionalArgs.length - 1];
381
+ }
382
+ extractRefs(args) {
383
+ return args.filter(a => !a.startsWith('-') && !KNOWN_REMOTES.test(a));
384
+ }
385
+ /**
386
+ * 目标是否落在保护分支上。
387
+ *
388
+ * **只剥已知的 ref 前缀,不对任意 `/` 取末段。** 早先的写法是
389
+ * `branch.split('/').pop()`,于是:
390
+ * - `refs/heads/main` → `main` ✅ 期望如此
391
+ * - `backup/main` → `main` ❌ **误判**(它是含斜杠的普通分支名)
392
+ * - `feat/release` → `release` ❌ **误判**
393
+ *
394
+ * 误判方向不是"更安全"。这条判据喂给 P0 硬拒——而 P0 是**不可配置撤销**的
395
+ * (见 engine.ts 顶部:"deterministic, monotonic, never negotiated")。
396
+ * 漏判还有关键词层与 LLM 层兜底,误判则是"合法工作流被永久挡住且无出口"。
397
+ */
398
+ isProtectedBranch(branch) {
399
+ if (!branch)
400
+ return false;
401
+ const cleanBranch = branch.replace(/^refs\/(?:heads|tags|remotes\/[^/]+)\//, '');
402
+ return PROTECTED_BRANCHES.test(cleanBranch);
403
+ }
404
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shell dangerous commands parser - rm, mv, cp, chmod, dd, diskpart, etc.
3
+ * P1 priority - detects destructive filesystem operations.
4
+ *
5
+ * Handles:
6
+ * - rm -rf / recursive deletion
7
+ * - mv overwriting existing files
8
+ * - cp -r with destructive patterns
9
+ * - chmod/chown with system directories
10
+ * - dd writing to disk
11
+ * - diskpart on Windows
12
+ * - format disk operations
13
+ * - PowerShell Remove-Item with dangerous patterns
14
+ */
15
+ import type { CommandParser, CommandSemantics } from '../command-semantics.js';
16
+ export declare class ShellDangerousParser implements CommandParser {
17
+ readonly name = "shell-dangerous";
18
+ readonly priority = 20;
19
+ canParse(command: string): boolean;
20
+ parse(command: string): CommandSemantics | null;
21
+ private extractBaseName;
22
+ private parseRm;
23
+ private parseMv;
24
+ private parseCp;
25
+ private parseChmod;
26
+ private parseDd;
27
+ private parseDiskpart;
28
+ private parsePowerShellRemove;
29
+ private parseArchive;
30
+ private extractArgs;
31
+ private extractFlags;
32
+ }