@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.
- package/CHANGELOG.md +19 -0
- package/package.json +1 -1
- package/src/config-modal.ts +10 -8
- package/src/config-store.ts +13 -34
- 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 +68 -51
- package/src/permission-event-rpc.ts +19 -15
- package/src/permission-prompter.ts +4 -3
- package/src/permission-session.ts +10 -67
- package/src/permissions-service.ts +3 -5
- package/src/prompting-gateway.ts +104 -0
- package/src/session-logger.ts +63 -12
- package/test/composition-root.test.ts +85 -1
- package/test/config-modal.test.ts +13 -7
- package/test/config-store.test.ts +23 -49
- 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 +40 -112
- package/test/prompting-gateway.test.ts +230 -0
- package/test/session-logger.test.ts +151 -64
- package/src/runtime.ts +0 -147
- 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
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. */
|
|
@@ -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:
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
120
|
+
ctx?.ui.notify(warning, "warning");
|
|
141
121
|
} else if (!warning) {
|
|
142
122
|
this.lastConfigWarning = null;
|
|
143
123
|
}
|
|
144
124
|
|
|
145
|
-
this.deps.logger.
|
|
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.
|
|
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.
|
|
213
|
+
this.deps.logger.review(
|
|
235
214
|
"config.resolved",
|
|
236
215
|
entry as unknown as Record<string, unknown>,
|
|
237
216
|
);
|
|
238
|
-
this.deps.logger.
|
|
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:
|
|
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
|
@@ -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 {
|
|
28
|
+
import { PromptingGateway } from "./prompting-gateway";
|
|
26
29
|
import { PermissionServiceLifecycle } from "./service-lifecycle";
|
|
27
30
|
import { createSessionLogger } from "./session-logger";
|
|
28
|
-
import {
|
|
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
|
|
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:
|
|
45
|
-
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
|
-
|
|
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:
|
|
61
|
-
|
|
81
|
+
config: configStore,
|
|
82
|
+
logger,
|
|
62
83
|
events: pi.events,
|
|
63
84
|
forwarder,
|
|
64
85
|
});
|
|
65
86
|
|
|
66
|
-
|
|
87
|
+
configStore.refresh();
|
|
67
88
|
|
|
68
|
-
const
|
|
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
|
-
|
|
72
|
-
|
|
97
|
+
paths,
|
|
98
|
+
logger,
|
|
73
99
|
new ForwardingManager(
|
|
74
|
-
|
|
100
|
+
paths.subagentSessionsDir,
|
|
75
101
|
forwarder,
|
|
76
102
|
subagentRegistry,
|
|
77
103
|
),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
permissionManager,
|
|
123
|
+
sessionRules,
|
|
124
|
+
session,
|
|
108
125
|
requestPermissionDecisionFromUi,
|
|
109
|
-
|
|
126
|
+
logger,
|
|
110
127
|
});
|
|
111
128
|
|
|
112
129
|
const permissionsService = new LocalPermissionsService(
|
|
113
|
-
|
|
114
|
-
|
|
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,
|
|
162
|
+
const gateRunner = new GateRunner(session, session, gateway, reporter);
|
|
146
163
|
const toolCallGatePipeline = new ToolCallGatePipeline(
|
|
147
164
|
session,
|
|
148
165
|
formatterRegistry,
|