69eb24eba6c0e01158ff0797e0c57187ae596669

Author
TheEdgeOfRage <git@theedgeofrage.com>
Committer
TheEdgeOfRage <git@theedgeofrage.com>
Date

Message

Simplify deterministic rules

Diff

  1diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
  2index 6f6616772b15e643860372e7b9fb0c7637c3faa7..3784f7ca4f7b745da8b6ef2fd1a977f4443b4b11 100644
  3--- a/src/core/deterministic.ts
  4+++ b/src/core/deterministic.ts
  5@@ -1,33 +1,11 @@
  6 import type { Decision } from "./types";
  7-import {
  8-  HARD_ALLOW_PATTERNS,
  9-  CONFIG_ALLOW_PATTERNS,
 10-  ASK_PATTERNS,
 11-  SHELL_CONTROL_RE,
 12-  STDERR_REDIRECT_RE,
 13-  DEVNULL_REDIRECT_RE,
 14-} from "./rules";
 15+import { ALLOW_PATTERNS, ASK_PATTERNS, SHELL_CONTROL_RE, STDERR_REDIRECT_RE, DEVNULL_REDIRECT_RE } from "./rules";
 16 import { stripRtkPrefix, expandHome } from "./normalize";
 17 
 18 function hasShellControl(cmd: string): boolean {
 19   return SHELL_CONTROL_RE.test(cmd);
 20 }
 21 
 22-function checkHardAllow(command: string): Decision | undefined {
 23-  const cmd = command.trim();
 24-  if (!cmd || hasShellControl(cmd)) return undefined;
 25-  for (const pat of HARD_ALLOW_PATTERNS) {
 26-    if (pat.test(cmd)) {
 27-      return {
 28-        decision: "allow",
 29-        reason: `Matched safe pattern: ${pat.source.slice(0, 50)}`,
 30-        category: "read_only",
 31-      };
 32-    }
 33-  }
 34-  return undefined;
 35-}
 36-
 37 function shellWords(command: string): string[] {
 38   return (command.match(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]+/g) ?? []).map((word) => {
 39     if ((word.startsWith('"') && word.endsWith('"')) || (word.startsWith("'") && word.endsWith("'"))) {
 40@@ -248,14 +226,14 @@ function checkAskPatterns(command: string): Decision | undefined {
 41   return undefined;
 42 }
 43 
 44-function checkConfigAllow(command: string): Decision | undefined {
 45+function checkAllow(command: string): Decision | undefined {
 46   const cmd = command.trim();
 47   if (!cmd || hasShellControl(cmd)) return undefined;
 48-  for (const pat of CONFIG_ALLOW_PATTERNS) {
 49-    if (pat.test(cmd)) {
 50+  for (const pattern of ALLOW_PATTERNS) {
 51+    if (pattern.test(cmd)) {
 52       return {
 53         decision: "allow",
 54-        reason: `Matched config pattern: ${pat.source.slice(0, 50)}`,
 55+        reason: `Matched allow pattern: ${pattern.source.slice(0, 50)}`,
 56         category: "config_allow",
 57       };
 58     }
 59@@ -270,13 +248,7 @@ function checkBash(command: string): Decision | undefined {
 60   // decisions apply to the proxied command. Expand `$HOME` so home-relative
 61   // paths match the same way `~` paths do.
 62   const cleaned = expandHome(stripRtkPrefix(command.replace(STDERR_REDIRECT_RE, "").replace(DEVNULL_REDIRECT_RE, "")));
 63-  return (
 64-    checkSecretPath(cleaned) ??
 65-    checkDocker(cleaned) ??
 66-    checkHardAllow(cleaned) ??
 67-    checkConfigAllow(cleaned) ??
 68-    checkAskPatterns(cleaned)
 69-  );
 70+  return checkSecretPath(cleaned) ?? checkDocker(cleaned) ?? checkAllow(cleaned) ?? checkAskPatterns(cleaned);
 71 }
 72 
 73 export function checkDeterministic(toolType: string, input: Record<string, unknown>): Decision | undefined {
 74diff --git a/src/core/rules.ts b/src/core/rules.ts
 75index 922dc10b03bcd20de191938cf86f959bc2faf2b1..dd0d4942f0c4e4323cf8ddab4cf1e17d03f95fcc 100644
 76--- a/src/core/rules.ts
 77+++ b/src/core/rules.ts
 78@@ -12,9 +12,9 @@ export const STDERR_REDIRECT_RE = /\s+2>(?:&1|\/dev\/null)\s*$/;
 79 // would otherwise trip SHELL_CONTROL_RE's `>` check.
 80 export const DEVNULL_REDIRECT_RE = /\s+>\s*\/dev\/null\s*$/;
 81 
 82-// Matched with test() on anchored patterns (equivalent to Python fullmatch).
 83-// Purely a performance optimization — these would pass LLM review anyway.
 84-export const HARD_ALLOW_PATTERNS: RegExp[] = [
 85+// Deterministic Bash allow patterns. Strict patterns are anchored; broader
 86+// command prefixes remain guarded by SHELL_CONTROL_RE.
 87+export const ALLOW_PATTERNS: RegExp[] = [
 88   // Git read-only (no mutation, no network)
 89   /^git\s+status(?:\s+--porcelain)?\s*$/,
 90   /^git\s+diff(?:\s+(--staged|--cached))?\s*$/,
 91@@ -66,14 +66,12 @@ export const HARD_ALLOW_PATTERNS: RegExp[] = [
 92   /^sed\s+-n\s+["']?\d{1,7}(?:,\d{1,7})?p(?:;\d{1,7}(?:,\d{1,7})?p)*["']?(?:\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*)?\s*$/,
 93   // find read-only on relative paths
 94   /^find\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*(?:\s+(?:-name|-iname)\s+["'][^"']+["']|\s+-type\s+(?:["']?[fdlbcps]["']?))*\s*$/,
 95-  // grep and bare rg searches on guarded local paths.
 96+  // grep searches on guarded local paths.
 97   /^grep(?:\s+-[A-Za-z]+)*\s+(?:["'][^"']+["']|[A-Za-z0-9._-]+)(?:\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*)*\s*$/,
 98-  /^rg\s+(?:["'][^"']+["']|[A-Za-z0-9._:-]+)(?:\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*)*\s*$/,
 99   // `cut -f` without a file operand reads stdin from an already-evaluated pipe.
100   /^cut\s+-f\s*\d[\d,-]*\s*$/,
101   /^cut\s+-d(?:'[^']*'|"[^"]*"|[^\s])\s+-f\s*\d[\d,-]*\s*$/,
102   /^tr\s+-d\s+(?:'[^']*'|"[^"]*"|[^\s])\s*$/,
103-  /^ps\s*$/,
104   /^lsof\s*$/,
105 
106   // Package metadata queries.
107@@ -81,8 +79,6 @@ export const HARD_ALLOW_PATTERNS: RegExp[] = [
108   /^pacman\s+-Qo\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*\s*$/,
109   /^rpm\s+-qa\s*$/,
110   /^dpkg-query\s+-W(?:\s+[A-Za-z0-9@._+:-]+)*\s*$/,
111-  /^npm\s+ls(?:\s+(?:--(?:all|json|long|parseable|global)|--depth(?:=\d+|\s+\d+)|[A-Za-z0-9@._+/-]+))*\s*$/,
112-  /^npm\s+view\s+[A-Za-z0-9@._+/-]+(?:\s+(?:version|versions|dist-tags|description|repository|license|engines|dependencies|peerDependencies|devDependencies))?(?:\s+--json)?\s*$/,
113 
114   // Local metadata and manifest inspection.
115   /^id\s+-(?:u|g)(?:\s+[A-Za-z0-9._-]+)?\s*$/,
116@@ -91,18 +87,8 @@ export const HARD_ALLOW_PATTERNS: RegExp[] = [
117   /^jar\s+tf\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*\s*$/,
118   /^getent\s+group(?:\s+[A-Za-z0-9._-]+)?\s*$/,
119 
120-  // Docker query forms only.
121-  /^docker\s+--version\s*$/,
122-  /^docker\s+manifest\s+inspect(?:\s+--verbose)?\s+[A-Za-z0-9][A-Za-z0-9._/@:-]*\s*$/,
123-  /^docker\s+compose\s+ps(?:\s+(?:-a|--all|-q|--quiet))?\s*$/,
124-  /^docker\s+compose\s+logs(?:\s+(?:--tail\s+\d{1,6}|--timestamps))?\s*$/,
125-  /^docker\s+(?:image\s+inspect|inspect)\s+[A-Za-z0-9][A-Za-z0-9._/@:-]*\s*$/,
126   /^systemctl\s+--user\s+is-(?:active|enabled)\s+[A-Za-z0-9@._-]+\s*$/,
127 
128-  // Version checks
129-  /^(node|npm|pnpm|yarn|bun|python|python3|go|cargo|rustc|java|javac)\s+(--version|-v|-V)\s*$/,
130-  /^uv\s+(--version|version)\s*$/,
131-
132   // gofmt -w on relative/project paths — formatting only, no logic change.
133   /^gofmt\s+-w(?:\s+(?!.*\/\.)(?!.*\.\.)[A-Za-z0-9][A-Za-z0-9._/-]*)+\s*$/,
134 
135@@ -112,11 +98,7 @@ export const HARD_ALLOW_PATTERNS: RegExp[] = [
136 
137   // `-n` checks syntax only; it never executes the script.
138   /^(ba)?sh\s+-n(?:\s+\S+)*\s*$/,
139-];
140 
141-// Broader prefix patterns remain guarded by SHELL_CONTROL_RE (no >, <, ${).
142-export const CONFIG_ALLOW_PATTERNS: RegExp[] = [
143-  // Tools without stricter HARD_ALLOW equivalents
144   /^echo\s/,
145   /^jq\s/,
146   /^sort(?:\s|$)/,
147@@ -135,10 +117,11 @@ export const CONFIG_ALLOW_PATTERNS: RegExp[] = [
148   /^netstat\s/,
149   /^pgrep\s/,
150   /^pg_isready(?:\s|$)/,
151-  /^ps\s/,
152+  /^ps(?:\s|$)/,
153 
154-  // Go toolchain
155+  // Go tooling
156   /^(?:TZ=\S+\s+)?go\s+(doc|env|get|list|mod|test|vet)(?:\s|$)/,
157+  /^golangci-lint(?:\s|$)/,
158 
159   // Make targets
160   /^(?:TZ=\S+\s+)?make\s+\S*check(?:\s|$)/,
161@@ -146,6 +129,7 @@ export const CONFIG_ALLOW_PATTERNS: RegExp[] = [
162 
163   // grep with recursion/include-exclude globs across relative project paths.
164   /^grep\s+(?:-[A-Za-z]+\s+)+(?:"[^"]*"|'[^']*'|\S+)(?:\s+(?:--(?:include|exclude)=\S+|(?!\/)(?!\.\.)[A-Za-z0-9*][A-Za-z0-9._*/-]*))*\s*$/,
165+  /^rg(?:\s|$)/,
166 
167   // gh pr diff is read-only.
168   /^gh\s+pr\s+diff\s+\d+(?:\s+--repo\s+\S+)?(?:\s+--\s+.+)?\s*$/,
169diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
170index f0af442c05c9dea36c10a31759898a6f859e61a8..4871132a0286d0f464d498d48971ba1f911175b7 100644
171--- a/test/core/deterministic.test.ts
172+++ b/test/core/deterministic.test.ts
173@@ -17,7 +17,7 @@ test("keeps shell execution subject to confirmation", () => {
174   expect(checkDeterministic("bash", { command: "bash script.sh -n" })).toMatchObject({ decision: "ask" });
175 });
176 
177-test("allows recognized read-only command grammars", () => {
178+test("allows recognized deterministic command grammars", () => {
179   for (const command of [
180     "command -v bun node",
181     "bun test",
182@@ -52,6 +52,9 @@ test("allows recognized read-only command grammars", () => {
183     "base64 --decode",
184     "tr -d '\\n'",
185     "cut -d: -f1,3",
186+    "golangci-lint run --fix",
187+    "rg --hidden --glob '*.ts' policy src",
188+    "rg --files --hidden",
189   ]) {
190     expect(checkDeterministic("bash", { command })).toMatchObject({ decision: "allow" });
191   }