9be7f142c8d2ddde36917048195af431bcc1f522
- Author
- TheEdgeOfRage <git@theedgeofrage.com>
- Committer
- TheEdgeOfRage <git@theedgeofrage.com>
- Date
Message
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