@hank-warren/pi-auto-permissions 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -183,6 +183,25 @@ A successful result from an allowlisted tool qualifies when its `details` are `{
183
183
 
184
184
  The allowlist matches what you wrote: a bare name such as `ask_user_question` matches that tool in any namespace (`functions.ask_user_question` included), while a dotted name matches exactly. The default is an empty list.
185
185
 
186
+ ## Prompted-review evaluation log
187
+
188
+ Auto Permissions can append a private JSONL regression record whenever the guardian asks for confirmation and the user chooses Allow or Block:
189
+
190
+ ```json
191
+ {
192
+ "evaluationLog": {
193
+ "enabled": true,
194
+ "path": "./review-evals.jsonl"
195
+ }
196
+ }
197
+ ```
198
+
199
+ The path defaults to `review-evals.jsonl` beside the Auto Permissions config and resolves relative to that config. The file is created with mode `0600`.
200
+
201
+ Each record contains the collected user request, exact command, compact reviewer evidence, guardian reason, gate and session metadata, raw user choice, and both labels used for evaluation. The guardian's `actualDecision` is `ask_user`; Allow maps to `expectedDecision: "approve"`, while Block maps to `expectedDecision: "ask_user"`. Automatic-review failures are identified separately with `decisionSource: "review_failure"`.
202
+
203
+ Logging is disabled by default. Records can contain sensitive conversation text and shell commands, so keep the file private and out of repositories. Cancelled or interrupted prompts are not labeled or logged.
204
+
186
205
  ## Review display
187
206
 
188
207
  The default UI shows guardian progress in a temporary widget below the editor. Configure it with:
package/config.ts CHANGED
@@ -23,6 +23,10 @@ export interface AutoPermissionsConfig {
23
23
  projectInstructions: boolean;
24
24
  userAnswerTools: string[];
25
25
  };
26
+ evaluationLog: {
27
+ enabled: boolean;
28
+ path: string;
29
+ };
26
30
  rules: Gate[];
27
31
  ui: {
28
32
  enabled: boolean;
@@ -121,6 +125,28 @@ function resolveReviewEvidence(raw: Record<string, unknown>): AutoPermissionsCon
121
125
  };
122
126
  }
123
127
 
128
+ function resolveEvaluationLog(
129
+ raw: Record<string, unknown>,
130
+ configFilePath: string,
131
+ ): AutoPermissionsConfig["evaluationLog"] {
132
+ const defaultPath = resolve(dirname(configFilePath), "review-evals.jsonl");
133
+ if (raw.evaluationLog === undefined) return { enabled: false, path: defaultPath };
134
+ if (!raw.evaluationLog || typeof raw.evaluationLog !== "object" || Array.isArray(raw.evaluationLog)) {
135
+ throw new Error("evaluationLog must be an object");
136
+ }
137
+ const evaluationLog = raw.evaluationLog as Record<string, unknown>;
138
+ if (evaluationLog.enabled !== undefined && typeof evaluationLog.enabled !== "boolean") {
139
+ throw new Error("evaluationLog.enabled must be boolean");
140
+ }
141
+ const configuredPath = optionalString(evaluationLog.path, "evaluationLog.path");
142
+ if (!configuredPath) return { enabled: evaluationLog.enabled === true, path: defaultPath };
143
+ const expanded = configuredPath.startsWith("~/") ? join(homedir(), configuredPath.slice(2)) : configuredPath;
144
+ return {
145
+ enabled: evaluationLog.enabled === true,
146
+ path: isAbsolute(expanded) ? expanded : resolve(dirname(configFilePath), expanded),
147
+ };
148
+ }
149
+
124
150
  function resolveUi(raw: Record<string, unknown>): AutoPermissionsConfig["ui"] {
125
151
  if (raw.ui === undefined) return { enabled: true, resultDisplayMs: 2500, placement: "widget" };
126
152
  if (!raw.ui || typeof raw.ui !== "object" || Array.isArray(raw.ui)) {
@@ -172,6 +198,7 @@ export function loadAutoPermissionsConfig(path = configPath()): AutoPermissionsC
172
198
  reviewer: resolveReviewer(raw),
173
199
  systemPrompt: resolvePrompt(raw, path),
174
200
  reviewEvidence: resolveReviewEvidence(raw),
201
+ evaluationLog: resolveEvaluationLog(raw, path),
175
202
  rules: customRules,
176
203
  ui: resolveUi(raw),
177
204
  };
@@ -0,0 +1,35 @@
1
+ import { chmodSync, mkdirSync, appendFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import type { PermissionDecision, ReviewEvidenceRecord } from "./review.js";
4
+
5
+ export interface PromptEvaluationRecord {
6
+ version: 1;
7
+ timestamp: string;
8
+ sessionId: string;
9
+ cwd: string;
10
+ tool: string;
11
+ gate: {
12
+ label: string;
13
+ group: string;
14
+ };
15
+ userRequest: string;
16
+ command: string;
17
+ relevantContext: ReviewEvidenceRecord[];
18
+ actualDecision: "ask_user";
19
+ actualReason: string;
20
+ decisionSource: "guardian" | "review_failure";
21
+ userChoice: "allow" | "block";
22
+ expectedDecision: Extract<PermissionDecision, "approve" | "ask_user">;
23
+ }
24
+
25
+ export function expectedDecisionForChoice(
26
+ choice: PromptEvaluationRecord["userChoice"],
27
+ ): PromptEvaluationRecord["expectedDecision"] {
28
+ return choice === "allow" ? "approve" : "ask_user";
29
+ }
30
+
31
+ export function appendPromptEvaluation(path: string, record: PromptEvaluationRecord): void {
32
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
33
+ appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
34
+ chmodSync(path, 0o600);
35
+ }
package/index.ts CHANGED
@@ -8,6 +8,7 @@ import { randomBytes, randomUUID } from "node:crypto";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { loadAutoPermissionsConfig, type AutoPermissionsConfig } from "./config.js";
11
+ import { appendPromptEvaluation, expectedDecisionForChoice } from "./evaluation-log.js";
11
12
  import { findGates, type Gate } from "./gates.js";
12
13
  import {
13
14
  buildReviewEnvelope,
@@ -191,6 +192,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
191
192
  const allowedConventionCommands = new Set<string>();
192
193
  let trustedGroups = new Set<string>();
193
194
  let lastConfigError: string | undefined;
195
+ let lastEvaluationLogError: string | undefined;
194
196
  let sessionActive = true;
195
197
  let bashRendererRegistered = false;
196
198
  let bashRendererSourcePath: string | undefined;
@@ -519,6 +521,50 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
519
521
  }
520
522
  }
521
523
 
524
+ function logPromptEvaluation(
525
+ gate: Gate,
526
+ command: string,
527
+ detail: string,
528
+ config: AutoPermissionsConfig,
529
+ ctx: ExtensionContext,
530
+ target: ReviewTarget,
531
+ relevantContext: ReviewEvidenceRecord[],
532
+ decisionSource: "guardian" | "review_failure",
533
+ userChoice: "allow" | "block",
534
+ ): void {
535
+ if (!config.evaluationLog.enabled) return;
536
+ try {
537
+ const userRequest = relevantContext
538
+ .filter((record) => record.source === "user")
539
+ .map((record) => record.text)
540
+ .join("\n");
541
+ appendPromptEvaluation(config.evaluationLog.path, {
542
+ version: 1,
543
+ timestamp: new Date().toISOString(),
544
+ sessionId: ctx.sessionManager.getSessionId(),
545
+ cwd: ctx.cwd,
546
+ tool: target.toolName,
547
+ gate: { label: gate.label, group: gate.group },
548
+ userRequest,
549
+ command,
550
+ relevantContext,
551
+ actualDecision: "ask_user",
552
+ actualReason: detail,
553
+ decisionSource,
554
+ userChoice,
555
+ expectedDecision: expectedDecisionForChoice(userChoice),
556
+ });
557
+ lastEvaluationLogError = undefined;
558
+ } catch (error) {
559
+ const message = error instanceof Error ? error.message : String(error);
560
+ if (message !== lastEvaluationLogError) {
561
+ lastEvaluationLogError = message;
562
+ console.error(`[pi-auto-permissions] could not append evaluation log: ${message}`);
563
+ ctx.ui.notify("Auto Permissions could not append its evaluation log.", "warning");
564
+ }
565
+ }
566
+ }
567
+
522
568
  async function askUser(
523
569
  gate: Gate,
524
570
  command: string,
@@ -528,6 +574,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
528
574
  ctx: ExtensionContext,
529
575
  target: ReviewTarget,
530
576
  lifecycleSignal: AbortSignal,
577
+ decisionSource: "guardian" | "review_failure",
531
578
  ): Promise<BlockResult | undefined> {
532
579
  const lifecycleStale = () => lifecycleSignal.aborted || reviewerLifecycleController.signal !== lifecycleSignal;
533
580
  const promptSignal = signal ? AbortSignal.any([signal, lifecycleSignal]) : lifecycleSignal;
@@ -542,6 +589,13 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
542
589
  return { block: true, reason: `${gate.label} requires user approval: ${detail}` };
543
590
  }
544
591
 
592
+ const evaluationContext = config.evaluationLog.enabled
593
+ ? collectReviewEvidence(
594
+ ctx.sessionManager.buildContextEntries(),
595
+ target.toolCallId,
596
+ config.reviewEvidence.userAnswerTools,
597
+ )
598
+ : [];
545
599
  setHerdrBlocked(true, gate.label);
546
600
  try {
547
601
  let choice: string | undefined;
@@ -563,9 +617,13 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
563
617
  return cancelled();
564
618
  }
565
619
  if (choice === "Allow") {
620
+ logPromptEvaluation(gate, command, detail, config, ctx, target, evaluationContext, decisionSource, "allow");
566
621
  showReviewDisplay(ctx, config, gate, command, target, "approved", "approved by user", true);
567
622
  return undefined;
568
623
  }
624
+ if (choice === "Block") {
625
+ logPromptEvaluation(gate, command, detail, config, ctx, target, evaluationContext, decisionSource, "block");
626
+ }
569
627
  showReviewDisplay(ctx, config, gate, command, target, "blocked", "blocked by user", true);
570
628
  return { block: true, reason: "Blocked by user" };
571
629
  } finally {
@@ -628,14 +686,24 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
628
686
  reason: `Auto Permissions requested revision: ${verdict.reason}\nRevise the command and try again.`,
629
687
  };
630
688
  }
631
- return askUser(gate, command, verdict.reason, config, signal, ctx, target, lifecycleSignal);
689
+ return askUser(gate, command, verdict.reason, config, signal, ctx, target, lifecycleSignal, "guardian");
632
690
  } catch (error) {
633
691
  if (lifecycleStale() || reviewCancelled(signal)) {
634
692
  if (!lifecycleStale() && sessionActive) clearReviewDisplay(ctx, target);
635
693
  return { block: true, reason: "Auto Permissions review cancelled" };
636
694
  }
637
695
  const reason = error instanceof Error ? error.message : String(error);
638
- return askUser(gate, command, `Automatic review failed: ${reason}`, config, signal, ctx, target, lifecycleSignal);
696
+ return askUser(
697
+ gate,
698
+ command,
699
+ `Automatic review failed: ${reason}`,
700
+ config,
701
+ signal,
702
+ ctx,
703
+ target,
704
+ lifecycleSignal,
705
+ "review_failure",
706
+ );
639
707
  }
640
708
  });
641
709
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hank-warren/pi-auto-permissions",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Fork of @ogulcancelik/pi-auto-permissions with dialog-answer user evidence: context-aware Bash permissions for Pi with automated guardian review.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -40,6 +40,7 @@
40
40
  "NOTICE.md",
41
41
  "index.ts",
42
42
  "config.ts",
43
+ "evaluation-log.ts",
43
44
  "gates.ts",
44
45
  "review.ts",
45
46
  "README.md",