@gotgenes/pi-permission-system 10.3.1 → 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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/config-modal.ts +10 -8
- package/src/config-store.ts +6 -11
- package/src/forwarded-permissions/io.ts +16 -22
- package/src/forwarded-permissions/permission-forwarder.ts +16 -19
- package/src/gate-prompter.ts +1 -3
- package/src/handlers/gates/runner.ts +1 -1
- package/src/index.ts +22 -40
- package/src/permission-event-rpc.ts +19 -15
- package/src/permission-prompter.ts +4 -3
- package/src/permission-session.ts +7 -63
- package/src/prompting-gateway.ts +104 -0
- package/src/session-logger.ts +17 -3
- package/test/config-modal.test.ts +13 -7
- package/test/config-store.test.ts +7 -9
- package/test/forwarded-permissions/io.test.ts +23 -26
- package/test/handlers/external-directory-integration.test.ts +45 -32
- package/test/handlers/external-directory-session-dedup.test.ts +36 -46
- package/test/handlers/gates/runner.test.ts +10 -16
- package/test/handlers/input-events.test.ts +19 -4
- package/test/handlers/input.test.ts +29 -13
- package/test/handlers/tool-call-events.test.ts +23 -5
- package/test/helpers/gate-fixtures.ts +6 -6
- package/test/helpers/handler-fixtures.ts +24 -39
- package/test/permission-event-rpc.test.ts +30 -28
- package/test/permission-forwarder.test.ts +6 -5
- package/test/permission-prompter.test.ts +28 -28
- package/test/permission-session.test.ts +27 -112
- package/test/prompting-gateway.test.ts +230 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ 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
|
+
|
|
8
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)
|
|
9
21
|
|
|
10
22
|
|
package/package.json
CHANGED
package/src/config-modal.ts
CHANGED
|
@@ -14,9 +14,12 @@ import type { Ruleset } from "./rule";
|
|
|
14
14
|
|
|
15
15
|
interface PermissionSystemConfigController {
|
|
16
16
|
config: CommandConfigStore;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
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
|
|
package/src/config-store.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
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.
|
|
213
|
+
this.deps.logger.review(
|
|
219
214
|
"config.resolved",
|
|
220
215
|
entry as unknown as Record<string, unknown>,
|
|
221
216
|
);
|
|
222
|
-
this.deps.logger.
|
|
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:
|
|
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?.
|
|
82
|
-
logger?.
|
|
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:
|
|
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?.
|
|
100
|
-
logger?.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
469
|
-
this.
|
|
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.
|
|
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.
|
|
508
|
+
this.logger.review(
|
|
512
509
|
decision.approved
|
|
513
510
|
? "forwarded_permission.approved"
|
|
514
511
|
: "forwarded_permission.denied",
|
package/src/gate-prompter.ts
CHANGED
|
@@ -8,7 +8,5 @@ import type { PromptPermissionDetails } from "./permission-prompter";
|
|
|
8
8
|
*/
|
|
9
9
|
export interface GatePrompter {
|
|
10
10
|
canConfirm(): boolean;
|
|
11
|
-
|
|
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.
|
|
124
|
+
const decision = await this.prompter.prompt({
|
|
125
125
|
requestId: toolCallId,
|
|
126
126
|
...descriptor.promptDetails,
|
|
127
127
|
});
|
package/src/index.ts
CHANGED
|
@@ -25,17 +25,13 @@ import { PermissionManager } from "./permission-manager";
|
|
|
25
25
|
import { PermissionPrompter } from "./permission-prompter";
|
|
26
26
|
import { PermissionSession } from "./permission-session";
|
|
27
27
|
import { LocalPermissionsService } from "./permissions-service";
|
|
28
|
+
import { PromptingGateway } from "./prompting-gateway";
|
|
28
29
|
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
29
30
|
import { createSessionLogger } from "./session-logger";
|
|
30
31
|
import { SessionRules } from "./session-rules";
|
|
31
|
-
import { isSubagentExecutionContext } from "./subagent-context";
|
|
32
32
|
import { subscribeSubagentLifecycle } from "./subagent-lifecycle-events";
|
|
33
33
|
import { getSubagentSessionRegistry } from "./subagent-registry";
|
|
34
34
|
import { ToolInputFormatterRegistry } from "./tool-input-formatter-registry";
|
|
35
|
-
import {
|
|
36
|
-
canResolveAskPermissionRequest,
|
|
37
|
-
shouldAutoApprovePermissionState,
|
|
38
|
-
} from "./yolo-mode";
|
|
39
35
|
|
|
40
36
|
export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
41
37
|
const agentDir = getAgentDir();
|
|
@@ -67,10 +63,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
67
63
|
configStore = new ConfigStore({
|
|
68
64
|
agentDir,
|
|
69
65
|
policyPaths: permissionManager,
|
|
70
|
-
logger
|
|
71
|
-
writeDebugLog: (e, d) => logger.debug(e, d),
|
|
72
|
-
writeReviewLog: (e, d) => logger.review(e, d),
|
|
73
|
-
},
|
|
66
|
+
logger,
|
|
74
67
|
});
|
|
75
68
|
|
|
76
69
|
const forwardingDeps: PermissionForwarderDeps = {
|
|
@@ -78,26 +71,28 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
78
71
|
subagentSessionsDir: paths.subagentSessionsDir,
|
|
79
72
|
registry: subagentRegistry,
|
|
80
73
|
events: pi.events,
|
|
81
|
-
logger
|
|
82
|
-
writeReviewLog: (event, details) => logger.review(event, details),
|
|
83
|
-
writeDebugLog: (event, details) => logger.debug(event, details),
|
|
84
|
-
},
|
|
85
|
-
writeReviewLog: (event, details) => logger.review(event, details),
|
|
74
|
+
logger,
|
|
86
75
|
requestPermissionDecisionFromUi,
|
|
87
|
-
|
|
88
|
-
shouldAutoApprovePermissionState("ask", configStore.current()),
|
|
76
|
+
config: configStore,
|
|
89
77
|
};
|
|
90
78
|
const forwarder = new PermissionForwarder(forwardingDeps);
|
|
91
79
|
|
|
92
80
|
const prompter = new PermissionPrompter({
|
|
93
81
|
config: configStore,
|
|
94
|
-
|
|
82
|
+
logger,
|
|
95
83
|
events: pi.events,
|
|
96
84
|
forwarder,
|
|
97
85
|
});
|
|
98
86
|
|
|
99
87
|
configStore.refresh();
|
|
100
88
|
|
|
89
|
+
const gateway = new PromptingGateway({
|
|
90
|
+
config: configStore,
|
|
91
|
+
subagentSessionsDir: paths.subagentSessionsDir,
|
|
92
|
+
registry: subagentRegistry,
|
|
93
|
+
prompter,
|
|
94
|
+
});
|
|
95
|
+
|
|
101
96
|
const session = new PermissionSession(
|
|
102
97
|
paths,
|
|
103
98
|
logger,
|
|
@@ -109,39 +104,26 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
109
104
|
permissionManager,
|
|
110
105
|
sessionRules,
|
|
111
106
|
configStore,
|
|
112
|
-
|
|
113
|
-
canRequestPermissionConfirmation: (ctx) =>
|
|
114
|
-
canResolveAskPermissionRequest({
|
|
115
|
-
config: configStore.current(),
|
|
116
|
-
hasUI: ctx.hasUI,
|
|
117
|
-
isSubagent: isSubagentExecutionContext(
|
|
118
|
-
ctx,
|
|
119
|
-
paths.subagentSessionsDir,
|
|
120
|
-
subagentRegistry,
|
|
121
|
-
),
|
|
122
|
-
}),
|
|
123
|
-
promptPermission: (ctx, details) => prompter.prompt(ctx, details),
|
|
124
|
-
},
|
|
107
|
+
gateway,
|
|
125
108
|
);
|
|
126
109
|
|
|
127
110
|
// Connect the notify sink now that session is available.
|
|
128
111
|
sessionNotify = session;
|
|
129
112
|
|
|
113
|
+
const configPath = getGlobalConfigPath(agentDir);
|
|
130
114
|
registerPermissionSystemCommand(pi, {
|
|
131
115
|
config: configStore,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
session.lastKnownActiveAgentName ?? undefined,
|
|
136
|
-
),
|
|
116
|
+
configPath,
|
|
117
|
+
permissionManager,
|
|
118
|
+
session,
|
|
137
119
|
});
|
|
138
120
|
|
|
139
121
|
const rpcHandles = registerPermissionRpcHandlers(pi.events, {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
permissionManager,
|
|
123
|
+
sessionRules,
|
|
124
|
+
session,
|
|
143
125
|
requestPermissionDecisionFromUi,
|
|
144
|
-
|
|
126
|
+
logger,
|
|
145
127
|
});
|
|
146
128
|
|
|
147
129
|
const permissionsService = new LocalPermissionsService(
|
|
@@ -177,7 +159,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
177
159
|
const lifecycle = new SessionLifecycleHandler(session, serviceLifecycle);
|
|
178
160
|
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
179
161
|
const reporter = new GateDecisionReporter(session.logger, pi.events);
|
|
180
|
-
const gateRunner = new GateRunner(session, session,
|
|
162
|
+
const gateRunner = new GateRunner(session, session, gateway, reporter);
|
|
181
163
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
182
164
|
session,
|
|
183
165
|
formatterRegistry,
|
|
@@ -28,19 +28,20 @@ import {
|
|
|
28
28
|
} from "./permission-events";
|
|
29
29
|
import type { PermissionManager } from "./permission-manager";
|
|
30
30
|
import { buildRpcUiPrompt } from "./permission-ui-prompt";
|
|
31
|
-
import type {
|
|
31
|
+
import type { ReviewLogger } from "./session-logger";
|
|
32
|
+
import type { SessionRules } from "./session-rules";
|
|
32
33
|
|
|
33
34
|
/** Dependencies injected into the RPC handler registry. */
|
|
34
35
|
export interface PermissionRpcDeps {
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
36
|
+
/** The shared PermissionManager instance. */
|
|
37
|
+
permissionManager: Pick<PermissionManager, "checkPermission">;
|
|
38
|
+
/** The shared SessionRules instance. */
|
|
39
|
+
sessionRules: Pick<SessionRules, "getRuleset">;
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
+
* Narrow session view: provides runtime context.
|
|
41
42
|
* Used by the prompt handler to check hasUI and access the UI dialog.
|
|
42
43
|
*/
|
|
43
|
-
getRuntimeContext(): ExtensionContext | null;
|
|
44
|
+
session: { getRuntimeContext(): ExtensionContext | null };
|
|
44
45
|
/** Show the interactive permission dialog in the parent session UI. */
|
|
45
46
|
requestPermissionDecisionFromUi(
|
|
46
47
|
ui: ExtensionContext["ui"],
|
|
@@ -48,8 +49,8 @@ export interface PermissionRpcDeps {
|
|
|
48
49
|
message: string,
|
|
49
50
|
options?: RequestPermissionOptions,
|
|
50
51
|
): Promise<PermissionPromptDecision>;
|
|
51
|
-
/** Write
|
|
52
|
-
|
|
52
|
+
/** Write review-log entries for prompted decisions. */
|
|
53
|
+
logger: ReviewLogger;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/** Unsubscribe handles returned from registerPermissionRpcHandlers. */
|
|
@@ -107,10 +108,13 @@ function handleCheckRpc(
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const input = buildInputForSurface(surface, value);
|
|
110
|
-
const sessionRules = deps.
|
|
111
|
-
const result = deps
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const sessionRules = deps.sessionRules.getRuleset();
|
|
112
|
+
const result = deps.permissionManager.checkPermission(
|
|
113
|
+
surface,
|
|
114
|
+
input,
|
|
115
|
+
agentName ?? undefined,
|
|
116
|
+
sessionRules,
|
|
117
|
+
);
|
|
114
118
|
|
|
115
119
|
const data: PermissionsCheckReplyData = {
|
|
116
120
|
result: result.state,
|
|
@@ -141,7 +145,7 @@ async function handlePromptRpc(
|
|
|
141
145
|
|
|
142
146
|
const replyChannel = `${PERMISSIONS_RPC_PROMPT_CHANNEL}:reply:${requestId}`;
|
|
143
147
|
|
|
144
|
-
const ctx = deps.getRuntimeContext();
|
|
148
|
+
const ctx = deps.session.getRuntimeContext();
|
|
145
149
|
if (!ctx?.hasUI) {
|
|
146
150
|
events.emit(replyChannel, errorReply("no_ui"));
|
|
147
151
|
return;
|
|
@@ -169,7 +173,7 @@ async function handlePromptRpc(
|
|
|
169
173
|
sessionLabel ? { sessionLabel } : undefined,
|
|
170
174
|
);
|
|
171
175
|
|
|
172
|
-
deps.
|
|
176
|
+
deps.logger.review("permission_request.rpc_prompt", {
|
|
173
177
|
requestId,
|
|
174
178
|
surface: surface ?? null,
|
|
175
179
|
value: value ?? null,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type PermissionEventBus,
|
|
8
8
|
} from "./permission-events";
|
|
9
9
|
import { buildDirectUiPrompt } from "./permission-ui-prompt";
|
|
10
|
+
import type { ReviewLogger } from "./session-logger";
|
|
10
11
|
import { shouldAutoApprovePermissionState } from "./yolo-mode";
|
|
11
12
|
|
|
12
13
|
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
@@ -40,14 +41,14 @@ export interface PermissionPrompterApi {
|
|
|
40
41
|
* Dependencies required by PermissionPrompter.
|
|
41
42
|
*
|
|
42
43
|
* Keeps the prompter's external surface narrow: callers provide config
|
|
43
|
-
* access, review
|
|
44
|
+
* access, a review logger, the UI-prompt event bus, and the forwarder
|
|
44
45
|
* that owns the UI/subagent-forwarding branching logic.
|
|
45
46
|
*/
|
|
46
47
|
export interface PermissionPrompterDeps {
|
|
47
48
|
/** Read current config for yolo-mode check (called at prompt time). */
|
|
48
49
|
config: ConfigReader;
|
|
49
50
|
/** Write structured entries to the permission review log. */
|
|
50
|
-
|
|
51
|
+
logger: ReviewLogger;
|
|
51
52
|
/** Event bus used for UI prompt broadcasts. */
|
|
52
53
|
events: PermissionEventBus;
|
|
53
54
|
/** Resolves the permission decision: direct UI dialog or forwarded to parent. */
|
|
@@ -122,7 +123,7 @@ export class PermissionPrompter implements PermissionPrompterApi {
|
|
|
122
123
|
denialReason?: string;
|
|
123
124
|
},
|
|
124
125
|
): void {
|
|
125
|
-
this.deps.
|
|
126
|
+
this.deps.logger.review(event, {
|
|
126
127
|
requestId: details.requestId,
|
|
127
128
|
source: details.source,
|
|
128
129
|
agentName: details.agentName,
|