@davisliu2006/pi-permission-system 0.1.0 → 0.3.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 +23 -8
- package/package.json +8 -4
- package/src/ai-gen.ts +28 -5
- package/src/ai-purpose.ts +3 -1
- package/src/ai-wildcard.ts +45 -4
- package/src/config.ts +59 -10
- package/src/constants.ts +2 -1
- package/src/extension.ts +24 -100
- package/src/permission-dialog.ts +48 -15
- package/src/policy-bash.ts +180 -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 +87 -16
- package/src/types-pi.ts +0 -9
- package/src/types.ts +30 -8
- package/src/ui-strings.ts +66 -21
- package/src/warning-strings.ts +58 -0
- package/src/wildcard-editor.ts +139 -0
- package/src/wildcard-validation.ts +68 -3
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Reject
|
|
|
29
29
|
|
|
30
30
|
## Configuration
|
|
31
31
|
|
|
32
|
-
The supported top-level keys are: `default`, `tools`, `bash`, and `
|
|
32
|
+
The supported top-level keys are: `default`, `tools`, `bash`, `external_directory`, and `settings`.
|
|
33
33
|
|
|
34
34
|
`bash` body matching supports wildcard:
|
|
35
35
|
- `*` matches any sequence and `?` matches one character
|
|
@@ -39,6 +39,8 @@ The supported top-level keys are: `default`, `tools`, `bash`, and `external_dire
|
|
|
39
39
|
|
|
40
40
|
The builtin path tools in pi are: `read`, `write`, `edit`, `find`, `grep`, `ls`. `external_directory` overrides the settings for these path tools. The primary use is to override `write` and `edit` to a stronger policy for external directories.
|
|
41
41
|
|
|
42
|
+
`settings` holds optional non-permission settings.
|
|
43
|
+
|
|
42
44
|
### Rule Precedence
|
|
43
45
|
|
|
44
46
|
More specific rules override less specific rules:
|
|
@@ -49,7 +51,7 @@ More specific rules override less specific rules:
|
|
|
49
51
|
|
|
50
52
|
### Global Configuration
|
|
51
53
|
|
|
52
|
-
Located in
|
|
54
|
+
Located in `~/.pi/agent/pi-permissions.jsonc`.
|
|
53
55
|
|
|
54
56
|
Example:
|
|
55
57
|
|
|
@@ -90,19 +92,30 @@ Example:
|
|
|
90
92
|
"rg*": "allow"
|
|
91
93
|
},
|
|
92
94
|
"external_directory": {
|
|
93
|
-
"write": "ask"
|
|
95
|
+
"write": "ask",
|
|
96
|
+
"edit": "ask"
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
```
|
|
97
100
|
|
|
98
101
|
### Local Configuration
|
|
99
102
|
|
|
100
|
-
Located in
|
|
103
|
+
Located in the local Pi config folder:
|
|
101
104
|
|
|
102
|
-
|
|
105
|
+
`<project-root>/.pi/pi-permissions.jsonc`
|
|
103
106
|
|
|
104
107
|
Local configuration overrides global configuration, including `deny` rules.
|
|
105
108
|
|
|
109
|
+
### Optional Settings
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
{
|
|
113
|
+
"settings": {
|
|
114
|
+
"ai-model": "provider/model-id"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
106
119
|
## "Allow Always" Matching
|
|
107
120
|
|
|
108
121
|
Matching is done as `(tool, body)`.
|
|
@@ -120,8 +133,10 @@ Body matching rules:
|
|
|
120
133
|
|
|
121
134
|
## Bash Matching Safeguards
|
|
122
135
|
|
|
123
|
-
Bash matching safeguards against
|
|
136
|
+
Bash matching safeguards against unsafe shell syntax.
|
|
137
|
+
|
|
138
|
+
Sequences separated by `;`, `&&`, `||`, or newlines are split and each segment is matched against `bash` rules. The strictest result wins (`deny` > `ask` > `allow`).
|
|
124
139
|
|
|
125
|
-
If a
|
|
140
|
+
If a command contains other chaining (`|`, `&`, command/process substitution, or parentheses), `bash` pattern rules (except for `deny`) are skipped and the `tools.bash` policy is used instead, falling back to `default`.
|
|
126
141
|
|
|
127
|
-
"Allow always" for
|
|
142
|
+
"Allow always" for complex sequences saves the exact command, not a wildcard.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davisliu2006/pi-permission-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Permission enforcement extension for the Pi coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"index.ts",
|
|
12
12
|
"src",
|
|
13
|
-
"README.md"
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
"pi-package",
|
|
22
23
|
"pi",
|
|
23
24
|
"pi-extension",
|
|
24
|
-
"permissions"
|
|
25
|
+
"permissions",
|
|
26
|
+
"pi-permissions"
|
|
25
27
|
],
|
|
26
28
|
"author": "davisliu2006",
|
|
27
29
|
"license": "MIT",
|
|
@@ -39,11 +41,13 @@
|
|
|
39
41
|
},
|
|
40
42
|
"peerDependencies": {
|
|
41
43
|
"@earendil-works/pi-ai": ">=0.78.0",
|
|
42
|
-
"@earendil-works/pi-coding-agent": ">=0.78.0"
|
|
44
|
+
"@earendil-works/pi-coding-agent": ">=0.78.0",
|
|
45
|
+
"@earendil-works/pi-tui": ">=0.78.0"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@earendil-works/pi-ai": "^0.78.1",
|
|
46
49
|
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
50
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
47
51
|
"@types/node": "^22.10.0",
|
|
48
52
|
"typescript": "^5.7.3"
|
|
49
53
|
}
|
package/src/ai-gen.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { complete, type Message } from "@earendil-works/pi-ai";
|
|
1
|
+
import { complete, type Api, type Message, type Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { parseAiModelRef } from "./types.ts";
|
|
3
4
|
|
|
4
5
|
export interface CompleteTextOptions {
|
|
5
6
|
systemPrompt: string;
|
|
@@ -8,6 +9,8 @@ export interface CompleteTextOptions {
|
|
|
8
9
|
timeoutMs: number;
|
|
9
10
|
/** How to join multiple text blocks. Defaults to a single space. */
|
|
10
11
|
joinWith?: string;
|
|
12
|
+
/** Optional `provider/model-id` override. Falls back to the session model. */
|
|
13
|
+
aiModel?: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
/**
|
|
@@ -23,17 +26,37 @@ function timeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): Abor
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
29
|
+
* Resolves the model for helper completions.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveCompletionModel(
|
|
32
|
+
ctx: ExtensionContext,
|
|
33
|
+
aiModel?: string
|
|
34
|
+
): Model<Api> | undefined {
|
|
35
|
+
if (aiModel) {
|
|
36
|
+
const parsed = parseAiModelRef(aiModel);
|
|
37
|
+
if (parsed) {
|
|
38
|
+
const configured = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
39
|
+
if (configured && ctx.modelRegistry.hasConfiguredAuth(configured)) {
|
|
40
|
+
return configured;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return ctx.model;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Runs a single-turn text completion against the extension helper model.
|
|
27
49
|
* Returns undefined when the model/auth is unavailable, the call aborts, or no text is returned.
|
|
28
50
|
*/
|
|
29
51
|
export async function completeText(
|
|
30
52
|
ctx: ExtensionContext,
|
|
31
53
|
options: CompleteTextOptions
|
|
32
54
|
): Promise<string | undefined> {
|
|
33
|
-
|
|
55
|
+
const model = resolveCompletionModel(ctx, options.aiModel);
|
|
56
|
+
if (!model) {return undefined;}
|
|
34
57
|
|
|
35
58
|
try {
|
|
36
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(
|
|
59
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
37
60
|
if (!auth.ok || !auth.apiKey) {return undefined;}
|
|
38
61
|
|
|
39
62
|
// prompt
|
|
@@ -50,7 +73,7 @@ export async function completeText(
|
|
|
50
73
|
|
|
51
74
|
// call model
|
|
52
75
|
const response = await complete(
|
|
53
|
-
|
|
76
|
+
model,
|
|
54
77
|
{
|
|
55
78
|
systemPrompt: options.systemPrompt,
|
|
56
79
|
messages: [message],
|
package/src/ai-purpose.ts
CHANGED
|
@@ -41,7 +41,8 @@ function sanitizePurpose(value: string): string | undefined {
|
|
|
41
41
|
export async function generateRequestPurpose(
|
|
42
42
|
ctx: ExtensionContext,
|
|
43
43
|
request: PermissionRequest,
|
|
44
|
-
context: string
|
|
44
|
+
context: string,
|
|
45
|
+
aiModel?: string
|
|
45
46
|
): Promise<string | undefined> {
|
|
46
47
|
const key = cacheKey(request);
|
|
47
48
|
const cached = purposeCache.get(key);
|
|
@@ -52,6 +53,7 @@ export async function generateRequestPurpose(
|
|
|
52
53
|
userText: buildPurposeInstructions(context),
|
|
53
54
|
maxTokens: MAX_PURPOSE_TOKENS,
|
|
54
55
|
timeoutMs: PURPOSE_TIMEOUT_MS,
|
|
56
|
+
aiModel,
|
|
55
57
|
});
|
|
56
58
|
if (!text) {return undefined;}
|
|
57
59
|
|
package/src/ai-wildcard.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { completeText } from "./ai-gen.ts";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
buildWildcardInstructions,
|
|
5
|
+
buildWildcardListInstructions,
|
|
6
|
+
WILDCARD_LIST_SYSTEM_PROMPT,
|
|
7
|
+
WILDCARD_SYSTEM_PROMPT,
|
|
8
|
+
} from "./prompts.ts";
|
|
9
|
+
import {
|
|
10
|
+
validateWildcardSuggestion,
|
|
11
|
+
validateWildcardSuggestionList,
|
|
12
|
+
} from "./wildcard-validation.ts";
|
|
5
13
|
|
|
6
14
|
const WILDCARD_TIMEOUT_MS = 8000;
|
|
7
15
|
const MAX_WILDCARD_TOKENS = 120;
|
|
16
|
+
const MAX_WILDCARD_LIST_TOKENS = 240;
|
|
8
17
|
|
|
9
18
|
/**
|
|
10
19
|
* Sanitizes against:
|
|
@@ -26,11 +35,12 @@ function sanitizeWildcard(value: string): string | undefined {
|
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
|
-
* Generates
|
|
38
|
+
* Generates one bash wildcard recommendation.
|
|
30
39
|
*/
|
|
31
40
|
export async function generateBashWildcard(
|
|
32
41
|
ctx: ExtensionContext,
|
|
33
|
-
command: string
|
|
42
|
+
command: string,
|
|
43
|
+
aiModel?: string
|
|
34
44
|
): Promise<string | undefined> {
|
|
35
45
|
const text = await completeText(ctx, {
|
|
36
46
|
systemPrompt: WILDCARD_SYSTEM_PROMPT,
|
|
@@ -38,6 +48,7 @@ export async function generateBashWildcard(
|
|
|
38
48
|
maxTokens: MAX_WILDCARD_TOKENS,
|
|
39
49
|
timeoutMs: WILDCARD_TIMEOUT_MS,
|
|
40
50
|
joinWith: "\n",
|
|
51
|
+
aiModel,
|
|
41
52
|
});
|
|
42
53
|
if (!text) {return undefined;}
|
|
43
54
|
|
|
@@ -49,3 +60,33 @@ export async function generateBashWildcard(
|
|
|
49
60
|
|
|
50
61
|
return pattern;
|
|
51
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generates a list of bash wildcards.
|
|
66
|
+
* Expects semicolon-separated patterns back.
|
|
67
|
+
*/
|
|
68
|
+
export async function generateBashWildcards(
|
|
69
|
+
ctx: ExtensionContext,
|
|
70
|
+
commands: readonly string[],
|
|
71
|
+
aiModel?: string
|
|
72
|
+
): Promise<string[] | undefined> {
|
|
73
|
+
if (commands.length === 0) {return undefined;}
|
|
74
|
+
|
|
75
|
+
const text = await completeText(ctx, {
|
|
76
|
+
systemPrompt: WILDCARD_LIST_SYSTEM_PROMPT,
|
|
77
|
+
userText: buildWildcardListInstructions(commands),
|
|
78
|
+
maxTokens: MAX_WILDCARD_LIST_TOKENS,
|
|
79
|
+
timeoutMs: WILDCARD_TIMEOUT_MS,
|
|
80
|
+
joinWith: "\n",
|
|
81
|
+
aiModel,
|
|
82
|
+
});
|
|
83
|
+
if (!text) {return undefined;}
|
|
84
|
+
|
|
85
|
+
const sanitized = sanitizeWildcard(text);
|
|
86
|
+
if (!sanitized) {return undefined;}
|
|
87
|
+
|
|
88
|
+
const validation = validateWildcardSuggestionList(sanitized, commands);
|
|
89
|
+
if (!validation.valid) {return undefined;}
|
|
90
|
+
|
|
91
|
+
return validation.patterns;
|
|
92
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { ASK
|
|
2
|
+
import { ASK } from "./constants.ts";
|
|
3
3
|
import { parseJsonc } from "./jsonc.ts";
|
|
4
4
|
import {
|
|
5
|
-
DEFAULT_PERMISSION_CONFIG,
|
|
6
5
|
createDefaultPermissionConfig,
|
|
6
|
+
parseAiModelRef,
|
|
7
7
|
toPermissionState,
|
|
8
8
|
toRecord,
|
|
9
9
|
type ConfigWarning,
|
|
10
10
|
type PermissionConfig,
|
|
11
11
|
type PermissionSection,
|
|
12
|
+
type PermissionSettings,
|
|
12
13
|
type PermissionState,
|
|
13
14
|
} from "./types.ts";
|
|
15
|
+
import {
|
|
16
|
+
CONFIG_MUST_BE_OBJECT,
|
|
17
|
+
formatConfigParseFailure,
|
|
18
|
+
formatConfigReadFailure,
|
|
19
|
+
formatIgnoredSectionEntryWarning,
|
|
20
|
+
formatIgnoredSectionWarning,
|
|
21
|
+
IGNORED_AI_MODEL_WARNING,
|
|
22
|
+
IGNORED_DEFAULT_WARNING,
|
|
23
|
+
IGNORED_SETTINGS_WARNING,
|
|
24
|
+
} from "./warning-strings.ts";
|
|
14
25
|
|
|
15
26
|
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
16
27
|
|
|
@@ -29,7 +40,7 @@ function normalizeSection(
|
|
|
29
40
|
if (!record) {
|
|
30
41
|
warnings.push({
|
|
31
42
|
path: configPath,
|
|
32
|
-
message:
|
|
43
|
+
message: formatIgnoredSectionWarning(sectionName),
|
|
33
44
|
});
|
|
34
45
|
return {};
|
|
35
46
|
}
|
|
@@ -43,18 +54,51 @@ function normalizeSection(
|
|
|
43
54
|
} else {
|
|
44
55
|
warnings.push({
|
|
45
56
|
path: configPath,
|
|
46
|
-
message:
|
|
57
|
+
message: formatIgnoredSectionEntryWarning(sectionName, key),
|
|
47
58
|
});
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
return result;
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Helper used by normalizeConfig.
|
|
66
|
+
* Ensures settings is a valid PermissionSettings object.
|
|
67
|
+
*/
|
|
68
|
+
function normalizeSettings(
|
|
69
|
+
raw: unknown,
|
|
70
|
+
configPath: string | undefined,
|
|
71
|
+
warnings: ConfigWarning[]
|
|
72
|
+
): PermissionSettings {
|
|
73
|
+
if (raw === undefined) {return {};}
|
|
74
|
+
const record = toRecord(raw);
|
|
75
|
+
if (!record) {
|
|
76
|
+
warnings.push({
|
|
77
|
+
path: configPath,
|
|
78
|
+
message: IGNORED_SETTINGS_WARNING,
|
|
79
|
+
});
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const settings: PermissionSettings = {};
|
|
84
|
+
if (record.ai_model !== undefined) {
|
|
85
|
+
if (typeof record.ai_model === "string" && parseAiModelRef(record.ai_model)) {
|
|
86
|
+
settings.ai_model = record.ai_model.trim();
|
|
87
|
+
} else {
|
|
88
|
+
warnings.push({
|
|
89
|
+
path: configPath,
|
|
90
|
+
message: IGNORED_AI_MODEL_WARNING,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return settings;
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
/**
|
|
54
98
|
* Convert the raw config to a PermissionConfig.
|
|
55
99
|
* Ensures the config is valid.
|
|
56
100
|
*/
|
|
57
|
-
function normalizeConfig(
|
|
101
|
+
export function normalizeConfig(
|
|
58
102
|
rawConfig: unknown,
|
|
59
103
|
configPath: string | undefined,
|
|
60
104
|
warnings: ConfigWarning[]
|
|
@@ -63,7 +107,7 @@ function normalizeConfig(
|
|
|
63
107
|
if (!config) {
|
|
64
108
|
warnings.push({
|
|
65
109
|
path: configPath,
|
|
66
|
-
message:
|
|
110
|
+
message: CONFIG_MUST_BE_OBJECT,
|
|
67
111
|
});
|
|
68
112
|
return createDefaultPermissionConfig();
|
|
69
113
|
}
|
|
@@ -72,7 +116,7 @@ function normalizeConfig(
|
|
|
72
116
|
if (config.default !== undefined && parsedDefault === undefined) {
|
|
73
117
|
warnings.push({
|
|
74
118
|
path: configPath,
|
|
75
|
-
message:
|
|
119
|
+
message: IGNORED_DEFAULT_WARNING,
|
|
76
120
|
});
|
|
77
121
|
}
|
|
78
122
|
const defaultState = parsedDefault ?? ASK;
|
|
@@ -87,6 +131,7 @@ function normalizeConfig(
|
|
|
87
131
|
configPath,
|
|
88
132
|
warnings
|
|
89
133
|
),
|
|
134
|
+
settings: normalizeSettings(config.settings, configPath, warnings),
|
|
90
135
|
};
|
|
91
136
|
}
|
|
92
137
|
|
|
@@ -105,7 +150,7 @@ async function loadConfigFile(
|
|
|
105
150
|
if (code === "ENOENT") {return undefined;} // file does not exist
|
|
106
151
|
warnings.push({
|
|
107
152
|
path,
|
|
108
|
-
message:
|
|
153
|
+
message: formatConfigReadFailure(error),
|
|
109
154
|
});
|
|
110
155
|
return undefined;
|
|
111
156
|
}
|
|
@@ -115,7 +160,7 @@ async function loadConfigFile(
|
|
|
115
160
|
} catch (error) {
|
|
116
161
|
warnings.push({
|
|
117
162
|
path,
|
|
118
|
-
message:
|
|
163
|
+
message: formatConfigParseFailure(error),
|
|
119
164
|
});
|
|
120
165
|
return undefined;
|
|
121
166
|
}
|
|
@@ -150,7 +195,7 @@ export function mergeConfigs(
|
|
|
150
195
|
return createDefaultPermissionConfig();
|
|
151
196
|
}
|
|
152
197
|
|
|
153
|
-
globalConfig = globalConfig ??
|
|
198
|
+
globalConfig = globalConfig ?? createDefaultPermissionConfig();
|
|
154
199
|
|
|
155
200
|
return {
|
|
156
201
|
default: localConfig?.default ?? globalConfig.default,
|
|
@@ -160,6 +205,10 @@ export function mergeConfigs(
|
|
|
160
205
|
...globalConfig.external_directory,
|
|
161
206
|
...(localConfig?.external_directory ?? {}),
|
|
162
207
|
},
|
|
208
|
+
settings: {
|
|
209
|
+
...globalConfig.settings,
|
|
210
|
+
...(localConfig?.settings ?? {}),
|
|
211
|
+
},
|
|
163
212
|
};
|
|
164
213
|
}
|
|
165
214
|
|
package/src/constants.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export const EXTENSION_NAME = "pi-permission-system";
|
|
2
2
|
export const STATUS_KEY = EXTENSION_NAME;
|
|
3
|
-
export const CONFIG_WARNING_PREFIX = `${EXTENSION_NAME}: `;
|
|
4
3
|
export const MAX_CONFIG_WARNINGS = 5;
|
|
5
4
|
|
|
6
5
|
export const CONFIG_FILENAME = "pi-permissions.jsonc";
|
|
6
|
+
/** Local config directory under the cwd. */
|
|
7
|
+
export const LOCAL_CONFIG_DIR_SEGMENTS = [".pi"] as const;
|
|
7
8
|
|
|
8
9
|
export const ALLOW = "allow";
|
|
9
10
|
export const ASK = "ask";
|
package/src/extension.ts
CHANGED
|
@@ -2,19 +2,18 @@ import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { clearPurposeCache, generateRequestPurpose } from "./ai-purpose.ts";
|
|
5
|
-
import { generateBashWildcard } from "./ai-wildcard.ts";
|
|
6
5
|
import { clearWildcardCache } from "./matcher.ts";
|
|
7
6
|
import {
|
|
8
7
|
ALLOW,
|
|
9
8
|
CONFIG_FILENAME,
|
|
10
|
-
CONFIG_WARNING_PREFIX,
|
|
11
9
|
DENY,
|
|
10
|
+
LOCAL_CONFIG_DIR_SEGMENTS,
|
|
12
11
|
MAX_CONFIG_WARNINGS,
|
|
13
12
|
STATUS_KEY,
|
|
14
13
|
} from "./constants.ts";
|
|
15
14
|
import { loadMergedConfig } from "./config.ts";
|
|
16
15
|
import { promptForPermission } from "./permission-dialog.ts";
|
|
17
|
-
import { findSessionApproval, resolvePolicy
|
|
16
|
+
import { findSessionApproval, resolvePolicy } from "./policy.ts";
|
|
18
17
|
import {
|
|
19
18
|
buildPurposePromptContext,
|
|
20
19
|
formatDenyReason,
|
|
@@ -23,22 +22,13 @@ import {
|
|
|
23
22
|
} from "./prompts.ts";
|
|
24
23
|
import { buildRequest } from "./request-builder.ts";
|
|
25
24
|
import { SessionApprovalStore } from "./session-approvals.ts";
|
|
25
|
+
import { STATUS_SUMMARIZING_REQUEST } from "./ui-strings.ts";
|
|
26
26
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
STATUS_SUMMARIZING_REQUEST,
|
|
33
|
-
WILDCARD_EDITOR_TITLE,
|
|
34
|
-
} from "./ui-strings.ts";
|
|
35
|
-
import { validateWildcardSuggestion } from "./wildcard-validation.ts";
|
|
36
|
-
import {
|
|
37
|
-
type PermissionConfig,
|
|
38
|
-
type PermissionRequest,
|
|
39
|
-
type PolicyResolution,
|
|
40
|
-
type AllowAlwaysRule,
|
|
41
|
-
} from "./types.ts";
|
|
27
|
+
CONFIG_WARNING_PREFIX,
|
|
28
|
+
formatSuppressedConfigWarnings,
|
|
29
|
+
} from "./warning-strings.ts";
|
|
30
|
+
import { chooseAlwaysRules } from "./wildcard-editor.ts";
|
|
31
|
+
import type { PermissionConfig } from "./types.ts";
|
|
42
32
|
|
|
43
33
|
interface ExtensionState {
|
|
44
34
|
config?: PermissionConfig;
|
|
@@ -60,7 +50,7 @@ function createNewState(): ExtensionState {
|
|
|
60
50
|
async function loadConfig(ctx: ExtensionContext): Promise<PermissionConfig> {
|
|
61
51
|
const { config, warnings } = await loadMergedConfig(
|
|
62
52
|
join(getAgentDir(), CONFIG_FILENAME),
|
|
63
|
-
join(ctx.cwd, CONFIG_FILENAME)
|
|
53
|
+
join(ctx.cwd, ...LOCAL_CONFIG_DIR_SEGMENTS, CONFIG_FILENAME)
|
|
64
54
|
);
|
|
65
55
|
|
|
66
56
|
if (ctx.hasUI) {
|
|
@@ -70,7 +60,7 @@ async function loadConfig(ctx: ExtensionContext): Promise<PermissionConfig> {
|
|
|
70
60
|
if (warnings.length > MAX_CONFIG_WARNINGS) {
|
|
71
61
|
const suppressed = warnings.length - MAX_CONFIG_WARNINGS;
|
|
72
62
|
ctx.ui.notify(
|
|
73
|
-
`${CONFIG_WARNING_PREFIX}${suppressed}
|
|
63
|
+
`${CONFIG_WARNING_PREFIX}${formatSuppressedConfigWarnings(suppressed)}`,
|
|
74
64
|
"warning"
|
|
75
65
|
);
|
|
76
66
|
}
|
|
@@ -79,71 +69,6 @@ async function loadConfig(ctx: ExtensionContext): Promise<PermissionConfig> {
|
|
|
79
69
|
return config;
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
/**
|
|
83
|
-
* Selects the "allow always" matching rule for the request.
|
|
84
|
-
*/
|
|
85
|
-
async function chooseAlwaysRule(
|
|
86
|
-
ctx: ExtensionContext,
|
|
87
|
-
request: PermissionRequest,
|
|
88
|
-
policy: PolicyResolution
|
|
89
|
-
): Promise<AllowAlwaysRule | undefined> {
|
|
90
|
-
if (request.isExternalDirectory && policy.source === "external") {
|
|
91
|
-
return {
|
|
92
|
-
scope: "external_directory",
|
|
93
|
-
tool: request.toolName,
|
|
94
|
-
kind: "exact",
|
|
95
|
-
body: request.body,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// no wildcard
|
|
100
|
-
if (!willGenerateBashWildcard(request, policy)) {
|
|
101
|
-
return { scope: "tool", tool: request.toolName, kind: "exact", body: request.body };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// generate wildcard
|
|
105
|
-
ctx.ui.setStatus(STATUS_KEY, STATUS_GENERATING_WILDCARD);
|
|
106
|
-
let suggestion: string | undefined;
|
|
107
|
-
try {
|
|
108
|
-
suggestion = await generateBashWildcard(ctx, request.body);
|
|
109
|
-
} finally {
|
|
110
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// show wildcard editor
|
|
114
|
-
let initial = suggestion ?? request.body;
|
|
115
|
-
while (true) {
|
|
116
|
-
const edited = await ctx.ui.editor(WILDCARD_EDITOR_TITLE, initial);
|
|
117
|
-
if (edited === undefined) {return undefined;}
|
|
118
|
-
|
|
119
|
-
const pattern = edited.trim();
|
|
120
|
-
const validation = validateWildcardSuggestion(pattern, request.body);
|
|
121
|
-
if (!validation.valid) {
|
|
122
|
-
ctx.ui.notify(`${INVALID_WILDCARD_PREFIX}${validation.reason}`, "warning");
|
|
123
|
-
initial = pattern || request.body;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (validation.broad) {
|
|
128
|
-
const confirmed = await ctx.ui.confirm(
|
|
129
|
-
BROAD_WILDCARD_TITLE,
|
|
130
|
-
formatBroadWildcardConfirm(pattern)
|
|
131
|
-
);
|
|
132
|
-
if (!confirmed) {
|
|
133
|
-
initial = pattern;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
scope: "tool",
|
|
140
|
-
tool: "bash",
|
|
141
|
-
kind: "wildcard",
|
|
142
|
-
body: pattern,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
72
|
/**
|
|
148
73
|
* Registers the extension.
|
|
149
74
|
*/
|
|
@@ -195,32 +120,24 @@ export function registerPermissionSystem(pi: ExtensionAPI): void {
|
|
|
195
120
|
}
|
|
196
121
|
|
|
197
122
|
// check session approval
|
|
198
|
-
const
|
|
199
|
-
policy.source === "external"
|
|
200
|
-
? request
|
|
201
|
-
: { ...request, isExternalDirectory: false };
|
|
202
|
-
const approval = findSessionApproval(
|
|
203
|
-
state.approvals.getRules(),
|
|
204
|
-
sessionRequest,
|
|
205
|
-
policy.isChainedBash
|
|
206
|
-
);
|
|
123
|
+
const approval = findSessionApproval(state.approvals.getRules(), request);
|
|
207
124
|
if (approval) {return undefined;} // approved, proceed
|
|
208
125
|
|
|
209
|
-
const summary = buildSelectorSummary(request);
|
|
210
126
|
ctx.ui.setStatus(STATUS_KEY, STATUS_SUMMARIZING_REQUEST);
|
|
211
127
|
let purpose: string | undefined;
|
|
212
128
|
try {
|
|
213
129
|
purpose = await generateRequestPurpose(
|
|
214
130
|
ctx,
|
|
215
131
|
request,
|
|
216
|
-
buildPurposePromptContext(request,
|
|
132
|
+
buildPurposePromptContext(request),
|
|
133
|
+
config.settings.ai_model
|
|
217
134
|
);
|
|
218
135
|
} finally {
|
|
219
136
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
220
137
|
}
|
|
221
138
|
|
|
222
139
|
while (true) {
|
|
223
|
-
const choice = await promptForPermission(ctx, request, policy,
|
|
140
|
+
const choice = await promptForPermission(ctx, request, policy, purpose);
|
|
224
141
|
if (choice.kind === "once") { // approved, proceed
|
|
225
142
|
return;
|
|
226
143
|
}
|
|
@@ -229,12 +146,19 @@ export function registerPermissionSystem(pi: ExtensionAPI): void {
|
|
|
229
146
|
}
|
|
230
147
|
|
|
231
148
|
// "allow always"
|
|
232
|
-
const
|
|
233
|
-
|
|
149
|
+
const rules = await chooseAlwaysRules(
|
|
150
|
+
ctx,
|
|
151
|
+
request,
|
|
152
|
+
policy,
|
|
153
|
+
config.settings.ai_model
|
|
154
|
+
);
|
|
155
|
+
if (!rules) {
|
|
234
156
|
// the user cancelled the wildcard editor, return to the permission dialog
|
|
235
157
|
continue;
|
|
236
158
|
}
|
|
237
|
-
|
|
159
|
+
for (const rule of rules) {
|
|
160
|
+
state.approvals.add(rule);
|
|
161
|
+
}
|
|
238
162
|
return undefined;
|
|
239
163
|
}
|
|
240
164
|
});
|