@gotgenes/pi-permission-system 2.0.0 → 3.0.1
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/README.md +92 -35
- package/config/config.example.json +6 -0
- package/package.json +1 -1
- package/schemas/permissions.schema.json +114 -16
- package/src/active-agent.ts +58 -0
- package/src/config-loader.ts +398 -0
- package/src/config-paths.ts +34 -0
- package/src/config-reporter.ts +16 -8
- package/src/external-directory.ts +113 -0
- package/src/forwarded-permissions/io.ts +328 -0
- package/src/forwarded-permissions/polling.ts +334 -0
- package/src/index.ts +153 -1095
- package/src/permission-manager.ts +25 -111
- package/src/permission-prompts.ts +131 -0
- package/src/subagent-context.ts +52 -0
- package/src/tool-input-preview.ts +206 -0
- package/tests/active-agent.test.ts +160 -0
- package/tests/bash-filter.test.ts +137 -0
- package/tests/common.test.ts +189 -0
- package/tests/config-loader.test.ts +364 -0
- package/tests/config-paths.test.ts +78 -0
- package/tests/config-reporter.test.ts +42 -33
- package/tests/extension-config.test.ts +51 -0
- package/tests/external-directory.test.ts +250 -0
- package/tests/permission-prompts.test.ts +301 -0
- package/tests/permission-system.test.ts +9 -26
- package/tests/session-start.test.ts +8 -33
- package/tests/skill-prompt-sanitizer.test.ts +244 -0
- package/tests/subagent-context.test.ts +124 -0
- package/tests/system-prompt-sanitizer.test.ts +186 -0
- package/tests/tool-input-preview.test.ts +452 -0
- package/tests/tool-registry.test.ts +155 -0
- package/tests/wildcard-matcher.test.ts +180 -0
- package/tests/yolo-mode.test.ts +110 -0
package/src/index.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
3
|
mkdirSync,
|
|
4
|
-
readdirSync,
|
|
5
|
-
readFileSync,
|
|
6
4
|
renameSync,
|
|
7
|
-
rmdirSync,
|
|
8
5
|
unlinkSync,
|
|
9
6
|
writeFileSync,
|
|
10
7
|
} from "node:fs";
|
|
11
|
-
import {
|
|
12
|
-
import { join, normalize, resolve, sep } from "node:path";
|
|
8
|
+
import { dirname, join, normalize } from "node:path";
|
|
13
9
|
import {
|
|
14
10
|
type ExtensionAPI,
|
|
15
11
|
type ExtensionCommandContext,
|
|
@@ -17,41 +13,68 @@ import {
|
|
|
17
13
|
getAgentDir,
|
|
18
14
|
isToolCallEventType,
|
|
19
15
|
} from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import {
|
|
17
|
+
getActiveAgentName,
|
|
18
|
+
getActiveAgentNameFromSystemPrompt,
|
|
19
|
+
} from "./active-agent.js";
|
|
20
20
|
import {
|
|
21
21
|
createActiveToolsCacheKey,
|
|
22
22
|
createBeforeAgentStartPromptStateKey,
|
|
23
23
|
shouldApplyCachedAgentStartState,
|
|
24
24
|
} from "./before-agent-start-cache.js";
|
|
25
|
-
import {
|
|
25
|
+
import { toRecord } from "./common.js";
|
|
26
|
+
import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader.js";
|
|
26
27
|
import { registerPermissionSystemCommand } from "./config-modal.js";
|
|
28
|
+
import {
|
|
29
|
+
DEBUG_LOG_FILENAME,
|
|
30
|
+
getGlobalConfigPath,
|
|
31
|
+
getGlobalLogsDir,
|
|
32
|
+
getLegacyExtensionConfigPath,
|
|
33
|
+
getLegacyGlobalPolicyPath,
|
|
34
|
+
getLegacyProjectPolicyPath,
|
|
35
|
+
getProjectConfigPath,
|
|
36
|
+
REVIEW_LOG_FILENAME,
|
|
37
|
+
} from "./config-paths.js";
|
|
27
38
|
import { buildResolvedConfigLogEntry } from "./config-reporter.js";
|
|
28
39
|
import {
|
|
29
|
-
CONFIG_PATH,
|
|
30
40
|
DEFAULT_EXTENSION_CONFIG,
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
EXTENSION_ROOT,
|
|
42
|
+
ensurePermissionSystemLogsDirectory,
|
|
33
43
|
normalizePermissionSystemConfig,
|
|
34
44
|
type PermissionSystemExtensionConfig,
|
|
35
|
-
savePermissionSystemConfig,
|
|
36
45
|
} from "./extension-config.js";
|
|
37
|
-
import { createPermissionSystemLogger, safeJsonStringify } from "./logging.js";
|
|
38
46
|
import {
|
|
39
|
-
|
|
47
|
+
formatExternalDirectoryAskPrompt,
|
|
48
|
+
formatExternalDirectoryDenyReason,
|
|
49
|
+
formatExternalDirectoryUserDeniedReason,
|
|
50
|
+
getPathBearingToolPath,
|
|
51
|
+
isPathOutsideWorkingDirectory,
|
|
52
|
+
normalizePathForComparison,
|
|
53
|
+
PATH_BEARING_TOOLS,
|
|
54
|
+
} from "./external-directory.js";
|
|
55
|
+
import { setForwardedPermissionLogger } from "./forwarded-permissions/io.js";
|
|
56
|
+
import {
|
|
57
|
+
confirmPermission,
|
|
58
|
+
type PermissionForwardingDeps,
|
|
59
|
+
processForwardedPermissionRequests,
|
|
60
|
+
} from "./forwarded-permissions/polling.js";
|
|
61
|
+
import { createPermissionSystemLogger } from "./logging.js";
|
|
62
|
+
import {
|
|
40
63
|
type PermissionPromptDecision,
|
|
41
64
|
requestPermissionDecisionFromUi,
|
|
42
65
|
} from "./permission-dialog.js";
|
|
43
|
-
import {
|
|
44
|
-
createPermissionForwardingLocation,
|
|
45
|
-
type ForwardedPermissionRequest,
|
|
46
|
-
type ForwardedPermissionResponse,
|
|
47
|
-
isForwardedPermissionRequestForSession,
|
|
48
|
-
PERMISSION_FORWARDING_POLL_INTERVAL_MS,
|
|
49
|
-
PERMISSION_FORWARDING_TIMEOUT_MS,
|
|
50
|
-
type PermissionForwardingLocation,
|
|
51
|
-
resolvePermissionForwardingTargetSessionId,
|
|
52
|
-
SUBAGENT_ENV_HINT_KEYS,
|
|
53
|
-
} from "./permission-forwarding.js";
|
|
66
|
+
import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding.js";
|
|
54
67
|
import { PermissionManager } from "./permission-manager.js";
|
|
68
|
+
import {
|
|
69
|
+
formatAskPrompt,
|
|
70
|
+
formatDenyReason,
|
|
71
|
+
formatMissingToolNameReason,
|
|
72
|
+
formatSkillAskPrompt,
|
|
73
|
+
formatSkillPathAskPrompt,
|
|
74
|
+
formatSkillPathDenyReason,
|
|
75
|
+
formatUnknownToolReason,
|
|
76
|
+
formatUserDeniedReason,
|
|
77
|
+
} from "./permission-prompts.js";
|
|
55
78
|
import {
|
|
56
79
|
findSkillPathMatch,
|
|
57
80
|
resolveSkillPromptEntries,
|
|
@@ -61,12 +84,13 @@ import {
|
|
|
61
84
|
PERMISSION_SYSTEM_STATUS_KEY,
|
|
62
85
|
syncPermissionSystemStatus,
|
|
63
86
|
} from "./status.js";
|
|
87
|
+
import { isSubagentExecutionContext } from "./subagent-context.js";
|
|
64
88
|
import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer.js";
|
|
89
|
+
import { getPermissionLogContext } from "./tool-input-preview.js";
|
|
65
90
|
import {
|
|
66
91
|
checkRequestedToolRegistration,
|
|
67
92
|
getToolNameFromValue,
|
|
68
93
|
} from "./tool-registry.js";
|
|
69
|
-
import type { PermissionCheckResult } from "./types.js";
|
|
70
94
|
import {
|
|
71
95
|
canResolveAskPermissionRequest,
|
|
72
96
|
shouldAutoApprovePermissionState,
|
|
@@ -77,23 +101,18 @@ const SESSIONS_DIR = join(PI_AGENT_DIR, "sessions");
|
|
|
77
101
|
const SUBAGENT_SESSIONS_DIR = join(PI_AGENT_DIR, "subagent-sessions");
|
|
78
102
|
const PERMISSION_FORWARDING_DIR = join(SESSIONS_DIR, "permission-forwarding");
|
|
79
103
|
|
|
80
|
-
const ACTIVE_AGENT_TAG_REGEX = /<active_agent\s+name=["']([^"']+)["'][^>]*>/i;
|
|
81
|
-
|
|
82
104
|
type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
|
|
83
|
-
const PATH_BEARING_TOOLS = new Set([
|
|
84
|
-
"read",
|
|
85
|
-
"write",
|
|
86
|
-
"edit",
|
|
87
|
-
"find",
|
|
88
|
-
"grep",
|
|
89
|
-
"ls",
|
|
90
|
-
]);
|
|
91
105
|
|
|
92
106
|
let extensionConfig: PermissionSystemExtensionConfig = {
|
|
93
107
|
...DEFAULT_EXTENSION_CONFIG,
|
|
94
108
|
};
|
|
109
|
+
const GLOBAL_LOGS_DIR = getGlobalLogsDir(PI_AGENT_DIR);
|
|
95
110
|
const extensionLogger = createPermissionSystemLogger({
|
|
96
111
|
getConfig: () => extensionConfig,
|
|
112
|
+
debugLogPath: join(GLOBAL_LOGS_DIR, DEBUG_LOG_FILENAME),
|
|
113
|
+
reviewLogPath: join(GLOBAL_LOGS_DIR, REVIEW_LOG_FILENAME),
|
|
114
|
+
ensureLogsDirectory: () =>
|
|
115
|
+
ensurePermissionSystemLogsDirectory(GLOBAL_LOGS_DIR),
|
|
97
116
|
});
|
|
98
117
|
const reportedLoggingWarnings = new Set<string>();
|
|
99
118
|
let loggingWarningReporter: ((message: string) => void) | null = null;
|
|
@@ -137,67 +156,6 @@ function writeReviewLog(
|
|
|
137
156
|
}
|
|
138
157
|
}
|
|
139
158
|
|
|
140
|
-
function normalizePathForComparison(pathValue: string, cwd: string): string {
|
|
141
|
-
const trimmed = pathValue.trim().replace(/^['"]|['"]$/g, "");
|
|
142
|
-
if (!trimmed) {
|
|
143
|
-
return "";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let normalizedPath = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
147
|
-
|
|
148
|
-
if (normalizedPath === "~") {
|
|
149
|
-
normalizedPath = homedir();
|
|
150
|
-
} else if (
|
|
151
|
-
normalizedPath.startsWith("~/") ||
|
|
152
|
-
normalizedPath.startsWith("~\\")
|
|
153
|
-
) {
|
|
154
|
-
normalizedPath = join(homedir(), normalizedPath.slice(2));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const absolutePath = resolve(cwd, normalizedPath);
|
|
158
|
-
const normalizedAbsolutePath = normalize(absolutePath);
|
|
159
|
-
return process.platform === "win32"
|
|
160
|
-
? normalizedAbsolutePath.toLowerCase()
|
|
161
|
-
: normalizedAbsolutePath;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isPathWithinDirectory(pathValue: string, directory: string): boolean {
|
|
165
|
-
if (!pathValue || !directory) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (pathValue === directory) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const prefix = directory.endsWith(sep) ? directory : `${directory}${sep}`;
|
|
174
|
-
return pathValue.startsWith(prefix);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function getPathBearingToolPath(
|
|
178
|
-
toolName: string,
|
|
179
|
-
input: unknown,
|
|
180
|
-
): string | null {
|
|
181
|
-
if (!PATH_BEARING_TOOLS.has(toolName)) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return getNonEmptyString(toRecord(input).path);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function isPathOutsideWorkingDirectory(
|
|
189
|
-
pathValue: string,
|
|
190
|
-
cwd: string,
|
|
191
|
-
): boolean {
|
|
192
|
-
const normalizedCwd = normalizePathForComparison(cwd, cwd);
|
|
193
|
-
const normalizedPath = normalizePathForComparison(pathValue, cwd);
|
|
194
|
-
return Boolean(
|
|
195
|
-
normalizedCwd &&
|
|
196
|
-
normalizedPath &&
|
|
197
|
-
!isPathWithinDirectory(normalizedPath, normalizedCwd),
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
159
|
function extractSkillNameFromInput(text: string): string | null {
|
|
202
160
|
const trimmed = text.trim();
|
|
203
161
|
if (!trimmed.startsWith("/skill:")) {
|
|
@@ -234,981 +192,14 @@ function getEventInput(event: unknown): unknown {
|
|
|
234
192
|
return {};
|
|
235
193
|
}
|
|
236
194
|
|
|
237
|
-
function normalizeAgentName(value: unknown): string | null {
|
|
238
|
-
if (typeof value !== "string") {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const trimmed = value.trim();
|
|
243
|
-
return trimmed ? trimmed : null;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function getActiveAgentName(ctx: ExtensionContext): string | null {
|
|
247
|
-
const entries = ctx.sessionManager.getEntries();
|
|
248
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
249
|
-
const entry = entries[i] as {
|
|
250
|
-
type: string;
|
|
251
|
-
customType?: string;
|
|
252
|
-
data?: unknown;
|
|
253
|
-
};
|
|
254
|
-
if (entry.type !== "custom" || entry.customType !== "active_agent") {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const data = entry.data as { name?: unknown } | undefined;
|
|
259
|
-
const normalizedName = normalizeAgentName(data?.name);
|
|
260
|
-
if (normalizedName) {
|
|
261
|
-
return normalizedName;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (data?.name === null) {
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function getActiveAgentNameFromSystemPrompt(
|
|
273
|
-
systemPrompt: string | undefined,
|
|
274
|
-
): string | null {
|
|
275
|
-
if (!systemPrompt) {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const match = systemPrompt.match(ACTIVE_AGENT_TAG_REGEX);
|
|
280
|
-
if (!match?.[1]) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return normalizeAgentName(match[1]);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function getContextSystemPrompt(ctx: ExtensionContext): string | undefined {
|
|
288
|
-
const getSystemPrompt = toRecord(ctx).getSystemPrompt;
|
|
289
|
-
if (typeof getSystemPrompt !== "function") {
|
|
290
|
-
return undefined;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
const systemPrompt = getSystemPrompt.call(ctx);
|
|
295
|
-
return typeof systemPrompt === "string" ? systemPrompt : undefined;
|
|
296
|
-
} catch (error) {
|
|
297
|
-
logPermissionForwardingWarning(
|
|
298
|
-
"Failed to read context system prompt for forwarded permission metadata",
|
|
299
|
-
error,
|
|
300
|
-
);
|
|
301
|
-
return undefined;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function formatMissingToolNameReason(): string {
|
|
306
|
-
return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function formatUnknownToolReason(
|
|
310
|
-
toolName: string,
|
|
311
|
-
availableToolNames: readonly string[],
|
|
312
|
-
): string {
|
|
313
|
-
const preview = availableToolNames.slice(0, 10);
|
|
314
|
-
const suffix = availableToolNames.length > preview.length ? ", ..." : "";
|
|
315
|
-
const availableList =
|
|
316
|
-
preview.length > 0 ? `${preview.join(", ")}${suffix}` : "none";
|
|
317
|
-
|
|
318
|
-
const mcpHint =
|
|
319
|
-
toolName === "mcp"
|
|
320
|
-
? ""
|
|
321
|
-
: ' If this was intended as an MCP server tool, call the registered \'mcp\' tool when available (for example: {"tool":"server:tool"}).';
|
|
322
|
-
|
|
323
|
-
return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function formatPermissionHardStopHint(result: PermissionCheckResult): string {
|
|
327
|
-
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
|
328
|
-
return "Hard stop: this MCP permission denial is policy-enforced. Do not retry this target, do not run discovery/investigation to bypass it, and report the block to the user.";
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return "Hard stop: this permission denial is policy-enforced. Do not retry or investigate bypasses; report the block to the user.";
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function formatDenyReason(
|
|
335
|
-
result: PermissionCheckResult,
|
|
336
|
-
agentName?: string,
|
|
337
|
-
): string {
|
|
338
|
-
const parts: string[] = [];
|
|
339
|
-
|
|
340
|
-
if (agentName) {
|
|
341
|
-
parts.push(`Agent '${agentName}'`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
|
345
|
-
parts.push(`is not permitted to run MCP target '${result.target}'`);
|
|
346
|
-
} else {
|
|
347
|
-
parts.push(`is not permitted to run '${result.toolName}'`);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (result.command) {
|
|
351
|
-
parts.push(`command '${result.command}'`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (result.matchedPattern) {
|
|
355
|
-
parts.push(`(matched '${result.matchedPattern}')`);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return `${parts.join(" ")}. ${formatPermissionHardStopHint(result)}`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function formatUserDeniedReason(
|
|
362
|
-
result: PermissionCheckResult,
|
|
363
|
-
denialReason?: string,
|
|
364
|
-
): string {
|
|
365
|
-
const base =
|
|
366
|
-
(result.source === "mcp" || result.toolName === "mcp") && result.target
|
|
367
|
-
? `User denied MCP target '${result.target}'.`
|
|
368
|
-
: result.toolName === "bash" && result.command
|
|
369
|
-
? `User denied bash command '${result.command}'.`
|
|
370
|
-
: `User denied tool '${result.toolName}'.`;
|
|
371
|
-
const reasonSuffix = denialReason ? ` Reason: ${denialReason}.` : "";
|
|
372
|
-
|
|
373
|
-
return `${base}${reasonSuffix} ${formatPermissionHardStopHint(result)}`;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
|
|
377
|
-
const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
|
|
378
|
-
const TOOL_TEXT_SUMMARY_MAX_LENGTH = 80;
|
|
379
|
-
|
|
380
|
-
function truncateInlineText(value: string, maxLength: number): string {
|
|
381
|
-
return value.length > maxLength ? `${value.slice(0, maxLength)}…` : value;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function sanitizeInlineText(
|
|
385
|
-
value: string,
|
|
386
|
-
maxLength = TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
387
|
-
): string {
|
|
388
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
389
|
-
return normalized ? truncateInlineText(normalized, maxLength) : "empty text";
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function countTextLines(value: string): number {
|
|
393
|
-
if (!value) {
|
|
394
|
-
return 0;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return value.split(/\r\n|\r|\n/).length;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function formatCount(value: number, singular: string, plural: string): string {
|
|
401
|
-
return `${value} ${value === 1 ? singular : plural}`;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function getPromptPath(input: Record<string, unknown>): string | null {
|
|
405
|
-
return getNonEmptyString(input.path) ?? getNonEmptyString(input.file_path);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function formatEditInputForPrompt(input: Record<string, unknown>): string {
|
|
409
|
-
const path = getPromptPath(input);
|
|
410
|
-
const rawEdits = Array.isArray(input.edits)
|
|
411
|
-
? input.edits
|
|
412
|
-
: typeof input.oldText === "string" && typeof input.newText === "string"
|
|
413
|
-
? [{ oldText: input.oldText, newText: input.newText }]
|
|
414
|
-
: [];
|
|
415
|
-
|
|
416
|
-
const edits = rawEdits
|
|
417
|
-
.map((edit) => toRecord(edit))
|
|
418
|
-
.filter(
|
|
419
|
-
(edit) =>
|
|
420
|
-
typeof edit.oldText === "string" && typeof edit.newText === "string",
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
const pathPart = path ? `for '${path}'` : "";
|
|
424
|
-
if (edits.length === 0) {
|
|
425
|
-
return pathPart ? `${pathPart} with edit input` : "with edit input";
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const firstEdit = edits[0];
|
|
429
|
-
const oldText = String(firstEdit.oldText);
|
|
430
|
-
const newText = String(firstEdit.newText);
|
|
431
|
-
const firstEditSummary = `edit #1 replaces ${formatCount(countTextLines(oldText), "line", "lines")} with ${formatCount(countTextLines(newText), "line", "lines")}`;
|
|
432
|
-
const extraEdits =
|
|
433
|
-
edits.length > 1
|
|
434
|
-
? `, plus ${formatCount(edits.length - 1, "additional edit", "additional edits")}`
|
|
435
|
-
: "";
|
|
436
|
-
const summary = `(${formatCount(edits.length, "replacement", "replacements")}: ${firstEditSummary}${extraEdits})`;
|
|
437
|
-
return pathPart ? `${pathPart} ${summary}` : summary;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function formatWriteInputForPrompt(input: Record<string, unknown>): string {
|
|
441
|
-
const path = getPromptPath(input);
|
|
442
|
-
const content = typeof input.content === "string" ? input.content : "";
|
|
443
|
-
const summary = `(${formatCount(countTextLines(content), "line", "lines")}, ${formatCount(content.length, "character", "characters")})`;
|
|
444
|
-
return path ? `for '${path}' ${summary}` : summary;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function formatReadInputForPrompt(input: Record<string, unknown>): string {
|
|
448
|
-
const path = getPromptPath(input);
|
|
449
|
-
const parts = path ? [`path '${path}'`] : [];
|
|
450
|
-
if (typeof input.offset === "number") {
|
|
451
|
-
parts.push(`offset ${input.offset}`);
|
|
452
|
-
}
|
|
453
|
-
if (typeof input.limit === "number") {
|
|
454
|
-
parts.push(`limit ${input.limit}`);
|
|
455
|
-
}
|
|
456
|
-
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function formatSearchInputForPrompt(
|
|
460
|
-
toolName: string,
|
|
461
|
-
input: Record<string, unknown>,
|
|
462
|
-
): string {
|
|
463
|
-
const parts: string[] = [];
|
|
464
|
-
const path = getPromptPath(input);
|
|
465
|
-
const pattern = getNonEmptyString(input.pattern);
|
|
466
|
-
const glob = getNonEmptyString(input.glob);
|
|
467
|
-
|
|
468
|
-
if (pattern) {
|
|
469
|
-
parts.push(`pattern '${sanitizeInlineText(pattern)}'`);
|
|
470
|
-
}
|
|
471
|
-
if (glob) {
|
|
472
|
-
parts.push(`glob '${sanitizeInlineText(glob)}'`);
|
|
473
|
-
}
|
|
474
|
-
if (path) {
|
|
475
|
-
parts.push(`path '${path}'`);
|
|
476
|
-
} else if (toolName === "find" || toolName === "grep" || toolName === "ls") {
|
|
477
|
-
parts.push("current working directory");
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function serializeToolInputPreview(input: unknown): string {
|
|
484
|
-
const serialized = safeJsonStringify(input);
|
|
485
|
-
if (!serialized || serialized === "{}" || serialized === "null") {
|
|
486
|
-
return "";
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return serialized.replace(/\s+/g, " ").trim();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function formatJsonInputForPrompt(input: unknown): string {
|
|
493
|
-
const inline = serializeToolInputPreview(input);
|
|
494
|
-
return inline
|
|
495
|
-
? `with input ${truncateInlineText(inline, TOOL_INPUT_PREVIEW_MAX_LENGTH)}`
|
|
496
|
-
: "";
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function formatToolInputForPrompt(toolName: string, input: unknown): string {
|
|
500
|
-
const inputRecord = toRecord(input);
|
|
501
|
-
|
|
502
|
-
switch (toolName) {
|
|
503
|
-
case "edit":
|
|
504
|
-
return formatEditInputForPrompt(inputRecord);
|
|
505
|
-
case "write":
|
|
506
|
-
return formatWriteInputForPrompt(inputRecord);
|
|
507
|
-
case "read":
|
|
508
|
-
return formatReadInputForPrompt(inputRecord);
|
|
509
|
-
case "find":
|
|
510
|
-
case "grep":
|
|
511
|
-
case "ls":
|
|
512
|
-
return formatSearchInputForPrompt(toolName, inputRecord);
|
|
513
|
-
default:
|
|
514
|
-
return formatJsonInputForPrompt(input);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function formatAskPrompt(
|
|
519
|
-
result: PermissionCheckResult,
|
|
520
|
-
agentName?: string,
|
|
521
|
-
input?: unknown,
|
|
522
|
-
): string {
|
|
523
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
524
|
-
|
|
525
|
-
if (result.toolName === "bash") {
|
|
526
|
-
const patternInfo = result.matchedPattern
|
|
527
|
-
? ` (matched '${result.matchedPattern}')`
|
|
528
|
-
: "";
|
|
529
|
-
return `${subject} requested bash command '${result.command || ""}'${patternInfo}. Allow this command?`;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
|
533
|
-
const patternInfo = result.matchedPattern
|
|
534
|
-
? ` (matched '${result.matchedPattern}')`
|
|
535
|
-
: "";
|
|
536
|
-
return `${subject} requested MCP target '${result.target}'${patternInfo}. Allow this call?`;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const patternInfo = result.matchedPattern
|
|
540
|
-
? ` (matched '${result.matchedPattern}')`
|
|
541
|
-
: "";
|
|
542
|
-
const inputPreview = formatToolInputForPrompt(result.toolName, input);
|
|
543
|
-
const inputSuffix = inputPreview ? ` ${inputPreview}` : "";
|
|
544
|
-
return `${subject} requested tool '${result.toolName}'${patternInfo}${inputSuffix}. Allow this call?`;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function formatSkillAskPrompt(skillName: string, agentName?: string): string {
|
|
548
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
549
|
-
return `${subject} requested skill '${skillName}'. Allow loading this skill?`;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function formatSkillPathAskPrompt(
|
|
553
|
-
skill: SkillPromptEntry,
|
|
554
|
-
readPath: string,
|
|
555
|
-
agentName?: string,
|
|
556
|
-
): string {
|
|
557
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
558
|
-
return `${subject} requested access to skill '${skill.name}' via '${readPath}'. Allow this read?`;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function formatSkillPathDenyReason(
|
|
562
|
-
skill: SkillPromptEntry,
|
|
563
|
-
readPath: string,
|
|
564
|
-
agentName?: string,
|
|
565
|
-
): string {
|
|
566
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
567
|
-
return `${subject} is not permitted to access skill '${skill.name}' via '${readPath}'.`;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function formatExternalDirectoryHardStopHint(): string {
|
|
571
|
-
return "Hard stop: this external directory permission denial is policy-enforced. Do not retry this path, do not attempt a filesystem bypass, and report the block to the user.";
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function formatExternalDirectoryAskPrompt(
|
|
575
|
-
toolName: string,
|
|
576
|
-
pathValue: string,
|
|
577
|
-
cwd: string,
|
|
578
|
-
agentName?: string,
|
|
579
|
-
): string {
|
|
580
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
581
|
-
return `${subject} requested tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. Allow this external directory access?`;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function formatExternalDirectoryDenyReason(
|
|
585
|
-
toolName: string,
|
|
586
|
-
pathValue: string,
|
|
587
|
-
cwd: string,
|
|
588
|
-
agentName?: string,
|
|
589
|
-
): string {
|
|
590
|
-
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
591
|
-
return `${subject} is not permitted to run tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. ${formatExternalDirectoryHardStopHint()}`;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function formatExternalDirectoryUserDeniedReason(
|
|
595
|
-
toolName: string,
|
|
596
|
-
pathValue: string,
|
|
597
|
-
denialReason?: string,
|
|
598
|
-
): string {
|
|
599
|
-
const reasonSuffix = denialReason ? ` Reason: ${denialReason}.` : "";
|
|
600
|
-
return `User denied external directory access for tool '${toolName}' path '${pathValue}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function formatGenericToolInputForLog(input: unknown): string | undefined {
|
|
604
|
-
const inline = serializeToolInputPreview(input);
|
|
605
|
-
return inline
|
|
606
|
-
? `input ${truncateInlineText(inline, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)}`
|
|
607
|
-
: undefined;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function getToolInputPreviewForLog(
|
|
611
|
-
result: PermissionCheckResult,
|
|
612
|
-
input: unknown,
|
|
613
|
-
): string | undefined {
|
|
614
|
-
if (
|
|
615
|
-
result.toolName === "bash" ||
|
|
616
|
-
result.toolName === "mcp" ||
|
|
617
|
-
result.source === "mcp"
|
|
618
|
-
) {
|
|
619
|
-
return undefined;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (PATH_BEARING_TOOLS.has(result.toolName)) {
|
|
623
|
-
const inputPreview = formatToolInputForPrompt(result.toolName, input);
|
|
624
|
-
return inputPreview
|
|
625
|
-
? truncateInlineText(inputPreview, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)
|
|
626
|
-
: undefined;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
return formatGenericToolInputForLog(input);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function getPermissionLogContext(
|
|
633
|
-
result: PermissionCheckResult,
|
|
634
|
-
input: unknown,
|
|
635
|
-
): { command?: string; target?: string; toolInputPreview?: string } {
|
|
636
|
-
return {
|
|
637
|
-
command: result.command,
|
|
638
|
-
target: result.target,
|
|
639
|
-
toolInputPreview: getToolInputPreviewForLog(result, input),
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function sleep(ms: number): Promise<void> {
|
|
644
|
-
return new Promise((resolve) => {
|
|
645
|
-
setTimeout(resolve, ms);
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function normalizeFilesystemPath(pathValue: string): string {
|
|
650
|
-
const normalizedPath = normalize(pathValue);
|
|
651
|
-
return process.platform === "win32"
|
|
652
|
-
? normalizedPath.toLowerCase()
|
|
653
|
-
: normalizedPath;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function getSessionId(ctx: ExtensionContext): string {
|
|
657
|
-
try {
|
|
658
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
659
|
-
if (typeof sessionId === "string" && sessionId.trim()) {
|
|
660
|
-
return sessionId.trim();
|
|
661
|
-
}
|
|
662
|
-
} catch {}
|
|
663
|
-
|
|
664
|
-
return "unknown";
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function isSubagentExecutionContext(ctx: ExtensionContext): boolean {
|
|
668
|
-
for (const key of SUBAGENT_ENV_HINT_KEYS) {
|
|
669
|
-
const value = process.env[key];
|
|
670
|
-
if (typeof value === "string" && value.trim()) {
|
|
671
|
-
return true;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
676
|
-
if (!sessionDir) {
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const normalizedSessionDir = normalizeFilesystemPath(sessionDir);
|
|
681
|
-
const normalizedSubagentRoot = normalizeFilesystemPath(SUBAGENT_SESSIONS_DIR);
|
|
682
|
-
return isPathWithinDirectory(normalizedSessionDir, normalizedSubagentRoot);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
195
|
function canRequestPermissionConfirmation(ctx: ExtensionContext): boolean {
|
|
686
196
|
return canResolveAskPermissionRequest({
|
|
687
197
|
config: extensionConfig,
|
|
688
198
|
hasUI: ctx.hasUI,
|
|
689
|
-
isSubagent: isSubagentExecutionContext(ctx),
|
|
199
|
+
isSubagent: isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR),
|
|
690
200
|
});
|
|
691
201
|
}
|
|
692
202
|
|
|
693
|
-
function formatUnknownErrorMessage(error: unknown): string {
|
|
694
|
-
if (error instanceof Error && error.message) {
|
|
695
|
-
return error.message;
|
|
696
|
-
}
|
|
697
|
-
return String(error);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function isErrnoCode(error: unknown, code: string): boolean {
|
|
701
|
-
return Boolean(
|
|
702
|
-
error &&
|
|
703
|
-
typeof error === "object" &&
|
|
704
|
-
"code" in error &&
|
|
705
|
-
(error as { code?: string }).code === code,
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
function logPermissionForwardingWarning(
|
|
710
|
-
message: string,
|
|
711
|
-
error?: unknown,
|
|
712
|
-
): void {
|
|
713
|
-
const details =
|
|
714
|
-
typeof error === "undefined"
|
|
715
|
-
? { message }
|
|
716
|
-
: { message, error: formatUnknownErrorMessage(error) };
|
|
717
|
-
|
|
718
|
-
writeReviewLog("permission_forwarding.warning", details);
|
|
719
|
-
writeDebugLog("permission_forwarding.warning", details);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function logPermissionForwardingError(message: string, error?: unknown): void {
|
|
723
|
-
const details =
|
|
724
|
-
typeof error === "undefined"
|
|
725
|
-
? { message }
|
|
726
|
-
: { message, error: formatUnknownErrorMessage(error) };
|
|
727
|
-
|
|
728
|
-
writeReviewLog("permission_forwarding.error", details);
|
|
729
|
-
writeDebugLog("permission_forwarding.error", details);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function ensureDirectoryExists(path: string, description: string): boolean {
|
|
733
|
-
try {
|
|
734
|
-
mkdirSync(path, { recursive: true });
|
|
735
|
-
return true;
|
|
736
|
-
} catch (error) {
|
|
737
|
-
logPermissionForwardingError(
|
|
738
|
-
`Failed to create ${description} directory '${path}'`,
|
|
739
|
-
error,
|
|
740
|
-
);
|
|
741
|
-
return false;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function getPermissionForwardingLocationForSession(
|
|
746
|
-
sessionId: string,
|
|
747
|
-
): PermissionForwardingLocation {
|
|
748
|
-
return createPermissionForwardingLocation(
|
|
749
|
-
PERMISSION_FORWARDING_DIR,
|
|
750
|
-
sessionId,
|
|
751
|
-
);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function ensurePermissionForwardingLocation(
|
|
755
|
-
sessionId: string,
|
|
756
|
-
): PermissionForwardingLocation | null {
|
|
757
|
-
let location: PermissionForwardingLocation;
|
|
758
|
-
try {
|
|
759
|
-
location = getPermissionForwardingLocationForSession(sessionId);
|
|
760
|
-
} catch (error) {
|
|
761
|
-
logPermissionForwardingError(
|
|
762
|
-
"Failed to resolve permission forwarding location",
|
|
763
|
-
error,
|
|
764
|
-
);
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const sessionRootReady = ensureDirectoryExists(
|
|
769
|
-
location.sessionRootDir,
|
|
770
|
-
"permission forwarding session root",
|
|
771
|
-
);
|
|
772
|
-
const requestsReady = ensureDirectoryExists(
|
|
773
|
-
location.requestsDir,
|
|
774
|
-
"permission forwarding requests",
|
|
775
|
-
);
|
|
776
|
-
const responsesReady = ensureDirectoryExists(
|
|
777
|
-
location.responsesDir,
|
|
778
|
-
"permission forwarding responses",
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
return sessionRootReady && requestsReady && responsesReady ? location : null;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function getExistingPermissionForwardingLocation(
|
|
785
|
-
sessionId: string,
|
|
786
|
-
): PermissionForwardingLocation | null {
|
|
787
|
-
let location: PermissionForwardingLocation;
|
|
788
|
-
try {
|
|
789
|
-
location = getPermissionForwardingLocationForSession(sessionId);
|
|
790
|
-
} catch {
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return existsSync(location.requestsDir) ? location : null;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
function tryRemoveDirectoryIfEmpty(path: string, description: string): void {
|
|
798
|
-
if (!existsSync(path)) {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
let entries: string[];
|
|
803
|
-
try {
|
|
804
|
-
entries = readdirSync(path);
|
|
805
|
-
} catch (error) {
|
|
806
|
-
logPermissionForwardingWarning(
|
|
807
|
-
`Failed to inspect ${description} directory '${path}'`,
|
|
808
|
-
error,
|
|
809
|
-
);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (entries.length > 0) {
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
try {
|
|
818
|
-
rmdirSync(path);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
if (isErrnoCode(error, "ENOENT") || isErrnoCode(error, "ENOTEMPTY")) {
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
logPermissionForwardingWarning(
|
|
825
|
-
`Failed to remove empty ${description} directory '${path}'`,
|
|
826
|
-
error,
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function cleanupPermissionForwardingLocationIfEmpty(
|
|
832
|
-
location: PermissionForwardingLocation,
|
|
833
|
-
): void {
|
|
834
|
-
tryRemoveDirectoryIfEmpty(
|
|
835
|
-
location.requestsDir,
|
|
836
|
-
`${location.label} permission forwarding requests`,
|
|
837
|
-
);
|
|
838
|
-
tryRemoveDirectoryIfEmpty(
|
|
839
|
-
location.responsesDir,
|
|
840
|
-
`${location.label} permission forwarding responses`,
|
|
841
|
-
);
|
|
842
|
-
tryRemoveDirectoryIfEmpty(
|
|
843
|
-
location.sessionRootDir,
|
|
844
|
-
`${location.label} permission forwarding session root`,
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function safeDeleteFile(filePath: string, description: string): void {
|
|
849
|
-
try {
|
|
850
|
-
unlinkSync(filePath);
|
|
851
|
-
} catch (error) {
|
|
852
|
-
if (isErrnoCode(error, "ENOENT")) {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
logPermissionForwardingWarning(
|
|
857
|
-
`Failed to delete ${description} file '${filePath}'`,
|
|
858
|
-
error,
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function writeJsonFileAtomic(filePath: string, value: unknown): void {
|
|
864
|
-
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
865
|
-
|
|
866
|
-
try {
|
|
867
|
-
writeFileSync(tempPath, JSON.stringify(value), "utf-8");
|
|
868
|
-
renameSync(tempPath, filePath);
|
|
869
|
-
} catch (error) {
|
|
870
|
-
safeDeleteFile(tempPath, "temporary permission-forwarding");
|
|
871
|
-
throw error;
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function readForwardedPermissionRequest(
|
|
876
|
-
filePath: string,
|
|
877
|
-
): ForwardedPermissionRequest | null {
|
|
878
|
-
try {
|
|
879
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
880
|
-
const parsed = JSON.parse(raw) as Partial<ForwardedPermissionRequest>;
|
|
881
|
-
if (
|
|
882
|
-
!parsed ||
|
|
883
|
-
typeof parsed.id !== "string" ||
|
|
884
|
-
typeof parsed.createdAt !== "number" ||
|
|
885
|
-
typeof parsed.requesterSessionId !== "string" ||
|
|
886
|
-
typeof parsed.targetSessionId !== "string" ||
|
|
887
|
-
typeof parsed.requesterAgentName !== "string" ||
|
|
888
|
-
typeof parsed.message !== "string"
|
|
889
|
-
) {
|
|
890
|
-
logPermissionForwardingWarning(
|
|
891
|
-
`Ignoring invalid forwarded permission request format in '${filePath}'`,
|
|
892
|
-
);
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
return {
|
|
897
|
-
id: parsed.id,
|
|
898
|
-
createdAt: parsed.createdAt,
|
|
899
|
-
requesterSessionId: parsed.requesterSessionId,
|
|
900
|
-
targetSessionId: parsed.targetSessionId,
|
|
901
|
-
requesterAgentName: parsed.requesterAgentName,
|
|
902
|
-
message: parsed.message,
|
|
903
|
-
};
|
|
904
|
-
} catch (error) {
|
|
905
|
-
logPermissionForwardingWarning(
|
|
906
|
-
`Failed to read forwarded permission request '${filePath}'`,
|
|
907
|
-
error,
|
|
908
|
-
);
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function readForwardedPermissionResponse(
|
|
914
|
-
filePath: string,
|
|
915
|
-
): ForwardedPermissionResponse | null {
|
|
916
|
-
try {
|
|
917
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
918
|
-
const parsed = JSON.parse(raw) as Partial<ForwardedPermissionResponse>;
|
|
919
|
-
if (
|
|
920
|
-
!parsed ||
|
|
921
|
-
typeof parsed.approved !== "boolean" ||
|
|
922
|
-
!isPermissionDecisionState(parsed.state) ||
|
|
923
|
-
typeof parsed.responderSessionId !== "string"
|
|
924
|
-
) {
|
|
925
|
-
logPermissionForwardingWarning(
|
|
926
|
-
`Ignoring invalid forwarded permission response format in '${filePath}'`,
|
|
927
|
-
);
|
|
928
|
-
return null;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
return {
|
|
932
|
-
approved: parsed.approved,
|
|
933
|
-
state: parsed.state,
|
|
934
|
-
denialReason:
|
|
935
|
-
typeof parsed.denialReason === "string"
|
|
936
|
-
? parsed.denialReason
|
|
937
|
-
: undefined,
|
|
938
|
-
responderSessionId: parsed.responderSessionId,
|
|
939
|
-
respondedAt:
|
|
940
|
-
typeof parsed.respondedAt === "number"
|
|
941
|
-
? parsed.respondedAt
|
|
942
|
-
: Date.now(),
|
|
943
|
-
};
|
|
944
|
-
} catch (error) {
|
|
945
|
-
logPermissionForwardingWarning(
|
|
946
|
-
`Failed to read forwarded permission response '${filePath}'`,
|
|
947
|
-
error,
|
|
948
|
-
);
|
|
949
|
-
return null;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function formatForwardedPermissionPrompt(
|
|
954
|
-
request: ForwardedPermissionRequest,
|
|
955
|
-
): string {
|
|
956
|
-
const agentName = request.requesterAgentName || "unknown";
|
|
957
|
-
const sessionId = request.requesterSessionId || "unknown";
|
|
958
|
-
return [
|
|
959
|
-
`Subagent '${agentName}' requested permission.`,
|
|
960
|
-
`Session ID: ${sessionId}`,
|
|
961
|
-
"",
|
|
962
|
-
request.message,
|
|
963
|
-
].join("\n");
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
async function waitForForwardedPermissionApproval(
|
|
967
|
-
ctx: ExtensionContext,
|
|
968
|
-
message: string,
|
|
969
|
-
): Promise<PermissionPromptDecision> {
|
|
970
|
-
const requesterSessionId = getSessionId(ctx);
|
|
971
|
-
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
972
|
-
hasUI: ctx.hasUI,
|
|
973
|
-
isSubagent: isSubagentExecutionContext(ctx),
|
|
974
|
-
currentSessionId: requesterSessionId,
|
|
975
|
-
env: process.env,
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
if (!targetSessionId) {
|
|
979
|
-
logPermissionForwardingError(
|
|
980
|
-
"Permission forwarding target session could not be resolved from subagent runtime metadata (expected PI_AGENT_ROUTER_PARENT_SESSION_ID)",
|
|
981
|
-
);
|
|
982
|
-
return { approved: false, state: "denied" };
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const location = ensurePermissionForwardingLocation(targetSessionId);
|
|
986
|
-
if (!location) {
|
|
987
|
-
logPermissionForwardingError(
|
|
988
|
-
`Permission forwarding is unavailable because session-scoped directories could not be prepared for '${targetSessionId}'`,
|
|
989
|
-
);
|
|
990
|
-
return { approved: false, state: "denied" };
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
|
|
994
|
-
const requesterAgentName =
|
|
995
|
-
getActiveAgentName(ctx) ||
|
|
996
|
-
getActiveAgentNameFromSystemPrompt(getContextSystemPrompt(ctx)) ||
|
|
997
|
-
"unknown";
|
|
998
|
-
const request: ForwardedPermissionRequest = {
|
|
999
|
-
id: requestId,
|
|
1000
|
-
createdAt: Date.now(),
|
|
1001
|
-
requesterSessionId,
|
|
1002
|
-
targetSessionId,
|
|
1003
|
-
requesterAgentName,
|
|
1004
|
-
message,
|
|
1005
|
-
};
|
|
1006
|
-
|
|
1007
|
-
const requestPath = join(location.requestsDir, `${requestId}.json`);
|
|
1008
|
-
const responsePath = join(location.responsesDir, `${requestId}.json`);
|
|
1009
|
-
|
|
1010
|
-
writeReviewLog("forwarded_permission.request_created", {
|
|
1011
|
-
requestId,
|
|
1012
|
-
requesterAgentName,
|
|
1013
|
-
requesterSessionId: request.requesterSessionId,
|
|
1014
|
-
targetSessionId,
|
|
1015
|
-
requestPath,
|
|
1016
|
-
responsePath,
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
try {
|
|
1020
|
-
writeJsonFileAtomic(requestPath, request);
|
|
1021
|
-
} catch (error) {
|
|
1022
|
-
logPermissionForwardingError(
|
|
1023
|
-
`Failed to write forwarded permission request '${requestPath}'`,
|
|
1024
|
-
error,
|
|
1025
|
-
);
|
|
1026
|
-
return { approved: false, state: "denied" };
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const deadline = Date.now() + PERMISSION_FORWARDING_TIMEOUT_MS;
|
|
1030
|
-
while (Date.now() < deadline) {
|
|
1031
|
-
if (existsSync(responsePath)) {
|
|
1032
|
-
const response = readForwardedPermissionResponse(responsePath);
|
|
1033
|
-
writeReviewLog("forwarded_permission.response_received", {
|
|
1034
|
-
requestId,
|
|
1035
|
-
approved: response?.approved ?? null,
|
|
1036
|
-
state: response?.state ?? null,
|
|
1037
|
-
denialReason: response?.denialReason ?? null,
|
|
1038
|
-
responderSessionId: response?.responderSessionId ?? null,
|
|
1039
|
-
targetSessionId,
|
|
1040
|
-
responsePath,
|
|
1041
|
-
});
|
|
1042
|
-
safeDeleteFile(responsePath, "forwarded permission response");
|
|
1043
|
-
safeDeleteFile(requestPath, "forwarded permission request");
|
|
1044
|
-
cleanupPermissionForwardingLocationIfEmpty(location);
|
|
1045
|
-
return response ?? { approved: false, state: "denied" };
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
await sleep(PERMISSION_FORWARDING_POLL_INTERVAL_MS);
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
logPermissionForwardingWarning(
|
|
1052
|
-
`Timed out waiting for forwarded permission response '${responsePath}'`,
|
|
1053
|
-
);
|
|
1054
|
-
writeReviewLog("forwarded_permission.response_timed_out", {
|
|
1055
|
-
requestId,
|
|
1056
|
-
requesterAgentName,
|
|
1057
|
-
targetSessionId,
|
|
1058
|
-
responsePath,
|
|
1059
|
-
});
|
|
1060
|
-
safeDeleteFile(requestPath, "forwarded permission request");
|
|
1061
|
-
cleanupPermissionForwardingLocationIfEmpty(location);
|
|
1062
|
-
return { approved: false, state: "denied" };
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
async function processForwardedPermissionRequests(
|
|
1066
|
-
ctx: ExtensionContext,
|
|
1067
|
-
): Promise<void> {
|
|
1068
|
-
if (!ctx.hasUI) {
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
const currentSessionId = getSessionId(ctx);
|
|
1073
|
-
const location = getExistingPermissionForwardingLocation(currentSessionId);
|
|
1074
|
-
if (!location) {
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
let requestFiles: string[] = [];
|
|
1079
|
-
try {
|
|
1080
|
-
requestFiles = readdirSync(location.requestsDir)
|
|
1081
|
-
.filter((name) => name.endsWith(".json"))
|
|
1082
|
-
.sort();
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
logPermissionForwardingWarning(
|
|
1085
|
-
`Failed to read ${location.label} permission forwarding requests from '${location.requestsDir}'`,
|
|
1086
|
-
error,
|
|
1087
|
-
);
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
for (const fileName of requestFiles) {
|
|
1092
|
-
const requestPath = join(location.requestsDir, fileName);
|
|
1093
|
-
const request = readForwardedPermissionRequest(requestPath);
|
|
1094
|
-
if (!request) {
|
|
1095
|
-
safeDeleteFile(
|
|
1096
|
-
requestPath,
|
|
1097
|
-
`${location.label} forwarded permission request`,
|
|
1098
|
-
);
|
|
1099
|
-
continue;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (!isForwardedPermissionRequestForSession(request, currentSessionId)) {
|
|
1103
|
-
logPermissionForwardingWarning(
|
|
1104
|
-
`Ignoring forwarded permission request '${request.id}' because it targets session '${request.targetSessionId}' instead of '${currentSessionId}'`,
|
|
1105
|
-
);
|
|
1106
|
-
safeDeleteFile(
|
|
1107
|
-
requestPath,
|
|
1108
|
-
`${location.label} forwarded permission request`,
|
|
1109
|
-
);
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
const forwardedPermissionLogDetails = {
|
|
1114
|
-
requestId: request.id,
|
|
1115
|
-
source: location.label,
|
|
1116
|
-
requesterAgentName: request.requesterAgentName,
|
|
1117
|
-
requesterSessionId: request.requesterSessionId,
|
|
1118
|
-
targetSessionId: request.targetSessionId,
|
|
1119
|
-
requestPath,
|
|
1120
|
-
};
|
|
1121
|
-
|
|
1122
|
-
let decision: PermissionPromptDecision = {
|
|
1123
|
-
approved: false,
|
|
1124
|
-
state: "denied",
|
|
1125
|
-
};
|
|
1126
|
-
if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
|
|
1127
|
-
writeReviewLog(
|
|
1128
|
-
"forwarded_permission.auto_approved",
|
|
1129
|
-
forwardedPermissionLogDetails,
|
|
1130
|
-
);
|
|
1131
|
-
decision = { approved: true, state: "approved" };
|
|
1132
|
-
} else {
|
|
1133
|
-
writeReviewLog(
|
|
1134
|
-
"forwarded_permission.prompted",
|
|
1135
|
-
forwardedPermissionLogDetails,
|
|
1136
|
-
);
|
|
1137
|
-
try {
|
|
1138
|
-
decision = await requestPermissionDecisionFromUi(
|
|
1139
|
-
ctx.ui,
|
|
1140
|
-
"Permission Required (Subagent)",
|
|
1141
|
-
formatForwardedPermissionPrompt(request),
|
|
1142
|
-
);
|
|
1143
|
-
} catch (error) {
|
|
1144
|
-
logPermissionForwardingError(
|
|
1145
|
-
"Failed to show forwarded permission confirmation dialog",
|
|
1146
|
-
error,
|
|
1147
|
-
);
|
|
1148
|
-
decision = { approved: false, state: "denied" };
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
const responsePath = join(location.responsesDir, `${request.id}.json`);
|
|
1153
|
-
writeReviewLog(
|
|
1154
|
-
decision.approved
|
|
1155
|
-
? "forwarded_permission.approved"
|
|
1156
|
-
: "forwarded_permission.denied",
|
|
1157
|
-
{
|
|
1158
|
-
requestId: request.id,
|
|
1159
|
-
source: location.label,
|
|
1160
|
-
requesterAgentName: request.requesterAgentName,
|
|
1161
|
-
requesterSessionId: request.requesterSessionId,
|
|
1162
|
-
targetSessionId: request.targetSessionId,
|
|
1163
|
-
responsePath,
|
|
1164
|
-
resolution: decision.state,
|
|
1165
|
-
denialReason: decision.denialReason ?? null,
|
|
1166
|
-
},
|
|
1167
|
-
);
|
|
1168
|
-
try {
|
|
1169
|
-
writeJsonFileAtomic(responsePath, {
|
|
1170
|
-
approved: decision.approved,
|
|
1171
|
-
state: decision.state,
|
|
1172
|
-
denialReason: decision.denialReason,
|
|
1173
|
-
responderSessionId: currentSessionId,
|
|
1174
|
-
respondedAt: Date.now(),
|
|
1175
|
-
} satisfies ForwardedPermissionResponse);
|
|
1176
|
-
} catch (error) {
|
|
1177
|
-
logPermissionForwardingError(
|
|
1178
|
-
`Failed to write ${location.label} forwarded permission response '${responsePath}'`,
|
|
1179
|
-
error,
|
|
1180
|
-
);
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
safeDeleteFile(
|
|
1185
|
-
requestPath,
|
|
1186
|
-
`${location.label} forwarded permission request`,
|
|
1187
|
-
);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
cleanupPermissionForwardingLocationIfEmpty(location);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
async function confirmPermission(
|
|
1194
|
-
ctx: ExtensionContext,
|
|
1195
|
-
message: string,
|
|
1196
|
-
): Promise<PermissionPromptDecision> {
|
|
1197
|
-
if (ctx.hasUI) {
|
|
1198
|
-
return requestPermissionDecisionFromUi(
|
|
1199
|
-
ctx.ui,
|
|
1200
|
-
"Permission Required",
|
|
1201
|
-
message,
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
if (!isSubagentExecutionContext(ctx)) {
|
|
1206
|
-
return { approved: false, state: "denied" };
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
return waitForForwardedPermissionApproval(ctx, message);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
203
|
function derivePiProjectPaths(cwd: string | undefined | null): {
|
|
1213
204
|
projectGlobalConfigPath: string;
|
|
1214
205
|
projectAgentsDir: string;
|
|
@@ -1217,24 +208,22 @@ function derivePiProjectPaths(cwd: string | undefined | null): {
|
|
|
1217
208
|
return null;
|
|
1218
209
|
}
|
|
1219
210
|
|
|
1220
|
-
const projectAgentRoot = join(cwd, ".pi", "agent");
|
|
1221
211
|
return {
|
|
1222
|
-
projectGlobalConfigPath:
|
|
1223
|
-
projectAgentsDir: join(
|
|
212
|
+
projectGlobalConfigPath: getProjectConfigPath(cwd),
|
|
213
|
+
projectAgentsDir: join(cwd, ".pi", "agent", "agents"),
|
|
1224
214
|
};
|
|
1225
215
|
}
|
|
1226
216
|
|
|
1227
217
|
function createPermissionManagerForCwd(
|
|
1228
218
|
cwd: string | undefined | null,
|
|
1229
219
|
): PermissionManager {
|
|
220
|
+
const agentDir = getAgentDir();
|
|
1230
221
|
const projectPaths = derivePiProjectPaths(cwd);
|
|
1231
|
-
if (!projectPaths) {
|
|
1232
|
-
return new PermissionManager();
|
|
1233
|
-
}
|
|
1234
222
|
|
|
1235
223
|
return new PermissionManager({
|
|
1236
|
-
|
|
1237
|
-
|
|
224
|
+
globalConfigPath: getGlobalConfigPath(agentDir),
|
|
225
|
+
projectGlobalConfigPath: projectPaths?.projectGlobalConfigPath,
|
|
226
|
+
projectAgentsDir: projectPaths?.projectAgentsDir,
|
|
1238
227
|
});
|
|
1239
228
|
}
|
|
1240
229
|
|
|
@@ -1269,26 +258,34 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1269
258
|
runtimeContext = ctx;
|
|
1270
259
|
}
|
|
1271
260
|
|
|
1272
|
-
const
|
|
1273
|
-
|
|
261
|
+
const cwd = runtimeContext?.cwd ?? null;
|
|
262
|
+
const agentDir = getAgentDir();
|
|
263
|
+
const mergeResult = loadAndMergeConfigs(
|
|
264
|
+
agentDir,
|
|
265
|
+
cwd ?? "",
|
|
266
|
+
EXTENSION_ROOT,
|
|
267
|
+
);
|
|
268
|
+
const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
|
|
269
|
+
setExtensionConfig(runtimeConfig);
|
|
1274
270
|
|
|
1275
271
|
if (runtimeContext?.hasUI) {
|
|
1276
|
-
syncPermissionSystemStatus(runtimeContext,
|
|
272
|
+
syncPermissionSystemStatus(runtimeContext, runtimeConfig);
|
|
1277
273
|
}
|
|
1278
274
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
275
|
+
const warning =
|
|
276
|
+
mergeResult.issues.length > 0 ? mergeResult.issues.join("\n") : undefined;
|
|
277
|
+
if (warning && warning !== lastConfigWarning) {
|
|
278
|
+
lastConfigWarning = warning;
|
|
279
|
+
notifyWarning(warning);
|
|
280
|
+
} else if (!warning) {
|
|
1283
281
|
lastConfigWarning = null;
|
|
1284
282
|
}
|
|
1285
283
|
|
|
1286
284
|
writeDebugLog("config.loaded", {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
yoloMode: result.config.yoloMode,
|
|
285
|
+
warning: warning ?? null,
|
|
286
|
+
debugLog: runtimeConfig.debugLog,
|
|
287
|
+
permissionReviewLog: runtimeConfig.permissionReviewLog,
|
|
288
|
+
yoloMode: runtimeConfig.yoloMode,
|
|
1292
289
|
});
|
|
1293
290
|
};
|
|
1294
291
|
|
|
@@ -1297,11 +294,35 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1297
294
|
ctx: ExtensionCommandContext,
|
|
1298
295
|
): void => {
|
|
1299
296
|
const normalized = normalizePermissionSystemConfig(next);
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
297
|
+
const globalPath = getGlobalConfigPath(getAgentDir());
|
|
298
|
+
|
|
299
|
+
// Load existing global config and merge runtime knobs into it
|
|
300
|
+
const existing = loadUnifiedConfig(globalPath);
|
|
301
|
+
const merged = {
|
|
302
|
+
...existing.config,
|
|
303
|
+
debugLog: normalized.debugLog,
|
|
304
|
+
permissionReviewLog: normalized.permissionReviewLog,
|
|
305
|
+
yoloMode: normalized.yoloMode,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const tmpPath = `${globalPath}.tmp`;
|
|
309
|
+
try {
|
|
310
|
+
mkdirSync(dirname(globalPath), { recursive: true });
|
|
311
|
+
writeFileSync(tmpPath, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
|
|
312
|
+
renameSync(tmpPath, globalPath);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
try {
|
|
315
|
+
if (existsSync(tmpPath)) {
|
|
316
|
+
unlinkSync(tmpPath);
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// Ignore cleanup failures.
|
|
1304
320
|
}
|
|
321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
322
|
+
ctx.ui.notify(
|
|
323
|
+
`Failed to save permission-system config at '${globalPath}': ${message}`,
|
|
324
|
+
"error",
|
|
325
|
+
);
|
|
1305
326
|
return;
|
|
1306
327
|
}
|
|
1307
328
|
|
|
@@ -1317,11 +338,22 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1317
338
|
};
|
|
1318
339
|
|
|
1319
340
|
setLoggingWarningReporter(notifyWarning);
|
|
341
|
+
setForwardedPermissionLogger({ writeReviewLog, writeDebugLog });
|
|
342
|
+
|
|
343
|
+
const forwardingDeps: PermissionForwardingDeps = {
|
|
344
|
+
forwardingDir: PERMISSION_FORWARDING_DIR,
|
|
345
|
+
subagentSessionsDir: SUBAGENT_SESSIONS_DIR,
|
|
346
|
+
writeReviewLog,
|
|
347
|
+
requestPermissionDecisionFromUi,
|
|
348
|
+
shouldAutoApprove: () =>
|
|
349
|
+
shouldAutoApprovePermissionState("ask", extensionConfig),
|
|
350
|
+
};
|
|
351
|
+
|
|
1320
352
|
refreshExtensionConfig();
|
|
1321
353
|
registerPermissionSystemCommand(pi, {
|
|
1322
354
|
getConfig: () => extensionConfig,
|
|
1323
355
|
setConfig: saveExtensionConfig,
|
|
1324
|
-
getConfigPath:
|
|
356
|
+
getConfigPath: () => getGlobalConfigPath(getAgentDir()),
|
|
1325
357
|
});
|
|
1326
358
|
|
|
1327
359
|
const createPermissionRequestId = (prefix: string): string => {
|
|
@@ -1386,7 +418,11 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1386
418
|
|
|
1387
419
|
reviewPermissionDecision("permission_request.waiting", details);
|
|
1388
420
|
|
|
1389
|
-
const decision = await confirmPermission(
|
|
421
|
+
const decision = await confirmPermission(
|
|
422
|
+
ctx,
|
|
423
|
+
details.message,
|
|
424
|
+
forwardingDeps,
|
|
425
|
+
);
|
|
1390
426
|
reviewPermissionDecision(
|
|
1391
427
|
decision.approved
|
|
1392
428
|
? "permission_request.approved"
|
|
@@ -1411,7 +447,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1411
447
|
};
|
|
1412
448
|
|
|
1413
449
|
const startForwardedPermissionPolling = (ctx: ExtensionContext): void => {
|
|
1414
|
-
if (!ctx.hasUI || isSubagentExecutionContext(ctx)) {
|
|
450
|
+
if (!ctx.hasUI || isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR)) {
|
|
1415
451
|
stopForwardedPermissionPolling();
|
|
1416
452
|
return;
|
|
1417
453
|
}
|
|
@@ -1429,6 +465,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1429
465
|
isProcessingForwardedRequests = true;
|
|
1430
466
|
void processForwardedPermissionRequests(
|
|
1431
467
|
permissionForwardingContext,
|
|
468
|
+
forwardingDeps,
|
|
1432
469
|
).finally(() => {
|
|
1433
470
|
isProcessingForwardedRequests = false;
|
|
1434
471
|
});
|
|
@@ -1470,11 +507,28 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1470
507
|
|
|
1471
508
|
const logResolvedConfigPaths = (): void => {
|
|
1472
509
|
const policyPaths = permissionManager.getResolvedPolicyPaths();
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
510
|
+
const cwd = runtimeContext?.cwd ?? null;
|
|
511
|
+
|
|
512
|
+
// Detect legacy files for the log entry
|
|
513
|
+
const agentDir = getAgentDir();
|
|
514
|
+
const legacyGlobalPolicyDetected = existsSync(
|
|
515
|
+
getLegacyGlobalPolicyPath(agentDir),
|
|
1477
516
|
);
|
|
517
|
+
const legacyProjectPolicyDetected = cwd
|
|
518
|
+
? existsSync(getLegacyProjectPolicyPath(cwd))
|
|
519
|
+
: false;
|
|
520
|
+
const legacyExtConfigPath = getLegacyExtensionConfigPath(EXTENSION_ROOT);
|
|
521
|
+
const newGlobalPath = getGlobalConfigPath(agentDir);
|
|
522
|
+
const legacyExtensionConfigDetected =
|
|
523
|
+
normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
|
|
524
|
+
existsSync(legacyExtConfigPath);
|
|
525
|
+
|
|
526
|
+
const entry = buildResolvedConfigLogEntry({
|
|
527
|
+
policyPaths,
|
|
528
|
+
legacyGlobalPolicyDetected,
|
|
529
|
+
legacyProjectPolicyDetected,
|
|
530
|
+
legacyExtensionConfigDetected,
|
|
531
|
+
});
|
|
1478
532
|
writeReviewLog(
|
|
1479
533
|
"config.resolved",
|
|
1480
534
|
entry as unknown as Record<string, unknown>,
|
|
@@ -1848,7 +902,11 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
1848
902
|
input,
|
|
1849
903
|
agentName ?? undefined,
|
|
1850
904
|
);
|
|
1851
|
-
const permissionLogContext = getPermissionLogContext(
|
|
905
|
+
const permissionLogContext = getPermissionLogContext(
|
|
906
|
+
check,
|
|
907
|
+
input,
|
|
908
|
+
PATH_BEARING_TOOLS,
|
|
909
|
+
);
|
|
1852
910
|
|
|
1853
911
|
if (check.state === "deny") {
|
|
1854
912
|
writeReviewLog("permission_request.blocked", {
|