@komarspn/pi-permission-system 16.0.2

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.
Files changed (203) hide show
  1. package/CHANGELOG.md +2234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +158 -0
  4. package/config/config.example.json +39 -0
  5. package/package.json +82 -0
  6. package/schemas/permissions.schema.json +158 -0
  7. package/src/active-agent.ts +72 -0
  8. package/src/async-cache.ts +21 -0
  9. package/src/bash-arity.ts +210 -0
  10. package/src/builtin-tool-input-formatters.ts +82 -0
  11. package/src/canonicalize-path.ts +30 -0
  12. package/src/common.ts +121 -0
  13. package/src/config-loader.ts +432 -0
  14. package/src/config-modal.ts +259 -0
  15. package/src/config-paths.ts +47 -0
  16. package/src/config-reporter.ts +34 -0
  17. package/src/config-store.ts +222 -0
  18. package/src/decision-audit.ts +75 -0
  19. package/src/decision-reporter.ts +41 -0
  20. package/src/denial-messages.ts +232 -0
  21. package/src/expand-home.ts +28 -0
  22. package/src/extension-config.ts +79 -0
  23. package/src/extension-paths.ts +66 -0
  24. package/src/forwarded-permissions/io.ts +404 -0
  25. package/src/forwarded-permissions/permission-forwarder.ts +580 -0
  26. package/src/forwarding-manager.ts +74 -0
  27. package/src/gate-prompter.ts +12 -0
  28. package/src/handlers/before-agent-start.ts +94 -0
  29. package/src/handlers/gates/bash-command.ts +75 -0
  30. package/src/handlers/gates/bash-external-directory.ts +127 -0
  31. package/src/handlers/gates/bash-path-extractor.ts +15 -0
  32. package/src/handlers/gates/bash-path.ts +152 -0
  33. package/src/handlers/gates/bash-program.ts +1143 -0
  34. package/src/handlers/gates/bash-token-classification.ts +105 -0
  35. package/src/handlers/gates/candidate-check.ts +32 -0
  36. package/src/handlers/gates/descriptor.ts +81 -0
  37. package/src/handlers/gates/external-directory-messages.ts +20 -0
  38. package/src/handlers/gates/external-directory.ts +133 -0
  39. package/src/handlers/gates/helpers.ts +76 -0
  40. package/src/handlers/gates/path.ts +91 -0
  41. package/src/handlers/gates/runner.ts +186 -0
  42. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  43. package/src/handlers/gates/skill-input.ts +46 -0
  44. package/src/handlers/gates/skill-read.ts +87 -0
  45. package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
  46. package/src/handlers/gates/tool.ts +102 -0
  47. package/src/handlers/gates/types.ts +13 -0
  48. package/src/handlers/index.ts +3 -0
  49. package/src/handlers/lifecycle.ts +95 -0
  50. package/src/handlers/permission-gate-handler.ts +190 -0
  51. package/src/handlers/tool-call-boundary.ts +91 -0
  52. package/src/index.ts +225 -0
  53. package/src/input-normalizer.ts +157 -0
  54. package/src/logging.ts +113 -0
  55. package/src/mcp-targets.ts +170 -0
  56. package/src/node-modules-discovery.ts +76 -0
  57. package/src/normalize.ts +43 -0
  58. package/src/path-utils.ts +355 -0
  59. package/src/pattern-suggest.ts +132 -0
  60. package/src/permission-dialog.ts +138 -0
  61. package/src/permission-event-rpc.ts +223 -0
  62. package/src/permission-events.ts +266 -0
  63. package/src/permission-forwarding.ts +188 -0
  64. package/src/permission-gate.ts +94 -0
  65. package/src/permission-manager.ts +392 -0
  66. package/src/permission-merge.ts +32 -0
  67. package/src/permission-prompter.ts +142 -0
  68. package/src/permission-prompts.ts +93 -0
  69. package/src/permission-resolver.ts +109 -0
  70. package/src/permission-session.ts +189 -0
  71. package/src/permission-ui-prompt.ts +127 -0
  72. package/src/permissions-service.ts +63 -0
  73. package/src/persistent-approval-recorder.ts +139 -0
  74. package/src/policy-loader.ts +350 -0
  75. package/src/prompting-gateway.ts +104 -0
  76. package/src/rule.ts +188 -0
  77. package/src/scope-merge.ts +72 -0
  78. package/src/service-lifecycle.ts +49 -0
  79. package/src/service.ts +163 -0
  80. package/src/session-approval-recorder.ts +6 -0
  81. package/src/session-approval.ts +43 -0
  82. package/src/session-logger.ts +91 -0
  83. package/src/session-rules.ts +79 -0
  84. package/src/skill-prompt-sanitizer.ts +292 -0
  85. package/src/status.ts +35 -0
  86. package/src/subagent-context.ts +104 -0
  87. package/src/subagent-lifecycle-events.ts +72 -0
  88. package/src/subagent-registry.ts +105 -0
  89. package/src/synthesize.ts +92 -0
  90. package/src/system-prompt-sanitizer.ts +274 -0
  91. package/src/tool-access-extractor-registry.ts +68 -0
  92. package/src/tool-input-formatter-registry.ts +67 -0
  93. package/src/tool-input-preview.ts +34 -0
  94. package/src/tool-input-prompt-formatters.ts +63 -0
  95. package/src/tool-preview-formatter.ts +207 -0
  96. package/src/tool-registry.ts +148 -0
  97. package/src/types.ts +64 -0
  98. package/src/wildcard-matcher.ts +120 -0
  99. package/src/yolo-mode.ts +30 -0
  100. package/test/active-agent.test.ts +155 -0
  101. package/test/async-cache.test.ts +48 -0
  102. package/test/bash-arity.test.ts +144 -0
  103. package/test/bash-external-directory.test.ts +956 -0
  104. package/test/builtin-tool-input-formatters.test.ts +109 -0
  105. package/test/canonicalize-path.test.ts +93 -0
  106. package/test/common.test.ts +287 -0
  107. package/test/composition-root.test.ts +603 -0
  108. package/test/config-loader.test.ts +740 -0
  109. package/test/config-modal.test.ts +320 -0
  110. package/test/config-paths.test.ts +83 -0
  111. package/test/config-pipeline.test.ts +90 -0
  112. package/test/config-reporter.test.ts +147 -0
  113. package/test/config-store.test.ts +466 -0
  114. package/test/decision-audit.test.ts +72 -0
  115. package/test/decision-reporter.test.ts +112 -0
  116. package/test/denial-messages.test.ts +656 -0
  117. package/test/detect-permissive-bash-fallback.test.ts +56 -0
  118. package/test/expand-home.test.ts +93 -0
  119. package/test/extension-config.test.ts +129 -0
  120. package/test/extension-paths.test.ts +108 -0
  121. package/test/forwarded-permissions/io.test.ts +251 -0
  122. package/test/forwarding-manager.test.ts +194 -0
  123. package/test/handlers/before-agent-start.test.ts +317 -0
  124. package/test/handlers/external-directory-integration.test.ts +623 -0
  125. package/test/handlers/external-directory-session-dedup.test.ts +430 -0
  126. package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
  127. package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
  128. package/test/handlers/gates/bash-command.test.ts +191 -0
  129. package/test/handlers/gates/bash-external-directory.test.ts +269 -0
  130. package/test/handlers/gates/bash-path.test.ts +337 -0
  131. package/test/handlers/gates/bash-program.test.ts +410 -0
  132. package/test/handlers/gates/bash-token-classification.test.ts +241 -0
  133. package/test/handlers/gates/candidate-check.test.ts +52 -0
  134. package/test/handlers/gates/external-directory-messages.test.ts +61 -0
  135. package/test/handlers/gates/external-directory.test.ts +259 -0
  136. package/test/handlers/gates/helpers.test.ts +177 -0
  137. package/test/handlers/gates/path.test.ts +294 -0
  138. package/test/handlers/gates/runner.test.ts +447 -0
  139. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  140. package/test/handlers/gates/skill-input.test.ts +131 -0
  141. package/test/handlers/gates/skill-read.test.ts +158 -0
  142. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
  143. package/test/handlers/gates/tool.test.ts +223 -0
  144. package/test/handlers/input-events.test.ts +168 -0
  145. package/test/handlers/input.test.ts +199 -0
  146. package/test/handlers/lifecycle.test.ts +221 -0
  147. package/test/handlers/tool-call-boundary.test.ts +145 -0
  148. package/test/handlers/tool-call-events.test.ts +277 -0
  149. package/test/handlers/tool-call.test.ts +395 -0
  150. package/test/handlers/validate-requested-tool.test.ts +92 -0
  151. package/test/helpers/gate-fixtures.ts +323 -0
  152. package/test/helpers/handler-fixtures.ts +335 -0
  153. package/test/helpers/make-fake-pi.ts +100 -0
  154. package/test/helpers/manager-harness.ts +112 -0
  155. package/test/helpers/session-fixtures.ts +204 -0
  156. package/test/input-normalizer.test.ts +367 -0
  157. package/test/logging.test.ts +51 -0
  158. package/test/mcp-targets.test.ts +233 -0
  159. package/test/node-modules-discovery.test.ts +97 -0
  160. package/test/normalize.test.ts +247 -0
  161. package/test/path-utils.test.ts +650 -0
  162. package/test/pattern-suggest.test.ts +248 -0
  163. package/test/permission-dialog.test.ts +241 -0
  164. package/test/permission-event-rpc.test.ts +541 -0
  165. package/test/permission-events.test.ts +402 -0
  166. package/test/permission-forwarder.test.ts +369 -0
  167. package/test/permission-forwarding.test.ts +315 -0
  168. package/test/permission-gate.test.ts +305 -0
  169. package/test/permission-manager-unified.test.ts +3368 -0
  170. package/test/permission-merge.test.ts +61 -0
  171. package/test/permission-prompter.test.ts +518 -0
  172. package/test/permission-prompts.test.ts +363 -0
  173. package/test/permission-resolver.test.ts +265 -0
  174. package/test/permission-session.test.ts +363 -0
  175. package/test/permission-ui-prompt.test.ts +146 -0
  176. package/test/permissions-service.test.ts +177 -0
  177. package/test/persistent-approval-recorder.test.ts +133 -0
  178. package/test/pi-infrastructure-read.test.ts +369 -0
  179. package/test/policy-loader.test.ts +561 -0
  180. package/test/prompting-gateway.test.ts +230 -0
  181. package/test/rule.test.ts +604 -0
  182. package/test/scope-merge.test.ts +116 -0
  183. package/test/service-lifecycle.test.ts +163 -0
  184. package/test/service.test.ts +308 -0
  185. package/test/session-approval.test.ts +75 -0
  186. package/test/session-logger.test.ts +200 -0
  187. package/test/session-rules.test.ts +304 -0
  188. package/test/session-start.test.ts +112 -0
  189. package/test/skill-prompt-sanitizer.test.ts +374 -0
  190. package/test/status.test.ts +10 -0
  191. package/test/subagent-context.test.ts +326 -0
  192. package/test/subagent-lifecycle-events.test.ts +132 -0
  193. package/test/subagent-registry.test.ts +145 -0
  194. package/test/synthesize.test.ts +300 -0
  195. package/test/system-prompt-sanitizer.test.ts +382 -0
  196. package/test/tool-access-extractor-registry.test.ts +77 -0
  197. package/test/tool-input-formatter-registry.test.ts +75 -0
  198. package/test/tool-input-preview.test.ts +129 -0
  199. package/test/tool-input-prompt-formatters.test.ts +115 -0
  200. package/test/tool-preview-formatter.test.ts +458 -0
  201. package/test/tool-registry.test.ts +197 -0
  202. package/test/wildcard-matcher.test.ts +424 -0
  203. package/test/yolo-mode.test.ts +188 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `makeFakePi()` — a composition-root test harness.
3
+ *
4
+ * Lets a test run the real `piPermissionSystemExtension(pi)` factory and then
5
+ * introspect and drive the result. Unlike the per-handler unit fixtures in
6
+ * `handler-fixtures.ts` (which inject collaborators), this harness exercises the
7
+ * factory itself — the wiring layer where registration completeness, shared-
8
+ * instance contracts, teardown, and event ordering live.
9
+ *
10
+ * It provides:
11
+ * - `events` — a real `createEventBus()` so cross-extension pub/sub and RPC
12
+ * behave as in production (tests can inject a shared bus to model parent/child
13
+ * instances).
14
+ * - `handlers` — every `pi.on(event, handler)` registration, keyed by event
15
+ * name, so a test can assert completeness and fire handlers.
16
+ * - `commands` — every `pi.registerCommand(name, …)` registration.
17
+ * - `fire(event, input, ctx)` — drive a registered handler; resolves to its
18
+ * (possibly async) result.
19
+ *
20
+ * The harness object is cast to `ExtensionAPI` at the call to the factory; the
21
+ * `FakePi` interface itself stays narrow (ISP — only what the factory touches).
22
+ */
23
+ import { createEventBus, type EventBus } from "@earendil-works/pi-coding-agent";
24
+ import { vi } from "vitest";
25
+
26
+ /** A handler recorded by `pi.on(...)`, kept generic over event/result shapes. */
27
+ export type RecordedHandler = (event: unknown, ctx: unknown) => unknown;
28
+
29
+ export interface FakePi {
30
+ /** Real event bus so cross-extension pub/sub and RPC behave as in production. */
31
+ events: EventBus;
32
+ /** Every `pi.on(event, handler)` registration, keyed by event name. */
33
+ handlers: Map<string, RecordedHandler>;
34
+ /** Every `pi.registerCommand(name, …)` registration, keyed by command name. */
35
+ commands: Map<string, unknown>;
36
+ /**
37
+ * Drive a registered handler; resolves to its (possibly async) result.
38
+ *
39
+ * Throws if no handler is registered for `event` so a typo in a test surfaces
40
+ * loudly instead of silently resolving to `undefined`.
41
+ */
42
+ fire(event: string, input?: unknown, ctx?: unknown): Promise<unknown>;
43
+ /** Minimal tool registry — returns the configured tool names. */
44
+ getAllTools(): { name: string }[];
45
+ /** Active tool names (`pi.getActiveTools()` shape — bare strings). */
46
+ getActiveTools(): string[];
47
+ setActiveTools(names: string[]): void;
48
+ }
49
+
50
+ export interface MakeFakePiOptions {
51
+ /** Inject a shared bus to model parent/child instances; defaults to a fresh bus. */
52
+ events?: EventBus;
53
+ /** Tool names returned by `getAllTools()`; defaults to a small set. */
54
+ toolNames?: readonly string[];
55
+ }
56
+
57
+ const DEFAULT_TOOL_NAMES = ["read", "write", "edit", "bash", "ls", "grep"];
58
+
59
+ /**
60
+ * Build a fake `ExtensionAPI` for composition-root tests.
61
+ *
62
+ * The returned object is structurally a `FakePi`; pass it to the factory as
63
+ * `piPermissionSystemExtension(pi as unknown as ExtensionAPI)`.
64
+ */
65
+ export function makeFakePi(options: MakeFakePiOptions = {}): FakePi {
66
+ const events = options.events ?? createEventBus();
67
+ const toolNames = options.toolNames ?? DEFAULT_TOOL_NAMES;
68
+ const handlers = new Map<string, RecordedHandler>();
69
+ const commands = new Map<string, unknown>();
70
+
71
+ return {
72
+ events,
73
+ handlers,
74
+ commands,
75
+ fire(event, input, ctx): Promise<unknown> {
76
+ const handler = handlers.get(event);
77
+ if (!handler) {
78
+ throw new Error(`No handler registered for event "${event}"`);
79
+ }
80
+ return Promise.resolve(handler(input, ctx));
81
+ },
82
+ getAllTools(): { name: string }[] {
83
+ return toolNames.map((name) => ({ name }));
84
+ },
85
+ getActiveTools(): string[] {
86
+ return [...toolNames];
87
+ },
88
+ setActiveTools: vi.fn(),
89
+ // ── ExtensionAPI methods the factory touches (recorded) ────────────────
90
+ on(event: string, handler: RecordedHandler): void {
91
+ handlers.set(event, handler);
92
+ },
93
+ registerCommand(name: string, optionsArg: unknown): void {
94
+ commands.set(name, optionsArg);
95
+ },
96
+ // ── ExtensionAPI methods present for the cast but unused by the factory ─
97
+ registerProvider: vi.fn(),
98
+ exec: vi.fn(),
99
+ } as FakePi & Record<string, unknown>;
100
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Filesystem-backed PermissionManager harness for integration tests.
3
+ *
4
+ * Writes a real config file and agents directory to a temp directory so
5
+ * PermissionManager can load them without mocking the file system.
6
+ */
7
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+
11
+ import { PermissionManager } from "#src/permission-manager";
12
+ import type { ScopeConfig } from "#src/types";
13
+
14
+ export type CreateManagerOptions = {
15
+ mcpServerNames?: readonly string[];
16
+ };
17
+
18
+ export type CreateManagerWithProjectOptions = CreateManagerOptions & {
19
+ projectConfig?: ScopeConfig;
20
+ projectAgentFiles?: Record<string, string>;
21
+ };
22
+
23
+ export function createManager(
24
+ config: ScopeConfig,
25
+ agentFiles: Record<string, string> = {},
26
+ options: CreateManagerOptions = {},
27
+ ) {
28
+ const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-test-"));
29
+ const globalConfigPath = join(baseDir, "pi-permissions.jsonc");
30
+ const agentsDir = join(baseDir, "agents");
31
+
32
+ mkdirSync(agentsDir, { recursive: true });
33
+ writeFileSync(
34
+ globalConfigPath,
35
+ `${JSON.stringify(config, null, 2)}\n`,
36
+ "utf8",
37
+ );
38
+
39
+ for (const [name, content] of Object.entries(agentFiles)) {
40
+ writeFileSync(join(agentsDir, `${name}.md`), content, "utf8");
41
+ }
42
+
43
+ const manager = new PermissionManager({
44
+ globalConfigPath,
45
+ agentsDir,
46
+ mcpServerNames: options.mcpServerNames,
47
+ });
48
+
49
+ return {
50
+ manager,
51
+ globalConfigPath,
52
+ cleanup: (): void => {
53
+ rmSync(baseDir, { recursive: true, force: true });
54
+ },
55
+ };
56
+ }
57
+
58
+ export function createManagerWithProject(
59
+ config: ScopeConfig,
60
+ agentFiles: Record<string, string> = {},
61
+ options: CreateManagerWithProjectOptions = {},
62
+ ) {
63
+ const baseDir = mkdtempSync(
64
+ join(tmpdir(), "pi-permission-system-proj-test-"),
65
+ );
66
+ const globalConfigPath = join(baseDir, "pi-permissions.jsonc");
67
+ const agentsDir = join(baseDir, "agents");
68
+ const projectRoot = join(baseDir, "project");
69
+ const projectGlobalConfigPath = join(projectRoot, "pi-permissions.jsonc");
70
+ const projectAgentsDir = join(projectRoot, "agents");
71
+
72
+ mkdirSync(agentsDir, { recursive: true });
73
+ mkdirSync(projectAgentsDir, { recursive: true });
74
+
75
+ writeFileSync(
76
+ globalConfigPath,
77
+ `${JSON.stringify(config, null, 2)}\n`,
78
+ "utf8",
79
+ );
80
+ if (options.projectConfig) {
81
+ writeFileSync(
82
+ projectGlobalConfigPath,
83
+ `${JSON.stringify(options.projectConfig, null, 2)}\n`,
84
+ "utf8",
85
+ );
86
+ }
87
+
88
+ for (const [name, content] of Object.entries(agentFiles)) {
89
+ writeFileSync(join(agentsDir, `${name}.md`), content, "utf8");
90
+ }
91
+
92
+ for (const [name, content] of Object.entries(
93
+ options.projectAgentFiles ?? {},
94
+ )) {
95
+ writeFileSync(join(projectAgentsDir, `${name}.md`), content, "utf8");
96
+ }
97
+
98
+ const manager = new PermissionManager({
99
+ globalConfigPath,
100
+ agentsDir,
101
+ projectGlobalConfigPath,
102
+ projectAgentsDir,
103
+ mcpServerNames: options.mcpServerNames,
104
+ });
105
+
106
+ return {
107
+ manager,
108
+ cleanup: (): void => {
109
+ rmSync(baseDir, { recursive: true, force: true });
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Shared real-instance test fixtures for PermissionSession and
3
+ * PermissionResolver.
4
+ *
5
+ * Use these instead of hand-rolling per-file mock intersection types.
6
+ * Build a real PermissionSession from small per-collaborator fakes so tests
7
+ * assert against actual behavior rather than mock contracts.
8
+ *
9
+ * Note: tests that exercise `resolveAgentName` must mock `active-agent` in
10
+ * their own file (the vi.hoisted / vi.mock pattern from permission-session.test.ts)
11
+ * since that mock is module-scoped.
12
+ */
13
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
14
+ import { vi } from "vitest";
15
+
16
+ import type { SessionConfigStore } from "#src/config-store";
17
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
18
+ import type { ExtensionPaths } from "#src/extension-paths";
19
+ import type { ForwardingController } from "#src/forwarding-manager";
20
+ import type { ScopedPermissionManager } from "#src/permission-manager";
21
+ import { PermissionResolver } from "#src/permission-resolver";
22
+ import { PermissionSession } from "#src/permission-session";
23
+ import type { PromptingGatewayLifecycle } from "#src/prompting-gateway";
24
+ import type { Ruleset } from "#src/rule";
25
+ import type { SessionLogger } from "#src/session-logger";
26
+ import { SessionRules } from "#src/session-rules";
27
+ import type { PermissionCheckResult, PermissionState } from "#src/types";
28
+
29
+ // ── Per-collaborator fake factories ────────────────────────────────────────
30
+
31
+ export function makePaths(
32
+ overrides: Partial<ExtensionPaths> = {},
33
+ ): ExtensionPaths {
34
+ return {
35
+ agentDir: "/test/agent",
36
+ sessionsDir: "/test/agent/sessions",
37
+ subagentSessionsDir: "/test/agent/subagent-sessions",
38
+ forwardingDir: "/test/agent/sessions/permission-forwarding",
39
+ globalLogsDir: "/test/agent/logs",
40
+ piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ export function makeLogger(): SessionLogger {
46
+ return {
47
+ debug: vi.fn(),
48
+ review: vi.fn(),
49
+ warn: vi.fn(),
50
+ };
51
+ }
52
+
53
+ export function makeConfigStore(
54
+ overrides: Partial<SessionConfigStore> = {},
55
+ ): SessionConfigStore {
56
+ return {
57
+ current:
58
+ overrides.current ??
59
+ vi
60
+ .fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
61
+ .mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
62
+ refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
63
+ logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
64
+ };
65
+ }
66
+
67
+ export function makeGateway(): PromptingGatewayLifecycle {
68
+ return {
69
+ activate: vi.fn<PromptingGatewayLifecycle["activate"]>(),
70
+ deactivate: vi.fn<PromptingGatewayLifecycle["deactivate"]>(),
71
+ };
72
+ }
73
+
74
+ export function makeForwarding(): ForwardingController {
75
+ return {
76
+ start: vi.fn(),
77
+ stop: vi.fn(),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Fake `ScopedPermissionManager` with vi.fn() stubs.
83
+ *
84
+ * Return type is intentionally unannotated so callers retain full `vi.fn()`
85
+ * mock access (`mock.calls`, `toHaveBeenCalledWith`, `mockReturnValue`, etc.).
86
+ */
87
+ export function makeFakePermissionManager() {
88
+ return {
89
+ configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
90
+ checkPermission: vi
91
+ .fn<
92
+ (
93
+ toolName: string,
94
+ input: unknown,
95
+ agentName?: string,
96
+ sessionRules?: Ruleset,
97
+ ) => PermissionCheckResult
98
+ >()
99
+ .mockReturnValue({
100
+ state: "allow",
101
+ toolName: "read",
102
+ source: "tool",
103
+ origin: "builtin",
104
+ }),
105
+ checkPathPolicy: vi
106
+ .fn<
107
+ (
108
+ values: readonly string[],
109
+ agentName?: string,
110
+ sessionRules?: Ruleset,
111
+ ) => PermissionCheckResult
112
+ >()
113
+ .mockReturnValue({
114
+ state: "allow",
115
+ toolName: "path",
116
+ source: "special",
117
+ origin: "builtin",
118
+ }),
119
+ getToolPermission: vi
120
+ .fn<(toolName: string, agentName?: string) => PermissionState>()
121
+ .mockReturnValue("allow"),
122
+ getConfigIssues: vi.fn((): string[] => []),
123
+ };
124
+ }
125
+
126
+ // ── Real-instance factories ────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Build a real PermissionSession from per-collaborator fakes.
130
+ *
131
+ * Returns the session and every collaborator so callers can destructure only
132
+ * what they need and assert against collaborator spies directly.
133
+ * The `permissionManager` is a `makeFakePermissionManager()` result unless
134
+ * the caller passes an explicit `ScopedPermissionManager`.
135
+ */
136
+ export function makeRealSession(overrides?: {
137
+ paths?: Partial<ExtensionPaths>;
138
+ logger?: SessionLogger;
139
+ forwarding?: ForwardingController;
140
+ permissionManager?: ScopedPermissionManager;
141
+ sessionRules?: SessionRules;
142
+ configStore?: SessionConfigStore;
143
+ gateway?: PromptingGatewayLifecycle;
144
+ }): {
145
+ session: PermissionSession;
146
+ paths: ExtensionPaths;
147
+ logger: SessionLogger;
148
+ forwarding: ForwardingController;
149
+ permissionManager: ReturnType<typeof makeFakePermissionManager>;
150
+ sessionRules: SessionRules;
151
+ configStore: SessionConfigStore;
152
+ gateway: PromptingGatewayLifecycle;
153
+ } {
154
+ const paths = makePaths(overrides?.paths);
155
+ const logger = overrides?.logger ?? makeLogger();
156
+ const forwarding = overrides?.forwarding ?? makeForwarding();
157
+ const permissionManager =
158
+ (overrides?.permissionManager as
159
+ | ReturnType<typeof makeFakePermissionManager>
160
+ | undefined) ?? makeFakePermissionManager();
161
+ const sessionRules = overrides?.sessionRules ?? new SessionRules();
162
+ const configStore = overrides?.configStore ?? makeConfigStore();
163
+ const gateway = overrides?.gateway ?? makeGateway();
164
+ const session = new PermissionSession(
165
+ paths,
166
+ forwarding,
167
+ permissionManager,
168
+ sessionRules,
169
+ configStore,
170
+ gateway,
171
+ );
172
+ return {
173
+ session,
174
+ paths,
175
+ logger,
176
+ forwarding,
177
+ permissionManager,
178
+ sessionRules,
179
+ configStore,
180
+ gateway,
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Build a real PermissionResolver from a fake manager and a SessionRules
186
+ * instance.
187
+ *
188
+ * When called with no arguments, creates a fresh fake manager and fresh
189
+ * SessionRules. Pass shared instances to connect the resolver to the same
190
+ * manager/rules used by a real session.
191
+ */
192
+ export function makeRealResolver(
193
+ manager?: ReturnType<typeof makeFakePermissionManager>,
194
+ sessionRules?: SessionRules,
195
+ ): {
196
+ resolver: PermissionResolver;
197
+ manager: ReturnType<typeof makeFakePermissionManager>;
198
+ sessionRules: SessionRules;
199
+ } {
200
+ const resolvedManager = manager ?? makeFakePermissionManager();
201
+ const resolvedRules = sessionRules ?? new SessionRules();
202
+ const resolver = new PermissionResolver(resolvedManager, resolvedRules);
203
+ return { resolver, manager: resolvedManager, sessionRules: resolvedRules };
204
+ }