@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
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
parseSimpleYamlMap,
|
|
11
11
|
toRecord,
|
|
12
12
|
} from "./common.js";
|
|
13
|
+
import { loadUnifiedConfig, stripJsonComments } from "./config-loader.js";
|
|
14
|
+
import { getGlobalConfigPath } from "./config-paths.js";
|
|
13
15
|
import type {
|
|
14
16
|
AgentPermissions,
|
|
15
17
|
BashPermissions,
|
|
@@ -26,7 +28,7 @@ import {
|
|
|
26
28
|
} from "./wildcard-matcher.js";
|
|
27
29
|
|
|
28
30
|
function defaultGlobalConfigPath(): string {
|
|
29
|
-
return
|
|
31
|
+
return getGlobalConfigPath(getAgentDir());
|
|
30
32
|
}
|
|
31
33
|
function defaultAgentsDir(): string {
|
|
32
34
|
return join(getAgentDir(), "agents");
|
|
@@ -61,87 +63,6 @@ const DEFAULT_POLICY: PermissionDefaultPolicy = {
|
|
|
61
63
|
special: "ask",
|
|
62
64
|
};
|
|
63
65
|
|
|
64
|
-
const EMPTY_GLOBAL_CONFIG: GlobalPermissionConfig = {
|
|
65
|
-
defaultPolicy: DEFAULT_POLICY,
|
|
66
|
-
tools: {},
|
|
67
|
-
bash: {},
|
|
68
|
-
mcp: {},
|
|
69
|
-
skills: {},
|
|
70
|
-
special: {},
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
function stripJsonComments(input: string): string {
|
|
74
|
-
let output = "";
|
|
75
|
-
let inString = false;
|
|
76
|
-
let stringQuote: '"' | "'" | "" = "";
|
|
77
|
-
let escaping = false;
|
|
78
|
-
let inLineComment = false;
|
|
79
|
-
let inBlockComment = false;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < input.length; i++) {
|
|
82
|
-
const char = input[i];
|
|
83
|
-
const next = input[i + 1] || "";
|
|
84
|
-
|
|
85
|
-
if (inLineComment) {
|
|
86
|
-
if (char === "\n") {
|
|
87
|
-
inLineComment = false;
|
|
88
|
-
output += char;
|
|
89
|
-
}
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (inBlockComment) {
|
|
94
|
-
if (char === "*" && next === "/") {
|
|
95
|
-
inBlockComment = false;
|
|
96
|
-
i++;
|
|
97
|
-
}
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!inString && char === "/" && next === "/") {
|
|
102
|
-
inLineComment = true;
|
|
103
|
-
i++;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!inString && char === "/" && next === "*") {
|
|
108
|
-
inBlockComment = true;
|
|
109
|
-
i++;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
output += char;
|
|
114
|
-
|
|
115
|
-
if (!inString && (char === '"' || char === "'")) {
|
|
116
|
-
inString = true;
|
|
117
|
-
stringQuote = char;
|
|
118
|
-
escaping = false;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!inString) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (escaping) {
|
|
127
|
-
escaping = false;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (char === "\\") {
|
|
132
|
-
escaping = true;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (char === stringQuote) {
|
|
137
|
-
inString = false;
|
|
138
|
-
stringQuote = "";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return output;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
66
|
function normalizePolicy(value: unknown): PermissionDefaultPolicy {
|
|
146
67
|
const record = toRecord(value);
|
|
147
68
|
return {
|
|
@@ -592,25 +513,17 @@ export class PermissionManager {
|
|
|
592
513
|
return this.globalConfigCache.value;
|
|
593
514
|
}
|
|
594
515
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
bash: normalized.bash || {},
|
|
607
|
-
mcp: normalized.mcp || {},
|
|
608
|
-
skills: normalized.skills || {},
|
|
609
|
-
special: normalized.special || {},
|
|
610
|
-
};
|
|
611
|
-
} catch {
|
|
612
|
-
value = EMPTY_GLOBAL_CONFIG;
|
|
613
|
-
}
|
|
516
|
+
const { config, issues } = loadUnifiedConfig(this.globalConfigPath);
|
|
517
|
+
this.accumulateConfigIssues(issues);
|
|
518
|
+
|
|
519
|
+
const value: GlobalPermissionConfig = {
|
|
520
|
+
defaultPolicy: normalizePolicy(config.defaultPolicy),
|
|
521
|
+
tools: config.tools || {},
|
|
522
|
+
bash: config.bash || {},
|
|
523
|
+
mcp: config.mcp || {},
|
|
524
|
+
skills: config.skills || {},
|
|
525
|
+
special: config.special || {},
|
|
526
|
+
};
|
|
614
527
|
|
|
615
528
|
this.globalConfigCache = { stamp, value };
|
|
616
529
|
return value;
|
|
@@ -626,16 +539,17 @@ export class PermissionManager {
|
|
|
626
539
|
return this.projectGlobalConfigCache.value;
|
|
627
540
|
}
|
|
628
541
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
542
|
+
const { config, issues } = loadUnifiedConfig(this.projectGlobalConfigPath);
|
|
543
|
+
this.accumulateConfigIssues(issues);
|
|
544
|
+
|
|
545
|
+
const value: AgentPermissions = {
|
|
546
|
+
defaultPolicy: config.defaultPolicy,
|
|
547
|
+
tools: config.tools,
|
|
548
|
+
bash: config.bash,
|
|
549
|
+
mcp: config.mcp,
|
|
550
|
+
skills: config.skills,
|
|
551
|
+
special: config.special,
|
|
552
|
+
};
|
|
639
553
|
|
|
640
554
|
this.projectGlobalConfigCache = { stamp, value };
|
|
641
555
|
return value;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { SkillPromptEntry } from "./skill-prompt-sanitizer.js";
|
|
2
|
+
import { formatToolInputForPrompt } from "./tool-input-preview.js";
|
|
3
|
+
import type { PermissionCheckResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export function formatMissingToolNameReason(): string {
|
|
6
|
+
return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatUnknownToolReason(
|
|
10
|
+
toolName: string,
|
|
11
|
+
availableToolNames: readonly string[],
|
|
12
|
+
): string {
|
|
13
|
+
const preview = availableToolNames.slice(0, 10);
|
|
14
|
+
const suffix = availableToolNames.length > preview.length ? ", ..." : "";
|
|
15
|
+
const availableList =
|
|
16
|
+
preview.length > 0 ? `${preview.join(", ")}${suffix}` : "none";
|
|
17
|
+
|
|
18
|
+
const mcpHint =
|
|
19
|
+
toolName === "mcp"
|
|
20
|
+
? ""
|
|
21
|
+
: ' If this was intended as an MCP server tool, call the registered \'mcp\' tool when available (for example: {"tool":"server:tool"}).';
|
|
22
|
+
|
|
23
|
+
return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatPermissionHardStopHint(
|
|
27
|
+
result: PermissionCheckResult,
|
|
28
|
+
): string {
|
|
29
|
+
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
|
30
|
+
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.";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return "Hard stop: this permission denial is policy-enforced. Do not retry or investigate bypasses; report the block to the user.";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatDenyReason(
|
|
37
|
+
result: PermissionCheckResult,
|
|
38
|
+
agentName?: string,
|
|
39
|
+
): string {
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (agentName) {
|
|
43
|
+
parts.push(`Agent '${agentName}'`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
|
47
|
+
parts.push(`is not permitted to run MCP target '${result.target}'`);
|
|
48
|
+
} else {
|
|
49
|
+
parts.push(`is not permitted to run '${result.toolName}'`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (result.command) {
|
|
53
|
+
parts.push(`command '${result.command}'`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (result.matchedPattern) {
|
|
57
|
+
parts.push(`(matched '${result.matchedPattern}')`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return `${parts.join(" ")}. ${formatPermissionHardStopHint(result)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatUserDeniedReason(
|
|
64
|
+
result: PermissionCheckResult,
|
|
65
|
+
denialReason?: string,
|
|
66
|
+
): string {
|
|
67
|
+
const base =
|
|
68
|
+
(result.source === "mcp" || result.toolName === "mcp") && result.target
|
|
69
|
+
? `User denied MCP target '${result.target}'.`
|
|
70
|
+
: result.toolName === "bash" && result.command
|
|
71
|
+
? `User denied bash command '${result.command}'.`
|
|
72
|
+
: `User denied tool '${result.toolName}'.`;
|
|
73
|
+
const reasonSuffix = denialReason ? ` Reason: ${denialReason}.` : "";
|
|
74
|
+
|
|
75
|
+
return `${base}${reasonSuffix} ${formatPermissionHardStopHint(result)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function formatAskPrompt(
|
|
79
|
+
result: PermissionCheckResult,
|
|
80
|
+
agentName?: string,
|
|
81
|
+
input?: unknown,
|
|
82
|
+
): string {
|
|
83
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
84
|
+
|
|
85
|
+
if (result.toolName === "bash") {
|
|
86
|
+
const patternInfo = result.matchedPattern
|
|
87
|
+
? ` (matched '${result.matchedPattern}')`
|
|
88
|
+
: "";
|
|
89
|
+
return `${subject} requested bash command '${result.command || ""}'${patternInfo}. Allow this command?`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ((result.source === "mcp" || result.toolName === "mcp") && result.target) {
|
|
93
|
+
const patternInfo = result.matchedPattern
|
|
94
|
+
? ` (matched '${result.matchedPattern}')`
|
|
95
|
+
: "";
|
|
96
|
+
return `${subject} requested MCP target '${result.target}'${patternInfo}. Allow this call?`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const patternInfo = result.matchedPattern
|
|
100
|
+
? ` (matched '${result.matchedPattern}')`
|
|
101
|
+
: "";
|
|
102
|
+
const inputPreview = formatToolInputForPrompt(result.toolName, input);
|
|
103
|
+
const inputSuffix = inputPreview ? ` ${inputPreview}` : "";
|
|
104
|
+
return `${subject} requested tool '${result.toolName}'${patternInfo}${inputSuffix}. Allow this call?`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function formatSkillAskPrompt(
|
|
108
|
+
skillName: string,
|
|
109
|
+
agentName?: string,
|
|
110
|
+
): string {
|
|
111
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
112
|
+
return `${subject} requested skill '${skillName}'. Allow loading this skill?`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatSkillPathAskPrompt(
|
|
116
|
+
skill: SkillPromptEntry,
|
|
117
|
+
readPath: string,
|
|
118
|
+
agentName?: string,
|
|
119
|
+
): string {
|
|
120
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
121
|
+
return `${subject} requested access to skill '${skill.name}' via '${readPath}'. Allow this read?`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatSkillPathDenyReason(
|
|
125
|
+
skill: SkillPromptEntry,
|
|
126
|
+
readPath: string,
|
|
127
|
+
agentName?: string,
|
|
128
|
+
): string {
|
|
129
|
+
const subject = agentName ? `Agent '${agentName}'` : "Current agent";
|
|
130
|
+
return `${subject} is not permitted to access skill '${skill.name}' via '${readPath}'.`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { normalize } from "node:path";
|
|
2
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
import { SUBAGENT_ENV_HINT_KEYS } from "./permission-forwarding.js";
|
|
5
|
+
|
|
6
|
+
export function normalizeFilesystemPath(pathValue: string): string {
|
|
7
|
+
const normalizedPath = normalize(pathValue);
|
|
8
|
+
return process.platform === "win32"
|
|
9
|
+
? normalizedPath.toLowerCase()
|
|
10
|
+
: normalizedPath;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPathWithinDirectoryForSubagent(
|
|
14
|
+
pathValue: string,
|
|
15
|
+
directory: string,
|
|
16
|
+
): boolean {
|
|
17
|
+
if (!pathValue || !directory) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (pathValue === directory) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sep = process.platform === "win32" ? "\\" : "/";
|
|
26
|
+
const prefix = directory.endsWith(sep) ? directory : `${directory}${sep}`;
|
|
27
|
+
return pathValue.startsWith(prefix);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isSubagentExecutionContext(
|
|
31
|
+
ctx: ExtensionContext,
|
|
32
|
+
subagentSessionsDir: string,
|
|
33
|
+
): boolean {
|
|
34
|
+
for (const key of SUBAGENT_ENV_HINT_KEYS) {
|
|
35
|
+
const value = process.env[key];
|
|
36
|
+
if (typeof value === "string" && value.trim()) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
42
|
+
if (!sessionDir) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalizedSessionDir = normalizeFilesystemPath(sessionDir);
|
|
47
|
+
const normalizedSubagentRoot = normalizeFilesystemPath(subagentSessionsDir);
|
|
48
|
+
return isPathWithinDirectoryForSubagent(
|
|
49
|
+
normalizedSessionDir,
|
|
50
|
+
normalizedSubagentRoot,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { getNonEmptyString, toRecord } from "./common.js";
|
|
2
|
+
import { safeJsonStringify } from "./logging.js";
|
|
3
|
+
import type { PermissionCheckResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
|
|
6
|
+
export const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
|
|
7
|
+
export const TOOL_TEXT_SUMMARY_MAX_LENGTH = 80;
|
|
8
|
+
|
|
9
|
+
export function truncateInlineText(value: string, maxLength: number): string {
|
|
10
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}…` : value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sanitizeInlineText(
|
|
14
|
+
value: string,
|
|
15
|
+
maxLength = TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
16
|
+
): string {
|
|
17
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
18
|
+
return normalized ? truncateInlineText(normalized, maxLength) : "empty text";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function countTextLines(value: string): number {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return value.split(/\r\n|\r|\n/).length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatCount(
|
|
30
|
+
value: number,
|
|
31
|
+
singular: string,
|
|
32
|
+
plural: string,
|
|
33
|
+
): string {
|
|
34
|
+
return `${value} ${value === 1 ? singular : plural}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getPromptPath(input: Record<string, unknown>): string | null {
|
|
38
|
+
return getNonEmptyString(input.path) ?? getNonEmptyString(input.file_path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatEditInputForPrompt(
|
|
42
|
+
input: Record<string, unknown>,
|
|
43
|
+
): string {
|
|
44
|
+
const path = getPromptPath(input);
|
|
45
|
+
const rawEdits = Array.isArray(input.edits)
|
|
46
|
+
? input.edits
|
|
47
|
+
: typeof input.oldText === "string" && typeof input.newText === "string"
|
|
48
|
+
? [{ oldText: input.oldText, newText: input.newText }]
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
const edits = rawEdits
|
|
52
|
+
.map((edit) => toRecord(edit))
|
|
53
|
+
.filter(
|
|
54
|
+
(edit) =>
|
|
55
|
+
typeof edit.oldText === "string" && typeof edit.newText === "string",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const pathPart = path ? `for '${path}'` : "";
|
|
59
|
+
if (edits.length === 0) {
|
|
60
|
+
return pathPart ? `${pathPart} with edit input` : "with edit input";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const firstEdit = edits[0];
|
|
64
|
+
const oldText = String(firstEdit.oldText);
|
|
65
|
+
const newText = String(firstEdit.newText);
|
|
66
|
+
const firstEditSummary = `edit #1 replaces ${formatCount(countTextLines(oldText), "line", "lines")} with ${formatCount(countTextLines(newText), "line", "lines")}`;
|
|
67
|
+
const extraEdits =
|
|
68
|
+
edits.length > 1
|
|
69
|
+
? `, plus ${formatCount(edits.length - 1, "additional edit", "additional edits")}`
|
|
70
|
+
: "";
|
|
71
|
+
const summary = `(${formatCount(edits.length, "replacement", "replacements")}: ${firstEditSummary}${extraEdits})`;
|
|
72
|
+
return pathPart ? `${pathPart} ${summary}` : summary;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatWriteInputForPrompt(
|
|
76
|
+
input: Record<string, unknown>,
|
|
77
|
+
): string {
|
|
78
|
+
const path = getPromptPath(input);
|
|
79
|
+
const content = typeof input.content === "string" ? input.content : "";
|
|
80
|
+
const summary = `(${formatCount(countTextLines(content), "line", "lines")}, ${formatCount(content.length, "character", "characters")})`;
|
|
81
|
+
return path ? `for '${path}' ${summary}` : summary;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatReadInputForPrompt(
|
|
85
|
+
input: Record<string, unknown>,
|
|
86
|
+
): string {
|
|
87
|
+
const path = getPromptPath(input);
|
|
88
|
+
const parts = path ? [`path '${path}'`] : [];
|
|
89
|
+
if (typeof input.offset === "number") {
|
|
90
|
+
parts.push(`offset ${input.offset}`);
|
|
91
|
+
}
|
|
92
|
+
if (typeof input.limit === "number") {
|
|
93
|
+
parts.push(`limit ${input.limit}`);
|
|
94
|
+
}
|
|
95
|
+
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function formatSearchInputForPrompt(
|
|
99
|
+
toolName: string,
|
|
100
|
+
input: Record<string, unknown>,
|
|
101
|
+
): string {
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
const path = getPromptPath(input);
|
|
104
|
+
const pattern = getNonEmptyString(input.pattern);
|
|
105
|
+
const glob = getNonEmptyString(input.glob);
|
|
106
|
+
|
|
107
|
+
if (pattern) {
|
|
108
|
+
parts.push(`pattern '${sanitizeInlineText(pattern)}'`);
|
|
109
|
+
}
|
|
110
|
+
if (glob) {
|
|
111
|
+
parts.push(`glob '${sanitizeInlineText(glob)}'`);
|
|
112
|
+
}
|
|
113
|
+
if (path) {
|
|
114
|
+
parts.push(`path '${path}'`);
|
|
115
|
+
} else if (toolName === "find" || toolName === "grep" || toolName === "ls") {
|
|
116
|
+
parts.push("current working directory");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return parts.length > 0 ? `for ${parts.join(", ")}` : "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function serializeToolInputPreview(input: unknown): string {
|
|
123
|
+
const serialized = safeJsonStringify(input);
|
|
124
|
+
if (!serialized || serialized === "{}" || serialized === "null") {
|
|
125
|
+
return "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return serialized.replace(/\s+/g, " ").trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function formatJsonInputForPrompt(input: unknown): string {
|
|
132
|
+
const inline = serializeToolInputPreview(input);
|
|
133
|
+
return inline
|
|
134
|
+
? `with input ${truncateInlineText(inline, TOOL_INPUT_PREVIEW_MAX_LENGTH)}`
|
|
135
|
+
: "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatToolInputForPrompt(
|
|
139
|
+
toolName: string,
|
|
140
|
+
input: unknown,
|
|
141
|
+
): string {
|
|
142
|
+
const inputRecord = toRecord(input);
|
|
143
|
+
|
|
144
|
+
switch (toolName) {
|
|
145
|
+
case "edit":
|
|
146
|
+
return formatEditInputForPrompt(inputRecord);
|
|
147
|
+
case "write":
|
|
148
|
+
return formatWriteInputForPrompt(inputRecord);
|
|
149
|
+
case "read":
|
|
150
|
+
return formatReadInputForPrompt(inputRecord);
|
|
151
|
+
case "find":
|
|
152
|
+
case "grep":
|
|
153
|
+
case "ls":
|
|
154
|
+
return formatSearchInputForPrompt(toolName, inputRecord);
|
|
155
|
+
default:
|
|
156
|
+
return formatJsonInputForPrompt(input);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function formatGenericToolInputForLog(
|
|
161
|
+
input: unknown,
|
|
162
|
+
): string | undefined {
|
|
163
|
+
const inline = serializeToolInputPreview(input);
|
|
164
|
+
return inline
|
|
165
|
+
? `input ${truncateInlineText(inline, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)}`
|
|
166
|
+
: undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getToolInputPreviewForLog(
|
|
170
|
+
result: PermissionCheckResult,
|
|
171
|
+
input: unknown,
|
|
172
|
+
pathBearingTools: ReadonlySet<string>,
|
|
173
|
+
): string | undefined {
|
|
174
|
+
if (
|
|
175
|
+
result.toolName === "bash" ||
|
|
176
|
+
result.toolName === "mcp" ||
|
|
177
|
+
result.source === "mcp"
|
|
178
|
+
) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (pathBearingTools.has(result.toolName)) {
|
|
183
|
+
const inputPreview = formatToolInputForPrompt(result.toolName, input);
|
|
184
|
+
return inputPreview
|
|
185
|
+
? truncateInlineText(inputPreview, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)
|
|
186
|
+
: undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return formatGenericToolInputForLog(input);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getPermissionLogContext(
|
|
193
|
+
result: PermissionCheckResult,
|
|
194
|
+
input: unknown,
|
|
195
|
+
pathBearingTools: ReadonlySet<string>,
|
|
196
|
+
): { command?: string; target?: string; toolInputPreview?: string } {
|
|
197
|
+
return {
|
|
198
|
+
command: result.command,
|
|
199
|
+
target: result.target,
|
|
200
|
+
toolInputPreview: getToolInputPreviewForLog(
|
|
201
|
+
result,
|
|
202
|
+
input,
|
|
203
|
+
pathBearingTools,
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
}
|