9be7f142c8d2ddde36917048195af431bcc1f522

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

Message

report Bash commands requiring review

Diff

This diff is truncated to protect this page.

  1diff --git a/src/core/deterministic.ts b/src/core/deterministic.ts
  2index f07a33a64e51fc51fbfa361a531b569054da6d7c..9ba9b64a244be231a8a531e31e9c7637c84edfee 100644
  3--- a/src/core/deterministic.ts
  4+++ b/src/core/deterministic.ts
  5@@ -65,6 +65,7 @@ export const ALLOW_COMMANDS: ReadonlySet<string> = new Set([
  6   "python3",
  7   "qmd",
  8   "readlink",
  9+  "rocm-smi",
 10   "rg",
 11   "rmdir",
 12   "rm",
 13@@ -129,8 +130,10 @@ const READ_ONLY = new Set([
 14   "ls",
 15   "nl",
 16   "printf",
 17+  "ps",
 18   "pwd",
 19   "readlink",
 20+  "rocm-smi",
 21   "rg",
 22   "sed",
 23   "sleep",
 24@@ -615,10 +618,20 @@ function specialAllow(program: string, args: string[]): boolean {
 25 }
 26 
 27 type ShellContext = { cwd: string; certain: boolean };
 28-type CommandResult = { decision?: Decision; context: ShellContext; changesDirectory?: boolean };
 29+type CommandResult = {
 30+  decision?: Decision;
 31+  context: ShellContext;
 32+  changesDirectory?: boolean;
 33+  reviewCommands?: string[];
 34+};
 35+
 36+export type BashDeterministicResult = Pick<CommandResult, "decision" | "reviewCommands">;
 37 
 38 function checkCommand(command: BashCommand, context: ShellContext): CommandResult {
 39-  const unknown: CommandResult = { context: { ...context, certain: false } };
 40+  const unknown: CommandResult = {
 41+    context: { ...context, certain: false },
 42+    reviewCommands: [command.source],
 43+  };
 44   const literalWords = command.words.map((word) => word.text);
 45   if (literalWords[0] === "rtk") literalWords.shift();
 46   const literalDanger = dangerousCommand(literalWords[0], literalWords.slice(1));
 47@@ -750,6 +763,11 @@ function checkCommand(command: BashCommand, context: ShellContext): CommandResul
 48   return { decision, context: { ...context, certain: !writes && readOnly } };
 49 }
 50 
 51+function combinedReviewCommands(left: CommandResult, right: CommandResult): string[] | undefined {
 52+  const commands = [...(left.reviewCommands ?? []), ...(right.reviewCommands ?? [])];
 53+  return commands.length ? commands : undefined;
 54+}
 55+
 56 function combine(left: Decision | undefined, right: Decision | undefined): Decision | undefined {
 57   if (left && left.decision !== "allow") return left;
 58   if (right && right.decision !== "allow") return right;
 59@@ -761,13 +779,21 @@ function evaluateNode(node: BashNode, context: ShellContext): CommandResult {
 60   if (node.kind === "command") return checkCommand(node.command, context);
 61   if (node.kind === "scope") {
 62     const result = evaluateNode(node.body, { ...context });
 63-    return { decision: result.decision, context: { ...context, certain: result.context.certain } };
 64+    return {
 65+      decision: result.decision,
 66+      context: { ...context, certain: result.context.certain },
 67+      reviewCommands: result.reviewCommands,
 68+    };
 69   }
 70   const left = evaluateNode(node.left, { ...context });
 71   if (node.operator === "||") {
 72     const right = evaluateNode(node.right, { ...context, certain: false });
 73     const decision = combine(left.decision, right.decision);
 74-    return { decision: decision?.decision === "allow" ? undefined : decision, context: { ...context, certain: false } };
 75+    return {
 76+      decision: decision?.decision === "allow" ? undefined : decision,
 77+      context: { ...context, certain: false },
 78+      reviewCommands: combinedReviewCommands(left, right),
 79+    };
 80   }
 81   const next =
 82     node.operator === "|"
 83@@ -783,20 +809,26 @@ function evaluateNode(node: BashNode, context: ShellContext): CommandResult {
 84     context:
 85       node.operator === "|" ? { ...context, certain: left.context.certain && right.context.certain } : right.context,
 86     changesDirectory: node.operator !== "|" && (left.changesDirectory || right.changesDirectory),
 87+    reviewCommands: combinedReviewCommands(left, right),
 88   };
 89 }
 90 
 91-export function checkParsedBash(parsed: BashParseResult, cwd = process.cwd()): Decision | undefined {
 92-  if (parsed.parserUnavailable) return { decision: "deny", reason: "Bash parser is unavailable", category: "bash" };
 93-  if (!parsed.parsed) return undefined;
 94-  if (!parsed.root) return { decision: "allow", reason: "No operations to evaluate", category: "empty" };
 95+export function evaluateParsedBash(parsed: BashParseResult, cwd = process.cwd()): BashDeterministicResult {
 96+  if (parsed.parserUnavailable) return { decision: { decision: "deny", reason: "Bash parser is unavailable", category: "bash" } };
 97+  if (!parsed.parsed) return {};
 98+  if (!parsed.root) return { decision: { decision: "allow", reason: "No operations to evaluate", category: "empty" } };
 99   try {
100-    return evaluateNode(parsed.root, { cwd, certain: true }).decision;
101+    const result = evaluateNode(parsed.root, { cwd, certain: true });
102+    return { decision: result.decision, reviewCommands: result.reviewCommands };
103   } catch {
104-    return undefined;
105diff --git a/src/pi/index.ts b/src/pi/index.ts
106index deb0da960133d9ef73331dd8eb35d6ae93916805..b8effc8859c438731447cfe2a89e0ff1dd5e324a 100644
107--- a/src/pi/index.ts
108+++ b/src/pi/index.ts
109@@ -3,7 +3,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
110 import { createJsonlDecisionAudit } from "../core/audit";
111 import { createJsonlDecisionCache } from "../core/cache";
112 import { parseBash, type BashParseResult } from "../core/bash";
113-import { checkDeterministic, checkParsedBash } from "../core/deterministic";
114+import { checkDeterministic, evaluateParsedBash } from "../core/deterministic";
115 import { auditInputSummary, cacheKey, normalizeRequest } from "../core/normalize";
116 import { createPolicyPipeline } from "../core/pipeline";
117 import { createPolicyReviewer, type PiReviewerCaller } from "../core/review";
118@@ -41,8 +41,11 @@ function approvalMessage(toolName: string, input: ToolInput, result: PolicyEvalu
119 
120   const commands = result.approvalRequests
121     ?.map((request) => request.input.command)
122-    .filter((command): command is string => typeof command === "string") ?? [input.command];
123-  return `Entire Bash input:\n${input.command}\n\nCommands requiring review:\n${commands.map((command) => `- ${command}`).join("\n")}\n\n${result.decision.reason}${rawResponse}`;
124+    .filter((command): command is string => typeof command === "string");
125+  const requiringReview = commands?.length
126+    ? `\n\nCommands requiring review:\n${commands.map((command) => `- ${command}`).join("\n")}`
127+    : "";
128+  return `Entire Bash input:\n${input.command}${requiringReview}\n\n${result.decision.reason}${rawResponse}`;
129 }
130 
131 function piReviewerCaller(ctx: ExtensionContext): PiReviewerCaller {
132@@ -92,6 +95,7 @@ function createPiPipeline(
133   cwd: string,
134   ctx: ExtensionContext,
135   parsedBash: WeakMap<object, BashParseResult>,
136+  bashReviewCommands: WeakMap<object, string[]>,
137 ) {
138   const config = loadPiPolicyConfig(cwd);
139   const paths = piPolicyPaths();
140@@ -120,9 +124,10 @@ function createPiPipeline(
141     ],
142     deterministic: (request, context) => {
143       const parsed = parsedBash.get(request.input);
144-      return parsed
145-        ? checkParsedBash(parsed, context.cwd ?? cwd)
146-        : checkDeterministic(request.toolName, request.input, context.cwd ?? cwd);
147+      if (!parsed) return checkDeterministic(request.toolName, request.input, context.cwd ?? cwd);
148+      const result = evaluateParsedBash(parsed, context.cwd ?? cwd);
149+      if (result.reviewCommands) bashReviewCommands.set(request.input, result.reviewCommands);
150+      return result.decision;
151     },
152     cache: createJsonlDecisionCache(paths.cacheFile),
153     audit: createJsonlDecisionAudit(paths.auditFile),
154@@ -144,7 +149,14 @@ export async function evaluatePiToolCall(
155 ): Promise<PolicyEvaluation> {
156   if (!ctx) throw new Error("Pi extension context is required");
157   const parsedBash = new WeakMap<object, BashParseResult>();
158-  const pipeline = createPiPipeline(sessionBashAllowOverride, context.cwd ?? process.cwd(), ctx, parsedBash);
159+  const bashReviewCommands = new WeakMap<object, string[]>();
160+  const pipeline = createPiPipeline(
161+    sessionBashAllowOverride,
162+    context.cwd ?? process.cwd(),
163+    ctx,
164+    parsedBash,
165+    bashReviewCommands,
166+  );
167   const evaluationOptions = {
168     skipPrechecks: true,
169     skipLLMReview: skipPermissions,
170@@ -170,7 +182,14 @@ export async function evaluatePiToolCall(
171   }
172 
173   parsedBash.set(input, parsed);
174-  return pipeline.evaluate(request, context, evaluationOptions);
175+  const result = await pipeline.evaluate(request, context, evaluationOptions);
176+  const reviewCommands = bashReviewCommands.get(input);
177+  return reviewCommands?.length
178+    ? {
179+        ...result,
180+        approvalRequests: reviewCommands.map((command) => ({ toolName, input: { ...input, command } })),
181+      }
182+    : result;
183 }
184 
185 export default function policyEngine(pi: ExtensionAPI) {
186diff --git a/test/core/deterministic.test.ts b/test/core/deterministic.test.ts
187index 232286fe2181215ff4518ab54e1ab0d3d4ee69fe..29cd0b8006b23ba5b091c9ebede1bc1864e708a5 100644
188--- a/test/core/deterministic.test.ts
189+++ b/test/core/deterministic.test.ts
190@@ -50,6 +50,7 @@ const allowed = [
191   "ls safe/Sources/*.swift",
192   "cd safe && wc -l Sources/*.swift",
193   "echo text | wc -c",
194+  'rocm-smi --showmemuse 2>&1; echo ---; ps aux | grep -E "comfyui|main.py" | grep -v grep',
195   "ls -la safe && ls safe/Sources 2>/dev/null | head -30",
196 ];
197