@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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/config-modal.ts +7 -10
- package/src/config-store.ts +243 -0
- package/src/index.ts +7 -15
- package/src/permission-prompter.ts +3 -3
- package/src/permission-session.ts +7 -10
- package/src/runtime.ts +34 -167
- package/test/config-modal.test.ts +15 -10
- package/test/config-store.test.ts +452 -0
- package/test/permission-prompter.test.ts +12 -5
- package/test/permission-session.test.ts +44 -26
- package/test/runtime.test.ts +10 -194
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 {
|
|
16
|
-
import {
|
|
17
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
286
|
-
setConfig: () => {},
|
|
291
|
+
config: { current: () => config, save: () => {} } as CommandConfigStore,
|
|
287
292
|
getConfigPath: () => "/fake/config.json",
|
|
288
293
|
// no getComposedRules
|
|
289
294
|
};
|