@gotgenes/pi-permission-system 33.0.6 → 33.0.8

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
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [33.0.8](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.7...pi-permission-system-v33.0.8) (2026-09-23)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **pi-permission-system:** stop reading a git revision range as a parent-directory path ([2656b8d](https://github.com/gotgenes/pi-packages/commit/2656b8d8867404a623534004a7edab41a0de93b1)), closes [#859](https://github.com/gotgenes/pi-packages/issues/859)
14
+
15
+ ## [33.0.7](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.6...pi-permission-system-v33.0.7) (2026-09-22)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **pi-permission-system:** stop a second permission ask from replacing the first ([e01fca6](https://github.com/gotgenes/pi-packages/commit/e01fca6f40a836e41286306b092db03f53601b54))
21
+ * **pi-permission-system:** answer pending permission asks when the session ends ([e0eb7f8](https://github.com/gotgenes/pi-packages/commit/e0eb7f8b6c511bf874d48d241f28c5bc5f7f6aee))
22
+
23
+ ### Documentation
24
+
25
+ * **pi-permission-system:** document per-session ask serialization ([fa3a16a](https://github.com/gotgenes/pi-packages/commit/fa3a16a48e7919ae5eed2a82caec83ce9bd9c755))
26
+
8
27
  ## [33.0.6](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.5...pi-permission-system-v33.0.6) (2026-09-22)
9
28
 
10
29
 
package/README.md CHANGED
@@ -20,8 +20,8 @@ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) codin
20
20
  - **Protects sensitive file patterns** — cross-cutting `path` rules deny `.env`, `~/.ssh/*`, etc. across all tools and bash at once, matching both the path as referenced and its symlink-resolved form so a deny cannot be evaded through a symlink alias
21
21
  - **Guards external paths** — prompts before file tools or bash commands reach outside `cwd`
22
22
  - **Fails closed** — an internal gate error blocks the tool (with a `gate_error` review-log entry and a matching `permissions:decision` broadcast), and a bash command the parser could not resolve, in whole or in part — or an indirection wrapper that hides the gated command (`bash -c`/`eval`, `sudo`, `env`, `xargs`, `find -exec`, …) — prompts (`ask`) rather than passing silently, unless the wrapped command is a pure reader whose direction is provable whatever it is fed (`xargs grep -l foo`); where a partial parse failure's own region re-parses cleanly on its own, the commands and paths it holds are recovered and gated rather than merely prompted for
23
- - **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts
24
- - **Broadcasts UI prompt events** — `permissions:ui_prompt` fires only when the permission system is about to invoke the active user-facing permission UI, and every prompt it announces — including one forwarded up from a subagent — is answered by a `permissions:decision` on the same bus
23
+ - **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts, and a forwarded prompt queues behind whatever dialog is already open instead of replacing it
24
+ - **Broadcasts UI prompt events** — `permissions:ui_prompt` fires only when the permission system is about to invoke the active user-facing permission UI (for a queued ask, when its turn comes rather than when it was raised), and every prompt it announces — including one forwarded up from a subagent — is answered by a `permissions:decision` on the same bus
25
25
  - **Native [`@gotgenes/pi-subagents`](https://github.com/gotgenes/pi-subagents) integration** — in-process child sessions register with the permission system automatically, enabling per-agent policy enforcement and `ask`-state forwarding to the parent UI without configuration
26
26
 
27
27
  ## Install
@@ -384,6 +384,8 @@ Policy decisions that resolve without an active UI prompt, such as `policy_allow
384
384
  Non-UI child sessions also do not emit this event when they create a forwarded permission request; the parent UI session emits it immediately before showing the forwarded permission dialog.
385
385
  A forwarded request the parent's own recorded policy decides (a matching `allow` or `deny`) is answered without a prompt and emits no event; the event fires only when the parent is actually about to ask the human.
386
386
  The matching terminal `permissions:decision` is emitted in the parent session too, so a consumer that reacts to this event has a signal on the same bus telling it the prompt is over.
387
+ Asks are presented one at a time: the host holds a single inline dialog slot, so a session that raises a second ask while one is still open queues it rather than mounting over the first.
388
+ The event marks the moment the queued ask is presented, not the moment it was raised, which is what keeps "the user needs to respond now" true for a consumer that alerts on it.
387
389
  Forwarded prompts that do reach the human are not degraded: the parent emits the child's original `source` and the same `surface`/`value` display projection, plus a populated `forwarding` context identifying the requesting subagent.
388
390
 
389
391
  The payload is lean by design — `surface`/`value` are the normalized display projection a notification consumer reads, not a mirror of the internal review log.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "33.0.6",
3
+ "version": "33.0.8",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -52,7 +52,7 @@ import type { PathFlavor } from "#src/path/path-flavor";
52
52
  * Accepts tokens that unambiguously look like filesystem paths:
53
53
  * - Absolute paths (starting with `/`)
54
54
  * - Home-relative paths (starting with `~/`)
55
- * - Parent-traversal paths (containing `..`)
55
+ * - Parent-traversal paths (carrying a whole `..` segment)
56
56
  * - Windows drive-letter absolute paths (`C:/…` or `C:\…`)
57
57
  *
58
58
  * Returns the raw token string if it qualifies, or `null` to skip.
@@ -62,7 +62,7 @@ export function classifyTokenAsPathCandidate(token: string): string | null {
62
62
 
63
63
  if (token.startsWith("/")) return token;
64
64
  if (token.startsWith("~/")) return token;
65
- if (token.includes("..")) return token;
65
+ if (hasParentTraversal(token)) return token;
66
66
  if (WINDOWS_DRIVE_PATH_PATTERN.test(token)) return token;
67
67
 
68
68
  return null;
@@ -85,7 +85,8 @@ export function classifyTokenAsPathCandidate(token: string): string | null {
85
85
  * and order-independent, and covers the backslash-only form (`D:\…`) which the POSIX
86
86
  * flavor's `hasPathSeparator` cannot reach.
87
87
  *
88
- * Does NOT require the strict "must start with `/` or `~/` or contain `..`"
88
+ * Does NOT require the strict "must start with `/` or `~/` or carry a whole
89
+ * `..` segment"
89
90
  * gate that the external-directory classifier uses.
90
91
  *
91
92
  * Returns the raw token string if it qualifies, or `null` to skip.
@@ -98,7 +99,7 @@ export function classifyTokenAsRuleCandidate(
98
99
 
99
100
  if (token.startsWith(".")) return token;
100
101
  if (flavor.hasPathSeparator(token)) return token; // ~/ paths, relative paths with /, and win32 dir\file
101
- if (token.includes("..")) return token; // bare ".." (no slash)
102
+ if (hasParentTraversal(token)) return token; // a backslash `..` segment on POSIX
102
103
  if (WINDOWS_DRIVE_PATH_PATTERN.test(token)) return token; // backslash-only drive form
103
104
 
104
105
  return null;
@@ -131,6 +132,23 @@ export function classifyBareTokenCandidate(token: string): string | null {
131
132
 
132
133
  // ── Private rejection predicate ────────────────────────────────────────────
133
134
 
135
+ /**
136
+ * A `..` standing as a whole path segment: exactly `..`, or a `..` bounded by
137
+ * a separator (`/` or `\`) or a token edge on both sides. A `..` inside a
138
+ * segment is not a traversal: a git revision range (`HEAD..origin/main`,
139
+ * `v1..v2`, `a...b`) never names a parent directory, so it must not reach the
140
+ * external-directory guard. A real file whose name carries `..` is still
141
+ * reached through the resolver's existence probe (#645).
142
+ *
143
+ * The backslash counts on every flavor: the strict classifier takes no flavor,
144
+ * and Git Bash hands a `\` to Windows file APIs as a separator.
145
+ */
146
+ const PARENT_TRAVERSAL_SEGMENT_PATTERN = /(^|[/\\])\.\.($|[/\\])/;
147
+
148
+ function hasParentTraversal(token: string): boolean {
149
+ return PARENT_TRAVERSAL_SEGMENT_PATTERN.test(token);
150
+ }
151
+
134
152
  /**
135
153
  * Windows drive-letter absolute path: a single ASCII letter, a colon, then a
136
154
  * separator (`/` or `\`). Matches `C:/…` and `C:\…` but not drive-relative
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The shutdown-facing slice of {@link AskDialogQueue}: settle everything this
3
+ * session still owes an answer for.
4
+ *
5
+ * Narrow on purpose (ISP) — the lifecycle handler releases asks and never
6
+ * admits one.
7
+ */
8
+ export interface AskDialogRelease {
9
+ releaseAll(reason: string): void;
10
+ }
11
+
12
+ /**
13
+ * The admission-facing slice of {@link AskDialogQueue}: present this ask once
14
+ * every earlier one has settled.
15
+ *
16
+ * The terminal that shows a dialog admits asks and never releases them, and
17
+ * depending on the slice rather than the class keeps the queue's private state
18
+ * out of its dependency type.
19
+ */
20
+ export interface AskDialogAdmission {
21
+ run<T>(
22
+ present: () => Promise<T>,
23
+ released: (reason: string) => T,
24
+ ): Promise<T>;
25
+ }
26
+
27
+ /**
28
+ * Serializes the human-facing presentations one session owns.
29
+ *
30
+ * Pi's inline `ctx.ui.custom` slot holds one component: a second presentation
31
+ * clears the editor container and mounts over the first, whose promise then
32
+ * never settles and whose component is never disposed. This extension raises
33
+ * asks from two independent tasks — the gate's local ask and the forwarded ask
34
+ * a poll tick drains — so without a queue whichever arrives second silently
35
+ * strands the first, and a forwarded ask stranded that way holds the
36
+ * forwarding inbox closed behind it (#965).
37
+ *
38
+ * The queue cannot see the host's slot, so its contract is only that it never
39
+ * lets two of *its own* presentations overlap. A presentation another
40
+ * extension mounts is outside its evidence; upstream declined to arbitrate
41
+ * globally (earendil-works/pi#7007), so that case stays open by design.
42
+ *
43
+ * Logger-free, following `transient-fs-retry.ts`: `PermissionPrompter` already
44
+ * brackets each ask with review entries, so a queue wait reads as the gap
45
+ * between them and a released ask reads as its terminal entry.
46
+ */
47
+ export class AskDialogQueue implements AskDialogAdmission, AskDialogRelease {
48
+ /** Settles once every ask admitted so far has been presented or released. */
49
+ private tail: Promise<void> = Promise.resolve();
50
+ /**
51
+ * The asks that still owe their caller a value, queued and in-flight alike.
52
+ * Typed as the release capability rather than `AdmittedAsk<unknown>` so the
53
+ * set does not have to name a value type it never reads.
54
+ */
55
+ private readonly outstanding = new Set<{ release(reason: string): void }>();
56
+
57
+ /**
58
+ * Admit an ask, presenting it only once every earlier one has settled.
59
+ *
60
+ * `released` is the caller's own fallback, supplied at admission: the queue
61
+ * never constructs a decision, because a decision is stamped at the site
62
+ * that decides and the queue does not know what it is presenting.
63
+ */
64
+ run<T>(
65
+ present: () => Promise<T>,
66
+ released: (reason: string) => T,
67
+ ): Promise<T> {
68
+ const ask = new AdmittedAsk(released);
69
+ this.outstanding.add(ask);
70
+ this.tail = this.tail.then(async () => {
71
+ // A release while this ask waited its turn means nothing may render:
72
+ // presenting it now would put a dialog on screen for a decision that
73
+ // has already been answered.
74
+ if (ask.isSettled) {
75
+ return;
76
+ }
77
+ try {
78
+ ask.settle(await present());
79
+ } catch (error) {
80
+ ask.fail(error);
81
+ } finally {
82
+ this.outstanding.delete(ask);
83
+ }
84
+ });
85
+ return ask.decision;
86
+ }
87
+
88
+ /**
89
+ * Settle every outstanding ask with its own released value.
90
+ *
91
+ * The tail is reset rather than chained on, because the in-flight ask's
92
+ * presentation is still pending in the host and nothing would ever settle
93
+ * it — a session that keeps running must not queue behind a dialog nobody
94
+ * can answer.
95
+ */
96
+ releaseAll(reason: string): void {
97
+ const releasing = [...this.outstanding];
98
+ this.outstanding.clear();
99
+ this.tail = Promise.resolve();
100
+ for (const ask of releasing) {
101
+ ask.release(reason);
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * One admitted ask and the promise its caller is holding.
108
+ *
109
+ * A release while a presentation is in flight leaves the host's own promise
110
+ * pending, and it may resolve later; that late answer is discarded because the
111
+ * caller's promise has already settled. `settled` is therefore read rather
112
+ * than enforced — it is what lets the queue skip presenting an ask that was
113
+ * released while it waited its turn.
114
+ */
115
+ class AdmittedAsk<T> {
116
+ readonly decision: Promise<T>;
117
+ private settled = false;
118
+ private readonly resolve: (value: T) => void;
119
+ private readonly reject: (error: unknown) => void;
120
+
121
+ constructor(private readonly released: (reason: string) => T) {
122
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
123
+ this.decision = promise;
124
+ this.resolve = resolve;
125
+ this.reject = reject;
126
+ }
127
+
128
+ get isSettled(): boolean {
129
+ return this.settled;
130
+ }
131
+
132
+ settle(value: T): void {
133
+ this.settled = true;
134
+ this.resolve(value);
135
+ }
136
+
137
+ fail(error: unknown): void {
138
+ this.settled = true;
139
+ this.reject(error);
140
+ }
141
+
142
+ release(reason: string): void {
143
+ this.settle(this.released(reason));
144
+ }
145
+ }
@@ -3,6 +3,7 @@ import type { DebugReviewLogger } from "#src/logging/session-logger";
3
3
  import type { AuthorizerLog, PermissionQuery } from "#src/service";
4
4
  import type { PermissionEventBus } from "#src/service/permission-events";
5
5
  import { ParentAuthorizer } from "./approval-escalator";
6
+ import type { AskDialogAdmission } from "./ask-dialog-queue";
6
7
  import { DenyingAuthorizer } from "./denying-authorizer";
7
8
  import { getSessionId } from "./forwarder-context";
8
9
  import type { TargetServingLookup } from "./forwarding-liveness";
@@ -110,6 +111,13 @@ export interface AuthorizerSelectionDeps {
110
111
  detection: SubagentDetector;
111
112
  /** Event bus used by `LocalUserAuthorizer` for the `permissions:ui_prompt` broadcast. */
112
113
  events: PermissionEventBus;
114
+ /**
115
+ * The session's dialog queue, threaded into `LocalUserAuthorizer`.
116
+ *
117
+ * Shared rather than owned by the terminal, because a terminal is rebuilt on
118
+ * every activation and a per-activation queue would serialize nothing (#965).
119
+ */
120
+ dialogs: AskDialogAdmission;
113
121
  /** Read live at prompt time; threaded into `LocalUserAuthorizer`. */
114
122
  getPromptPreferences: () => PromptPreferences;
115
123
  /** Injected for testability; production callers pass the real function. */
@@ -147,6 +155,7 @@ export function selectAuthorizer(
147
155
  ui: ctx.ui,
148
156
  mode: ctx.mode,
149
157
  events: deps.events,
158
+ dialogs: deps.dialogs,
150
159
  getPromptPreferences: deps.getPromptPreferences,
151
160
  requestPermissionDecision: deps.requestPermissionDecision,
152
161
  }),
@@ -10,6 +10,7 @@ import {
10
10
  } from "#src/service/permission-events";
11
11
  import { buildUiPrompt } from "#src/service/permission-ui-prompt";
12
12
  import { provenDirectionOf } from "#src/session/approval-grant";
13
+ import type { AskDialogAdmission } from "./ask-dialog-queue";
13
14
  import type { TerminalAuthorizer } from "./authorizer";
14
15
  import type {
15
16
  PermissionPromptDecision,
@@ -30,6 +31,8 @@ export interface LocalUserAuthorizerDeps {
30
31
  mode: ExtensionContext["mode"];
31
32
  /** Event bus used for the `permissions:ui_prompt` broadcast. */
32
33
  events: PermissionEventBus;
34
+ /** Serializes this session's dialogs so no ask replaces another (#965). */
35
+ dialogs: AskDialogAdmission;
33
36
  /** Read live at prompt time so a settings-modal toggle takes effect on the next prompt. */
34
37
  getPromptPreferences: () => PromptPreferences;
35
38
  /** Injected for testability; production callers pass the real function. */
@@ -45,6 +48,10 @@ export interface LocalUserAuthorizerDeps {
45
48
  * forwarded ask carries its provenance on `details.forwarding`, which this
46
49
  * class renders (populated `forwarding` context + "(Subagent)" title) so the
47
50
  * broadcast stays non-degraded (#292) without a second emission path.
51
+ *
52
+ * Every ask goes through the session's `AskDialogAdmission`, because the host
53
+ * holds one inline dialog slot: a second presentation mounts over the first and
54
+ * strands its promise (#965).
48
55
  */
49
56
  export class LocalUserAuthorizer implements TerminalAuthorizer {
50
57
  constructor(private readonly deps: LocalUserAuthorizerDeps) {}
@@ -52,8 +59,24 @@ export class LocalUserAuthorizer implements TerminalAuthorizer {
52
59
  authorize(
53
60
  details: PromptPermissionDetails,
54
61
  ): Promise<PermissionPromptDecision> {
55
- const uiPrompt = buildUiPrompt(details);
56
- emitUiPromptEvent(this.deps.events, uiPrompt);
62
+ return this.deps.dialogs.run(
63
+ () => this.present(details),
64
+ unansweredDecision,
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Announce the imminent prompt, then show it.
70
+ *
71
+ * Both live inside the queued region: `permissions:ui_prompt` is documented
72
+ * as firing immediately before the user-facing UI is invoked, so an emit at
73
+ * admission would alert a notification consumer for a dialog that is still
74
+ * minutes of deliberation away.
75
+ */
76
+ private present(
77
+ details: PromptPermissionDetails,
78
+ ): Promise<PermissionPromptDecision> {
79
+ emitUiPromptEvent(this.deps.events, buildUiPrompt(details));
57
80
  return this.deps.requestPermissionDecision(
58
81
  {
59
82
  mode: this.deps.mode,
@@ -69,6 +92,25 @@ export class LocalUserAuthorizer implements TerminalAuthorizer {
69
92
  }
70
93
  }
71
94
 
95
+ /**
96
+ * The answer an ask gets when the session released it before a human ruled.
97
+ *
98
+ * Mirrors `ParentAuthorizer`'s abandonment: `confirmationUnavailable` keeps it
99
+ * out of the "User denied" family, since a user who was never asked denied
100
+ * nothing (#719), and the agent-facing reason and the provenance record reuse
101
+ * one string so what the model is told and what the log attributes cannot
102
+ * drift (#726).
103
+ */
104
+ function unansweredDecision(reason: string): PermissionPromptDecision {
105
+ return {
106
+ approved: false,
107
+ state: "denied",
108
+ confirmationUnavailable: true,
109
+ denialReason: reason,
110
+ decidedBy: { kind: "unavailable", reason },
111
+ };
112
+ }
113
+
72
114
  /**
73
115
  * The dialog options this ask offers, composed from three independent groups.
74
116
  *
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { AskDialogRelease } from "#src/authority/ask-dialog-queue";
2
3
  import type { ConfigIssueReporting } from "#src/config/config-issue-reporter";
3
4
  import { PERMISSION_SYSTEM_STATUS_KEY } from "#src/config/status";
4
5
  import type { DecisionSummaryWriter } from "#src/logging/decision-audit";
@@ -21,6 +22,15 @@ interface ResourcesDiscoverPayload {
21
22
  * Shown when project config is skipped because the project is untrusted, so the
22
23
  * reduced-scope state is never silent (#644). Exported for assertion in tests.
23
24
  */
25
+ /**
26
+ * What a permission request still open at shutdown is answered with.
27
+ *
28
+ * Reaches the agent as the block reason and the review log as the decider's
29
+ * `reason`, so both name the same thing (#726).
30
+ */
31
+ export const SESSION_ENDED_REASON =
32
+ "The session ended before this permission request was answered";
33
+
24
34
  export const UNTRUSTED_PROJECT_MESSAGE =
25
35
  "pi-permission-system: project is not trusted — skipping project-scoped " +
26
36
  "permission configuration. Only global policy applies. Grant project trust " +
@@ -40,6 +50,8 @@ export const UNTRUSTED_PROJECT_MESSAGE =
40
50
  * - `configIssues` — reports what is wrong with the extension config, latched
41
51
  * per issue; driven here and on every turn, so an issue already on disk when
42
52
  * the session opens is shown rather than swallowed (#933)
53
+ * - `dialogs` — the session's ask queue, released on shutdown so a gate waiting
54
+ * on a dialog nobody can answer any more is unblocked (#965)
43
55
  */
44
56
  export class SessionLifecycleHandler {
45
57
  constructor(
@@ -49,6 +61,7 @@ export class SessionLifecycleHandler {
49
61
  private readonly logger: SessionLogger,
50
62
  private readonly audit: DecisionSummaryWriter,
51
63
  private readonly configIssues: ConfigIssueReporting,
64
+ private readonly dialogs: AskDialogRelease,
52
65
  ) {}
53
66
 
54
67
  handleSessionStart(
@@ -131,6 +144,9 @@ export class SessionLifecycleHandler {
131
144
  ctx.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
132
145
  }
133
146
  this.audit.writeSummary(this.logger);
147
+ // Ahead of the session shutdown that stops forwarding: a drain awaiting a
148
+ // forwarded ask can only write its response file once that ask is settled.
149
+ this.dialogs.releaseAll(SESSION_ENDED_REASON);
134
150
  this.session.shutdown();
135
151
  this.serviceLifecycle.teardown();
136
152
  return Promise.resolve();
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getAgentDir, getPackageDir } from "@earendil-works/pi-coding-agent";
3
3
  import { warmBashParser } from "#src/access-intent/bash/parser";
4
4
  import { buildResolvedIntentFromMatchValues } from "#src/access-intent/input-normalizer";
5
+ import { AskDialogQueue } from "#src/authority/ask-dialog-queue";
5
6
  import { AuthorizerChainAudit } from "#src/authority/authorizer-chain-audit";
6
7
  import {
7
8
  AuthorizerRegistry,
@@ -132,6 +133,11 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
132
133
 
133
134
  const prompter = new PermissionPrompter({ logger });
134
135
 
136
+ // One queue per factory invocation, so it is rebuilt with the session rather
137
+ // than outliving it: the host holds a single inline dialog slot, and a second
138
+ // presentation there strands the first ask's promise (#965).
139
+ const askDialogQueue = new AskDialogQueue();
140
+
135
141
  // The filesystem half of the serving announcement. `servingRegistry` reaches
136
142
  // an in-process child through `globalThis`; a child in its own process shares
137
143
  // nothing but this directory, so the served session publishes a heartbeat
@@ -149,6 +155,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
149
155
  const authorizerSelection = new AuthorizerSelection({
150
156
  detection: subagentDetection,
151
157
  events: pi.events,
158
+ dialogs: askDialogQueue,
152
159
  getPromptPreferences: () => ({
153
160
  doublePressToConfirm: configStore.current().doublePressToConfirm,
154
161
  budget: resolveRenderBudget(configStore.current()),
@@ -321,6 +328,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
321
328
  logger,
322
329
  audit,
323
330
  configIssueReporter,
331
+ askDialogQueue,
324
332
  );
325
333
  const turnPrep = new SessionTurnPrep(
326
334
  session,