@hank-warren/pi-auto-permissions 0.7.1 → 0.8.0
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/CHANGELOG.md +16 -0
- package/README.md +16 -6
- package/approval-pulse.ts +80 -0
- package/index.ts +32 -8
- package/openai-codex-provider.ts +42 -0
- package/package.json +3 -1
- package/widget-status.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @hank-warren/pi-auto-permissions
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 52ff06e: Add a dedicated `OpenAI Codex - Auto Permissions` provider with its own OAuth credential slot, allowing guardian reviews to use a separate ChatGPT account without replacing the main OpenAI Codex login.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 312da9a: Add a pulsing warning light to interactive approval prompts so they remain visually distinct from the surrounding transcript.
|
|
12
|
+
|
|
13
|
+
## 0.7.2
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 97918ec: move the review status widget above the editor so the prompt bar no longer shifts up and down as guardian status appears and disappears
|
|
18
|
+
|
|
3
19
|
## 0.7.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -114,12 +114,18 @@ Set `enabled` to `false` to disable the extension. Invalid configuration fails c
|
|
|
114
114
|
|
|
115
115
|
## Guardian configuration
|
|
116
116
|
|
|
117
|
-
By default, the guardian uses Pi's active model with low reasoning effort and a 30-second timeout. You can select a separate low-cost model
|
|
117
|
+
By default, the guardian uses Pi's active model with low reasoning effort and a 30-second timeout. You can select a separate low-cost model.
|
|
118
|
+
|
|
119
|
+
For OpenAI Codex, this package registers a second provider named **OpenAI Codex - Auto Permissions**. Pi keys OAuth credentials by provider id, so this provider can use a different ChatGPT account without replacing the normal `openai-codex` login used by your main agent.
|
|
120
|
+
|
|
121
|
+
1. Run `/login` and select **OpenAI Codex - Auto Permissions**.
|
|
122
|
+
2. Complete OAuth with the account reserved for guardian reviews.
|
|
123
|
+
3. Select the dedicated provider in the Auto Permissions config:
|
|
118
124
|
|
|
119
125
|
```json
|
|
120
126
|
{
|
|
121
127
|
"reviewer": {
|
|
122
|
-
"provider": "openai-codex",
|
|
128
|
+
"provider": "openai-codex-auto-permissions",
|
|
123
129
|
"model": "gpt-5.6-luna",
|
|
124
130
|
"reasoningEffort": "low",
|
|
125
131
|
"timeoutMs": 30000
|
|
@@ -127,6 +133,10 @@ By default, the guardian uses Pi's active model with low reasoning effort and a
|
|
|
127
133
|
}
|
|
128
134
|
```
|
|
129
135
|
|
|
136
|
+
The separate credential is stored in Pi's normal `auth.json` under `openai-codex-auto-permissions`; the existing `openai-codex` credential is unchanged. The alias uses Pi's built-in OpenAI Codex OAuth flow, model catalog, and transport. Because it is a normal provider, its models also appear under the dedicated provider name in Pi's model selector after login.
|
|
137
|
+
|
|
138
|
+
Other reviewer providers continue to work by setting their normal provider and model ids.
|
|
139
|
+
|
|
130
140
|
### Guardian prompt
|
|
131
141
|
|
|
132
142
|
The bundled prompt evaluates authorization, command risk, and compliance with user constraints. Replace it inline with `systemPrompt`, or load a file:
|
|
@@ -235,13 +245,13 @@ The path defaults to `usage.jsonl` beside the Auto Permissions config and resolv
|
|
|
235
245
|
|
|
236
246
|
## Review display
|
|
237
247
|
|
|
238
|
-
The default UI shows guardian progress as a single animated status line in a temporary widget
|
|
248
|
+
The default UI shows guardian progress as a single animated status line in a temporary widget above the editor:
|
|
239
249
|
|
|
240
250
|
```text
|
|
241
|
-
auto permissions · Git commit · ✶ waiting for openai-codex/gpt-5.6-luna
|
|
251
|
+
auto permissions · Git commit · ✶ waiting for openai-codex-auto-permissions/gpt-5.6-luna
|
|
242
252
|
```
|
|
243
253
|
|
|
244
|
-
A sparkle spinner (`✶ ✸ ✻ ✽`) cycles while the guardian is reviewing and resolves to `✓ approved`, `↻ revision requested`, `? waiting for your approval`, or `✗ blocked`. The guardian's reason, when present, appears on a dim second line; the command itself is not repeated because it is already visible in the Bash tool box. Configure the widget with:
|
|
254
|
+
A sparkle spinner (`✶ ✸ ✻ ✽`) cycles while the guardian is reviewing and resolves to `✓ approved`, `↻ revision requested`, `? waiting for your approval`, or `✗ blocked`. When approval is needed, the selector's leading `●` pulses between warning-bright and dim so the active prompt remains visually distinct from the transcript. The guardian's reason, when present, appears on a dim second line; the command itself is not repeated because it is already visible in the Bash tool box. Configure the widget with:
|
|
245
255
|
|
|
246
256
|
```json
|
|
247
257
|
{
|
|
@@ -257,7 +267,7 @@ Set `placement` to `toolRow` to show the review inside Pi's Bash tool row:
|
|
|
257
267
|
|
|
258
268
|
```text
|
|
259
269
|
$ git commit --dry-run -m "fix auth"
|
|
260
|
-
◌ guardian running · Git commit · openai-codex/gpt-5.6-luna
|
|
270
|
+
◌ guardian running · Git commit · openai-codex-auto-permissions/gpt-5.6-luna
|
|
261
271
|
```
|
|
262
272
|
|
|
263
273
|
`toolRow` reconstructs Pi's standard local Bash definition because Pi does not expose renderer-only decoration. Do not use it with SDK-provided, remote, sandboxed, or otherwise replaced Bash backends. The extension detects non-native Bash tools and falls back to the widget instead of replacing them.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export const APPROVAL_PULSE_INTERVAL_MS = 600;
|
|
4
|
+
|
|
5
|
+
export interface ApprovalPulseTheme {
|
|
6
|
+
fg(role: "warning" | "dim", text: string): string;
|
|
7
|
+
bold(text: string): string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ApprovalPulseScheduler {
|
|
11
|
+
setInterval(callback: () => void, intervalMs: number): unknown;
|
|
12
|
+
clearInterval(handle: unknown): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type InteractiveComponent = Component & Required<Pick<Component, "handleInput">>;
|
|
16
|
+
|
|
17
|
+
const DEFAULT_SCHEDULER: ApprovalPulseScheduler = {
|
|
18
|
+
setInterval(callback, intervalMs) {
|
|
19
|
+
const timer = setInterval(callback, intervalMs);
|
|
20
|
+
timer.unref?.();
|
|
21
|
+
return timer;
|
|
22
|
+
},
|
|
23
|
+
clearInterval(handle) {
|
|
24
|
+
clearInterval(handle as ReturnType<typeof setInterval>);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decorates an interactive prompt with a bright/dim approval light.
|
|
30
|
+
*
|
|
31
|
+
* The child still owns all content and input. Only its first rendered line is
|
|
32
|
+
* replaced, preserving the selector's existing spacing and wrapping exactly.
|
|
33
|
+
*/
|
|
34
|
+
export class PulsingApprovalIndicator implements Component {
|
|
35
|
+
private bright = true;
|
|
36
|
+
private timer: unknown;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly headline: string,
|
|
40
|
+
private readonly child: InteractiveComponent,
|
|
41
|
+
private readonly theme: ApprovalPulseTheme,
|
|
42
|
+
private readonly requestRender: () => void,
|
|
43
|
+
private readonly scheduler: ApprovalPulseScheduler = DEFAULT_SCHEDULER,
|
|
44
|
+
) {}
|
|
45
|
+
|
|
46
|
+
start(): void {
|
|
47
|
+
if (this.timer !== undefined) return;
|
|
48
|
+
this.timer = this.scheduler.setInterval(() => {
|
|
49
|
+
this.bright = !this.bright;
|
|
50
|
+
this.requestRender();
|
|
51
|
+
}, APPROVAL_PULSE_INTERVAL_MS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
stop(): void {
|
|
55
|
+
if (this.timer === undefined) return;
|
|
56
|
+
this.scheduler.clearInterval(this.timer);
|
|
57
|
+
this.timer = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isRunning(): boolean {
|
|
61
|
+
return this.timer !== undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render(width: number): string[] {
|
|
65
|
+
const lines = this.child.render(width);
|
|
66
|
+
const glyph = this.bright
|
|
67
|
+
? this.theme.fg("warning", this.theme.bold("●"))
|
|
68
|
+
: this.theme.fg("dim", "●");
|
|
69
|
+
const headline = truncateToWidth(`${glyph} ${this.headline}`, Math.max(1, width), "");
|
|
70
|
+
return lines.length > 0 ? [headline, ...lines.slice(1)] : [headline];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
handleInput(data: string): void {
|
|
74
|
+
this.child.handleInput(data);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
invalidate(): void {
|
|
78
|
+
this.child.invalidate();
|
|
79
|
+
}
|
|
80
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { cleanupSessionResources, type Message } from "@earendil-works/pi-ai";
|
|
2
2
|
import { resolveGuardianCompleteSimple } from "./guardian-transport.js";
|
|
3
|
+
import {
|
|
4
|
+
createAutoPermissionsOpenAICodexProvider,
|
|
5
|
+
isOpenAICodexModel,
|
|
6
|
+
} from "./openai-codex-provider.js";
|
|
3
7
|
import { detectSubagentContext } from "./subagent-context.js";
|
|
4
8
|
import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
|
|
5
9
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
} from "./review.js";
|
|
39
43
|
import { mergeOverrideEvidence, type PermissionOverride } from "./override-evidence.js";
|
|
40
44
|
import { OptionSelector } from "@hank-warren/pi-permission-selector/selector.ts";
|
|
45
|
+
import { PulsingApprovalIndicator } from "./approval-pulse.js";
|
|
41
46
|
|
|
42
47
|
const WIDGET_KEY = "auto-permissions";
|
|
43
48
|
const PROJECT_CONFIG_DIR_NAME = (PiCodingAgent as { CONFIG_DIR_NAME?: string }).CONFIG_DIR_NAME ?? ".pi";
|
|
@@ -125,8 +130,8 @@ function createUuidV7(): string {
|
|
|
125
130
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
function reviewerSessionId(
|
|
129
|
-
return
|
|
133
|
+
function reviewerSessionId(model: { api?: string }): string {
|
|
134
|
+
return isOpenAICodexModel(model) ? createUuidV7() : `ap-review-${randomUUID()}`;
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
function loadNativeBashOptions(cwd: string, projectTrusted: boolean): { commandPrefix?: string; shellPath?: string } {
|
|
@@ -359,7 +364,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
359
364
|
throw new Error("compact review evidence exceeds the review model's safe context budget");
|
|
360
365
|
}
|
|
361
366
|
|
|
362
|
-
const sessionId = base?.sessionId ?? reviewerSessionId(model
|
|
367
|
+
const sessionId = base?.sessionId ?? reviewerSessionId(model);
|
|
363
368
|
const attemptGeneration = reviewerGeneration;
|
|
364
369
|
activeReviewerSessionId = sessionId;
|
|
365
370
|
const timeoutSignal = AbortSignal.timeout(config.reviewer?.timeoutMs ?? 30_000);
|
|
@@ -388,7 +393,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
388
393
|
signal: reviewSignal,
|
|
389
394
|
reasoning: config.reviewer?.reasoningEffort ?? "low",
|
|
390
395
|
sessionId,
|
|
391
|
-
transport: model
|
|
396
|
+
transport: isOpenAICodexModel(model) ? "websocket" : "auto",
|
|
392
397
|
cacheRetention: "long",
|
|
393
398
|
},
|
|
394
399
|
);
|
|
@@ -600,7 +605,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
600
605
|
stopTimer();
|
|
601
606
|
},
|
|
602
607
|
};
|
|
603
|
-
}, { placement: "
|
|
608
|
+
}, { placement: "aboveEditor" });
|
|
604
609
|
|
|
605
610
|
if (autoClear) {
|
|
606
611
|
clearWidgetTimer = setTimeout(() => {
|
|
@@ -677,9 +682,14 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
677
682
|
signal: AbortSignal,
|
|
678
683
|
opts: { allowComment: boolean },
|
|
679
684
|
): Promise<string | undefined> {
|
|
685
|
+
let indicator: PulsingApprovalIndicator | undefined;
|
|
680
686
|
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
|
|
687
|
+
let finished = false;
|
|
681
688
|
const onAbort = () => finish(undefined);
|
|
682
689
|
const finish = (result: string | undefined) => {
|
|
690
|
+
if (finished) return;
|
|
691
|
+
finished = true;
|
|
692
|
+
indicator?.stop();
|
|
683
693
|
signal.removeEventListener("abort", onAbort);
|
|
684
694
|
done(result);
|
|
685
695
|
};
|
|
@@ -701,12 +711,19 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
701
711
|
onCancel: () => finish(undefined),
|
|
702
712
|
requestRender: () => tui.requestRender(),
|
|
703
713
|
});
|
|
714
|
+
indicator = new PulsingApprovalIndicator(
|
|
715
|
+
title.split("\n", 1)[0] ?? "Auto Permissions needs approval",
|
|
716
|
+
selector,
|
|
717
|
+
theme,
|
|
718
|
+
() => tui.requestRender(),
|
|
719
|
+
);
|
|
720
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
704
721
|
// `done` before the factory resolves is safe: pi marks the dialog closed
|
|
705
722
|
// and never mounts the component.
|
|
706
723
|
if (signal.aborted) onAbort();
|
|
707
|
-
else
|
|
708
|
-
return
|
|
709
|
-
});
|
|
724
|
+
else indicator.start();
|
|
725
|
+
return indicator;
|
|
726
|
+
}).finally(() => indicator?.stop());
|
|
710
727
|
}
|
|
711
728
|
|
|
712
729
|
async function askUser(
|
|
@@ -980,6 +997,13 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
|
|
|
980
997
|
});
|
|
981
998
|
|
|
982
999
|
pi.on("session_start", async (_event, ctx) => {
|
|
1000
|
+
const openaiCodex = ctx.modelRegistry.getProvider("openai-codex");
|
|
1001
|
+
if (openaiCodex?.auth.oauth) {
|
|
1002
|
+
pi.registerProvider(createAutoPermissionsOpenAICodexProvider(openaiCodex));
|
|
1003
|
+
} else {
|
|
1004
|
+
console.warn("[pi-auto-permissions] OpenAI Codex OAuth is unavailable; the dedicated Auto Permissions login was not registered.");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
983
1007
|
reviewerLifecycleController.abort();
|
|
984
1008
|
discardReviewerLineage();
|
|
985
1009
|
reviewerLifecycleController = new AbortController();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Provider } from "@earendil-works/pi-ai";
|
|
2
|
+
|
|
3
|
+
export const AUTO_PERMISSIONS_OPENAI_CODEX_PROVIDER_ID = "openai-codex-auto-permissions";
|
|
4
|
+
export const AUTO_PERMISSIONS_OPENAI_CODEX_PROVIDER_NAME = "OpenAI Codex - Auto Permissions";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registering a second provider id gives Pi a second auth.json credential slot.
|
|
8
|
+
* The transport and model catalog still come from Pi's built-in OpenAI Codex
|
|
9
|
+
* provider, so the alias follows the host Pi version instead of duplicating its
|
|
10
|
+
* OAuth or Responses implementation here. The effective catalog is snapshotted
|
|
11
|
+
* when the session starts; the source provider remains the sole owner of any
|
|
12
|
+
* dynamic catalog refresh state.
|
|
13
|
+
*/
|
|
14
|
+
export function createAutoPermissionsOpenAICodexProvider(source: Provider): Provider {
|
|
15
|
+
const models = source.getModels().map((model) => ({
|
|
16
|
+
...model,
|
|
17
|
+
provider: AUTO_PERMISSIONS_OPENAI_CODEX_PROVIDER_ID,
|
|
18
|
+
}));
|
|
19
|
+
const oauth = source.auth.oauth;
|
|
20
|
+
|
|
21
|
+
if (!oauth) {
|
|
22
|
+
throw new Error("Pi's OpenAI Codex provider does not expose OAuth authentication");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...source,
|
|
27
|
+
id: AUTO_PERMISSIONS_OPENAI_CODEX_PROVIDER_ID,
|
|
28
|
+
name: AUTO_PERMISSIONS_OPENAI_CODEX_PROVIDER_NAME,
|
|
29
|
+
auth: {
|
|
30
|
+
oauth: {
|
|
31
|
+
...oauth,
|
|
32
|
+
name: AUTO_PERMISSIONS_OPENAI_CODEX_PROVIDER_NAME,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
getModels: () => models,
|
|
36
|
+
refreshModels: undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isOpenAICodexModel(model: { api?: string }): boolean {
|
|
41
|
+
return model.api === "openai-codex-responses";
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hank-warren/pi-auto-permissions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": [
|
|
@@ -39,8 +39,10 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"NOTICE.md",
|
|
41
41
|
"index.ts",
|
|
42
|
+
"approval-pulse.ts",
|
|
42
43
|
"config.ts",
|
|
43
44
|
"guardian-transport.ts",
|
|
45
|
+
"openai-codex-provider.ts",
|
|
44
46
|
"subagent-context.ts",
|
|
45
47
|
"evaluation-log.ts",
|
|
46
48
|
"gates.ts",
|
package/widget-status.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure helpers for the
|
|
2
|
+
* Pure helpers for the above-editor review status widget: single-line status
|
|
3
3
|
* formatting plus frame progression for the waiting sparkle spinner. Kept
|
|
4
4
|
* free of TUI dependencies so they are unit-testable.
|
|
5
5
|
*/
|