@hank-warren/pi-auto-permissions 0.1.0 → 0.1.2
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/NOTICE.md +1 -1
- package/README.md +27 -0
- package/config.ts +27 -0
- package/evaluation-log.ts +72 -0
- package/index.ts +88 -4
- package/package.json +2 -1
package/NOTICE.md
CHANGED
|
@@ -4,4 +4,4 @@ This package is a fork of [`@ogulcancelik/pi-auto-permissions`](https://github.c
|
|
|
4
4
|
|
|
5
5
|
The original work is Copyright (c) 2026 Can Celik and is used under the MIT License included in [`LICENSE`](LICENSE).
|
|
6
6
|
|
|
7
|
-
Fork-specific changes (the `reviewEvidence.userAnswerTools` dialog-answer evidence feature, proposed upstream in [ogulcancelik/pi-extensions#24](https://github.com/ogulcancelik/pi-extensions/issues/24) / [PR #25](https://github.com/ogulcancelik/pi-extensions/pull/25)
|
|
7
|
+
Fork-specific changes are Copyright (c) 2026 Hank Warren and are released under the same MIT License. They include the `reviewEvidence.userAnswerTools` dialog-answer evidence feature, proposed upstream in [ogulcancelik/pi-extensions#24](https://github.com/ogulcancelik/pi-extensions/issues/24) / [PR #25](https://github.com/ogulcancelik/pi-extensions/pull/25), and the prompted-review evaluation log with explicit user feedback.
|
package/README.md
CHANGED
|
@@ -183,6 +183,33 @@ 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 gives explicit feedback:
|
|
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
|
+
When logging is enabled, prompted reviews offer three choices:
|
|
202
|
+
|
|
203
|
+
- **Allow — asking was unnecessary** executes the command and records `userChoice: "allow_unnecessary"` with `expectedDecision: "approve"`.
|
|
204
|
+
- **Block** remains the second choice, blocks the command, and records `userChoice: "block"` with `expectedDecision: "ask_user"`.
|
|
205
|
+
- **Allow — asking was appropriate** executes the command and records `userChoice: "allow_appropriate"` with `expectedDecision: "ask_user"`.
|
|
206
|
+
|
|
207
|
+
When logging is disabled, the prompt retains the normal **Allow** and **Block** choices. Prompts from Pi or other extensions are unchanged.
|
|
208
|
+
|
|
209
|
+
Each version 2 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`. Automatic-review failures are identified separately with `decisionSource: "review_failure"`. Existing version 1 records can remain in the same JSONL file.
|
|
210
|
+
|
|
211
|
+
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.
|
|
212
|
+
|
|
186
213
|
## Review display
|
|
187
214
|
|
|
188
215
|
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,72 @@
|
|
|
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 const PROMPT_FEEDBACK_OPTIONS = {
|
|
6
|
+
allowUnnecessary: "Allow — asking was unnecessary",
|
|
7
|
+
allowAppropriate: "Allow — asking was appropriate",
|
|
8
|
+
block: "Block",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type PromptEvaluationUserChoice = "allow_unnecessary" | "allow_appropriate" | "block";
|
|
12
|
+
|
|
13
|
+
export interface PromptChoiceClassification {
|
|
14
|
+
allowsExecution: boolean;
|
|
15
|
+
userChoice?: PromptEvaluationUserChoice;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function permissionPromptOptions(evaluationLoggingEnabled: boolean): string[] {
|
|
19
|
+
return evaluationLoggingEnabled
|
|
20
|
+
? [
|
|
21
|
+
PROMPT_FEEDBACK_OPTIONS.allowUnnecessary,
|
|
22
|
+
PROMPT_FEEDBACK_OPTIONS.block,
|
|
23
|
+
PROMPT_FEEDBACK_OPTIONS.allowAppropriate,
|
|
24
|
+
]
|
|
25
|
+
: ["Allow", "Block"];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PromptEvaluationRecord {
|
|
29
|
+
version: 2;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
tool: string;
|
|
34
|
+
gate: {
|
|
35
|
+
label: string;
|
|
36
|
+
group: string;
|
|
37
|
+
};
|
|
38
|
+
userRequest: string;
|
|
39
|
+
command: string;
|
|
40
|
+
relevantContext: ReviewEvidenceRecord[];
|
|
41
|
+
actualDecision: "ask_user";
|
|
42
|
+
actualReason: string;
|
|
43
|
+
decisionSource: "guardian" | "review_failure";
|
|
44
|
+
userChoice: PromptEvaluationUserChoice;
|
|
45
|
+
expectedDecision: Extract<PermissionDecision, "approve" | "ask_user">;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function classifyPromptChoice(choice: string | undefined): PromptChoiceClassification | undefined {
|
|
49
|
+
if (choice === "Allow") return { allowsExecution: true };
|
|
50
|
+
if (choice === PROMPT_FEEDBACK_OPTIONS.allowUnnecessary) {
|
|
51
|
+
return { allowsExecution: true, userChoice: "allow_unnecessary" };
|
|
52
|
+
}
|
|
53
|
+
if (choice === PROMPT_FEEDBACK_OPTIONS.allowAppropriate) {
|
|
54
|
+
return { allowsExecution: true, userChoice: "allow_appropriate" };
|
|
55
|
+
}
|
|
56
|
+
if (choice === PROMPT_FEEDBACK_OPTIONS.block) {
|
|
57
|
+
return { allowsExecution: false, userChoice: "block" };
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function expectedDecisionForChoice(
|
|
63
|
+
choice: PromptEvaluationRecord["userChoice"],
|
|
64
|
+
): PromptEvaluationRecord["expectedDecision"] {
|
|
65
|
+
return choice === "allow_unnecessary" ? "approve" : "ask_user";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function appendPromptEvaluation(path: string, record: PromptEvaluationRecord): void {
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
70
|
+
appendFileSync(path, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
71
|
+
chmodSync(path, 0o600);
|
|
72
|
+
}
|
package/index.ts
CHANGED
|
@@ -8,6 +8,13 @@ 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 {
|
|
12
|
+
appendPromptEvaluation,
|
|
13
|
+
classifyPromptChoice,
|
|
14
|
+
expectedDecisionForChoice,
|
|
15
|
+
permissionPromptOptions,
|
|
16
|
+
type PromptEvaluationUserChoice,
|
|
17
|
+
} from "./evaluation-log.js";
|
|
11
18
|
import { findGates, type Gate } from "./gates.js";
|
|
12
19
|
import {
|
|
13
20
|
buildReviewEnvelope,
|
|
@@ -191,6 +198,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
191
198
|
const allowedConventionCommands = new Set<string>();
|
|
192
199
|
let trustedGroups = new Set<string>();
|
|
193
200
|
let lastConfigError: string | undefined;
|
|
201
|
+
let lastEvaluationLogError: string | undefined;
|
|
194
202
|
let sessionActive = true;
|
|
195
203
|
let bashRendererRegistered = false;
|
|
196
204
|
let bashRendererSourcePath: string | undefined;
|
|
@@ -519,6 +527,50 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
519
527
|
}
|
|
520
528
|
}
|
|
521
529
|
|
|
530
|
+
function logPromptEvaluation(
|
|
531
|
+
gate: Gate,
|
|
532
|
+
command: string,
|
|
533
|
+
detail: string,
|
|
534
|
+
config: AutoPermissionsConfig,
|
|
535
|
+
ctx: ExtensionContext,
|
|
536
|
+
target: ReviewTarget,
|
|
537
|
+
relevantContext: ReviewEvidenceRecord[],
|
|
538
|
+
decisionSource: "guardian" | "review_failure",
|
|
539
|
+
userChoice: PromptEvaluationUserChoice,
|
|
540
|
+
): void {
|
|
541
|
+
if (!config.evaluationLog.enabled) return;
|
|
542
|
+
try {
|
|
543
|
+
const userRequest = relevantContext
|
|
544
|
+
.filter((record) => record.source === "user")
|
|
545
|
+
.map((record) => record.text)
|
|
546
|
+
.join("\n");
|
|
547
|
+
appendPromptEvaluation(config.evaluationLog.path, {
|
|
548
|
+
version: 2,
|
|
549
|
+
timestamp: new Date().toISOString(),
|
|
550
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
551
|
+
cwd: ctx.cwd,
|
|
552
|
+
tool: target.toolName,
|
|
553
|
+
gate: { label: gate.label, group: gate.group },
|
|
554
|
+
userRequest,
|
|
555
|
+
command,
|
|
556
|
+
relevantContext,
|
|
557
|
+
actualDecision: "ask_user",
|
|
558
|
+
actualReason: detail,
|
|
559
|
+
decisionSource,
|
|
560
|
+
userChoice,
|
|
561
|
+
expectedDecision: expectedDecisionForChoice(userChoice),
|
|
562
|
+
});
|
|
563
|
+
lastEvaluationLogError = undefined;
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
566
|
+
if (message !== lastEvaluationLogError) {
|
|
567
|
+
lastEvaluationLogError = message;
|
|
568
|
+
console.error(`[pi-auto-permissions] could not append evaluation log: ${message}`);
|
|
569
|
+
ctx.ui.notify("Auto Permissions could not append its evaluation log.", "warning");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
522
574
|
async function askUser(
|
|
523
575
|
gate: Gate,
|
|
524
576
|
command: string,
|
|
@@ -528,6 +580,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
528
580
|
ctx: ExtensionContext,
|
|
529
581
|
target: ReviewTarget,
|
|
530
582
|
lifecycleSignal: AbortSignal,
|
|
583
|
+
decisionSource: "guardian" | "review_failure",
|
|
531
584
|
): Promise<BlockResult | undefined> {
|
|
532
585
|
const lifecycleStale = () => lifecycleSignal.aborted || reviewerLifecycleController.signal !== lifecycleSignal;
|
|
533
586
|
const promptSignal = signal ? AbortSignal.any([signal, lifecycleSignal]) : lifecycleSignal;
|
|
@@ -542,13 +595,20 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
542
595
|
return { block: true, reason: `${gate.label} requires user approval: ${detail}` };
|
|
543
596
|
}
|
|
544
597
|
|
|
598
|
+
const evaluationContext = config.evaluationLog.enabled
|
|
599
|
+
? collectReviewEvidence(
|
|
600
|
+
ctx.sessionManager.buildContextEntries(),
|
|
601
|
+
target.toolCallId,
|
|
602
|
+
config.reviewEvidence.userAnswerTools,
|
|
603
|
+
)
|
|
604
|
+
: [];
|
|
545
605
|
setHerdrBlocked(true, gate.label);
|
|
546
606
|
try {
|
|
547
607
|
let choice: string | undefined;
|
|
548
608
|
try {
|
|
549
609
|
choice = await ctx.ui.select(
|
|
550
610
|
`${gate.label} — Auto Permissions needs approval\n\n${detail}\n\n${command}`,
|
|
551
|
-
|
|
611
|
+
permissionPromptOptions(config.evaluationLog.enabled),
|
|
552
612
|
{ signal: promptSignal },
|
|
553
613
|
);
|
|
554
614
|
} catch (error) {
|
|
@@ -562,7 +622,21 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
562
622
|
if (sessionActive) clearReviewDisplay(ctx, target);
|
|
563
623
|
return cancelled();
|
|
564
624
|
}
|
|
565
|
-
|
|
625
|
+
const classification = classifyPromptChoice(choice);
|
|
626
|
+
if (config.evaluationLog.enabled && classification?.userChoice) {
|
|
627
|
+
logPromptEvaluation(
|
|
628
|
+
gate,
|
|
629
|
+
command,
|
|
630
|
+
detail,
|
|
631
|
+
config,
|
|
632
|
+
ctx,
|
|
633
|
+
target,
|
|
634
|
+
evaluationContext,
|
|
635
|
+
decisionSource,
|
|
636
|
+
classification.userChoice,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (classification?.allowsExecution) {
|
|
566
640
|
showReviewDisplay(ctx, config, gate, command, target, "approved", "approved by user", true);
|
|
567
641
|
return undefined;
|
|
568
642
|
}
|
|
@@ -628,14 +702,24 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
628
702
|
reason: `Auto Permissions requested revision: ${verdict.reason}\nRevise the command and try again.`,
|
|
629
703
|
};
|
|
630
704
|
}
|
|
631
|
-
return askUser(gate, command, verdict.reason, config, signal, ctx, target, lifecycleSignal);
|
|
705
|
+
return askUser(gate, command, verdict.reason, config, signal, ctx, target, lifecycleSignal, "guardian");
|
|
632
706
|
} catch (error) {
|
|
633
707
|
if (lifecycleStale() || reviewCancelled(signal)) {
|
|
634
708
|
if (!lifecycleStale() && sessionActive) clearReviewDisplay(ctx, target);
|
|
635
709
|
return { block: true, reason: "Auto Permissions review cancelled" };
|
|
636
710
|
}
|
|
637
711
|
const reason = error instanceof Error ? error.message : String(error);
|
|
638
|
-
return askUser(
|
|
712
|
+
return askUser(
|
|
713
|
+
gate,
|
|
714
|
+
command,
|
|
715
|
+
`Automatic review failed: ${reason}`,
|
|
716
|
+
config,
|
|
717
|
+
signal,
|
|
718
|
+
ctx,
|
|
719
|
+
target,
|
|
720
|
+
lifecycleSignal,
|
|
721
|
+
"review_failure",
|
|
722
|
+
);
|
|
639
723
|
}
|
|
640
724
|
});
|
|
641
725
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-auto-permissions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|