@gotgenes/pi-permission-system 1.2.1 → 3.0.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 +44 -0
- package/LICENSE +1 -1
- package/README.md +93 -36
- package/config/config.example.json +6 -0
- package/package.json +1 -1
- package/schemas/permissions.schema.json +18 -4
- package/src/config-loader.ts +398 -0
- package/src/config-paths.ts +34 -0
- package/src/config-reporter.ts +16 -8
- package/src/index.ts +98 -112
- package/src/permission-manager.ts +25 -111
- package/tests/config-loader.test.ts +364 -0
- package/tests/config-paths.test.ts +78 -0
- package/tests/config-reporter.test.ts +42 -33
- package/tests/extension-config.test.ts +51 -0
- package/tests/permission-system.test.ts +9 -26
- package/tests/session-start.test.ts +8 -33
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
loadAndMergeConfigs,
|
|
8
|
+
loadUnifiedConfig,
|
|
9
|
+
mergeUnifiedConfigs,
|
|
10
|
+
} from "../src/config-loader.js";
|
|
11
|
+
|
|
12
|
+
describe("loadUnifiedConfig", () => {
|
|
13
|
+
let tempDir: string;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = mkdtempSync(join(tmpdir(), "config-loader-test-"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("parses a valid JSON file with runtime knobs and policy", () => {
|
|
24
|
+
const configPath = join(tempDir, "config.json");
|
|
25
|
+
writeFileSync(
|
|
26
|
+
configPath,
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
debugLog: true,
|
|
29
|
+
permissionReviewLog: false,
|
|
30
|
+
yoloMode: true,
|
|
31
|
+
defaultPolicy: { tools: "allow", bash: "deny" },
|
|
32
|
+
tools: { read: "allow", write: "deny" },
|
|
33
|
+
bash: { "git status": "allow" },
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const result = loadUnifiedConfig(configPath);
|
|
38
|
+
expect(result.issues).toEqual([]);
|
|
39
|
+
expect(result.config.debugLog).toBe(true);
|
|
40
|
+
expect(result.config.permissionReviewLog).toBe(false);
|
|
41
|
+
expect(result.config.yoloMode).toBe(true);
|
|
42
|
+
expect(result.config.defaultPolicy).toEqual({
|
|
43
|
+
tools: "allow",
|
|
44
|
+
bash: "deny",
|
|
45
|
+
});
|
|
46
|
+
expect(result.config.tools).toEqual({ read: "allow", write: "deny" });
|
|
47
|
+
expect(result.config.bash).toEqual({ "git status": "allow" });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("strips JSONC comments before parsing", () => {
|
|
51
|
+
const configPath = join(tempDir, "config.json");
|
|
52
|
+
writeFileSync(
|
|
53
|
+
configPath,
|
|
54
|
+
`{
|
|
55
|
+
// This is a comment
|
|
56
|
+
"debugLog": true,
|
|
57
|
+
/* block comment */
|
|
58
|
+
"defaultPolicy": { "tools": "ask" }
|
|
59
|
+
}`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const result = loadUnifiedConfig(configPath);
|
|
63
|
+
expect(result.issues).toEqual([]);
|
|
64
|
+
expect(result.config.debugLog).toBe(true);
|
|
65
|
+
expect(result.config.defaultPolicy).toEqual({ tools: "ask" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("ignores unknown keys without emitting issues", () => {
|
|
69
|
+
const configPath = join(tempDir, "config.json");
|
|
70
|
+
writeFileSync(
|
|
71
|
+
configPath,
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
debugLog: false,
|
|
74
|
+
unknownField: "ignored",
|
|
75
|
+
anotherRandom: 42,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const result = loadUnifiedConfig(configPath);
|
|
80
|
+
expect(result.issues).toEqual([]);
|
|
81
|
+
expect(result.config.debugLog).toBe(false);
|
|
82
|
+
expect(result.config).not.toHaveProperty("unknownField");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns defaults and no issues when the file does not exist", () => {
|
|
86
|
+
const configPath = join(tempDir, "nonexistent.json");
|
|
87
|
+
const result = loadUnifiedConfig(configPath);
|
|
88
|
+
expect(result.issues).toEqual([]);
|
|
89
|
+
expect(result.config.debugLog).toBeUndefined();
|
|
90
|
+
expect(result.config.defaultPolicy).toBeUndefined();
|
|
91
|
+
expect(result.config.tools).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns defaults and an issue when the file contains invalid JSON", () => {
|
|
95
|
+
const configPath = join(tempDir, "config.json");
|
|
96
|
+
writeFileSync(configPath, "not valid json {{{");
|
|
97
|
+
|
|
98
|
+
const result = loadUnifiedConfig(configPath);
|
|
99
|
+
expect(result.issues).toHaveLength(1);
|
|
100
|
+
expect(result.issues[0]).toContain(configPath);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("normalizes boolean fields strictly", () => {
|
|
104
|
+
const configPath = join(tempDir, "config.json");
|
|
105
|
+
writeFileSync(
|
|
106
|
+
configPath,
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
debugLog: "yes",
|
|
109
|
+
permissionReviewLog: 1,
|
|
110
|
+
yoloMode: null,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const result = loadUnifiedConfig(configPath);
|
|
115
|
+
// Non-boolean values are not included
|
|
116
|
+
expect(result.config.debugLog).toBeUndefined();
|
|
117
|
+
expect(result.config.permissionReviewLog).toBeUndefined();
|
|
118
|
+
expect(result.config.yoloMode).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("normalizes permission maps, keeping only valid PermissionState values", () => {
|
|
122
|
+
const configPath = join(tempDir, "config.json");
|
|
123
|
+
writeFileSync(
|
|
124
|
+
configPath,
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
tools: { read: "allow", write: "invalid", edit: "deny" },
|
|
127
|
+
bash: { "git *": "ask", "rm -rf": 42 },
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const result = loadUnifiedConfig(configPath);
|
|
132
|
+
expect(result.config.tools).toEqual({ read: "allow", edit: "deny" });
|
|
133
|
+
expect(result.config.bash).toEqual({ "git *": "ask" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("collects deprecated special key issues", () => {
|
|
137
|
+
const configPath = join(tempDir, "config.json");
|
|
138
|
+
writeFileSync(
|
|
139
|
+
configPath,
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
special: { doom_loop: "deny", tool_call_limit: "ask" },
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const result = loadUnifiedConfig(configPath);
|
|
146
|
+
expect(result.issues).toHaveLength(1);
|
|
147
|
+
expect(result.issues[0]).toContain("tool_call_limit");
|
|
148
|
+
expect(result.config.special).toEqual({ doom_loop: "deny" });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("mergeUnifiedConfigs", () => {
|
|
153
|
+
it("deep-merges object fields so project overrides global per-key", () => {
|
|
154
|
+
const merged = mergeUnifiedConfigs(
|
|
155
|
+
{
|
|
156
|
+
defaultPolicy: { tools: "ask", bash: "deny" },
|
|
157
|
+
tools: { read: "allow", write: "deny" },
|
|
158
|
+
bash: { "git status": "allow" },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
defaultPolicy: { tools: "allow" },
|
|
162
|
+
tools: { write: "allow", edit: "ask" },
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(merged.defaultPolicy).toEqual({ tools: "allow", bash: "deny" });
|
|
167
|
+
expect(merged.tools).toEqual({
|
|
168
|
+
read: "allow",
|
|
169
|
+
write: "allow",
|
|
170
|
+
edit: "ask",
|
|
171
|
+
});
|
|
172
|
+
expect(merged.bash).toEqual({ "git status": "allow" });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("replaces scalar runtime knobs (project wins)", () => {
|
|
176
|
+
const merged = mergeUnifiedConfigs(
|
|
177
|
+
{ debugLog: true, permissionReviewLog: true, yoloMode: false },
|
|
178
|
+
{ debugLog: false, yoloMode: true },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(merged.debugLog).toBe(false);
|
|
182
|
+
expect(merged.permissionReviewLog).toBe(true);
|
|
183
|
+
expect(merged.yoloMode).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns base unchanged when override is empty", () => {
|
|
187
|
+
const base = {
|
|
188
|
+
debugLog: true,
|
|
189
|
+
defaultPolicy: { tools: "ask" as const },
|
|
190
|
+
tools: { read: "allow" as const },
|
|
191
|
+
};
|
|
192
|
+
const merged = mergeUnifiedConfigs(base, {});
|
|
193
|
+
|
|
194
|
+
expect(merged.debugLog).toBe(true);
|
|
195
|
+
expect(merged.defaultPolicy).toEqual({ tools: "ask" });
|
|
196
|
+
expect(merged.tools).toEqual({ read: "allow" });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("returns override unchanged when base is empty", () => {
|
|
200
|
+
const override = {
|
|
201
|
+
yoloMode: true,
|
|
202
|
+
bash: { "rm -rf": "deny" as const },
|
|
203
|
+
};
|
|
204
|
+
const merged = mergeUnifiedConfigs({}, override);
|
|
205
|
+
|
|
206
|
+
expect(merged.yoloMode).toBe(true);
|
|
207
|
+
expect(merged.bash).toEqual({ "rm -rf": "deny" });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not set undefined keys in the merged result", () => {
|
|
211
|
+
const merged = mergeUnifiedConfigs({ debugLog: true }, { yoloMode: false });
|
|
212
|
+
|
|
213
|
+
expect(merged.debugLog).toBe(true);
|
|
214
|
+
expect(merged.yoloMode).toBe(false);
|
|
215
|
+
expect(merged).not.toHaveProperty("permissionReviewLog");
|
|
216
|
+
expect(merged).not.toHaveProperty("defaultPolicy");
|
|
217
|
+
expect(merged).not.toHaveProperty("tools");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("loadAndMergeConfigs", () => {
|
|
222
|
+
let tempDir: string;
|
|
223
|
+
let agentDir: string;
|
|
224
|
+
let cwd: string;
|
|
225
|
+
let extensionRoot: string;
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
tempDir = mkdtempSync(join(tmpdir(), "config-merge-test-"));
|
|
229
|
+
agentDir = join(tempDir, "agent");
|
|
230
|
+
cwd = join(tempDir, "project");
|
|
231
|
+
extensionRoot = join(tempDir, "ext");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
afterEach(() => {
|
|
235
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
function writeGlobal(content: Record<string, unknown>): void {
|
|
239
|
+
const dir = join(agentDir, "extensions", "pi-permission-system");
|
|
240
|
+
mkdirSync(dir, { recursive: true });
|
|
241
|
+
writeFileSync(join(dir, "config.json"), JSON.stringify(content));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function writeProject(content: Record<string, unknown>): void {
|
|
245
|
+
const dir = join(cwd, ".pi", "extensions", "pi-permission-system");
|
|
246
|
+
mkdirSync(dir, { recursive: true });
|
|
247
|
+
writeFileSync(join(dir, "config.json"), JSON.stringify(content));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function writeLegacyGlobalPolicy(content: Record<string, unknown>): void {
|
|
251
|
+
mkdirSync(agentDir, { recursive: true });
|
|
252
|
+
writeFileSync(
|
|
253
|
+
join(agentDir, "pi-permissions.jsonc"),
|
|
254
|
+
JSON.stringify(content),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function writeLegacyProjectPolicy(content: Record<string, unknown>): void {
|
|
259
|
+
const dir = join(cwd, ".pi", "agent");
|
|
260
|
+
mkdirSync(dir, { recursive: true });
|
|
261
|
+
writeFileSync(join(dir, "pi-permissions.jsonc"), JSON.stringify(content));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writeLegacyExtensionConfig(content: Record<string, unknown>): void {
|
|
265
|
+
mkdirSync(extensionRoot, { recursive: true });
|
|
266
|
+
writeFileSync(join(extensionRoot, "config.json"), JSON.stringify(content));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
it("merges global and project new-layout configs", () => {
|
|
270
|
+
writeGlobal({
|
|
271
|
+
debugLog: true,
|
|
272
|
+
defaultPolicy: { tools: "ask", bash: "deny" },
|
|
273
|
+
tools: { read: "allow" },
|
|
274
|
+
});
|
|
275
|
+
writeProject({
|
|
276
|
+
defaultPolicy: { tools: "allow" },
|
|
277
|
+
tools: { write: "deny" },
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
|
|
281
|
+
expect(result.issues).toEqual([]);
|
|
282
|
+
expect(result.merged.debugLog).toBe(true);
|
|
283
|
+
expect(result.merged.defaultPolicy).toEqual({
|
|
284
|
+
tools: "allow",
|
|
285
|
+
bash: "deny",
|
|
286
|
+
});
|
|
287
|
+
expect(result.merged.tools).toEqual({ read: "allow", write: "deny" });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("detects legacy global policy and emits migration issue", () => {
|
|
291
|
+
writeLegacyGlobalPolicy({
|
|
292
|
+
defaultPolicy: { tools: "allow" },
|
|
293
|
+
tools: { read: "allow" },
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
|
|
297
|
+
expect(result.issues).toHaveLength(1);
|
|
298
|
+
expect(result.issues[0]).toContain("pi-permissions.jsonc");
|
|
299
|
+
expect(result.issues[0]).toContain("extensions/pi-permission-system");
|
|
300
|
+
// Legacy values are merged
|
|
301
|
+
expect(result.merged.defaultPolicy).toEqual({ tools: "allow" });
|
|
302
|
+
expect(result.merged.tools).toEqual({ read: "allow" });
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("detects legacy project policy and emits migration issue", () => {
|
|
306
|
+
writeLegacyProjectPolicy({
|
|
307
|
+
bash: { "git status": "allow" },
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
|
|
311
|
+
expect(result.issues).toHaveLength(1);
|
|
312
|
+
expect(result.issues[0]).toContain(".pi/agent/pi-permissions.jsonc");
|
|
313
|
+
expect(result.issues[0]).toContain(".pi/extensions/pi-permission-system");
|
|
314
|
+
expect(result.merged.bash).toEqual({ "git status": "allow" });
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("detects legacy extension runtime config and emits migration issue", () => {
|
|
318
|
+
writeLegacyExtensionConfig({
|
|
319
|
+
debugLog: true,
|
|
320
|
+
yoloMode: true,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
|
|
324
|
+
expect(result.issues).toHaveLength(1);
|
|
325
|
+
expect(result.issues[0]).toContain(extensionRoot);
|
|
326
|
+
expect(result.merged.debugLog).toBe(true);
|
|
327
|
+
expect(result.merged.yoloMode).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("does not emit legacy extension config issue when path equals new global path", () => {
|
|
331
|
+
// If the extension happens to be installed at the new path, no warning
|
|
332
|
+
const newGlobalDir = join(agentDir, "extensions", "pi-permission-system");
|
|
333
|
+
mkdirSync(newGlobalDir, { recursive: true });
|
|
334
|
+
writeFileSync(
|
|
335
|
+
join(newGlobalDir, "config.json"),
|
|
336
|
+
JSON.stringify({ debugLog: true }),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const result = loadAndMergeConfigs(agentDir, cwd, newGlobalDir);
|
|
340
|
+
expect(result.issues.filter((i) => i.includes("legacy"))).toHaveLength(0);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("emits no issues when no legacy files exist and no new files exist", () => {
|
|
344
|
+
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
|
|
345
|
+
expect(result.issues).toEqual([]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("new-layout config takes precedence over legacy config at same scope", () => {
|
|
349
|
+
writeGlobal({
|
|
350
|
+
defaultPolicy: { tools: "deny" },
|
|
351
|
+
});
|
|
352
|
+
writeLegacyGlobalPolicy({
|
|
353
|
+
defaultPolicy: { tools: "allow" },
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = loadAndMergeConfigs(agentDir, cwd, extensionRoot);
|
|
357
|
+
// New layout wins
|
|
358
|
+
expect(result.merged.defaultPolicy).toEqual({ tools: "deny" });
|
|
359
|
+
// But legacy still emits a migration warning
|
|
360
|
+
expect(result.issues.some((i) => i.includes("pi-permissions.jsonc"))).toBe(
|
|
361
|
+
true,
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEBUG_LOG_FILENAME,
|
|
6
|
+
getGlobalConfigDir,
|
|
7
|
+
getGlobalConfigPath,
|
|
8
|
+
getGlobalLogsDir,
|
|
9
|
+
getLegacyExtensionConfigPath,
|
|
10
|
+
getLegacyGlobalPolicyPath,
|
|
11
|
+
getLegacyProjectPolicyPath,
|
|
12
|
+
getProjectConfigPath,
|
|
13
|
+
REVIEW_LOG_FILENAME,
|
|
14
|
+
} from "../src/config-paths.js";
|
|
15
|
+
|
|
16
|
+
describe("config-paths", () => {
|
|
17
|
+
const agentDir = "/home/user/.pi/agent";
|
|
18
|
+
const cwd = "/projects/my-app";
|
|
19
|
+
const extensionRoot = "/opt/extensions/pi-permission-system";
|
|
20
|
+
|
|
21
|
+
describe("new layout paths", () => {
|
|
22
|
+
it("getGlobalConfigDir returns extensions/pi-permission-system under agentDir", () => {
|
|
23
|
+
expect(getGlobalConfigDir(agentDir)).toBe(
|
|
24
|
+
join(agentDir, "extensions", "pi-permission-system"),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("getGlobalConfigPath returns config.json under the global config dir", () => {
|
|
29
|
+
expect(getGlobalConfigPath(agentDir)).toBe(
|
|
30
|
+
join(agentDir, "extensions", "pi-permission-system", "config.json"),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("getGlobalLogsDir returns logs under the global config dir", () => {
|
|
35
|
+
expect(getGlobalLogsDir(agentDir)).toBe(
|
|
36
|
+
join(agentDir, "extensions", "pi-permission-system", "logs"),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("getProjectConfigPath returns .pi/extensions/pi-permission-system/config.json under cwd", () => {
|
|
41
|
+
expect(getProjectConfigPath(cwd)).toBe(
|
|
42
|
+
join(cwd, ".pi", "extensions", "pi-permission-system", "config.json"),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("legacy paths", () => {
|
|
48
|
+
it("getLegacyGlobalPolicyPath returns pi-permissions.jsonc under agentDir", () => {
|
|
49
|
+
expect(getLegacyGlobalPolicyPath(agentDir)).toBe(
|
|
50
|
+
join(agentDir, "pi-permissions.jsonc"),
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("getLegacyProjectPolicyPath returns .pi/agent/pi-permissions.jsonc under cwd", () => {
|
|
55
|
+
expect(getLegacyProjectPolicyPath(cwd)).toBe(
|
|
56
|
+
join(cwd, ".pi", "agent", "pi-permissions.jsonc"),
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("getLegacyExtensionConfigPath returns config.json under extensionRoot", () => {
|
|
61
|
+
expect(getLegacyExtensionConfigPath(extensionRoot)).toBe(
|
|
62
|
+
join(extensionRoot, "config.json"),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("log filenames", () => {
|
|
68
|
+
it("DEBUG_LOG_FILENAME is a .jsonl file", () => {
|
|
69
|
+
expect(DEBUG_LOG_FILENAME).toBe("pi-permission-system-debug.jsonl");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("REVIEW_LOG_FILENAME is a .jsonl file", () => {
|
|
73
|
+
expect(REVIEW_LOG_FILENAME).toBe(
|
|
74
|
+
"pi-permission-system-permission-review.jsonl",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -14,11 +14,13 @@ import { createPermissionSystemLogger } from "../src/logging.js";
|
|
|
14
14
|
import type { ResolvedPolicyPaths } from "../src/permission-manager.js";
|
|
15
15
|
import { PermissionManager } from "../src/permission-manager.js";
|
|
16
16
|
|
|
17
|
-
test("buildResolvedConfigLogEntry
|
|
17
|
+
test("buildResolvedConfigLogEntry includes policy paths and legacy detection flags", () => {
|
|
18
18
|
const policyPaths: ResolvedPolicyPaths = {
|
|
19
|
-
globalConfigPath:
|
|
19
|
+
globalConfigPath:
|
|
20
|
+
"/home/user/.pi/agent/extensions/pi-permission-system/config.json",
|
|
20
21
|
globalConfigExists: true,
|
|
21
|
-
projectConfigPath:
|
|
22
|
+
projectConfigPath:
|
|
23
|
+
"/projects/my-app/.pi/extensions/pi-permission-system/config.json",
|
|
22
24
|
projectConfigExists: false,
|
|
23
25
|
agentsDir: "/home/user/.pi/agent/agents",
|
|
24
26
|
agentsDirExists: true,
|
|
@@ -26,36 +28,31 @@ test("buildResolvedConfigLogEntry merges extension config path with policy paths
|
|
|
26
28
|
projectAgentsDirExists: false,
|
|
27
29
|
};
|
|
28
30
|
|
|
29
|
-
const result = buildResolvedConfigLogEntry(
|
|
30
|
-
"/ext/pi-permission-system/config.json",
|
|
31
|
-
true,
|
|
32
|
-
policyPaths,
|
|
33
|
-
);
|
|
31
|
+
const result = buildResolvedConfigLogEntry({ policyPaths });
|
|
34
32
|
|
|
35
|
-
assert.equal(
|
|
36
|
-
result.extensionConfigPath,
|
|
37
|
-
"/ext/pi-permission-system/config.json",
|
|
38
|
-
);
|
|
39
|
-
assert.equal(result.extensionConfigExists, true);
|
|
40
33
|
assert.equal(
|
|
41
34
|
result.globalConfigPath,
|
|
42
|
-
"/home/user/.pi/agent/pi-
|
|
35
|
+
"/home/user/.pi/agent/extensions/pi-permission-system/config.json",
|
|
43
36
|
);
|
|
44
37
|
assert.equal(result.globalConfigExists, true);
|
|
45
38
|
assert.equal(
|
|
46
39
|
result.projectConfigPath,
|
|
47
|
-
"/projects/my-app/.pi/
|
|
40
|
+
"/projects/my-app/.pi/extensions/pi-permission-system/config.json",
|
|
48
41
|
);
|
|
49
42
|
assert.equal(result.projectConfigExists, false);
|
|
50
43
|
assert.equal(result.agentsDir, "/home/user/.pi/agent/agents");
|
|
51
44
|
assert.equal(result.agentsDirExists, true);
|
|
52
45
|
assert.equal(result.projectAgentsDir, "/projects/my-app/.pi/agent/agents");
|
|
53
46
|
assert.equal(result.projectAgentsDirExists, false);
|
|
47
|
+
assert.equal(result.legacyGlobalPolicyDetected, false);
|
|
48
|
+
assert.equal(result.legacyProjectPolicyDetected, false);
|
|
49
|
+
assert.equal(result.legacyExtensionConfigDetected, false);
|
|
54
50
|
});
|
|
55
51
|
|
|
56
52
|
test("buildResolvedConfigLogEntry handles null project paths", () => {
|
|
57
53
|
const policyPaths: ResolvedPolicyPaths = {
|
|
58
|
-
globalConfigPath:
|
|
54
|
+
globalConfigPath:
|
|
55
|
+
"/home/user/.pi/agent/extensions/pi-permission-system/config.json",
|
|
59
56
|
globalConfigExists: false,
|
|
60
57
|
projectConfigPath: null,
|
|
61
58
|
projectConfigExists: false,
|
|
@@ -65,20 +62,38 @@ test("buildResolvedConfigLogEntry handles null project paths", () => {
|
|
|
65
62
|
projectAgentsDirExists: false,
|
|
66
63
|
};
|
|
67
64
|
|
|
68
|
-
const result = buildResolvedConfigLogEntry(
|
|
69
|
-
"/ext/config.json",
|
|
70
|
-
false,
|
|
71
|
-
policyPaths,
|
|
72
|
-
);
|
|
65
|
+
const result = buildResolvedConfigLogEntry({ policyPaths });
|
|
73
66
|
|
|
74
|
-
assert.equal(result.extensionConfigPath, "/ext/config.json");
|
|
75
|
-
assert.equal(result.extensionConfigExists, false);
|
|
76
67
|
assert.equal(result.projectConfigPath, null);
|
|
77
68
|
assert.equal(result.projectConfigExists, false);
|
|
78
69
|
assert.equal(result.projectAgentsDir, null);
|
|
79
70
|
assert.equal(result.projectAgentsDirExists, false);
|
|
80
71
|
});
|
|
81
72
|
|
|
73
|
+
test("buildResolvedConfigLogEntry surfaces legacy detection flags", () => {
|
|
74
|
+
const policyPaths: ResolvedPolicyPaths = {
|
|
75
|
+
globalConfigPath:
|
|
76
|
+
"/home/user/.pi/agent/extensions/pi-permission-system/config.json",
|
|
77
|
+
globalConfigExists: true,
|
|
78
|
+
projectConfigPath: null,
|
|
79
|
+
projectConfigExists: false,
|
|
80
|
+
agentsDir: "/home/user/.pi/agent/agents",
|
|
81
|
+
agentsDirExists: false,
|
|
82
|
+
projectAgentsDir: null,
|
|
83
|
+
projectAgentsDirExists: false,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const result = buildResolvedConfigLogEntry({
|
|
87
|
+
policyPaths,
|
|
88
|
+
legacyGlobalPolicyDetected: true,
|
|
89
|
+
legacyExtensionConfigDetected: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assert.equal(result.legacyGlobalPolicyDetected, true);
|
|
93
|
+
assert.equal(result.legacyProjectPolicyDetected, false);
|
|
94
|
+
assert.equal(result.legacyExtensionConfigDetected, true);
|
|
95
|
+
});
|
|
96
|
+
|
|
82
97
|
test("config.resolved entry appears in review log via logger", () => {
|
|
83
98
|
const tempDir = mkdtempSync(join(tmpdir(), "config-resolved-log-"));
|
|
84
99
|
try {
|
|
@@ -95,9 +110,6 @@ test("config.resolved entry appears in review log via logger", () => {
|
|
|
95
110
|
agentsDir,
|
|
96
111
|
});
|
|
97
112
|
|
|
98
|
-
const extensionConfigPath = join(tempDir, "config.json");
|
|
99
|
-
writeFileSync(extensionConfigPath, "{}", "utf-8");
|
|
100
|
-
|
|
101
113
|
const logger = createPermissionSystemLogger({
|
|
102
114
|
getConfig: () => ({
|
|
103
115
|
debugLog: false,
|
|
@@ -109,11 +121,7 @@ test("config.resolved entry appears in review log via logger", () => {
|
|
|
109
121
|
});
|
|
110
122
|
|
|
111
123
|
const policyPaths = pm.getResolvedPolicyPaths();
|
|
112
|
-
const entry = buildResolvedConfigLogEntry(
|
|
113
|
-
extensionConfigPath,
|
|
114
|
-
true,
|
|
115
|
-
policyPaths,
|
|
116
|
-
);
|
|
124
|
+
const entry = buildResolvedConfigLogEntry({ policyPaths });
|
|
117
125
|
logger.review(
|
|
118
126
|
"config.resolved",
|
|
119
127
|
entry as unknown as Record<string, unknown>,
|
|
@@ -123,8 +131,6 @@ test("config.resolved entry appears in review log via logger", () => {
|
|
|
123
131
|
const parsed = JSON.parse(logContent) as Record<string, unknown>;
|
|
124
132
|
|
|
125
133
|
assert.equal(parsed.event, "config.resolved");
|
|
126
|
-
assert.equal(parsed.extensionConfigPath, extensionConfigPath);
|
|
127
|
-
assert.equal(parsed.extensionConfigExists, true);
|
|
128
134
|
assert.equal(parsed.globalConfigPath, globalConfigPath);
|
|
129
135
|
assert.equal(parsed.globalConfigExists, true);
|
|
130
136
|
assert.equal(parsed.agentsDir, agentsDir);
|
|
@@ -133,6 +139,9 @@ test("config.resolved entry appears in review log via logger", () => {
|
|
|
133
139
|
assert.equal(parsed.projectConfigExists, false);
|
|
134
140
|
assert.equal(parsed.projectAgentsDir, null);
|
|
135
141
|
assert.equal(parsed.projectAgentsDirExists, false);
|
|
142
|
+
assert.equal(parsed.legacyGlobalPolicyDetected, false);
|
|
143
|
+
assert.equal(parsed.legacyProjectPolicyDetected, false);
|
|
144
|
+
assert.equal(parsed.legacyExtensionConfigDetected, false);
|
|
136
145
|
} finally {
|
|
137
146
|
rmSync(tempDir, { recursive: true, force: true });
|
|
138
147
|
}
|
|
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
6
6
|
import {
|
|
7
7
|
detectMisplacedPermissionKeys,
|
|
8
8
|
loadPermissionSystemConfig,
|
|
9
|
+
normalizePermissionSystemConfig,
|
|
9
10
|
} from "../src/extension-config.js";
|
|
10
11
|
|
|
11
12
|
describe("detectMisplacedPermissionKeys", () => {
|
|
@@ -118,3 +119,53 @@ describe("loadPermissionSystemConfig", () => {
|
|
|
118
119
|
expect(result.config.debugLog).toBe(true);
|
|
119
120
|
});
|
|
120
121
|
});
|
|
122
|
+
|
|
123
|
+
describe("normalizePermissionSystemConfig", () => {
|
|
124
|
+
it("normalizes a valid config object", () => {
|
|
125
|
+
const result = normalizePermissionSystemConfig({
|
|
126
|
+
debugLog: true,
|
|
127
|
+
permissionReviewLog: false,
|
|
128
|
+
yoloMode: true,
|
|
129
|
+
});
|
|
130
|
+
expect(result).toEqual({
|
|
131
|
+
debugLog: true,
|
|
132
|
+
permissionReviewLog: false,
|
|
133
|
+
yoloMode: true,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("defaults debugLog to false when missing", () => {
|
|
138
|
+
const result = normalizePermissionSystemConfig({});
|
|
139
|
+
expect(result.debugLog).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("defaults permissionReviewLog to true when missing", () => {
|
|
143
|
+
const result = normalizePermissionSystemConfig({});
|
|
144
|
+
expect(result.permissionReviewLog).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("defaults yoloMode to false when missing", () => {
|
|
148
|
+
const result = normalizePermissionSystemConfig({});
|
|
149
|
+
expect(result.yoloMode).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("coerces non-boolean values to their defaults", () => {
|
|
153
|
+
const result = normalizePermissionSystemConfig({
|
|
154
|
+
debugLog: "yes",
|
|
155
|
+
permissionReviewLog: 1,
|
|
156
|
+
yoloMode: null,
|
|
157
|
+
});
|
|
158
|
+
expect(result.debugLog).toBe(false);
|
|
159
|
+
expect(result.permissionReviewLog).toBe(true);
|
|
160
|
+
expect(result.yoloMode).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("handles null/undefined input gracefully", () => {
|
|
164
|
+
const result = normalizePermissionSystemConfig(null);
|
|
165
|
+
expect(result).toEqual({
|
|
166
|
+
debugLog: false,
|
|
167
|
+
permissionReviewLog: true,
|
|
168
|
+
yoloMode: false,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|