@gotgenes/pi-permission-system 33.0.5 → 33.0.7
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 +19 -0
- package/README.md +2 -2
- package/docs/cross-extension-api.md +2 -0
- package/package.json +1 -1
- package/src/authority/ask-dialog-queue.ts +145 -0
- package/src/authority/authorizer.ts +9 -0
- package/src/authority/local-user-authorizer.ts +44 -2
- package/src/handlers/lifecycle.ts +16 -0
- package/src/index.ts +8 -0
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.7](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.6...pi-permission-system-v33.0.7) (2026-09-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** stop a second permission ask from replacing the first ([e01fca6](https://github.com/gotgenes/pi-packages/commit/e01fca6f40a836e41286306b092db03f53601b54))
|
|
14
|
+
* **pi-permission-system:** answer pending permission asks when the session ends ([e0eb7f8](https://github.com/gotgenes/pi-packages/commit/e0eb7f8b6c511bf874d48d241f28c5bc5f7f6aee))
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* **pi-permission-system:** document per-session ask serialization ([fa3a16a](https://github.com/gotgenes/pi-packages/commit/fa3a16a48e7919ae5eed2a82caec83ce9bd9c755))
|
|
19
|
+
|
|
20
|
+
## [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)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
* snapshot fallow vital signs per package and trend them at phase close ([bc8bbc3](https://github.com/gotgenes/pi-packages/commit/bc8bbc34e32f30c99260aaa05a044df3f445857a)), closes [#966](https://github.com/gotgenes/pi-packages/issues/966)
|
|
26
|
+
|
|
8
27
|
## [33.0.5](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v33.0.4...pi-permission-system-v33.0.5) (2026-09-20)
|
|
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
|
@@ -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
|
-
|
|
56
|
-
|
|
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,
|