a2e4f27b409d479caa9d70aec80b41d1111b1858

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

Message

Check Bash commands independently and require approval for broad searches

Diff

This diff is truncated to protect this page.

  1diff --git a/AGENTS.md b/AGENTS.md
  2index 30ee8f8a9516588aab9777aa8f422dcc10d383a1..50b377c7b54386ddfdfd6055113e8f910387a232 100644
  3--- a/AGENTS.md
  4+++ b/AGENTS.md
  5@@ -16,7 +16,7 @@ Your general goal is to keep this code simple. Don't add things you're not asked
  6 
  7 ## Policy flow
  8 
  9diff --git a/README.md b/README.md
 10index 6a75e02dd865b305a14ac3836e7fddfb8e45b2cf..829855dce0379221f30aa1dd8c2e1258a81fb4bc 100644
 11--- a/README.md
 12+++ b/README.md
 13@@ -93,7 +93,7 @@ Pi uses a root-level `tools` map and `externalDirectories` list. Any tool name c
 14 
 15diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
 16index 11cf1ecff320c7ee680544ffe4bab64aab96a03b..63e4aa2c374488f346b7e70acd8004b63ce3d338 100644
 17--- a/src/core/deterministic.ts
 18+++ b/src/core/deterministic.ts
 19@@ -113,39 +113,6 @@ const SYSTEM_DIRECTORIES = [
 20   "/var",
 21 ];
 22 const MUTATIONS = new Set(["chmod", "chown", "cp", "ln", "mkdir", "mv", "rm", "rmdir", "tee", "touch"]);
 23-const READ_ONLY = new Set([
 24-  "base64",
 25-  "cat",
 26-  "command",
 27-  "cut",
 28-  "date",
 29-  "dirname",
 30-  "echo",
 31-  "file",
 32-  "find",
 33-  "getent",
 34-  "grep",
 35-  "head",
 36-  "id",
 37-  "ls",
 38-  "nl",
 39-  "printf",
 40-  "ps",
 41-  "pwd",
 42-  "readlink",
 43-  "rocm-smi",
 44-  "rg",
 45-  "sed",
 46-  "sleep",
 47-  "stat",
 48-  "strings",
 49-  "tail",
 50-  "true",
 51-  "type",
 52-  "wc",
 53-  "whereis",
 54-  "which",
 55-]);
 56 const FIND_OPERATORS = new Set(["!", "-not", "-a", "-and", "-o", "-or", "(", ")"]);
 57 const FIND_UNARY = new Set([
 58   "-depth",
 59@@ -343,7 +310,7 @@ function safeFind(args: string[]): boolean {
 60   return true;
 61 }
 62 
 63-type ArgumentRoles = { paths: string[]; review?: boolean };
 64+type ArgumentRoles = { paths: string[]; searchRoots?: string[]; review?: boolean };
 65 
 66 function searchPaths(program: string, args: string[]): ArgumentRoles {
 67   const paths: string[] = [];
 68@@ -423,6 +390,8 @@ function searchPaths(program: string, args: string[]): ArgumentRoles {
 69     "-j",
 70   ]);
 71   let expression = false;
 72+  let recursive = program === "rg";
 73+  let files = false;
 74   let options = true;
 75   for (let index = 0; index < args.length; index += 1) {
 76     const arg = args[index];
 77@@ -435,6 +404,8 @@ function searchPaths(program: string, args: string[]): ArgumentRoles {
 78       continue;
 79     }
 80     if (arg === "--pre" || arg.startsWith("--pre=")) return { paths, review: true };
 81+    if (["--recursive", "--dereference-recursive"].includes(arg)) recursive = true;
 82+    if (arg === "--files") files = true;
 83     const equals = arg.indexOf("=");
 84     const option = equals < 0 ? arg : arg.slice(0, equals);
 85     if (values.has(option)) {
 86@@ -449,6 +420,7 @@ function searchPaths(program: string, args: string[]): ArgumentRoles {
 87       for (let offset = 1; offset < arg.length; offset += 1) {
 88         const flag = `-${arg[offset]}`;
 89         if (!values.has(flag)) {
 90+          if (flag === "-r" || flag === "-R") recursive = true;
 91           if (!"nrRiIilLhHqsvwxcobazZEFGPUSuN0123456789".includes(arg[offset])) return { paths, review: true };
 92           continue;
 93         }
 94@@ -460,14 +432,18 @@ function searchPaths(program: string, args: string[]): ArgumentRoles {
 95       }
 96     }
 97   }
 98-  paths.push(...(expression || (program === "rg" && args.includes("--files")) ? positional : positional.slice(1)));
 99-  return { paths };
100+  const roots = expression || (program === "rg" && files) ? positional : positional.slice(1);
101+  paths.push(...roots);
102+  return { paths, searchRoots: recursive ? (roots.length ? roots : ["."]) : undefined };
103 }
104 
105 function argumentRoles(program: string, args: string[]): ArgumentRoles {
106-  if (["echo", "printf", "cd", "dirname", "tr"].includes(program)) return { paths: [] };
107+  if (["echo", "printf", "dirname", "tr"].includes(program)) return { paths: [] };
108   if (program === "grep" || program === "rg") return searchPaths(program, args);
109-  if (program === "find") return { paths: findRoots(args) };
110+  if (program === "find") {
111+    const roots = findRoots(args);
112+    return { paths: roots, searchRoots: roots.length ? roots : ["."] };
113+  }
114   if (program === "sed") return { paths: args.slice(2) };
115   if (program === "jq") {
116     const paths: string[] = [];
117@@ -617,38 +593,28 @@ function specialAllow(program: string, args: string[]): boolean {
118   return program === "systemctl" && args[0] === "--user" && ["is-active", "is-enabled"].includes(args[1]);
119diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
120index 5fbd63e7c77fb3265976c61bff01070f1825122a..82e7874cd5cfaa4a0d046379d6ff81c19305924a 100644
121--- a/test/core/deterministic.test.ts
122+++ b/test/core/deterministic.test.ts
123@@ -30,10 +30,14 @@ beforeAll(() => {
124   symlinkSync(join(cwd, "credentials/missing"), join(cwd, "future.swift"));
125   symlinkSync("/etc/policy-engine-missing", join(cwd, "system-output"));
126   symlinkSync("cycle", join(cwd, "cycle"));
127+  symlinkSync("/", join(cwd, "root-link"));
128+  symlinkSync(process.env.HOME!, join(cwd, "home-link"));
129 });
130 afterAll(() => rmSync(cwd, { recursive: true, force: true }));
131 
132 const allowed = [
133+  "cd safe",
134+  "cd -- safe",
135   "bash -n script.sh",
136   "command -v bun node",
137   'grep -rn "namespace\\|Namespace" README.md',
138@@ -43,16 +47,36 @@ const allowed = [
139   "bun test",
140   "sed -n '10,20p' src/core/deterministic.ts",
141   "find src -type f -name '*.ts'",
142+  "find . -iname '*.md'",
143+  "find -iname '*.md'",
144+  "find ~/dev -name '*.md'",
145+  "rg --files safe",
146+  "rg needle",
147+  "grep -rn needle safe",
148+  "grep -r needle",
149+  "rg -e / safe",
150+  "rg --glob / needle safe",
151   "docker compose ps --all",
152   "systemctl --user is-active pi.service",
153   "echo value > output.txt",
154   "cat README.md 2>/dev/null",
155   "ls safe/Sources/*.swift",
156-  "cd safe && wc -l Sources/*.swift",
157+  "cd safe && wc -l safe/Sources/*.swift",
158+  "cd safe; cat safe/Sources/*.swift",
159+  "cd safe || cat safe/Sources/*.swift",
160+  "touch safe/Sources/new.swift; cat safe/Sources/*.swift",
161+  "echo value > out | cat safe/Sources/*.swift",
162+  "printf -v LABEL example; ls output/",
163+  "cd /home/pavle/dev/kaiwari/pixel/comfyui && find user -type f | head -20; echo ---; ls output/",
164+  "cd /mnt/media/documents/mama_prevod && sed -n '85,100p' drugi_rad_raw.txt; echo \"=== doc3 60-130 ===\"; sed -n '60,130p' treci_rad_raw.txt | grep -n -i -E \"abstract|key word|©|Received|Accepted\"",
165   "echo text | wc -c",
166+  "(cd safe && ls Sources/); ls output/",
167   'rocm-smi --showmemuse 2>&1; echo ---; ps aux | grep -E "comfyui|main.py" | grep -v grep',
168   "ls -la safe && ls safe/Sources 2>/dev/null | head -30",
169   'ls -la /mnt/media/documents/mama_prevod/sr/ && echo "---FRONT---" && cat /mnt/media/documents/mama_prevod/sr/01_front.txt 2>/dev/null || echo "no front file"',
170+  'rg -n "arecord|WAV|wav|Rate|Bits|bit" safe/Sources/*.swift 2>/dev/null || ls safe/Sources/; rg -rn "arecord" safe --type swift',
171+  "git -C /home/pavle/dev/kaiwari/server status --short && git -C /home/pavle/dev/kaiwari/server diff && git -C /home/pavle/dev/kaiwari/server branch --show-current",
172+  'cd /home/pavle/dev/kaiwari/ios && find . -name "*.png" -not -path "./.build/*" 2>/dev/null; echo "---all pngs incl build---"; find . -name "*.png" 2>/dev/null | head; echo "---git tracked files---"; git ls-files | grep -iE "png|icon"',
173 ];
174 
175 test.each(allowed)("deterministically allows %s", async (command) => {
176@@ -61,6 +85,12 @@ test.each(allowed)("deterministically allows %s", async (command) => {
177 
178 const asks = [
179   "sudo id",
180+  "cd ~/.ssh",
181+  "cd ~/.ssh && cat config",
182+  "cd -- credentials",
183+  "cd shortcut",
184+  "cd cred*",
185+  "cd safe/../credentials",
186   "cat .env",
187   'cat .e"nv"',
188   "cat ~/.ssh/id_ed25519",
189@@ -81,7 +111,6 @@ const asks = [
190   "herdr server stop",
191   "echo value > .env",
192   "cat < .env",
193-  "find ~ -name '*.ts'",
194   "docker volume rm app-data",
195   "docker compose down -v",
196   "kubectl --namespace default apply -f deploy.yaml",
197@@ -93,22 +122,60 @@ const asks = [
198   "aws s3api put-object --bucket production --key file --body file",
199   "aws configure set region us-east-1",
200   "wc -l mixed/*.swift",
201-  "cd injected && grep -r needle --include=*.go .",
202   "cat safe/*.swift",
203   "cat shortcut/../data.txt",
204   "cat dangling/*.swift",
205   "cat future.swift",
206   "touch system-output",
207-  "cd /etc && touch policy-engine",
208-  "cd options && grep pattern *",
209+  "cd safe; touch /etc/policy-engine",
210   "command -v node; rm -rf /",
211+  "(echo ok; cat .env) | head -20",
212 ];
213 
214 test.each(asks)("requires approval for %s", async (command) => {
215   expect(await checkDeterministic("bash", { command }, cwd)).toMatchObject({ decision: "ask" });
216 });
217 
218+test.each([
219+  'find / -iname "*.md" 2>/dev/null',
220+  "find ~ -name '*.ts'",
221+  'find "$HOME" -name "*.md"',
222+  "find /tmp/.. -name '*.md'",