@gotgenes/pi-permission-system 17.0.0 → 17.1.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/access-intent/access-path.ts +5 -5
- package/src/access-intent/bash/bash-path-resolver.ts +535 -0
- package/src/access-intent/bash/program.ts +21 -14
- package/src/access-intent/bash/token-classification.ts +24 -1
- package/src/canonicalize-path.ts +11 -5
- package/src/forwarded-permissions/permission-forwarder.ts +11 -1
- package/src/forwarding-manager.ts +7 -1
- package/src/handlers/before-agent-start.ts +1 -1
- package/src/handlers/gates/bash-path-extractor.ts +7 -5
- package/src/handlers/gates/external-directory.ts +7 -18
- package/src/handlers/gates/path.ts +3 -2
- package/src/handlers/gates/skill-read.ts +4 -2
- package/src/handlers/gates/tool-call-gate-pipeline.ts +20 -4
- package/src/handlers/gates/tool.ts +4 -2
- package/src/index.ts +12 -1
- package/src/input-normalizer.ts +9 -4
- package/src/path-normalizer.ts +100 -0
- package/src/path-utils.ts +39 -28
- package/src/permission-manager.ts +21 -7
- package/src/permission-session.ts +35 -2
- package/src/prompting-gateway.ts +3 -0
- package/src/rule.ts +8 -6
- package/src/skill-prompt-sanitizer.ts +8 -10
- package/src/subagent-context.ts +17 -9
- package/test/access-intent/access-path.test.ts +73 -24
- package/test/access-intent/bash/program.test.ts +131 -65
- package/test/access-intent/bash/token-classification.test.ts +75 -0
- package/test/bash-external-directory.test.ts +53 -1
- package/test/canonicalize-path.test.ts +34 -8
- package/test/forwarding-manager.test.ts +7 -1
- package/test/handlers/external-directory-symlink-acceptance.test.ts +23 -4
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
- package/test/handlers/gates/bash-external-directory.test.ts +5 -1
- package/test/handlers/gates/bash-path.test.ts +6 -1
- package/test/handlers/gates/external-directory-policy.test.ts +26 -8
- package/test/handlers/gates/external-directory.test.ts +8 -1
- package/test/handlers/gates/path.test.ts +53 -14
- package/test/handlers/gates/skill-read.test.ts +26 -13
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
- package/test/handlers/gates/tool.test.ts +13 -1
- package/test/helpers/gate-fixtures.ts +10 -0
- package/test/helpers/session-fixtures.ts +3 -0
- package/test/input-normalizer.test.ts +104 -39
- package/test/path-normalizer.test.ts +166 -0
- package/test/path-utils.test.ts +130 -51
- package/test/permission-forwarder.test.ts +1 -0
- package/test/permission-manager-unified.test.ts +40 -0
- package/test/permission-resolver.test.ts +12 -3
- package/test/permission-session.test.ts +41 -0
- package/test/pi-infrastructure-read.test.ts +27 -4
- package/test/prompting-gateway.test.ts +1 -0
- package/test/rule.test.ts +88 -42
- package/test/session-rules.test.ts +21 -4
- package/test/skill-prompt-sanitizer.test.ts +37 -16
- package/test/subagent-context.test.ts +77 -31
- package/test/synthesize.test.ts +13 -11
- package/src/access-intent/bash/cwd-projection.ts +0 -500
|
@@ -131,6 +131,7 @@ describe("SessionRules", () => {
|
|
|
131
131
|
"external_directory",
|
|
132
132
|
"/other/project/src/foo.ts",
|
|
133
133
|
session.getRuleset(),
|
|
134
|
+
"linux",
|
|
134
135
|
);
|
|
135
136
|
expect(result.action).toBe("allow");
|
|
136
137
|
});
|
|
@@ -142,6 +143,7 @@ describe("SessionRules", () => {
|
|
|
142
143
|
"external_directory",
|
|
143
144
|
"/other/unrelated/file.ts",
|
|
144
145
|
session.getRuleset(),
|
|
146
|
+
"linux",
|
|
145
147
|
);
|
|
146
148
|
// No rule matches — evaluate returns synthetic rule with default action "ask"
|
|
147
149
|
expect(result.action).toBe("ask");
|
|
@@ -154,6 +156,7 @@ describe("SessionRules", () => {
|
|
|
154
156
|
"external_directory",
|
|
155
157
|
"/other/project-b/foo.ts",
|
|
156
158
|
session.getRuleset(),
|
|
159
|
+
"linux",
|
|
157
160
|
);
|
|
158
161
|
expect(result.action).toBe("ask");
|
|
159
162
|
});
|
|
@@ -166,6 +169,7 @@ describe("SessionRules", () => {
|
|
|
166
169
|
"external_directory",
|
|
167
170
|
"/other/project/src/",
|
|
168
171
|
session.getRuleset(),
|
|
172
|
+
"linux",
|
|
169
173
|
);
|
|
170
174
|
expect(result.action).toBe("allow");
|
|
171
175
|
});
|
|
@@ -179,6 +183,7 @@ describe("SessionRules", () => {
|
|
|
179
183
|
"external_directory",
|
|
180
184
|
"/project-a/foo.ts",
|
|
181
185
|
session.getRuleset(),
|
|
186
|
+
"linux",
|
|
182
187
|
).action,
|
|
183
188
|
).toBe("allow");
|
|
184
189
|
expect(
|
|
@@ -186,6 +191,7 @@ describe("SessionRules", () => {
|
|
|
186
191
|
"external_directory",
|
|
187
192
|
"/project-b/bar.ts",
|
|
188
193
|
session.getRuleset(),
|
|
194
|
+
"linux",
|
|
189
195
|
).action,
|
|
190
196
|
).toBe("allow");
|
|
191
197
|
expect(
|
|
@@ -193,6 +199,7 @@ describe("SessionRules", () => {
|
|
|
193
199
|
"external_directory",
|
|
194
200
|
"/project-c/baz.ts",
|
|
195
201
|
session.getRuleset(),
|
|
202
|
+
"linux",
|
|
196
203
|
).action,
|
|
197
204
|
).toBe("ask");
|
|
198
205
|
});
|
|
@@ -204,6 +211,7 @@ describe("SessionRules", () => {
|
|
|
204
211
|
"bash",
|
|
205
212
|
"/other/project/foo.ts",
|
|
206
213
|
session.getRuleset(),
|
|
214
|
+
"linux",
|
|
207
215
|
);
|
|
208
216
|
expect(result.action).toBe("ask");
|
|
209
217
|
});
|
|
@@ -218,6 +226,7 @@ describe("SessionRules", () => {
|
|
|
218
226
|
"external_directory",
|
|
219
227
|
"/old/project/file.ts",
|
|
220
228
|
session.getRuleset(),
|
|
229
|
+
"linux",
|
|
221
230
|
).action,
|
|
222
231
|
).toBe("ask");
|
|
223
232
|
expect(
|
|
@@ -225,6 +234,7 @@ describe("SessionRules", () => {
|
|
|
225
234
|
"external_directory",
|
|
226
235
|
"/new/project/file.ts",
|
|
227
236
|
session.getRuleset(),
|
|
237
|
+
"linux",
|
|
228
238
|
).action,
|
|
229
239
|
).toBe("allow");
|
|
230
240
|
});
|
|
@@ -270,6 +280,7 @@ describe("deriveApprovalPattern", () => {
|
|
|
270
280
|
"external_directory",
|
|
271
281
|
"/other/project/src/bar.ts",
|
|
272
282
|
session.getRuleset(),
|
|
283
|
+
"linux",
|
|
273
284
|
).action,
|
|
274
285
|
).toBe("allow");
|
|
275
286
|
});
|
|
@@ -283,6 +294,7 @@ describe("deriveApprovalPattern", () => {
|
|
|
283
294
|
"external_directory",
|
|
284
295
|
"/other/project/lib/bar.ts",
|
|
285
296
|
session.getRuleset(),
|
|
297
|
+
"linux",
|
|
286
298
|
).action,
|
|
287
299
|
).toBe("ask");
|
|
288
300
|
});
|
|
@@ -295,10 +307,15 @@ describe("deriveApprovalPattern", () => {
|
|
|
295
307
|
const session = new SessionRules();
|
|
296
308
|
session.approve("edit", pattern);
|
|
297
309
|
expect(
|
|
298
|
-
evaluate(
|
|
310
|
+
evaluate(
|
|
311
|
+
"edit",
|
|
312
|
+
"/test/project/index.html",
|
|
313
|
+
session.getRuleset(),
|
|
314
|
+
"linux",
|
|
315
|
+
).action,
|
|
299
316
|
).toBe("allow");
|
|
300
|
-
expect(
|
|
301
|
-
"
|
|
302
|
-
);
|
|
317
|
+
expect(
|
|
318
|
+
evaluate("edit", "/etc/passwd", session.getRuleset(), "linux").action,
|
|
319
|
+
).toBe("ask");
|
|
303
320
|
});
|
|
304
321
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
3
4
|
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
4
5
|
import {
|
|
5
6
|
findSkillPathMatch,
|
|
@@ -30,6 +31,11 @@ afterEach(() => {
|
|
|
30
31
|
|
|
31
32
|
const CWD = "/projects/my-app";
|
|
32
33
|
|
|
34
|
+
// `findSkillPathMatch` only uses the normalizer's platform (it compares two
|
|
35
|
+
// already-absolute paths via `isWithinDirectory`), so this CWD-baked instance
|
|
36
|
+
// serves every call regardless of the entries' cwd.
|
|
37
|
+
const normalizer = new PathNormalizer("linux", CWD);
|
|
38
|
+
|
|
33
39
|
function makeManager(
|
|
34
40
|
defaultState: "allow" | "deny" | "ask" = "allow",
|
|
35
41
|
overrides: Record<string, "allow" | "deny" | "ask"> = {},
|
|
@@ -72,7 +78,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
72
78
|
test("returns unchanged prompt and empty entries when no skills section present", () => {
|
|
73
79
|
const input = "You are a helpful assistant.";
|
|
74
80
|
const manager = makeManager("allow");
|
|
75
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
81
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
76
82
|
expect(result.prompt).toBe(input);
|
|
77
83
|
expect(result.entries).toEqual([]);
|
|
78
84
|
expect(manager.checkPermission).not.toHaveBeenCalled();
|
|
@@ -81,7 +87,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
81
87
|
test("keeps all skills when all are allowed", () => {
|
|
82
88
|
const input = availableSkillsSection("librarian", "ask-user");
|
|
83
89
|
const manager = makeManager("allow");
|
|
84
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
90
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
85
91
|
expect(result.prompt).toContain("librarian");
|
|
86
92
|
expect(result.prompt).toContain("ask-user");
|
|
87
93
|
expect(result.entries).toHaveLength(2);
|
|
@@ -90,7 +96,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
90
96
|
test("removes denied skill from section", () => {
|
|
91
97
|
const input = availableSkillsSection("librarian", "dangerous");
|
|
92
98
|
const manager = makeManager("allow", { dangerous: "deny" });
|
|
93
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
99
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
94
100
|
expect(result.prompt).toContain("librarian");
|
|
95
101
|
expect(result.prompt).not.toContain("dangerous");
|
|
96
102
|
// denied skill is excluded from returned entries
|
|
@@ -100,7 +106,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
100
106
|
test("removes entire section when all skills are denied", () => {
|
|
101
107
|
const input = `Intro\n${availableSkillsSection("dangerous")}\nOutro`;
|
|
102
108
|
const manager = makeManager("deny");
|
|
103
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
109
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
104
110
|
expect(result.prompt).not.toContain("<available_skills>");
|
|
105
111
|
expect(result.prompt).toContain("Intro");
|
|
106
112
|
expect(result.prompt).toContain("Outro");
|
|
@@ -110,7 +116,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
110
116
|
test("keeps ask-state skills in section and entries", () => {
|
|
111
117
|
const input = availableSkillsSection("librarian");
|
|
112
118
|
const manager = makeManager("ask");
|
|
113
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
119
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
114
120
|
expect(result.prompt).toContain("librarian");
|
|
115
121
|
expect(result.entries).toHaveLength(1);
|
|
116
122
|
expect(result.entries[0].state).toBe("ask");
|
|
@@ -119,7 +125,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
119
125
|
test("delegates permission check to permissionManager for each skill", () => {
|
|
120
126
|
const input = availableSkillsSection("alpha", "beta");
|
|
121
127
|
const manager = makeManager("allow");
|
|
122
|
-
resolveSkillPromptEntries(input, manager, null,
|
|
128
|
+
resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
123
129
|
expect(manager.checkPermission).toHaveBeenCalledWith(
|
|
124
130
|
"skill",
|
|
125
131
|
{ name: "alpha" },
|
|
@@ -135,7 +141,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
135
141
|
test("passes agentName to permissionManager", () => {
|
|
136
142
|
const input = availableSkillsSection("librarian");
|
|
137
143
|
const manager = makeManager("allow");
|
|
138
|
-
resolveSkillPromptEntries(input, manager, "my-agent",
|
|
144
|
+
resolveSkillPromptEntries(input, manager, "my-agent", normalizer);
|
|
139
145
|
expect(manager.checkPermission).toHaveBeenCalledWith(
|
|
140
146
|
"skill",
|
|
141
147
|
{ name: "librarian" },
|
|
@@ -150,7 +156,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
150
156
|
availableSkillsSection("librarian"),
|
|
151
157
|
].join("\n");
|
|
152
158
|
const manager = makeManager("allow");
|
|
153
|
-
resolveSkillPromptEntries(input, manager, null,
|
|
159
|
+
resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
154
160
|
// Should only be called once despite appearing twice.
|
|
155
161
|
expect(manager.checkPermission).toHaveBeenCalledTimes(1);
|
|
156
162
|
});
|
|
@@ -159,7 +165,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
159
165
|
const location = "/skills/librarian/SKILL.md";
|
|
160
166
|
const input = availableSkillsSection("librarian");
|
|
161
167
|
const manager = makeManager("allow");
|
|
162
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
168
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
163
169
|
expect(result.entries[0].normalizedLocation).toBe(location);
|
|
164
170
|
expect(result.entries[0].normalizedBaseDir).toBe("/skills/librarian");
|
|
165
171
|
});
|
|
@@ -169,7 +175,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
169
175
|
const section2 = availableSkillsSection("beta");
|
|
170
176
|
const input = `${section1}\n${section2}`;
|
|
171
177
|
const manager = makeManager("allow", { beta: "deny" });
|
|
172
|
-
const result = resolveSkillPromptEntries(input, manager, null,
|
|
178
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
173
179
|
expect(result.entries.map((e) => e.name)).toContain("alpha");
|
|
174
180
|
expect(result.entries.map((e) => e.name)).not.toContain("beta");
|
|
175
181
|
});
|
|
@@ -198,15 +204,21 @@ describe("findSkillPathMatch", () => {
|
|
|
198
204
|
];
|
|
199
205
|
|
|
200
206
|
test("returns null for empty normalized path", () => {
|
|
201
|
-
expect(findSkillPathMatch("", entries)).toBeNull();
|
|
207
|
+
expect(findSkillPathMatch("", entries, normalizer)).toBeNull();
|
|
202
208
|
});
|
|
203
209
|
|
|
204
210
|
test("returns null for empty entries array", () => {
|
|
205
|
-
expect(
|
|
211
|
+
expect(
|
|
212
|
+
findSkillPathMatch("/skills/librarian/SKILL.md", [], normalizer),
|
|
213
|
+
).toBeNull();
|
|
206
214
|
});
|
|
207
215
|
|
|
208
216
|
test("matches exact location path", () => {
|
|
209
|
-
const match = findSkillPathMatch(
|
|
217
|
+
const match = findSkillPathMatch(
|
|
218
|
+
"/skills/librarian/SKILL.md",
|
|
219
|
+
entries,
|
|
220
|
+
normalizer,
|
|
221
|
+
);
|
|
210
222
|
expect(match?.name).toBe("librarian");
|
|
211
223
|
});
|
|
212
224
|
|
|
@@ -214,12 +226,17 @@ describe("findSkillPathMatch", () => {
|
|
|
214
226
|
const match = findSkillPathMatch(
|
|
215
227
|
"/skills/librarian/extra/helper.md",
|
|
216
228
|
entries,
|
|
229
|
+
normalizer,
|
|
217
230
|
);
|
|
218
231
|
expect(match?.name).toBe("librarian");
|
|
219
232
|
});
|
|
220
233
|
|
|
221
234
|
test("returns null for path not within any skill directory", () => {
|
|
222
|
-
const match = findSkillPathMatch(
|
|
235
|
+
const match = findSkillPathMatch(
|
|
236
|
+
"/other/path/file.md",
|
|
237
|
+
entries,
|
|
238
|
+
normalizer,
|
|
239
|
+
);
|
|
223
240
|
expect(match).toBeNull();
|
|
224
241
|
});
|
|
225
242
|
|
|
@@ -228,6 +245,7 @@ describe("findSkillPathMatch", () => {
|
|
|
228
245
|
const match = findSkillPathMatch(
|
|
229
246
|
"/skills/librarian-extra/SKILL.md",
|
|
230
247
|
entries,
|
|
248
|
+
normalizer,
|
|
231
249
|
);
|
|
232
250
|
expect(match).toBeNull();
|
|
233
251
|
});
|
|
@@ -254,6 +272,7 @@ describe("findSkillPathMatch", () => {
|
|
|
254
272
|
const match = findSkillPathMatch(
|
|
255
273
|
"/skills/parent/child/helper.md",
|
|
256
274
|
nestedEntries,
|
|
275
|
+
normalizer,
|
|
257
276
|
);
|
|
258
277
|
expect(match?.name).toBe("child");
|
|
259
278
|
});
|
|
@@ -329,7 +348,7 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
329
348
|
prompt,
|
|
330
349
|
asChecker(manager),
|
|
331
350
|
null,
|
|
332
|
-
"/cwd",
|
|
351
|
+
new PathNormalizer("linux", "/cwd"),
|
|
333
352
|
);
|
|
334
353
|
|
|
335
354
|
expect(result.prompt).not.toContain("denied-skill");
|
|
@@ -376,17 +395,19 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
|
|
|
376
395
|
prompt,
|
|
377
396
|
asChecker(manager),
|
|
378
397
|
null,
|
|
379
|
-
"/cwd",
|
|
398
|
+
new PathNormalizer("linux", "/cwd"),
|
|
380
399
|
);
|
|
381
400
|
const visiblePath = resolve("/cwd", "./skills/visible/file.ts");
|
|
382
401
|
const blockedPath = resolve("/cwd", "./skills/blocked/file.ts");
|
|
383
402
|
const matchedVisibleSkill = findSkillPathMatch(
|
|
384
403
|
process.platform === "win32" ? visiblePath.toLowerCase() : visiblePath,
|
|
385
404
|
result.entries,
|
|
405
|
+
normalizer,
|
|
386
406
|
);
|
|
387
407
|
const matchedBlockedSkill = findSkillPathMatch(
|
|
388
408
|
process.platform === "win32" ? blockedPath.toLowerCase() : blockedPath,
|
|
389
409
|
result.entries,
|
|
410
|
+
normalizer,
|
|
390
411
|
);
|
|
391
412
|
|
|
392
413
|
expect(matchedVisibleSkill?.name).toBe("visible-skill");
|
|
@@ -66,43 +66,73 @@ describe("isRegisteredSubagentChild", () => {
|
|
|
66
66
|
|
|
67
67
|
describe("normalizeFilesystemPath", () => {
|
|
68
68
|
test("normalizes a simple absolute path", () => {
|
|
69
|
-
expect(normalizeFilesystemPath("/projects/my-app")).toBe(
|
|
69
|
+
expect(normalizeFilesystemPath("/projects/my-app", "linux")).toBe(
|
|
70
70
|
"/projects/my-app",
|
|
71
71
|
);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
test("collapses redundant separators", () => {
|
|
75
|
-
expect(normalizeFilesystemPath("/projects//my-app")).toBe(
|
|
75
|
+
expect(normalizeFilesystemPath("/projects//my-app", "linux")).toBe(
|
|
76
76
|
"/projects/my-app",
|
|
77
77
|
);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
test("resolves . and .. segments", () => {
|
|
81
|
-
expect(normalizeFilesystemPath("/projects/my-app/../other")).toBe(
|
|
81
|
+
expect(normalizeFilesystemPath("/projects/my-app/../other", "linux")).toBe(
|
|
82
82
|
"/projects/other",
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
test("win32: lowercases and normalizes with win32 separators", () => {
|
|
87
|
+
expect(normalizeFilesystemPath("C:\\Projects\\My-App", "win32")).toBe(
|
|
88
|
+
"c:\\projects\\my-app",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("posix: leaves case untouched", () => {
|
|
93
|
+
expect(normalizeFilesystemPath("/Projects/My-App", "linux")).toBe(
|
|
94
|
+
"/Projects/My-App",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("isSubagentExecutionContext — injected platform (#510)", () => {
|
|
100
|
+
test("win32: detects a subagent session dir case-insensitively", () => {
|
|
101
|
+
const subagentRoot = "C:\\Sessions\\Subagents";
|
|
102
|
+
const sessionDir = "c:\\sessions\\subagents\\child";
|
|
103
|
+
expect(
|
|
104
|
+
isSubagentExecutionContext(makeCtx(sessionDir), subagentRoot, "win32"),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("posix: the same mixed-case dir is not a subagent context", () => {
|
|
109
|
+
const subagentRoot = "/Sessions/Subagents";
|
|
110
|
+
const sessionDir = "/sessions/subagents/child";
|
|
111
|
+
expect(
|
|
112
|
+
isSubagentExecutionContext(makeCtx(sessionDir), subagentRoot, "linux"),
|
|
113
|
+
).toBe(false);
|
|
114
|
+
});
|
|
85
115
|
});
|
|
86
116
|
|
|
87
117
|
describe("isSubagentExecutionContext — env hint detection", () => {
|
|
88
118
|
test("returns true when PI_IS_SUBAGENT is set", () => {
|
|
89
119
|
vi.stubEnv("PI_IS_SUBAGENT", "true");
|
|
90
120
|
expect(
|
|
91
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
121
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
92
122
|
).toBe(true);
|
|
93
123
|
});
|
|
94
124
|
|
|
95
125
|
test("returns true when PI_SUBAGENT_SESSION_ID is set", () => {
|
|
96
126
|
vi.stubEnv("PI_SUBAGENT_SESSION_ID", "abc123");
|
|
97
127
|
expect(
|
|
98
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
128
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
99
129
|
).toBe(true);
|
|
100
130
|
});
|
|
101
131
|
|
|
102
132
|
test("returns true when PI_AGENT_ROUTER_SUBAGENT is set", () => {
|
|
103
133
|
vi.stubEnv("PI_AGENT_ROUTER_SUBAGENT", "1");
|
|
104
134
|
expect(
|
|
105
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
135
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
106
136
|
).toBe(true);
|
|
107
137
|
});
|
|
108
138
|
|
|
@@ -110,35 +140,35 @@ describe("isSubagentExecutionContext — env hint detection", () => {
|
|
|
110
140
|
test("returns true when PI_SUBAGENT_CHILD is set", () => {
|
|
111
141
|
vi.stubEnv("PI_SUBAGENT_CHILD", "1");
|
|
112
142
|
expect(
|
|
113
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
143
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
114
144
|
).toBe(true);
|
|
115
145
|
});
|
|
116
146
|
|
|
117
147
|
test("returns true when PI_SUBAGENT_RUN_ID is set", () => {
|
|
118
148
|
vi.stubEnv("PI_SUBAGENT_RUN_ID", "run-abc");
|
|
119
149
|
expect(
|
|
120
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
150
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
121
151
|
).toBe(true);
|
|
122
152
|
});
|
|
123
153
|
|
|
124
154
|
test("returns true when PI_SUBAGENT_CHILD_AGENT is set", () => {
|
|
125
155
|
vi.stubEnv("PI_SUBAGENT_CHILD_AGENT", "worker");
|
|
126
156
|
expect(
|
|
127
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
157
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
128
158
|
).toBe(true);
|
|
129
159
|
});
|
|
130
160
|
|
|
131
161
|
test("returns true when PI_SUBAGENT_DEPTH is set", () => {
|
|
132
162
|
vi.stubEnv("PI_SUBAGENT_DEPTH", "1");
|
|
133
163
|
expect(
|
|
134
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
164
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
135
165
|
).toBe(true);
|
|
136
166
|
});
|
|
137
167
|
|
|
138
168
|
test("returns true when PI_SUBAGENT_DEPTH is zero (depth-0 is still a subagent context)", () => {
|
|
139
169
|
vi.stubEnv("PI_SUBAGENT_DEPTH", "0");
|
|
140
170
|
expect(
|
|
141
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
171
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
142
172
|
).toBe(true);
|
|
143
173
|
});
|
|
144
174
|
|
|
@@ -146,28 +176,28 @@ describe("isSubagentExecutionContext — env hint detection", () => {
|
|
|
146
176
|
test("returns true when PI_SUBAGENT_NAME is set", () => {
|
|
147
177
|
vi.stubEnv("PI_SUBAGENT_NAME", "my-agent");
|
|
148
178
|
expect(
|
|
149
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
179
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
150
180
|
).toBe(true);
|
|
151
181
|
});
|
|
152
182
|
|
|
153
183
|
test("returns true when PI_SUBAGENT_ID is set", () => {
|
|
154
184
|
vi.stubEnv("PI_SUBAGENT_ID", "id-xyz");
|
|
155
185
|
expect(
|
|
156
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
186
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
157
187
|
).toBe(true);
|
|
158
188
|
});
|
|
159
189
|
|
|
160
190
|
test("returns true when PI_SUBAGENT_SESSION is set", () => {
|
|
161
191
|
vi.stubEnv("PI_SUBAGENT_SESSION", "session-xyz");
|
|
162
192
|
expect(
|
|
163
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
193
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
164
194
|
).toBe(true);
|
|
165
195
|
});
|
|
166
196
|
|
|
167
197
|
test("returns true when PI_SUBAGENT_ACTIVITY_FILE is set", () => {
|
|
168
198
|
vi.stubEnv("PI_SUBAGENT_ACTIVITY_FILE", "/tmp/activity.json");
|
|
169
199
|
expect(
|
|
170
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
200
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
171
201
|
).toBe(true);
|
|
172
202
|
});
|
|
173
203
|
|
|
@@ -191,14 +221,14 @@ describe("isSubagentExecutionContext — env hint detection", () => {
|
|
|
191
221
|
test("returns false when env hint value is empty string", () => {
|
|
192
222
|
vi.stubEnv("PI_IS_SUBAGENT", "");
|
|
193
223
|
expect(
|
|
194
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
224
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
195
225
|
).toBe(false);
|
|
196
226
|
});
|
|
197
227
|
|
|
198
228
|
test("returns false when env hint value is whitespace only", () => {
|
|
199
229
|
vi.stubEnv("PI_IS_SUBAGENT", " ");
|
|
200
230
|
expect(
|
|
201
|
-
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents"),
|
|
231
|
+
isSubagentExecutionContext(makeCtx(null), "/sessions/subagents", "linux"),
|
|
202
232
|
).toBe(false);
|
|
203
233
|
});
|
|
204
234
|
});
|
|
@@ -208,38 +238,42 @@ describe("isSubagentExecutionContext — session dir detection", () => {
|
|
|
208
238
|
|
|
209
239
|
test("returns true when session dir is within subagent root", () => {
|
|
210
240
|
const sessionDir = `${subagentRoot}/session-abc`;
|
|
211
|
-
expect(
|
|
212
|
-
|
|
213
|
-
);
|
|
241
|
+
expect(
|
|
242
|
+
isSubagentExecutionContext(makeCtx(sessionDir), subagentRoot, "linux"),
|
|
243
|
+
).toBe(true);
|
|
214
244
|
});
|
|
215
245
|
|
|
216
246
|
test("returns true when session dir equals subagent root", () => {
|
|
217
247
|
expect(
|
|
218
|
-
isSubagentExecutionContext(makeCtx(subagentRoot), subagentRoot),
|
|
248
|
+
isSubagentExecutionContext(makeCtx(subagentRoot), subagentRoot, "linux"),
|
|
219
249
|
).toBe(true);
|
|
220
250
|
});
|
|
221
251
|
|
|
222
252
|
test("returns false when session dir is outside subagent root", () => {
|
|
223
253
|
const sessionDir = "/home/user/.pi/agent/sessions/main-session";
|
|
224
|
-
expect(
|
|
225
|
-
|
|
226
|
-
);
|
|
254
|
+
expect(
|
|
255
|
+
isSubagentExecutionContext(makeCtx(sessionDir), subagentRoot, "linux"),
|
|
256
|
+
).toBe(false);
|
|
227
257
|
});
|
|
228
258
|
|
|
229
259
|
test("returns false when session dir is a sibling with shared prefix", () => {
|
|
230
260
|
// "/sessions/subagents-extra" should not match root "/sessions/subagents"
|
|
231
261
|
const sessionDir = `${subagentRoot}-extra/session-abc`;
|
|
232
|
-
expect(
|
|
233
|
-
|
|
234
|
-
);
|
|
262
|
+
expect(
|
|
263
|
+
isSubagentExecutionContext(makeCtx(sessionDir), subagentRoot, "linux"),
|
|
264
|
+
).toBe(false);
|
|
235
265
|
});
|
|
236
266
|
|
|
237
267
|
test("returns false when getSessionDir returns null", () => {
|
|
238
|
-
expect(
|
|
268
|
+
expect(
|
|
269
|
+
isSubagentExecutionContext(makeCtx(null), subagentRoot, "linux"),
|
|
270
|
+
).toBe(false);
|
|
239
271
|
});
|
|
240
272
|
|
|
241
273
|
test("returns false when getSessionDir returns empty string", () => {
|
|
242
|
-
expect(isSubagentExecutionContext(makeCtx(""), subagentRoot)).toBe(
|
|
274
|
+
expect(isSubagentExecutionContext(makeCtx(""), subagentRoot, "linux")).toBe(
|
|
275
|
+
false,
|
|
276
|
+
);
|
|
243
277
|
});
|
|
244
278
|
});
|
|
245
279
|
|
|
@@ -256,6 +290,7 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
256
290
|
isSubagentExecutionContext(
|
|
257
291
|
makeCtx(outsideDir, childSessionId),
|
|
258
292
|
subagentRoot,
|
|
293
|
+
"linux",
|
|
259
294
|
registry,
|
|
260
295
|
),
|
|
261
296
|
).toBe(true);
|
|
@@ -268,6 +303,7 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
268
303
|
isSubagentExecutionContext(
|
|
269
304
|
makeCtx(outsideDir, childSessionId),
|
|
270
305
|
subagentRoot,
|
|
306
|
+
"linux",
|
|
271
307
|
registry,
|
|
272
308
|
),
|
|
273
309
|
).toBe(true);
|
|
@@ -279,6 +315,7 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
279
315
|
isSubagentExecutionContext(
|
|
280
316
|
makeCtx(outsideDir, childSessionId),
|
|
281
317
|
subagentRoot,
|
|
318
|
+
"linux",
|
|
282
319
|
registry,
|
|
283
320
|
),
|
|
284
321
|
).toBe(false);
|
|
@@ -287,7 +324,12 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
287
324
|
test("returns false when session id is empty and registry has no matching entry", () => {
|
|
288
325
|
const registry = new SubagentSessionRegistry();
|
|
289
326
|
expect(
|
|
290
|
-
isSubagentExecutionContext(
|
|
327
|
+
isSubagentExecutionContext(
|
|
328
|
+
makeCtx(null, ""),
|
|
329
|
+
subagentRoot,
|
|
330
|
+
"linux",
|
|
331
|
+
registry,
|
|
332
|
+
),
|
|
291
333
|
).toBe(false);
|
|
292
334
|
});
|
|
293
335
|
|
|
@@ -301,6 +343,7 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
301
343
|
isSubagentExecutionContext(
|
|
302
344
|
makeCtx(outsideDir, childSessionId),
|
|
303
345
|
subagentRoot,
|
|
346
|
+
"linux",
|
|
304
347
|
registry,
|
|
305
348
|
),
|
|
306
349
|
).toBe(true);
|
|
@@ -314,6 +357,7 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
314
357
|
isSubagentExecutionContext(
|
|
315
358
|
makeCtx(outsideDir, childSessionId),
|
|
316
359
|
subagentRoot,
|
|
360
|
+
"linux",
|
|
317
361
|
registry,
|
|
318
362
|
),
|
|
319
363
|
).toBe(true);
|
|
@@ -321,6 +365,8 @@ describe("isSubagentExecutionContext — registry detection", () => {
|
|
|
321
365
|
|
|
322
366
|
test("no registry passed — existing behaviour unchanged", () => {
|
|
323
367
|
// Ensure the parameter is truly optional (no registry arg)
|
|
324
|
-
expect(
|
|
368
|
+
expect(
|
|
369
|
+
isSubagentExecutionContext(makeCtx(null), subagentRoot, "linux"),
|
|
370
|
+
).toBe(false);
|
|
325
371
|
});
|
|
326
372
|
});
|
package/test/synthesize.test.ts
CHANGED
|
@@ -30,15 +30,17 @@ describe("synthesizeDefaults", () => {
|
|
|
30
30
|
|
|
31
31
|
test("universal rule catches any surface via wildcardMatch", () => {
|
|
32
32
|
const rules = synthesizeDefaults("ask");
|
|
33
|
-
expect(evaluate("read", "*", rules).action).toBe("ask");
|
|
34
|
-
expect(evaluate("bash", "git status", rules).action).toBe("ask");
|
|
35
|
-
expect(evaluate("external_directory", "*", rules).action).toBe(
|
|
36
|
-
|
|
33
|
+
expect(evaluate("read", "*", rules, "linux").action).toBe("ask");
|
|
34
|
+
expect(evaluate("bash", "git status", rules, "linux").action).toBe("ask");
|
|
35
|
+
expect(evaluate("external_directory", "*", rules, "linux").action).toBe(
|
|
36
|
+
"ask",
|
|
37
|
+
);
|
|
38
|
+
expect(evaluate("future_surface", "*", rules, "linux").action).toBe("ask");
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
test("universal rule has layer 'default'", () => {
|
|
40
42
|
const rules = synthesizeDefaults("allow");
|
|
41
|
-
expect(evaluate("read", "*", rules).layer).toBe("default");
|
|
43
|
+
expect(evaluate("read", "*", rules, "linux").layer).toBe("default");
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
test("defaults to origin 'builtin' when no origin supplied", () => {
|
|
@@ -54,7 +56,7 @@ describe("synthesizeDefaults", () => {
|
|
|
54
56
|
|
|
55
57
|
test("origin is preserved through evaluate()", () => {
|
|
56
58
|
const rules = synthesizeDefaults("allow", "project");
|
|
57
|
-
const result = evaluate("read", "*", rules);
|
|
59
|
+
const result = evaluate("read", "*", rules, "linux");
|
|
58
60
|
expect(result.origin).toBe("project");
|
|
59
61
|
});
|
|
60
62
|
|
|
@@ -171,7 +173,7 @@ describe("synthesizeBaseline", () => {
|
|
|
171
173
|
},
|
|
172
174
|
];
|
|
173
175
|
const rules = synthesizeBaseline(configRules);
|
|
174
|
-
const result = evaluate("mcp", "mcp_status", rules);
|
|
176
|
+
const result = evaluate("mcp", "mcp_status", rules, "linux");
|
|
175
177
|
expect(result.action).toBe("allow");
|
|
176
178
|
expect(result.layer).toBe("baseline");
|
|
177
179
|
expect(result.origin).toBe("baseline");
|
|
@@ -218,7 +220,7 @@ describe("composeRuleset", () => {
|
|
|
218
220
|
},
|
|
219
221
|
];
|
|
220
222
|
const composed = composeRuleset(defaults, [], config);
|
|
221
|
-
const result = evaluate("bash", "echo hello", composed);
|
|
223
|
+
const result = evaluate("bash", "echo hello", composed, "linux");
|
|
222
224
|
expect(result.action).toBe("deny");
|
|
223
225
|
expect(result.layer).toBe("config");
|
|
224
226
|
});
|
|
@@ -235,7 +237,7 @@ describe("composeRuleset", () => {
|
|
|
235
237
|
},
|
|
236
238
|
];
|
|
237
239
|
const composed = composeRuleset(defaults, [], config);
|
|
238
|
-
const result = evaluate("read", "*", composed);
|
|
240
|
+
const result = evaluate("read", "*", composed, "linux");
|
|
239
241
|
expect(result.action).toBe("allow");
|
|
240
242
|
expect(result.layer).toBe("config");
|
|
241
243
|
});
|
|
@@ -261,7 +263,7 @@ describe("composeRuleset", () => {
|
|
|
261
263
|
},
|
|
262
264
|
];
|
|
263
265
|
const composed = composeRuleset(defaults, baseline, config);
|
|
264
|
-
const result = evaluate("mcp", "mcp_status", composed);
|
|
266
|
+
const result = evaluate("mcp", "mcp_status", composed, "linux");
|
|
265
267
|
expect(result.action).toBe("deny");
|
|
266
268
|
expect(result.layer).toBe("config");
|
|
267
269
|
});
|
|
@@ -287,7 +289,7 @@ describe("composeRuleset", () => {
|
|
|
287
289
|
},
|
|
288
290
|
];
|
|
289
291
|
const composed = composeRuleset(defaults, baseline, config);
|
|
290
|
-
const result = evaluate("mcp", "exa_web_search", composed);
|
|
292
|
+
const result = evaluate("mcp", "exa_web_search", composed, "linux");
|
|
291
293
|
expect(result.action).toBe("allow");
|
|
292
294
|
expect(result.layer).toBe("config");
|
|
293
295
|
});
|