dsh-codex-approval 0.2.2 → 0.3.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.
package/rules.js CHANGED
@@ -1,90 +1,90 @@
1
- /**
2
- * dsh-codex-approval — rules.js
3
- *
4
- * Codex-style rule matching. A rule matches a request via a single glob
5
- * pattern over the "matchable text": `ToolName(args preview) reason:<reason>`.
6
- * Examples:
7
- * - `Bash(git *)` — the recovered bash command starts with "git "
8
- * - `Bash(rm -rf /*)` — destructive command
9
- * - `reason:*curl*` — the approval reason mentions curl
10
- *
11
- * Evaluation priority is safety-first regardless of list order:
12
- * deny > ask > allow
13
- * (an explicit ask or deny can never be overridden by a blanket allow,
14
- * mirroring Codex where ask/reject rules take precedence over auto-approve).
15
- */
16
-
17
- /** Classic glob match: `*` = any sequence (incl. empty), `?` = one char. Case-insensitive. */
18
- export function wildcardMatch(pattern, text) {
19
- if (typeof pattern !== "string" || typeof text !== "string") return false;
20
- pattern = pattern.toLowerCase();
21
- text = text.toLowerCase();
22
- let pi = 0;
23
- let ti = 0;
24
- let star = -1;
25
- let mark = 0;
26
- while (ti < text.length) {
27
- if (pi < pattern.length && (pattern[pi] === "?" || pattern[pi] === text[ti])) {
28
- pi += 1;
29
- ti += 1;
30
- } else if (pi < pattern.length && pattern[pi] === "*") {
31
- star = pi;
32
- pi += 1;
33
- mark = ti;
34
- } else if (star !== -1) {
35
- pi = star + 1;
36
- ti = mark + 1;
37
- mark += 1;
38
- } else {
39
- return false;
40
- }
41
- }
42
- while (pi < pattern.length && pattern[pi] === "*") pi += 1;
43
- return pi === pattern.length;
44
- }
45
-
46
- /**
47
- * Build the single string rules match against.
48
- * @param req - { toolName, argsText, reason }
49
- */
50
- export function matchableText(req) {
51
- const bits = [];
52
- if (req.toolName) bits.push(`${req.toolName}(${req.argsText ?? ""})`);
53
- if (req.reason) bits.push(`reason:${req.reason}`);
54
- return bits.join(" ");
55
- }
56
-
57
- /**
58
- * The surfaces a rule pattern is tested against, in order: the tool call
59
- * alone (`ToolName(args)`), the reason alone (`reason:...`), then the
60
- * combined string. This lets `Bash(git *)` match regardless of an appended
61
- * reason, and `reason:*curl*` match the reason alone.
62
- */
63
- export function matchSurfaces(req) {
64
- const surfaces = [];
65
- if (req.toolName) surfaces.push(`${req.toolName}(${req.argsText ?? ""})`);
66
- if (req.reason) surfaces.push(`reason:${req.reason}`);
67
- const combined = surfaces.join(" ");
68
- if (!surfaces.includes(combined)) surfaces.push(combined);
69
- return surfaces.filter((surface) => surface !== "");
70
- }
71
-
72
- /**
73
- * Evaluate an ordered rule list against one request.
74
- * @param rules - [{ match: string, action: "allow"|"ask"|"deny" }]
75
- * @param req - { toolName, argsText, reason }
76
- * @returns the first matching rule under deny > ask > allow priority, or null.
77
- */
78
- export function evaluateRules(rules, req) {
79
- const surfaces = matchSurfaces(req);
80
- if (surfaces.length === 0) return null;
81
- for (const action of ["deny", "ask", "allow"]) {
82
- for (const rule of rules) {
83
- if (rule.action !== action) continue;
84
- for (const surface of surfaces) {
85
- if (wildcardMatch(rule.match, surface)) return rule;
86
- }
87
- }
88
- }
89
- return null;
90
- }
1
+ /**
2
+ * dsh-codex-approval — rules.js
3
+ *
4
+ * Codex-style rule matching. A rule matches a request via a single glob
5
+ * pattern over the "matchable text": `ToolName(args preview) reason:<reason>`.
6
+ * Examples:
7
+ * - `Bash(git *)` — the recovered bash command starts with "git "
8
+ * - `Bash(rm -rf /*)` — destructive command
9
+ * - `reason:*curl*` — the approval reason mentions curl
10
+ *
11
+ * Evaluation priority is safety-first regardless of list order:
12
+ * deny > ask > allow
13
+ * (an explicit ask or deny can never be overridden by a blanket allow,
14
+ * mirroring Codex where ask/reject rules take precedence over auto-approve).
15
+ */
16
+
17
+ /** Classic glob match: `*` = any sequence (incl. empty), `?` = one char. Case-insensitive. */
18
+ export function wildcardMatch(pattern, text) {
19
+ if (typeof pattern !== "string" || typeof text !== "string") return false;
20
+ pattern = pattern.toLowerCase();
21
+ text = text.toLowerCase();
22
+ let pi = 0;
23
+ let ti = 0;
24
+ let star = -1;
25
+ let mark = 0;
26
+ while (ti < text.length) {
27
+ if (pi < pattern.length && (pattern[pi] === "?" || pattern[pi] === text[ti])) {
28
+ pi += 1;
29
+ ti += 1;
30
+ } else if (pi < pattern.length && pattern[pi] === "*") {
31
+ star = pi;
32
+ pi += 1;
33
+ mark = ti;
34
+ } else if (star !== -1) {
35
+ pi = star + 1;
36
+ ti = mark + 1;
37
+ mark += 1;
38
+ } else {
39
+ return false;
40
+ }
41
+ }
42
+ while (pi < pattern.length && pattern[pi] === "*") pi += 1;
43
+ return pi === pattern.length;
44
+ }
45
+
46
+ /**
47
+ * Build the single string rules match against.
48
+ * @param req - { toolName, argsText, reason }
49
+ */
50
+ export function matchableText(req) {
51
+ const bits = [];
52
+ if (req.toolName) bits.push(`${req.toolName}(${req.argsText ?? ""})`);
53
+ if (req.reason) bits.push(`reason:${req.reason}`);
54
+ return bits.join(" ");
55
+ }
56
+
57
+ /**
58
+ * The surfaces a rule pattern is tested against, in order: the tool call
59
+ * alone (`ToolName(args)`), the reason alone (`reason:...`), then the
60
+ * combined string. This lets `Bash(git *)` match regardless of an appended
61
+ * reason, and `reason:*curl*` match the reason alone.
62
+ */
63
+ export function matchSurfaces(req) {
64
+ const surfaces = [];
65
+ if (req.toolName) surfaces.push(`${req.toolName}(${req.argsText ?? ""})`);
66
+ if (req.reason) surfaces.push(`reason:${req.reason}`);
67
+ const combined = surfaces.join(" ");
68
+ if (!surfaces.includes(combined)) surfaces.push(combined);
69
+ return surfaces.filter((surface) => surface !== "");
70
+ }
71
+
72
+ /**
73
+ * Evaluate an ordered rule list against one request.
74
+ * @param rules - [{ match: string, action: "allow"|"ask"|"deny" }]
75
+ * @param req - { toolName, argsText, reason }
76
+ * @returns the first matching rule under deny > ask > allow priority, or null.
77
+ */
78
+ export function evaluateRules(rules, req) {
79
+ const surfaces = matchSurfaces(req);
80
+ if (surfaces.length === 0) return null;
81
+ for (const action of ["deny", "ask", "allow"]) {
82
+ for (const rule of rules) {
83
+ if (rule.action !== action) continue;
84
+ for (const surface of surfaces) {
85
+ if (wildcardMatch(rule.match, surface)) return rule;
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }