@gotgenes/pi-permission-system 3.6.0 → 3.8.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 +36 -0
- package/package.json +1 -1
- package/src/forwarded-permissions/io.ts +47 -12
- package/src/forwarded-permissions/polling.ts +33 -11
- package/src/handlers/before-agent-start.ts +112 -0
- package/src/handlers/index.ts +16 -0
- package/src/handlers/input.ts +99 -0
- package/src/handlers/lifecycle.ts +81 -0
- package/src/handlers/tool-call.ts +410 -0
- package/src/handlers/types.ts +72 -0
- package/src/index.ts +73 -1040
- package/src/runtime.ts +484 -0
- package/tests/forwarded-permissions/io.test.ts +135 -0
- package/tests/handlers/before-agent-start.test.ts +290 -0
- package/tests/handlers/input.test.ts +301 -0
- package/tests/handlers/lifecycle.test.ts +352 -0
- package/tests/handlers/tool-call.test.ts +441 -0
- package/tests/runtime.test.ts +618 -0
package/src/index.ts
CHANGED
|
@@ -1,1066 +1,99 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
mkdirSync,
|
|
4
|
-
renameSync,
|
|
5
|
-
unlinkSync,
|
|
6
|
-
writeFileSync,
|
|
7
|
-
} from "node:fs";
|
|
8
|
-
import { dirname, join, normalize } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
type ExtensionAPI,
|
|
11
|
-
type ExtensionCommandContext,
|
|
12
|
-
type ExtensionContext,
|
|
13
|
-
getAgentDir,
|
|
14
|
-
isToolCallEventType,
|
|
15
|
-
} from "@mariozechner/pi-coding-agent";
|
|
16
|
-
import {
|
|
17
|
-
getActiveAgentName,
|
|
18
|
-
getActiveAgentNameFromSystemPrompt,
|
|
19
|
-
} from "./active-agent";
|
|
20
|
-
import {
|
|
21
|
-
createActiveToolsCacheKey,
|
|
22
|
-
createBeforeAgentStartPromptStateKey,
|
|
23
|
-
shouldApplyCachedAgentStartState,
|
|
24
|
-
} from "./before-agent-start-cache";
|
|
25
|
-
import { getNonEmptyString, toRecord } from "./common";
|
|
26
|
-
import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
27
2
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
3
|
+
import { getGlobalConfigPath } from "./config-paths";
|
|
4
|
+
import type { PermissionForwardingDeps } from "./forwarded-permissions/polling";
|
|
28
5
|
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} from "./config-paths";
|
|
38
|
-
import { buildResolvedConfigLogEntry } from "./config-reporter";
|
|
39
|
-
import {
|
|
40
|
-
DEFAULT_EXTENSION_CONFIG,
|
|
41
|
-
EXTENSION_ROOT,
|
|
42
|
-
ensurePermissionSystemLogsDirectory,
|
|
43
|
-
normalizePermissionSystemConfig,
|
|
44
|
-
type PermissionSystemExtensionConfig,
|
|
45
|
-
} from "./extension-config";
|
|
46
|
-
import {
|
|
47
|
-
extractExternalPathsFromBashCommand,
|
|
48
|
-
formatBashExternalDirectoryAskPrompt,
|
|
49
|
-
formatBashExternalDirectoryDenyReason,
|
|
50
|
-
formatExternalDirectoryAskPrompt,
|
|
51
|
-
formatExternalDirectoryDenyReason,
|
|
52
|
-
formatExternalDirectoryHardStopHint,
|
|
53
|
-
formatExternalDirectoryUserDeniedReason,
|
|
54
|
-
getPathBearingToolPath,
|
|
55
|
-
isPathOutsideWorkingDirectory,
|
|
56
|
-
normalizePathForComparison,
|
|
57
|
-
PATH_BEARING_TOOLS,
|
|
58
|
-
} from "./external-directory";
|
|
59
|
-
import { setForwardedPermissionLogger } from "./forwarded-permissions/io";
|
|
60
|
-
import {
|
|
61
|
-
confirmPermission,
|
|
62
|
-
type PermissionForwardingDeps,
|
|
63
|
-
processForwardedPermissionRequests,
|
|
64
|
-
} from "./forwarded-permissions/polling";
|
|
65
|
-
import { createPermissionSystemLogger } from "./logging";
|
|
6
|
+
type HandlerDeps,
|
|
7
|
+
handleBeforeAgentStart,
|
|
8
|
+
handleInput,
|
|
9
|
+
handleResourcesDiscover,
|
|
10
|
+
handleSessionShutdown,
|
|
11
|
+
handleSessionStart,
|
|
12
|
+
handleToolCall,
|
|
13
|
+
} from "./handlers";
|
|
66
14
|
import {
|
|
67
15
|
type PermissionPromptDecision,
|
|
68
16
|
requestPermissionDecisionFromUi,
|
|
69
17
|
} from "./permission-dialog";
|
|
70
|
-
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
|
|
71
|
-
import { applyPermissionGate } from "./permission-gate";
|
|
72
|
-
import { PermissionManager } from "./permission-manager";
|
|
73
|
-
import {
|
|
74
|
-
formatAskPrompt,
|
|
75
|
-
formatDenyReason,
|
|
76
|
-
formatMissingToolNameReason,
|
|
77
|
-
formatSkillAskPrompt,
|
|
78
|
-
formatSkillPathAskPrompt,
|
|
79
|
-
formatSkillPathDenyReason,
|
|
80
|
-
formatUnknownToolReason,
|
|
81
|
-
formatUserDeniedReason,
|
|
82
|
-
} from "./permission-prompts";
|
|
83
|
-
import {
|
|
84
|
-
deriveApprovalPrefix,
|
|
85
|
-
SessionApprovalCache,
|
|
86
|
-
} from "./session-approval-cache";
|
|
87
18
|
import {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
19
|
+
createExtensionRuntime,
|
|
20
|
+
createPermissionManagerForCwd,
|
|
21
|
+
logResolvedConfigPaths,
|
|
22
|
+
promptPermission,
|
|
23
|
+
refreshExtensionConfig,
|
|
24
|
+
resolveAgentName,
|
|
25
|
+
saveExtensionConfig,
|
|
26
|
+
startForwardedPermissionPolling,
|
|
27
|
+
stopForwardedPermissionPolling,
|
|
28
|
+
} from "./runtime";
|
|
96
29
|
import { isSubagentExecutionContext } from "./subagent-context";
|
|
97
|
-
import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer";
|
|
98
|
-
import { getPermissionLogContext } from "./tool-input-preview";
|
|
99
|
-
import {
|
|
100
|
-
checkRequestedToolRegistration,
|
|
101
|
-
getToolNameFromValue,
|
|
102
|
-
} from "./tool-registry";
|
|
103
30
|
import {
|
|
104
31
|
canResolveAskPermissionRequest,
|
|
105
32
|
shouldAutoApprovePermissionState,
|
|
106
33
|
} from "./yolo-mode";
|
|
107
34
|
|
|
108
|
-
const PI_AGENT_DIR = getAgentDir();
|
|
109
|
-
const SESSIONS_DIR = join(PI_AGENT_DIR, "sessions");
|
|
110
|
-
const SUBAGENT_SESSIONS_DIR = join(PI_AGENT_DIR, "subagent-sessions");
|
|
111
|
-
const PERMISSION_FORWARDING_DIR = join(SESSIONS_DIR, "permission-forwarding");
|
|
112
|
-
|
|
113
|
-
type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
114
|
-
|
|
115
|
-
let extensionConfig: PermissionSystemExtensionConfig = {
|
|
116
|
-
...DEFAULT_EXTENSION_CONFIG,
|
|
117
|
-
};
|
|
118
|
-
const GLOBAL_LOGS_DIR = getGlobalLogsDir(PI_AGENT_DIR);
|
|
119
|
-
const extensionLogger = createPermissionSystemLogger({
|
|
120
|
-
getConfig: () => extensionConfig,
|
|
121
|
-
debugLogPath: join(GLOBAL_LOGS_DIR, DEBUG_LOG_FILENAME),
|
|
122
|
-
reviewLogPath: join(GLOBAL_LOGS_DIR, REVIEW_LOG_FILENAME),
|
|
123
|
-
ensureLogsDirectory: () =>
|
|
124
|
-
ensurePermissionSystemLogsDirectory(GLOBAL_LOGS_DIR),
|
|
125
|
-
});
|
|
126
|
-
const reportedLoggingWarnings = new Set<string>();
|
|
127
|
-
let loggingWarningReporter: ((message: string) => void) | null = null;
|
|
128
|
-
|
|
129
|
-
function setExtensionConfig(config: PermissionSystemExtensionConfig): void {
|
|
130
|
-
extensionConfig = normalizePermissionSystemConfig(config);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function setLoggingWarningReporter(
|
|
134
|
-
reporter: ((message: string) => void) | null,
|
|
135
|
-
): void {
|
|
136
|
-
loggingWarningReporter = reporter;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function reportLoggingWarning(message: string): void {
|
|
140
|
-
if (!loggingWarningReporter || reportedLoggingWarnings.has(message)) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
reportedLoggingWarnings.add(message);
|
|
145
|
-
loggingWarningReporter(message);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function writeDebugLog(
|
|
149
|
-
event: string,
|
|
150
|
-
details: Record<string, unknown> = {},
|
|
151
|
-
): void {
|
|
152
|
-
const warning = extensionLogger.debug(event, details);
|
|
153
|
-
if (warning) {
|
|
154
|
-
reportLoggingWarning(warning);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function writeReviewLog(
|
|
159
|
-
event: string,
|
|
160
|
-
details: Record<string, unknown> = {},
|
|
161
|
-
): void {
|
|
162
|
-
const warning = extensionLogger.review(event, details);
|
|
163
|
-
if (warning) {
|
|
164
|
-
reportLoggingWarning(warning);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function extractSkillNameFromInput(text: string): string | null {
|
|
169
|
-
const trimmed = text.trim();
|
|
170
|
-
if (!trimmed.startsWith("/skill:")) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const afterPrefix = trimmed.slice("/skill:".length);
|
|
175
|
-
if (!afterPrefix) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const firstWhitespace = afterPrefix.search(/\s/);
|
|
180
|
-
const skillName = (
|
|
181
|
-
firstWhitespace === -1 ? afterPrefix : afterPrefix.slice(0, firstWhitespace)
|
|
182
|
-
).trim();
|
|
183
|
-
return skillName || null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function getEventToolName(event: unknown): string | null {
|
|
187
|
-
return getToolNameFromValue(event);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function getEventInput(event: unknown): unknown {
|
|
191
|
-
const record = toRecord(event);
|
|
192
|
-
|
|
193
|
-
if (record.input !== undefined) {
|
|
194
|
-
return record.input;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (record.arguments !== undefined) {
|
|
198
|
-
return record.arguments;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return {};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function canRequestPermissionConfirmation(ctx: ExtensionContext): boolean {
|
|
205
|
-
return canResolveAskPermissionRequest({
|
|
206
|
-
config: extensionConfig,
|
|
207
|
-
hasUI: ctx.hasUI,
|
|
208
|
-
isSubagent: isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function derivePiProjectPaths(cwd: string | undefined | null): {
|
|
213
|
-
projectGlobalConfigPath: string;
|
|
214
|
-
projectAgentsDir: string;
|
|
215
|
-
} | null {
|
|
216
|
-
if (!cwd) {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
projectGlobalConfigPath: getProjectConfigPath(cwd),
|
|
222
|
-
projectAgentsDir: join(cwd, ".pi", "agent", "agents"),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function createPermissionManagerForCwd(
|
|
227
|
-
cwd: string | undefined | null,
|
|
228
|
-
): PermissionManager {
|
|
229
|
-
const agentDir = getAgentDir();
|
|
230
|
-
const projectPaths = derivePiProjectPaths(cwd);
|
|
231
|
-
|
|
232
|
-
return new PermissionManager({
|
|
233
|
-
globalConfigPath: getGlobalConfigPath(agentDir),
|
|
234
|
-
projectGlobalConfigPath: projectPaths?.projectGlobalConfigPath,
|
|
235
|
-
projectAgentsDir: projectPaths?.projectAgentsDir,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
35
|
export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
240
|
-
|
|
241
|
-
const sessionApprovalCache = new SessionApprovalCache();
|
|
242
|
-
let activeSkillEntries: SkillPromptEntry[] = [];
|
|
243
|
-
let lastKnownActiveAgentName: string | null = null;
|
|
244
|
-
let lastActiveToolsCacheKey: string | null = null;
|
|
245
|
-
let lastPromptStateCacheKey: string | null = null;
|
|
246
|
-
let permissionForwardingContext: ExtensionContext | null = null;
|
|
247
|
-
let permissionForwardingTimer: NodeJS.Timeout | null = null;
|
|
248
|
-
let isProcessingForwardedRequests = false;
|
|
249
|
-
let runtimeContext: ExtensionContext | null = null;
|
|
250
|
-
let lastConfigWarning: string | null = null;
|
|
251
|
-
|
|
252
|
-
const invalidateAgentStartCache = (): void => {
|
|
253
|
-
activeSkillEntries = [];
|
|
254
|
-
lastActiveToolsCacheKey = null;
|
|
255
|
-
lastPromptStateCacheKey = null;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const notifyWarning = (message: string): void => {
|
|
259
|
-
if (!runtimeContext?.hasUI) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
runtimeContext.ui.notify(message, "warning");
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const refreshExtensionConfig = (ctx?: ExtensionContext): void => {
|
|
267
|
-
if (ctx) {
|
|
268
|
-
runtimeContext = ctx;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const cwd = runtimeContext?.cwd ?? null;
|
|
272
|
-
const agentDir = getAgentDir();
|
|
273
|
-
const mergeResult = loadAndMergeConfigs(
|
|
274
|
-
agentDir,
|
|
275
|
-
cwd ?? "",
|
|
276
|
-
EXTENSION_ROOT,
|
|
277
|
-
);
|
|
278
|
-
const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
|
|
279
|
-
setExtensionConfig(runtimeConfig);
|
|
280
|
-
|
|
281
|
-
if (runtimeContext?.hasUI) {
|
|
282
|
-
syncPermissionSystemStatus(runtimeContext, runtimeConfig);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const warning =
|
|
286
|
-
mergeResult.issues.length > 0 ? mergeResult.issues.join("\n") : undefined;
|
|
287
|
-
if (warning && warning !== lastConfigWarning) {
|
|
288
|
-
lastConfigWarning = warning;
|
|
289
|
-
notifyWarning(warning);
|
|
290
|
-
} else if (!warning) {
|
|
291
|
-
lastConfigWarning = null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
writeDebugLog("config.loaded", {
|
|
295
|
-
warning: warning ?? null,
|
|
296
|
-
debugLog: runtimeConfig.debugLog,
|
|
297
|
-
permissionReviewLog: runtimeConfig.permissionReviewLog,
|
|
298
|
-
yoloMode: runtimeConfig.yoloMode,
|
|
299
|
-
});
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
const saveExtensionConfig = (
|
|
303
|
-
next: PermissionSystemExtensionConfig,
|
|
304
|
-
ctx: ExtensionCommandContext,
|
|
305
|
-
): void => {
|
|
306
|
-
const normalized = normalizePermissionSystemConfig(next);
|
|
307
|
-
const globalPath = getGlobalConfigPath(getAgentDir());
|
|
308
|
-
|
|
309
|
-
// Load existing global config and merge runtime knobs into it
|
|
310
|
-
const existing = loadUnifiedConfig(globalPath);
|
|
311
|
-
const merged = {
|
|
312
|
-
...existing.config,
|
|
313
|
-
debugLog: normalized.debugLog,
|
|
314
|
-
permissionReviewLog: normalized.permissionReviewLog,
|
|
315
|
-
yoloMode: normalized.yoloMode,
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const tmpPath = `${globalPath}.tmp`;
|
|
319
|
-
try {
|
|
320
|
-
mkdirSync(dirname(globalPath), { recursive: true });
|
|
321
|
-
writeFileSync(tmpPath, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
|
|
322
|
-
renameSync(tmpPath, globalPath);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
try {
|
|
325
|
-
if (existsSync(tmpPath)) {
|
|
326
|
-
unlinkSync(tmpPath);
|
|
327
|
-
}
|
|
328
|
-
} catch {
|
|
329
|
-
// Ignore cleanup failures.
|
|
330
|
-
}
|
|
331
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
332
|
-
ctx.ui.notify(
|
|
333
|
-
`Failed to save permission-system config at '${globalPath}': ${message}`,
|
|
334
|
-
"error",
|
|
335
|
-
);
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
setExtensionConfig(normalized);
|
|
340
|
-
syncPermissionSystemStatus(ctx, normalized);
|
|
341
|
-
lastConfigWarning = null;
|
|
342
|
-
|
|
343
|
-
writeDebugLog("config.saved", {
|
|
344
|
-
debugLog: normalized.debugLog,
|
|
345
|
-
permissionReviewLog: normalized.permissionReviewLog,
|
|
346
|
-
yoloMode: normalized.yoloMode,
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
setLoggingWarningReporter(notifyWarning);
|
|
351
|
-
setForwardedPermissionLogger({ writeReviewLog, writeDebugLog });
|
|
36
|
+
const runtime = createExtensionRuntime();
|
|
352
37
|
|
|
353
38
|
const forwardingDeps: PermissionForwardingDeps = {
|
|
354
|
-
forwardingDir:
|
|
355
|
-
subagentSessionsDir:
|
|
356
|
-
|
|
39
|
+
forwardingDir: runtime.forwardingDir,
|
|
40
|
+
subagentSessionsDir: runtime.subagentSessionsDir,
|
|
41
|
+
logger: {
|
|
42
|
+
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
43
|
+
writeDebugLog: runtime.writeDebugLog.bind(runtime),
|
|
44
|
+
},
|
|
45
|
+
writeReviewLog: runtime.writeReviewLog.bind(runtime),
|
|
357
46
|
requestPermissionDecisionFromUi,
|
|
358
47
|
shouldAutoApprove: () =>
|
|
359
|
-
shouldAutoApprovePermissionState("ask",
|
|
48
|
+
shouldAutoApprovePermissionState("ask", runtime.config),
|
|
360
49
|
};
|
|
361
50
|
|
|
362
|
-
refreshExtensionConfig();
|
|
51
|
+
refreshExtensionConfig(runtime);
|
|
363
52
|
registerPermissionSystemCommand(pi, {
|
|
364
|
-
getConfig: () =>
|
|
365
|
-
setConfig: saveExtensionConfig,
|
|
366
|
-
getConfigPath: () => getGlobalConfigPath(
|
|
53
|
+
getConfig: () => runtime.config,
|
|
54
|
+
setConfig: (next, ctx) => saveExtensionConfig(runtime, next, ctx),
|
|
55
|
+
getConfigPath: () => getGlobalConfigPath(runtime.agentDir),
|
|
367
56
|
});
|
|
368
57
|
|
|
369
|
-
const createPermissionRequestId = (prefix: string): string =>
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
},
|
|
390
|
-
): void => {
|
|
391
|
-
writeReviewLog(event, {
|
|
392
|
-
requestId: details.requestId,
|
|
393
|
-
source: details.source,
|
|
394
|
-
agentName: details.agentName,
|
|
395
|
-
message: details.message,
|
|
396
|
-
toolCallId: details.toolCallId ?? null,
|
|
397
|
-
toolName: details.toolName ?? null,
|
|
398
|
-
skillName: details.skillName ?? null,
|
|
399
|
-
path: details.path ?? null,
|
|
400
|
-
command: details.command ?? null,
|
|
401
|
-
target: details.target ?? null,
|
|
402
|
-
toolInputPreview: details.toolInputPreview ?? null,
|
|
403
|
-
resolution: details.resolution ?? null,
|
|
404
|
-
denialReason: details.denialReason ?? null,
|
|
405
|
-
});
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const promptPermission = async (
|
|
409
|
-
ctx: ExtensionContext,
|
|
410
|
-
details: {
|
|
411
|
-
requestId: string;
|
|
412
|
-
source: PermissionReviewSource;
|
|
413
|
-
agentName: string | null;
|
|
414
|
-
message: string;
|
|
415
|
-
toolCallId?: string;
|
|
416
|
-
toolName?: string;
|
|
417
|
-
skillName?: string;
|
|
418
|
-
path?: string;
|
|
419
|
-
command?: string;
|
|
420
|
-
target?: string;
|
|
421
|
-
toolInputPreview?: string;
|
|
422
|
-
},
|
|
423
|
-
): Promise<PermissionPromptDecision> => {
|
|
424
|
-
if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
|
|
425
|
-
reviewPermissionDecision("permission_request.auto_approved", details);
|
|
426
|
-
return { approved: true, state: "approved" };
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
reviewPermissionDecision("permission_request.waiting", details);
|
|
430
|
-
|
|
431
|
-
const decision = await confirmPermission(
|
|
432
|
-
ctx,
|
|
433
|
-
details.message,
|
|
434
|
-
forwardingDeps,
|
|
435
|
-
);
|
|
436
|
-
reviewPermissionDecision(
|
|
437
|
-
decision.approved
|
|
438
|
-
? "permission_request.approved"
|
|
439
|
-
: "permission_request.denied",
|
|
440
|
-
{
|
|
441
|
-
...details,
|
|
442
|
-
resolution: decision.state,
|
|
443
|
-
denialReason: decision.denialReason,
|
|
444
|
-
},
|
|
445
|
-
);
|
|
446
|
-
return decision;
|
|
447
|
-
};
|
|
448
|
-
|
|
449
|
-
const stopForwardedPermissionPolling = (): void => {
|
|
450
|
-
if (permissionForwardingTimer) {
|
|
451
|
-
clearInterval(permissionForwardingTimer);
|
|
452
|
-
permissionForwardingTimer = null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
permissionForwardingContext = null;
|
|
456
|
-
isProcessingForwardedRequests = false;
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
const startForwardedPermissionPolling = (ctx: ExtensionContext): void => {
|
|
460
|
-
if (!ctx.hasUI || isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR)) {
|
|
461
|
-
stopForwardedPermissionPolling();
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
permissionForwardingContext = ctx;
|
|
466
|
-
if (permissionForwardingTimer) {
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
permissionForwardingTimer = setInterval(() => {
|
|
471
|
-
if (!permissionForwardingContext || isProcessingForwardedRequests) {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
isProcessingForwardedRequests = true;
|
|
476
|
-
void processForwardedPermissionRequests(
|
|
477
|
-
permissionForwardingContext,
|
|
478
|
-
forwardingDeps,
|
|
479
|
-
).finally(() => {
|
|
480
|
-
isProcessingForwardedRequests = false;
|
|
481
|
-
});
|
|
482
|
-
}, PERMISSION_FORWARDING_POLL_INTERVAL_MS);
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
const resolveAgentName = (
|
|
486
|
-
ctx: ExtensionContext,
|
|
487
|
-
systemPrompt?: string,
|
|
488
|
-
): string | null => {
|
|
489
|
-
const fromSession = getActiveAgentName(ctx);
|
|
490
|
-
if (fromSession) {
|
|
491
|
-
lastKnownActiveAgentName = fromSession;
|
|
492
|
-
return fromSession;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
|
|
496
|
-
if (fromSystemPrompt) {
|
|
497
|
-
lastKnownActiveAgentName = fromSystemPrompt;
|
|
498
|
-
return fromSystemPrompt;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return lastKnownActiveAgentName;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
const shouldExposeTool = (
|
|
505
|
-
toolName: string,
|
|
506
|
-
agentName: string | null,
|
|
507
|
-
): boolean => {
|
|
508
|
-
// Use tool-level permission check for tool injection decisions
|
|
509
|
-
// This ensures that agent-specific tool deny rules (e.g., bash: deny) are respected
|
|
510
|
-
// before any command-level permissions are considered
|
|
511
|
-
const toolPermission = permissionManager.getToolPermission(
|
|
512
|
-
toolName,
|
|
513
|
-
agentName ?? undefined,
|
|
514
|
-
);
|
|
515
|
-
return toolPermission !== "deny";
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
const logResolvedConfigPaths = (): void => {
|
|
519
|
-
const policyPaths = permissionManager.getResolvedPolicyPaths();
|
|
520
|
-
const cwd = runtimeContext?.cwd ?? null;
|
|
521
|
-
|
|
522
|
-
// Detect legacy files for the log entry
|
|
523
|
-
const agentDir = getAgentDir();
|
|
524
|
-
const legacyGlobalPolicyDetected = existsSync(
|
|
525
|
-
getLegacyGlobalPolicyPath(agentDir),
|
|
526
|
-
);
|
|
527
|
-
const legacyProjectPolicyDetected = cwd
|
|
528
|
-
? existsSync(getLegacyProjectPolicyPath(cwd))
|
|
529
|
-
: false;
|
|
530
|
-
const legacyExtConfigPath = getLegacyExtensionConfigPath(EXTENSION_ROOT);
|
|
531
|
-
const newGlobalPath = getGlobalConfigPath(agentDir);
|
|
532
|
-
const legacyExtensionConfigDetected =
|
|
533
|
-
normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
|
|
534
|
-
existsSync(legacyExtConfigPath);
|
|
535
|
-
|
|
536
|
-
const entry = buildResolvedConfigLogEntry({
|
|
537
|
-
policyPaths,
|
|
538
|
-
legacyGlobalPolicyDetected,
|
|
539
|
-
legacyProjectPolicyDetected,
|
|
540
|
-
legacyExtensionConfigDetected,
|
|
541
|
-
});
|
|
542
|
-
writeReviewLog(
|
|
543
|
-
"config.resolved",
|
|
544
|
-
entry as unknown as Record<string, unknown>,
|
|
545
|
-
);
|
|
546
|
-
writeDebugLog(
|
|
547
|
-
"config.resolved",
|
|
548
|
-
entry as unknown as Record<string, unknown>,
|
|
549
|
-
);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
pi.on("session_start", async (event, ctx) => {
|
|
553
|
-
runtimeContext = ctx;
|
|
554
|
-
refreshExtensionConfig(ctx);
|
|
555
|
-
permissionManager = createPermissionManagerForCwd(ctx.cwd);
|
|
556
|
-
invalidateAgentStartCache();
|
|
557
|
-
lastKnownActiveAgentName = getActiveAgentName(ctx);
|
|
558
|
-
startForwardedPermissionPolling(ctx);
|
|
559
|
-
logResolvedConfigPaths();
|
|
560
|
-
|
|
561
|
-
const policyIssues = permissionManager.getConfigIssues(
|
|
562
|
-
lastKnownActiveAgentName,
|
|
563
|
-
);
|
|
564
|
-
for (const issue of policyIssues) {
|
|
565
|
-
notifyWarning(issue);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (event.reason === "reload") {
|
|
569
|
-
writeDebugLog("lifecycle.reload", {
|
|
570
|
-
triggeredBy: "session_start",
|
|
571
|
-
reason: event.reason,
|
|
572
|
-
cwd: ctx.cwd,
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
pi.on("resources_discover", async (event, _ctx) => {
|
|
578
|
-
if (event.reason === "reload") {
|
|
579
|
-
permissionManager = runtimeContext
|
|
580
|
-
? createPermissionManagerForCwd(runtimeContext.cwd)
|
|
581
|
-
: new PermissionManager();
|
|
582
|
-
invalidateAgentStartCache();
|
|
583
|
-
writeDebugLog("lifecycle.reload", {
|
|
584
|
-
triggeredBy: "resources_discover",
|
|
585
|
-
reason: event.reason,
|
|
586
|
-
cwd: runtimeContext?.cwd ?? null,
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
pi.on("session_shutdown", async () => {
|
|
592
|
-
runtimeContext?.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
|
|
593
|
-
runtimeContext = null;
|
|
594
|
-
invalidateAgentStartCache();
|
|
595
|
-
sessionApprovalCache.clear();
|
|
596
|
-
stopForwardedPermissionPolling();
|
|
597
|
-
});
|
|
598
|
-
|
|
599
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
600
|
-
runtimeContext = ctx;
|
|
601
|
-
refreshExtensionConfig(ctx);
|
|
602
|
-
startForwardedPermissionPolling(ctx);
|
|
603
|
-
const agentName = resolveAgentName(ctx, event.systemPrompt);
|
|
604
|
-
const allTools = pi.getAllTools();
|
|
605
|
-
const allowedTools: string[] = [];
|
|
606
|
-
|
|
607
|
-
for (const tool of allTools) {
|
|
608
|
-
const toolName = getEventToolName(tool);
|
|
609
|
-
if (!toolName) {
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (shouldExposeTool(toolName, agentName)) {
|
|
614
|
-
allowedTools.push(toolName);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
|
|
619
|
-
if (
|
|
620
|
-
shouldApplyCachedAgentStartState(
|
|
621
|
-
lastActiveToolsCacheKey,
|
|
622
|
-
activeToolsCacheKey,
|
|
623
|
-
)
|
|
624
|
-
) {
|
|
625
|
-
pi.setActiveTools(allowedTools);
|
|
626
|
-
lastActiveToolsCacheKey = activeToolsCacheKey;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
|
|
630
|
-
agentName,
|
|
631
|
-
cwd: ctx.cwd,
|
|
632
|
-
permissionStamp: permissionManager.getPolicyCacheStamp(
|
|
633
|
-
agentName ?? undefined,
|
|
634
|
-
),
|
|
635
|
-
systemPrompt: event.systemPrompt,
|
|
636
|
-
allowedToolNames: allowedTools,
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
if (
|
|
640
|
-
!shouldApplyCachedAgentStartState(
|
|
641
|
-
lastPromptStateCacheKey,
|
|
642
|
-
promptStateCacheKey,
|
|
643
|
-
)
|
|
644
|
-
) {
|
|
645
|
-
return {};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
lastPromptStateCacheKey = promptStateCacheKey;
|
|
649
|
-
const toolPromptResult = sanitizeAvailableToolsSection(
|
|
650
|
-
event.systemPrompt,
|
|
651
|
-
allowedTools,
|
|
652
|
-
);
|
|
653
|
-
const skillPromptResult = resolveSkillPromptEntries(
|
|
654
|
-
toolPromptResult.prompt,
|
|
655
|
-
permissionManager,
|
|
656
|
-
agentName,
|
|
657
|
-
ctx.cwd,
|
|
658
|
-
);
|
|
659
|
-
activeSkillEntries = skillPromptResult.entries;
|
|
660
|
-
|
|
661
|
-
if (skillPromptResult.prompt !== event.systemPrompt) {
|
|
662
|
-
return { systemPrompt: skillPromptResult.prompt };
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
return {};
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
pi.on("input", async (event, ctx) => {
|
|
669
|
-
runtimeContext = ctx;
|
|
670
|
-
startForwardedPermissionPolling(ctx);
|
|
671
|
-
const skillName = extractSkillNameFromInput(event.text);
|
|
672
|
-
if (!skillName) {
|
|
673
|
-
return { action: "continue" };
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const agentName = resolveAgentName(ctx);
|
|
677
|
-
const check = permissionManager.checkPermission(
|
|
678
|
-
"skill",
|
|
679
|
-
{ name: skillName },
|
|
680
|
-
agentName ?? undefined,
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
if (check.state === "deny" && ctx.hasUI) {
|
|
684
|
-
const notifyMessage = agentName
|
|
685
|
-
? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
|
|
686
|
-
: `Skill '${skillName}' is not permitted by the current skill policy.`;
|
|
687
|
-
ctx.ui.notify(notifyMessage, "warning");
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const skillInputMessage = formatSkillAskPrompt(
|
|
691
|
-
skillName,
|
|
692
|
-
agentName ?? undefined,
|
|
693
|
-
);
|
|
694
|
-
const skillInputGate = await applyPermissionGate({
|
|
695
|
-
state: check.state,
|
|
696
|
-
canConfirm: canRequestPermissionConfirmation(ctx),
|
|
697
|
-
promptForApproval: () =>
|
|
698
|
-
promptPermission(ctx, {
|
|
699
|
-
requestId: createPermissionRequestId("skill-input"),
|
|
700
|
-
source: "skill_input",
|
|
701
|
-
agentName,
|
|
702
|
-
message: skillInputMessage,
|
|
703
|
-
skillName,
|
|
704
|
-
}),
|
|
705
|
-
writeLog: writeReviewLog,
|
|
706
|
-
logContext: {
|
|
707
|
-
source: "skill_input",
|
|
708
|
-
skillName,
|
|
709
|
-
agentName,
|
|
710
|
-
message: skillInputMessage,
|
|
711
|
-
},
|
|
712
|
-
messages: {
|
|
713
|
-
denyReason: skillInputMessage,
|
|
714
|
-
unavailableReason:
|
|
715
|
-
"Skill requires approval, but no interactive UI is available.",
|
|
716
|
-
userDeniedReason: () => "User denied skill.",
|
|
717
|
-
},
|
|
718
|
-
});
|
|
719
|
-
if (skillInputGate.action === "block") {
|
|
720
|
-
return { action: "handled" };
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return { action: "continue" };
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
727
|
-
runtimeContext = ctx;
|
|
728
|
-
startForwardedPermissionPolling(ctx);
|
|
729
|
-
const agentName = resolveAgentName(ctx);
|
|
730
|
-
const toolName = getEventToolName(event);
|
|
731
|
-
|
|
732
|
-
if (!toolName) {
|
|
733
|
-
return { block: true, reason: formatMissingToolNameReason() };
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
const registrationCheck = checkRequestedToolRegistration(
|
|
737
|
-
toolName,
|
|
738
|
-
pi.getAllTools(),
|
|
739
|
-
);
|
|
740
|
-
if (registrationCheck.status === "missing-tool-name") {
|
|
741
|
-
return { block: true, reason: formatMissingToolNameReason() };
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (registrationCheck.status === "unregistered") {
|
|
745
|
-
return {
|
|
746
|
-
block: true,
|
|
747
|
-
reason: formatUnknownToolReason(
|
|
748
|
-
registrationCheck.requestedToolName,
|
|
749
|
-
registrationCheck.availableToolNames,
|
|
58
|
+
const createPermissionRequestId = (prefix: string): string =>
|
|
59
|
+
`${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
60
|
+
|
|
61
|
+
const deps: HandlerDeps = {
|
|
62
|
+
runtime,
|
|
63
|
+
createPermissionManagerForCwd: (cwd) =>
|
|
64
|
+
createPermissionManagerForCwd(runtime.agentDir, cwd),
|
|
65
|
+
refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
|
|
66
|
+
notifyWarning: (message) =>
|
|
67
|
+
runtime.runtimeContext?.ui.notify(message, "warning"),
|
|
68
|
+
logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
|
|
69
|
+
resolveAgentName: (ctx, systemPrompt) =>
|
|
70
|
+
resolveAgentName(runtime, ctx, systemPrompt),
|
|
71
|
+
canRequestPermissionConfirmation: (ctx) =>
|
|
72
|
+
canResolveAskPermissionRequest({
|
|
73
|
+
config: runtime.config,
|
|
74
|
+
hasUI: ctx.hasUI,
|
|
75
|
+
isSubagent: isSubagentExecutionContext(
|
|
76
|
+
ctx,
|
|
77
|
+
runtime.subagentSessionsDir,
|
|
750
78
|
),
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
)
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
);
|
|
763
|
-
|
|
764
|
-
if (matchedSkill) {
|
|
765
|
-
const skillReadMessage = formatSkillPathAskPrompt(
|
|
766
|
-
matchedSkill,
|
|
767
|
-
event.input.path,
|
|
768
|
-
agentName ?? undefined,
|
|
769
|
-
);
|
|
770
|
-
const skillReadGate = await applyPermissionGate({
|
|
771
|
-
state: matchedSkill.state,
|
|
772
|
-
canConfirm: canRequestPermissionConfirmation(ctx),
|
|
773
|
-
promptForApproval: () =>
|
|
774
|
-
promptPermission(ctx, {
|
|
775
|
-
requestId: event.toolCallId,
|
|
776
|
-
source: "skill_read",
|
|
777
|
-
agentName,
|
|
778
|
-
message: skillReadMessage,
|
|
779
|
-
toolCallId: event.toolCallId,
|
|
780
|
-
toolName: toolName,
|
|
781
|
-
skillName: matchedSkill.name,
|
|
782
|
-
path: event.input.path,
|
|
783
|
-
}),
|
|
784
|
-
writeLog: writeReviewLog,
|
|
785
|
-
logContext: {
|
|
786
|
-
source: "skill_read",
|
|
787
|
-
skillName: matchedSkill.name,
|
|
788
|
-
agentName,
|
|
789
|
-
path: event.input.path,
|
|
790
|
-
message: skillReadMessage,
|
|
791
|
-
},
|
|
792
|
-
messages: {
|
|
793
|
-
denyReason: formatSkillPathDenyReason(
|
|
794
|
-
matchedSkill,
|
|
795
|
-
event.input.path,
|
|
796
|
-
agentName ?? undefined,
|
|
797
|
-
),
|
|
798
|
-
unavailableReason: `Accessing skill '${matchedSkill.name}' requires approval, but no interactive UI is available.`,
|
|
799
|
-
userDeniedReason: (decision) => {
|
|
800
|
-
const denialReason = decision.denialReason
|
|
801
|
-
? ` Reason: ${decision.denialReason}.`
|
|
802
|
-
: "";
|
|
803
|
-
return `User denied access to skill '${matchedSkill.name}'.${denialReason}`;
|
|
804
|
-
},
|
|
805
|
-
},
|
|
806
|
-
});
|
|
807
|
-
if (skillReadGate.action === "block") {
|
|
808
|
-
return { block: true, reason: skillReadGate.reason };
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
const input = getEventInput(event);
|
|
814
|
-
const externalDirectoryPath = ctx.cwd
|
|
815
|
-
? getPathBearingToolPath(toolName, input)
|
|
816
|
-
: null;
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
ctx.cwd &&
|
|
820
|
-
externalDirectoryPath &&
|
|
821
|
-
isPathOutsideWorkingDirectory(externalDirectoryPath, ctx.cwd)
|
|
822
|
-
) {
|
|
823
|
-
const normalizedExtPath = normalizePathForComparison(
|
|
824
|
-
externalDirectoryPath,
|
|
825
|
-
ctx.cwd,
|
|
826
|
-
);
|
|
827
|
-
const sessionPrefix = sessionApprovalCache.findMatchingPrefix(
|
|
828
|
-
"external_directory",
|
|
829
|
-
normalizedExtPath,
|
|
830
|
-
);
|
|
831
|
-
|
|
832
|
-
if (sessionPrefix) {
|
|
833
|
-
writeReviewLog("permission_request.session_approved", {
|
|
834
|
-
source: "tool_call",
|
|
835
|
-
toolCallId: event.toolCallId,
|
|
836
|
-
toolName,
|
|
837
|
-
agentName,
|
|
838
|
-
path: externalDirectoryPath,
|
|
839
|
-
resolution: "session_approved",
|
|
840
|
-
sessionApprovalPrefix: sessionPrefix,
|
|
841
|
-
});
|
|
842
|
-
// Fall through to normal permission check
|
|
843
|
-
} else {
|
|
844
|
-
const extCheck = permissionManager.checkPermission(
|
|
845
|
-
"external_directory",
|
|
846
|
-
{},
|
|
847
|
-
agentName ?? undefined,
|
|
848
|
-
);
|
|
849
|
-
|
|
850
|
-
let extDirDecision: PermissionPromptDecision | null = null;
|
|
851
|
-
const extDirMessage = formatExternalDirectoryAskPrompt(
|
|
852
|
-
toolName,
|
|
853
|
-
externalDirectoryPath,
|
|
854
|
-
ctx.cwd,
|
|
855
|
-
agentName ?? undefined,
|
|
856
|
-
);
|
|
857
|
-
const extDirGate = await applyPermissionGate({
|
|
858
|
-
state: extCheck.state,
|
|
859
|
-
canConfirm: canRequestPermissionConfirmation(ctx),
|
|
860
|
-
promptForApproval: async () => {
|
|
861
|
-
const decision = await promptPermission(ctx, {
|
|
862
|
-
requestId: event.toolCallId,
|
|
863
|
-
source: "tool_call",
|
|
864
|
-
agentName,
|
|
865
|
-
message: extDirMessage,
|
|
866
|
-
toolCallId: event.toolCallId,
|
|
867
|
-
toolName,
|
|
868
|
-
path: externalDirectoryPath,
|
|
869
|
-
});
|
|
870
|
-
extDirDecision = decision;
|
|
871
|
-
return decision;
|
|
872
|
-
},
|
|
873
|
-
writeLog: writeReviewLog,
|
|
874
|
-
logContext: {
|
|
875
|
-
source: "tool_call",
|
|
876
|
-
toolCallId: event.toolCallId,
|
|
877
|
-
toolName,
|
|
878
|
-
agentName,
|
|
879
|
-
path: externalDirectoryPath,
|
|
880
|
-
message: extDirMessage,
|
|
881
|
-
},
|
|
882
|
-
messages: {
|
|
883
|
-
denyReason: formatExternalDirectoryDenyReason(
|
|
884
|
-
toolName,
|
|
885
|
-
externalDirectoryPath,
|
|
886
|
-
ctx.cwd,
|
|
887
|
-
agentName ?? undefined,
|
|
888
|
-
),
|
|
889
|
-
unavailableReason: `Accessing '${externalDirectoryPath}' outside the working directory requires approval, but no interactive UI is available.`,
|
|
890
|
-
userDeniedReason: (decision) =>
|
|
891
|
-
formatExternalDirectoryUserDeniedReason(
|
|
892
|
-
toolName,
|
|
893
|
-
externalDirectoryPath,
|
|
894
|
-
decision.denialReason,
|
|
895
|
-
),
|
|
896
|
-
},
|
|
897
|
-
});
|
|
898
|
-
if (extDirGate.action === "block") {
|
|
899
|
-
return { block: true, reason: extDirGate.reason };
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (extDirDecision?.state === "approved_for_session") {
|
|
903
|
-
const prefix = deriveApprovalPrefix(normalizedExtPath);
|
|
904
|
-
sessionApprovalCache.approve("external_directory", prefix);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
// Fall through to normal permission check
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Bash external directory gate: extract paths from bash commands
|
|
911
|
-
if (ctx.cwd && toolName === "bash") {
|
|
912
|
-
const command = getNonEmptyString(toRecord(input).command);
|
|
913
|
-
if (command) {
|
|
914
|
-
const externalPaths = extractExternalPathsFromBashCommand(
|
|
915
|
-
command,
|
|
916
|
-
ctx.cwd,
|
|
917
|
-
);
|
|
918
|
-
if (externalPaths.length > 0) {
|
|
919
|
-
// Filter out paths already covered by session approvals
|
|
920
|
-
const uncoveredPaths = externalPaths.filter(
|
|
921
|
-
(p) => !sessionApprovalCache.has("external_directory", p),
|
|
922
|
-
);
|
|
923
|
-
|
|
924
|
-
if (uncoveredPaths.length === 0) {
|
|
925
|
-
// All external paths are session-approved
|
|
926
|
-
writeReviewLog("permission_request.session_approved", {
|
|
927
|
-
source: "tool_call",
|
|
928
|
-
toolCallId: event.toolCallId,
|
|
929
|
-
toolName,
|
|
930
|
-
agentName,
|
|
931
|
-
command,
|
|
932
|
-
externalPaths,
|
|
933
|
-
resolution: "session_approved",
|
|
934
|
-
});
|
|
935
|
-
// Fall through to normal bash permission check
|
|
936
|
-
} else {
|
|
937
|
-
const extCheck = permissionManager.checkPermission(
|
|
938
|
-
"external_directory",
|
|
939
|
-
{},
|
|
940
|
-
agentName ?? undefined,
|
|
941
|
-
);
|
|
942
|
-
|
|
943
|
-
let bashExtDecision: PermissionPromptDecision | null = null;
|
|
944
|
-
const bashExtMessage = formatBashExternalDirectoryAskPrompt(
|
|
945
|
-
command,
|
|
946
|
-
uncoveredPaths,
|
|
947
|
-
ctx.cwd,
|
|
948
|
-
agentName ?? undefined,
|
|
949
|
-
);
|
|
950
|
-
const bashExtGate = await applyPermissionGate({
|
|
951
|
-
state: extCheck.state,
|
|
952
|
-
canConfirm: canRequestPermissionConfirmation(ctx),
|
|
953
|
-
promptForApproval: async () => {
|
|
954
|
-
const decision = await promptPermission(ctx, {
|
|
955
|
-
requestId: event.toolCallId,
|
|
956
|
-
source: "tool_call",
|
|
957
|
-
agentName,
|
|
958
|
-
message: bashExtMessage,
|
|
959
|
-
toolCallId: event.toolCallId,
|
|
960
|
-
toolName,
|
|
961
|
-
command,
|
|
962
|
-
});
|
|
963
|
-
bashExtDecision = decision;
|
|
964
|
-
return decision;
|
|
965
|
-
},
|
|
966
|
-
writeLog: writeReviewLog,
|
|
967
|
-
logContext: {
|
|
968
|
-
source: "tool_call",
|
|
969
|
-
toolCallId: event.toolCallId,
|
|
970
|
-
toolName,
|
|
971
|
-
agentName,
|
|
972
|
-
command,
|
|
973
|
-
externalPaths: uncoveredPaths,
|
|
974
|
-
message: bashExtMessage,
|
|
975
|
-
},
|
|
976
|
-
messages: {
|
|
977
|
-
denyReason: formatBashExternalDirectoryDenyReason(
|
|
978
|
-
command,
|
|
979
|
-
uncoveredPaths,
|
|
980
|
-
ctx.cwd,
|
|
981
|
-
agentName ?? undefined,
|
|
982
|
-
),
|
|
983
|
-
unavailableReason: `Bash command '${command}' references path(s) outside the working directory and requires approval, but no interactive UI is available.`,
|
|
984
|
-
userDeniedReason: (decision) => {
|
|
985
|
-
const reasonSuffix = decision.denialReason
|
|
986
|
-
? ` Reason: ${decision.denialReason}.`
|
|
987
|
-
: "";
|
|
988
|
-
return `User denied external directory access for bash command '${command}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
|
|
989
|
-
},
|
|
990
|
-
},
|
|
991
|
-
});
|
|
992
|
-
if (bashExtGate.action === "block") {
|
|
993
|
-
return { block: true, reason: bashExtGate.reason };
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (bashExtDecision?.state === "approved_for_session") {
|
|
997
|
-
for (const extPath of uncoveredPaths) {
|
|
998
|
-
const prefix = deriveApprovalPrefix(extPath);
|
|
999
|
-
sessionApprovalCache.approve("external_directory", prefix);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
// Fall through to normal bash permission check
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const check = permissionManager.checkPermission(
|
|
1009
|
-
toolName,
|
|
1010
|
-
input,
|
|
1011
|
-
agentName ?? undefined,
|
|
1012
|
-
);
|
|
1013
|
-
const permissionLogContext = getPermissionLogContext(
|
|
1014
|
-
check,
|
|
1015
|
-
input,
|
|
1016
|
-
PATH_BEARING_TOOLS,
|
|
1017
|
-
);
|
|
1018
|
-
|
|
1019
|
-
const toolUnavailableReason =
|
|
1020
|
-
toolName === "bash" && isToolCallEventType("bash", event)
|
|
1021
|
-
? `Running bash command '${event.input.command}' requires approval, but no interactive UI is available.`
|
|
1022
|
-
: toolName === "mcp"
|
|
1023
|
-
? "Using tool 'mcp' requires approval, but no interactive UI is available."
|
|
1024
|
-
: `Using tool '${toolName}' requires approval, but no interactive UI is available.`;
|
|
1025
|
-
|
|
1026
|
-
const toolAskMessage = formatAskPrompt(
|
|
1027
|
-
check,
|
|
1028
|
-
agentName ?? undefined,
|
|
1029
|
-
input,
|
|
1030
|
-
);
|
|
1031
|
-
const toolGate = await applyPermissionGate({
|
|
1032
|
-
state: check.state,
|
|
1033
|
-
canConfirm: canRequestPermissionConfirmation(ctx),
|
|
1034
|
-
promptForApproval: () =>
|
|
1035
|
-
promptPermission(ctx, {
|
|
1036
|
-
requestId: event.toolCallId,
|
|
1037
|
-
source: "tool_call",
|
|
1038
|
-
agentName,
|
|
1039
|
-
message: toolAskMessage,
|
|
1040
|
-
toolCallId: event.toolCallId,
|
|
1041
|
-
toolName,
|
|
1042
|
-
...permissionLogContext,
|
|
1043
|
-
}),
|
|
1044
|
-
writeLog: writeReviewLog,
|
|
1045
|
-
logContext: {
|
|
1046
|
-
source: "tool_call",
|
|
1047
|
-
toolCallId: event.toolCallId,
|
|
1048
|
-
toolName,
|
|
1049
|
-
agentName,
|
|
1050
|
-
message: toolAskMessage,
|
|
1051
|
-
...permissionLogContext,
|
|
1052
|
-
},
|
|
1053
|
-
messages: {
|
|
1054
|
-
denyReason: formatDenyReason(check, agentName ?? undefined),
|
|
1055
|
-
unavailableReason: toolUnavailableReason,
|
|
1056
|
-
userDeniedReason: (decision) =>
|
|
1057
|
-
formatUserDeniedReason(check, decision.denialReason),
|
|
1058
|
-
},
|
|
1059
|
-
});
|
|
1060
|
-
if (toolGate.action === "block") {
|
|
1061
|
-
return { block: true, reason: toolGate.reason };
|
|
1062
|
-
}
|
|
79
|
+
}),
|
|
80
|
+
promptPermission: (ctx, details) =>
|
|
81
|
+
promptPermission(runtime, forwardingDeps, ctx, details),
|
|
82
|
+
createPermissionRequestId,
|
|
83
|
+
startForwardedPermissionPolling: (ctx) =>
|
|
84
|
+
startForwardedPermissionPolling(runtime, forwardingDeps, ctx),
|
|
85
|
+
stopForwardedPermissionPolling: () =>
|
|
86
|
+
stopForwardedPermissionPolling(runtime),
|
|
87
|
+
getAllTools: () => pi.getAllTools(),
|
|
88
|
+
setActiveTools: (names) => pi.setActiveTools(names),
|
|
89
|
+
};
|
|
1063
90
|
|
|
1064
|
-
|
|
1065
|
-
|
|
91
|
+
pi.on("session_start", (event, ctx) => handleSessionStart(deps, event, ctx));
|
|
92
|
+
pi.on("resources_discover", (event) => handleResourcesDiscover(deps, event));
|
|
93
|
+
pi.on("session_shutdown", () => handleSessionShutdown(deps));
|
|
94
|
+
pi.on("before_agent_start", (event, ctx) =>
|
|
95
|
+
handleBeforeAgentStart(deps, event, ctx),
|
|
96
|
+
);
|
|
97
|
+
pi.on("input", (event, ctx) => handleInput(deps, event, ctx));
|
|
98
|
+
pi.on("tool_call", (event, ctx) => handleToolCall(deps, event, ctx));
|
|
1066
99
|
}
|