@gotgenes/pi-permission-system 3.11.0 → 4.0.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/CHANGELOG.md +35 -0
- package/README.md +135 -168
- package/config/config.example.json +11 -21
- package/package.json +1 -1
- package/schemas/permissions.schema.json +34 -102
- package/src/config-loader.ts +87 -118
- package/src/defaults.ts +6 -62
- package/src/extension-config.ts +3 -4
- package/src/normalize.ts +22 -60
- package/src/permission-manager.ts +244 -348
- package/src/synthesize.ts +17 -82
- package/src/types.ts +12 -18
- package/tests/config-loader.test.ts +113 -63
- package/tests/defaults.test.ts +8 -101
- package/tests/extension-config.test.ts +12 -4
- package/tests/normalize.test.ts +67 -64
- package/tests/permission-system.test.ts +153 -714
- package/tests/session-start.test.ts +1 -7
- package/tests/synthesize.test.ts +46 -219
|
@@ -9,21 +9,23 @@ import {
|
|
|
9
9
|
parseSimpleYamlMap,
|
|
10
10
|
toRecord,
|
|
11
11
|
} from "./common";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
loadUnifiedConfig,
|
|
14
|
+
normalizeUnifiedConfig,
|
|
15
|
+
stripJsonComments,
|
|
16
|
+
} from "./config-loader";
|
|
13
17
|
import { getGlobalConfigPath } from "./config-paths";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import type { Ruleset } from "./rule";
|
|
18
|
+
import { normalizeFlatConfig } from "./normalize";
|
|
19
|
+
import type { Rule, Ruleset } from "./rule";
|
|
17
20
|
import { evaluate } from "./rule";
|
|
18
21
|
import {
|
|
19
22
|
composeRuleset,
|
|
20
23
|
synthesizeBaseline,
|
|
21
24
|
synthesizeDefaults,
|
|
22
|
-
synthesizeOverrides,
|
|
23
25
|
} from "./synthesize";
|
|
24
26
|
import type {
|
|
27
|
+
FlatPermissionConfig,
|
|
25
28
|
PermissionCheckResult,
|
|
26
|
-
PermissionDefaultPolicy,
|
|
27
29
|
PermissionState,
|
|
28
30
|
ScopeConfig,
|
|
29
31
|
} from "./types";
|
|
@@ -48,71 +50,37 @@ const BUILT_IN_TOOL_PERMISSION_NAMES = new Set([
|
|
|
48
50
|
"ls",
|
|
49
51
|
]);
|
|
50
52
|
const SPECIAL_PERMISSION_KEYS = new Set(["external_directory"]);
|
|
51
|
-
const DEFAULT_POLICY: PermissionDefaultPolicy = {
|
|
52
|
-
tools: "ask",
|
|
53
|
-
bash: "ask",
|
|
54
|
-
mcp: "ask",
|
|
55
|
-
skills: "ask",
|
|
56
|
-
special: "ask",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
function normalizePolicy(value: unknown): PermissionDefaultPolicy {
|
|
60
|
-
const record = toRecord(value);
|
|
61
|
-
return {
|
|
62
|
-
tools: isPermissionState(record.tools)
|
|
63
|
-
? record.tools
|
|
64
|
-
: DEFAULT_POLICY.tools,
|
|
65
|
-
bash: isPermissionState(record.bash) ? record.bash : DEFAULT_POLICY.bash,
|
|
66
|
-
mcp: isPermissionState(record.mcp) ? record.mcp : DEFAULT_POLICY.mcp,
|
|
67
|
-
skills: isPermissionState(record.skills)
|
|
68
|
-
? record.skills
|
|
69
|
-
: DEFAULT_POLICY.skills,
|
|
70
|
-
special: isPermissionState(record.special)
|
|
71
|
-
? record.special
|
|
72
|
-
: DEFAULT_POLICY.special,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function normalizePartialPolicy(
|
|
77
|
-
value: unknown,
|
|
78
|
-
): Partial<PermissionDefaultPolicy> {
|
|
79
|
-
const record = toRecord(value);
|
|
80
|
-
const normalized: Partial<PermissionDefaultPolicy> = {};
|
|
81
|
-
|
|
82
|
-
if (isPermissionState(record.tools)) {
|
|
83
|
-
normalized.tools = record.tools;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (isPermissionState(record.bash)) {
|
|
87
|
-
normalized.bash = record.bash;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (isPermissionState(record.mcp)) {
|
|
91
|
-
normalized.mcp = record.mcp;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (isPermissionState(record.skills)) {
|
|
95
|
-
normalized.skills = record.skills;
|
|
96
|
-
}
|
|
97
53
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
54
|
+
/** Universal fallback when permission["*"] is absent from all scopes. */
|
|
55
|
+
const DEFAULT_UNIVERSAL_FALLBACK: PermissionState = "ask";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Deep-shallow merge two flat permission configs.
|
|
59
|
+
* Both objects → shallow-merge the pattern maps.
|
|
60
|
+
* Otherwise → override replaces base.
|
|
61
|
+
*/
|
|
62
|
+
function mergeFlatPermissions(
|
|
63
|
+
base: FlatPermissionConfig,
|
|
64
|
+
override: FlatPermissionConfig,
|
|
65
|
+
): FlatPermissionConfig {
|
|
66
|
+
const merged: FlatPermissionConfig = { ...base };
|
|
67
|
+
for (const [key, value] of Object.entries(override)) {
|
|
68
|
+
const baseVal = merged[key];
|
|
69
|
+
if (
|
|
70
|
+
typeof baseVal === "object" &&
|
|
71
|
+
baseVal !== null &&
|
|
72
|
+
typeof value === "object" &&
|
|
73
|
+
value !== null
|
|
74
|
+
) {
|
|
75
|
+
merged[key] = {
|
|
76
|
+
...(baseVal as Record<string, PermissionState>),
|
|
77
|
+
...(value as Record<string, PermissionState>),
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
merged[key] = value;
|
|
113
81
|
}
|
|
114
82
|
}
|
|
115
|
-
return
|
|
83
|
+
return merged;
|
|
116
84
|
}
|
|
117
85
|
|
|
118
86
|
function readConfiguredMcpServerNamesFromConfigPath(
|
|
@@ -148,208 +116,6 @@ function getConfiguredMcpServerNamesFromPaths(
|
|
|
148
116
|
);
|
|
149
117
|
}
|
|
150
118
|
|
|
151
|
-
const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
|
|
152
|
-
"doom_loop",
|
|
153
|
-
"tool_call_limit",
|
|
154
|
-
]);
|
|
155
|
-
|
|
156
|
-
export interface NormalizeResult {
|
|
157
|
-
permissions: ScopeConfig;
|
|
158
|
-
configIssues: string[];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function normalizeRawPermission(raw: unknown): NormalizeResult {
|
|
162
|
-
const record = toRecord(raw);
|
|
163
|
-
const configIssues: string[] = [];
|
|
164
|
-
const normalizedTools = normalizePermissionRecord(record.tools);
|
|
165
|
-
|
|
166
|
-
const normalized: ScopeConfig = {
|
|
167
|
-
defaultPolicy: normalizePartialPolicy(record.defaultPolicy),
|
|
168
|
-
tools: normalizedTools,
|
|
169
|
-
bash: normalizePermissionRecord(record.bash),
|
|
170
|
-
mcp: normalizePermissionRecord(record.mcp),
|
|
171
|
-
skills: normalizePermissionRecord(record.skills),
|
|
172
|
-
special: normalizePermissionRecord(record.special),
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// Detect deprecated keys in the raw special sub-object before discarding.
|
|
176
|
-
const rawSpecial = toRecord(record.special);
|
|
177
|
-
for (const key of DEPRECATED_SPECIAL_KEYS) {
|
|
178
|
-
if (key in rawSpecial) {
|
|
179
|
-
configIssues.push(
|
|
180
|
-
`special.${key} is deprecated and ignored — remove it from your policy file.`,
|
|
181
|
-
);
|
|
182
|
-
// Ensure the key is stripped even if its value was a valid PermissionState.
|
|
183
|
-
if (normalized.special) {
|
|
184
|
-
delete normalized.special[key];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const [key, value] of Object.entries(record)) {
|
|
190
|
-
if (!isPermissionState(value)) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (BUILT_IN_TOOL_PERMISSION_NAMES.has(key)) {
|
|
195
|
-
normalized.tools = { ...(normalized.tools || {}), [key]: value };
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (SPECIAL_PERMISSION_KEYS.has(key)) {
|
|
200
|
-
normalized.special = { ...(normalized.special || {}), [key]: value };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { permissions: normalized, configIssues };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function parseQualifiedMcpToolName(
|
|
208
|
-
value: string,
|
|
209
|
-
): { server: string; tool: string } | null {
|
|
210
|
-
const trimmed = value.trim();
|
|
211
|
-
if (!trimmed) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const colonIndex = trimmed.indexOf(":");
|
|
216
|
-
if (colonIndex <= 0 || colonIndex >= trimmed.length - 1) {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const server = trimmed.slice(0, colonIndex).trim();
|
|
221
|
-
const tool = trimmed.slice(colonIndex + 1).trim();
|
|
222
|
-
if (!server || !tool) {
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return { server, tool };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function addDerivedMcpServerTargets(
|
|
230
|
-
toolName: string,
|
|
231
|
-
configuredServerNames: readonly string[],
|
|
232
|
-
pushTarget: (value: string | null) => void,
|
|
233
|
-
): void {
|
|
234
|
-
const trimmedToolName = toolName.trim();
|
|
235
|
-
if (!trimmedToolName) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const serverName of configuredServerNames) {
|
|
240
|
-
const trimmedServerName = serverName.trim();
|
|
241
|
-
if (!trimmedServerName) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (!trimmedToolName.endsWith(`_${trimmedServerName}`)) {
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (trimmedToolName.startsWith(`${trimmedServerName}_`)) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
pushTarget(`${trimmedServerName}_${trimmedToolName}`);
|
|
254
|
-
pushTarget(`${trimmedServerName}:${trimmedToolName}`);
|
|
255
|
-
pushTarget(trimmedServerName);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function pushMcpToolPermissionTargets(
|
|
260
|
-
rawReference: string,
|
|
261
|
-
serverHint: string | null,
|
|
262
|
-
configuredServerNames: readonly string[],
|
|
263
|
-
pushTarget: (value: string | null) => void,
|
|
264
|
-
): void {
|
|
265
|
-
const qualified = parseQualifiedMcpToolName(rawReference);
|
|
266
|
-
const resolvedServer = serverHint ?? qualified?.server ?? null;
|
|
267
|
-
const resolvedTool = qualified?.tool ?? rawReference;
|
|
268
|
-
|
|
269
|
-
if (resolvedServer) {
|
|
270
|
-
pushTarget(`${resolvedServer}_${resolvedTool}`);
|
|
271
|
-
pushTarget(`${resolvedServer}:${resolvedTool}`);
|
|
272
|
-
pushTarget(resolvedServer);
|
|
273
|
-
} else {
|
|
274
|
-
addDerivedMcpServerTargets(resolvedTool, configuredServerNames, pushTarget);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
pushTarget(resolvedTool);
|
|
278
|
-
pushTarget(rawReference);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function createMcpPermissionTargets(
|
|
282
|
-
input: unknown,
|
|
283
|
-
configuredServerNames: readonly string[] = [],
|
|
284
|
-
): string[] {
|
|
285
|
-
const record = toRecord(input);
|
|
286
|
-
const tool = getNonEmptyString(record.tool);
|
|
287
|
-
const server = getNonEmptyString(record.server);
|
|
288
|
-
const connect = getNonEmptyString(record.connect);
|
|
289
|
-
const describe = getNonEmptyString(record.describe);
|
|
290
|
-
const search = getNonEmptyString(record.search);
|
|
291
|
-
|
|
292
|
-
const targets: string[] = [];
|
|
293
|
-
const pushTarget = (value: string | null) => {
|
|
294
|
-
if (!value) {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
if (!targets.includes(value)) {
|
|
298
|
-
targets.push(value);
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
if (tool) {
|
|
303
|
-
pushMcpToolPermissionTargets(
|
|
304
|
-
tool,
|
|
305
|
-
server,
|
|
306
|
-
configuredServerNames,
|
|
307
|
-
pushTarget,
|
|
308
|
-
);
|
|
309
|
-
pushTarget("mcp_call");
|
|
310
|
-
return targets;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (connect) {
|
|
314
|
-
pushTarget(`mcp_connect_${connect}`);
|
|
315
|
-
pushTarget(connect);
|
|
316
|
-
pushTarget("mcp_connect");
|
|
317
|
-
return targets;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (describe) {
|
|
321
|
-
pushMcpToolPermissionTargets(
|
|
322
|
-
describe,
|
|
323
|
-
server,
|
|
324
|
-
configuredServerNames,
|
|
325
|
-
pushTarget,
|
|
326
|
-
);
|
|
327
|
-
pushTarget("mcp_describe");
|
|
328
|
-
return targets;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (search) {
|
|
332
|
-
if (server) {
|
|
333
|
-
pushTarget(`mcp_server_${server}`);
|
|
334
|
-
pushTarget(server);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
pushTarget(search);
|
|
338
|
-
pushTarget("mcp_search");
|
|
339
|
-
return targets;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (server) {
|
|
343
|
-
pushTarget(`mcp_server_${server}`);
|
|
344
|
-
pushTarget(server);
|
|
345
|
-
pushTarget("mcp_list");
|
|
346
|
-
return targets;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
pushTarget("mcp_status");
|
|
350
|
-
return targets;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
119
|
export interface ResolvedPolicyPaths {
|
|
354
120
|
globalConfigPath: string;
|
|
355
121
|
globalConfigExists: boolean;
|
|
@@ -363,7 +129,7 @@ export interface ResolvedPolicyPaths {
|
|
|
363
129
|
|
|
364
130
|
type ResolvedPermissions = {
|
|
365
131
|
/**
|
|
366
|
-
* Fully composed ruleset: synthesized defaults → baseline →
|
|
132
|
+
* Fully composed ruleset: synthesized defaults → baseline → config.
|
|
367
133
|
* Session rules are appended at call-time inside checkPermission().
|
|
368
134
|
*/
|
|
369
135
|
composedRules: Ruleset;
|
|
@@ -460,12 +226,7 @@ export class PermissionManager {
|
|
|
460
226
|
this.accumulateConfigIssues(issues);
|
|
461
227
|
|
|
462
228
|
const value: ScopeConfig = {
|
|
463
|
-
|
|
464
|
-
tools: config.tools || {},
|
|
465
|
-
bash: config.bash || {},
|
|
466
|
-
mcp: config.mcp || {},
|
|
467
|
-
skills: config.skills || {},
|
|
468
|
-
special: config.special || {},
|
|
229
|
+
permission: config.permission,
|
|
469
230
|
};
|
|
470
231
|
|
|
471
232
|
this.globalConfigCache = { stamp, value };
|
|
@@ -486,12 +247,7 @@ export class PermissionManager {
|
|
|
486
247
|
this.accumulateConfigIssues(issues);
|
|
487
248
|
|
|
488
249
|
const value: ScopeConfig = {
|
|
489
|
-
|
|
490
|
-
tools: config.tools,
|
|
491
|
-
bash: config.bash,
|
|
492
|
-
mcp: config.mcp,
|
|
493
|
-
skills: config.skills,
|
|
494
|
-
special: config.special,
|
|
250
|
+
permission: config.permission,
|
|
495
251
|
};
|
|
496
252
|
|
|
497
253
|
this.projectGlobalConfigCache = { stamp, value };
|
|
@@ -522,9 +278,11 @@ export class PermissionManager {
|
|
|
522
278
|
value = {};
|
|
523
279
|
} else {
|
|
524
280
|
const parsed = parseSimpleYamlMap(frontmatter);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
281
|
+
// Re-use the config-loader normalizer so the flat permission shape
|
|
282
|
+
// is validated the same way as on-disk config files.
|
|
283
|
+
const { config, issues } = normalizeUnifiedConfig(parsed);
|
|
284
|
+
this.accumulateConfigIssues(issues);
|
|
285
|
+
value = { permission: config.permission };
|
|
528
286
|
}
|
|
529
287
|
} catch {
|
|
530
288
|
value = {};
|
|
@@ -595,48 +353,46 @@ export class PermissionManager {
|
|
|
595
353
|
const agentConfig = this.loadScopeConfig(agentName);
|
|
596
354
|
const projectAgentConfig = this.loadProjectScopeConfig(agentName);
|
|
597
355
|
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
356
|
+
// Merge permission objects across scopes (lowest → highest precedence).
|
|
357
|
+
let mergedPermission: FlatPermissionConfig = {};
|
|
358
|
+
for (const scope of [
|
|
359
|
+
globalConfig,
|
|
360
|
+
projectConfig,
|
|
361
|
+
agentConfig,
|
|
362
|
+
projectAgentConfig,
|
|
363
|
+
]) {
|
|
364
|
+
if (scope.permission) {
|
|
365
|
+
mergedPermission = mergeFlatPermissions(
|
|
366
|
+
mergedPermission,
|
|
367
|
+
scope.permission,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Extract the universal fallback from permission["*"].
|
|
373
|
+
// The "*" key feeds synthesizeDefaults() only — it is NOT included as a
|
|
374
|
+
// config rule so that extension tools fall through to source:"default".
|
|
375
|
+
const universalFallback = isPermissionState(mergedPermission["*"])
|
|
376
|
+
? (mergedPermission["*"] as PermissionState)
|
|
377
|
+
: DEFAULT_UNIVERSAL_FALLBACK;
|
|
378
|
+
|
|
379
|
+
// Build config rules from everything except the universal "*" key.
|
|
380
|
+
const permissionWithoutUniversal: FlatPermissionConfig = Object.fromEntries(
|
|
381
|
+
Object.entries(mergedPermission).filter(([k]) => k !== "*"),
|
|
617
382
|
);
|
|
618
383
|
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
{ bash: projectConfig.tools?.bash, mcp: projectConfig.tools?.mcp },
|
|
624
|
-
{ bash: agentConfig.tools?.bash, mcp: agentConfig.tools?.mcp },
|
|
625
|
-
{
|
|
626
|
-
bash: projectAgentConfig.tools?.bash,
|
|
627
|
-
mcp: projectAgentConfig.tools?.mcp,
|
|
628
|
-
},
|
|
629
|
-
];
|
|
384
|
+
// Normalize to config rules, tagged with "config" layer.
|
|
385
|
+
const configRules: Ruleset = normalizeFlatConfig(
|
|
386
|
+
permissionWithoutUniversal,
|
|
387
|
+
).map((r): Rule => ({ ...r, layer: "config" }));
|
|
630
388
|
|
|
631
389
|
const composedRules = composeRuleset(
|
|
632
|
-
synthesizeDefaults(
|
|
390
|
+
synthesizeDefaults(universalFallback),
|
|
633
391
|
synthesizeBaseline(configRules),
|
|
634
|
-
synthesizeOverrides(overrideScopes),
|
|
635
392
|
configRules,
|
|
636
393
|
);
|
|
637
394
|
|
|
638
395
|
const value: ResolvedPermissions = { composedRules };
|
|
639
|
-
|
|
640
396
|
this.resolvedPermissionsCache.set(cacheKey, { stamp, value });
|
|
641
397
|
return value;
|
|
642
398
|
}
|
|
@@ -660,31 +416,20 @@ export class PermissionManager {
|
|
|
660
416
|
}
|
|
661
417
|
|
|
662
418
|
/**
|
|
663
|
-
* Get the tool-level permission state for a tool, without considering
|
|
664
|
-
*
|
|
665
|
-
* at the tool level before checking specific command permissions.
|
|
666
|
-
*
|
|
667
|
-
* With tool-name-as-surface normalization, tools.bash becomes a bash catch-all
|
|
668
|
-
* { surface: "bash", pattern: "*", action } so getToolPermission("bash")
|
|
669
|
-
* naturally picks it up via evaluate("bash", "*", rules).
|
|
670
|
-
*
|
|
671
|
-
* @param toolName - The name of the tool (for example "bash", "read", or a third-party tool name)
|
|
672
|
-
* @param agentName - Optional agent name to check agent-specific permissions
|
|
673
|
-
* @returns The permission state for the tool at the tool level
|
|
419
|
+
* Get the tool-level permission state for a tool, without considering
|
|
420
|
+
* command-level rules. Used for tool injection decisions.
|
|
674
421
|
*/
|
|
675
422
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
676
423
|
const { composedRules } = this.resolvePermissions(agentName);
|
|
677
424
|
const normalizedToolName = toolName.trim();
|
|
678
425
|
|
|
679
|
-
// Special surfaces: evaluate
|
|
680
|
-
// and the synthesized special default.
|
|
426
|
+
// Special surfaces (external_directory): evaluate directly by surface name.
|
|
681
427
|
if (SPECIAL_PERMISSION_KEYS.has(normalizedToolName)) {
|
|
682
|
-
return evaluate("
|
|
428
|
+
return evaluate(normalizedToolName, "*", composedRules).action;
|
|
683
429
|
}
|
|
684
430
|
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// respond correctly to a "*" lookup without matching specific patterns.
|
|
431
|
+
// Bash, MCP, skill: evaluate with "*" value — the per-surface catch-all
|
|
432
|
+
// (or universal default) handles this correctly.
|
|
688
433
|
if (normalizedToolName === "bash") {
|
|
689
434
|
return evaluate("bash", "*", composedRules).action;
|
|
690
435
|
}
|
|
@@ -709,12 +454,11 @@ export class PermissionManager {
|
|
|
709
454
|
const normalizedToolName = toolName.trim();
|
|
710
455
|
|
|
711
456
|
// --- Special surfaces (external_directory) ---
|
|
712
|
-
// Config/default rules use surface "special"; session rules use surface
|
|
713
|
-
// "external_directory" with path patterns. Check each independently.
|
|
714
457
|
if (SPECIAL_PERMISSION_KEYS.has(normalizedToolName)) {
|
|
715
|
-
// Session check: match by specific normalized path.
|
|
716
458
|
const record = toRecord(input);
|
|
717
459
|
const pathValue = typeof record.path === "string" ? record.path : null;
|
|
460
|
+
|
|
461
|
+
// Session check: match by specific normalized path.
|
|
718
462
|
if (pathValue && sessionRules && sessionRules.length > 0) {
|
|
719
463
|
const sessionRule = evaluate(
|
|
720
464
|
"external_directory",
|
|
@@ -730,8 +474,13 @@ export class PermissionManager {
|
|
|
730
474
|
};
|
|
731
475
|
}
|
|
732
476
|
}
|
|
477
|
+
|
|
733
478
|
// Config/default check.
|
|
734
|
-
const rule = evaluate(
|
|
479
|
+
const rule = evaluate(
|
|
480
|
+
normalizedToolName,
|
|
481
|
+
pathValue ?? "*",
|
|
482
|
+
composedRules,
|
|
483
|
+
);
|
|
735
484
|
return {
|
|
736
485
|
toolName,
|
|
737
486
|
state: rule.action,
|
|
@@ -778,10 +527,7 @@ export class PermissionManager {
|
|
|
778
527
|
];
|
|
779
528
|
const fallbackTarget = mcpTargets[0] || "mcp";
|
|
780
529
|
|
|
781
|
-
// Try each candidate target. Stop on the first non-default match
|
|
782
|
-
// (config, override, or baseline rule). Default rules are catch-alls
|
|
783
|
-
// that would fire on every target — skip them so more-specific targets
|
|
784
|
-
// can be checked first.
|
|
530
|
+
// Try each candidate target. Stop on the first non-default match.
|
|
785
531
|
for (const target of mcpTargets) {
|
|
786
532
|
const rule = evaluate("mcp", target, composedRules);
|
|
787
533
|
if (rule.layer !== "default") {
|
|
@@ -808,8 +554,6 @@ export class PermissionManager {
|
|
|
808
554
|
// --- Tools (read, write, edit, grep, find, ls, extension tools) ---
|
|
809
555
|
const rule = evaluate(normalizedToolName, "*", composedRules);
|
|
810
556
|
|
|
811
|
-
// Built-in tools always report source "tool" regardless of which layer
|
|
812
|
-
// supplied the decision (matches current behaviour).
|
|
813
557
|
if (BUILT_IN_TOOL_PERMISSION_NAMES.has(normalizedToolName)) {
|
|
814
558
|
return {
|
|
815
559
|
toolName,
|
|
@@ -818,8 +562,6 @@ export class PermissionManager {
|
|
|
818
562
|
};
|
|
819
563
|
}
|
|
820
564
|
|
|
821
|
-
// Extension tools: "default" layer → source "default"; any explicit rule
|
|
822
|
-
// (config or override) → source "tool".
|
|
823
565
|
return {
|
|
824
566
|
toolName,
|
|
825
567
|
state: rule.action,
|
|
@@ -827,3 +569,157 @@ export class PermissionManager {
|
|
|
827
569
|
};
|
|
828
570
|
}
|
|
829
571
|
}
|
|
572
|
+
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// MCP target derivation helpers (unchanged)
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
function parseQualifiedMcpToolName(
|
|
578
|
+
value: string,
|
|
579
|
+
): { server: string; tool: string } | null {
|
|
580
|
+
const trimmed = value.trim();
|
|
581
|
+
if (!trimmed) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const colonIndex = trimmed.indexOf(":");
|
|
586
|
+
if (colonIndex <= 0 || colonIndex >= trimmed.length - 1) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const server = trimmed.slice(0, colonIndex).trim();
|
|
591
|
+
const tool = trimmed.slice(colonIndex + 1).trim();
|
|
592
|
+
if (!server || !tool) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { server, tool };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function addDerivedMcpServerTargets(
|
|
600
|
+
toolName: string,
|
|
601
|
+
configuredServerNames: readonly string[],
|
|
602
|
+
pushTarget: (value: string | null) => void,
|
|
603
|
+
): void {
|
|
604
|
+
const trimmedToolName = toolName.trim();
|
|
605
|
+
if (!trimmedToolName) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
for (const serverName of configuredServerNames) {
|
|
610
|
+
const trimmedServerName = serverName.trim();
|
|
611
|
+
if (!trimmedServerName) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!trimmedToolName.endsWith(`_${trimmedServerName}`)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (trimmedToolName.startsWith(`${trimmedServerName}_`)) {
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
pushTarget(`${trimmedServerName}_${trimmedToolName}`);
|
|
624
|
+
pushTarget(`${trimmedServerName}:${trimmedToolName}`);
|
|
625
|
+
pushTarget(trimmedServerName);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function pushMcpToolPermissionTargets(
|
|
630
|
+
rawReference: string,
|
|
631
|
+
serverHint: string | null,
|
|
632
|
+
configuredServerNames: readonly string[],
|
|
633
|
+
pushTarget: (value: string | null) => void,
|
|
634
|
+
): void {
|
|
635
|
+
const qualified = parseQualifiedMcpToolName(rawReference);
|
|
636
|
+
const resolvedServer = serverHint ?? qualified?.server ?? null;
|
|
637
|
+
const resolvedTool = qualified?.tool ?? rawReference;
|
|
638
|
+
|
|
639
|
+
if (resolvedServer) {
|
|
640
|
+
pushTarget(`${resolvedServer}_${resolvedTool}`);
|
|
641
|
+
pushTarget(`${resolvedServer}:${resolvedTool}`);
|
|
642
|
+
pushTarget(resolvedServer);
|
|
643
|
+
} else {
|
|
644
|
+
addDerivedMcpServerTargets(resolvedTool, configuredServerNames, pushTarget);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
pushTarget(resolvedTool);
|
|
648
|
+
pushTarget(rawReference);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function createMcpPermissionTargets(
|
|
652
|
+
input: unknown,
|
|
653
|
+
configuredServerNames: readonly string[] = [],
|
|
654
|
+
): string[] {
|
|
655
|
+
const record = toRecord(input);
|
|
656
|
+
const tool = getNonEmptyString(record.tool);
|
|
657
|
+
const server = getNonEmptyString(record.server);
|
|
658
|
+
const connect = getNonEmptyString(record.connect);
|
|
659
|
+
const describe = getNonEmptyString(record.describe);
|
|
660
|
+
const search = getNonEmptyString(record.search);
|
|
661
|
+
|
|
662
|
+
const targets: string[] = [];
|
|
663
|
+
const pushTarget = (value: string | null) => {
|
|
664
|
+
if (!value) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (!targets.includes(value)) {
|
|
668
|
+
targets.push(value);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
if (tool) {
|
|
673
|
+
pushMcpToolPermissionTargets(
|
|
674
|
+
tool,
|
|
675
|
+
server,
|
|
676
|
+
configuredServerNames,
|
|
677
|
+
pushTarget,
|
|
678
|
+
);
|
|
679
|
+
pushTarget("mcp_call");
|
|
680
|
+
return targets;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (connect) {
|
|
684
|
+
pushTarget(`mcp_connect_${connect}`);
|
|
685
|
+
pushTarget(connect);
|
|
686
|
+
pushTarget("mcp_connect");
|
|
687
|
+
return targets;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (describe) {
|
|
691
|
+
pushMcpToolPermissionTargets(
|
|
692
|
+
describe,
|
|
693
|
+
server,
|
|
694
|
+
configuredServerNames,
|
|
695
|
+
pushTarget,
|
|
696
|
+
);
|
|
697
|
+
pushTarget("mcp_describe");
|
|
698
|
+
return targets;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (search) {
|
|
702
|
+
if (server) {
|
|
703
|
+
pushTarget(`mcp_server_${server}`);
|
|
704
|
+
pushTarget(server);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
pushTarget(search);
|
|
708
|
+
pushTarget("mcp_search");
|
|
709
|
+
return targets;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (server) {
|
|
713
|
+
pushTarget(`mcp_server_${server}`);
|
|
714
|
+
pushTarget(server);
|
|
715
|
+
pushTarget("mcp_list");
|
|
716
|
+
return targets;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
pushTarget("mcp_status");
|
|
720
|
+
return targets;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Keep isPermissionState and toRecord available for convenience — they are
|
|
724
|
+
// used directly in some handler files that import from permission-manager.
|
|
725
|
+
export { isPermissionState, toRecord };
|