@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 CHANGED
@@ -29,7 +29,7 @@ Reject
29
29
 
30
30
  ## Configuration
31
31
 
32
- The supported top-level keys are: `default`, `tools`, `bash`, and `external_directory`.
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 `.pi/agent/pi-permissions.jsonc`.
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 a file in the local project directory root:
103
+ Located in the local Pi config folder:
101
104
 
102
- `pi-permissions.jsonc`
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 chaining.
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 bash command contains shell chaining or control syntax (`;`, `&&`, `||`, `|`, `&`, newlines, command substitution, or process substitution), `bash` pattern rules are skipped and the `tools.bash` policy is used instead, falling back to `default`.
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 a chained command saves the exact command, not a wildcard.
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.1.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
- * Runs a single-turn text completion against the extension model.
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
- if (!ctx.model) {return undefined;}
55
+ const model = resolveCompletionModel(ctx, options.aiModel);
56
+ if (!model) {return undefined;}
34
57
 
35
58
  try {
36
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
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
- ctx.model,
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
 
@@ -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 { buildWildcardInstructions, WILDCARD_SYSTEM_PROMPT } from "./prompts.ts";
4
- import { validateWildcardSuggestion } from "./wildcard-validation.ts";
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 the bash wildcard recommendation.
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, PERMISSION_STATES_DESCRIPTION } from "./constants.ts";
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: `Ignoring "${sectionName}": expected an object mapping names to ${PERMISSION_STATES_DESCRIPTION}.`,
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: `Ignoring "${sectionName}.${key}": expected ${PERMISSION_STATES_DESCRIPTION}.`,
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: "Permission config must be a JSON object; using defaults.",
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: `Ignoring "default": expected ${PERMISSION_STATES_DESCRIPTION}.`,
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: `Failed to read permission config: ${error instanceof Error ? error.message : String(error)}`,
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: `Failed to parse permission config: ${error instanceof Error ? error.message : String(error)}`,
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 ?? DEFAULT_PERMISSION_CONFIG;
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, willGenerateBashWildcard } from "./policy.ts";
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
- BROAD_WILDCARD_TITLE,
28
- buildSelectorSummary,
29
- formatBroadWildcardConfirm,
30
- INVALID_WILDCARD_PREFIX,
31
- STATUS_GENERATING_WILDCARD,
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} additional config warning(s) 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 sessionRequest =
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, policy)
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, summary, purpose);
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 rule = await chooseAlwaysRule(ctx, request, policy);
233
- if (!rule) {
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
- state.approvals.add(rule);
159
+ for (const rule of rules) {
160
+ state.approvals.add(rule);
161
+ }
238
162
  return undefined;
239
163
  }
240
164
  });