@gotgenes/pi-permission-system 3.7.0 → 3.9.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 +39 -0
- package/package.json +1 -1
- package/src/defaults.ts +60 -0
- package/src/forwarded-permissions/io.ts +47 -12
- package/src/forwarded-permissions/polling.ts +33 -11
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/input.ts +7 -5
- package/src/handlers/lifecycle.ts +25 -24
- package/src/handlers/tool-call.ts +32 -22
- package/src/handlers/types.ts +7 -30
- package/src/index.ts +47 -417
- package/src/normalize.ts +70 -0
- package/src/permission-manager.ts +127 -254
- package/src/rule.ts +7 -23
- package/src/runtime.ts +484 -0
- package/src/types.ts +13 -18
- package/tests/defaults.test.ts +105 -0
- package/tests/forwarded-permissions/io.test.ts +135 -0
- package/tests/handlers/before-agent-start.test.ts +47 -31
- package/tests/handlers/input.test.ts +69 -39
- package/tests/handlers/lifecycle.test.ts +86 -65
- package/tests/handlers/tool-call.test.ts +92 -69
- package/tests/normalize.test.ts +121 -0
- package/tests/permission-system.test.ts +11 -39
- package/tests/rule.test.ts +24 -42
- package/tests/runtime.test.ts +618 -0
- package/tests/session-start.test.ts +2 -2
- package/src/bash-filter.ts +0 -51
- package/tests/bash-filter.test.ts +0 -142
|
@@ -2,7 +2,6 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
|
|
5
|
-
import { BashFilter } from "./bash-filter";
|
|
6
5
|
import {
|
|
7
6
|
extractFrontmatter,
|
|
8
7
|
getNonEmptyString,
|
|
@@ -12,22 +11,16 @@ import {
|
|
|
12
11
|
} from "./common";
|
|
13
12
|
import { loadUnifiedConfig, stripJsonComments } from "./config-loader";
|
|
14
13
|
import { getGlobalConfigPath } from "./config-paths";
|
|
15
|
-
import
|
|
14
|
+
import { mergeDefaults } from "./defaults";
|
|
15
|
+
import { normalizeConfig } from "./normalize";
|
|
16
|
+
import type { Ruleset } from "./rule";
|
|
16
17
|
import { evaluate } from "./rule";
|
|
17
18
|
import type {
|
|
18
|
-
AgentPermissions,
|
|
19
|
-
BashPermissions,
|
|
20
|
-
GlobalPermissionConfig,
|
|
21
19
|
PermissionCheckResult,
|
|
22
20
|
PermissionDefaultPolicy,
|
|
23
21
|
PermissionState,
|
|
22
|
+
ScopeConfig,
|
|
24
23
|
} from "./types";
|
|
25
|
-
import {
|
|
26
|
-
type CompiledWildcardPattern,
|
|
27
|
-
compileWildcardPatternEntries,
|
|
28
|
-
findCompiledWildcardMatch,
|
|
29
|
-
findCompiledWildcardMatchForNames,
|
|
30
|
-
} from "./wildcard-matcher";
|
|
31
24
|
|
|
32
25
|
function defaultGlobalConfigPath(): string {
|
|
33
26
|
return getGlobalConfigPath(getAgentDir());
|
|
@@ -163,7 +156,7 @@ const DEPRECATED_SPECIAL_KEYS: ReadonlySet<string> = new Set([
|
|
|
163
156
|
]);
|
|
164
157
|
|
|
165
158
|
export interface NormalizeResult {
|
|
166
|
-
permissions:
|
|
159
|
+
permissions: ScopeConfig;
|
|
167
160
|
configIssues: string[];
|
|
168
161
|
}
|
|
169
162
|
|
|
@@ -172,7 +165,7 @@ export function normalizeRawPermission(raw: unknown): NormalizeResult {
|
|
|
172
165
|
const configIssues: string[] = [];
|
|
173
166
|
const normalizedTools = normalizePermissionRecord(record.tools);
|
|
174
167
|
|
|
175
|
-
const normalized:
|
|
168
|
+
const normalized: ScopeConfig = {
|
|
176
169
|
defaultPolicy: normalizePartialPolicy(record.defaultPolicy),
|
|
177
170
|
tools: normalizedTools,
|
|
178
171
|
bash: normalizePermissionRecord(record.bash),
|
|
@@ -359,9 +352,6 @@ function createMcpPermissionTargets(
|
|
|
359
352
|
return targets;
|
|
360
353
|
}
|
|
361
354
|
|
|
362
|
-
type CompiledPermissionPatterns =
|
|
363
|
-
readonly CompiledWildcardPattern<PermissionState>[];
|
|
364
|
-
|
|
365
355
|
export interface ResolvedPolicyPaths {
|
|
366
356
|
globalConfigPath: string;
|
|
367
357
|
globalConfigExists: boolean;
|
|
@@ -374,76 +364,15 @@ export interface ResolvedPolicyPaths {
|
|
|
374
364
|
}
|
|
375
365
|
|
|
376
366
|
type ResolvedPermissions = {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
compiledBash: CompiledPermissionPatterns;
|
|
367
|
+
rules: Ruleset;
|
|
368
|
+
defaults: PermissionDefaultPolicy;
|
|
369
|
+
/** tools.bash fallback: tools.bash || defaults.bash */
|
|
381
370
|
bashDefault: PermissionState;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
bashFilter: BashFilter;
|
|
371
|
+
/** tools.mcp fallback (undefined = no explicit tools.mcp) */
|
|
372
|
+
mcpToolLevel: PermissionState | undefined;
|
|
373
|
+
hasAnyMcpAllowRule: boolean;
|
|
386
374
|
};
|
|
387
375
|
|
|
388
|
-
function compilePermissionPatternsFromSources(
|
|
389
|
-
...sources: Array<Record<string, PermissionState> | undefined>
|
|
390
|
-
): CompiledPermissionPatterns {
|
|
391
|
-
const entries: Array<readonly [string, PermissionState]> = [];
|
|
392
|
-
|
|
393
|
-
for (const source of sources) {
|
|
394
|
-
if (!source) {
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
for (const entry of Object.entries(source)) {
|
|
399
|
-
entries.push(entry);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (entries.length === 0) {
|
|
404
|
-
return [];
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return compileWildcardPatternEntries(entries);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Convert compiled wildcard patterns into a Ruleset for use with evaluate().
|
|
412
|
-
* The returned Rule objects are the same references as the input; evaluate()
|
|
413
|
-
* uses reference equality to distinguish an explicit match from the synthetic
|
|
414
|
-
* default it returns when nothing matches.
|
|
415
|
-
*/
|
|
416
|
-
function compiledToRuleset(
|
|
417
|
-
surface: string,
|
|
418
|
-
patterns: CompiledPermissionPatterns,
|
|
419
|
-
): Ruleset {
|
|
420
|
-
return patterns.map(
|
|
421
|
-
(p): Rule => ({ surface, pattern: p.pattern, action: p.state }),
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function findCompiledPermissionMatch(
|
|
426
|
-
patterns: CompiledPermissionPatterns,
|
|
427
|
-
name: string,
|
|
428
|
-
) {
|
|
429
|
-
if (patterns.length === 0) {
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return findCompiledWildcardMatch(patterns, name);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function findCompiledPermissionMatchForNames(
|
|
437
|
-
patterns: CompiledPermissionPatterns,
|
|
438
|
-
names: readonly string[],
|
|
439
|
-
) {
|
|
440
|
-
if (patterns.length === 0) {
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return findCompiledWildcardMatchForNames(patterns, names);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
376
|
type FileCacheEntry<TValue> = {
|
|
448
377
|
stamp: string;
|
|
449
378
|
value: TValue;
|
|
@@ -464,17 +393,15 @@ export class PermissionManager {
|
|
|
464
393
|
private readonly projectAgentsDir: string | null;
|
|
465
394
|
private readonly globalMcpConfigPath: string;
|
|
466
395
|
private readonly configuredMcpServerNamesOverride: readonly string[] | null;
|
|
467
|
-
private globalConfigCache: FileCacheEntry<
|
|
468
|
-
|
|
469
|
-
private projectGlobalConfigCache: FileCacheEntry<AgentPermissions> | null =
|
|
470
|
-
null;
|
|
396
|
+
private globalConfigCache: FileCacheEntry<ScopeConfig> | null = null;
|
|
397
|
+
private projectGlobalConfigCache: FileCacheEntry<ScopeConfig> | null = null;
|
|
471
398
|
private readonly agentConfigCache = new Map<
|
|
472
399
|
string,
|
|
473
|
-
FileCacheEntry<
|
|
400
|
+
FileCacheEntry<ScopeConfig>
|
|
474
401
|
>();
|
|
475
402
|
private readonly projectAgentConfigCache = new Map<
|
|
476
403
|
string,
|
|
477
|
-
FileCacheEntry<
|
|
404
|
+
FileCacheEntry<ScopeConfig>
|
|
478
405
|
>();
|
|
479
406
|
private readonly resolvedPermissionsCache = new Map<
|
|
480
407
|
string,
|
|
@@ -527,7 +454,7 @@ export class PermissionManager {
|
|
|
527
454
|
return [...this.accumulatedConfigIssues];
|
|
528
455
|
}
|
|
529
456
|
|
|
530
|
-
private loadGlobalConfig():
|
|
457
|
+
private loadGlobalConfig(): ScopeConfig {
|
|
531
458
|
const stamp = getFileStamp(this.globalConfigPath);
|
|
532
459
|
if (this.globalConfigCache?.stamp === stamp) {
|
|
533
460
|
return this.globalConfigCache.value;
|
|
@@ -536,7 +463,7 @@ export class PermissionManager {
|
|
|
536
463
|
const { config, issues } = loadUnifiedConfig(this.globalConfigPath);
|
|
537
464
|
this.accumulateConfigIssues(issues);
|
|
538
465
|
|
|
539
|
-
const value:
|
|
466
|
+
const value: ScopeConfig = {
|
|
540
467
|
defaultPolicy: normalizePolicy(config.defaultPolicy),
|
|
541
468
|
tools: config.tools || {},
|
|
542
469
|
bash: config.bash || {},
|
|
@@ -549,7 +476,7 @@ export class PermissionManager {
|
|
|
549
476
|
return value;
|
|
550
477
|
}
|
|
551
478
|
|
|
552
|
-
private loadProjectGlobalConfig():
|
|
479
|
+
private loadProjectGlobalConfig(): ScopeConfig {
|
|
553
480
|
if (!this.projectGlobalConfigPath) {
|
|
554
481
|
return {};
|
|
555
482
|
}
|
|
@@ -562,7 +489,7 @@ export class PermissionManager {
|
|
|
562
489
|
const { config, issues } = loadUnifiedConfig(this.projectGlobalConfigPath);
|
|
563
490
|
this.accumulateConfigIssues(issues);
|
|
564
491
|
|
|
565
|
-
const value:
|
|
492
|
+
const value: ScopeConfig = {
|
|
566
493
|
defaultPolicy: config.defaultPolicy,
|
|
567
494
|
tools: config.tools,
|
|
568
495
|
bash: config.bash,
|
|
@@ -575,11 +502,11 @@ export class PermissionManager {
|
|
|
575
502
|
return value;
|
|
576
503
|
}
|
|
577
504
|
|
|
578
|
-
private
|
|
505
|
+
private loadScopeConfigFrom(
|
|
579
506
|
dir: string | null,
|
|
580
|
-
cache: Map<string, FileCacheEntry<
|
|
507
|
+
cache: Map<string, FileCacheEntry<ScopeConfig>>,
|
|
581
508
|
agentName?: string,
|
|
582
|
-
):
|
|
509
|
+
): ScopeConfig {
|
|
583
510
|
if (!dir || !agentName) {
|
|
584
511
|
return {};
|
|
585
512
|
}
|
|
@@ -591,7 +518,7 @@ export class PermissionManager {
|
|
|
591
518
|
return cached.value;
|
|
592
519
|
}
|
|
593
520
|
|
|
594
|
-
let value:
|
|
521
|
+
let value: ScopeConfig;
|
|
595
522
|
try {
|
|
596
523
|
const markdown = readFileSync(filePath, "utf-8");
|
|
597
524
|
const frontmatter = extractFrontmatter(markdown);
|
|
@@ -611,54 +538,22 @@ export class PermissionManager {
|
|
|
611
538
|
return value;
|
|
612
539
|
}
|
|
613
540
|
|
|
614
|
-
private
|
|
615
|
-
return this.
|
|
541
|
+
private loadScopeConfig(agentName?: string): ScopeConfig {
|
|
542
|
+
return this.loadScopeConfigFrom(
|
|
616
543
|
this.agentsDir,
|
|
617
544
|
this.agentConfigCache,
|
|
618
545
|
agentName,
|
|
619
546
|
);
|
|
620
547
|
}
|
|
621
548
|
|
|
622
|
-
private
|
|
623
|
-
return this.
|
|
549
|
+
private loadProjectScopeConfig(agentName?: string): ScopeConfig {
|
|
550
|
+
return this.loadScopeConfigFrom(
|
|
624
551
|
this.projectAgentsDir,
|
|
625
552
|
this.projectAgentConfigCache,
|
|
626
553
|
agentName,
|
|
627
554
|
);
|
|
628
555
|
}
|
|
629
556
|
|
|
630
|
-
private mergePermissions(
|
|
631
|
-
globalConfig: GlobalPermissionConfig,
|
|
632
|
-
agentConfig: AgentPermissions,
|
|
633
|
-
): GlobalPermissionConfig {
|
|
634
|
-
return {
|
|
635
|
-
defaultPolicy: {
|
|
636
|
-
...globalConfig.defaultPolicy,
|
|
637
|
-
...(agentConfig.defaultPolicy || {}),
|
|
638
|
-
},
|
|
639
|
-
tools: {
|
|
640
|
-
...(globalConfig.tools || {}),
|
|
641
|
-
...(agentConfig.tools || {}),
|
|
642
|
-
},
|
|
643
|
-
bash: {
|
|
644
|
-
...(globalConfig.bash || {}),
|
|
645
|
-
...(agentConfig.bash || {}),
|
|
646
|
-
},
|
|
647
|
-
mcp: {
|
|
648
|
-
...(globalConfig.mcp || {}),
|
|
649
|
-
...(agentConfig.mcp || {}),
|
|
650
|
-
},
|
|
651
|
-
skills: {
|
|
652
|
-
...(globalConfig.skills || {}),
|
|
653
|
-
...(agentConfig.skills || {}),
|
|
654
|
-
},
|
|
655
|
-
special: {
|
|
656
|
-
...(globalConfig.special || {}),
|
|
657
|
-
...(agentConfig.special || {}),
|
|
658
|
-
},
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
|
|
662
557
|
getResolvedPolicyPaths(): ResolvedPolicyPaths {
|
|
663
558
|
return {
|
|
664
559
|
globalConfigPath: this.globalConfigPath,
|
|
@@ -701,67 +596,57 @@ export class PermissionManager {
|
|
|
701
596
|
|
|
702
597
|
const globalConfig = this.loadGlobalConfig();
|
|
703
598
|
const projectConfig = this.loadProjectGlobalConfig();
|
|
704
|
-
const agentConfig = this.
|
|
705
|
-
const projectAgentConfig = this.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
599
|
+
const agentConfig = this.loadScopeConfig(agentName);
|
|
600
|
+
const projectAgentConfig = this.loadProjectScopeConfig(agentName);
|
|
601
|
+
|
|
602
|
+
// Normalize each scope into a flat Ruleset and concatenate.
|
|
603
|
+
// Later scopes appear last → higher priority via last-match-wins.
|
|
604
|
+
const rules: Ruleset = [
|
|
605
|
+
...normalizeConfig(globalConfig),
|
|
606
|
+
...normalizeConfig(projectConfig),
|
|
607
|
+
...normalizeConfig(agentConfig),
|
|
608
|
+
...normalizeConfig(projectAgentConfig),
|
|
609
|
+
];
|
|
610
|
+
|
|
611
|
+
// Merge defaults separately (shallow spread, same precedence order).
|
|
612
|
+
const defaults = mergeDefaults(
|
|
613
|
+
globalConfig.defaultPolicy,
|
|
614
|
+
projectConfig.defaultPolicy,
|
|
615
|
+
agentConfig.defaultPolicy,
|
|
616
|
+
projectAgentConfig.defaultPolicy,
|
|
714
617
|
);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
618
|
+
|
|
619
|
+
// tools.bash / tools.mcp are fallback overrides, not catch-all rules.
|
|
620
|
+
// Extract with last-scope-wins precedence.
|
|
621
|
+
const toolBash =
|
|
622
|
+
projectAgentConfig.tools?.bash ??
|
|
623
|
+
agentConfig.tools?.bash ??
|
|
624
|
+
projectConfig.tools?.bash ??
|
|
625
|
+
globalConfig.tools?.bash;
|
|
626
|
+
const bashDefault = toolBash ?? defaults.bash;
|
|
627
|
+
|
|
628
|
+
const mcpToolLevel =
|
|
629
|
+
projectAgentConfig.tools?.mcp ??
|
|
630
|
+
agentConfig.tools?.mcp ??
|
|
631
|
+
projectConfig.tools?.mcp ??
|
|
632
|
+
globalConfig.tools?.mcp;
|
|
633
|
+
|
|
634
|
+
const hasAnyMcpAllowRule = rules.some(
|
|
635
|
+
(r) => r.surface === "mcp" && r.action === "allow",
|
|
728
636
|
);
|
|
637
|
+
|
|
729
638
|
const value: ResolvedPermissions = {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
merged,
|
|
733
|
-
compiledBash,
|
|
639
|
+
rules,
|
|
640
|
+
defaults,
|
|
734
641
|
bashDefault,
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
projectConfig.special,
|
|
738
|
-
agentConfig.special,
|
|
739
|
-
projectAgentConfig.special,
|
|
740
|
-
),
|
|
741
|
-
compiledSkills: compilePermissionPatternsFromSources(
|
|
742
|
-
globalConfig.skills,
|
|
743
|
-
projectConfig.skills,
|
|
744
|
-
agentConfig.skills,
|
|
745
|
-
projectAgentConfig.skills,
|
|
746
|
-
),
|
|
747
|
-
compiledMcp: compilePermissionPatternsFromSources(
|
|
748
|
-
globalConfig.mcp,
|
|
749
|
-
projectConfig.mcp,
|
|
750
|
-
agentConfig.mcp,
|
|
751
|
-
projectAgentConfig.mcp,
|
|
752
|
-
),
|
|
753
|
-
bashFilter: new BashFilter(compiledBash, bashDefault),
|
|
642
|
+
mcpToolLevel,
|
|
643
|
+
hasAnyMcpAllowRule,
|
|
754
644
|
};
|
|
755
645
|
|
|
756
646
|
this.resolvedPermissionsCache.set(cacheKey, { stamp, value });
|
|
757
647
|
return value;
|
|
758
648
|
}
|
|
759
649
|
|
|
760
|
-
getBashPermissions(agentName?: string): BashPermissions {
|
|
761
|
-
const { merged } = this.resolvePermissions(agentName);
|
|
762
|
-
return merged.bash || {};
|
|
763
|
-
}
|
|
764
|
-
|
|
765
650
|
private getConfiguredMcpServerNames(): readonly string[] {
|
|
766
651
|
if (this.configuredMcpServerNamesOverride) {
|
|
767
652
|
return this.configuredMcpServerNamesOverride;
|
|
@@ -785,34 +670,37 @@ export class PermissionManager {
|
|
|
785
670
|
* This is used for tool injection decisions where we need to know if a tool is allowed/denied
|
|
786
671
|
* at the tool level before checking specific command permissions.
|
|
787
672
|
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
673
|
+
* With tool-name-as-surface normalization, tools.bash becomes a bash catch-all
|
|
674
|
+
* { surface: "bash", pattern: "*", action } so getToolPermission("bash")
|
|
675
|
+
* naturally picks it up via evaluate("bash", "*", rules).
|
|
790
676
|
*
|
|
791
677
|
* @param toolName - The name of the tool (for example "bash", "read", or a third-party tool name)
|
|
792
678
|
* @param agentName - Optional agent name to check agent-specific permissions
|
|
793
679
|
* @returns The permission state for the tool at the tool level
|
|
794
680
|
*/
|
|
795
681
|
getToolPermission(toolName: string, agentName?: string): PermissionState {
|
|
796
|
-
const {
|
|
682
|
+
const { rules, defaults, bashDefault, mcpToolLevel } =
|
|
683
|
+
this.resolvePermissions(agentName);
|
|
797
684
|
const normalizedToolName = toolName.trim();
|
|
798
685
|
|
|
686
|
+
// Special keys use the special default.
|
|
799
687
|
if (SPECIAL_PERMISSION_KEYS.has(normalizedToolName)) {
|
|
800
|
-
|
|
688
|
+
const rule = evaluate("special", normalizedToolName, rules);
|
|
689
|
+
if (rules.includes(rule)) return rule.action;
|
|
690
|
+
return defaults.special;
|
|
801
691
|
}
|
|
802
692
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
693
|
+
// Bash and MCP have dedicated fallback overrides from tools.bash / tools.mcp.
|
|
694
|
+
if (normalizedToolName === "bash") return bashDefault;
|
|
695
|
+
if (normalizedToolName === "mcp") return mcpToolLevel ?? defaults.mcp;
|
|
806
696
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
697
|
+
// Skills use the skills default.
|
|
698
|
+
if (normalizedToolName === "skill") return defaults.skills;
|
|
810
699
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
return merged.tools?.[normalizedToolName] || merged.defaultPolicy.tools;
|
|
700
|
+
// Tool-name surfaces: check rules, fall back to tools default.
|
|
701
|
+
const rule = evaluate(normalizedToolName, "*", rules);
|
|
702
|
+
if (rules.includes(rule)) return rule.action;
|
|
703
|
+
return defaults.tools;
|
|
816
704
|
}
|
|
817
705
|
|
|
818
706
|
checkPermission(
|
|
@@ -820,56 +708,48 @@ export class PermissionManager {
|
|
|
820
708
|
input: unknown,
|
|
821
709
|
agentName?: string,
|
|
822
710
|
): PermissionCheckResult {
|
|
823
|
-
const {
|
|
824
|
-
|
|
825
|
-
merged,
|
|
826
|
-
compiledBash,
|
|
827
|
-
bashDefault,
|
|
828
|
-
compiledSpecial,
|
|
829
|
-
compiledSkills,
|
|
830
|
-
compiledMcp,
|
|
831
|
-
bashFilter: _bashFilter,
|
|
832
|
-
} = this.resolvePermissions(agentName);
|
|
711
|
+
const { rules, defaults, bashDefault, mcpToolLevel, hasAnyMcpAllowRule } =
|
|
712
|
+
this.resolvePermissions(agentName);
|
|
833
713
|
const normalizedToolName = toolName.trim();
|
|
834
714
|
|
|
715
|
+
// --- Special surfaces (external_directory) ---
|
|
835
716
|
if (SPECIAL_PERMISSION_KEYS.has(normalizedToolName)) {
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
const explicit = specialRuleset.includes(rule);
|
|
717
|
+
const rule = evaluate("special", normalizedToolName, rules);
|
|
718
|
+
const explicit = rules.includes(rule);
|
|
839
719
|
return {
|
|
840
720
|
toolName,
|
|
841
|
-
state: explicit ? rule.action :
|
|
721
|
+
state: explicit ? rule.action : defaults.special,
|
|
842
722
|
matchedPattern: explicit ? rule.pattern : undefined,
|
|
843
723
|
source: "special",
|
|
844
724
|
};
|
|
845
725
|
}
|
|
846
726
|
|
|
727
|
+
// --- Skills ---
|
|
847
728
|
if (normalizedToolName === "skill") {
|
|
848
729
|
const skillName = toRecord(input).name;
|
|
849
730
|
if (typeof skillName === "string") {
|
|
850
|
-
const
|
|
851
|
-
const
|
|
852
|
-
const explicit = skillRuleset.includes(rule);
|
|
731
|
+
const rule = evaluate("skill", skillName, rules);
|
|
732
|
+
const explicit = rules.includes(rule);
|
|
853
733
|
return {
|
|
854
734
|
toolName,
|
|
855
|
-
state: explicit ? rule.action :
|
|
735
|
+
state: explicit ? rule.action : defaults.skills,
|
|
856
736
|
matchedPattern: explicit ? rule.pattern : undefined,
|
|
857
|
-
source: "skill",
|
|
737
|
+
source: explicit ? "skill" : "skill",
|
|
858
738
|
};
|
|
859
739
|
}
|
|
860
740
|
return {
|
|
861
741
|
toolName,
|
|
862
|
-
state:
|
|
742
|
+
state: defaults.skills,
|
|
863
743
|
source: "skill",
|
|
864
744
|
};
|
|
865
745
|
}
|
|
866
746
|
|
|
747
|
+
// --- Bash ---
|
|
867
748
|
if (normalizedToolName === "bash") {
|
|
868
749
|
const record = toRecord(input);
|
|
869
750
|
const command = typeof record.command === "string" ? record.command : "";
|
|
870
|
-
const
|
|
871
|
-
const
|
|
872
|
-
const explicit = bashRuleset.includes(rule);
|
|
751
|
+
const rule = evaluate("bash", command, rules);
|
|
752
|
+
const explicit = rules.includes(rule);
|
|
873
753
|
return {
|
|
874
754
|
toolName,
|
|
875
755
|
state: explicit ? rule.action : bashDefault,
|
|
@@ -879,6 +759,7 @@ export class PermissionManager {
|
|
|
879
759
|
};
|
|
880
760
|
}
|
|
881
761
|
|
|
762
|
+
// --- MCP ---
|
|
882
763
|
if (normalizedToolName === "mcp") {
|
|
883
764
|
const mcpTargets = [
|
|
884
765
|
...createMcpPermissionTargets(
|
|
@@ -888,44 +769,38 @@ export class PermissionManager {
|
|
|
888
769
|
"mcp",
|
|
889
770
|
];
|
|
890
771
|
const fallbackTarget = mcpTargets[0] || "mcp";
|
|
891
|
-
const toolLevelMcpState = merged.tools?.mcp;
|
|
892
772
|
|
|
893
|
-
|
|
894
|
-
let mcpExplicitMatch: { target: string; rule: Rule } | null = null;
|
|
773
|
+
// Try each candidate target against the merged rules.
|
|
895
774
|
for (const target of mcpTargets) {
|
|
896
|
-
const rule = evaluate("mcp", target,
|
|
897
|
-
if (
|
|
898
|
-
|
|
899
|
-
|
|
775
|
+
const rule = evaluate("mcp", target, rules);
|
|
776
|
+
if (rules.includes(rule)) {
|
|
777
|
+
return {
|
|
778
|
+
toolName,
|
|
779
|
+
state: rule.action,
|
|
780
|
+
matchedPattern: rule.pattern,
|
|
781
|
+
target,
|
|
782
|
+
source: "mcp",
|
|
783
|
+
};
|
|
900
784
|
}
|
|
901
785
|
}
|
|
902
|
-
if (mcpExplicitMatch) {
|
|
903
|
-
return {
|
|
904
|
-
toolName,
|
|
905
|
-
state: mcpExplicitMatch.rule.action,
|
|
906
|
-
matchedPattern: mcpExplicitMatch.rule.pattern,
|
|
907
|
-
target: mcpExplicitMatch.target,
|
|
908
|
-
source: "mcp",
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
786
|
|
|
912
|
-
|
|
787
|
+
// tools.mcp fallback (e.g. tools: { mcp: "allow" }).
|
|
788
|
+
if (mcpToolLevel) {
|
|
913
789
|
return {
|
|
914
790
|
toolName,
|
|
915
|
-
state:
|
|
791
|
+
state: mcpToolLevel,
|
|
916
792
|
target: fallbackTarget,
|
|
917
793
|
source: "tool",
|
|
918
794
|
};
|
|
919
795
|
}
|
|
920
796
|
|
|
797
|
+
// Baseline auto-allow: if this is a metadata operation and at least one
|
|
798
|
+
// MCP rule allows something (or the default is allow), auto-allow.
|
|
921
799
|
const baselineTarget = mcpTargets.find((target) =>
|
|
922
800
|
MCP_BASELINE_TARGETS.has(target),
|
|
923
801
|
);
|
|
924
802
|
if (baselineTarget) {
|
|
925
|
-
|
|
926
|
-
(state) => state === "allow",
|
|
927
|
-
);
|
|
928
|
-
if (hasAnyMcpAllowRule || merged.defaultPolicy.mcp === "allow") {
|
|
803
|
+
if (hasAnyMcpAllowRule || defaults.mcp === "allow") {
|
|
929
804
|
return {
|
|
930
805
|
toolName,
|
|
931
806
|
state: "allow",
|
|
@@ -937,37 +812,35 @@ export class PermissionManager {
|
|
|
937
812
|
|
|
938
813
|
return {
|
|
939
814
|
toolName,
|
|
940
|
-
state:
|
|
815
|
+
state: defaults.mcp,
|
|
941
816
|
target: fallbackTarget,
|
|
942
817
|
source: "default",
|
|
943
818
|
};
|
|
944
819
|
}
|
|
945
820
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
);
|
|
949
|
-
const toolRule = evaluate("tool", normalizedToolName, toolRuleset);
|
|
950
|
-
const explicitTool = toolRuleset.includes(toolRule);
|
|
821
|
+
// --- Tools (read, write, edit, grep, find, ls, extension tools) ---
|
|
822
|
+
const rule = evaluate(normalizedToolName, "*", rules);
|
|
823
|
+
const explicit = rules.includes(rule);
|
|
951
824
|
|
|
952
825
|
if (BUILT_IN_TOOL_PERMISSION_NAMES.has(normalizedToolName)) {
|
|
953
826
|
return {
|
|
954
827
|
toolName,
|
|
955
|
-
state:
|
|
828
|
+
state: explicit ? rule.action : defaults.tools,
|
|
956
829
|
source: "tool",
|
|
957
830
|
};
|
|
958
831
|
}
|
|
959
832
|
|
|
960
|
-
if (
|
|
833
|
+
if (explicit) {
|
|
961
834
|
return {
|
|
962
835
|
toolName,
|
|
963
|
-
state:
|
|
836
|
+
state: rule.action,
|
|
964
837
|
source: "tool",
|
|
965
838
|
};
|
|
966
839
|
}
|
|
967
840
|
|
|
968
841
|
return {
|
|
969
842
|
toolName,
|
|
970
|
-
state:
|
|
843
|
+
state: defaults.tools,
|
|
971
844
|
source: "default",
|
|
972
845
|
};
|
|
973
846
|
}
|
package/src/rule.ts
CHANGED
|
@@ -14,37 +14,21 @@ export interface Rule {
|
|
|
14
14
|
/** An ordered list of rules. Later rules take priority (last-match-wins). */
|
|
15
15
|
export type Ruleset = Rule[];
|
|
16
16
|
|
|
17
|
-
const SURFACE_DEFAULTS: Record<string, PermissionState> = {
|
|
18
|
-
tools: "ask",
|
|
19
|
-
bash: "ask",
|
|
20
|
-
mcp: "ask",
|
|
21
|
-
skill: "ask",
|
|
22
|
-
special: "ask",
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Returns the default action for a surface when no rules match.
|
|
27
|
-
* Defaults to "ask" for unknown surfaces (least privilege).
|
|
28
|
-
*/
|
|
29
|
-
export function getDefaultAction(surface: string): PermissionState {
|
|
30
|
-
return SURFACE_DEFAULTS[surface] ?? "ask";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
17
|
/**
|
|
34
18
|
* Pure permission evaluation.
|
|
35
19
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* rulesets / later entries have higher priority).
|
|
20
|
+
* Returns the last rule in `rules` whose surface and pattern both
|
|
21
|
+
* wildcard-match the supplied values (last-match-wins).
|
|
39
22
|
*
|
|
40
|
-
* When no rule matches, returns a synthetic rule
|
|
23
|
+
* When no rule matches, returns a synthetic rule with `defaultAction`
|
|
24
|
+
* (defaults to "ask" — least privilege).
|
|
41
25
|
*/
|
|
42
26
|
export function evaluate(
|
|
43
27
|
surface: string,
|
|
44
28
|
pattern: string,
|
|
45
|
-
|
|
29
|
+
rules: Ruleset,
|
|
30
|
+
defaultAction?: PermissionState,
|
|
46
31
|
): Rule {
|
|
47
|
-
const rules = rulesets.flat();
|
|
48
32
|
for (let i = rules.length - 1; i >= 0; i -= 1) {
|
|
49
33
|
const rule = rules[i];
|
|
50
34
|
if (
|
|
@@ -54,5 +38,5 @@ export function evaluate(
|
|
|
54
38
|
return rule;
|
|
55
39
|
}
|
|
56
40
|
}
|
|
57
|
-
return { surface, pattern, action:
|
|
41
|
+
return { surface, pattern, action: defaultAction ?? "ask" };
|
|
58
42
|
}
|