@gotgenes/pi-permission-system 10.3.0 → 10.4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/src/config-modal.ts +10 -8
  4. package/src/config-store.ts +13 -34
  5. package/src/forwarded-permissions/io.ts +16 -22
  6. package/src/forwarded-permissions/permission-forwarder.ts +16 -19
  7. package/src/gate-prompter.ts +1 -3
  8. package/src/handlers/gates/runner.ts +1 -1
  9. package/src/index.ts +68 -51
  10. package/src/permission-event-rpc.ts +19 -15
  11. package/src/permission-prompter.ts +4 -3
  12. package/src/permission-session.ts +10 -67
  13. package/src/permissions-service.ts +3 -5
  14. package/src/prompting-gateway.ts +104 -0
  15. package/src/session-logger.ts +63 -12
  16. package/test/composition-root.test.ts +85 -1
  17. package/test/config-modal.test.ts +13 -7
  18. package/test/config-store.test.ts +23 -49
  19. package/test/forwarded-permissions/io.test.ts +23 -26
  20. package/test/handlers/external-directory-integration.test.ts +45 -32
  21. package/test/handlers/external-directory-session-dedup.test.ts +36 -46
  22. package/test/handlers/gates/runner.test.ts +10 -16
  23. package/test/handlers/input-events.test.ts +19 -4
  24. package/test/handlers/input.test.ts +29 -13
  25. package/test/handlers/tool-call-events.test.ts +23 -5
  26. package/test/helpers/gate-fixtures.ts +6 -6
  27. package/test/helpers/handler-fixtures.ts +24 -39
  28. package/test/permission-event-rpc.test.ts +30 -28
  29. package/test/permission-forwarder.test.ts +6 -5
  30. package/test/permission-prompter.test.ts +28 -28
  31. package/test/permission-session.test.ts +40 -112
  32. package/test/prompting-gateway.test.ts +230 -0
  33. package/test/session-logger.test.ts +151 -64
  34. package/src/runtime.ts +0 -147
  35. package/test/runtime.test.ts +0 -303
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
+ ## [10.4.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.3.1...pi-permission-system-v10.4.0) (2026-06-07)
9
+
10
+
11
+ ### Features
12
+
13
+ * add context-owning PromptingGateway ([1885be2](https://github.com/gotgenes/pi-packages/commit/1885be28fb797eb5ed67a7a30d51e58fa73e3ff0))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * mark Phase 4 Step 6 complete; drop unused beforeEach import ([217057a](https://github.com/gotgenes/pi-packages/commit/217057ab5f8a1d3290322b442e267287b31635cf))
19
+
20
+ ## [10.3.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.3.0...pi-permission-system-v10.3.1) (2026-06-06)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * share one PermissionManager and SessionRules across gate and RPC paths ([#337](https://github.com/gotgenes/pi-packages/issues/337)) ([7dd1e65](https://github.com/gotgenes/pi-packages/commit/7dd1e65493fa0061a3b84eb329457f939b953e0a))
26
+
8
27
  ## [10.3.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.2.0...pi-permission-system-v10.3.0) (2026-06-05)
9
28
 
10
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "10.3.0",
3
+ "version": "10.4.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,9 +14,12 @@ import type { Ruleset } from "./rule";
14
14
 
15
15
  interface PermissionSystemConfigController {
16
16
  config: CommandConfigStore;
17
- getConfigPath(): string;
18
- /** Optional: returns the composed config-layer ruleset for origin display. */
19
- getComposedRules?(): Ruleset;
17
+ /** Precomputed global config file path. */
18
+ configPath: string;
19
+ /** Returns the composed config-layer ruleset for origin display. */
20
+ permissionManager: { getComposedConfigRules(agentName?: string): Ruleset };
21
+ /** Provides the active agent name for scoped rule lookup. */
22
+ session: { readonly lastKnownActiveAgentName: string | null };
20
23
  }
21
24
 
22
25
  const ON_OFF = ["on", "off"];
@@ -203,7 +206,9 @@ function handleArgs(
203
206
  }
204
207
 
205
208
  if (normalized === "show") {
206
- const rules = controller.getComposedRules?.();
209
+ const rules = controller.permissionManager.getComposedConfigRules(
210
+ controller.session.lastKnownActiveAgentName ?? undefined,
211
+ );
207
212
  ctx.ui.notify(
208
213
  `permission-system: ${summarizeConfig(controller.config.current(), rules)}`,
209
214
  "info",
@@ -212,10 +217,7 @@ function handleArgs(
212
217
  }
213
218
 
214
219
  if (normalized === "path") {
215
- ctx.ui.notify(
216
- `permission-system config: ${controller.getConfigPath()}`,
217
- "info",
218
- );
220
+ ctx.ui.notify(`permission-system config: ${controller.configPath}`, "info");
219
221
  return true;
220
222
  }
221
223
 
@@ -26,6 +26,7 @@ import {
26
26
  type PermissionSystemExtensionConfig,
27
27
  } from "./extension-config";
28
28
  import type { ResolvedPolicyPaths } from "./policy-loader";
29
+ import type { DebugReviewLogger } from "./session-logger";
29
30
  import { syncPermissionSystemStatus } from "./status";
30
31
 
31
32
  /** Read-only view of the current config — for consumers that only read. */
@@ -41,7 +42,7 @@ export interface ConfigReader {
41
42
  */
42
43
  export interface SessionConfigStore extends ConfigReader {
43
44
  refresh(ctx?: ExtensionContext): void;
44
- logResolvedPaths(): void;
45
+ logResolvedPaths(cwd?: string): void;
45
46
  }
46
47
 
47
48
  /**
@@ -57,22 +58,6 @@ export interface CommandConfigStore extends ConfigReader {
57
58
  ): void;
58
59
  }
59
60
 
60
- /**
61
- * Transitional get/set seam over the runtime-owned context.
62
- *
63
- * Retired in Step 4 (#337) when context ownership moves to `PermissionSession`.
64
- */
65
- export interface RuntimeContextRef {
66
- get(): ExtensionContext | null;
67
- set(ctx: ExtensionContext): void;
68
- }
69
-
70
- /** Narrow logging sink — replaced by an injected logger in Step 3 (#336). */
71
- export interface ConfigStoreLogger {
72
- writeDebugLog(event: string, details?: Record<string, unknown>): void;
73
- writeReviewLog(event: string, details?: Record<string, unknown>): void;
74
- }
75
-
76
61
  /** Narrow view of the manager's resolved policy paths (for `logResolvedPaths`). */
77
62
  export interface ResolvedPolicyPathProvider {
78
63
  getResolvedPolicyPaths(): ResolvedPolicyPaths;
@@ -80,9 +65,8 @@ export interface ResolvedPolicyPathProvider {
80
65
 
81
66
  export interface ConfigStoreDeps {
82
67
  agentDir: string;
83
- context: RuntimeContextRef;
84
68
  policyPaths: ResolvedPolicyPathProvider;
85
- logger: ConfigStoreLogger;
69
+ logger: DebugReviewLogger;
86
70
  }
87
71
 
88
72
  /**
@@ -111,14 +95,11 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
111
95
  /**
112
96
  * Reload merged config from disk.
113
97
  *
114
- * If `ctx` is provided, updates the stored runtime context via the seam first.
98
+ * If `ctx` is provided, uses it to derive the cwd and sync UI status.
115
99
  * Equivalent to `refreshExtensionConfig(runtime, ctx?)`.
116
100
  */
117
101
  refresh(ctx?: ExtensionContext): void {
118
- if (ctx) {
119
- this.deps.context.set(ctx);
120
- }
121
- const cwd = this.deps.context.get()?.cwd ?? null;
102
+ const cwd = ctx?.cwd ?? null;
122
103
  const mergeResult = loadAndMergeConfigs(
123
104
  this.deps.agentDir,
124
105
  cwd ?? "",
@@ -127,9 +108,8 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
127
108
  const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
128
109
  this.config = runtimeConfig;
129
110
 
130
- const currentCtx = this.deps.context.get();
131
- if (currentCtx?.hasUI) {
132
- syncPermissionSystemStatus(currentCtx, runtimeConfig);
111
+ if (ctx?.hasUI) {
112
+ syncPermissionSystemStatus(ctx, runtimeConfig);
133
113
  }
134
114
 
135
115
  const warning =
@@ -137,12 +117,12 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
137
117
 
138
118
  if (warning && warning !== this.lastConfigWarning) {
139
119
  this.lastConfigWarning = warning;
140
- currentCtx?.ui.notify(warning, "warning");
120
+ ctx?.ui.notify(warning, "warning");
141
121
  } else if (!warning) {
142
122
  this.lastConfigWarning = null;
143
123
  }
144
124
 
145
- this.deps.logger.writeDebugLog("config.loaded", {
125
+ this.deps.logger.debug("config.loaded", {
146
126
  warning: warning ?? null,
147
127
  debugLog: runtimeConfig.debugLog,
148
128
  permissionReviewLog: runtimeConfig.permissionReviewLog,
@@ -198,7 +178,7 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
198
178
  syncPermissionSystemStatus(ctx, normalized);
199
179
  this.lastConfigWarning = null;
200
180
 
201
- this.deps.logger.writeDebugLog("config.saved", {
181
+ this.deps.logger.debug("config.saved", {
202
182
  debugLog: normalized.debugLog,
203
183
  permissionReviewLog: normalized.permissionReviewLog,
204
184
  yoloMode: normalized.yoloMode,
@@ -210,9 +190,8 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
210
190
  *
211
191
  * Equivalent to `logResolvedConfigPaths(runtime)`.
212
192
  */
213
- logResolvedPaths(): void {
193
+ logResolvedPaths(cwd?: string): void {
214
194
  const policyPaths = this.deps.policyPaths.getResolvedPolicyPaths();
215
- const cwd = this.deps.context.get()?.cwd ?? null;
216
195
  const { agentDir } = this.deps;
217
196
  const legacyGlobalPolicyDetected = existsSync(
218
197
  getLegacyGlobalPolicyPath(agentDir),
@@ -231,11 +210,11 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
231
210
  legacyProjectPolicyDetected,
232
211
  legacyExtensionConfigDetected,
233
212
  });
234
- this.deps.logger.writeReviewLog(
213
+ this.deps.logger.review(
235
214
  "config.resolved",
236
215
  entry as unknown as Record<string, unknown>,
237
216
  );
238
- this.deps.logger.writeDebugLog(
217
+ this.deps.logger.debug(
239
218
  "config.resolved",
240
219
  entry as unknown as Record<string, unknown>,
241
220
  );
@@ -17,6 +17,7 @@ import {
17
17
  type ForwardedPermissionResponse,
18
18
  type PermissionForwardingLocation,
19
19
  } from "#src/permission-forwarding";
20
+ import type { DebugReviewLogger } from "#src/session-logger";
20
21
 
21
22
  /** Valid `permissions:ui_prompt` source values, for tolerant request reads. */
22
23
  const UI_PROMPT_SOURCES = [
@@ -41,13 +42,6 @@ function asNullableDisplayString(value: unknown): string | null | undefined {
41
42
  return undefined;
42
43
  }
43
44
 
44
- type LogFn = (event: string, details: Record<string, unknown>) => void;
45
-
46
- export interface ForwardedPermissionLogger {
47
- writeReviewLog: LogFn;
48
- writeDebugLog: LogFn;
49
- }
50
-
51
45
  export function formatUnknownErrorMessage(error: unknown): string {
52
46
  if (error instanceof Error && error.message) {
53
47
  return error.message;
@@ -69,7 +63,7 @@ export function isErrnoCode(error: unknown, code: string): boolean {
69
63
  * Pass `null` for `logger` to silently no-op (e.g. in unit tests without IO).
70
64
  */
71
65
  export function logPermissionForwardingWarning(
72
- logger: ForwardedPermissionLogger | null,
66
+ logger: DebugReviewLogger | null,
73
67
  message: string,
74
68
  error?: unknown,
75
69
  ): void {
@@ -78,8 +72,8 @@ export function logPermissionForwardingWarning(
78
72
  ? { message }
79
73
  : { message, error: formatUnknownErrorMessage(error) };
80
74
 
81
- logger?.writeReviewLog("permission_forwarding.warning", details);
82
- logger?.writeDebugLog("permission_forwarding.warning", details);
75
+ logger?.review("permission_forwarding.warning", details);
76
+ logger?.debug("permission_forwarding.warning", details);
83
77
  }
84
78
 
85
79
  /**
@@ -87,7 +81,7 @@ export function logPermissionForwardingWarning(
87
81
  * Pass `null` for `logger` to silently no-op (e.g. in unit tests without IO).
88
82
  */
89
83
  export function logPermissionForwardingError(
90
- logger: ForwardedPermissionLogger | null,
84
+ logger: DebugReviewLogger | null,
91
85
  message: string,
92
86
  error?: unknown,
93
87
  ): void {
@@ -96,12 +90,12 @@ export function logPermissionForwardingError(
96
90
  ? { message }
97
91
  : { message, error: formatUnknownErrorMessage(error) };
98
92
 
99
- logger?.writeReviewLog("permission_forwarding.error", details);
100
- logger?.writeDebugLog("permission_forwarding.error", details);
93
+ logger?.review("permission_forwarding.error", details);
94
+ logger?.debug("permission_forwarding.error", details);
101
95
  }
102
96
 
103
97
  export function ensureDirectoryExists(
104
- logger: ForwardedPermissionLogger | null,
98
+ logger: DebugReviewLogger | null,
105
99
  path: string,
106
100
  description: string,
107
101
  ): boolean {
@@ -126,7 +120,7 @@ export function getPermissionForwardingLocationForSession(
126
120
  }
127
121
 
128
122
  export function ensurePermissionForwardingLocation(
129
- logger: ForwardedPermissionLogger | null,
123
+ logger: DebugReviewLogger | null,
130
124
  forwardingDir: string,
131
125
  sessionId: string,
132
126
  ): PermissionForwardingLocation | null {
@@ -182,7 +176,7 @@ export function getExistingPermissionForwardingLocation(
182
176
  }
183
177
 
184
178
  export function tryRemoveDirectoryIfEmpty(
185
- logger: ForwardedPermissionLogger | null,
179
+ logger: DebugReviewLogger | null,
186
180
  path: string,
187
181
  description: string,
188
182
  ): void {
@@ -222,7 +216,7 @@ export function tryRemoveDirectoryIfEmpty(
222
216
  }
223
217
 
224
218
  export function cleanupPermissionForwardingLocationIfEmpty(
225
- logger: ForwardedPermissionLogger | null,
219
+ logger: DebugReviewLogger | null,
226
220
  location: PermissionForwardingLocation,
227
221
  ): void {
228
222
  tryRemoveDirectoryIfEmpty(
@@ -243,7 +237,7 @@ export function cleanupPermissionForwardingLocationIfEmpty(
243
237
  }
244
238
 
245
239
  export function safeDeleteFile(
246
- logger: ForwardedPermissionLogger | null,
240
+ logger: DebugReviewLogger | null,
247
241
  filePath: string,
248
242
  description: string,
249
243
  ): void {
@@ -263,7 +257,7 @@ export function safeDeleteFile(
263
257
  }
264
258
 
265
259
  export function writeJsonFileAtomic(
266
- logger: ForwardedPermissionLogger | null,
260
+ logger: DebugReviewLogger | null,
267
261
  filePath: string,
268
262
  value: unknown,
269
263
  ): void {
@@ -279,7 +273,7 @@ export function writeJsonFileAtomic(
279
273
  }
280
274
 
281
275
  export function readForwardedPermissionRequest(
282
- logger: ForwardedPermissionLogger | null,
276
+ logger: DebugReviewLogger | null,
283
277
  filePath: string,
284
278
  ): ForwardedPermissionRequest | null {
285
279
  try {
@@ -326,7 +320,7 @@ export function readForwardedPermissionRequest(
326
320
  }
327
321
 
328
322
  export function readForwardedPermissionResponse(
329
- logger: ForwardedPermissionLogger | null,
323
+ logger: DebugReviewLogger | null,
330
324
  filePath: string,
331
325
  ): ForwardedPermissionResponse | null {
332
326
  try {
@@ -370,7 +364,7 @@ export function readForwardedPermissionResponse(
370
364
  }
371
365
 
372
366
  export function listRequestFiles(
373
- logger: ForwardedPermissionLogger | null,
367
+ logger: DebugReviewLogger | null,
374
368
  requestsDir: string,
375
369
  ): string[] {
376
370
  try {
@@ -7,6 +7,7 @@ import {
7
7
  getActiveAgentNameFromSystemPrompt,
8
8
  } from "#src/active-agent";
9
9
  import { toRecord } from "#src/common";
10
+ import type { ConfigReader } from "#src/config-store";
10
11
  import type {
11
12
  PermissionPromptDecision,
12
13
  RequestPermissionOptions,
@@ -27,13 +28,14 @@ import {
27
28
  SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
28
29
  } from "#src/permission-forwarding";
29
30
  import { buildForwardedUiPrompt } from "#src/permission-ui-prompt";
31
+ import type { DebugReviewLogger } from "#src/session-logger";
30
32
  import { isSubagentExecutionContext } from "#src/subagent-context";
31
33
  import type { SubagentSessionRegistry } from "#src/subagent-registry";
34
+ import { shouldAutoApprovePermissionState } from "#src/yolo-mode";
32
35
 
33
36
  import {
34
37
  cleanupPermissionForwardingLocationIfEmpty,
35
38
  ensurePermissionForwardingLocation,
36
- type ForwardedPermissionLogger,
37
39
  getExistingPermissionForwardingLocation,
38
40
  listRequestFiles,
39
41
  logPermissionForwardingError,
@@ -59,15 +61,15 @@ export interface PermissionForwarderDeps {
59
61
  registry?: SubagentSessionRegistry;
60
62
  /** Event bus used for UI prompt broadcasts. */
61
63
  events?: PermissionEventBus;
62
- logger: ForwardedPermissionLogger;
63
- writeReviewLog: (event: string, details: Record<string, unknown>) => void;
64
+ logger: DebugReviewLogger;
64
65
  requestPermissionDecisionFromUi: (
65
66
  ui: ExtensionContext["ui"],
66
67
  title: string,
67
68
  message: string,
68
69
  options?: RequestPermissionOptions,
69
70
  ) => Promise<PermissionPromptDecision>;
70
- shouldAutoApprove: () => boolean;
71
+ /** Read current config for yolo-mode auto-approve check (called at prompt time). */
72
+ config: ConfigReader;
71
73
  }
72
74
 
73
75
  // ── Module-private helpers ────────────────────────────────────────────────
@@ -165,18 +167,14 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
165
167
  private readonly subagentSessionsDir: string;
166
168
  private readonly registry: SubagentSessionRegistry | undefined;
167
169
  private readonly events: PermissionEventBus | undefined;
168
- private readonly logger: ForwardedPermissionLogger;
169
- private readonly writeReviewLog: (
170
- event: string,
171
- details: Record<string, unknown>,
172
- ) => void;
170
+ private readonly logger: DebugReviewLogger;
173
171
  private readonly requestPermissionDecisionFromUi: (
174
172
  ui: ExtensionContext["ui"],
175
173
  title: string,
176
174
  message: string,
177
175
  options?: RequestPermissionOptions,
178
176
  ) => Promise<PermissionPromptDecision>;
179
- private readonly shouldAutoApprove: () => boolean;
177
+ private readonly config: ConfigReader;
180
178
 
181
179
  constructor(deps: PermissionForwarderDeps) {
182
180
  this.forwardingDir = deps.forwardingDir;
@@ -184,9 +182,8 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
184
182
  this.registry = deps.registry;
185
183
  this.events = deps.events;
186
184
  this.logger = deps.logger;
187
- this.writeReviewLog = deps.writeReviewLog;
188
185
  this.requestPermissionDecisionFromUi = deps.requestPermissionDecisionFromUi;
189
- this.shouldAutoApprove = deps.shouldAutoApprove;
186
+ this.config = deps.config;
190
187
  }
191
188
 
192
189
  // ── Public seam methods ────────────────────────────────────────────────
@@ -319,7 +316,7 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
319
316
  const requestPath = join(location.requestsDir, `${request.id}.json`);
320
317
  const responsePath = join(location.responsesDir, `${request.id}.json`);
321
318
 
322
- this.writeReviewLog("forwarded_permission.request_created", {
319
+ this.logger.review("forwarded_permission.request_created", {
323
320
  requestId: request.id,
324
321
  requesterAgentName: request.requesterAgentName,
325
322
  requesterSessionId: request.requesterSessionId,
@@ -391,7 +388,7 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
391
388
  this.logger,
392
389
  responsePath,
393
390
  );
394
- this.writeReviewLog("forwarded_permission.response_received", {
391
+ this.logger.review("forwarded_permission.response_received", {
395
392
  requestId,
396
393
  approved: response?.approved ?? null,
397
394
  state: response?.state ?? null,
@@ -421,7 +418,7 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
421
418
  this.logger,
422
419
  `Timed out waiting for forwarded permission response '${responsePath}'`,
423
420
  );
424
- this.writeReviewLog("forwarded_permission.response_timed_out", {
421
+ this.logger.review("forwarded_permission.response_timed_out", {
425
422
  requestId,
426
423
  requesterAgentName,
427
424
  targetSessionId,
@@ -465,14 +462,14 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
465
462
  approved: false,
466
463
  state: "denied",
467
464
  };
468
- if (this.shouldAutoApprove()) {
469
- this.writeReviewLog(
465
+ if (shouldAutoApprovePermissionState("ask", this.config.current())) {
466
+ this.logger.review(
470
467
  "forwarded_permission.auto_approved",
471
468
  forwardedPermissionLogDetails,
472
469
  );
473
470
  decision = { approved: true, state: "approved" };
474
471
  } else {
475
- this.writeReviewLog(
472
+ this.logger.review(
476
473
  "forwarded_permission.prompted",
477
474
  forwardedPermissionLogDetails,
478
475
  );
@@ -508,7 +505,7 @@ export class PermissionForwarder implements ApprovalRequester, InboxProcessor {
508
505
  }
509
506
 
510
507
  const responsePath = join(location.responsesDir, `${request.id}.json`);
511
- this.writeReviewLog(
508
+ this.logger.review(
512
509
  decision.approved
513
510
  ? "forwarded_permission.approved"
514
511
  : "forwarded_permission.denied",
@@ -8,7 +8,5 @@ import type { PromptPermissionDetails } from "./permission-prompter";
8
8
  */
9
9
  export interface GatePrompter {
10
10
  canConfirm(): boolean;
11
- promptPermission(
12
- details: PromptPermissionDetails,
13
- ): Promise<PermissionPromptDecision>;
11
+ prompt(details: PromptPermissionDetails): Promise<PermissionPromptDecision>;
14
12
  }
@@ -121,7 +121,7 @@ export class GateRunner {
121
121
  canConfirm,
122
122
  sessionApproval: descriptor.sessionApproval?.toGateApproval(),
123
123
  promptForApproval: async () => {
124
- const decision = await this.prompter.promptPermission({
124
+ const decision = await this.prompter.prompt({
125
125
  requestId: toolCallId,
126
126
  ...descriptor.promptDetails,
127
127
  });
package/src/index.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
3
  import { registerBuiltinToolInputFormatters } from "./builtin-tool-input-formatters";
3
4
  import { registerPermissionSystemCommand } from "./config-modal";
4
5
  import { getGlobalConfigPath } from "./config-paths";
6
+ import { ConfigStore } from "./config-store";
5
7
  import { GateDecisionReporter } from "./decision-reporter";
8
+ import { computeExtensionPaths } from "./extension-paths";
6
9
  import {
7
10
  PermissionForwarder,
8
11
  type PermissionForwarderDeps,
@@ -22,96 +25,110 @@ import { PermissionManager } from "./permission-manager";
22
25
  import { PermissionPrompter } from "./permission-prompter";
23
26
  import { PermissionSession } from "./permission-session";
24
27
  import { LocalPermissionsService } from "./permissions-service";
25
- import { createExtensionRuntime } from "./runtime";
28
+ import { PromptingGateway } from "./prompting-gateway";
26
29
  import { PermissionServiceLifecycle } from "./service-lifecycle";
27
30
  import { createSessionLogger } from "./session-logger";
28
- import { isSubagentExecutionContext } from "./subagent-context";
31
+ import { SessionRules } from "./session-rules";
29
32
  import { subscribeSubagentLifecycle } from "./subagent-lifecycle-events";
30
33
  import { getSubagentSessionRegistry } from "./subagent-registry";
31
34
  import { ToolInputFormatterRegistry } from "./tool-input-formatter-registry";
32
- import {
33
- canResolveAskPermissionRequest,
34
- shouldAutoApprovePermissionState,
35
- } from "./yolo-mode";
36
35
 
37
36
  export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
38
- const runtime = createExtensionRuntime();
37
+ const agentDir = getAgentDir();
38
+ const paths = computeExtensionPaths(agentDir);
39
+ const permissionManager = new PermissionManager({ agentDir });
40
+ const sessionRules = new SessionRules();
39
41
  const subagentRegistry = getSubagentSessionRegistry();
40
42
  const formatterRegistry = new ToolInputFormatterRegistry();
41
43
  registerBuiltinToolInputFormatters(formatterRegistry);
42
44
 
45
+ // Forward reference: configStore is declared before the logger so the
46
+ // logger's getConfig thunk can close over the variable; assigned immediately
47
+ // after. Typed via cast so the closure compiles without assertions.
48
+ // The same null-at-init pattern used in the former createExtensionRuntime.
49
+ let configStore = null as unknown as ConfigStore;
50
+
51
+ // sessionNotify is a mutable holder so the logger's notify closure can
52
+ // reach the UI once PermissionSession is constructed. Starts as null;
53
+ // notify is a best-effort sink (no-op at factory-init when there is no UI).
54
+ let sessionNotify: PermissionSession | null = null;
55
+
56
+ const logger = createSessionLogger({
57
+ globalLogsDir: paths.globalLogsDir,
58
+ getConfig: () => configStore.current(),
59
+ notify: (message) =>
60
+ sessionNotify?.getRuntimeContext()?.ui.notify(message, "warning"),
61
+ });
62
+
63
+ configStore = new ConfigStore({
64
+ agentDir,
65
+ policyPaths: permissionManager,
66
+ logger,
67
+ });
68
+
43
69
  const forwardingDeps: PermissionForwarderDeps = {
44
- forwardingDir: runtime.forwardingDir,
45
- subagentSessionsDir: runtime.subagentSessionsDir,
70
+ forwardingDir: paths.forwardingDir,
71
+ subagentSessionsDir: paths.subagentSessionsDir,
46
72
  registry: subagentRegistry,
47
73
  events: pi.events,
48
- logger: {
49
- writeReviewLog: runtime.writeReviewLog.bind(runtime),
50
- writeDebugLog: runtime.writeDebugLog.bind(runtime),
51
- },
52
- writeReviewLog: runtime.writeReviewLog.bind(runtime),
74
+ logger,
53
75
  requestPermissionDecisionFromUi,
54
- shouldAutoApprove: () =>
55
- shouldAutoApprovePermissionState("ask", runtime.configStore.current()),
76
+ config: configStore,
56
77
  };
57
78
  const forwarder = new PermissionForwarder(forwardingDeps);
58
79
 
59
80
  const prompter = new PermissionPrompter({
60
- config: runtime.configStore,
61
- writeReviewLog: runtime.writeReviewLog.bind(runtime),
81
+ config: configStore,
82
+ logger,
62
83
  events: pi.events,
63
84
  forwarder,
64
85
  });
65
86
 
66
- runtime.configStore.refresh();
87
+ configStore.refresh();
67
88
 
68
- const sessionManager = new PermissionManager({ agentDir: runtime.agentDir });
89
+ const gateway = new PromptingGateway({
90
+ config: configStore,
91
+ subagentSessionsDir: paths.subagentSessionsDir,
92
+ registry: subagentRegistry,
93
+ prompter,
94
+ });
69
95
 
70
96
  const session = new PermissionSession(
71
- runtime,
72
- createSessionLogger(runtime),
97
+ paths,
98
+ logger,
73
99
  new ForwardingManager(
74
- runtime.subagentSessionsDir,
100
+ paths.subagentSessionsDir,
75
101
  forwarder,
76
102
  subagentRegistry,
77
103
  ),
78
- sessionManager,
79
- runtime.configStore,
80
- {
81
- canRequestPermissionConfirmation: (ctx) =>
82
- canResolveAskPermissionRequest({
83
- config: runtime.configStore.current(),
84
- hasUI: ctx.hasUI,
85
- isSubagent: isSubagentExecutionContext(
86
- ctx,
87
- runtime.subagentSessionsDir,
88
- subagentRegistry,
89
- ),
90
- }),
91
- promptPermission: (ctx, details) => prompter.prompt(ctx, details),
92
- },
104
+ permissionManager,
105
+ sessionRules,
106
+ configStore,
107
+ gateway,
93
108
  );
94
109
 
110
+ // Connect the notify sink now that session is available.
111
+ sessionNotify = session;
112
+
113
+ const configPath = getGlobalConfigPath(agentDir);
95
114
  registerPermissionSystemCommand(pi, {
96
- config: runtime.configStore,
97
- getConfigPath: () => getGlobalConfigPath(runtime.agentDir),
98
- getComposedRules: () =>
99
- runtime.permissionManager.getComposedConfigRules(
100
- runtime.lastKnownActiveAgentName ?? undefined,
101
- ),
115
+ config: configStore,
116
+ configPath,
117
+ permissionManager,
118
+ session,
102
119
  });
103
120
 
104
121
  const rpcHandles = registerPermissionRpcHandlers(pi.events, {
105
- getPermissionManager: () => runtime.permissionManager,
106
- getSessionRules: () => runtime.sessionRules.getRuleset(),
107
- getRuntimeContext: () => runtime.runtimeContext,
122
+ permissionManager,
123
+ sessionRules,
124
+ session,
108
125
  requestPermissionDecisionFromUi,
109
- writeReviewLog: runtime.writeReviewLog.bind(runtime),
126
+ logger,
110
127
  });
111
128
 
112
129
  const permissionsService = new LocalPermissionsService(
113
- runtime.permissionManager,
114
- runtime.sessionRules,
130
+ permissionManager,
131
+ sessionRules,
115
132
  formatterRegistry,
116
133
  );
117
134
 
@@ -142,7 +159,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
142
159
  const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
143
160
  const agentPrep = new AgentPrepHandler(session, toolRegistry);
144
161
  const reporter = new GateDecisionReporter(session.logger, pi.events);
145
- const gateRunner = new GateRunner(session, session, session, reporter);
162
+ const gateRunner = new GateRunner(session, session, gateway, reporter);
146
163
  const toolCallGatePipeline = new ToolCallGatePipeline(
147
164
  session,
148
165
  formatterRegistry,