@gotgenes/pi-permission-system 10.1.0 → 10.3.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 CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [10.3.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.2.0...pi-permission-system-v10.3.0) (2026-06-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * add ConfigStore owning extension config state ([5941733](https://github.com/gotgenes/pi-packages/commit/5941733a67c0ad9aef3d3b2e5908a82e76ac8603))
14
+
15
+ ## [10.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.1.0...pi-permission-system-v10.2.0) (2026-06-04)
16
+
17
+
18
+ ### Features
19
+
20
+ * add PermissionManager.configureForCwd and agentDir option ([5a2d363](https://github.com/gotgenes/pi-packages/commit/5a2d3634a0b8466a5d6aa8baa170a9bf53e068fb))
21
+
8
22
  ## [10.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v10.0.0...pi-permission-system-v10.1.0) (2026-06-03)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "10.1.0",
3
+ "version": "10.3.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -5,6 +5,7 @@ import {
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { type SettingItem, SettingsList } from "@earendil-works/pi-tui";
7
7
 
8
+ import type { CommandConfigStore } from "./config-store";
8
9
  import {
9
10
  DEFAULT_EXTENSION_CONFIG,
10
11
  type PermissionSystemExtensionConfig,
@@ -12,11 +13,7 @@ import {
12
13
  import type { Ruleset } from "./rule";
13
14
 
14
15
  interface PermissionSystemConfigController {
15
- getConfig(): PermissionSystemExtensionConfig;
16
- setConfig(
17
- next: PermissionSystemExtensionConfig,
18
- ctx: ExtensionCommandContext,
19
- ): void;
16
+ config: CommandConfigStore;
20
17
  getConfigPath(): string;
21
18
  /** Optional: returns the composed config-layer ruleset for origin display. */
22
19
  getComposedRules?(): Ruleset;
@@ -175,15 +172,15 @@ async function openSettingsModal(
175
172
  // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- ctx.ui.custom<void> is valid; rule does not allow void in generic fn call type args
176
173
  await ctx.ui.custom<void>(
177
174
  (_tui, _theme, _keybindings, done) => {
178
- let current = controller.getConfig();
175
+ let current = controller.config.current();
179
176
  const settingsList = new SettingsList(
180
177
  buildSettingItems(current),
181
178
  10,
182
179
  getSettingsListTheme(),
183
180
  (id, newValue) => {
184
181
  current = applySetting(current, id, newValue);
185
- controller.setConfig(current, ctx);
186
- current = controller.getConfig();
182
+ controller.config.save(current, ctx);
183
+ current = controller.config.current();
187
184
  syncSettingValues(settingsList, current);
188
185
  },
189
186
  () => done(),
@@ -208,7 +205,7 @@ function handleArgs(
208
205
  if (normalized === "show") {
209
206
  const rules = controller.getComposedRules?.();
210
207
  ctx.ui.notify(
211
- `permission-system: ${summarizeConfig(controller.getConfig(), rules)}`,
208
+ `permission-system: ${summarizeConfig(controller.config.current(), rules)}`,
212
209
  "info",
213
210
  );
214
211
  return true;
@@ -223,7 +220,7 @@ function handleArgs(
223
220
  }
224
221
 
225
222
  if (normalized === "reset") {
226
- controller.setConfig(cloneDefaultConfig(), ctx);
223
+ controller.config.save(cloneDefaultConfig(), ctx);
227
224
  ctx.ui.notify("Permission system settings reset to defaults.", "info");
228
225
  return true;
229
226
  }
@@ -0,0 +1,243 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ renameSync,
5
+ unlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { dirname, normalize } from "node:path";
9
+ import type {
10
+ ExtensionCommandContext,
11
+ ExtensionContext,
12
+ } from "@earendil-works/pi-coding-agent";
13
+
14
+ import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
15
+ import {
16
+ getGlobalConfigPath,
17
+ getLegacyExtensionConfigPath,
18
+ getLegacyGlobalPolicyPath,
19
+ getLegacyProjectPolicyPath,
20
+ } from "./config-paths";
21
+ import { buildResolvedConfigLogEntry } from "./config-reporter";
22
+ import {
23
+ DEFAULT_EXTENSION_CONFIG,
24
+ EXTENSION_ROOT,
25
+ normalizePermissionSystemConfig,
26
+ type PermissionSystemExtensionConfig,
27
+ } from "./extension-config";
28
+ import type { ResolvedPolicyPaths } from "./policy-loader";
29
+ import { syncPermissionSystemStatus } from "./status";
30
+
31
+ /** Read-only view of the current config — for consumers that only read. */
32
+ export interface ConfigReader {
33
+ current(): PermissionSystemExtensionConfig;
34
+ }
35
+
36
+ /**
37
+ * Narrow subset of `ConfigStore` that `PermissionSession` depends on.
38
+ *
39
+ * Using an interface rather than the concrete class avoids private-member
40
+ * coupling between the class and test doubles.
41
+ */
42
+ export interface SessionConfigStore extends ConfigReader {
43
+ refresh(ctx?: ExtensionContext): void;
44
+ logResolvedPaths(): void;
45
+ }
46
+
47
+ /**
48
+ * Narrow subset of `ConfigStore` for the `/permission-system` command.
49
+ *
50
+ * Using an interface rather than the concrete class avoids private-member
51
+ * coupling between the class and test doubles.
52
+ */
53
+ export interface CommandConfigStore extends ConfigReader {
54
+ save(
55
+ next: PermissionSystemExtensionConfig,
56
+ ctx: ExtensionCommandContext,
57
+ ): void;
58
+ }
59
+
60
+ /**
61
+ * Transitional get/set seam over the runtime-owned context.
62
+ *
63
+ * Retired in Step 4 (#337) when context ownership moves to `PermissionSession`.
64
+ */
65
+ export interface RuntimeContextRef {
66
+ get(): ExtensionContext | null;
67
+ set(ctx: ExtensionContext): void;
68
+ }
69
+
70
+ /** Narrow logging sink — replaced by an injected logger in Step 3 (#336). */
71
+ export interface ConfigStoreLogger {
72
+ writeDebugLog(event: string, details?: Record<string, unknown>): void;
73
+ writeReviewLog(event: string, details?: Record<string, unknown>): void;
74
+ }
75
+
76
+ /** Narrow view of the manager's resolved policy paths (for `logResolvedPaths`). */
77
+ export interface ResolvedPolicyPathProvider {
78
+ getResolvedPolicyPaths(): ResolvedPolicyPaths;
79
+ }
80
+
81
+ export interface ConfigStoreDeps {
82
+ agentDir: string;
83
+ context: RuntimeContextRef;
84
+ policyPaths: ResolvedPolicyPathProvider;
85
+ logger: ConfigStoreLogger;
86
+ }
87
+
88
+ /**
89
+ * Owns the mutable extension config and the operations that read/write it.
90
+ *
91
+ * Replaces the three `(runtime, …)` config free functions
92
+ * (`refreshExtensionConfig`, `saveExtensionConfig`, `logResolvedConfigPaths`)
93
+ * with methods that privately own `config` and `lastConfigWarning`.
94
+ *
95
+ * Implements {@link ConfigReader} so consumers that only read the current config
96
+ * can depend on the narrow interface rather than the full class.
97
+ */
98
+ export class ConfigStore implements SessionConfigStore, CommandConfigStore {
99
+ private config: PermissionSystemExtensionConfig;
100
+ private lastConfigWarning: string | null = null;
101
+
102
+ constructor(private readonly deps: ConfigStoreDeps) {
103
+ this.config = { ...DEFAULT_EXTENSION_CONFIG };
104
+ }
105
+
106
+ /** Return the current extension config. */
107
+ current(): PermissionSystemExtensionConfig {
108
+ return this.config;
109
+ }
110
+
111
+ /**
112
+ * Reload merged config from disk.
113
+ *
114
+ * If `ctx` is provided, updates the stored runtime context via the seam first.
115
+ * Equivalent to `refreshExtensionConfig(runtime, ctx?)`.
116
+ */
117
+ refresh(ctx?: ExtensionContext): void {
118
+ if (ctx) {
119
+ this.deps.context.set(ctx);
120
+ }
121
+ const cwd = this.deps.context.get()?.cwd ?? null;
122
+ const mergeResult = loadAndMergeConfigs(
123
+ this.deps.agentDir,
124
+ cwd ?? "",
125
+ EXTENSION_ROOT,
126
+ );
127
+ const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
128
+ this.config = runtimeConfig;
129
+
130
+ const currentCtx = this.deps.context.get();
131
+ if (currentCtx?.hasUI) {
132
+ syncPermissionSystemStatus(currentCtx, runtimeConfig);
133
+ }
134
+
135
+ const warning =
136
+ mergeResult.issues.length > 0 ? mergeResult.issues.join("\n") : undefined;
137
+
138
+ if (warning && warning !== this.lastConfigWarning) {
139
+ this.lastConfigWarning = warning;
140
+ currentCtx?.ui.notify(warning, "warning");
141
+ } else if (!warning) {
142
+ this.lastConfigWarning = null;
143
+ }
144
+
145
+ this.deps.logger.writeDebugLog("config.loaded", {
146
+ warning: warning ?? null,
147
+ debugLog: runtimeConfig.debugLog,
148
+ permissionReviewLog: runtimeConfig.permissionReviewLog,
149
+ yoloMode: runtimeConfig.yoloMode,
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Save updated runtime knobs to the global config file, then update
155
+ * the current config and sync UI status.
156
+ *
157
+ * Equivalent to `saveExtensionConfig(runtime, next, ctx)`.
158
+ */
159
+ // Called via the CommandConfigStore interface from config-modal.ts — fallow cannot trace through interfaces.
160
+ // fallow-ignore-next-line unused-class-member
161
+ save(
162
+ next: PermissionSystemExtensionConfig,
163
+ ctx: ExtensionCommandContext,
164
+ ): void {
165
+ const normalized = normalizePermissionSystemConfig(next);
166
+ const globalPath = getGlobalConfigPath(this.deps.agentDir);
167
+
168
+ const existing = loadUnifiedConfig(globalPath);
169
+ const merged = {
170
+ ...existing.config,
171
+ debugLog: normalized.debugLog,
172
+ permissionReviewLog: normalized.permissionReviewLog,
173
+ yoloMode: normalized.yoloMode,
174
+ };
175
+
176
+ const tmpPath = `${globalPath}.tmp`;
177
+ try {
178
+ mkdirSync(dirname(globalPath), { recursive: true });
179
+ writeFileSync(tmpPath, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
180
+ renameSync(tmpPath, globalPath);
181
+ } catch (error) {
182
+ try {
183
+ if (existsSync(tmpPath)) {
184
+ unlinkSync(tmpPath);
185
+ }
186
+ } catch {
187
+ // Ignore cleanup failures.
188
+ }
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ ctx.ui.notify(
191
+ `Failed to save permission-system config at '${globalPath}': ${message}`,
192
+ "error",
193
+ );
194
+ return;
195
+ }
196
+
197
+ this.config = normalized;
198
+ syncPermissionSystemStatus(ctx, normalized);
199
+ this.lastConfigWarning = null;
200
+
201
+ this.deps.logger.writeDebugLog("config.saved", {
202
+ debugLog: normalized.debugLog,
203
+ permissionReviewLog: normalized.permissionReviewLog,
204
+ yoloMode: normalized.yoloMode,
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Write the resolved config path set to the review and debug logs.
210
+ *
211
+ * Equivalent to `logResolvedConfigPaths(runtime)`.
212
+ */
213
+ logResolvedPaths(): void {
214
+ const policyPaths = this.deps.policyPaths.getResolvedPolicyPaths();
215
+ const cwd = this.deps.context.get()?.cwd ?? null;
216
+ const { agentDir } = this.deps;
217
+ const legacyGlobalPolicyDetected = existsSync(
218
+ getLegacyGlobalPolicyPath(agentDir),
219
+ );
220
+ const legacyProjectPolicyDetected = cwd
221
+ ? existsSync(getLegacyProjectPolicyPath(cwd))
222
+ : false;
223
+ const legacyExtConfigPath = getLegacyExtensionConfigPath(EXTENSION_ROOT);
224
+ const newGlobalPath = getGlobalConfigPath(agentDir);
225
+ const legacyExtensionConfigDetected =
226
+ normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
227
+ existsSync(legacyExtConfigPath);
228
+ const entry = buildResolvedConfigLogEntry({
229
+ policyPaths,
230
+ legacyGlobalPolicyDetected,
231
+ legacyProjectPolicyDetected,
232
+ legacyExtensionConfigDetected,
233
+ });
234
+ this.deps.logger.writeReviewLog(
235
+ "config.resolved",
236
+ entry as unknown as Record<string, unknown>,
237
+ );
238
+ this.deps.logger.writeDebugLog(
239
+ "config.resolved",
240
+ entry as unknown as Record<string, unknown>,
241
+ );
242
+ }
243
+ }
package/src/index.ts CHANGED
@@ -18,15 +18,11 @@ import { SkillInputGatePipeline } from "./handlers/gates/skill-input-gate-pipeli
18
18
  import { ToolCallGatePipeline } from "./handlers/gates/tool-call-gate-pipeline";
19
19
  import { requestPermissionDecisionFromUi } from "./permission-dialog";
20
20
  import { registerPermissionRpcHandlers } from "./permission-event-rpc";
21
+ import { PermissionManager } from "./permission-manager";
21
22
  import { PermissionPrompter } from "./permission-prompter";
22
23
  import { PermissionSession } from "./permission-session";
23
24
  import { LocalPermissionsService } from "./permissions-service";
24
- import {
25
- createExtensionRuntime,
26
- logResolvedConfigPaths,
27
- refreshExtensionConfig,
28
- saveExtensionConfig,
29
- } from "./runtime";
25
+ import { createExtensionRuntime } from "./runtime";
30
26
  import { PermissionServiceLifecycle } from "./service-lifecycle";
31
27
  import { createSessionLogger } from "./session-logger";
32
28
  import { isSubagentExecutionContext } from "./subagent-context";
@@ -56,18 +52,20 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
56
52
  writeReviewLog: runtime.writeReviewLog.bind(runtime),
57
53
  requestPermissionDecisionFromUi,
58
54
  shouldAutoApprove: () =>
59
- shouldAutoApprovePermissionState("ask", runtime.config),
55
+ shouldAutoApprovePermissionState("ask", runtime.configStore.current()),
60
56
  };
61
57
  const forwarder = new PermissionForwarder(forwardingDeps);
62
58
 
63
59
  const prompter = new PermissionPrompter({
64
- getConfig: () => runtime.config,
60
+ config: runtime.configStore,
65
61
  writeReviewLog: runtime.writeReviewLog.bind(runtime),
66
62
  events: pi.events,
67
63
  forwarder,
68
64
  });
69
65
 
70
- refreshExtensionConfig(runtime);
66
+ runtime.configStore.refresh();
67
+
68
+ const sessionManager = new PermissionManager({ agentDir: runtime.agentDir });
71
69
 
72
70
  const session = new PermissionSession(
73
71
  runtime,
@@ -77,13 +75,12 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
77
75
  forwarder,
78
76
  subagentRegistry,
79
77
  ),
78
+ sessionManager,
79
+ runtime.configStore,
80
80
  {
81
- refreshExtensionConfig: (ctx) => refreshExtensionConfig(runtime, ctx),
82
- logResolvedConfigPaths: () => logResolvedConfigPaths(runtime),
83
- getConfig: () => runtime.config,
84
81
  canRequestPermissionConfirmation: (ctx) =>
85
82
  canResolveAskPermissionRequest({
86
- config: runtime.config,
83
+ config: runtime.configStore.current(),
87
84
  hasUI: ctx.hasUI,
88
85
  isSubagent: isSubagentExecutionContext(
89
86
  ctx,
@@ -96,8 +93,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
96
93
  );
97
94
 
98
95
  registerPermissionSystemCommand(pi, {
99
- getConfig: () => runtime.config,
100
- setConfig: (next, ctx) => saveExtensionConfig(runtime, next, ctx),
96
+ config: runtime.configStore,
101
97
  getConfigPath: () => getGlobalConfigPath(runtime.agentDir),
102
98
  getComposedRules: () =>
103
99
  runtime.permissionManager.getComposedConfigRules(
@@ -1,4 +1,6 @@
1
+ import { join } from "node:path";
1
2
  import { isPermissionState } from "./common";
3
+ import { getGlobalConfigPath, getProjectConfigPath } from "./config-paths";
2
4
  import { normalizeInput } from "./input-normalizer";
3
5
  import { normalizeFlatConfig } from "./normalize";
4
6
  import {
@@ -48,19 +50,66 @@ type ResolvedPermissions = {
48
50
  composedRules: Ruleset;
49
51
  };
50
52
 
53
+ /**
54
+ * Narrow interface for session-scoped permission checking.
55
+ * `PermissionSession` depends on this — not the full concrete class — so
56
+ * test mocks can satisfy it without an `as unknown as PermissionManager` cast.
57
+ */
58
+ export interface ScopedPermissionManager {
59
+ configureForCwd(cwd: string | undefined | null): void;
60
+ checkPermission(
61
+ toolName: string,
62
+ input: unknown,
63
+ agentName?: string,
64
+ sessionRules?: Ruleset,
65
+ ): PermissionCheckResult;
66
+ getToolPermission(toolName: string, agentName?: string): PermissionState;
67
+ getConfigIssues(agentName?: string): string[];
68
+ getPolicyCacheStamp(agentName?: string): string;
69
+ }
70
+
51
71
  export interface PermissionManagerOptions extends PolicyLoaderOptions {
52
72
  policyLoader?: PolicyLoader;
73
+ /**
74
+ * Pi agent directory. When provided, the manager derives all loader paths
75
+ * from this value and supports {@link PermissionManager.configureForCwd}.
76
+ */
77
+ agentDir?: string;
53
78
  }
54
79
 
55
- export class PermissionManager {
56
- private readonly loader: PolicyLoader;
80
+ export class PermissionManager implements ScopedPermissionManager {
81
+ private readonly agentDir: string | undefined;
82
+ private loader: PolicyLoader;
57
83
  private readonly resolvedPermissionsCache = new Map<
58
84
  string,
59
85
  FileCacheEntry<ResolvedPermissions>
60
86
  >();
61
87
 
62
88
  constructor(options: PermissionManagerOptions = {}) {
63
- this.loader = options.policyLoader ?? new FilePolicyLoader(options);
89
+ this.agentDir = options.agentDir;
90
+ this.loader =
91
+ options.policyLoader ??
92
+ new FilePolicyLoader(
93
+ options.agentDir !== undefined
94
+ ? derivePolicyLoaderOptions(options.agentDir, undefined)
95
+ : options,
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Rebuild the policy loader for a new working directory and clear the
101
+ * resolved-permissions cache.
102
+ *
103
+ * When `agentDir` was not provided at construction (e.g. test managers
104
+ * built with explicit paths), only the cache is cleared.
105
+ */
106
+ configureForCwd(cwd: string | undefined | null): void {
107
+ if (this.agentDir !== undefined) {
108
+ this.loader = new FilePolicyLoader(
109
+ derivePolicyLoaderOptions(this.agentDir, cwd),
110
+ );
111
+ }
112
+ this.resolvedPermissionsCache.clear();
64
113
  }
65
114
 
66
115
  getConfigIssues(agentName?: string): string[] {
@@ -219,6 +268,23 @@ export class PermissionManager {
219
268
  }
220
269
  }
221
270
 
271
+ /**
272
+ * Derive `PolicyLoaderOptions` from an agentDir + an optional cwd.
273
+ * Setting agentsDir explicitly from agentDir removes the hidden
274
+ * `getAgentDir()` env-read that FilePolicyLoader's default would perform.
275
+ */
276
+ function derivePolicyLoaderOptions(
277
+ agentDir: string,
278
+ cwd: string | undefined | null,
279
+ ): PolicyLoaderOptions {
280
+ return {
281
+ globalConfigPath: getGlobalConfigPath(agentDir),
282
+ agentsDir: join(agentDir, "agents"),
283
+ projectGlobalConfigPath: cwd ? getProjectConfigPath(cwd) : undefined,
284
+ projectAgentsDir: cwd ? join(cwd, ".pi", "agent", "agents") : undefined,
285
+ };
286
+ }
287
+
222
288
  /**
223
289
  * Map a matched rule + tool name to the correct PermissionCheckResult.source.
224
290
  *
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { PermissionSystemExtensionConfig } from "./extension-config";
2
+ import type { ConfigReader } from "./config-store";
3
3
  import type { ApprovalRequester } from "./forwarded-permissions/permission-forwarder";
4
4
  import type { PermissionPromptDecision } from "./permission-dialog";
5
5
  import {
@@ -45,7 +45,7 @@ export interface PermissionPrompterApi {
45
45
  */
46
46
  export interface PermissionPrompterDeps {
47
47
  /** Read current config for yolo-mode check (called at prompt time). */
48
- getConfig(): PermissionSystemExtensionConfig;
48
+ config: ConfigReader;
49
49
  /** Write structured entries to the permission review log. */
50
50
  writeReviewLog(event: string, details: Record<string, unknown>): void;
51
51
  /** Event bus used for UI prompt broadcasts. */
@@ -72,7 +72,7 @@ export class PermissionPrompter implements PermissionPrompterApi {
72
72
  ctx: ExtensionContext,
73
73
  details: PromptPermissionDetails,
74
74
  ): Promise<PermissionPromptDecision> {
75
- if (shouldAutoApprovePermissionState("ask", this.deps.getConfig())) {
75
+ if (shouldAutoApprovePermissionState("ask", this.deps.config.current())) {
76
76
  this.writeReviewEntry("permission_request.auto_approved", details);
77
77
  return { approved: true, state: "approved", autoApproved: true };
78
78
  }
@@ -5,17 +5,17 @@ import {
5
5
  getActiveAgentNameFromSystemPrompt,
6
6
  } from "./active-agent";
7
7
  import type { AgentPrepSession } from "./agent-prep-session";
8
+ import type { SessionConfigStore } from "./config-store";
8
9
  import type { PermissionSystemExtensionConfig } from "./extension-config";
9
10
  import type { ExtensionPaths } from "./extension-paths";
10
11
  import type { ForwardingController } from "./forwarding-manager";
11
12
  import type { GateHandlerSession } from "./gate-handler-session";
12
13
  import type { GatePrompter } from "./gate-prompter";
13
14
  import type { PermissionPromptDecision } from "./permission-dialog";
14
- import type { PermissionManager } from "./permission-manager";
15
+ import type { ScopedPermissionManager } from "./permission-manager";
15
16
  import type { PromptPermissionDetails } from "./permission-prompter";
16
17
  import type { PermissionResolver } from "./permission-resolver";
17
18
  import type { Rule } from "./rule";
18
- import { createPermissionManagerForCwd } from "./runtime";
19
19
  import type { SessionApproval } from "./session-approval";
20
20
  import type { SessionApprovalRecorder } from "./session-approval-recorder";
21
21
  import type { SessionLifecycleSession } from "./session-lifecycle-session";
@@ -35,12 +35,6 @@ import type { PermissionCheckResult, PermissionState } from "./types";
35
35
  * where the `ExtensionRuntime` is available.
36
36
  */
37
37
  export interface PermissionSessionRuntimeDeps {
38
- /** Reload merged config from disk; optionally update the stored runtime context. */
39
- refreshExtensionConfig(ctx?: ExtensionContext): void;
40
- /** Write the resolved config path set to the review and debug logs. */
41
- logResolvedConfigPaths(): void;
42
- /** Read current extension config (called at query time). */
43
- getConfig(): PermissionSystemExtensionConfig;
44
38
  /** Whether the current context can show an interactive permission prompt. */
45
39
  canRequestPermissionConfirmation(ctx: ExtensionContext): boolean;
46
40
  /** Prompt the user for a permission decision, log the outcome, and return it. */
@@ -62,7 +56,8 @@ export interface PermissionSessionRuntimeDeps {
62
56
  * - `ExtensionPaths` — immutable path constants
63
57
  * - `SessionLogger` — debug + review + warn
64
58
  * - `ForwardingController` — polling lifecycle
65
- * - `PermissionSessionRuntimeDeps` — config refresh + log delegates
59
+ * - `SessionConfigStore` — owns extension config; provides refresh, log, read
60
+ * - `PermissionSessionRuntimeDeps` — prompting + permission-confirmation bridge
66
61
  */
67
62
  export class PermissionSession
68
63
  implements
@@ -74,7 +69,6 @@ export class PermissionSession
74
69
  SessionLifecycleSession
75
70
  {
76
71
  private context: ExtensionContext | null = null;
77
- private permissionManager: PermissionManager;
78
72
  private readonly sessionRules = new SessionRules();
79
73
  private skillEntries: SkillPromptEntry[] = [];
80
74
  private knownAgentName: string | null = null;
@@ -85,13 +79,10 @@ export class PermissionSession
85
79
  private readonly paths: ExtensionPaths,
86
80
  readonly logger: SessionLogger,
87
81
  private readonly forwarding: ForwardingController,
82
+ private readonly permissionManager: ScopedPermissionManager,
83
+ private readonly configStore: SessionConfigStore,
88
84
  private readonly runtimeDeps: PermissionSessionRuntimeDeps,
89
- ) {
90
- this.permissionManager = createPermissionManagerForCwd(
91
- paths.agentDir,
92
- undefined,
93
- );
94
- }
85
+ ) {}
95
86
 
96
87
  // ── Context lifecycle ──────────────────────────────────────────────────
97
88
 
@@ -173,14 +164,11 @@ export class PermissionSession
173
164
  /**
174
165
  * Reset all mutable state for a new session.
175
166
  *
176
- * Creates a fresh PermissionManager scoped to `ctx.cwd`, clears caches,
167
+ * Configures the injected PermissionManager for `ctx.cwd`, clears caches,
177
168
  * skill entries, and activates the new context.
178
169
  */
179
170
  resetForNewSession(ctx: ExtensionContext): void {
180
- this.permissionManager = createPermissionManagerForCwd(
181
- this.paths.agentDir,
182
- ctx.cwd,
183
- );
171
+ this.permissionManager.configureForCwd(ctx.cwd);
184
172
  this.skillEntries = [];
185
173
  this.toolsCacheKey = null;
186
174
  this.promptCacheKey = null;
@@ -204,10 +192,7 @@ export class PermissionSession
204
192
  * Used on config reload (e.g. `resources_discover` with reason "reload").
205
193
  */
206
194
  reload(): void {
207
- this.permissionManager = createPermissionManagerForCwd(
208
- this.paths.agentDir,
209
- this.context?.cwd,
210
- );
195
+ this.permissionManager.configureForCwd(this.context?.cwd);
211
196
  this.skillEntries = [];
212
197
  this.toolsCacheKey = null;
213
198
  this.promptCacheKey = null;
@@ -272,17 +257,17 @@ export class PermissionSession
272
257
 
273
258
  /** Reload merged config from disk; optionally update the stored runtime context. */
274
259
  refreshConfig(ctx?: ExtensionContext): void {
275
- this.runtimeDeps.refreshExtensionConfig(ctx);
260
+ this.configStore.refresh(ctx);
276
261
  }
277
262
 
278
263
  /** Write the resolved config path set to the review and debug logs. */
279
264
  logResolvedConfigPaths(): void {
280
- this.runtimeDeps.logResolvedConfigPaths();
265
+ this.configStore.logResolvedPaths();
281
266
  }
282
267
 
283
268
  /** Read current extension config. */
284
269
  get config(): PermissionSystemExtensionConfig {
285
- return this.runtimeDeps.getConfig();
270
+ return this.configStore.current();
286
271
  }
287
272
 
288
273
  // ── Infrastructure paths ───────────────────────────────────────────────