96c55fbe1e173af984c019d569ccc4171ac64245

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

Message

Show reviewed Bash commands in approval prompt

Diff

  1diff --git a/src/core/pipeline.ts b/src/core/pipeline.ts
  2index 8d54065e6a4cc82142bed48298ffe0c2cb055467..eb753e9eb56fe6ab26ca19dfd24505d4a7c5a2d5 100644
  3--- a/src/core/pipeline.ts
  4+++ b/src/core/pipeline.ts
  5@@ -141,13 +141,16 @@ export function createPolicyPipeline(options: PolicyPipelineOptions) {
  6     if (requests.length === 0) return { decision: EMPTY_DECISION, source: "deterministic" }
  7 
  8     let worst: PolicyEvaluation | undefined
  9+    const approvalRequests: PolicyRequest[] = []
 10     for (const request of requests) {
 11       const result = await evaluate(request, context, evaluationOptions)
 12       if (result.decision.decision === "deny") return result
 13-      if (result.decision.decision === "ask") worst = result
 14-      else if (!worst) worst = result
 15+      if (result.decision.decision === "ask") {
 16+        worst = result
 17+        approvalRequests.push(request)
 18+      } else if (!worst) worst = result
 19     }
 20-    return worst!
 21+    return approvalRequests.length > 0 ? { ...worst!, approvalRequests } : worst!
 22   }
 23 
 24   return { precheck, evaluate, evaluateMany }
 25diff --git a/src/core/types.ts b/src/core/types.ts
 26index 384abbbaf31f22532354889e0576dea0781d6dfc..5d84d31b0ef779a8c584a61fee2e878fccae14c2 100644
 27--- a/src/core/types.ts
 28+++ b/src/core/types.ts
 29@@ -110,4 +110,5 @@ export type PolicyEvaluation = {
 30   source: DecisionSource
 31   rawResponse?: string
 32   error?: string
 33+  approvalRequests?: PolicyRequest[]
 34 }
 35diff --git a/src/pi/index.ts b/src/pi/index.ts
 36index 7b60cb0492ac072d1f9de708ff70c891b377af77..eaba2f6d0569172544d56f540898cdd559ffeb5e 100644
 37--- a/src/pi/index.ts
 38+++ b/src/pi/index.ts
 39@@ -32,6 +32,21 @@ function inputSummary(toolName: string, input: ToolInput): string {
 40   return JSON.stringify(input).slice(0, 500)
 41 }
 42 
 43+function approvalMessage(toolName: string, input: ToolInput, result: PolicyEvaluation): string {
 44+  const rawResponse = result.error === "Failed to parse response JSON" && result.rawResponse
 45+    ? `\n\nLLM response:\n${result.rawResponse}`
 46+    : ""
 47+  if (toolName !== "bash" || typeof input.command !== "string") {
 48+    return `${toolName}: ${inputSummary(toolName, input)}\n\n${result.decision.reason}${rawResponse}`
 49+  }
 50+
 51+  const commands = result.approvalRequests
 52+    ?.map((request) => request.input.command)
 53+    .filter((command): command is string => typeof command === "string")
 54+    ?? [input.command]
 55+  return `Entire Bash input:\n${input.command}\n\nCommands requiring review:\n${commands.map((command) => `- ${command}`).join("\n")}\n\n${result.decision.reason}${rawResponse}`
 56+}
 57+
 58 function bypassesPiPolicy(toolName: string): boolean {
 59   return toolName === "mcp" || toolName.startsWith("mcp__")
 60 }
 61@@ -197,10 +212,7 @@ export default function policyEngine(pi: ExtensionAPI) {
 62     let approved: boolean
 63     try {
 64       const title = "Policy approval required"
 65-      const rawResponse = result.error === "Failed to parse response JSON" && result.rawResponse
 66-        ? `\n\nLLM response:\n${result.rawResponse}`
 67-        : ""
 68-      const message = `${event.toolName}: ${inputSummary(event.toolName, input)}\n\n${result.decision.reason}${rawResponse}`
 69+      const message = approvalMessage(event.toolName, input, result)
 70       approved = ctx.mode === "tui"
 71         ? await ctx.ui.custom(
 72           (tui, theme, keybindings, done) => new PolicyApprovalDialog(tui, theme, keybindings, title, message, done),
 73diff --git a/test/pi/extension.test.ts b/test/pi/extension.test.ts
 74index d885484fc12636cef1a1939228ce88b7af37d992..2e2c22e7174b3e7d8c54be4b31e381a17d8f9d8c 100644
 75--- a/test/pi/extension.test.ts
 76+++ b/test/pi/extension.test.ts
 77@@ -125,6 +125,23 @@ describe("Pi policy extension", () => {
 78     expect(message).toContain("LLM response:\nnot JSON")
 79   })
 80 
 81+  test("shows the full Bash input and commands requiring review", async () => {
 82+    let message = ""
 83+    const extension = loadExtension()
 84+    await extension.toolCall(
 85+      { toolName: "bash", input: { command: "git status; sudo id; pwd" } },
 86+      context(true, async (_title: string, value: string) => {
 87+        message = value
 88+        return false
 89+      }),
 90+    )
 91+
 92+    expect(message).toContain("Entire Bash input:\ngit status; sudo id; pwd")
 93+    expect(message).toContain("Commands requiring review:\n- sudo id")
 94+    expect(message).not.toContain("- git status")
 95+    expect(message).not.toContain("- pwd")
 96+  })
 97+
 98   test("allows base64 from a pipe", async () => {
 99     let reviews = 0
100     globalThis.fetch = (async (_input, _init) => {