@gotgenes/pi-permission-system 16.2.1 → 17.1.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 +26 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +8 -10
- package/src/access-intent/access-path.ts +39 -16
- package/src/access-intent/bash/bash-path-resolver.ts +531 -0
- package/src/access-intent/bash/program.ts +21 -14
- package/src/access-intent/bash/token-classification.ts +1 -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 -0
- package/src/handlers/gates/bash-path-extractor.ts +7 -5
- package/src/handlers/gates/bash-path.ts +13 -13
- package/src/handlers/gates/external-directory.ts +14 -16
- package/src/handlers/gates/path.ts +12 -7
- package/src/handlers/gates/skill-read.ts +7 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -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 +70 -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 +15 -4
- package/src/subagent-context.ts +17 -9
- package/test/access-intent/access-path.test.ts +124 -15
- package/test/access-intent/bash/program.test.ts +178 -80
- package/test/bash-external-directory.test.ts +15 -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 +26 -4
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
- package/test/handlers/gates/bash-external-directory.test.ts +5 -2
- package/test/handlers/gates/bash-path.test.ts +14 -9
- package/test/handlers/gates/external-directory-policy.test.ts +28 -18
- package/test/handlers/gates/external-directory.test.ts +9 -1
- package/test/handlers/gates/path.test.ts +90 -20
- package/test/handlers/gates/skill-read.test.ts +22 -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 +11 -2
- package/test/helpers/session-fixtures.ts +3 -0
- package/test/input-normalizer.test.ts +104 -39
- package/test/path-normalizer.test.ts +123 -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 +14 -32
- 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 +69 -14
- package/test/subagent-context.test.ts +77 -31
- package/test/synthesize.test.ts +13 -11
- package/src/access-intent/bash/cwd-projection.ts +0 -498
|
@@ -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
|
});
|
|
@@ -72,7 +72,13 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
72
72
|
test("returns unchanged prompt and empty entries when no skills section present", () => {
|
|
73
73
|
const input = "You are a helpful assistant.";
|
|
74
74
|
const manager = makeManager("allow");
|
|
75
|
-
const result = resolveSkillPromptEntries(
|
|
75
|
+
const result = resolveSkillPromptEntries(
|
|
76
|
+
input,
|
|
77
|
+
manager,
|
|
78
|
+
null,
|
|
79
|
+
CWD,
|
|
80
|
+
"linux",
|
|
81
|
+
);
|
|
76
82
|
expect(result.prompt).toBe(input);
|
|
77
83
|
expect(result.entries).toEqual([]);
|
|
78
84
|
expect(manager.checkPermission).not.toHaveBeenCalled();
|
|
@@ -81,7 +87,13 @@ 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(
|
|
90
|
+
const result = resolveSkillPromptEntries(
|
|
91
|
+
input,
|
|
92
|
+
manager,
|
|
93
|
+
null,
|
|
94
|
+
CWD,
|
|
95
|
+
"linux",
|
|
96
|
+
);
|
|
85
97
|
expect(result.prompt).toContain("librarian");
|
|
86
98
|
expect(result.prompt).toContain("ask-user");
|
|
87
99
|
expect(result.entries).toHaveLength(2);
|
|
@@ -90,7 +102,13 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
90
102
|
test("removes denied skill from section", () => {
|
|
91
103
|
const input = availableSkillsSection("librarian", "dangerous");
|
|
92
104
|
const manager = makeManager("allow", { dangerous: "deny" });
|
|
93
|
-
const result = resolveSkillPromptEntries(
|
|
105
|
+
const result = resolveSkillPromptEntries(
|
|
106
|
+
input,
|
|
107
|
+
manager,
|
|
108
|
+
null,
|
|
109
|
+
CWD,
|
|
110
|
+
"linux",
|
|
111
|
+
);
|
|
94
112
|
expect(result.prompt).toContain("librarian");
|
|
95
113
|
expect(result.prompt).not.toContain("dangerous");
|
|
96
114
|
// denied skill is excluded from returned entries
|
|
@@ -100,7 +118,13 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
100
118
|
test("removes entire section when all skills are denied", () => {
|
|
101
119
|
const input = `Intro\n${availableSkillsSection("dangerous")}\nOutro`;
|
|
102
120
|
const manager = makeManager("deny");
|
|
103
|
-
const result = resolveSkillPromptEntries(
|
|
121
|
+
const result = resolveSkillPromptEntries(
|
|
122
|
+
input,
|
|
123
|
+
manager,
|
|
124
|
+
null,
|
|
125
|
+
CWD,
|
|
126
|
+
"linux",
|
|
127
|
+
);
|
|
104
128
|
expect(result.prompt).not.toContain("<available_skills>");
|
|
105
129
|
expect(result.prompt).toContain("Intro");
|
|
106
130
|
expect(result.prompt).toContain("Outro");
|
|
@@ -110,7 +134,13 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
110
134
|
test("keeps ask-state skills in section and entries", () => {
|
|
111
135
|
const input = availableSkillsSection("librarian");
|
|
112
136
|
const manager = makeManager("ask");
|
|
113
|
-
const result = resolveSkillPromptEntries(
|
|
137
|
+
const result = resolveSkillPromptEntries(
|
|
138
|
+
input,
|
|
139
|
+
manager,
|
|
140
|
+
null,
|
|
141
|
+
CWD,
|
|
142
|
+
"linux",
|
|
143
|
+
);
|
|
114
144
|
expect(result.prompt).toContain("librarian");
|
|
115
145
|
expect(result.entries).toHaveLength(1);
|
|
116
146
|
expect(result.entries[0].state).toBe("ask");
|
|
@@ -119,7 +149,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
119
149
|
test("delegates permission check to permissionManager for each skill", () => {
|
|
120
150
|
const input = availableSkillsSection("alpha", "beta");
|
|
121
151
|
const manager = makeManager("allow");
|
|
122
|
-
resolveSkillPromptEntries(input, manager, null, CWD);
|
|
152
|
+
resolveSkillPromptEntries(input, manager, null, CWD, "linux");
|
|
123
153
|
expect(manager.checkPermission).toHaveBeenCalledWith(
|
|
124
154
|
"skill",
|
|
125
155
|
{ name: "alpha" },
|
|
@@ -135,7 +165,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
135
165
|
test("passes agentName to permissionManager", () => {
|
|
136
166
|
const input = availableSkillsSection("librarian");
|
|
137
167
|
const manager = makeManager("allow");
|
|
138
|
-
resolveSkillPromptEntries(input, manager, "my-agent", CWD);
|
|
168
|
+
resolveSkillPromptEntries(input, manager, "my-agent", CWD, "linux");
|
|
139
169
|
expect(manager.checkPermission).toHaveBeenCalledWith(
|
|
140
170
|
"skill",
|
|
141
171
|
{ name: "librarian" },
|
|
@@ -150,7 +180,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
150
180
|
availableSkillsSection("librarian"),
|
|
151
181
|
].join("\n");
|
|
152
182
|
const manager = makeManager("allow");
|
|
153
|
-
resolveSkillPromptEntries(input, manager, null, CWD);
|
|
183
|
+
resolveSkillPromptEntries(input, manager, null, CWD, "linux");
|
|
154
184
|
// Should only be called once despite appearing twice.
|
|
155
185
|
expect(manager.checkPermission).toHaveBeenCalledTimes(1);
|
|
156
186
|
});
|
|
@@ -159,7 +189,13 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
159
189
|
const location = "/skills/librarian/SKILL.md";
|
|
160
190
|
const input = availableSkillsSection("librarian");
|
|
161
191
|
const manager = makeManager("allow");
|
|
162
|
-
const result = resolveSkillPromptEntries(
|
|
192
|
+
const result = resolveSkillPromptEntries(
|
|
193
|
+
input,
|
|
194
|
+
manager,
|
|
195
|
+
null,
|
|
196
|
+
CWD,
|
|
197
|
+
"linux",
|
|
198
|
+
);
|
|
163
199
|
expect(result.entries[0].normalizedLocation).toBe(location);
|
|
164
200
|
expect(result.entries[0].normalizedBaseDir).toBe("/skills/librarian");
|
|
165
201
|
});
|
|
@@ -169,7 +205,13 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
169
205
|
const section2 = availableSkillsSection("beta");
|
|
170
206
|
const input = `${section1}\n${section2}`;
|
|
171
207
|
const manager = makeManager("allow", { beta: "deny" });
|
|
172
|
-
const result = resolveSkillPromptEntries(
|
|
208
|
+
const result = resolveSkillPromptEntries(
|
|
209
|
+
input,
|
|
210
|
+
manager,
|
|
211
|
+
null,
|
|
212
|
+
CWD,
|
|
213
|
+
"linux",
|
|
214
|
+
);
|
|
173
215
|
expect(result.entries.map((e) => e.name)).toContain("alpha");
|
|
174
216
|
expect(result.entries.map((e) => e.name)).not.toContain("beta");
|
|
175
217
|
});
|
|
@@ -198,15 +240,21 @@ describe("findSkillPathMatch", () => {
|
|
|
198
240
|
];
|
|
199
241
|
|
|
200
242
|
test("returns null for empty normalized path", () => {
|
|
201
|
-
expect(findSkillPathMatch("", entries)).toBeNull();
|
|
243
|
+
expect(findSkillPathMatch("", entries, "linux")).toBeNull();
|
|
202
244
|
});
|
|
203
245
|
|
|
204
246
|
test("returns null for empty entries array", () => {
|
|
205
|
-
expect(
|
|
247
|
+
expect(
|
|
248
|
+
findSkillPathMatch("/skills/librarian/SKILL.md", [], "linux"),
|
|
249
|
+
).toBeNull();
|
|
206
250
|
});
|
|
207
251
|
|
|
208
252
|
test("matches exact location path", () => {
|
|
209
|
-
const match = findSkillPathMatch(
|
|
253
|
+
const match = findSkillPathMatch(
|
|
254
|
+
"/skills/librarian/SKILL.md",
|
|
255
|
+
entries,
|
|
256
|
+
"linux",
|
|
257
|
+
);
|
|
210
258
|
expect(match?.name).toBe("librarian");
|
|
211
259
|
});
|
|
212
260
|
|
|
@@ -214,12 +262,13 @@ describe("findSkillPathMatch", () => {
|
|
|
214
262
|
const match = findSkillPathMatch(
|
|
215
263
|
"/skills/librarian/extra/helper.md",
|
|
216
264
|
entries,
|
|
265
|
+
"linux",
|
|
217
266
|
);
|
|
218
267
|
expect(match?.name).toBe("librarian");
|
|
219
268
|
});
|
|
220
269
|
|
|
221
270
|
test("returns null for path not within any skill directory", () => {
|
|
222
|
-
const match = findSkillPathMatch("/other/path/file.md", entries);
|
|
271
|
+
const match = findSkillPathMatch("/other/path/file.md", entries, "linux");
|
|
223
272
|
expect(match).toBeNull();
|
|
224
273
|
});
|
|
225
274
|
|
|
@@ -228,6 +277,7 @@ describe("findSkillPathMatch", () => {
|
|
|
228
277
|
const match = findSkillPathMatch(
|
|
229
278
|
"/skills/librarian-extra/SKILL.md",
|
|
230
279
|
entries,
|
|
280
|
+
"linux",
|
|
231
281
|
);
|
|
232
282
|
expect(match).toBeNull();
|
|
233
283
|
});
|
|
@@ -254,6 +304,7 @@ describe("findSkillPathMatch", () => {
|
|
|
254
304
|
const match = findSkillPathMatch(
|
|
255
305
|
"/skills/parent/child/helper.md",
|
|
256
306
|
nestedEntries,
|
|
307
|
+
"linux",
|
|
257
308
|
);
|
|
258
309
|
expect(match?.name).toBe("child");
|
|
259
310
|
});
|
|
@@ -330,6 +381,7 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
330
381
|
asChecker(manager),
|
|
331
382
|
null,
|
|
332
383
|
"/cwd",
|
|
384
|
+
"linux",
|
|
333
385
|
);
|
|
334
386
|
|
|
335
387
|
expect(result.prompt).not.toContain("denied-skill");
|
|
@@ -377,16 +429,19 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
|
|
|
377
429
|
asChecker(manager),
|
|
378
430
|
null,
|
|
379
431
|
"/cwd",
|
|
432
|
+
"linux",
|
|
380
433
|
);
|
|
381
434
|
const visiblePath = resolve("/cwd", "./skills/visible/file.ts");
|
|
382
435
|
const blockedPath = resolve("/cwd", "./skills/blocked/file.ts");
|
|
383
436
|
const matchedVisibleSkill = findSkillPathMatch(
|
|
384
437
|
process.platform === "win32" ? visiblePath.toLowerCase() : visiblePath,
|
|
385
438
|
result.entries,
|
|
439
|
+
"linux",
|
|
386
440
|
);
|
|
387
441
|
const matchedBlockedSkill = findSkillPathMatch(
|
|
388
442
|
process.platform === "win32" ? blockedPath.toLowerCase() : blockedPath,
|
|
389
443
|
result.entries,
|
|
444
|
+
"linux",
|
|
390
445
|
);
|
|
391
446
|
|
|
392
447
|
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
|
});
|