@gotgenes/pi-permission-system 10.2.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/src/runtime.ts CHANGED
@@ -1,34 +1,12 @@
1
+ import { join } from "node:path";
1
2
  import {
2
- existsSync,
3
- mkdirSync,
4
- renameSync,
5
- unlinkSync,
6
- writeFileSync,
7
- } from "node:fs";
8
- import { dirname, join, normalize } from "node:path";
9
- import {
10
- type ExtensionCommandContext,
11
3
  type ExtensionContext,
12
4
  getAgentDir,
13
5
  } from "@earendil-works/pi-coding-agent";
14
6
 
15
- import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
16
- import {
17
- DEBUG_LOG_FILENAME,
18
- getGlobalConfigPath,
19
- getLegacyExtensionConfigPath,
20
- getLegacyGlobalPolicyPath,
21
- getLegacyProjectPolicyPath,
22
- REVIEW_LOG_FILENAME,
23
- } from "./config-paths";
24
- import { buildResolvedConfigLogEntry } from "./config-reporter";
25
- import {
26
- DEFAULT_EXTENSION_CONFIG,
27
- EXTENSION_ROOT,
28
- ensurePermissionSystemLogsDirectory,
29
- normalizePermissionSystemConfig,
30
- type PermissionSystemExtensionConfig,
31
- } from "./extension-config";
7
+ import { DEBUG_LOG_FILENAME, REVIEW_LOG_FILENAME } from "./config-paths";
8
+ import { ConfigStore, type RuntimeContextRef } from "./config-store";
9
+ import { ensurePermissionSystemLogsDirectory } from "./extension-config";
32
10
  import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
33
11
 
34
12
  export type { ExtensionPaths } from "./extension-paths";
@@ -37,7 +15,6 @@ import { createPermissionSystemLogger } from "./logging";
37
15
  import { PermissionManager } from "./permission-manager";
38
16
  import { SessionRules } from "./session-rules";
39
17
  import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
40
- import { syncPermissionSystemStatus } from "./status";
41
18
 
42
19
  /**
43
20
  * Mutable session state — the subset of ExtensionRuntime that holds
@@ -67,144 +44,14 @@ interface SessionState {
67
44
  * without timing issues around `PI_CODING_AGENT_DIR`.
68
45
  */
69
46
  export interface ExtensionRuntime extends ExtensionPaths, SessionState {
70
- // ── Mutable state (beyond SessionState) ───────────────────────────────────
71
- config: PermissionSystemExtensionConfig;
72
- lastConfigWarning: string | null;
47
+ /** The store that owns extension config. */
48
+ configStore: ConfigStore;
73
49
 
74
50
  // ── Logging (backed by logger created at construction) ─────────────────
75
51
  writeDebugLog(event: string, details?: Record<string, unknown>): void;
76
52
  writeReviewLog(event: string, details?: Record<string, unknown>): void;
77
53
  }
78
54
 
79
- /**
80
- * Reload merged config from disk into the runtime.
81
- * If `ctx` is provided, updates `runtime.runtimeContext` first.
82
- */
83
- export function refreshExtensionConfig(
84
- runtime: ExtensionRuntime,
85
- ctx?: ExtensionContext,
86
- ): void {
87
- if (ctx) {
88
- runtime.runtimeContext = ctx;
89
- }
90
- const cwd = runtime.runtimeContext?.cwd ?? null;
91
- const mergeResult = loadAndMergeConfigs(
92
- runtime.agentDir,
93
- cwd ?? "",
94
- EXTENSION_ROOT,
95
- );
96
- const runtimeConfig = normalizePermissionSystemConfig(mergeResult.merged);
97
- runtime.config = runtimeConfig;
98
-
99
- if (runtime.runtimeContext?.hasUI) {
100
- syncPermissionSystemStatus(runtime.runtimeContext, runtimeConfig);
101
- }
102
-
103
- const warning =
104
- mergeResult.issues.length > 0 ? mergeResult.issues.join("\n") : undefined;
105
-
106
- if (warning && warning !== runtime.lastConfigWarning) {
107
- runtime.lastConfigWarning = warning;
108
- runtime.runtimeContext?.ui.notify(warning, "warning");
109
- } else if (!warning) {
110
- runtime.lastConfigWarning = null;
111
- }
112
-
113
- runtime.writeDebugLog("config.loaded", {
114
- warning: warning ?? null,
115
- debugLog: runtimeConfig.debugLog,
116
- permissionReviewLog: runtimeConfig.permissionReviewLog,
117
- yoloMode: runtimeConfig.yoloMode,
118
- });
119
- }
120
-
121
- /**
122
- * Save updated runtime knobs (debugLog, permissionReviewLog, yoloMode) to the
123
- * global config file, then update runtime.config and sync UI status.
124
- */
125
- export function saveExtensionConfig(
126
- runtime: ExtensionRuntime,
127
- next: PermissionSystemExtensionConfig,
128
- ctx: ExtensionCommandContext,
129
- ): void {
130
- const normalized = normalizePermissionSystemConfig(next);
131
- const globalPath = getGlobalConfigPath(runtime.agentDir);
132
-
133
- const existing = loadUnifiedConfig(globalPath);
134
- const merged = {
135
- ...existing.config,
136
- debugLog: normalized.debugLog,
137
- permissionReviewLog: normalized.permissionReviewLog,
138
- yoloMode: normalized.yoloMode,
139
- };
140
-
141
- const tmpPath = `${globalPath}.tmp`;
142
- try {
143
- mkdirSync(dirname(globalPath), { recursive: true });
144
- writeFileSync(tmpPath, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
145
- renameSync(tmpPath, globalPath);
146
- } catch (error) {
147
- try {
148
- if (existsSync(tmpPath)) {
149
- unlinkSync(tmpPath);
150
- }
151
- } catch {
152
- // Ignore cleanup failures.
153
- }
154
- const message = error instanceof Error ? error.message : String(error);
155
- ctx.ui.notify(
156
- `Failed to save permission-system config at '${globalPath}': ${message}`,
157
- "error",
158
- );
159
- return;
160
- }
161
-
162
- runtime.config = normalized;
163
- syncPermissionSystemStatus(ctx, normalized);
164
- runtime.lastConfigWarning = null;
165
-
166
- runtime.writeDebugLog("config.saved", {
167
- debugLog: normalized.debugLog,
168
- permissionReviewLog: normalized.permissionReviewLog,
169
- yoloMode: normalized.yoloMode,
170
- });
171
- }
172
-
173
- /**
174
- * Write the resolved config path set (global, project, legacy) to the review
175
- * and debug logs.
176
- */
177
- export function logResolvedConfigPaths(runtime: ExtensionRuntime): void {
178
- const policyPaths = runtime.permissionManager.getResolvedPolicyPaths();
179
- const cwd = runtime.runtimeContext?.cwd ?? null;
180
- const { agentDir } = runtime;
181
- const legacyGlobalPolicyDetected = existsSync(
182
- getLegacyGlobalPolicyPath(agentDir),
183
- );
184
- const legacyProjectPolicyDetected = cwd
185
- ? existsSync(getLegacyProjectPolicyPath(cwd))
186
- : false;
187
- const legacyExtConfigPath = getLegacyExtensionConfigPath(EXTENSION_ROOT);
188
- const newGlobalPath = getGlobalConfigPath(agentDir);
189
- const legacyExtensionConfigDetected =
190
- normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
191
- existsSync(legacyExtConfigPath);
192
- const entry = buildResolvedConfigLogEntry({
193
- policyPaths,
194
- legacyGlobalPolicyDetected,
195
- legacyProjectPolicyDetected,
196
- legacyExtensionConfigDetected,
197
- });
198
- runtime.writeReviewLog(
199
- "config.resolved",
200
- entry as unknown as Record<string, unknown>,
201
- );
202
- runtime.writeDebugLog(
203
- "config.resolved",
204
- entry as unknown as Record<string, unknown>,
205
- );
206
- }
207
-
208
55
  // ── Factory ────────────────────────────────────────────────────────────────
209
56
 
210
57
  /**
@@ -219,28 +66,49 @@ export function createExtensionRuntime(options?: {
219
66
  const agentDir = options?.agentDir ?? getAgentDir();
220
67
  const paths = computeExtensionPaths(agentDir);
221
68
 
222
- // Build a plain-object runtime first so the logger's `getConfig` closure
223
- // can reference `runtime.config` directly (always reads current value).
69
+ const permissionManager = new PermissionManager({ agentDir });
70
+
224
71
  const runtime: ExtensionRuntime = {
225
72
  ...paths,
226
- config: { ...DEFAULT_EXTENSION_CONFIG },
227
73
  runtimeContext: null,
228
- permissionManager: new PermissionManager({ agentDir }),
74
+ configStore: null as unknown as ConfigStore,
75
+ permissionManager,
229
76
  activeSkillEntries: [],
230
77
  lastKnownActiveAgentName: null,
231
78
  lastActiveToolsCacheKey: null,
232
79
  lastPromptStateCacheKey: null,
233
- lastConfigWarning: null,
234
80
  sessionRules: new SessionRules(),
235
81
  // Logging methods are replaced below after the logger is constructed.
236
82
  writeDebugLog: () => {},
237
83
  writeReviewLog: () => {},
238
84
  };
239
85
 
86
+ // Transitional RuntimeContextRef: reads/writes the still-runtime-owned
87
+ // `runtimeContext` field until Step 4 (#337) unifies context onto
88
+ // PermissionSession.
89
+ const contextRef: RuntimeContextRef = {
90
+ get: () => runtime.runtimeContext,
91
+ set: (ctx) => {
92
+ runtime.runtimeContext = ctx;
93
+ },
94
+ };
95
+
96
+ const configStore = new ConfigStore({
97
+ agentDir,
98
+ context: contextRef,
99
+ policyPaths: permissionManager,
100
+ logger: {
101
+ // Deferred-binding: `runtime.writeDebugLog` is replaced below after
102
+ // the logger is constructed — same deferred pattern as before Step 2.
103
+ writeDebugLog: (e, d) => runtime.writeDebugLog(e, d),
104
+ writeReviewLog: (e, d) => runtime.writeReviewLog(e, d),
105
+ },
106
+ });
107
+ runtime.configStore = configStore;
108
+
240
109
  const reportedLoggingWarnings = new Set<string>();
241
110
  const logger = createPermissionSystemLogger({
242
- // Reads runtime.config at call time — always current.
243
- getConfig: () => runtime.config,
111
+ getConfig: () => configStore.current(),
244
112
  debugLogPath: join(paths.globalLogsDir, DEBUG_LOG_FILENAME),
245
113
  reviewLogPath: join(paths.globalLogsDir, REVIEW_LOG_FILENAME),
246
114
  ensureLogsDirectory: () =>
@@ -252,7 +120,6 @@ export function createExtensionRuntime(options?: {
252
120
  return;
253
121
  }
254
122
  reportedLoggingWarnings.add(message);
255
- // Reads runtime.runtimeContext at call time — always current.
256
123
  runtime.runtimeContext?.ui.notify(message, "warning");
257
124
  };
258
125
 
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { expect, test, vi } from "vitest";
5
5
  import { registerPermissionSystemCommand } from "#src/config-modal";
6
+ import type { CommandConfigStore } from "#src/config-store";
6
7
  import {
7
8
  DEFAULT_EXTENSION_CONFIG,
8
9
  normalizePermissionSystemConfig,
@@ -79,11 +80,14 @@ test("permission-system command completions expose top-level config actions", ()
79
80
  let config: PermissionSystemExtensionConfig = { ...DEFAULT_EXTENSION_CONFIG };
80
81
 
81
82
  try {
82
- const controller = {
83
- getConfig: () => config,
84
- setConfig: (next: PermissionSystemExtensionConfig) => {
83
+ const configStore: CommandConfigStore = {
84
+ current: () => config,
85
+ save: (next) => {
85
86
  config = next;
86
87
  },
88
+ };
89
+ const controller = {
90
+ config: configStore,
87
91
  getConfigPath: () => configPath,
88
92
  };
89
93
 
@@ -136,9 +140,9 @@ test("permission-system command handlers manage config summary, persistence, and
136
140
  "utf-8",
137
141
  );
138
142
 
139
- const controller = {
140
- getConfig: () => config,
141
- setConfig: (next: PermissionSystemExtensionConfig) => {
143
+ const configStore: CommandConfigStore = {
144
+ current: () => config,
145
+ save: (next) => {
142
146
  const currentConfig = normalizePermissionSystemConfig(
143
147
  JSON.parse(readFileSync(configPath, "utf-8")) as unknown,
144
148
  );
@@ -153,6 +157,9 @@ test("permission-system command handlers manage config summary, persistence, and
153
157
  );
154
158
  expect(config).not.toEqual(currentConfig);
155
159
  },
160
+ };
161
+ const controller = {
162
+ config: configStore,
156
163
  getConfigPath: () => configPath,
157
164
  };
158
165
 
@@ -249,8 +256,7 @@ test("show output includes rule origins when getComposedRules is provided", asyn
249
256
  ];
250
257
 
251
258
  const controller = {
252
- getConfig: () => config,
253
- setConfig: () => {},
259
+ config: { current: () => config, save: () => {} } as CommandConfigStore,
254
260
  getConfigPath: () => "/fake/config.json",
255
261
  getComposedRules: () => composedRules,
256
262
  };
@@ -282,8 +288,7 @@ test("show output omits rule summary when getComposedRules is not provided", asy
282
288
  const config = { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true };
283
289
 
284
290
  const controller = {
285
- getConfig: () => config,
286
- setConfig: () => {},
291
+ config: { current: () => config, save: () => {} } as CommandConfigStore,
287
292
  getConfigPath: () => "/fake/config.json",
288
293
  // no getComposedRules
289
294
  };