@gotgenes/pi-permission-system 4.9.0 → 5.1.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 +37 -0
- package/package.json +1 -1
- package/src/config-modal.ts +25 -3
- package/src/external-directory.ts +238 -14
- package/src/index.ts +4 -0
- package/src/normalize.ts +2 -2
- package/src/permission-manager.ts +72 -17
- package/src/rule.ts +26 -2
- package/src/session-rules.ts +7 -1
- package/src/synthesize.ts +7 -2
- package/src/tool-input-preview.ts +7 -1
- package/src/types.ts +6 -0
- package/tests/bash-external-directory.test.ts +227 -0
- package/tests/config-modal.test.ts +83 -0
- package/tests/handlers/tool-call.test.ts +2 -1
- package/tests/normalize.test.ts +64 -22
- package/tests/permission-manager-unified.test.ts +215 -0
- package/tests/permission-prompts.test.ts +8 -1
- package/tests/permission-system.test.ts +12 -0
- package/tests/rule.test.ts +76 -8
- package/tests/session-rules.test.ts +7 -1
- package/tests/skill-prompt-sanitizer.test.ts +1 -1
- package/tests/synthesize.test.ts +64 -4
- package/tests/tool-input-preview.test.ts +29 -0
package/src/types.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type PermissionState = "allow" | "deny" | "ask";
|
|
2
2
|
|
|
3
|
+
import type { RuleOrigin } from "./rule";
|
|
4
|
+
|
|
5
|
+
export type { RuleOrigin };
|
|
6
|
+
|
|
3
7
|
/**
|
|
4
8
|
* The on-disk permission shape inside the `"permission"` key.
|
|
5
9
|
* Each key is a surface name; values are either a PermissionState string
|
|
@@ -36,4 +40,6 @@ export interface PermissionCheckResult {
|
|
|
36
40
|
command?: string;
|
|
37
41
|
target?: string;
|
|
38
42
|
source: "tool" | "bash" | "mcp" | "skill" | "special" | "default" | "session";
|
|
43
|
+
/** Which source contributed the winning rule. */
|
|
44
|
+
origin: RuleOrigin;
|
|
39
45
|
}
|
|
@@ -545,6 +545,233 @@ describe("extractExternalPathsFromBashCommand", () => {
|
|
|
545
545
|
});
|
|
546
546
|
});
|
|
547
547
|
|
|
548
|
+
describe("command-aware extraction", () => {
|
|
549
|
+
describe("sed", () => {
|
|
550
|
+
test("issue #91 reproducer: sed address pattern is not flagged", async () => {
|
|
551
|
+
const cmd = `sed -i '' '/source: "tool",/{/origin:/!s/source: "tool",/source: "tool",\n origin: "builtin",/;}' tests/tool-input-preview.test.ts`;
|
|
552
|
+
const result = await extractExternalPathsFromBashCommand(cmd, cwd);
|
|
553
|
+
expect(result).toHaveLength(0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("sed script is skipped but file argument is extracted", async () => {
|
|
557
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
558
|
+
"sed 's/foo/bar/g' /etc/hosts",
|
|
559
|
+
cwd,
|
|
560
|
+
);
|
|
561
|
+
expect(result).toContain("/etc/hosts");
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("sed address pattern starting with / is skipped", async () => {
|
|
565
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
566
|
+
"sed '/pattern/d' /etc/hosts",
|
|
567
|
+
cwd,
|
|
568
|
+
);
|
|
569
|
+
expect(result).toContain("/etc/hosts");
|
|
570
|
+
expect(result).toHaveLength(1);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("sed with only in-CWD file returns empty", async () => {
|
|
574
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
575
|
+
"sed 's/foo/bar/' src/index.ts",
|
|
576
|
+
cwd,
|
|
577
|
+
);
|
|
578
|
+
expect(result).toHaveLength(0);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("sed -e: script consumed by flag, file extracted", async () => {
|
|
582
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
583
|
+
"sed -e 's/foo/bar/' /etc/hosts",
|
|
584
|
+
cwd,
|
|
585
|
+
);
|
|
586
|
+
expect(result).toContain("/etc/hosts");
|
|
587
|
+
expect(result).toHaveLength(1);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("sed -n: regular flag does not consume next arg", async () => {
|
|
591
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
592
|
+
"sed -n '/pattern/p' /etc/hosts",
|
|
593
|
+
cwd,
|
|
594
|
+
);
|
|
595
|
+
expect(result).toContain("/etc/hosts");
|
|
596
|
+
expect(result).toHaveLength(1);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("sed -f: script file is extracted as path", async () => {
|
|
600
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
601
|
+
"sed -f /etc/sed-script.sed input.txt",
|
|
602
|
+
cwd,
|
|
603
|
+
);
|
|
604
|
+
expect(result).toContain("/etc/sed-script.sed");
|
|
605
|
+
expect(result).toHaveLength(1);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("sed -i '': extension consumed, script skipped, file extracted", async () => {
|
|
609
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
610
|
+
"sed -i '' 's/foo/bar/' /etc/hosts",
|
|
611
|
+
cwd,
|
|
612
|
+
);
|
|
613
|
+
expect(result).toContain("/etc/hosts");
|
|
614
|
+
expect(result).toHaveLength(1);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe("grep", () => {
|
|
619
|
+
test("grep: pattern skipped, file extracted", async () => {
|
|
620
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
621
|
+
"grep '/etc/' /var/log/syslog",
|
|
622
|
+
cwd,
|
|
623
|
+
);
|
|
624
|
+
expect(result).toContain("/var/log/syslog");
|
|
625
|
+
expect(result).toHaveLength(1);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("grep -e: pattern consumed by flag, file extracted", async () => {
|
|
629
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
630
|
+
"grep -e '/etc/' /var/log/syslog",
|
|
631
|
+
cwd,
|
|
632
|
+
);
|
|
633
|
+
expect(result).toContain("/var/log/syslog");
|
|
634
|
+
expect(result).toHaveLength(1);
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe("awk", () => {
|
|
639
|
+
test("awk: program skipped, file extracted", async () => {
|
|
640
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
641
|
+
"awk '{print}' /etc/hosts",
|
|
642
|
+
cwd,
|
|
643
|
+
);
|
|
644
|
+
expect(result).toContain("/etc/hosts");
|
|
645
|
+
expect(result).toHaveLength(1);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("awk -F: separator consumed, program skipped, file extracted", async () => {
|
|
649
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
650
|
+
"awk -F: '{print $1}' /etc/passwd",
|
|
651
|
+
cwd,
|
|
652
|
+
);
|
|
653
|
+
expect(result).toContain("/etc/passwd");
|
|
654
|
+
expect(result).toHaveLength(1);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe("rg", () => {
|
|
659
|
+
test("rg: pattern skipped, path extracted", async () => {
|
|
660
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
661
|
+
"rg '/usr/local' /etc/profile.d/",
|
|
662
|
+
cwd,
|
|
663
|
+
);
|
|
664
|
+
expect(result).toContain("/etc/profile.d");
|
|
665
|
+
expect(result).toHaveLength(1);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("rg -e: pattern consumed by flag, path extracted", async () => {
|
|
669
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
670
|
+
"rg -e '/usr/local' /etc/profile.d/",
|
|
671
|
+
cwd,
|
|
672
|
+
);
|
|
673
|
+
expect(result).toContain("/etc/profile.d");
|
|
674
|
+
expect(result).toHaveLength(1);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
describe("sd", () => {
|
|
679
|
+
test("sd: both pattern positionals skipped, file extracted", async () => {
|
|
680
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
681
|
+
"sd '/usr/local/bin' '/opt/bin' /etc/profile",
|
|
682
|
+
cwd,
|
|
683
|
+
);
|
|
684
|
+
expect(result).toContain("/etc/profile");
|
|
685
|
+
expect(result).toHaveLength(1);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("sd with only in-CWD file returns empty", async () => {
|
|
689
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
690
|
+
"sd 'foo' 'bar' src/index.ts",
|
|
691
|
+
cwd,
|
|
692
|
+
);
|
|
693
|
+
expect(result).toHaveLength(0);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("unknown commands", () => {
|
|
698
|
+
test("unknown command: all args go through generic extraction", async () => {
|
|
699
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
700
|
+
"some-tool /etc/hosts",
|
|
701
|
+
cwd,
|
|
702
|
+
);
|
|
703
|
+
expect(result).toContain("/etc/hosts");
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
describe("edge cases", () => {
|
|
708
|
+
test("full-path command invocation: /usr/bin/sed", async () => {
|
|
709
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
710
|
+
"/usr/bin/sed 's/foo/bar/' /etc/hosts",
|
|
711
|
+
cwd,
|
|
712
|
+
);
|
|
713
|
+
expect(result).toContain("/etc/hosts");
|
|
714
|
+
expect(result).toHaveLength(1);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("-- end-of-flags: all remaining args are positional files", async () => {
|
|
718
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
719
|
+
"grep -- '/etc/' /var/log/syslog",
|
|
720
|
+
cwd,
|
|
721
|
+
);
|
|
722
|
+
// After --, '/etc/' is the pattern positional, /var/log/syslog is a file
|
|
723
|
+
expect(result).toContain("/var/log/syslog");
|
|
724
|
+
expect(result).toHaveLength(1);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
test("redirect target still extracted for pattern-first command", async () => {
|
|
728
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
729
|
+
"sed 's/foo/bar/' input.txt > /tmp/output.txt",
|
|
730
|
+
cwd,
|
|
731
|
+
);
|
|
732
|
+
expect(result).toContain("/tmp/output.txt");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test("pipeline: sed piped to cat with external path", async () => {
|
|
736
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
737
|
+
"sed 's/foo/bar/' src/file.ts | cat /etc/hosts",
|
|
738
|
+
cwd,
|
|
739
|
+
);
|
|
740
|
+
expect(result).toContain("/etc/hosts");
|
|
741
|
+
expect(result).toHaveLength(1);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("command substitution inside pattern-first command", async () => {
|
|
745
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
746
|
+
"grep 'pattern' $(cat /etc/file-list)",
|
|
747
|
+
cwd,
|
|
748
|
+
);
|
|
749
|
+
// /etc/file-list is an argument to cat inside command substitution
|
|
750
|
+
expect(result).toContain("/etc/file-list");
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
describe("known limitations", () => {
|
|
755
|
+
test("sed -i without extension (GNU sed): /etc/hosts is missed (false negative)", async () => {
|
|
756
|
+
// GNU sed treats -i as a flag with no argument, so 's/foo/bar/' is
|
|
757
|
+
// the inline script and /etc/hosts is the input file. Our logic
|
|
758
|
+
// treats -i as arg-consuming (correct for BSD sed -i ''), so it
|
|
759
|
+
// consumes the script as the -i extension and /etc/hosts becomes
|
|
760
|
+
// the first positional — which is skipped as the inline script.
|
|
761
|
+
// This is a known false negative. The bash permission gate still
|
|
762
|
+
// applies, so external access is not silently allowed.
|
|
763
|
+
const result = await extractExternalPathsFromBashCommand(
|
|
764
|
+
"sed -i 's/foo/bar/' /etc/hosts",
|
|
765
|
+
cwd,
|
|
766
|
+
);
|
|
767
|
+
// Ideally this would detect /etc/hosts, but position tracking
|
|
768
|
+
// treats it as the inline script. Assert current behavior so
|
|
769
|
+
// a future fix can flip this expectation.
|
|
770
|
+
expect(result).toHaveLength(0);
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
548
775
|
describe("regex patterns are not mistaken for paths", () => {
|
|
549
776
|
test("grep -v with //.*pattern is not flagged", async () => {
|
|
550
777
|
const result = await extractExternalPathsFromBashCommand(
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type PermissionSystemExtensionConfig,
|
|
11
11
|
savePermissionSystemConfig,
|
|
12
12
|
} from "../src/extension-config";
|
|
13
|
+
import type { Rule } from "../src/rule";
|
|
13
14
|
|
|
14
15
|
vi.mock("@mariozechner/pi-coding-agent", () => ({
|
|
15
16
|
getSettingsListTheme: () => ({}),
|
|
@@ -234,3 +235,85 @@ test("permission-system command handlers manage config summary, persistence, and
|
|
|
234
235
|
rmSync(baseDir, { recursive: true, force: true });
|
|
235
236
|
}
|
|
236
237
|
});
|
|
238
|
+
|
|
239
|
+
test("show output includes rule origins when getComposedRules is provided", async () => {
|
|
240
|
+
const config = { ...DEFAULT_EXTENSION_CONFIG };
|
|
241
|
+
const composedRules: Rule[] = [
|
|
242
|
+
{
|
|
243
|
+
surface: "read",
|
|
244
|
+
pattern: "*",
|
|
245
|
+
action: "allow",
|
|
246
|
+
layer: "config",
|
|
247
|
+
origin: "global",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
surface: "bash",
|
|
251
|
+
pattern: "rm *",
|
|
252
|
+
action: "deny",
|
|
253
|
+
layer: "config",
|
|
254
|
+
origin: "project",
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
const controller = {
|
|
259
|
+
getConfig: () => config,
|
|
260
|
+
setConfig: () => {},
|
|
261
|
+
getConfigPath: () => "/fake/config.json",
|
|
262
|
+
getComposedRules: () => composedRules,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
let definition: {
|
|
266
|
+
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
267
|
+
} | null = null;
|
|
268
|
+
|
|
269
|
+
registerPermissionSystemCommand(
|
|
270
|
+
{
|
|
271
|
+
registerCommand(_name: string, nextDef: typeof definition) {
|
|
272
|
+
definition = nextDef;
|
|
273
|
+
},
|
|
274
|
+
} as never,
|
|
275
|
+
controller as never,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const ctx = createCommandContext(true);
|
|
279
|
+
await definition!.handler("show", ctx.ctx);
|
|
280
|
+
const msg = lastNotification(ctx.notifications).message;
|
|
281
|
+
|
|
282
|
+
assert.ok(msg.includes("global"), `expected 'global' in: ${msg}`);
|
|
283
|
+
assert.ok(msg.includes("project"), `expected 'project' in: ${msg}`);
|
|
284
|
+
assert.ok(msg.includes("read"), `expected 'read' in: ${msg}`);
|
|
285
|
+
assert.ok(msg.includes("bash"), `expected 'bash' in: ${msg}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("show output omits rule summary when getComposedRules is not provided", async () => {
|
|
289
|
+
const config = { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true };
|
|
290
|
+
|
|
291
|
+
const controller = {
|
|
292
|
+
getConfig: () => config,
|
|
293
|
+
setConfig: () => {},
|
|
294
|
+
getConfigPath: () => "/fake/config.json",
|
|
295
|
+
// no getComposedRules
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
let definition: {
|
|
299
|
+
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
|
300
|
+
} | null = null;
|
|
301
|
+
|
|
302
|
+
registerPermissionSystemCommand(
|
|
303
|
+
{
|
|
304
|
+
registerCommand(_name: string, nextDef: typeof definition) {
|
|
305
|
+
definition = nextDef;
|
|
306
|
+
},
|
|
307
|
+
} as never,
|
|
308
|
+
controller as never,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const ctx = createCommandContext(true);
|
|
312
|
+
await definition!.handler("show", ctx.ctx);
|
|
313
|
+
const msg = lastNotification(ctx.notifications).message;
|
|
314
|
+
|
|
315
|
+
// Config knobs still present.
|
|
316
|
+
assert.ok(msg.includes("yoloMode=on"), `expected yoloMode=on in: ${msg}`);
|
|
317
|
+
// No rule annotation lines.
|
|
318
|
+
assert.ok(!msg.includes("(global)"), `unexpected '(global)' in: ${msg}`);
|
|
319
|
+
});
|
|
@@ -52,7 +52,7 @@ function makeToolCallEvent(
|
|
|
52
52
|
function makePermissionResult(
|
|
53
53
|
state: "allow" | "deny" | "ask",
|
|
54
54
|
): PermissionCheckResult {
|
|
55
|
-
return { state, toolName: "read", source: "tool" };
|
|
55
|
+
return { state, toolName: "read", source: "tool", origin: "builtin" };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function makeRuntime(
|
|
@@ -761,6 +761,7 @@ describe("handleToolCall — session recording on approved_for_session", () => {
|
|
|
761
761
|
state: "ask",
|
|
762
762
|
toolName: "read",
|
|
763
763
|
source: "tool",
|
|
764
|
+
origin: "builtin",
|
|
764
765
|
}),
|
|
765
766
|
} as unknown as ExtensionRuntime["permissionManager"],
|
|
766
767
|
sessionRules,
|
package/tests/normalize.test.ts
CHANGED
|
@@ -6,27 +6,34 @@ describe("normalizeFlatConfig", () => {
|
|
|
6
6
|
test("string value produces a single catch-all rule for the surface", () => {
|
|
7
7
|
const result = normalizeFlatConfig({ read: "allow" });
|
|
8
8
|
expect(result).toEqual([
|
|
9
|
-
{ surface: "read", pattern: "*", action: "allow" },
|
|
9
|
+
{ surface: "read", pattern: "*", action: "allow", origin: "builtin" },
|
|
10
10
|
]);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
test("string shorthand works for multiple surfaces", () => {
|
|
14
14
|
const result = normalizeFlatConfig({ read: "allow", write: "deny" });
|
|
15
15
|
expect(result).toEqual([
|
|
16
|
-
{ surface: "read", pattern: "*", action: "allow" },
|
|
17
|
-
{ surface: "write", pattern: "*", action: "deny" },
|
|
16
|
+
{ surface: "read", pattern: "*", action: "allow", origin: "builtin" },
|
|
17
|
+
{ surface: "write", pattern: "*", action: "deny", origin: "builtin" },
|
|
18
18
|
]);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
test("universal fallback '*' becomes a catch-all rule with surface '*'", () => {
|
|
22
22
|
const result = normalizeFlatConfig({ "*": "ask" });
|
|
23
|
-
expect(result).toEqual([
|
|
23
|
+
expect(result).toEqual([
|
|
24
|
+
{ surface: "*", pattern: "*", action: "ask", origin: "builtin" },
|
|
25
|
+
]);
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
test("external_directory string shorthand maps directly to its surface", () => {
|
|
27
29
|
const result = normalizeFlatConfig({ external_directory: "ask" });
|
|
28
30
|
expect(result).toEqual([
|
|
29
|
-
{
|
|
31
|
+
{
|
|
32
|
+
surface: "external_directory",
|
|
33
|
+
pattern: "*",
|
|
34
|
+
action: "ask",
|
|
35
|
+
origin: "builtin",
|
|
36
|
+
},
|
|
30
37
|
]);
|
|
31
38
|
});
|
|
32
39
|
|
|
@@ -36,7 +43,7 @@ describe("normalizeFlatConfig", () => {
|
|
|
36
43
|
write: "invalid" as never,
|
|
37
44
|
});
|
|
38
45
|
expect(result).toEqual([
|
|
39
|
-
{ surface: "read", pattern: "*", action: "allow" },
|
|
46
|
+
{ surface: "read", pattern: "*", action: "allow", origin: "builtin" },
|
|
40
47
|
]);
|
|
41
48
|
});
|
|
42
49
|
});
|
|
@@ -47,8 +54,13 @@ describe("normalizeFlatConfig", () => {
|
|
|
47
54
|
bash: { "*": "ask", "git *": "allow" },
|
|
48
55
|
});
|
|
49
56
|
expect(result).toEqual([
|
|
50
|
-
{ surface: "bash", pattern: "*", action: "ask" },
|
|
51
|
-
{
|
|
57
|
+
{ surface: "bash", pattern: "*", action: "ask", origin: "builtin" },
|
|
58
|
+
{
|
|
59
|
+
surface: "bash",
|
|
60
|
+
pattern: "git *",
|
|
61
|
+
action: "allow",
|
|
62
|
+
origin: "builtin",
|
|
63
|
+
},
|
|
52
64
|
]);
|
|
53
65
|
});
|
|
54
66
|
|
|
@@ -57,8 +69,13 @@ describe("normalizeFlatConfig", () => {
|
|
|
57
69
|
mcp: { "*": "ask", mcp_status: "allow" },
|
|
58
70
|
});
|
|
59
71
|
expect(result).toEqual([
|
|
60
|
-
{ surface: "mcp", pattern: "*", action: "ask" },
|
|
61
|
-
{
|
|
72
|
+
{ surface: "mcp", pattern: "*", action: "ask", origin: "builtin" },
|
|
73
|
+
{
|
|
74
|
+
surface: "mcp",
|
|
75
|
+
pattern: "mcp_status",
|
|
76
|
+
action: "allow",
|
|
77
|
+
origin: "builtin",
|
|
78
|
+
},
|
|
62
79
|
]);
|
|
63
80
|
});
|
|
64
81
|
|
|
@@ -67,8 +84,13 @@ describe("normalizeFlatConfig", () => {
|
|
|
67
84
|
skill: { "*": "ask", librarian: "allow" },
|
|
68
85
|
});
|
|
69
86
|
expect(result).toEqual([
|
|
70
|
-
{ surface: "skill", pattern: "*", action: "ask" },
|
|
71
|
-
{
|
|
87
|
+
{ surface: "skill", pattern: "*", action: "ask", origin: "builtin" },
|
|
88
|
+
{
|
|
89
|
+
surface: "skill",
|
|
90
|
+
pattern: "librarian",
|
|
91
|
+
action: "allow",
|
|
92
|
+
origin: "builtin",
|
|
93
|
+
},
|
|
72
94
|
]);
|
|
73
95
|
});
|
|
74
96
|
|
|
@@ -77,7 +99,12 @@ describe("normalizeFlatConfig", () => {
|
|
|
77
99
|
bash: { "git *": "allow", "rm -rf *": "bad" as never },
|
|
78
100
|
});
|
|
79
101
|
expect(result).toEqual([
|
|
80
|
-
{
|
|
102
|
+
{
|
|
103
|
+
surface: "bash",
|
|
104
|
+
pattern: "git *",
|
|
105
|
+
action: "allow",
|
|
106
|
+
origin: "builtin",
|
|
107
|
+
},
|
|
81
108
|
]);
|
|
82
109
|
});
|
|
83
110
|
});
|
|
@@ -94,14 +121,29 @@ describe("normalizeFlatConfig", () => {
|
|
|
94
121
|
external_directory: "ask",
|
|
95
122
|
});
|
|
96
123
|
expect(result).toEqual([
|
|
97
|
-
{ surface: "*", pattern: "*", action: "ask" },
|
|
98
|
-
{ surface: "read", pattern: "*", action: "allow" },
|
|
99
|
-
{ surface: "write", pattern: "*", action: "deny" },
|
|
100
|
-
{ surface: "bash", pattern: "*", action: "ask" },
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
{ surface: "*", pattern: "*", action: "ask", origin: "builtin" },
|
|
125
|
+
{ surface: "read", pattern: "*", action: "allow", origin: "builtin" },
|
|
126
|
+
{ surface: "write", pattern: "*", action: "deny", origin: "builtin" },
|
|
127
|
+
{ surface: "bash", pattern: "*", action: "ask", origin: "builtin" },
|
|
128
|
+
{
|
|
129
|
+
surface: "bash",
|
|
130
|
+
pattern: "git *",
|
|
131
|
+
action: "allow",
|
|
132
|
+
origin: "builtin",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
surface: "mcp",
|
|
136
|
+
pattern: "mcp_status",
|
|
137
|
+
action: "allow",
|
|
138
|
+
origin: "builtin",
|
|
139
|
+
},
|
|
140
|
+
{ surface: "skill", pattern: "*", action: "ask", origin: "builtin" },
|
|
141
|
+
{
|
|
142
|
+
surface: "external_directory",
|
|
143
|
+
pattern: "*",
|
|
144
|
+
action: "ask",
|
|
145
|
+
origin: "builtin",
|
|
146
|
+
},
|
|
105
147
|
]);
|
|
106
148
|
});
|
|
107
149
|
});
|
|
@@ -117,7 +159,7 @@ describe("normalizeFlatConfig", () => {
|
|
|
117
159
|
read: "allow",
|
|
118
160
|
});
|
|
119
161
|
expect(result).toEqual([
|
|
120
|
-
{ surface: "read", pattern: "*", action: "allow" },
|
|
162
|
+
{ surface: "read", pattern: "*", action: "allow", origin: "builtin" },
|
|
121
163
|
]);
|
|
122
164
|
});
|
|
123
165
|
});
|