@gotgenes/pi-permission-system 3.6.0 → 3.7.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 +18 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +112 -0
- package/src/handlers/index.ts +16 -0
- package/src/handlers/input.ts +97 -0
- package/src/handlers/lifecycle.ts +80 -0
- package/src/handlers/tool-call.ts +400 -0
- package/src/handlers/types.ts +95 -0
- package/src/index.ts +101 -701
- package/tests/handlers/before-agent-start.test.ts +274 -0
- package/tests/handlers/input.test.ts +271 -0
- package/tests/handlers/lifecycle.test.ts +331 -0
- package/tests/handlers/tool-call.test.ts +418 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
ToolCallEvent,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
import { getNonEmptyString, toRecord } from "../common";
|
|
8
|
+
import {
|
|
9
|
+
extractExternalPathsFromBashCommand,
|
|
10
|
+
formatBashExternalDirectoryAskPrompt,
|
|
11
|
+
formatBashExternalDirectoryDenyReason,
|
|
12
|
+
formatExternalDirectoryAskPrompt,
|
|
13
|
+
formatExternalDirectoryDenyReason,
|
|
14
|
+
formatExternalDirectoryHardStopHint,
|
|
15
|
+
formatExternalDirectoryUserDeniedReason,
|
|
16
|
+
getPathBearingToolPath,
|
|
17
|
+
isPathOutsideWorkingDirectory,
|
|
18
|
+
normalizePathForComparison,
|
|
19
|
+
PATH_BEARING_TOOLS,
|
|
20
|
+
} from "../external-directory";
|
|
21
|
+
import type { PermissionPromptDecision } from "../permission-dialog";
|
|
22
|
+
import { applyPermissionGate } from "../permission-gate";
|
|
23
|
+
import {
|
|
24
|
+
formatAskPrompt,
|
|
25
|
+
formatDenyReason,
|
|
26
|
+
formatMissingToolNameReason,
|
|
27
|
+
formatSkillPathAskPrompt,
|
|
28
|
+
formatSkillPathDenyReason,
|
|
29
|
+
formatUnknownToolReason,
|
|
30
|
+
formatUserDeniedReason,
|
|
31
|
+
} from "../permission-prompts";
|
|
32
|
+
import { deriveApprovalPrefix } from "../session-approval-cache";
|
|
33
|
+
import { findSkillPathMatch } from "../skill-prompt-sanitizer";
|
|
34
|
+
import { getPermissionLogContext } from "../tool-input-preview";
|
|
35
|
+
import {
|
|
36
|
+
checkRequestedToolRegistration,
|
|
37
|
+
getToolNameFromValue,
|
|
38
|
+
} from "../tool-registry";
|
|
39
|
+
import type { HandlerDeps } from "./types";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract the tool input from an event, checking both `input` and `arguments`
|
|
43
|
+
* fields (different Pi SDK versions use different names).
|
|
44
|
+
*/
|
|
45
|
+
export function getEventInput(event: unknown): unknown {
|
|
46
|
+
const record = toRecord(event);
|
|
47
|
+
|
|
48
|
+
if (record.input !== undefined) {
|
|
49
|
+
return record.input;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (record.arguments !== undefined) {
|
|
53
|
+
return record.arguments;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function handleToolCall(
|
|
60
|
+
deps: HandlerDeps,
|
|
61
|
+
event: unknown,
|
|
62
|
+
ctx: ExtensionContext,
|
|
63
|
+
): Promise<{ block?: true; reason?: string }> {
|
|
64
|
+
deps.setRuntimeContext(ctx);
|
|
65
|
+
deps.startForwardedPermissionPolling(ctx);
|
|
66
|
+
|
|
67
|
+
const agentName = deps.resolveAgentName(ctx);
|
|
68
|
+
const toolName = getToolNameFromValue(event);
|
|
69
|
+
|
|
70
|
+
if (!toolName) {
|
|
71
|
+
return { block: true, reason: formatMissingToolNameReason() };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const registrationCheck = checkRequestedToolRegistration(
|
|
75
|
+
toolName,
|
|
76
|
+
deps.getAllTools(),
|
|
77
|
+
);
|
|
78
|
+
if (registrationCheck.status === "missing-tool-name") {
|
|
79
|
+
return { block: true, reason: formatMissingToolNameReason() };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (registrationCheck.status === "unregistered") {
|
|
83
|
+
return {
|
|
84
|
+
block: true,
|
|
85
|
+
reason: formatUnknownToolReason(
|
|
86
|
+
registrationCheck.requestedToolName,
|
|
87
|
+
registrationCheck.availableToolNames,
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Skill-read gate ──────────────────────────────────────────────────────
|
|
93
|
+
if (
|
|
94
|
+
isToolCallEventType("read", event as ToolCallEvent) &&
|
|
95
|
+
deps.getActiveSkillEntries().length > 0
|
|
96
|
+
) {
|
|
97
|
+
const normalizedReadPath = normalizePathForComparison(
|
|
98
|
+
(event as ToolCallEvent & { input: { path: string } }).input.path,
|
|
99
|
+
ctx.cwd,
|
|
100
|
+
);
|
|
101
|
+
const matchedSkill = findSkillPathMatch(
|
|
102
|
+
normalizedReadPath,
|
|
103
|
+
deps.getActiveSkillEntries(),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (matchedSkill) {
|
|
107
|
+
const readEvent = event as ToolCallEvent & { input: { path: string } };
|
|
108
|
+
const skillReadMessage = formatSkillPathAskPrompt(
|
|
109
|
+
matchedSkill,
|
|
110
|
+
readEvent.input.path,
|
|
111
|
+
agentName ?? undefined,
|
|
112
|
+
);
|
|
113
|
+
const skillReadGate = await applyPermissionGate({
|
|
114
|
+
state: matchedSkill.state,
|
|
115
|
+
canConfirm: deps.canRequestPermissionConfirmation(ctx),
|
|
116
|
+
promptForApproval: () =>
|
|
117
|
+
deps.promptPermission(ctx, {
|
|
118
|
+
requestId: (readEvent as { toolCallId: string }).toolCallId,
|
|
119
|
+
source: "skill_read",
|
|
120
|
+
agentName,
|
|
121
|
+
message: skillReadMessage,
|
|
122
|
+
toolCallId: (readEvent as { toolCallId: string }).toolCallId,
|
|
123
|
+
toolName,
|
|
124
|
+
skillName: matchedSkill.name,
|
|
125
|
+
path: readEvent.input.path,
|
|
126
|
+
}),
|
|
127
|
+
writeLog: deps.writeReviewLog,
|
|
128
|
+
logContext: {
|
|
129
|
+
source: "skill_read",
|
|
130
|
+
skillName: matchedSkill.name,
|
|
131
|
+
agentName,
|
|
132
|
+
path: readEvent.input.path,
|
|
133
|
+
message: skillReadMessage,
|
|
134
|
+
},
|
|
135
|
+
messages: {
|
|
136
|
+
denyReason: formatSkillPathDenyReason(
|
|
137
|
+
matchedSkill,
|
|
138
|
+
readEvent.input.path,
|
|
139
|
+
agentName ?? undefined,
|
|
140
|
+
),
|
|
141
|
+
unavailableReason: `Accessing skill '${matchedSkill.name}' requires approval, but no interactive UI is available.`,
|
|
142
|
+
userDeniedReason: (decision) => {
|
|
143
|
+
const denialReason = decision.denialReason
|
|
144
|
+
? ` Reason: ${decision.denialReason}.`
|
|
145
|
+
: "";
|
|
146
|
+
return `User denied access to skill '${matchedSkill.name}'.${denialReason}`;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
if (skillReadGate.action === "block") {
|
|
151
|
+
return { block: true, reason: skillReadGate.reason };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const input = getEventInput(event);
|
|
157
|
+
|
|
158
|
+
// ── External-directory gate (file tools) ─────────────────────────────────
|
|
159
|
+
const externalDirectoryPath = ctx.cwd
|
|
160
|
+
? getPathBearingToolPath(toolName, input)
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
ctx.cwd &&
|
|
165
|
+
externalDirectoryPath &&
|
|
166
|
+
isPathOutsideWorkingDirectory(externalDirectoryPath, ctx.cwd)
|
|
167
|
+
) {
|
|
168
|
+
const normalizedExtPath = normalizePathForComparison(
|
|
169
|
+
externalDirectoryPath,
|
|
170
|
+
ctx.cwd,
|
|
171
|
+
);
|
|
172
|
+
const sessionPrefix = deps.sessionApprovalCache.findMatchingPrefix(
|
|
173
|
+
"external_directory",
|
|
174
|
+
normalizedExtPath,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (sessionPrefix) {
|
|
178
|
+
deps.writeReviewLog("permission_request.session_approved", {
|
|
179
|
+
source: "tool_call",
|
|
180
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
181
|
+
toolName,
|
|
182
|
+
agentName,
|
|
183
|
+
path: externalDirectoryPath,
|
|
184
|
+
resolution: "session_approved",
|
|
185
|
+
sessionApprovalPrefix: sessionPrefix,
|
|
186
|
+
});
|
|
187
|
+
// Fall through to normal permission check
|
|
188
|
+
} else {
|
|
189
|
+
const extCheck = deps
|
|
190
|
+
.getPermissionManager()
|
|
191
|
+
.checkPermission("external_directory", {}, agentName ?? undefined);
|
|
192
|
+
|
|
193
|
+
let extDirDecision: PermissionPromptDecision | null = null;
|
|
194
|
+
const extDirMessage = formatExternalDirectoryAskPrompt(
|
|
195
|
+
toolName,
|
|
196
|
+
externalDirectoryPath,
|
|
197
|
+
ctx.cwd,
|
|
198
|
+
agentName ?? undefined,
|
|
199
|
+
);
|
|
200
|
+
const extDirGate = await applyPermissionGate({
|
|
201
|
+
state: extCheck.state,
|
|
202
|
+
canConfirm: deps.canRequestPermissionConfirmation(ctx),
|
|
203
|
+
promptForApproval: async () => {
|
|
204
|
+
const decision = await deps.promptPermission(ctx, {
|
|
205
|
+
requestId: (event as { toolCallId: string }).toolCallId,
|
|
206
|
+
source: "tool_call",
|
|
207
|
+
agentName,
|
|
208
|
+
message: extDirMessage,
|
|
209
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
210
|
+
toolName,
|
|
211
|
+
path: externalDirectoryPath,
|
|
212
|
+
});
|
|
213
|
+
extDirDecision = decision;
|
|
214
|
+
return decision;
|
|
215
|
+
},
|
|
216
|
+
writeLog: deps.writeReviewLog,
|
|
217
|
+
logContext: {
|
|
218
|
+
source: "tool_call",
|
|
219
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
220
|
+
toolName,
|
|
221
|
+
agentName,
|
|
222
|
+
path: externalDirectoryPath,
|
|
223
|
+
message: extDirMessage,
|
|
224
|
+
},
|
|
225
|
+
messages: {
|
|
226
|
+
denyReason: formatExternalDirectoryDenyReason(
|
|
227
|
+
toolName,
|
|
228
|
+
externalDirectoryPath,
|
|
229
|
+
ctx.cwd,
|
|
230
|
+
agentName ?? undefined,
|
|
231
|
+
),
|
|
232
|
+
unavailableReason: `Accessing '${externalDirectoryPath}' outside the working directory requires approval, but no interactive UI is available.`,
|
|
233
|
+
userDeniedReason: (decision) =>
|
|
234
|
+
formatExternalDirectoryUserDeniedReason(
|
|
235
|
+
toolName,
|
|
236
|
+
externalDirectoryPath,
|
|
237
|
+
decision.denialReason,
|
|
238
|
+
),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
if (extDirGate.action === "block") {
|
|
242
|
+
return { block: true, reason: extDirGate.reason };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (extDirDecision?.state === "approved_for_session") {
|
|
246
|
+
const prefix = deriveApprovalPrefix(normalizedExtPath);
|
|
247
|
+
deps.sessionApprovalCache.approve("external_directory", prefix);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Fall through to normal permission check
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Bash external-directory gate ─────────────────────────────────────────
|
|
254
|
+
if (ctx.cwd && toolName === "bash") {
|
|
255
|
+
const command = getNonEmptyString(toRecord(input).command);
|
|
256
|
+
if (command) {
|
|
257
|
+
const externalPaths = extractExternalPathsFromBashCommand(
|
|
258
|
+
command,
|
|
259
|
+
ctx.cwd,
|
|
260
|
+
);
|
|
261
|
+
if (externalPaths.length > 0) {
|
|
262
|
+
const uncoveredPaths = externalPaths.filter(
|
|
263
|
+
(p) => !deps.sessionApprovalCache.has("external_directory", p),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (uncoveredPaths.length === 0) {
|
|
267
|
+
deps.writeReviewLog("permission_request.session_approved", {
|
|
268
|
+
source: "tool_call",
|
|
269
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
270
|
+
toolName,
|
|
271
|
+
agentName,
|
|
272
|
+
command,
|
|
273
|
+
externalPaths,
|
|
274
|
+
resolution: "session_approved",
|
|
275
|
+
});
|
|
276
|
+
// Fall through to normal bash permission check
|
|
277
|
+
} else {
|
|
278
|
+
const extCheck = deps
|
|
279
|
+
.getPermissionManager()
|
|
280
|
+
.checkPermission("external_directory", {}, agentName ?? undefined);
|
|
281
|
+
|
|
282
|
+
let bashExtDecision: PermissionPromptDecision | null = null;
|
|
283
|
+
const bashExtMessage = formatBashExternalDirectoryAskPrompt(
|
|
284
|
+
command,
|
|
285
|
+
uncoveredPaths,
|
|
286
|
+
ctx.cwd,
|
|
287
|
+
agentName ?? undefined,
|
|
288
|
+
);
|
|
289
|
+
const bashExtGate = await applyPermissionGate({
|
|
290
|
+
state: extCheck.state,
|
|
291
|
+
canConfirm: deps.canRequestPermissionConfirmation(ctx),
|
|
292
|
+
promptForApproval: async () => {
|
|
293
|
+
const decision = await deps.promptPermission(ctx, {
|
|
294
|
+
requestId: (event as { toolCallId: string }).toolCallId,
|
|
295
|
+
source: "tool_call",
|
|
296
|
+
agentName,
|
|
297
|
+
message: bashExtMessage,
|
|
298
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
299
|
+
toolName,
|
|
300
|
+
command,
|
|
301
|
+
});
|
|
302
|
+
bashExtDecision = decision;
|
|
303
|
+
return decision;
|
|
304
|
+
},
|
|
305
|
+
writeLog: deps.writeReviewLog,
|
|
306
|
+
logContext: {
|
|
307
|
+
source: "tool_call",
|
|
308
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
309
|
+
toolName,
|
|
310
|
+
agentName,
|
|
311
|
+
command,
|
|
312
|
+
externalPaths: uncoveredPaths,
|
|
313
|
+
message: bashExtMessage,
|
|
314
|
+
},
|
|
315
|
+
messages: {
|
|
316
|
+
denyReason: formatBashExternalDirectoryDenyReason(
|
|
317
|
+
command,
|
|
318
|
+
uncoveredPaths,
|
|
319
|
+
ctx.cwd,
|
|
320
|
+
agentName ?? undefined,
|
|
321
|
+
),
|
|
322
|
+
unavailableReason: `Bash command '${command}' references path(s) outside the working directory and requires approval, but no interactive UI is available.`,
|
|
323
|
+
userDeniedReason: (decision) => {
|
|
324
|
+
const reasonSuffix = decision.denialReason
|
|
325
|
+
? ` Reason: ${decision.denialReason}.`
|
|
326
|
+
: "";
|
|
327
|
+
return `User denied external directory access for bash command '${command}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
if (bashExtGate.action === "block") {
|
|
332
|
+
return { block: true, reason: bashExtGate.reason };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (bashExtDecision?.state === "approved_for_session") {
|
|
336
|
+
for (const extPath of uncoveredPaths) {
|
|
337
|
+
const prefix = deriveApprovalPrefix(extPath);
|
|
338
|
+
deps.sessionApprovalCache.approve("external_directory", prefix);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Fall through to normal bash permission check
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Normal tool permission gate ───────────────────────────────────────────
|
|
348
|
+
const check = deps
|
|
349
|
+
.getPermissionManager()
|
|
350
|
+
.checkPermission(toolName, input, agentName ?? undefined);
|
|
351
|
+
const permissionLogContext = getPermissionLogContext(
|
|
352
|
+
check,
|
|
353
|
+
input,
|
|
354
|
+
PATH_BEARING_TOOLS,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const toolUnavailableReason =
|
|
358
|
+
toolName === "bash" && isToolCallEventType("bash", event as ToolCallEvent)
|
|
359
|
+
? `Running bash command '${(event as ToolCallEvent & { input: { command: string } }).input.command}' requires approval, but no interactive UI is available.`
|
|
360
|
+
: toolName === "mcp"
|
|
361
|
+
? "Using tool 'mcp' requires approval, but no interactive UI is available."
|
|
362
|
+
: `Using tool '${toolName}' requires approval, but no interactive UI is available.`;
|
|
363
|
+
|
|
364
|
+
const toolAskMessage = formatAskPrompt(check, agentName ?? undefined, input);
|
|
365
|
+
const toolGate = await applyPermissionGate({
|
|
366
|
+
state: check.state,
|
|
367
|
+
canConfirm: deps.canRequestPermissionConfirmation(ctx),
|
|
368
|
+
promptForApproval: () =>
|
|
369
|
+
deps.promptPermission(ctx, {
|
|
370
|
+
requestId: (event as { toolCallId: string }).toolCallId,
|
|
371
|
+
source: "tool_call",
|
|
372
|
+
agentName,
|
|
373
|
+
message: toolAskMessage,
|
|
374
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
375
|
+
toolName,
|
|
376
|
+
...permissionLogContext,
|
|
377
|
+
}),
|
|
378
|
+
writeLog: deps.writeReviewLog,
|
|
379
|
+
logContext: {
|
|
380
|
+
source: "tool_call",
|
|
381
|
+
toolCallId: (event as { toolCallId: string }).toolCallId,
|
|
382
|
+
toolName,
|
|
383
|
+
agentName,
|
|
384
|
+
message: toolAskMessage,
|
|
385
|
+
...permissionLogContext,
|
|
386
|
+
},
|
|
387
|
+
messages: {
|
|
388
|
+
denyReason: formatDenyReason(check, agentName ?? undefined),
|
|
389
|
+
unavailableReason: toolUnavailableReason,
|
|
390
|
+
userDeniedReason: (decision) =>
|
|
391
|
+
formatUserDeniedReason(check, decision.denialReason),
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (toolGate.action === "block") {
|
|
396
|
+
return { block: true, reason: toolGate.reason };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {};
|
|
400
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { PermissionPromptDecision } from "../permission-dialog";
|
|
4
|
+
import type { PermissionManager } from "../permission-manager";
|
|
5
|
+
import type { SessionApprovalCache } from "../session-approval-cache";
|
|
6
|
+
import type { SkillPromptEntry } from "../skill-prompt-sanitizer";
|
|
7
|
+
|
|
8
|
+
export type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
9
|
+
|
|
10
|
+
/** Details passed when prompting the user for a permission decision. */
|
|
11
|
+
export interface PromptPermissionDetails {
|
|
12
|
+
requestId: string;
|
|
13
|
+
source: PermissionReviewSource;
|
|
14
|
+
agentName: string | null;
|
|
15
|
+
message: string;
|
|
16
|
+
toolCallId?: string;
|
|
17
|
+
toolName?: string;
|
|
18
|
+
skillName?: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
command?: string;
|
|
21
|
+
target?: string;
|
|
22
|
+
toolInputPreview?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Explicit dependency bag passed to each extracted event handler.
|
|
27
|
+
*
|
|
28
|
+
* All mutable shared state is wrapped in getter/setter pairs so that:
|
|
29
|
+
* - Tests can construct a plain object with stubs and exercise handlers in
|
|
30
|
+
* isolation without importing src/index.ts.
|
|
31
|
+
* - Issue #43 can replace this interface with ExtensionRuntime without
|
|
32
|
+
* changing handler signatures — only the deps object construction in
|
|
33
|
+
* index.ts needs to change.
|
|
34
|
+
*/
|
|
35
|
+
export interface HandlerDeps {
|
|
36
|
+
// ── Mutable state accessors ────────────────────────────────────────────
|
|
37
|
+
getPermissionManager(): PermissionManager;
|
|
38
|
+
setPermissionManager(pm: PermissionManager): void;
|
|
39
|
+
getRuntimeContext(): ExtensionContext | null;
|
|
40
|
+
setRuntimeContext(ctx: ExtensionContext | null): void;
|
|
41
|
+
getActiveSkillEntries(): SkillPromptEntry[];
|
|
42
|
+
setActiveSkillEntries(entries: SkillPromptEntry[]): void;
|
|
43
|
+
getLastKnownActiveAgentName(): string | null;
|
|
44
|
+
setLastKnownActiveAgentName(name: string | null): void;
|
|
45
|
+
/** Cache key for the last set of active tools passed to setActiveTools(). */
|
|
46
|
+
getLastActiveToolsCacheKey(): string | null;
|
|
47
|
+
setLastActiveToolsCacheKey(key: string | null): void;
|
|
48
|
+
/** Cache key for the last before_agent_start prompt state. */
|
|
49
|
+
getLastPromptStateCacheKey(): string | null;
|
|
50
|
+
setLastPromptStateCacheKey(key: string | null): void;
|
|
51
|
+
/** Session-scoped approval cache (passed by reference; mutations are visible). */
|
|
52
|
+
sessionApprovalCache: SessionApprovalCache;
|
|
53
|
+
|
|
54
|
+
// ── Factories ──────────────────────────────────────────────────────────
|
|
55
|
+
/** Create a new PermissionManager scoped to cwd's config hierarchy. */
|
|
56
|
+
createPermissionManagerForCwd(
|
|
57
|
+
cwd: string | undefined | null,
|
|
58
|
+
): PermissionManager;
|
|
59
|
+
|
|
60
|
+
// ── Config & lifecycle helpers ─────────────────────────────────────────
|
|
61
|
+
/** Reload merged config from disk; optionally update the stored runtime context. */
|
|
62
|
+
refreshExtensionConfig(ctx?: ExtensionContext): void;
|
|
63
|
+
/** Show a warning notification to the user (no-op when no UI is available). */
|
|
64
|
+
notifyWarning(message: string): void;
|
|
65
|
+
/** Write the resolved config path set to the review and debug logs. */
|
|
66
|
+
logResolvedConfigPaths(): void;
|
|
67
|
+
|
|
68
|
+
// ── Permission helpers ─────────────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the active agent name from the session context or system prompt.
|
|
71
|
+
* Updates the stored lastKnownActiveAgentName as a side effect.
|
|
72
|
+
*/
|
|
73
|
+
resolveAgentName(ctx: ExtensionContext, systemPrompt?: string): string | null;
|
|
74
|
+
/** Whether the current context can show an interactive permission prompt. */
|
|
75
|
+
canRequestPermissionConfirmation(ctx: ExtensionContext): boolean;
|
|
76
|
+
/** Prompt the user for a permission decision, log the outcome, and return it. */
|
|
77
|
+
promptPermission(
|
|
78
|
+
ctx: ExtensionContext,
|
|
79
|
+
details: PromptPermissionDetails,
|
|
80
|
+
): Promise<PermissionPromptDecision>;
|
|
81
|
+
/** Generate a unique ID for a permission request. */
|
|
82
|
+
createPermissionRequestId(prefix: string): string;
|
|
83
|
+
|
|
84
|
+
// ── Forwarding ─────────────────────────────────────────────────────────
|
|
85
|
+
startForwardedPermissionPolling(ctx: ExtensionContext): void;
|
|
86
|
+
stopForwardedPermissionPolling(): void;
|
|
87
|
+
|
|
88
|
+
// ── Logging ────────────────────────────────────────────────────────────
|
|
89
|
+
writeReviewLog(event: string, details?: Record<string, unknown>): void;
|
|
90
|
+
writeDebugLog(event: string, details?: Record<string, unknown>): void;
|
|
91
|
+
|
|
92
|
+
// ── Pi API subset ──────────────────────────────────────────────────────
|
|
93
|
+
getAllTools(): unknown[];
|
|
94
|
+
setActiveTools(names: string[]): void;
|
|
95
|
+
}
|