@hank-warren/pi-auto-permissions 0.7.2 → 0.9.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 CHANGED
@@ -1,5 +1,40 @@
1
1
  # @hank-warren/pi-auto-permissions
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7d96995: Stop `pi-herdr-auto-title` depending on `pi-auto-permissions`.
8
+
9
+ The pane-title extension deep-imported `guardian-transport.ts` — roughly forty lines resolving `completeSimple` through the host `ModelRuntime` so provider extensions (Anthropic OAuth shaping) apply to background model calls. Reusing it cost an entire dependency on the permissions engine: 125 kB unpacked in `node_modules`, a transitive `pi-permission-selector`, and a no-op release of the title extension every time the guardian shipped.
10
+
11
+ The file is now duplicated byte-for-byte into both packages, and `scripts/validate.py` fails if the copies drift. `pi-herdr-auto-title` declares no dependencies at all and its tarball is unchanged apart from the added source file.
12
+
13
+ To keep the copies identical, `resolveGuardianCompleteSimple` takes the calling package's name as a second argument and uses it in the compat-fallback warning, replacing the hardcoded `[pi-auto-permissions]` prefix. That is a breaking change only for an external deep import of `@hank-warren/pi-auto-permissions/guardian-transport.ts`; the extension's own behavior is unchanged.
14
+
15
+ ### Patch Changes
16
+
17
+ - 7d96995: Make every declared dependency accurate.
18
+
19
+ `pi-permission-selector` no longer declares a `@earendil-works/pi-coding-agent` peer dependency. It imports nothing from it — the claim was left over from 1.0.0, which removed the extension and the selector monkey patch and turned the package into a pure library.
20
+
21
+ `pi-auto-permissions` floors `@hank-warren/pi-permission-selector` at `^1.1.0` and `pi-plan-mode` floors `@narumitw/pi-tui-kit` at `^0.49.3`, in both cases the lowest version the package is actually tested against. Neither uses a feature the old floor lacked, so nothing changes at install time under caret resolution; the ranges simply no longer claim support that nothing verifies.
22
+
23
+ `scripts/validate.py` now enforces that a package's Pi `peerDependencies` are exactly the Pi packages its shipped sources import, in both directions.
24
+
25
+ - Updated dependencies [7d96995]
26
+ - @hank-warren/pi-permission-selector@1.1.1
27
+
28
+ ## 0.8.0
29
+
30
+ ### Minor Changes
31
+
32
+ - 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.
33
+
34
+ ### Patch Changes
35
+
36
+ - 312da9a: Add a pulsing warning light to interactive approval prompts so they remain visually distinct from the surrounding transcript.
37
+
3
38
  ## 0.7.2
4
39
 
5
40
  ### 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:
@@ -238,10 +248,10 @@ The path defaults to `usage.jsonl` beside the Auto Permissions config and resolv
238
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
+ }
@@ -1,17 +1,17 @@
1
1
  import { completeSimple } from "@earendil-works/pi-ai/compat";
2
2
 
3
3
  /**
4
- * Guardian model dispatch seam.
4
+ * Model dispatch seam for extension-issued background model calls.
5
5
  *
6
6
  * pi-ai's `compat.completeSimple` dispatches through pi-ai's own api registry,
7
7
  * which extension-registered provider transports never reach: pi routes
8
8
  * `pi.registerProvider()` configs only through the coding agent's
9
9
  * `ModelRuntime` -> `provider-composer.streamWith`. Auth-shaping extensions
10
10
  * such as `@gotgenes/pi-anthropic-auth` install their transport wrapper there,
11
- * so a reviewer request issued via `compat.completeSimple` reaches Anthropic
12
- * unshaped, and Claude Pro/Max OAuth requests fail with the misleading
13
- * "extra usage" HTTP 400 (see gotgenes/pi-anthropic-auth
14
- * docs/architecture.md, "The remaining gap: pi-ai compat dispatch").
11
+ * so a request issued via `compat.completeSimple` reaches Anthropic unshaped,
12
+ * and Claude Pro/Max OAuth requests fail with the misleading "extra usage"
13
+ * HTTP 400 (see gotgenes/pi-anthropic-auth docs/architecture.md, "The
14
+ * remaining gap: pi-ai compat dispatch").
15
15
  *
16
16
  * `ExtensionContext.modelRegistry` is a deliberately narrowed facade over
17
17
  * `ModelRuntime` that re-exports `complete()` but not `completeSimple()` —
@@ -21,12 +21,24 @@ import { completeSimple } from "@earendil-works/pi-ai/compat";
21
21
  * marked `private` in the .d.ts), and fall back to compat dispatch when a
22
22
  * future pi release renames the seam — degrading to the historical behavior
23
23
  * instead of crashing.
24
+ *
25
+ * THIS FILE IS DUPLICATED VERBATIM across packages/pi-auto-permissions and
26
+ * packages/pi-herdr-auto-title, and `scripts/validate.py` fails if the copies
27
+ * differ by a single byte. The duplication is deliberate: sharing it made a
28
+ * pane-titling extension depend on an entire permissions engine to reuse a
29
+ * dozen lines. Edit one copy, then copy it over the other. Every
30
+ * package-specific detail must stay behind a parameter — hence `packageLabel`.
31
+ * The "guardian" in the names is historical: pi-auto-permissions' guardian
32
+ * reviewer was the first caller.
24
33
  */
25
34
  export type GuardianCompleteSimple = typeof completeSimple;
26
35
 
27
36
  let warnedCompatFallback = false;
28
37
 
29
- export function resolveGuardianCompleteSimple(modelRegistry: unknown): GuardianCompleteSimple {
38
+ export function resolveGuardianCompleteSimple(
39
+ modelRegistry: unknown,
40
+ packageLabel: string,
41
+ ): GuardianCompleteSimple {
30
42
  const runtime = (modelRegistry as { runtime?: unknown } | null | undefined)?.runtime;
31
43
  const candidate = (runtime as { completeSimple?: unknown } | null | undefined)?.completeSimple;
32
44
  if (typeof candidate === "function") {
@@ -35,7 +47,7 @@ export function resolveGuardianCompleteSimple(modelRegistry: unknown): GuardianC
35
47
  if (!warnedCompatFallback) {
36
48
  warnedCompatFallback = true;
37
49
  console.warn(
38
- "[pi-auto-permissions] host ModelRuntime completeSimple is unavailable; falling back to pi-ai compat dispatch. Extension-registered provider transports (e.g. Anthropic OAuth shaping) will not apply to reviewer calls.",
50
+ `[${packageLabel}] host ModelRuntime completeSimple is unavailable; falling back to pi-ai compat dispatch. Extension-registered provider transports (e.g. Anthropic OAuth shaping) will not apply to this extension's model calls.`,
39
51
  );
40
52
  }
41
53
  return completeSimple;
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(provider: string): string {
129
- return provider === "openai-codex" ? createUuidV7() : `ap-review-${randomUUID()}`;
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.provider);
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);
@@ -378,7 +383,7 @@ export default function autoPermissionsExtension(pi: ExtensionAPI) {
378
383
  }
379
384
  // Dispatch through the host ModelRuntime so extension-registered provider
380
385
  // transports (e.g. pi-anthropic-auth OAuth shaping) apply; see guardian-transport.ts.
381
- const response = await resolveGuardianCompleteSimple(ctx.modelRegistry)(
386
+ const response = await resolveGuardianCompleteSimple(ctx.modelRegistry, "pi-auto-permissions")(
382
387
  model,
383
388
  { systemPrompt, messages },
384
389
  {
@@ -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.provider === "openai-codex" ? "websocket" : "auto",
396
+ transport: isOpenAICodexModel(model) ? "websocket" : "auto",
392
397
  cacheRetention: "long",
393
398
  },
394
399
  );
@@ -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 signal.addEventListener("abort", onAbort, { once: true });
708
- return selector;
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.7.2",
3
+ "version": "0.9.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",
@@ -53,7 +55,7 @@
53
55
  "CHANGELOG.md"
54
56
  ],
55
57
  "dependencies": {
56
- "@hank-warren/pi-permission-selector": "^1.0.1"
58
+ "@hank-warren/pi-permission-selector": "^1.1.1"
57
59
  },
58
60
  "peerDependencies": {
59
61
  "@earendil-works/pi-ai": "*",