dsh-perm-gate 2.1.2 → 4.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 (57) hide show
  1. package/CHANGELOG.ja.md +82 -0
  2. package/CHANGELOG.ko.md +80 -0
  3. package/CHANGELOG.md +225 -0
  4. package/INSTALL.ja.md +15 -9
  5. package/INSTALL.ko.md +16 -10
  6. package/INSTALL.md +17 -9
  7. package/INSTALL.zh.md +13 -8
  8. package/README.ja.md +17 -5
  9. package/README.ko.md +18 -6
  10. package/README.md +33 -3
  11. package/README.zh.md +11 -3
  12. package/docs/baseline-guard.md +56 -0
  13. package/docs/rules-format.en.md +294 -0
  14. package/docs/rules-format.md +279 -0
  15. package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
  16. package/dsh.plugin.json +2 -2
  17. package/examples/permissions.example.yaml +16 -1
  18. package/lib/allowlist.d.ts +27 -0
  19. package/lib/allowlist.js +103 -1
  20. package/lib/cli.js +11 -14
  21. package/lib/client.js +646 -95
  22. package/lib/client.js.map +1 -1
  23. package/lib/command-dispatcher.d.ts +22 -0
  24. package/lib/command-dispatcher.js +97 -0
  25. package/lib/command-semantics.d.ts +62 -0
  26. package/lib/command-semantics.js +6 -0
  27. package/lib/compiler.d.ts +11 -0
  28. package/lib/compiler.js +16 -0
  29. package/lib/config.d.ts +73 -12
  30. package/lib/config.js +56 -14
  31. package/lib/dry-run.d.ts +87 -0
  32. package/lib/dry-run.js +110 -0
  33. package/lib/engine.js +52 -2
  34. package/lib/evaluate.js +76 -2
  35. package/lib/events.d.ts +91 -2
  36. package/lib/events.js +220 -0
  37. package/lib/index.js +111 -26
  38. package/lib/parsers/git.d.ts +43 -0
  39. package/lib/parsers/git.js +404 -0
  40. package/lib/parsers/shell-cmds.d.ts +32 -0
  41. package/lib/parsers/shell-cmds.js +293 -0
  42. package/lib/preset.d.ts +13 -0
  43. package/lib/preset.js +17 -0
  44. package/lib/risk.d.ts +17 -3
  45. package/lib/risk.js +17 -3
  46. package/lib/rule-chain.js +13 -22
  47. package/lib/rule-dims.d.ts +35 -0
  48. package/lib/rule-dims.js +36 -0
  49. package/lib/rule.d.ts +36 -1
  50. package/lib/rule.js +56 -8
  51. package/lib/rules-view.d.ts +52 -0
  52. package/lib/rules-view.js +126 -0
  53. package/lib/runtime.d.ts +121 -7
  54. package/lib/runtime.js +204 -25
  55. package/lib/shell.d.ts +14 -0
  56. package/lib/shell.js +17 -0
  57. package/package.json +13 -5
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 file's `allow` list and reloads, so a human-approved command is
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 { PermGateRuntime } from './runtime.js';
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 = new PermGateRuntime({
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
- const decision = runtime.decideExecution({
57
- name: tool,
58
- arguments: args,
59
- cwd: values.cwd ? resolve(values.cwd) : process.cwd(),
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: decision === undefined ? 'allow' : decision.kind,
63
- reason: decision?.reason ?? '(default/passthrough)',
64
- audited: runtime.auditEntries.length,
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();