@gotgenes/pi-permission-system 3.8.0 → 3.10.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/src/runtime.ts CHANGED
@@ -44,7 +44,7 @@ import { createPermissionSystemLogger } from "./logging";
44
44
  import type { PermissionPromptDecision } from "./permission-dialog";
45
45
  import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
46
46
  import { PermissionManager } from "./permission-manager";
47
- import { SessionApprovalCache } from "./session-approval-cache";
47
+ import { SessionRules } from "./session-rules";
48
48
  import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
49
49
  import { syncPermissionSystemStatus } from "./status";
50
50
  import { isSubagentExecutionContext } from "./subagent-context";
@@ -78,7 +78,7 @@ export interface ExtensionRuntime {
78
78
  lastActiveToolsCacheKey: string | null;
79
79
  lastPromptStateCacheKey: string | null;
80
80
  lastConfigWarning: string | null;
81
- readonly sessionApprovalCache: SessionApprovalCache;
81
+ readonly sessionRules: SessionRules;
82
82
 
83
83
  // ── Forwarding polling state ───────────────────────────────────────────
84
84
  permissionForwardingContext: ExtensionContext | null;
@@ -432,7 +432,7 @@ export function createExtensionRuntime(options?: {
432
432
  lastActiveToolsCacheKey: null,
433
433
  lastPromptStateCacheKey: null,
434
434
  lastConfigWarning: null,
435
- sessionApprovalCache: new SessionApprovalCache(),
435
+ sessionRules: new SessionRules(),
436
436
  permissionForwardingContext: null,
437
437
  permissionForwardingTimer: null,
438
438
  isProcessingForwardedRequests: false,
@@ -0,0 +1,54 @@
1
+ import { dirname, sep } from "node:path";
2
+
3
+ import type { Ruleset } from "./rule";
4
+
5
+ /**
6
+ * Ephemeral in-memory store of session-scoped permission approvals.
7
+ *
8
+ * Each approval is stored as a `Rule` with `action: "allow"`, making the
9
+ * ruleset directly usable with `evaluate()` — no custom matching engine needed.
10
+ *
11
+ * Cleared on session_shutdown — never persisted to disk.
12
+ */
13
+ export class SessionRules {
14
+ private rules: Ruleset = [];
15
+
16
+ /** Record a wildcard pattern as approved for the given surface. */
17
+ approve(surface: string, pattern: string): void {
18
+ this.rules.push({ surface, pattern, action: "allow" });
19
+ }
20
+
21
+ /** Return a defensive copy of the current session ruleset. */
22
+ getRuleset(): Ruleset {
23
+ return [...this.rules];
24
+ }
25
+
26
+ /** Remove all session approvals. */
27
+ clear(): void {
28
+ this.rules = [];
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Derive the wildcard glob pattern to approve from a normalized path.
34
+ *
35
+ * Returns `<parent-dir>/*` so that `evaluate()` / `wildcardMatch()` matches
36
+ * all paths under the approved directory — identical semantics to the former
37
+ * `SessionApprovalCache` prefix matching, using the unified wildcard engine.
38
+ *
39
+ * For paths that already end with a separator (directories), the separator
40
+ * is treated as the directory boundary and `*` is appended directly.
41
+ */
42
+ export function deriveApprovalPattern(normalizedPath: string): string {
43
+ // If the path already ends with a separator, it's a directory — glob its contents.
44
+ if (normalizedPath.endsWith(sep)) {
45
+ return `${normalizedPath}*`;
46
+ }
47
+ const dir = dirname(normalizedPath);
48
+ if (dir === normalizedPath) {
49
+ // Root path — dirname('/') === '/'
50
+ return `${dir}*`;
51
+ }
52
+ const prefix = dir.endsWith(sep) ? dir : `${dir}${sep}`;
53
+ return `${prefix}*`;
54
+ }
package/src/types.ts CHANGED
@@ -9,16 +9,8 @@ export type BuiltInToolName =
9
9
  | "find"
10
10
  | "ls";
11
11
 
12
- export type ToolPermissions = Record<string, PermissionState>;
13
-
14
- export type BashPermissions = Record<string, PermissionState>;
15
-
16
- export type SkillPermissions = Record<string, PermissionState>;
17
-
18
12
  export type SpecialPermissionName = "external_directory";
19
13
 
20
- export type SpecialPermissions = Record<string, PermissionState>;
21
-
22
14
  export interface PermissionDefaultPolicy {
23
15
  tools: PermissionState;
24
16
  bash: PermissionState;
@@ -27,17 +19,20 @@ export interface PermissionDefaultPolicy {
27
19
  special: PermissionState;
28
20
  }
29
21
 
30
- export interface AgentPermissions {
22
+ /**
23
+ * Per-scope permission config shape after loading and validation.
24
+ * All fields optional — each scope may define a subset of the policy.
25
+ *
26
+ * This replaces the former AgentPermissions / GlobalPermissionConfig
27
+ * interfaces (removed in #56).
28
+ */
29
+ export interface ScopeConfig {
31
30
  defaultPolicy?: Partial<PermissionDefaultPolicy>;
32
- tools?: ToolPermissions;
33
- bash?: BashPermissions;
34
- mcp?: ToolPermissions;
35
- skills?: SkillPermissions;
36
- special?: SpecialPermissions;
37
- }
38
-
39
- export interface GlobalPermissionConfig extends AgentPermissions {
40
- defaultPolicy: PermissionDefaultPolicy;
31
+ tools?: Record<string, PermissionState>;
32
+ bash?: Record<string, PermissionState>;
33
+ mcp?: Record<string, PermissionState>;
34
+ skills?: Record<string, PermissionState>;
35
+ special?: Record<string, PermissionState>;
41
36
  }
42
37
 
43
38
  export interface PermissionCheckResult {
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ DEFAULT_POLICY,
4
+ getSurfaceDefault,
5
+ mergeDefaults,
6
+ } from "../src/defaults";
7
+ import type { PermissionDefaultPolicy } from "../src/types";
8
+
9
+ const SPECIAL_KEYS = new Set(["external_directory"]);
10
+
11
+ describe("getSurfaceDefault", () => {
12
+ const defaults: PermissionDefaultPolicy = {
13
+ tools: "allow",
14
+ bash: "deny",
15
+ mcp: "ask",
16
+ skills: "allow",
17
+ special: "deny",
18
+ };
19
+
20
+ test("returns defaults.bash for surface 'bash'", () => {
21
+ expect(getSurfaceDefault("bash", defaults, SPECIAL_KEYS)).toBe("deny");
22
+ });
23
+
24
+ test("returns defaults.mcp for surface 'mcp'", () => {
25
+ expect(getSurfaceDefault("mcp", defaults, SPECIAL_KEYS)).toBe("ask");
26
+ });
27
+
28
+ test("returns defaults.skills for surface 'skill'", () => {
29
+ expect(getSurfaceDefault("skill", defaults, SPECIAL_KEYS)).toBe("allow");
30
+ });
31
+
32
+ test("returns defaults.special for special-key surfaces", () => {
33
+ expect(
34
+ getSurfaceDefault("external_directory", defaults, SPECIAL_KEYS),
35
+ ).toBe("deny");
36
+ });
37
+
38
+ test("returns defaults.tools for tool-name surfaces", () => {
39
+ expect(getSurfaceDefault("read", defaults, SPECIAL_KEYS)).toBe("allow");
40
+ expect(getSurfaceDefault("write", defaults, SPECIAL_KEYS)).toBe("allow");
41
+ expect(getSurfaceDefault("edit", defaults, SPECIAL_KEYS)).toBe("allow");
42
+ });
43
+
44
+ test("returns defaults.tools for unknown surfaces (least privilege via tools default)", () => {
45
+ expect(
46
+ getSurfaceDefault("unknown_extension_tool", defaults, SPECIAL_KEYS),
47
+ ).toBe("allow");
48
+ });
49
+
50
+ test("uses DEFAULT_POLICY when no overrides exist", () => {
51
+ expect(getSurfaceDefault("bash", DEFAULT_POLICY, SPECIAL_KEYS)).toBe("ask");
52
+ expect(getSurfaceDefault("read", DEFAULT_POLICY, SPECIAL_KEYS)).toBe("ask");
53
+ expect(
54
+ getSurfaceDefault("external_directory", DEFAULT_POLICY, SPECIAL_KEYS),
55
+ ).toBe("ask");
56
+ });
57
+ });
58
+
59
+ describe("mergeDefaults", () => {
60
+ test("returns DEFAULT_POLICY when called with no partials", () => {
61
+ expect(mergeDefaults()).toEqual(DEFAULT_POLICY);
62
+ });
63
+
64
+ test("overrides specific fields from a single partial", () => {
65
+ const result = mergeDefaults({ tools: "allow", bash: "deny" });
66
+ expect(result).toEqual({
67
+ tools: "allow",
68
+ bash: "deny",
69
+ mcp: "ask",
70
+ skills: "ask",
71
+ special: "ask",
72
+ });
73
+ });
74
+
75
+ test("later partials override earlier ones", () => {
76
+ const global: Partial<PermissionDefaultPolicy> = { tools: "allow" };
77
+ const project: Partial<PermissionDefaultPolicy> = { tools: "deny" };
78
+ const result = mergeDefaults(global, project);
79
+ expect(result.tools).toBe("deny");
80
+ });
81
+
82
+ test("merges across multiple partials", () => {
83
+ const global: Partial<PermissionDefaultPolicy> = {
84
+ tools: "allow",
85
+ bash: "allow",
86
+ };
87
+ const project: Partial<PermissionDefaultPolicy> = { bash: "deny" };
88
+ const agent: Partial<PermissionDefaultPolicy> = { mcp: "allow" };
89
+ const result = mergeDefaults(global, project, agent);
90
+ expect(result).toEqual({
91
+ tools: "allow",
92
+ bash: "deny",
93
+ mcp: "allow",
94
+ skills: "ask",
95
+ special: "ask",
96
+ });
97
+ });
98
+
99
+ test("undefined fields in later partials do not override earlier values", () => {
100
+ const global: Partial<PermissionDefaultPolicy> = { tools: "allow" };
101
+ const project: Partial<PermissionDefaultPolicy> = { bash: "deny" };
102
+ const result = mergeDefaults(global, project);
103
+ expect(result.tools).toBe("allow");
104
+ });
105
+ });
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { describe, expect, it, vi } from "vitest";
3
3
 
4
4
  import {
5
5
  handleBeforeAgentStart,
@@ -74,12 +74,11 @@ function makeRuntime(
74
74
  lastActiveToolsCacheKey: null,
75
75
  lastPromptStateCacheKey: null,
76
76
  lastConfigWarning: null,
77
- sessionApprovalCache: {
77
+ sessionRules: {
78
78
  approve: vi.fn(),
79
- has: vi.fn(),
80
- findMatchingPrefix: vi.fn(),
79
+ getRuleset: vi.fn().mockReturnValue([]),
81
80
  clear: vi.fn(),
82
- } as unknown as ExtensionRuntime["sessionApprovalCache"],
81
+ } as unknown as ExtensionRuntime["sessionRules"],
83
82
  permissionForwardingContext: null,
84
83
  permissionForwardingTimer: null,
85
84
  isProcessingForwardedRequests: false,
@@ -53,12 +53,11 @@ function makeRuntime(
53
53
  lastActiveToolsCacheKey: null,
54
54
  lastPromptStateCacheKey: null,
55
55
  lastConfigWarning: null,
56
- sessionApprovalCache: {
56
+ sessionRules: {
57
57
  approve: vi.fn(),
58
- has: vi.fn(),
59
- findMatchingPrefix: vi.fn(),
58
+ getRuleset: vi.fn().mockReturnValue([]),
60
59
  clear: vi.fn(),
61
- } as unknown as ExtensionRuntime["sessionApprovalCache"],
60
+ } as unknown as ExtensionRuntime["sessionRules"],
62
61
  permissionForwardingContext: null,
63
62
  permissionForwardingTimer: null,
64
63
  isProcessingForwardedRequests: false,
@@ -8,7 +8,7 @@ import {
8
8
  import type { HandlerDeps } from "../../src/handlers/types";
9
9
  import type { PermissionManager } from "../../src/permission-manager";
10
10
  import type { ExtensionRuntime } from "../../src/runtime";
11
- import type { SessionApprovalCache } from "../../src/session-approval-cache";
11
+ import type { SessionRules } from "../../src/session-rules";
12
12
  import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
13
13
 
14
14
  // ── active-agent stub ──────────────────────────────────────────────────────
@@ -59,13 +59,12 @@ function makePermissionManager(
59
59
  };
60
60
  }
61
61
 
62
- function makeSessionApprovalCache(): SessionApprovalCache {
62
+ function makeSessionRules(): SessionRules {
63
63
  return {
64
64
  approve: vi.fn(),
65
- has: vi.fn().mockReturnValue(false),
66
- findMatchingPrefix: vi.fn().mockReturnValue(null),
65
+ getRuleset: vi.fn().mockReturnValue([]),
67
66
  clear: vi.fn(),
68
- } as unknown as SessionApprovalCache;
67
+ } as unknown as SessionRules;
69
68
  }
70
69
 
71
70
  function makeRuntime(
@@ -85,7 +84,7 @@ function makeRuntime(
85
84
  lastActiveToolsCacheKey: null,
86
85
  lastPromptStateCacheKey: null,
87
86
  lastConfigWarning: null,
88
- sessionApprovalCache: makeSessionApprovalCache(),
87
+ sessionRules: makeSessionRules(),
89
88
  permissionForwardingContext: null,
90
89
  permissionForwardingTimer: null,
91
90
  isProcessingForwardedRequests: false,
@@ -330,10 +329,10 @@ describe("handleSessionShutdown", () => {
330
329
  expect(deps.runtime.lastPromptStateCacheKey).toBeNull();
331
330
  });
332
331
 
333
- it("clears the session approval cache", async () => {
332
+ it("clears the session rules", async () => {
334
333
  const deps = makeDeps();
335
334
  await handleSessionShutdown(deps);
336
- expect(deps.runtime.sessionApprovalCache.clear).toHaveBeenCalledOnce();
335
+ expect(deps.runtime.sessionRules.clear).toHaveBeenCalledOnce();
337
336
  });
338
337
 
339
338
  it("stops forwarded permission polling", async () => {
@@ -74,12 +74,11 @@ function makeRuntime(
74
74
  lastActiveToolsCacheKey: null,
75
75
  lastPromptStateCacheKey: null,
76
76
  lastConfigWarning: null,
77
- sessionApprovalCache: {
77
+ sessionRules: {
78
78
  approve: vi.fn(),
79
- has: vi.fn().mockReturnValue(false),
80
- findMatchingPrefix: vi.fn().mockReturnValue(null),
79
+ getRuleset: vi.fn().mockReturnValue([]),
81
80
  clear: vi.fn(),
82
- } as unknown as ExtensionRuntime["sessionApprovalCache"],
81
+ } as unknown as ExtensionRuntime["sessionRules"],
83
82
  permissionForwardingContext: null,
84
83
  permissionForwardingTimer: null,
85
84
  isProcessingForwardedRequests: false,
@@ -339,12 +338,17 @@ describe("handleToolCall — external-directory gate", () => {
339
338
  it("allows when session has an existing approval for the external path", async () => {
340
339
  const deps = makeDeps({
341
340
  runtime: makeRuntime({
342
- sessionApprovalCache: {
341
+ sessionRules: {
343
342
  approve: vi.fn(),
344
- has: vi.fn().mockReturnValue(false),
345
- findMatchingPrefix: vi.fn().mockReturnValue("/outside/project/"),
343
+ getRuleset: vi.fn().mockReturnValue([
344
+ {
345
+ surface: "external_directory",
346
+ pattern: "/outside/project/*",
347
+ action: "allow",
348
+ },
349
+ ]),
346
350
  clear: vi.fn(),
347
- } as unknown as ExtensionRuntime["sessionApprovalCache"],
351
+ } as unknown as ExtensionRuntime["sessionRules"],
348
352
  }),
349
353
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
350
354
  });
@@ -359,18 +363,17 @@ describe("handleToolCall — external-directory gate", () => {
359
363
  });
360
364
 
361
365
  it("approves session when user selects approved_for_session", async () => {
362
- const approveCache = {
366
+ const sessionRules = {
363
367
  approve: vi.fn(),
364
- has: vi.fn().mockReturnValue(false),
365
- findMatchingPrefix: vi.fn().mockReturnValue(null),
368
+ getRuleset: vi.fn().mockReturnValue([]),
366
369
  clear: vi.fn(),
367
- } as unknown as ExtensionRuntime["sessionApprovalCache"];
370
+ } as unknown as ExtensionRuntime["sessionRules"];
368
371
  const deps = makeDeps({
369
372
  runtime: makeRuntime({
370
373
  permissionManager: {
371
374
  checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
372
375
  } as unknown as ExtensionRuntime["permissionManager"],
373
- sessionApprovalCache: approveCache,
376
+ sessionRules,
374
377
  }),
375
378
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
376
379
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
@@ -385,7 +388,7 @@ describe("handleToolCall — external-directory gate", () => {
385
388
  input: { path: "/outside/project/file.ts" },
386
389
  };
387
390
  await handleToolCall(deps, event, makeCtx());
388
- expect(approveCache.approve).toHaveBeenCalledWith(
391
+ expect(sessionRules.approve).toHaveBeenCalledWith(
389
392
  "external_directory",
390
393
  expect.any(String),
391
394
  );
@@ -419,13 +422,18 @@ describe("handleToolCall — bash external-directory gate", () => {
419
422
  it("skips bash external gate when all referenced paths are session-approved", async () => {
420
423
  const deps = makeDeps({
421
424
  runtime: makeRuntime({
422
- sessionApprovalCache: {
425
+ sessionRules: {
423
426
  approve: vi.fn(),
424
- // All paths are covered
425
- has: vi.fn().mockReturnValue(true),
426
- findMatchingPrefix: vi.fn().mockReturnValue(null),
427
+ // /outside/project/* covers /outside/project/file.ts
428
+ getRuleset: vi.fn().mockReturnValue([
429
+ {
430
+ surface: "external_directory",
431
+ pattern: "/outside/project/*",
432
+ action: "allow",
433
+ },
434
+ ]),
427
435
  clear: vi.fn(),
428
- } as unknown as ExtensionRuntime["sessionApprovalCache"],
436
+ } as unknown as ExtensionRuntime["sessionRules"],
429
437
  }),
430
438
  getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
431
439
  });
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { normalizeConfig } from "../src/normalize";
3
+
4
+ describe("normalizeConfig", () => {
5
+ describe("tools entries", () => {
6
+ test("converts tools entries to tool-name-as-surface rules", () => {
7
+ const result = normalizeConfig({
8
+ tools: { read: "allow", write: "deny" },
9
+ });
10
+ expect(result).toEqual([
11
+ { surface: "read", pattern: "*", action: "allow" },
12
+ { surface: "write", pattern: "*", action: "deny" },
13
+ ]);
14
+ });
15
+
16
+ test("tools.bash is excluded (handled as fallback override)", () => {
17
+ const result = normalizeConfig({
18
+ tools: { bash: "allow", read: "allow" },
19
+ });
20
+ expect(result).toEqual([
21
+ { surface: "read", pattern: "*", action: "allow" },
22
+ ]);
23
+ });
24
+
25
+ test("tools.mcp is excluded (handled as fallback override)", () => {
26
+ const result = normalizeConfig({
27
+ tools: { mcp: "ask", read: "allow" },
28
+ });
29
+ expect(result).toEqual([
30
+ { surface: "read", pattern: "*", action: "allow" },
31
+ ]);
32
+ });
33
+ });
34
+
35
+ describe("bash entries", () => {
36
+ test("converts bash entries to surface 'bash' rules", () => {
37
+ const result = normalizeConfig({
38
+ bash: { "git *": "allow", "rm -rf *": "deny" },
39
+ });
40
+ expect(result).toEqual([
41
+ { surface: "bash", pattern: "git *", action: "allow" },
42
+ { surface: "bash", pattern: "rm -rf *", action: "deny" },
43
+ ]);
44
+ });
45
+ });
46
+
47
+ describe("mcp entries", () => {
48
+ test("converts mcp entries to surface 'mcp' rules", () => {
49
+ const result = normalizeConfig({
50
+ mcp: { "exa:*": "allow", mcp_status: "allow" },
51
+ });
52
+ expect(result).toEqual([
53
+ { surface: "mcp", pattern: "exa:*", action: "allow" },
54
+ { surface: "mcp", pattern: "mcp_status", action: "allow" },
55
+ ]);
56
+ });
57
+ });
58
+
59
+ describe("skills entries", () => {
60
+ test("converts skills entries to surface 'skill' rules", () => {
61
+ const result = normalizeConfig({
62
+ skills: { "*": "ask", librarian: "allow" },
63
+ });
64
+ expect(result).toEqual([
65
+ { surface: "skill", pattern: "*", action: "ask" },
66
+ { surface: "skill", pattern: "librarian", action: "allow" },
67
+ ]);
68
+ });
69
+ });
70
+
71
+ describe("special entries", () => {
72
+ test("converts special entries to surface 'special' with key as pattern", () => {
73
+ const result = normalizeConfig({
74
+ special: { external_directory: "ask" },
75
+ });
76
+ expect(result).toEqual([
77
+ { surface: "special", pattern: "external_directory", action: "ask" },
78
+ ]);
79
+ });
80
+ });
81
+
82
+ describe("ordering", () => {
83
+ test("tools.bash excluded; bash entries come after tools", () => {
84
+ const result = normalizeConfig({
85
+ tools: { bash: "allow", read: "deny" },
86
+ bash: { "git *": "ask" },
87
+ });
88
+ expect(result).toEqual([
89
+ { surface: "read", pattern: "*", action: "deny" },
90
+ { surface: "bash", pattern: "git *", action: "ask" },
91
+ ]);
92
+ });
93
+
94
+ test("full ordering: tools → bash → mcp → skills → special", () => {
95
+ const result = normalizeConfig({
96
+ tools: { read: "allow" },
97
+ bash: { "git *": "allow" },
98
+ mcp: { "exa:*": "allow" },
99
+ skills: { librarian: "allow" },
100
+ special: { external_directory: "ask" },
101
+ });
102
+ expect(result).toEqual([
103
+ { surface: "read", pattern: "*", action: "allow" },
104
+ { surface: "bash", pattern: "git *", action: "allow" },
105
+ { surface: "mcp", pattern: "exa:*", action: "allow" },
106
+ { surface: "skill", pattern: "librarian", action: "allow" },
107
+ { surface: "special", pattern: "external_directory", action: "ask" },
108
+ ]);
109
+ });
110
+ });
111
+
112
+ describe("empty and missing sections", () => {
113
+ test("empty config produces empty ruleset", () => {
114
+ expect(normalizeConfig({})).toEqual([]);
115
+ });
116
+
117
+ test("undefined sections are skipped", () => {
118
+ expect(normalizeConfig({ tools: undefined })).toEqual([]);
119
+ });
120
+ });
121
+ });
@@ -10,7 +10,7 @@ import {
10
10
  import { tmpdir } from "node:os";
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  import { test } from "vitest";
13
- import { BashFilter } from "../src/bash-filter";
13
+
14
14
  import {
15
15
  createActiveToolsCacheKey,
16
16
  createBeforeAgentStartPromptStateKey,
@@ -46,11 +46,7 @@ import {
46
46
  checkRequestedToolRegistration,
47
47
  getToolNameFromValue,
48
48
  } from "../src/tool-registry";
49
- import type {
50
- AgentPermissions,
51
- GlobalPermissionConfig,
52
- PermissionState,
53
- } from "../src/types";
49
+ import type { PermissionState, ScopeConfig } from "../src/types";
54
50
  import {
55
51
  canResolveAskPermissionRequest,
56
52
  shouldAutoApprovePermissionState,
@@ -61,7 +57,7 @@ type CreateManagerOptions = {
61
57
  };
62
58
 
63
59
  function createManager(
64
- config: GlobalPermissionConfig,
60
+ config: ScopeConfig,
65
61
  agentFiles: Record<string, string> = {},
66
62
  options: CreateManagerOptions = {},
67
63
  ) {
@@ -146,7 +142,7 @@ async function withIsolatedSubagentEnv<T>(
146
142
  }
147
143
 
148
144
  function createToolCallHarness(
149
- config: GlobalPermissionConfig,
145
+ config: ScopeConfig,
150
146
  toolNames: readonly string[],
151
147
  options: ExtensionHarnessOptions = {},
152
148
  ): ExtensionHarness {
@@ -652,30 +648,6 @@ test("Permission-system logger respects debug toggle and keeps review log enable
652
648
  }
653
649
  });
654
650
 
655
- test("BashFilter uses opencode-style last-match hierarchy", () => {
656
- const filter = new BashFilter(
657
- {
658
- "*": "ask",
659
- "git *": "deny",
660
- "git status *": "ask",
661
- "git status": "allow",
662
- },
663
- "deny",
664
- );
665
-
666
- const exact = filter.check("git status");
667
- assert.equal(exact.state, "allow");
668
- assert.equal(exact.matchedPattern, "git status");
669
-
670
- const subcommand = filter.check("git status --short");
671
- assert.equal(subcommand.state, "ask");
672
- assert.equal(subcommand.matchedPattern, "git status *");
673
-
674
- const generic = filter.check("git commit -m test");
675
- assert.equal(generic.state, "deny");
676
- assert.equal(generic.matchedPattern, "git *");
677
- });
678
-
679
651
  test("PermissionManager canonical built-in permission checking", () => {
680
652
  const { manager, cleanup } = createManager({
681
653
  defaultPolicy: {
@@ -910,7 +882,7 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
910
882
  // which matches this rule and returns "allow".
911
883
  // After the fix, settings.json is ignored, so no server name is derived and the
912
884
  // result falls through to the default mcp policy ("ask").
913
- const config: GlobalPermissionConfig = {
885
+ const config: ScopeConfig = {
914
886
  defaultPolicy: {
915
887
  tools: "ask",
916
888
  bash: "ask",
@@ -1510,12 +1482,12 @@ test("Permission forwarding rejects unresolved sentinel session ids", () => {
1510
1482
  });
1511
1483
 
1512
1484
  type CreateManagerWithProjectOptions = CreateManagerOptions & {
1513
- projectConfig?: AgentPermissions;
1485
+ projectConfig?: ScopeConfig;
1514
1486
  projectAgentFiles?: Record<string, string>;
1515
1487
  };
1516
1488
 
1517
1489
  function createManagerWithProject(
1518
- config: GlobalPermissionConfig,
1490
+ config: ScopeConfig,
1519
1491
  agentFiles: Record<string, string> = {},
1520
1492
  options: CreateManagerWithProjectOptions = {},
1521
1493
  ) {
@@ -1807,7 +1779,7 @@ test("PermissionManager reads config from PI_CODING_AGENT_DIR when set", () => {
1807
1779
  mkdirSync(agentsDir, { recursive: true });
1808
1780
  mkdirSync(dirname(newConfigPath), { recursive: true });
1809
1781
 
1810
- const config: GlobalPermissionConfig = {
1782
+ const config: ScopeConfig = {
1811
1783
  defaultPolicy: {
1812
1784
  tools: "deny",
1813
1785
  bash: "deny",
@@ -2609,7 +2581,7 @@ test("normalizeRawPermission emits no issues when special is absent", () => {
2609
2581
  });
2610
2582
 
2611
2583
  test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit in global config", () => {
2612
- const config: GlobalPermissionConfig = {
2584
+ const config: ScopeConfig = {
2613
2585
  defaultPolicy: {
2614
2586
  tools: "ask",
2615
2587
  bash: "ask",
@@ -2634,7 +2606,7 @@ test("PermissionManager.getConfigIssues returns deprecation for tool_call_limit
2634
2606
  });
2635
2607
 
2636
2608
  test("PermissionManager.getConfigIssues returns empty array for clean config", () => {
2637
- const config: GlobalPermissionConfig = {
2609
+ const config: ScopeConfig = {
2638
2610
  defaultPolicy: {
2639
2611
  tools: "ask",
2640
2612
  bash: "ask",
@@ -2660,7 +2632,7 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
2660
2632
  // --- doom_loop config-loader deprecation tests (#54) ---
2661
2633
 
2662
2634
  test("PermissionManager.getConfigIssues returns deprecation for doom_loop in global config", () => {
2663
- const config: GlobalPermissionConfig = {
2635
+ const config: ScopeConfig = {
2664
2636
  defaultPolicy: {
2665
2637
  tools: "ask",
2666
2638
  bash: "ask",