@gotgenes/pi-permission-system 10.3.1 → 10.5.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 (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +1 -1
  3. package/src/config-modal.ts +10 -8
  4. package/src/config-store.ts +6 -11
  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/bash-command.ts +2 -2
  9. package/src/handlers/gates/bash-external-directory.ts +2 -2
  10. package/src/handlers/gates/bash-path.ts +2 -2
  11. package/src/handlers/gates/path.ts +2 -2
  12. package/src/handlers/gates/runner.ts +3 -3
  13. package/src/handlers/gates/tool-call-gate-pipeline.ts +10 -9
  14. package/src/index.ts +27 -41
  15. package/src/permission-event-rpc.ts +19 -15
  16. package/src/permission-prompter.ts +4 -3
  17. package/src/permission-resolver.ts +69 -2
  18. package/src/permission-session.ts +7 -83
  19. package/src/prompting-gateway.ts +104 -0
  20. package/src/session-logger.ts +17 -3
  21. package/test/config-modal.test.ts +13 -7
  22. package/test/config-store.test.ts +7 -9
  23. package/test/forwarded-permissions/io.test.ts +23 -26
  24. package/test/handlers/external-directory-integration.test.ts +45 -32
  25. package/test/handlers/external-directory-session-dedup.test.ts +47 -57
  26. package/test/handlers/gates/bash-external-directory.test.ts +2 -2
  27. package/test/handlers/gates/bash-path.test.ts +2 -2
  28. package/test/handlers/gates/runner.test.ts +10 -16
  29. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +30 -21
  30. package/test/handlers/input-events.test.ts +19 -4
  31. package/test/handlers/input.test.ts +29 -13
  32. package/test/handlers/tool-call-events.test.ts +23 -5
  33. package/test/helpers/gate-fixtures.ts +11 -15
  34. package/test/helpers/handler-fixtures.ts +31 -50
  35. package/test/permission-event-rpc.test.ts +30 -28
  36. package/test/permission-forwarder.test.ts +6 -5
  37. package/test/permission-prompter.test.ts +28 -28
  38. package/test/permission-resolver.test.ts +194 -0
  39. package/test/permission-session.test.ts +27 -180
  40. package/test/prompting-gateway.test.ts +230 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ 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.5.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.4.0...pi-permission-system-v10.5.0) (2026-06-07)
9
+
10
+
11
+ ### Features
12
+
13
+ * add PermissionResolver class and route gate runner through it ([#340](https://github.com/gotgenes/pi-packages/issues/340)) ([4133601](https://github.com/gotgenes/pi-packages/commit/41336018d495f85b30b7b77fadb5912870f0dedd))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * suppress fallow unused-class-member for pre-Step-8 resolver methods ([#340](https://github.com/gotgenes/pi-packages/issues/340)) ([fd65626](https://github.com/gotgenes/pi-packages/commit/fd65626ae867457edeb829ea28d0ab94fe51dea6))
19
+
20
+ ## [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)
21
+
22
+
23
+ ### Features
24
+
25
+ * add context-owning PromptingGateway ([1885be2](https://github.com/gotgenes/pi-packages/commit/1885be28fb797eb5ed67a7a30d51e58fa73e3ff0))
26
+
27
+
28
+ ### Documentation
29
+
30
+ * mark Phase 4 Step 6 complete; drop unused beforeEach import ([217057a](https://github.com/gotgenes/pi-packages/commit/217057ab5f8a1d3290322b442e267287b31635cf))
31
+
8
32
  ## [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)
9
33
 
10
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "10.3.1",
3
+ "version": "10.5.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. */
@@ -57,12 +58,6 @@ export interface CommandConfigStore extends ConfigReader {
57
58
  ): void;
58
59
  }
59
60
 
60
- /** Narrow logging sink — replaced by an injected logger in Step 3 (#336). */
61
- export interface ConfigStoreLogger {
62
- writeDebugLog(event: string, details?: Record<string, unknown>): void;
63
- writeReviewLog(event: string, details?: Record<string, unknown>): void;
64
- }
65
-
66
61
  /** Narrow view of the manager's resolved policy paths (for `logResolvedPaths`). */
67
62
  export interface ResolvedPolicyPathProvider {
68
63
  getResolvedPolicyPaths(): ResolvedPolicyPaths;
@@ -71,7 +66,7 @@ export interface ResolvedPolicyPathProvider {
71
66
  export interface ConfigStoreDeps {
72
67
  agentDir: string;
73
68
  policyPaths: ResolvedPolicyPathProvider;
74
- logger: ConfigStoreLogger;
69
+ logger: DebugReviewLogger;
75
70
  }
76
71
 
77
72
  /**
@@ -127,7 +122,7 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
127
122
  this.lastConfigWarning = null;
128
123
  }
129
124
 
130
- this.deps.logger.writeDebugLog("config.loaded", {
125
+ this.deps.logger.debug("config.loaded", {
131
126
  warning: warning ?? null,
132
127
  debugLog: runtimeConfig.debugLog,
133
128
  permissionReviewLog: runtimeConfig.permissionReviewLog,
@@ -183,7 +178,7 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
183
178
  syncPermissionSystemStatus(ctx, normalized);
184
179
  this.lastConfigWarning = null;
185
180
 
186
- this.deps.logger.writeDebugLog("config.saved", {
181
+ this.deps.logger.debug("config.saved", {
187
182
  debugLog: normalized.debugLog,
188
183
  permissionReviewLog: normalized.permissionReviewLog,
189
184
  yoloMode: normalized.yoloMode,
@@ -215,11 +210,11 @@ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
215
210
  legacyProjectPolicyDetected,
216
211
  legacyExtensionConfigDetected,
217
212
  });
218
- this.deps.logger.writeReviewLog(
213
+ this.deps.logger.review(
219
214
  "config.resolved",
220
215
  entry as unknown as Record<string, unknown>,
221
216
  );
222
- this.deps.logger.writeDebugLog(
217
+ this.deps.logger.debug(
223
218
  "config.resolved",
224
219
  entry as unknown as Record<string, unknown>,
225
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
  }
@@ -1,6 +1,6 @@
1
1
  import type { BashCommand } from "#src/handlers/gates/bash-program";
2
2
  import { pickMostRestrictive } from "#src/handlers/gates/candidate-check";
3
- import type { PermissionResolver } from "#src/permission-resolver";
3
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
4
  import type { PermissionCheckResult } from "#src/types";
5
5
 
6
6
  /**
@@ -30,7 +30,7 @@ export function resolveBashCommandCheck(
30
30
  command: string,
31
31
  commands: BashCommand[],
32
32
  agentName: string | undefined,
33
- resolver: PermissionResolver,
33
+ resolver: ScopedPermissionResolver,
34
34
  ): PermissionCheckResult {
35
35
  const results = commands.map((cmd) => {
36
36
  const result = resolver.resolve("bash", { command: cmd.text }, agentName);
@@ -1,5 +1,5 @@
1
1
  import { getNonEmptyString, toRecord } from "#src/common";
2
- import type { PermissionResolver } from "#src/permission-resolver";
2
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
3
  import { SessionApproval } from "#src/session-approval";
4
4
  import { deriveApprovalPattern } from "#src/session-rules";
5
5
  import type { PermissionCheckResult } from "#src/types";
@@ -21,7 +21,7 @@ import type { ToolCallContext } from "./types";
21
21
  export function describeBashExternalDirectoryGate(
22
22
  tcc: ToolCallContext,
23
23
  bashProgram: BashProgram | null,
24
- resolver: PermissionResolver,
24
+ resolver: ScopedPermissionResolver,
25
25
  ): GateResult {
26
26
  if (tcc.toolName !== "bash" || !tcc.cwd) return null;
27
27
 
@@ -1,5 +1,5 @@
1
1
  import { getNonEmptyString, toRecord } from "#src/common";
2
- import type { PermissionResolver } from "#src/permission-resolver";
2
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
3
  import { SessionApproval } from "#src/session-approval";
4
4
  import { deriveApprovalPattern } from "#src/session-rules";
5
5
  import type { PermissionCheckResult } from "#src/types";
@@ -25,7 +25,7 @@ import type { ToolCallContext } from "./types";
25
25
  export function describeBashPathGate(
26
26
  tcc: ToolCallContext,
27
27
  bashProgram: BashProgram | null,
28
- resolver: PermissionResolver,
28
+ resolver: ScopedPermissionResolver,
29
29
  ): GateResult {
30
30
  if (tcc.toolName !== "bash") return null;
31
31
 
@@ -1,5 +1,5 @@
1
1
  import { getPathBearingToolPath } from "#src/path-utils";
2
- import type { PermissionResolver } from "#src/permission-resolver";
2
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
3
  import { SessionApproval } from "#src/session-approval";
4
4
  import { deriveApprovalPattern } from "#src/session-rules";
5
5
  import type { GateDescriptor, GateResult } from "./descriptor";
@@ -15,7 +15,7 @@ import type { ToolCallContext } from "./types";
15
15
  */
16
16
  export function describePathGate(
17
17
  tcc: ToolCallContext,
18
- resolver: PermissionResolver,
18
+ resolver: ScopedPermissionResolver,
19
19
  ): GateResult {
20
20
  const filePath = getPathBearingToolPath(tcc.toolName, tcc.input);
21
21
  if (!filePath) return null;
@@ -7,7 +7,7 @@ import {
7
7
  import type { GatePrompter } from "#src/gate-prompter";
8
8
  import type { PermissionPromptDecision } from "#src/permission-dialog";
9
9
  import { applyPermissionGate } from "#src/permission-gate";
10
- import type { PermissionResolver } from "#src/permission-resolver";
10
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
11
11
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
12
12
  import type { PermissionCheckResult } from "#src/types";
13
13
  import type { GateDescriptor, GateResult } from "./descriptor";
@@ -28,7 +28,7 @@ import type { GateOutcome } from "./types";
28
28
  */
29
29
  export class GateRunner {
30
30
  constructor(
31
- private readonly resolver: PermissionResolver,
31
+ private readonly resolver: ScopedPermissionResolver,
32
32
  private readonly recorder: SessionApprovalRecorder,
33
33
  private readonly prompter: GatePrompter,
34
34
  private readonly reporter: DecisionReporter,
@@ -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
  });
@@ -1,5 +1,5 @@
1
1
  import { getNonEmptyString, toRecord } from "#src/common";
2
- import type { PermissionResolver } from "#src/permission-resolver";
2
+ import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
3
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
4
4
  import type { ToolInputFormatterLookup } from "#src/tool-input-formatter-registry";
5
5
  import {
@@ -21,14 +21,14 @@ import type { GateOutcome, ToolCallContext } from "./types";
21
21
  /**
22
22
  * Narrow interface the pipeline needs from its session-side dependency.
23
23
  *
24
- * Extends `PermissionResolver` (the `resolve` method gate factories use)
25
- * with the three query methods needed to assemble gate inputs.
24
+ * The three query methods needed to assemble gate inputs.
25
+ * The resolver is injected separately as a constructor parameter.
26
26
  *
27
27
  * `PermissionSession` satisfies this structurally at the construction call
28
28
  * site; no `implements` clause is needed and would create a layer-inversion
29
29
  * import from the domain module into the handler layer.
30
30
  */
31
- export interface ToolCallGateInputs extends PermissionResolver {
31
+ export interface ToolCallGateInputs {
32
32
  /** Active skill prompt entries for the skill-read gate. */
33
33
  getActiveSkillEntries(): SkillPromptEntry[];
34
34
  /** Combined infrastructure read directories (static + config-derived). */
@@ -50,6 +50,7 @@ export interface ToolCallGateInputs extends PermissionResolver {
50
50
  */
51
51
  export class ToolCallGatePipeline {
52
52
  constructor(
53
+ private readonly resolver: ScopedPermissionResolver,
53
54
  private readonly inputs: ToolCallGateInputs,
54
55
  private readonly customFormatters?: ToolInputFormatterLookup,
55
56
  ) {}
@@ -76,10 +77,10 @@ export class ToolCallGatePipeline {
76
77
  const gateProducers: Array<() => GateResult | Promise<GateResult>> = [
77
78
  () =>
78
79
  describeSkillReadGate(tcc, () => this.inputs.getActiveSkillEntries()),
79
- () => describePathGate(tcc, this.inputs),
80
+ () => describePathGate(tcc, this.resolver),
80
81
  () => describeExternalDirectoryGate(tcc, infraDirs),
81
- () => describeBashExternalDirectoryGate(tcc, bashProgram, this.inputs),
82
- () => describeBashPathGate(tcc, bashProgram, this.inputs),
82
+ () => describeBashExternalDirectoryGate(tcc, bashProgram, this.resolver),
83
+ () => describeBashPathGate(tcc, bashProgram, this.resolver),
83
84
  () => {
84
85
  // Bash commands may chain several sub-commands (`a && b`, `a | b`, …);
85
86
  // evaluate each unit from the shared parse on the bash surface and
@@ -91,9 +92,9 @@ export class ToolCallGatePipeline {
91
92
  command ?? "",
92
93
  bashProgram.commands(),
93
94
  tcc.agentName ?? undefined,
94
- this.inputs,
95
+ this.resolver,
95
96
  )
96
- : this.inputs.resolve(
97
+ : this.resolver.resolve(
97
98
  tcc.toolName,
98
99
  tcc.input,
99
100
  tcc.agentName ?? undefined,