@davisliu2006/pi-permission-system 0.1.0 → 0.2.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/README.md +8 -6
- package/package.json +4 -2
- package/src/ai-wildcard.ts +40 -3
- package/src/config.ts +16 -9
- package/src/constants.ts +2 -1
- package/src/extension.ts +16 -94
- package/src/permission-dialog.ts +58 -14
- package/src/policy-bash.ts +157 -0
- package/src/policy.ts +18 -72
- package/src/prompts.ts +44 -18
- package/src/request-builder.ts +2 -2
- package/src/session-approvals.ts +9 -0
- package/src/shell.ts +77 -16
- package/src/types-pi.ts +0 -9
- package/src/types.ts +8 -8
- package/src/ui-strings.ts +12 -8
- package/src/warning-strings.ts +54 -0
- package/src/wildcard-editor.ts +136 -0
- package/src/wildcard-validation.ts +68 -3
package/src/policy.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findBashSessionApproval,
|
|
3
|
+
resolveBashPolicy,
|
|
4
|
+
} from "./policy-bash.ts";
|
|
2
5
|
import type {
|
|
3
6
|
PermissionConfig,
|
|
4
7
|
PermissionRequest,
|
|
@@ -7,6 +10,8 @@ import type {
|
|
|
7
10
|
AllowAlwaysRule,
|
|
8
11
|
} from "./types.ts";
|
|
9
12
|
|
|
13
|
+
export { resolveAllowAlwaysMode } from "./policy-bash.ts";
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* Returns the permission for that tool if specified in the config.
|
|
12
17
|
*/
|
|
@@ -17,13 +22,6 @@ function configuredToolState(
|
|
|
17
22
|
return Object.hasOwn(config.tools, toolName) ? config.tools[toolName] : undefined;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
/**
|
|
21
|
-
* Returns the permission for that tool or the default if not specified.
|
|
22
|
-
*/
|
|
23
|
-
function fallbackState(config: PermissionConfig, toolName: string): PermissionState {
|
|
24
|
-
return configuredToolState(config, toolName) ?? config.default;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
25
|
/**
|
|
28
26
|
* Returns the policy for the request using the correct precedence rules.
|
|
29
27
|
*/
|
|
@@ -40,36 +38,12 @@ export function resolvePolicy(
|
|
|
40
38
|
state: externalState,
|
|
41
39
|
source: "external",
|
|
42
40
|
matchedPattern: request.toolName,
|
|
43
|
-
isChainedBash: false,
|
|
44
41
|
};
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
if (request.toolName === "bash") {
|
|
49
|
-
|
|
50
|
-
if (chained) { // chained bash - can't match wildcard
|
|
51
|
-
return {
|
|
52
|
-
state: fallbackState(config, "bash"),
|
|
53
|
-
source: configuredToolState(config, "bash") ? "tool" : "default",
|
|
54
|
-
isChainedBash: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const match = findLastWildcardMatch(config.bash, request.body);
|
|
59
|
-
if (match) { // matched with wildcard
|
|
60
|
-
return {
|
|
61
|
-
state: match.state,
|
|
62
|
-
source: "bash",
|
|
63
|
-
matchedPattern: match.pattern,
|
|
64
|
-
isChainedBash: false,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { // fallback to default
|
|
69
|
-
state: fallbackState(config, "bash"),
|
|
70
|
-
source: configuredToolState(config, "bash") ? "tool" : "default",
|
|
71
|
-
isChainedBash: false,
|
|
72
|
-
};
|
|
45
|
+
if (request.toolName === "bash") {
|
|
46
|
+
return resolveBashPolicy(config, request);
|
|
73
47
|
}
|
|
74
48
|
|
|
75
49
|
// other tool
|
|
@@ -77,7 +51,6 @@ export function resolvePolicy(
|
|
|
77
51
|
return {
|
|
78
52
|
state: toolState ?? config.default,
|
|
79
53
|
source: toolState ? "tool" : "default",
|
|
80
|
-
isChainedBash: false,
|
|
81
54
|
};
|
|
82
55
|
}
|
|
83
56
|
|
|
@@ -86,32 +59,19 @@ export function resolvePolicy(
|
|
|
86
59
|
*/
|
|
87
60
|
export function findSessionApproval(
|
|
88
61
|
rules: readonly AllowAlwaysRule[],
|
|
89
|
-
request: PermissionRequest
|
|
90
|
-
isChainedBash: boolean
|
|
62
|
+
request: PermissionRequest
|
|
91
63
|
): AllowAlwaysRule | undefined {
|
|
92
|
-
|
|
64
|
+
if (request.toolName === "bash") {
|
|
65
|
+
return findBashSessionApproval(rules, request);
|
|
66
|
+
}
|
|
93
67
|
|
|
94
|
-
for (let index = rules.length - 1; index >= 0; index
|
|
68
|
+
for (let index = rules.length - 1; index >= 0; index--) {
|
|
95
69
|
const rule = rules[index]!;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (isExternal) {continue;}
|
|
105
|
-
if (rule.tool !== request.toolName) {continue;}
|
|
106
|
-
|
|
107
|
-
if (rule.kind === "exact") {
|
|
108
|
-
if (rule.body === request.body) {return rule;}
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (rule.tool !== "bash" || request.bodyKind !== "command") {continue;}
|
|
113
|
-
if (isChainedBash) {continue;}
|
|
114
|
-
if (matchesWildcard(rule.body, request.body)) {return rule;}
|
|
70
|
+
if (
|
|
71
|
+
rule.kind === "exact"
|
|
72
|
+
&& rule.tool === request.toolName
|
|
73
|
+
&& rule.body === request.body
|
|
74
|
+
) {return rule;}
|
|
115
75
|
}
|
|
116
76
|
|
|
117
77
|
return undefined;
|
|
@@ -129,17 +89,3 @@ export function describePolicy(policy: PolicyResolution): string {
|
|
|
129
89
|
}
|
|
130
90
|
return `${policy.state} (default)`;
|
|
131
91
|
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* True when "Allow Always" will generate a bash wildcard instead of an exact rule.
|
|
135
|
-
* Mirrors the branching in chooseAlwaysRule.
|
|
136
|
-
*/
|
|
137
|
-
export function willGenerateBashWildcard(
|
|
138
|
-
request: PermissionRequest,
|
|
139
|
-
policy: PolicyResolution
|
|
140
|
-
): boolean {
|
|
141
|
-
if (request.isExternalDirectory && policy.source === "external") {return false;}
|
|
142
|
-
if (request.toolName !== "bash") {return false;}
|
|
143
|
-
if (policy.isChainedBash) {return false;}
|
|
144
|
-
return true;
|
|
145
|
-
}
|
package/src/prompts.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function describeBlockedSubject(
|
|
|
20
20
|
return `bash command ${JSON.stringify(request.body)}`;
|
|
21
21
|
}
|
|
22
22
|
if (request.bodyKind === "path") {
|
|
23
|
-
return
|
|
23
|
+
return `${request.toolName} access to ${JSON.stringify(request.body)}`;
|
|
24
24
|
}
|
|
25
25
|
return includeGenericSubject
|
|
26
26
|
? `tool ${JSON.stringify(request.toolName)} with input ${request.body}`
|
|
@@ -30,20 +30,13 @@ export function describeBlockedSubject(
|
|
|
30
30
|
/**
|
|
31
31
|
* Builds context for the ai-generated purpose.
|
|
32
32
|
*/
|
|
33
|
-
export function buildPurposePromptContext(
|
|
34
|
-
request: PermissionRequest,
|
|
35
|
-
policy: PolicyResolution
|
|
36
|
-
): string {
|
|
33
|
+
export function buildPurposePromptContext(request: PermissionRequest): string {
|
|
37
34
|
const lines = [
|
|
38
35
|
`Tool: ${request.toolName}`,
|
|
39
|
-
`Policy: ${describePolicy(policy)}`,
|
|
40
36
|
];
|
|
41
37
|
|
|
42
38
|
if (request.bodyKind === "command") {
|
|
43
39
|
lines.push(`Command:\n${preview(request.body, 600)}`);
|
|
44
|
-
if (policy.isChainedBash) {
|
|
45
|
-
lines.push(CHAINED_BASH_NOTE);
|
|
46
|
-
}
|
|
47
40
|
} else if (request.bodyKind === "path") {
|
|
48
41
|
lines.push(`Path: ${preview(request.body, 400)}`);
|
|
49
42
|
if (request.isExternalDirectory) {
|
|
@@ -63,6 +56,14 @@ export const PURPOSE_SYSTEM_PROMPT = [
|
|
|
63
56
|
|
|
64
57
|
export function buildPurposeInstructions(context: string): string {
|
|
65
58
|
return [
|
|
59
|
+
"Examples:",
|
|
60
|
+
"Tool: bash",
|
|
61
|
+
`Command: git commit -m "Add feature" && git log`,
|
|
62
|
+
"Commits the changes to the repository and lists the commit history.",
|
|
63
|
+
"Tool: bash",
|
|
64
|
+
`Command: grep -rnEI --color=never --exclude-dir={node_modules} --include=\\*.py -B 2 -A 3 "^[[:space:]]*[^/]*\\b(func_1|func_2)\\s*(" .`,
|
|
65
|
+
"Searches the project for instances of func_1 or func_2.",
|
|
66
|
+
"",
|
|
66
67
|
"Explain why the following request is being made in a single sentence.",
|
|
67
68
|
"Do not use markdown or quotes.",
|
|
68
69
|
"",
|
|
@@ -75,31 +76,56 @@ export const WILDCARD_SYSTEM_PROMPT = [
|
|
|
75
76
|
"Return only the pattern itself with no explanation or markdown.",
|
|
76
77
|
].join(" ");
|
|
77
78
|
|
|
79
|
+
export const WILDCARD_LIST_SYSTEM_PROMPT = [
|
|
80
|
+
"You generate shell command permission patterns for the user to approve.",
|
|
81
|
+
"Return only semicolon-separated patterns with no explanation or markdown.",
|
|
82
|
+
].join(" ");
|
|
83
|
+
|
|
84
|
+
const WILDCARD_RULES = [
|
|
85
|
+
"Rules for each pattern:",
|
|
86
|
+
"- Use wildcard syntax, not regular expressions.",
|
|
87
|
+
"- * matches any sequence of characters.",
|
|
88
|
+
"- ? matches exactly one character.",
|
|
89
|
+
"- \\* and \\? match literal characters.",
|
|
90
|
+
"- All other characters are literal.",
|
|
91
|
+
];
|
|
92
|
+
|
|
78
93
|
export function buildWildcardInstructions(command: string): string {
|
|
79
94
|
return [
|
|
80
95
|
"Create a permission wildcard for this command.",
|
|
81
96
|
"Select one that is general enough to be useful but narrow enough to not pose security risks.",
|
|
82
97
|
"",
|
|
83
|
-
|
|
84
|
-
"- Use wildcard syntax, not regular expressions.",
|
|
85
|
-
"- * matches any sequence of characters.",
|
|
86
|
-
"- ? matches exactly one character.",
|
|
87
|
-
"- \\* and \\? match literal characters.",
|
|
88
|
-
"- All other characters are literal.",
|
|
98
|
+
...WILDCARD_RULES,
|
|
89
99
|
"The pattern must match the given command.",
|
|
90
100
|
"",
|
|
91
101
|
`Command: ${JSON.stringify(command)}`,
|
|
92
102
|
].join("\n");
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Builds instructions for generating one wildcard per command in a simple list.
|
|
107
|
+
*/
|
|
108
|
+
export function buildWildcardListInstructions(commands: readonly string[]): string {
|
|
109
|
+
return [
|
|
110
|
+
"Create a permission wildcard for each command below.",
|
|
111
|
+
"Select patterns that are general enough to be useful but narrow enough to not pose security risks.",
|
|
112
|
+
"Return exactly one pattern per command, separated by semicolons, in the same order.",
|
|
113
|
+
"",
|
|
114
|
+
...WILDCARD_RULES,
|
|
115
|
+
"Each pattern must match its corresponding command.",
|
|
116
|
+
"",
|
|
117
|
+
`Commands: ${commands.join("; ")}`,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
/**
|
|
96
122
|
* Prompt given to the agent on deny.
|
|
97
123
|
*/
|
|
98
124
|
export function formatDenyReason(request: PermissionRequest, policy: PolicyResolution): string {
|
|
99
125
|
const subject = describeBlockedSubject(request, true);
|
|
100
|
-
const chainedNote =
|
|
126
|
+
const chainedNote = request.bashAnalysis?.isComplex ? ` ${CHAINED_BASH_NOTE}` : "";
|
|
101
127
|
|
|
102
|
-
return `Permission denied for ${subject}. Policy: ${describePolicy(policy)}.${chainedNote} Hard stop: do not retry or investigate a bypass
|
|
128
|
+
return `Permission denied for ${subject}. Policy: ${describePolicy(policy)}.${chainedNote} Hard stop: do not retry or investigate a bypass. Report the denial to the user.`;
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
/**
|
|
@@ -114,5 +140,5 @@ export function formatNoUiReason(request: PermissionRequest, policy: PolicyResol
|
|
|
114
140
|
*/
|
|
115
141
|
export function formatRejectReason(request: PermissionRequest): string {
|
|
116
142
|
const subject = describeBlockedSubject(request, false);
|
|
117
|
-
return `User rejected ${subject}.
|
|
143
|
+
return `User rejected ${subject}. Pause the task immediately (do not do further analysis or run more tool requests) and concisely ask the user what to do next.`;
|
|
118
144
|
}
|
package/src/request-builder.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isPathTool, resolveToolPath } from "./paths.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { analyzeShellCommand } from "./shell.ts";
|
|
3
3
|
import type { PiToolInput } from "./types-pi.ts";
|
|
4
4
|
import { toRecord, type PermissionRequest } from "./types.ts";
|
|
5
5
|
|
|
@@ -45,7 +45,7 @@ export async function buildRequest(
|
|
|
45
45
|
body: command,
|
|
46
46
|
bodyKind: "command",
|
|
47
47
|
cwd,
|
|
48
|
-
|
|
48
|
+
bashAnalysis: analyzeShellCommand(command),
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
package/src/session-approvals.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { AllowAlwaysRule } from "./types.ts";
|
|
2
2
|
|
|
3
|
+
function ruleEq(a: AllowAlwaysRule, b: AllowAlwaysRule): boolean {
|
|
4
|
+
return a.kind === b.kind && a.tool === b.tool && a.body === b.body;
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export class SessionApprovalStore {
|
|
8
|
+
/**
|
|
9
|
+
* Stored as an array rather than a set because JS
|
|
10
|
+
* objects don't compare by value.
|
|
11
|
+
*/
|
|
4
12
|
private readonly rules: AllowAlwaysRule[] = [];
|
|
5
13
|
|
|
6
14
|
add(rule: AllowAlwaysRule): void {
|
|
15
|
+
if (this.rules.some((existing) => ruleEq(existing, rule))) {return;}
|
|
7
16
|
this.rules.push(rule);
|
|
8
17
|
}
|
|
9
18
|
|
package/src/shell.ts
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Result of a conservative shell scan.
|
|
3
|
+
*
|
|
4
|
+
* When `isComplex` is false, `segments` are commands separated only by `;`,
|
|
5
|
+
* `&&`, `||`, or newlines (or a single command). When true, `segments` is empty.
|
|
6
|
+
*/
|
|
7
|
+
export type ShellCommandAnalysis = {
|
|
8
|
+
isComplex: boolean;
|
|
9
|
+
segments: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const COMPLEX: ShellCommandAnalysis = { isComplex: true, segments: [] };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Conservative shell command scanner.
|
|
16
|
+
* This is not a full parser and is biased toward `isComplex: true`.
|
|
17
|
+
*
|
|
18
|
+
* Splits on:
|
|
19
|
+
* - `;`, `&&`, `||`, and newlines outside quotes
|
|
20
|
+
*
|
|
21
|
+
* Treats as complex:
|
|
6
22
|
* - Substitutions like `command` and $(command)
|
|
7
|
-
* -
|
|
23
|
+
* - Pipes `|` and background `&`
|
|
8
24
|
* - Parentheses
|
|
9
25
|
*/
|
|
10
|
-
export function
|
|
26
|
+
export function analyzeShellCommand(command: string): ShellCommandAnalysis {
|
|
11
27
|
let quote: "'" | '"' | undefined;
|
|
12
28
|
let index = 0;
|
|
29
|
+
let current = "";
|
|
30
|
+
const segments: string[] = [];
|
|
31
|
+
|
|
32
|
+
const pushCurrent = (): void => {
|
|
33
|
+
const trimmed = current.trim();
|
|
34
|
+
if (trimmed) {segments.push(trimmed);}
|
|
35
|
+
current = "";
|
|
36
|
+
};
|
|
13
37
|
|
|
14
38
|
while (index < command.length) {
|
|
15
39
|
const character = command[index]!;
|
|
@@ -17,52 +41,89 @@ export function hasShellChaining(command: string): boolean {
|
|
|
17
41
|
// inside quote
|
|
18
42
|
if (quote === "'") {
|
|
19
43
|
if (character === "'") {quote = undefined;}
|
|
44
|
+
current += character;
|
|
20
45
|
index++;
|
|
21
46
|
continue;
|
|
22
47
|
}
|
|
23
48
|
if (quote === '"') {
|
|
24
49
|
if (character === "\\") {
|
|
50
|
+
current += character + (command[index + 1] ?? "");
|
|
25
51
|
index += 2;
|
|
26
52
|
continue;
|
|
27
53
|
}
|
|
28
54
|
if (character === '"') {
|
|
29
55
|
quote = undefined;
|
|
56
|
+
current += character;
|
|
30
57
|
index++;
|
|
31
58
|
continue;
|
|
32
59
|
}
|
|
33
|
-
if (character === "`") {return
|
|
34
|
-
if (character === "$" && command[index + 1] === "(") {return
|
|
60
|
+
if (character === "`") {return COMPLEX;}
|
|
61
|
+
if (character === "$" && command[index + 1] === "(") {return COMPLEX;}
|
|
62
|
+
current += character;
|
|
35
63
|
index++;
|
|
36
64
|
continue;
|
|
37
65
|
}
|
|
38
66
|
|
|
39
67
|
// escape character
|
|
40
68
|
if (character === "\\") {
|
|
69
|
+
current += character + (command[index + 1] ?? "");
|
|
41
70
|
index += 2;
|
|
42
71
|
continue;
|
|
43
72
|
}
|
|
44
|
-
|
|
45
|
-
//
|
|
73
|
+
|
|
74
|
+
// start quote
|
|
46
75
|
if (character === "'") {
|
|
47
76
|
quote = "'";
|
|
77
|
+
current += character;
|
|
48
78
|
index++;
|
|
49
79
|
continue;
|
|
50
80
|
}
|
|
51
81
|
if (character === '"') {
|
|
52
82
|
quote = '"';
|
|
83
|
+
current += character;
|
|
53
84
|
index++;
|
|
54
85
|
continue;
|
|
55
86
|
}
|
|
56
87
|
|
|
57
|
-
if (character === "`") {return
|
|
58
|
-
if (character === "$" && command[index + 1] === "(") {return
|
|
59
|
-
if (character === "
|
|
60
|
-
if (character === "\n" || character === "\r") {return true;}
|
|
61
|
-
if (character === "(" || character === ")") {return true;}
|
|
88
|
+
if (character === "`") {return COMPLEX;}
|
|
89
|
+
if (character === "$" && command[index + 1] === "(") {return COMPLEX;}
|
|
90
|
+
if (character === "(" || character === ")") {return COMPLEX;}
|
|
62
91
|
|
|
92
|
+
// simple separators
|
|
93
|
+
if (character === ";") {
|
|
94
|
+
pushCurrent();
|
|
95
|
+
index++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (character === "&") {
|
|
99
|
+
if (command[index+1] !== "&") {return COMPLEX;}
|
|
100
|
+
pushCurrent();
|
|
101
|
+
index += 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (character === "|") {
|
|
105
|
+
if (command[index+1] !== "|") {return COMPLEX;}
|
|
106
|
+
pushCurrent();
|
|
107
|
+
index += 2;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (character === "\n" || character === "\r") {
|
|
111
|
+
pushCurrent();
|
|
112
|
+
if (character === "\r" && command[index + 1] === "\n") {index += 2;}
|
|
113
|
+
else {index++;}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
current += character;
|
|
63
118
|
index++;
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
// Unterminated quotes are unsafe to reason about.
|
|
67
|
-
|
|
122
|
+
if (quote !== undefined) {return COMPLEX;}
|
|
123
|
+
|
|
124
|
+
pushCurrent();
|
|
125
|
+
return {
|
|
126
|
+
isComplex: false,
|
|
127
|
+
segments: segments.length > 0 ? segments : [""],
|
|
128
|
+
};
|
|
68
129
|
}
|
package/src/types-pi.ts
CHANGED
|
@@ -23,12 +23,3 @@ export type PiToolInput =
|
|
|
23
23
|
| PathToolInput
|
|
24
24
|
| BashToolInput
|
|
25
25
|
| Record<string, unknown>;
|
|
26
|
-
|
|
27
|
-
export function validatePathToolInput(input: PiToolInput): PathToolInput | undefined {
|
|
28
|
-
if (!("path" in input) || typeof(input.path) !== "string") {return undefined;}
|
|
29
|
-
return input as PathToolInput;
|
|
30
|
-
}
|
|
31
|
-
export function validateBashToolInput(input: PiToolInput): BashToolInput | undefined {
|
|
32
|
-
if (!("command" in input) || typeof(input.command) !== "string") {return undefined;}
|
|
33
|
-
return input as BashToolInput;
|
|
34
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ASK, PERMISSION_STATES } from "./constants.ts";
|
|
2
|
+
import type { ShellCommandAnalysis } from "./shell.ts";
|
|
2
3
|
import type { PiToolInput } from "./types-pi.ts";
|
|
3
4
|
|
|
4
5
|
// utility functions
|
|
@@ -31,11 +32,9 @@ export function createDefaultPermissionConfig(): PermissionConfig {
|
|
|
31
32
|
return { default: ASK, tools: {}, bash: {}, external_directory: {} };
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
export const DEFAULT_PERMISSION_CONFIG = createDefaultPermissionConfig();
|
|
35
|
-
|
|
36
35
|
// permission request
|
|
37
36
|
|
|
38
|
-
export type RequestBodyKind = "
|
|
37
|
+
export type RequestBodyKind = "path" | "command" | "generic";
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Request for a tool.
|
|
@@ -44,6 +43,8 @@ export type RequestBodyKind = "command" | "path" | "generic";
|
|
|
44
43
|
* @property body - The body for the permission request.
|
|
45
44
|
* @property bodyKind - The meaning of the body.
|
|
46
45
|
* @property cwd - The current working directory.
|
|
46
|
+
* @property isExternalDirectory - Whether the request uses path to an external directory.
|
|
47
|
+
* @property bashAnalysis - Shell scan for bash requests.
|
|
47
48
|
*/
|
|
48
49
|
export interface PermissionRequest {
|
|
49
50
|
toolName: string;
|
|
@@ -52,12 +53,12 @@ export interface PermissionRequest {
|
|
|
52
53
|
bodyKind: RequestBodyKind;
|
|
53
54
|
cwd: string;
|
|
54
55
|
isExternalDirectory?: boolean;
|
|
55
|
-
|
|
56
|
+
bashAnalysis?: ShellCommandAnalysis;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// resolved policy
|
|
59
60
|
|
|
60
|
-
export type PolicySource = "
|
|
61
|
+
export type PolicySource = "default" | "tool" | "bash" | "external";
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* A resolved policy.
|
|
@@ -66,18 +67,17 @@ export interface PolicyResolution {
|
|
|
66
67
|
state: PermissionState;
|
|
67
68
|
source: PolicySource;
|
|
68
69
|
matchedPattern?: string;
|
|
69
|
-
isChainedBash: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export type AllowAlwaysMode = "wildcard" | "wildcard-list" | "exact";
|
|
73
|
+
|
|
72
74
|
// other types
|
|
73
75
|
|
|
74
76
|
export type AllowAlwaysRule = {
|
|
75
|
-
scope: "tool" | "external_directory";
|
|
76
77
|
kind: "exact";
|
|
77
78
|
tool: string;
|
|
78
79
|
body: string;
|
|
79
80
|
} | {
|
|
80
|
-
scope: "tool";
|
|
81
81
|
kind: "wildcard";
|
|
82
82
|
tool: "bash";
|
|
83
83
|
body: string;
|
package/src/ui-strings.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
ALLOW_ALWAYS_EXACT,
|
|
3
3
|
ALLOW_ALWAYS_WILDCARD,
|
|
4
4
|
} from "./constants.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveAllowAlwaysMode } from "./policy-bash.ts";
|
|
6
6
|
import type { PermissionRequest, PolicyResolution } from "./types.ts";
|
|
7
7
|
|
|
8
8
|
export const STATUS_SUMMARIZING_REQUEST = "Summarizing permission request...";
|
|
@@ -13,8 +13,8 @@ export const PERMISSION_DIALOG_QUESTION = "What should the agent be allowed to d
|
|
|
13
13
|
export const PURPOSE_PREFIX = "Purpose: ";
|
|
14
14
|
|
|
15
15
|
export const WILDCARD_EDITOR_TITLE = "Review bash wildcard";
|
|
16
|
+
export const WILDCARD_LIST_EDITOR_TITLE = "Review bash wildcards (semicolon-separated)";
|
|
16
17
|
export const BROAD_WILDCARD_TITLE = "Very broad wildcard";
|
|
17
|
-
export const INVALID_WILDCARD_PREFIX = "Invalid wildcard: ";
|
|
18
18
|
|
|
19
19
|
const CHAINED_BASH_NOTE = "Shell chaining detected.";
|
|
20
20
|
const EXTERNAL_DIRECTORY_NOTE = "Outside the current working directory.";
|
|
@@ -33,8 +33,8 @@ export function buildSelectorSummary(request: PermissionRequest): string {
|
|
|
33
33
|
];
|
|
34
34
|
|
|
35
35
|
if (request.bodyKind === "command") {
|
|
36
|
-
lines.push(`Command:
|
|
37
|
-
if (request.
|
|
36
|
+
lines.push(`Command: ${preview(request.body, 600)}`);
|
|
37
|
+
if (request.bashAnalysis?.isComplex) {
|
|
38
38
|
lines.push(CHAINED_BASH_NOTE);
|
|
39
39
|
}
|
|
40
40
|
} else if (request.bodyKind === "path") {
|
|
@@ -50,17 +50,21 @@ export function buildSelectorSummary(request: PermissionRequest): string {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Label for the "
|
|
53
|
+
* Label for the "allow always" select option.
|
|
54
54
|
*/
|
|
55
55
|
export function formatAllowAlwaysOption(
|
|
56
56
|
request: PermissionRequest,
|
|
57
57
|
policy: PolicyResolution
|
|
58
58
|
): string {
|
|
59
|
-
return
|
|
60
|
-
?
|
|
61
|
-
:
|
|
59
|
+
return resolveAllowAlwaysMode(request, policy) === "exact"
|
|
60
|
+
? ALLOW_ALWAYS_EXACT
|
|
61
|
+
: ALLOW_ALWAYS_WILDCARD;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function formatBroadWildcardConfirm(pattern: string): string {
|
|
65
65
|
return `This pattern can match many commands:\n\n${pattern}\n\nSave it for this session?`;
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
export function formatBroadWildcardListConfirm(patterns: readonly string[]): string {
|
|
69
|
+
return `These patterns can match many commands:\n\n${patterns.join("\n")}\n\nSave them for this session?`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { EXTENSION_NAME, PERMISSION_STATES_DESCRIPTION } from "./constants.ts";
|
|
2
|
+
|
|
3
|
+
export const CONFIG_WARNING_PREFIX = `${EXTENSION_NAME}: `;
|
|
4
|
+
export const INVALID_WILDCARD_PREFIX = "Invalid wildcard: ";
|
|
5
|
+
|
|
6
|
+
export const CONFIG_MUST_BE_OBJECT =
|
|
7
|
+
"Permission config must be a JSON object; using defaults.";
|
|
8
|
+
export const IGNORED_DEFAULT_WARNING =
|
|
9
|
+
`Ignoring "default": expected ${PERMISSION_STATES_DESCRIPTION}.`;
|
|
10
|
+
|
|
11
|
+
export const WILDCARD_LENGTH_REASON =
|
|
12
|
+
"Pattern must be between 1 and 500 characters.";
|
|
13
|
+
export const WILDCARD_NO_MATCH_REASON =
|
|
14
|
+
"Pattern does not match the current command.";
|
|
15
|
+
|
|
16
|
+
export function formatIgnoredSectionWarning(sectionName: string): string {
|
|
17
|
+
return `Ignoring "${sectionName}": expected an object mapping names to ${PERMISSION_STATES_DESCRIPTION}.`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatIgnoredSectionEntryWarning(
|
|
21
|
+
sectionName: string,
|
|
22
|
+
key: string
|
|
23
|
+
): string {
|
|
24
|
+
return `Ignoring "${sectionName}.${key}": expected ${PERMISSION_STATES_DESCRIPTION}.`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatConfigReadFailure(error: unknown): string {
|
|
28
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
29
|
+
return `Failed to read permission config: ${detail}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatConfigParseFailure(error: unknown): string {
|
|
33
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
34
|
+
return `Failed to parse permission config: ${detail}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatSuppressedConfigWarnings(suppressed: number): string {
|
|
38
|
+
return `${suppressed} additional config warning(s) suppressed.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function formatWildcardListCountReason(
|
|
42
|
+
expected: number,
|
|
43
|
+
actual: number
|
|
44
|
+
): string {
|
|
45
|
+
return `Expected ${expected} semicolon-separated pattern(s), got ${actual}.`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatWildcardListItemReason(
|
|
49
|
+
index: number,
|
|
50
|
+
pattern: string,
|
|
51
|
+
reason: string
|
|
52
|
+
): string {
|
|
53
|
+
return `Pattern ${index} (${JSON.stringify(pattern)}): ${reason}`;
|
|
54
|
+
}
|