@gotgenes/pi-permission-system 19.0.0 → 20.0.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 +23 -0
- package/docs/cross-extension-api.md +17 -126
- package/docs/guides/permission-frontmatter-for-subagent-extensions.md +18 -50
- package/docs/guides/upstream-issue-template.md +2 -2
- package/docs/subagent-integration.md +0 -1
- package/package.json +1 -1
- package/src/{forwarded-permissions/permission-forwarder.ts → authority/approval-escalator.ts} +35 -301
- package/src/authority/forwarded-request-server.ts +285 -0
- package/src/authority/forwarder-context.ts +32 -0
- package/src/{forwarded-permissions/io.ts → authority/forwarding-io.ts} +0 -1
- package/src/{subagent-context.ts → authority/subagent-context.ts} +2 -2
- package/src/authority/subagent-detection.ts +65 -0
- package/src/forwarding-manager.ts +4 -16
- package/src/index.ts +34 -31
- package/src/permission-events.ts +7 -103
- package/src/permission-prompter.ts +1 -1
- package/src/permission-ui-prompt.ts +1 -25
- package/src/prompting-gateway.ts +5 -19
- package/src/service-lifecycle.ts +3 -5
- package/src/service.ts +3 -8
- package/src/session-logger.ts +1 -1
- package/src/permission-event-rpc.ts +0 -227
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
type ForwarderContext,
|
|
4
|
+
getSessionId,
|
|
5
|
+
} from "#src/authority/forwarder-context";
|
|
6
|
+
import type { ConfigReader } from "#src/config-store";
|
|
7
|
+
import { isYoloModeEnabled } from "#src/extension-config";
|
|
8
|
+
import type {
|
|
9
|
+
PermissionDecisionUi,
|
|
10
|
+
PermissionPromptDecision,
|
|
11
|
+
RequestPermissionOptions,
|
|
12
|
+
} from "#src/permission-dialog";
|
|
13
|
+
import {
|
|
14
|
+
emitUiPromptEvent,
|
|
15
|
+
type PermissionEventBus,
|
|
16
|
+
} from "#src/permission-events";
|
|
17
|
+
import {
|
|
18
|
+
type ForwardedPermissionRequest,
|
|
19
|
+
type ForwardedPermissionResponse,
|
|
20
|
+
isForwardedPermissionRequestForSession,
|
|
21
|
+
type PermissionForwardingLocation,
|
|
22
|
+
} from "#src/permission-forwarding";
|
|
23
|
+
import { buildForwardedUiPrompt } from "#src/permission-ui-prompt";
|
|
24
|
+
import type { DebugReviewLogger } from "#src/session-logger";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
cleanupPermissionForwardingLocationIfEmpty,
|
|
28
|
+
ensureDirectoryExists,
|
|
29
|
+
getExistingPermissionForwardingLocation,
|
|
30
|
+
listRequestFiles,
|
|
31
|
+
logPermissionForwardingError,
|
|
32
|
+
logPermissionForwardingWarning,
|
|
33
|
+
readForwardedPermissionRequest,
|
|
34
|
+
safeDeleteFile,
|
|
35
|
+
writeJsonFileAtomic,
|
|
36
|
+
} from "./forwarding-io";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Narrow seam describing what `ForwardingManager` needs from the server: a
|
|
40
|
+
* single method that drains this session's forwarded-permission inbox.
|
|
41
|
+
*
|
|
42
|
+
* Depending on the interface (not the concrete `ForwardedRequestServer`)
|
|
43
|
+
* keeps the manager's unit tests free of casts — they inject a plain
|
|
44
|
+
* `{ processInbox: vi.fn() }` mock.
|
|
45
|
+
*/
|
|
46
|
+
export interface InboxProcessor {
|
|
47
|
+
processInbox(ctx: ForwarderContext): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Constructor config for `ForwardedRequestServer`. */
|
|
51
|
+
export interface ForwardedRequestServerDeps {
|
|
52
|
+
forwardingDir: string;
|
|
53
|
+
logger: DebugReviewLogger;
|
|
54
|
+
/** Event bus used for UI prompt broadcasts. */
|
|
55
|
+
events?: PermissionEventBus;
|
|
56
|
+
requestPermissionDecisionFromUi: (
|
|
57
|
+
ui: PermissionDecisionUi,
|
|
58
|
+
title: string,
|
|
59
|
+
message: string,
|
|
60
|
+
options?: RequestPermissionOptions,
|
|
61
|
+
) => Promise<PermissionPromptDecision>;
|
|
62
|
+
/** Read current config for the retained forwarded-inbox yolo auto-approve check. */
|
|
63
|
+
config: ConfigReader;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Module-private helpers ────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function formatForwardedPermissionPrompt(
|
|
69
|
+
request: ForwardedPermissionRequest,
|
|
70
|
+
): string {
|
|
71
|
+
const agentName = request.requesterAgentName || "unknown";
|
|
72
|
+
const sessionId = request.requesterSessionId || "unknown";
|
|
73
|
+
return [
|
|
74
|
+
`Subagent '${agentName}' requested permission.`,
|
|
75
|
+
`Session ID: ${sessionId}`,
|
|
76
|
+
"",
|
|
77
|
+
request.message,
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── ForwardedRequestServer ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Owner of the serving-down role of the forwarded-permission behavior:
|
|
85
|
+
* draining this session's forwarded-permission inbox and answering each
|
|
86
|
+
* request (auto-approve under yolo, or prompt via the injected UI).
|
|
87
|
+
*/
|
|
88
|
+
export class ForwardedRequestServer implements InboxProcessor {
|
|
89
|
+
private readonly forwardingDir: string;
|
|
90
|
+
private readonly events: PermissionEventBus | undefined;
|
|
91
|
+
private readonly logger: DebugReviewLogger;
|
|
92
|
+
private readonly requestPermissionDecisionFromUi: (
|
|
93
|
+
ui: PermissionDecisionUi,
|
|
94
|
+
title: string,
|
|
95
|
+
message: string,
|
|
96
|
+
options?: RequestPermissionOptions,
|
|
97
|
+
) => Promise<PermissionPromptDecision>;
|
|
98
|
+
private readonly config: ConfigReader;
|
|
99
|
+
|
|
100
|
+
constructor(deps: ForwardedRequestServerDeps) {
|
|
101
|
+
this.forwardingDir = deps.forwardingDir;
|
|
102
|
+
this.events = deps.events;
|
|
103
|
+
this.logger = deps.logger;
|
|
104
|
+
this.requestPermissionDecisionFromUi = deps.requestPermissionDecisionFromUi;
|
|
105
|
+
this.config = deps.config;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Drain and respond to this session's forwarded-permission inbox. */
|
|
109
|
+
async processInbox(ctx: ForwarderContext): Promise<void> {
|
|
110
|
+
if (!ctx.hasUI) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const currentSessionId = getSessionId(ctx);
|
|
115
|
+
const location = getExistingPermissionForwardingLocation(
|
|
116
|
+
this.forwardingDir,
|
|
117
|
+
currentSessionId,
|
|
118
|
+
);
|
|
119
|
+
if (!location) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const requestFiles = listRequestFiles(this.logger, location.requestsDir);
|
|
124
|
+
if (requestFiles.length === 0) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Defensively recreate responses/ before writing any response — a
|
|
129
|
+
// concurrent cleanup pass may have removed it between the requestsDir
|
|
130
|
+
// existence check above and the write inside processSingleForwardedRequest
|
|
131
|
+
// (the ENOENT write loop reported in issue #398).
|
|
132
|
+
if (
|
|
133
|
+
!ensureDirectoryExists(
|
|
134
|
+
this.logger,
|
|
135
|
+
location.responsesDir,
|
|
136
|
+
"permission forwarding responses",
|
|
137
|
+
)
|
|
138
|
+
) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const fileName of requestFiles) {
|
|
143
|
+
const requestPath = join(location.requestsDir, fileName);
|
|
144
|
+
const request = readForwardedPermissionRequest(this.logger, requestPath);
|
|
145
|
+
if (!request) {
|
|
146
|
+
safeDeleteFile(
|
|
147
|
+
this.logger,
|
|
148
|
+
requestPath,
|
|
149
|
+
`${location.label} forwarded permission request`,
|
|
150
|
+
);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await this.processSingleForwardedRequest(
|
|
155
|
+
ctx,
|
|
156
|
+
request,
|
|
157
|
+
location,
|
|
158
|
+
requestPath,
|
|
159
|
+
currentSessionId,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
cleanupPermissionForwardingLocationIfEmpty(this.logger, location);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Private methods ────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
private async processSingleForwardedRequest(
|
|
169
|
+
ctx: ForwarderContext,
|
|
170
|
+
request: ForwardedPermissionRequest,
|
|
171
|
+
location: PermissionForwardingLocation,
|
|
172
|
+
requestPath: string,
|
|
173
|
+
currentSessionId: string,
|
|
174
|
+
): Promise<void> {
|
|
175
|
+
if (!isForwardedPermissionRequestForSession(request, currentSessionId)) {
|
|
176
|
+
logPermissionForwardingWarning(
|
|
177
|
+
this.logger,
|
|
178
|
+
`Ignoring forwarded permission request '${request.id}' because it targets session '${request.targetSessionId}' instead of '${currentSessionId}'`,
|
|
179
|
+
);
|
|
180
|
+
safeDeleteFile(
|
|
181
|
+
this.logger,
|
|
182
|
+
requestPath,
|
|
183
|
+
`${location.label} forwarded permission request`,
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const forwardedPermissionLogDetails = {
|
|
189
|
+
requestId: request.id,
|
|
190
|
+
source: location.label,
|
|
191
|
+
requesterAgentName: request.requesterAgentName,
|
|
192
|
+
requesterSessionId: request.requesterSessionId,
|
|
193
|
+
targetSessionId: request.targetSessionId,
|
|
194
|
+
requestPath,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
let decision: PermissionPromptDecision = {
|
|
198
|
+
approved: false,
|
|
199
|
+
state: "denied",
|
|
200
|
+
};
|
|
201
|
+
// Last yolo check outside the composed ruleset: dissolves when
|
|
202
|
+
// processInbox is refactored onto evaluate() + Authorizer selection in
|
|
203
|
+
// the Phase 9 spine work.
|
|
204
|
+
if (isYoloModeEnabled(this.config.current())) {
|
|
205
|
+
this.logger.review(
|
|
206
|
+
"forwarded_permission.auto_approved",
|
|
207
|
+
forwardedPermissionLogDetails,
|
|
208
|
+
);
|
|
209
|
+
decision = { approved: true, state: "approved" };
|
|
210
|
+
} else {
|
|
211
|
+
this.logger.review(
|
|
212
|
+
"forwarded_permission.prompted",
|
|
213
|
+
forwardedPermissionLogDetails,
|
|
214
|
+
);
|
|
215
|
+
try {
|
|
216
|
+
const forwardedMessage = formatForwardedPermissionPrompt(request);
|
|
217
|
+
if (this.events) {
|
|
218
|
+
emitUiPromptEvent(
|
|
219
|
+
this.events,
|
|
220
|
+
buildForwardedUiPrompt({
|
|
221
|
+
requestId: request.id,
|
|
222
|
+
message: forwardedMessage,
|
|
223
|
+
requesterAgentName: request.requesterAgentName || null,
|
|
224
|
+
requesterSessionId: request.requesterSessionId || null,
|
|
225
|
+
source: request.source ?? null,
|
|
226
|
+
surface: request.surface ?? null,
|
|
227
|
+
value: request.value ?? null,
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
decision = await this.requestPermissionDecisionFromUi(
|
|
232
|
+
ctx.ui,
|
|
233
|
+
"Permission Required (Subagent)",
|
|
234
|
+
forwardedMessage,
|
|
235
|
+
);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
logPermissionForwardingError(
|
|
238
|
+
this.logger,
|
|
239
|
+
"Failed to show forwarded permission confirmation dialog",
|
|
240
|
+
error,
|
|
241
|
+
);
|
|
242
|
+
decision = { approved: false, state: "denied" };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
247
|
+
this.logger.review(
|
|
248
|
+
decision.approved
|
|
249
|
+
? "forwarded_permission.approved"
|
|
250
|
+
: "forwarded_permission.denied",
|
|
251
|
+
{
|
|
252
|
+
requestId: request.id,
|
|
253
|
+
source: location.label,
|
|
254
|
+
requesterAgentName: request.requesterAgentName,
|
|
255
|
+
requesterSessionId: request.requesterSessionId,
|
|
256
|
+
targetSessionId: request.targetSessionId,
|
|
257
|
+
responsePath,
|
|
258
|
+
resolution: decision.state,
|
|
259
|
+
denialReason: decision.denialReason ?? null,
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
try {
|
|
263
|
+
writeJsonFileAtomic(this.logger, responsePath, {
|
|
264
|
+
approved: decision.approved,
|
|
265
|
+
state: decision.state,
|
|
266
|
+
denialReason: decision.denialReason,
|
|
267
|
+
responderSessionId: currentSessionId,
|
|
268
|
+
respondedAt: Date.now(),
|
|
269
|
+
} satisfies ForwardedPermissionResponse);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logPermissionForwardingError(
|
|
272
|
+
this.logger,
|
|
273
|
+
`Failed to write ${location.label} forwarded permission response '${responsePath}'`,
|
|
274
|
+
error,
|
|
275
|
+
);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
safeDeleteFile(
|
|
280
|
+
this.logger,
|
|
281
|
+
requestPath,
|
|
282
|
+
`${location.label} forwarded permission request`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionEntryView } from "#src/active-agent";
|
|
2
|
+
import type { PermissionDecisionUi } from "#src/permission-dialog";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Narrow context the forwarding subsystem reads: the UI gate (`hasUI`), the
|
|
6
|
+
* dialog UI surface, and the three session-manager readers `getSessionId`
|
|
7
|
+
* and the `active-agent` helpers use.
|
|
8
|
+
*
|
|
9
|
+
* A full `ExtensionContext` satisfies this structurally, so production
|
|
10
|
+
* callers pass `ctx` unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export interface ForwarderContext {
|
|
13
|
+
hasUI: boolean;
|
|
14
|
+
ui: PermissionDecisionUi;
|
|
15
|
+
sessionManager: {
|
|
16
|
+
getSessionId(): string;
|
|
17
|
+
getSessionDir(): string;
|
|
18
|
+
getEntries(): readonly SessionEntryView[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Reads the current session id off `ctx`, falling back to `"unknown"`. */
|
|
23
|
+
export function getSessionId(ctx: ForwarderContext): string {
|
|
24
|
+
try {
|
|
25
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
26
|
+
if (typeof sessionId === "string" && sessionId.trim()) {
|
|
27
|
+
return sessionId.trim();
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { posix as posixPath, win32 as winPath } from "node:path";
|
|
2
2
|
|
|
3
|
-
import { SUBAGENT_ENV_HINT_KEYS } from "
|
|
4
|
-
import type { SubagentSessionRegistry } from "
|
|
3
|
+
import { SUBAGENT_ENV_HINT_KEYS } from "#src/permission-forwarding";
|
|
4
|
+
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Narrow context for subagent detection — the only session-manager readers
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isRegisteredSubagentChild,
|
|
3
|
+
isSubagentExecutionContext,
|
|
4
|
+
type SubagentDetectionContext,
|
|
5
|
+
} from "#src/authority/subagent-context";
|
|
6
|
+
import type { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Narrow seam for the ask-path consumers: "is the current session a subagent?"
|
|
10
|
+
*
|
|
11
|
+
* `PromptingGateway`, `ForwardingManager`, and `ApprovalEscalator` depend on
|
|
12
|
+
* this single-method view so their unit tests inject a one-field fake without
|
|
13
|
+
* casts. It is the Authorizer-selection predicate the Phase 9 spine consumes.
|
|
14
|
+
*/
|
|
15
|
+
export interface SubagentDetector {
|
|
16
|
+
isSubagent(ctx: SubagentDetectionContext): boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Narrow seam for the service-publication guard (#302): "is the current
|
|
21
|
+
* session a registered in-process child?"
|
|
22
|
+
*
|
|
23
|
+
* `PermissionServiceLifecycle` depends on this single-method view so a
|
|
24
|
+
* registered child never publishes over its parent's process-global slot.
|
|
25
|
+
*/
|
|
26
|
+
export interface RegisteredChildDetector {
|
|
27
|
+
isRegisteredChild(ctx: SubagentDetectionContext): boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Composition-root inputs for {@link SubagentDetection}. */
|
|
31
|
+
export interface SubagentDetectionDeps {
|
|
32
|
+
subagentSessionsDir: string;
|
|
33
|
+
platform: NodeJS.Platform;
|
|
34
|
+
registry?: SubagentSessionRegistry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Single owner of subagent detection.
|
|
39
|
+
*
|
|
40
|
+
* Constructed once in the composition root with the detection inputs
|
|
41
|
+
* (`subagentSessionsDir`, `platform`, `registry`) and shared across every
|
|
42
|
+
* consumer, replacing the dep triple those consumers previously threaded
|
|
43
|
+
* individually. Delegates to the pure detection functions in
|
|
44
|
+
* {@link ./subagent-context}, holding only the deps.
|
|
45
|
+
*/
|
|
46
|
+
export class SubagentDetection
|
|
47
|
+
implements SubagentDetector, RegisteredChildDetector
|
|
48
|
+
{
|
|
49
|
+
constructor(private readonly deps: SubagentDetectionDeps) {}
|
|
50
|
+
|
|
51
|
+
isSubagent(ctx: SubagentDetectionContext): boolean {
|
|
52
|
+
return isSubagentExecutionContext(
|
|
53
|
+
ctx,
|
|
54
|
+
this.deps.subagentSessionsDir,
|
|
55
|
+
this.deps.platform,
|
|
56
|
+
this.deps.registry,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
isRegisteredChild(ctx: SubagentDetectionContext): boolean {
|
|
61
|
+
return this.deps.registry
|
|
62
|
+
? isRegisteredSubagentChild(ctx, this.deps.registry)
|
|
63
|
+
: false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
|
-
import type {
|
|
2
|
+
import type { InboxProcessor } from "./authority/forwarded-request-server";
|
|
3
|
+
import type { SubagentDetector } from "./authority/subagent-detection";
|
|
4
4
|
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
|
|
5
|
-
import { isSubagentExecutionContext } from "./subagent-context";
|
|
6
|
-
import type { SubagentSessionRegistry } from "./subagent-registry";
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
7
|
* Narrow interface for the forwarding lifecycle used by `PermissionSession`.
|
|
@@ -28,10 +26,8 @@ export class ForwardingManager {
|
|
|
28
26
|
private processing = false;
|
|
29
27
|
|
|
30
28
|
constructor(
|
|
31
|
-
private readonly
|
|
29
|
+
private readonly detection: SubagentDetector,
|
|
32
30
|
private readonly forwarder: InboxProcessor,
|
|
33
|
-
private readonly platform: NodeJS.Platform,
|
|
34
|
-
private readonly registry?: SubagentSessionRegistry,
|
|
35
31
|
) {}
|
|
36
32
|
|
|
37
33
|
/**
|
|
@@ -41,15 +37,7 @@ export class ForwardingManager {
|
|
|
41
37
|
* Stops any existing poll when the context does not qualify for forwarding.
|
|
42
38
|
*/
|
|
43
39
|
start(ctx: ExtensionContext): void {
|
|
44
|
-
if (
|
|
45
|
-
!ctx.hasUI ||
|
|
46
|
-
isSubagentExecutionContext(
|
|
47
|
-
ctx,
|
|
48
|
-
this.subagentSessionsDir,
|
|
49
|
-
this.platform,
|
|
50
|
-
this.registry,
|
|
51
|
-
)
|
|
52
|
-
) {
|
|
40
|
+
if (!ctx.hasUI || this.detection.isSubagent(ctx)) {
|
|
53
41
|
this.stop();
|
|
54
42
|
return;
|
|
55
43
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { getAgentDir, getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import {
|
|
4
|
+
ApprovalEscalator,
|
|
5
|
+
type ApprovalEscalatorDeps,
|
|
6
|
+
} from "./authority/approval-escalator";
|
|
7
|
+
import {
|
|
8
|
+
ForwardedRequestServer,
|
|
9
|
+
type ForwardedRequestServerDeps,
|
|
10
|
+
} from "./authority/forwarded-request-server";
|
|
11
|
+
import { SubagentDetection } from "./authority/subagent-detection";
|
|
3
12
|
import { registerBuiltinToolInputFormatters } from "./builtin-tool-input-formatters";
|
|
4
13
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
5
14
|
import { getGlobalConfigPath } from "./config-paths";
|
|
@@ -8,10 +17,6 @@ import { DecisionAudit } from "./decision-audit";
|
|
|
8
17
|
import { GateDecisionReporter } from "./decision-reporter";
|
|
9
18
|
import { isYoloModeEnabled } from "./extension-config";
|
|
10
19
|
import { computeExtensionPaths } from "./extension-paths";
|
|
11
|
-
import {
|
|
12
|
-
PermissionForwarder,
|
|
13
|
-
type PermissionForwarderDeps,
|
|
14
|
-
} from "./forwarded-permissions/permission-forwarder";
|
|
15
20
|
import { ForwardingManager } from "./forwarding-manager";
|
|
16
21
|
import {
|
|
17
22
|
AgentPrepHandler,
|
|
@@ -23,7 +28,6 @@ import { SkillInputGatePipeline } from "./handlers/gates/skill-input-gate-pipeli
|
|
|
23
28
|
import { ToolCallGatePipeline } from "./handlers/gates/tool-call-gate-pipeline";
|
|
24
29
|
import { createFailClosedToolCall } from "./handlers/tool-call-boundary";
|
|
25
30
|
import { requestPermissionDecisionFromUi } from "./permission-dialog";
|
|
26
|
-
import { registerPermissionRpcHandlers } from "./permission-event-rpc";
|
|
27
31
|
import { PermissionManager } from "./permission-manager";
|
|
28
32
|
import { PermissionPrompter } from "./permission-prompter";
|
|
29
33
|
import { PermissionResolver } from "./permission-resolver";
|
|
@@ -49,6 +53,13 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
49
53
|
const hostPlatform = process.platform;
|
|
50
54
|
const sessionRules = new SessionRules();
|
|
51
55
|
const subagentRegistry = getSubagentSessionRegistry();
|
|
56
|
+
// Single owner of subagent detection, shared across every consumer instead of
|
|
57
|
+
// threading the (subagentSessionsDir, platform, registry) triple into each.
|
|
58
|
+
const subagentDetection = new SubagentDetection({
|
|
59
|
+
subagentSessionsDir: paths.subagentSessionsDir,
|
|
60
|
+
platform: hostPlatform,
|
|
61
|
+
registry: subagentRegistry,
|
|
62
|
+
});
|
|
52
63
|
const formatterRegistry = new ToolInputFormatterRegistry();
|
|
53
64
|
registerBuiltinToolInputFormatters(formatterRegistry);
|
|
54
65
|
const accessExtractorRegistry = new ToolAccessExtractorRegistry();
|
|
@@ -83,39 +94,38 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
83
94
|
logger,
|
|
84
95
|
});
|
|
85
96
|
|
|
86
|
-
const
|
|
97
|
+
const escalatorDeps: ApprovalEscalatorDeps = {
|
|
87
98
|
forwardingDir: paths.forwardingDir,
|
|
88
|
-
|
|
89
|
-
platform: hostPlatform,
|
|
99
|
+
detection: subagentDetection,
|
|
90
100
|
registry: subagentRegistry,
|
|
91
|
-
events: pi.events,
|
|
92
101
|
logger,
|
|
93
102
|
requestPermissionDecisionFromUi,
|
|
103
|
+
};
|
|
104
|
+
const escalator = new ApprovalEscalator(escalatorDeps);
|
|
105
|
+
|
|
106
|
+
const requestServerDeps: ForwardedRequestServerDeps = {
|
|
107
|
+
forwardingDir: paths.forwardingDir,
|
|
108
|
+
logger,
|
|
109
|
+
events: pi.events,
|
|
110
|
+
requestPermissionDecisionFromUi,
|
|
94
111
|
config: configStore,
|
|
95
112
|
};
|
|
96
|
-
const
|
|
113
|
+
const requestServer = new ForwardedRequestServer(requestServerDeps);
|
|
97
114
|
|
|
98
115
|
const prompter = new PermissionPrompter({
|
|
99
116
|
logger,
|
|
100
117
|
events: pi.events,
|
|
101
|
-
forwarder,
|
|
118
|
+
forwarder: escalator,
|
|
102
119
|
});
|
|
103
120
|
|
|
104
121
|
const gateway = new PromptingGateway({
|
|
105
|
-
|
|
106
|
-
platform: hostPlatform,
|
|
107
|
-
registry: subagentRegistry,
|
|
122
|
+
detection: subagentDetection,
|
|
108
123
|
prompter,
|
|
109
124
|
});
|
|
110
125
|
|
|
111
126
|
session = new PermissionSession(
|
|
112
127
|
paths,
|
|
113
|
-
new ForwardingManager(
|
|
114
|
-
paths.subagentSessionsDir,
|
|
115
|
-
forwarder,
|
|
116
|
-
hostPlatform,
|
|
117
|
-
subagentRegistry,
|
|
118
|
-
),
|
|
128
|
+
new ForwardingManager(subagentDetection, requestServer),
|
|
119
129
|
permissionManager,
|
|
120
130
|
sessionRules,
|
|
121
131
|
configStore,
|
|
@@ -139,17 +149,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
139
149
|
});
|
|
140
150
|
|
|
141
151
|
// Resolver composes the manager + session ruleset and owns the
|
|
142
|
-
// access-path → path-values unwrap; the
|
|
143
|
-
// queries through it, so it is constructed before
|
|
152
|
+
// access-path → path-values unwrap; the service routes its policy
|
|
153
|
+
// queries through it, so it is constructed before it.
|
|
144
154
|
const resolver = new PermissionResolver(permissionManager, sessionRules);
|
|
145
155
|
|
|
146
|
-
const rpcHandles = registerPermissionRpcHandlers(pi.events, {
|
|
147
|
-
resolver,
|
|
148
|
-
session,
|
|
149
|
-
requestPermissionDecisionFromUi,
|
|
150
|
-
logger,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
156
|
const permissionsService = new LocalPermissionsService(
|
|
154
157
|
resolver,
|
|
155
158
|
session,
|
|
@@ -171,9 +174,9 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
171
174
|
// requires the session id from ctx, unavailable at factory-init time.
|
|
172
175
|
const serviceLifecycle = new PermissionServiceLifecycle(
|
|
173
176
|
permissionsService,
|
|
174
|
-
|
|
177
|
+
subagentDetection,
|
|
175
178
|
pi.events,
|
|
176
|
-
[
|
|
179
|
+
[unsubSubagentLifecycle],
|
|
177
180
|
);
|
|
178
181
|
|
|
179
182
|
const toolRegistry = {
|