@gotgenes/pi-permission-system 10.2.0 → 10.3.1
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 +14 -0
- package/package.json +1 -1
- package/src/config-modal.ts +7 -10
- package/src/config-store.ts +227 -0
- package/src/index.ts +65 -38
- package/src/permission-prompter.ts +3 -3
- package/src/permission-session.ts +10 -14
- package/src/permissions-service.ts +3 -5
- package/src/session-logger.ts +46 -9
- package/test/composition-root.test.ts +85 -1
- package/test/config-modal.test.ts +15 -10
- package/test/config-store.test.ts +428 -0
- package/test/permission-prompter.test.ts +12 -5
- package/test/permission-session.test.ts +57 -26
- package/test/session-logger.test.ts +151 -64
- package/src/runtime.ts +0 -280
- package/test/runtime.test.ts +0 -487
package/src/runtime.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
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
|
-
type ExtensionContext,
|
|
12
|
-
getAgentDir,
|
|
13
|
-
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
|
|
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";
|
|
32
|
-
import { computeExtensionPaths, type ExtensionPaths } from "./extension-paths";
|
|
33
|
-
|
|
34
|
-
export type { ExtensionPaths } from "./extension-paths";
|
|
35
|
-
|
|
36
|
-
import { createPermissionSystemLogger } from "./logging";
|
|
37
|
-
import { PermissionManager } from "./permission-manager";
|
|
38
|
-
import { SessionRules } from "./session-rules";
|
|
39
|
-
import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
|
|
40
|
-
import { syncPermissionSystemStatus } from "./status";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Mutable session state — the subset of ExtensionRuntime that holds
|
|
44
|
-
* per-session fields. `PermissionSession` now owns these for handler
|
|
45
|
-
* use; this interface remains so `ExtensionRuntime` can still serve
|
|
46
|
-
* as the internal composition root (config-modal, RPC handlers).
|
|
47
|
-
*/
|
|
48
|
-
interface SessionState {
|
|
49
|
-
runtimeContext: ExtensionContext | null;
|
|
50
|
-
permissionManager: PermissionManager;
|
|
51
|
-
readonly sessionRules: SessionRules;
|
|
52
|
-
activeSkillEntries: SkillPromptEntry[];
|
|
53
|
-
lastKnownActiveAgentName: string | null;
|
|
54
|
-
lastActiveToolsCacheKey: string | null;
|
|
55
|
-
lastPromptStateCacheKey: string | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Runtime context object created once inside `piPermissionSystemExtension()`.
|
|
60
|
-
*
|
|
61
|
-
* Holds all path constants (derived from `getAgentDir()` at construction time),
|
|
62
|
-
* mutable extension state, and the log-writing methods — eliminating the
|
|
63
|
-
* module-scope cached constants and setter-injection pattern that previously
|
|
64
|
-
* lived in `src/index.ts`.
|
|
65
|
-
*
|
|
66
|
-
* Tests construct this via `createExtensionRuntime({ agentDir: tmpDir })`
|
|
67
|
-
* without timing issues around `PI_CODING_AGENT_DIR`.
|
|
68
|
-
*/
|
|
69
|
-
export interface ExtensionRuntime extends ExtensionPaths, SessionState {
|
|
70
|
-
// ── Mutable state (beyond SessionState) ───────────────────────────────────
|
|
71
|
-
config: PermissionSystemExtensionConfig;
|
|
72
|
-
lastConfigWarning: string | null;
|
|
73
|
-
|
|
74
|
-
// ── Logging (backed by logger created at construction) ─────────────────
|
|
75
|
-
writeDebugLog(event: string, details?: Record<string, unknown>): void;
|
|
76
|
-
writeReviewLog(event: string, details?: Record<string, unknown>): void;
|
|
77
|
-
}
|
|
78
|
-
|
|
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
|
-
// ── Factory ────────────────────────────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Create a fully-initialized `ExtensionRuntime`.
|
|
212
|
-
*
|
|
213
|
-
* Calls `getAgentDir()` at invocation time (never at module scope), so tests
|
|
214
|
-
* may set `PI_CODING_AGENT_DIR` before calling the factory.
|
|
215
|
-
*/
|
|
216
|
-
export function createExtensionRuntime(options?: {
|
|
217
|
-
agentDir?: string;
|
|
218
|
-
}): ExtensionRuntime {
|
|
219
|
-
const agentDir = options?.agentDir ?? getAgentDir();
|
|
220
|
-
const paths = computeExtensionPaths(agentDir);
|
|
221
|
-
|
|
222
|
-
// Build a plain-object runtime first so the logger's `getConfig` closure
|
|
223
|
-
// can reference `runtime.config` directly (always reads current value).
|
|
224
|
-
const runtime: ExtensionRuntime = {
|
|
225
|
-
...paths,
|
|
226
|
-
config: { ...DEFAULT_EXTENSION_CONFIG },
|
|
227
|
-
runtimeContext: null,
|
|
228
|
-
permissionManager: new PermissionManager({ agentDir }),
|
|
229
|
-
activeSkillEntries: [],
|
|
230
|
-
lastKnownActiveAgentName: null,
|
|
231
|
-
lastActiveToolsCacheKey: null,
|
|
232
|
-
lastPromptStateCacheKey: null,
|
|
233
|
-
lastConfigWarning: null,
|
|
234
|
-
sessionRules: new SessionRules(),
|
|
235
|
-
// Logging methods are replaced below after the logger is constructed.
|
|
236
|
-
writeDebugLog: () => {},
|
|
237
|
-
writeReviewLog: () => {},
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const reportedLoggingWarnings = new Set<string>();
|
|
241
|
-
const logger = createPermissionSystemLogger({
|
|
242
|
-
// Reads runtime.config at call time — always current.
|
|
243
|
-
getConfig: () => runtime.config,
|
|
244
|
-
debugLogPath: join(paths.globalLogsDir, DEBUG_LOG_FILENAME),
|
|
245
|
-
reviewLogPath: join(paths.globalLogsDir, REVIEW_LOG_FILENAME),
|
|
246
|
-
ensureLogsDirectory: () =>
|
|
247
|
-
ensurePermissionSystemLogsDirectory(paths.globalLogsDir),
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const reportLoggingWarning = (message: string): void => {
|
|
251
|
-
if (reportedLoggingWarnings.has(message)) {
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
reportedLoggingWarnings.add(message);
|
|
255
|
-
// Reads runtime.runtimeContext at call time — always current.
|
|
256
|
-
runtime.runtimeContext?.ui.notify(message, "warning");
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
runtime.writeDebugLog = (
|
|
260
|
-
event: string,
|
|
261
|
-
details: Record<string, unknown> = {},
|
|
262
|
-
): void => {
|
|
263
|
-
const warning = logger.debug(event, details);
|
|
264
|
-
if (warning) {
|
|
265
|
-
reportLoggingWarning(warning);
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
runtime.writeReviewLog = (
|
|
270
|
-
event: string,
|
|
271
|
-
details: Record<string, unknown> = {},
|
|
272
|
-
): void => {
|
|
273
|
-
const warning = logger.review(event, details);
|
|
274
|
-
if (warning) {
|
|
275
|
-
reportLoggingWarning(warning);
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
return runtime;
|
|
280
|
-
}
|