@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
|
@@ -117,6 +117,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
117
117
|
"/home/user/.pi/agent/extensions/pi-permission-system/config.json",
|
|
118
118
|
INFRA_DIRS,
|
|
119
119
|
CWD,
|
|
120
|
+
"linux",
|
|
120
121
|
),
|
|
121
122
|
).toBe(true);
|
|
122
123
|
});
|
|
@@ -128,6 +129,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
128
129
|
"/opt/homebrew/lib/node_modules/pi-ask-user/skills",
|
|
129
130
|
INFRA_DIRS,
|
|
130
131
|
CWD,
|
|
132
|
+
"linux",
|
|
131
133
|
),
|
|
132
134
|
).toBe(true);
|
|
133
135
|
});
|
|
@@ -139,6 +141,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
139
141
|
"/home/user/.pi/agent/git/some-package/README.md",
|
|
140
142
|
INFRA_DIRS,
|
|
141
143
|
CWD,
|
|
144
|
+
"linux",
|
|
142
145
|
),
|
|
143
146
|
).toBe(true);
|
|
144
147
|
});
|
|
@@ -150,6 +153,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
150
153
|
"/opt/homebrew/lib/node_modules/pi-permission-system",
|
|
151
154
|
INFRA_DIRS,
|
|
152
155
|
CWD,
|
|
156
|
+
"linux",
|
|
153
157
|
),
|
|
154
158
|
).toBe(true);
|
|
155
159
|
});
|
|
@@ -163,6 +167,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
163
167
|
"/home/user/.pi/agent/extensions/pi-permission-system/config.json",
|
|
164
168
|
INFRA_DIRS,
|
|
165
169
|
CWD,
|
|
170
|
+
"linux",
|
|
166
171
|
),
|
|
167
172
|
).toBe(false);
|
|
168
173
|
});
|
|
@@ -174,6 +179,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
174
179
|
"/opt/homebrew/lib/node_modules/pi-ask-user/skills/ask-user/SKILL.md",
|
|
175
180
|
INFRA_DIRS,
|
|
176
181
|
CWD,
|
|
182
|
+
"linux",
|
|
177
183
|
),
|
|
178
184
|
).toBe(false);
|
|
179
185
|
});
|
|
@@ -185,6 +191,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
185
191
|
"/opt/homebrew/lib/node_modules/pi-ask-user/SKILL.md",
|
|
186
192
|
INFRA_DIRS,
|
|
187
193
|
CWD,
|
|
194
|
+
"linux",
|
|
188
195
|
),
|
|
189
196
|
).toBe(false);
|
|
190
197
|
});
|
|
@@ -192,9 +199,9 @@ describe("isPiInfrastructureRead", () => {
|
|
|
192
199
|
// ── non-infra paths not allowed ──────────────────────────────────────────
|
|
193
200
|
|
|
194
201
|
test("does not allow 'read' for a path outside all infra dirs", () => {
|
|
195
|
-
expect(
|
|
196
|
-
|
|
197
|
-
);
|
|
202
|
+
expect(
|
|
203
|
+
isPiInfrastructureRead("read", "/etc/passwd", INFRA_DIRS, CWD, "linux"),
|
|
204
|
+
).toBe(false);
|
|
198
205
|
});
|
|
199
206
|
|
|
200
207
|
test("does not allow 'read' for a path only partially matching an infra dir prefix", () => {
|
|
@@ -205,6 +212,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
205
212
|
"/home/user/.pi/agent-other/config.json",
|
|
206
213
|
INFRA_DIRS,
|
|
207
214
|
CWD,
|
|
215
|
+
"linux",
|
|
208
216
|
),
|
|
209
217
|
).toBe(false);
|
|
210
218
|
});
|
|
@@ -218,6 +226,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
218
226
|
`${CWD}/.pi/npm/node_modules/some-skill/SKILL.md`,
|
|
219
227
|
INFRA_DIRS,
|
|
220
228
|
CWD,
|
|
229
|
+
"linux",
|
|
221
230
|
),
|
|
222
231
|
).toBe(true);
|
|
223
232
|
});
|
|
@@ -229,6 +238,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
229
238
|
`${CWD}/.pi/git/github.com/org/skill-repo/SKILL.md`,
|
|
230
239
|
INFRA_DIRS,
|
|
231
240
|
CWD,
|
|
241
|
+
"linux",
|
|
232
242
|
),
|
|
233
243
|
).toBe(true);
|
|
234
244
|
});
|
|
@@ -240,6 +250,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
240
250
|
`${CWD}/.pi/npm/node_modules/some-skill/SKILL.md`,
|
|
241
251
|
INFRA_DIRS,
|
|
242
252
|
CWD,
|
|
253
|
+
"linux",
|
|
243
254
|
),
|
|
244
255
|
).toBe(false);
|
|
245
256
|
});
|
|
@@ -247,7 +258,9 @@ describe("isPiInfrastructureRead", () => {
|
|
|
247
258
|
// ── empty / edge cases ───────────────────────────────────────────────────
|
|
248
259
|
|
|
249
260
|
test("returns false when infrastructureDirs is empty and path is not project-local", () => {
|
|
250
|
-
expect(
|
|
261
|
+
expect(
|
|
262
|
+
isPiInfrastructureRead("read", "/etc/passwd", [], CWD, "linux"),
|
|
263
|
+
).toBe(false);
|
|
251
264
|
});
|
|
252
265
|
|
|
253
266
|
test("returns false when infrastructureDirs is empty but path IS project-local .pi/npm", () => {
|
|
@@ -258,6 +271,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
258
271
|
`${CWD}/.pi/npm/node_modules/x/SKILL.md`,
|
|
259
272
|
[],
|
|
260
273
|
CWD,
|
|
274
|
+
"linux",
|
|
261
275
|
),
|
|
262
276
|
).toBe(true);
|
|
263
277
|
});
|
|
@@ -273,6 +287,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
273
287
|
"/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
|
|
274
288
|
["/opt/homebrew/*/@earendil-works/pi-coding-agent/*"],
|
|
275
289
|
CWD,
|
|
290
|
+
"linux",
|
|
276
291
|
),
|
|
277
292
|
).toBe(true);
|
|
278
293
|
});
|
|
@@ -284,6 +299,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
284
299
|
"/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
|
|
285
300
|
["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
|
|
286
301
|
CWD,
|
|
302
|
+
"linux",
|
|
287
303
|
),
|
|
288
304
|
).toBe(true);
|
|
289
305
|
});
|
|
@@ -295,6 +311,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
295
311
|
"/etc/passwd",
|
|
296
312
|
["/opt/homebrew/*/@earendil-works/pi-coding-agent/*"],
|
|
297
313
|
CWD,
|
|
314
|
+
"linux",
|
|
298
315
|
),
|
|
299
316
|
).toBe(false);
|
|
300
317
|
});
|
|
@@ -306,6 +323,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
306
323
|
"/opt/homebrew/X/file.md",
|
|
307
324
|
["/opt/homebrew/?/file.md"],
|
|
308
325
|
CWD,
|
|
326
|
+
"linux",
|
|
309
327
|
),
|
|
310
328
|
).toBe(true);
|
|
311
329
|
});
|
|
@@ -317,6 +335,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
317
335
|
"/opt/homebrew/abc/file.md",
|
|
318
336
|
["/opt/homebrew/?/file.md"],
|
|
319
337
|
CWD,
|
|
338
|
+
"linux",
|
|
320
339
|
),
|
|
321
340
|
).toBe(false);
|
|
322
341
|
});
|
|
@@ -332,6 +351,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
332
351
|
"/home/user/.pi/agent/config.json",
|
|
333
352
|
dirs,
|
|
334
353
|
CWD,
|
|
354
|
+
"linux",
|
|
335
355
|
),
|
|
336
356
|
).toBe(true);
|
|
337
357
|
expect(
|
|
@@ -340,6 +360,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
340
360
|
"/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
|
|
341
361
|
dirs,
|
|
342
362
|
CWD,
|
|
363
|
+
"linux",
|
|
343
364
|
),
|
|
344
365
|
).toBe(true);
|
|
345
366
|
});
|
|
@@ -352,6 +373,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
352
373
|
`${home}/.pi/agent/config.json`,
|
|
353
374
|
["~/.pi/agent"],
|
|
354
375
|
CWD,
|
|
376
|
+
"linux",
|
|
355
377
|
),
|
|
356
378
|
).toBe(true);
|
|
357
379
|
});
|
|
@@ -363,6 +385,7 @@ describe("isPiInfrastructureRead with glob patterns", () => {
|
|
|
363
385
|
"/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
|
|
364
386
|
["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
|
|
365
387
|
CWD,
|
|
388
|
+
"linux",
|
|
366
389
|
),
|
|
367
390
|
).toBe(false);
|
|
368
391
|
});
|
|
@@ -78,6 +78,7 @@ function makeDeps(
|
|
|
78
78
|
config: overrides.config ?? makeConfigReader(),
|
|
79
79
|
subagentSessionsDir:
|
|
80
80
|
overrides.subagentSessionsDir ?? "/test/agent/subagent-sessions",
|
|
81
|
+
platform: overrides.platform ?? "linux",
|
|
81
82
|
registry: overrides.registry,
|
|
82
83
|
prompter: overrides.prompter ?? makePrompterApi(),
|
|
83
84
|
};
|
package/test/rule.test.ts
CHANGED
|
@@ -47,50 +47,65 @@ describe("evaluate", () => {
|
|
|
47
47
|
|
|
48
48
|
test("returns matching rule when a rule matches", () => {
|
|
49
49
|
const ruleset: Ruleset = [allowBashGit];
|
|
50
|
-
const result = evaluate("bash", "git status", ruleset);
|
|
50
|
+
const result = evaluate("bash", "git status", ruleset, "linux");
|
|
51
51
|
expect(result).toEqual(allowBashGit);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test("returns synthetic rule with 'ask' when no rules match and no defaultAction", () => {
|
|
55
|
-
const result = evaluate("bash", "npm install", [allowBashGit]);
|
|
55
|
+
const result = evaluate("bash", "npm install", [allowBashGit], "linux");
|
|
56
56
|
expect(result.surface).toBe("bash");
|
|
57
57
|
expect(result.pattern).toBe("npm install");
|
|
58
58
|
expect(result.action).toBe("ask");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
test("returns synthetic rule with custom defaultAction when no rules match", () => {
|
|
62
|
-
const result = evaluate(
|
|
62
|
+
const result = evaluate(
|
|
63
|
+
"bash",
|
|
64
|
+
"npm install",
|
|
65
|
+
[allowBashGit],
|
|
66
|
+
"linux",
|
|
67
|
+
"deny",
|
|
68
|
+
);
|
|
63
69
|
expect(result.surface).toBe("bash");
|
|
64
70
|
expect(result.pattern).toBe("npm install");
|
|
65
71
|
expect(result.action).toBe("deny");
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
test("defaultAction does not affect matched rules", () => {
|
|
69
|
-
const result = evaluate(
|
|
75
|
+
const result = evaluate(
|
|
76
|
+
"bash",
|
|
77
|
+
"git status",
|
|
78
|
+
[allowBashGit],
|
|
79
|
+
"linux",
|
|
80
|
+
"deny",
|
|
81
|
+
);
|
|
70
82
|
expect(result).toEqual(allowBashGit);
|
|
71
83
|
});
|
|
72
84
|
|
|
73
85
|
test("returns synthetic rule for empty ruleset", () => {
|
|
74
|
-
const result = evaluate("mcp", "exa_search", []);
|
|
86
|
+
const result = evaluate("mcp", "exa_search", [], "linux");
|
|
75
87
|
expect(result.surface).toBe("mcp");
|
|
76
88
|
expect(result.pattern).toBe("exa_search");
|
|
77
89
|
expect(result.action).toBe("ask");
|
|
78
90
|
});
|
|
79
91
|
|
|
80
92
|
test("matches rules for all permission surfaces", () => {
|
|
81
|
-
expect(evaluate("read", "src/foo.ts", [allowRead]).action).toBe(
|
|
82
|
-
expect(evaluate("mcp", "exa_search", [askMcp]).action).toBe("ask");
|
|
83
|
-
expect(evaluate("skill", "librarian", [allowSkillLibrarian]).action).toBe(
|
|
93
|
+
expect(evaluate("read", "src/foo.ts", [allowRead], "linux").action).toBe(
|
|
84
94
|
"allow",
|
|
85
95
|
);
|
|
96
|
+
expect(evaluate("mcp", "exa_search", [askMcp], "linux").action).toBe("ask");
|
|
86
97
|
expect(
|
|
87
|
-
evaluate("
|
|
98
|
+
evaluate("skill", "librarian", [allowSkillLibrarian], "linux").action,
|
|
99
|
+
).toBe("allow");
|
|
100
|
+
expect(
|
|
101
|
+
evaluate("special", "external_directory", [askSpecialExtDir], "linux")
|
|
102
|
+
.action,
|
|
88
103
|
).toBe("ask");
|
|
89
104
|
});
|
|
90
105
|
|
|
91
106
|
test("last-match-wins: later conflicting rule overrides earlier", () => {
|
|
92
107
|
const ruleset: Ruleset = [allowBashGit, denyBashGitPush];
|
|
93
|
-
const result = evaluate("bash", "git push origin main", ruleset);
|
|
108
|
+
const result = evaluate("bash", "git push origin main", ruleset, "linux");
|
|
94
109
|
expect(result).toEqual(denyBashGitPush);
|
|
95
110
|
});
|
|
96
111
|
|
|
@@ -107,7 +122,12 @@ describe("evaluate", () => {
|
|
|
107
122
|
action: "allow",
|
|
108
123
|
origin: "global",
|
|
109
124
|
};
|
|
110
|
-
const result = evaluate(
|
|
125
|
+
const result = evaluate(
|
|
126
|
+
"bash",
|
|
127
|
+
"git status",
|
|
128
|
+
[denyAll, allowStatus],
|
|
129
|
+
"linux",
|
|
130
|
+
);
|
|
111
131
|
expect(result).toEqual(allowStatus);
|
|
112
132
|
});
|
|
113
133
|
|
|
@@ -118,17 +138,21 @@ describe("evaluate", () => {
|
|
|
118
138
|
action: "allow",
|
|
119
139
|
origin: "global",
|
|
120
140
|
};
|
|
121
|
-
expect(evaluate("bash", "anything", [universalAllow]).action).toBe(
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
expect(evaluate("bash", "anything", [universalAllow], "linux").action).toBe(
|
|
142
|
+
"allow",
|
|
143
|
+
);
|
|
144
|
+
expect(evaluate("mcp", "something", [universalAllow], "linux").action).toBe(
|
|
124
145
|
"allow",
|
|
125
146
|
);
|
|
147
|
+
expect(
|
|
148
|
+
evaluate("skill", "librarian", [universalAllow], "linux").action,
|
|
149
|
+
).toBe("allow");
|
|
126
150
|
});
|
|
127
151
|
|
|
128
152
|
test("specific surface rule does not match a different surface", () => {
|
|
129
153
|
const ruleset: Ruleset = [allowBashGit];
|
|
130
154
|
// bash rule should not match mcp surface
|
|
131
|
-
const result = evaluate("mcp", "git status", ruleset);
|
|
155
|
+
const result = evaluate("mcp", "git status", ruleset, "linux");
|
|
132
156
|
expect(result.action).toBe("ask"); // falls back to default
|
|
133
157
|
});
|
|
134
158
|
|
|
@@ -140,7 +164,7 @@ describe("evaluate", () => {
|
|
|
140
164
|
{ surface: "bash", pattern: "git *", action: "allow", origin: "agent" },
|
|
141
165
|
];
|
|
142
166
|
const merged = [...globalRules, ...agentRules];
|
|
143
|
-
const result = evaluate("bash", "git status", merged);
|
|
167
|
+
const result = evaluate("bash", "git status", merged, "linux");
|
|
144
168
|
expect(result.action).toBe("allow"); // agent rule wins
|
|
145
169
|
});
|
|
146
170
|
|
|
@@ -153,12 +177,12 @@ describe("evaluate", () => {
|
|
|
153
177
|
];
|
|
154
178
|
// git status matches global but not agent rule
|
|
155
179
|
const merged = [...globalRules, ...agentRules];
|
|
156
|
-
const result = evaluate("bash", "git status", merged);
|
|
180
|
+
const result = evaluate("bash", "git status", merged, "linux");
|
|
157
181
|
expect(result.action).toBe("allow"); // global rule is the last match for this pattern
|
|
158
182
|
});
|
|
159
183
|
|
|
160
184
|
test("empty ruleset returns synthetic default", () => {
|
|
161
|
-
const result = evaluate("bash", "git status", []);
|
|
185
|
+
const result = evaluate("bash", "git status", [], "linux");
|
|
162
186
|
expect(result.surface).toBe("bash");
|
|
163
187
|
expect(result.pattern).toBe("git status");
|
|
164
188
|
expect(result.action).toBe("ask");
|
|
@@ -186,14 +210,18 @@ describe("evaluate", () => {
|
|
|
186
210
|
origin: "builtin",
|
|
187
211
|
};
|
|
188
212
|
// Both rules with and without layer field produce the same match.
|
|
189
|
-
expect(evaluate("bash", "git status", [withLayer]).action).toBe(
|
|
190
|
-
|
|
213
|
+
expect(evaluate("bash", "git status", [withLayer], "linux").action).toBe(
|
|
214
|
+
"allow",
|
|
215
|
+
);
|
|
216
|
+
expect(evaluate("bash", "git status", [withoutLayer], "linux").action).toBe(
|
|
217
|
+
"allow",
|
|
218
|
+
);
|
|
191
219
|
// Layer metadata does not affect last-match-wins ordering.
|
|
192
220
|
const ruleset: Rule[] = [withDefault, withLayer];
|
|
193
|
-
expect(evaluate("bash", "git status", ruleset)).toEqual(withLayer);
|
|
221
|
+
expect(evaluate("bash", "git status", ruleset, "linux")).toEqual(withLayer);
|
|
194
222
|
// A rule with layer: "default" still wins if it is last in the array.
|
|
195
223
|
const reversedRuleset: Rule[] = [withLayer, withDefault];
|
|
196
|
-
expect(evaluate("bash", "git status", reversedRuleset)).toEqual(
|
|
224
|
+
expect(evaluate("bash", "git status", reversedRuleset, "linux")).toEqual(
|
|
197
225
|
withDefault,
|
|
198
226
|
);
|
|
199
227
|
});
|
|
@@ -207,12 +235,12 @@ describe("evaluate", () => {
|
|
|
207
235
|
layer: "config",
|
|
208
236
|
origin,
|
|
209
237
|
};
|
|
210
|
-
const result = evaluate("bash", "git status", [rule]);
|
|
238
|
+
const result = evaluate("bash", "git status", [rule], "linux");
|
|
211
239
|
expect(result.origin).toBe("project");
|
|
212
240
|
});
|
|
213
241
|
|
|
214
242
|
test("evaluate() synthetic fallback rule has origin 'builtin'", () => {
|
|
215
|
-
const result = evaluate("bash", "npm install", []);
|
|
243
|
+
const result = evaluate("bash", "npm install", [], "linux");
|
|
216
244
|
expect(result.origin).toBe("builtin");
|
|
217
245
|
});
|
|
218
246
|
|
|
@@ -225,7 +253,7 @@ describe("evaluate", () => {
|
|
|
225
253
|
layer: "config",
|
|
226
254
|
origin: "global",
|
|
227
255
|
};
|
|
228
|
-
const result = evaluate("bash", "npm install", [rule]);
|
|
256
|
+
const result = evaluate("bash", "npm install", [rule], "linux");
|
|
229
257
|
expect(result.action).toBe("deny");
|
|
230
258
|
expect(result.reason).toBe("Use pnpm instead");
|
|
231
259
|
});
|
|
@@ -246,7 +274,12 @@ describe("evaluate", () => {
|
|
|
246
274
|
layer: "config",
|
|
247
275
|
origin: "global",
|
|
248
276
|
};
|
|
249
|
-
const result = evaluate(
|
|
277
|
+
const result = evaluate(
|
|
278
|
+
"bash",
|
|
279
|
+
"npm install",
|
|
280
|
+
[allowAll, denyNpm],
|
|
281
|
+
"linux",
|
|
282
|
+
);
|
|
250
283
|
expect(result.action).toBe("deny");
|
|
251
284
|
expect(result.reason).toBe("Use pnpm");
|
|
252
285
|
});
|
|
@@ -267,13 +300,18 @@ describe("evaluate", () => {
|
|
|
267
300
|
layer: "config",
|
|
268
301
|
origin: "global",
|
|
269
302
|
};
|
|
270
|
-
const result = evaluate(
|
|
303
|
+
const result = evaluate(
|
|
304
|
+
"bash",
|
|
305
|
+
"npm install",
|
|
306
|
+
[denyNpm, allowInstall],
|
|
307
|
+
"linux",
|
|
308
|
+
);
|
|
271
309
|
expect(result.action).toBe("allow");
|
|
272
310
|
expect(result.reason).toBeUndefined();
|
|
273
311
|
});
|
|
274
312
|
|
|
275
313
|
test("evaluate() synthetic fallback rule has no reason", () => {
|
|
276
|
-
const result = evaluate("bash", "npm install", []);
|
|
314
|
+
const result = evaluate("bash", "npm install", [], "linux");
|
|
277
315
|
expect(result.reason).toBeUndefined();
|
|
278
316
|
});
|
|
279
317
|
|
|
@@ -295,7 +333,7 @@ describe("evaluate", () => {
|
|
|
295
333
|
layer: "config",
|
|
296
334
|
origin,
|
|
297
335
|
};
|
|
298
|
-
expect(evaluate("read", "*", [rule]).origin).toBe(origin);
|
|
336
|
+
expect(evaluate("read", "*", [rule], "linux").origin).toBe(origin);
|
|
299
337
|
}
|
|
300
338
|
});
|
|
301
339
|
|
|
@@ -321,7 +359,6 @@ describe("evaluate", () => {
|
|
|
321
359
|
"external_directory",
|
|
322
360
|
"c:\\users\\foo\\pi\\docs\\readme.md",
|
|
323
361
|
[denyExternalAll, allowExternalPi],
|
|
324
|
-
undefined,
|
|
325
362
|
"win32",
|
|
326
363
|
);
|
|
327
364
|
expect(result.action).toBe("allow");
|
|
@@ -332,7 +369,6 @@ describe("evaluate", () => {
|
|
|
332
369
|
"external_directory",
|
|
333
370
|
"c:\\users\\foo\\pi\\docs\\readme.md",
|
|
334
371
|
[denyExternalAll, allowExternalPi],
|
|
335
|
-
undefined,
|
|
336
372
|
"linux",
|
|
337
373
|
);
|
|
338
374
|
expect(result.action).toBe("deny");
|
|
@@ -350,7 +386,6 @@ describe("evaluate", () => {
|
|
|
350
386
|
"external_directory",
|
|
351
387
|
"c:\\users\\foo\\pi\\docs\\readme.md",
|
|
352
388
|
[denyExternalAll, allowForwardSlash],
|
|
353
|
-
undefined,
|
|
354
389
|
"win32",
|
|
355
390
|
);
|
|
356
391
|
expect(result.action).toBe("allow");
|
|
@@ -368,7 +403,6 @@ describe("evaluate", () => {
|
|
|
368
403
|
origin: "global",
|
|
369
404
|
},
|
|
370
405
|
],
|
|
371
|
-
undefined,
|
|
372
406
|
"win32",
|
|
373
407
|
);
|
|
374
408
|
expect(result.action).toBe("ask");
|
|
@@ -400,7 +434,7 @@ describe("evaluateFirst", () => {
|
|
|
400
434
|
|
|
401
435
|
test("returns the first candidate that matches a non-default rule", () => {
|
|
402
436
|
const rules: Ruleset = [defaultRule, allowBash];
|
|
403
|
-
const result = evaluateFirst("bash", ["git status", "*"], rules);
|
|
437
|
+
const result = evaluateFirst("bash", ["git status", "*"], rules, "linux");
|
|
404
438
|
expect(result.rule).toEqual(allowBash);
|
|
405
439
|
expect(result.value).toBe("git status");
|
|
406
440
|
});
|
|
@@ -409,14 +443,14 @@ describe("evaluateFirst", () => {
|
|
|
409
443
|
// "npm install" matches only the default; "*" also matches only the
|
|
410
444
|
// default — falls back to first candidate.
|
|
411
445
|
const rules: Ruleset = [defaultRule];
|
|
412
|
-
const result = evaluateFirst("bash", ["npm install", "*"], rules);
|
|
446
|
+
const result = evaluateFirst("bash", ["npm install", "*"], rules, "linux");
|
|
413
447
|
expect(result.rule.layer).toBe("default");
|
|
414
448
|
expect(result.value).toBe("npm install");
|
|
415
449
|
});
|
|
416
450
|
|
|
417
451
|
test("falls back to first candidate when all candidates match only the default", () => {
|
|
418
452
|
const rules: Ruleset = [defaultRule];
|
|
419
|
-
const result = evaluateFirst("bash", ["a", "b", "c"], rules);
|
|
453
|
+
const result = evaluateFirst("bash", ["a", "b", "c"], rules, "linux");
|
|
420
454
|
expect(result.value).toBe("a");
|
|
421
455
|
});
|
|
422
456
|
|
|
@@ -431,7 +465,7 @@ describe("evaluateFirst", () => {
|
|
|
431
465
|
origin: "global",
|
|
432
466
|
};
|
|
433
467
|
const rules: Ruleset = [defaultRule, denyMcp, allowMcpCatchAll];
|
|
434
|
-
const result = evaluateFirst("mcp", ["exa_search", "mcp"], rules);
|
|
468
|
+
const result = evaluateFirst("mcp", ["exa_search", "mcp"], rules, "linux");
|
|
435
469
|
expect(result.rule).toEqual(denyMcp);
|
|
436
470
|
expect(result.value).toBe("exa_search");
|
|
437
471
|
});
|
|
@@ -440,21 +474,26 @@ describe("evaluateFirst", () => {
|
|
|
440
474
|
// "unknown_tool" matches only the universal default;
|
|
441
475
|
// "exa_search" matches denyMcp (non-default) — that is the result.
|
|
442
476
|
const rules: Ruleset = [defaultRule, denyMcp];
|
|
443
|
-
const result = evaluateFirst(
|
|
477
|
+
const result = evaluateFirst(
|
|
478
|
+
"mcp",
|
|
479
|
+
["unknown_tool", "exa_search"],
|
|
480
|
+
rules,
|
|
481
|
+
"linux",
|
|
482
|
+
);
|
|
444
483
|
expect(result.rule).toEqual(denyMcp);
|
|
445
484
|
expect(result.value).toBe("exa_search");
|
|
446
485
|
});
|
|
447
486
|
|
|
448
487
|
test("single-candidate array behaves like evaluate()", () => {
|
|
449
488
|
const rules: Ruleset = [defaultRule, allowBash];
|
|
450
|
-
const result = evaluateFirst("bash", ["git status"], rules);
|
|
489
|
+
const result = evaluateFirst("bash", ["git status"], rules, "linux");
|
|
451
490
|
expect(result.rule).toEqual(allowBash);
|
|
452
491
|
expect(result.value).toBe("git status");
|
|
453
492
|
});
|
|
454
493
|
|
|
455
494
|
test("uses '*' as fallback value when values array is empty", () => {
|
|
456
495
|
const rules: Ruleset = [defaultRule];
|
|
457
|
-
const result = evaluateFirst("bash", [], rules);
|
|
496
|
+
const result = evaluateFirst("bash", [], rules, "linux");
|
|
458
497
|
expect(result.value).toBe("*");
|
|
459
498
|
});
|
|
460
499
|
});
|
|
@@ -495,6 +534,7 @@ describe("evaluateAnyValue", () => {
|
|
|
495
534
|
"path",
|
|
496
535
|
["/proj/src/foo.ts", "src/foo.ts"],
|
|
497
536
|
rules,
|
|
537
|
+
"linux",
|
|
498
538
|
);
|
|
499
539
|
expect(result.rule).toEqual(relativeDeny);
|
|
500
540
|
expect(result.value).toBe("src/foo.ts");
|
|
@@ -506,6 +546,7 @@ describe("evaluateAnyValue", () => {
|
|
|
506
546
|
"path",
|
|
507
547
|
["/proj/src/foo.ts", "src/foo.ts"],
|
|
508
548
|
rules,
|
|
549
|
+
"linux",
|
|
509
550
|
);
|
|
510
551
|
expect(result.rule).toEqual(absoluteAllow);
|
|
511
552
|
expect(result.value).toBe("/proj/src/foo.ts");
|
|
@@ -516,13 +557,14 @@ describe("evaluateAnyValue", () => {
|
|
|
516
557
|
"path",
|
|
517
558
|
["/proj/src/foo.ts", "src/foo.ts"],
|
|
518
559
|
[],
|
|
560
|
+
"linux",
|
|
519
561
|
);
|
|
520
562
|
expect(result.rule.action).toBe("ask");
|
|
521
563
|
expect(result.value).toBe("/proj/src/foo.ts");
|
|
522
564
|
});
|
|
523
565
|
|
|
524
566
|
test("uses '*' as fallback value when values array is empty", () => {
|
|
525
|
-
const result = evaluateAnyValue("path", [], []);
|
|
567
|
+
const result = evaluateAnyValue("path", [], [], "linux");
|
|
526
568
|
expect(result.value).toBe("*");
|
|
527
569
|
});
|
|
528
570
|
});
|
|
@@ -556,6 +598,7 @@ describe("evaluateMostRestrictive", () => {
|
|
|
556
598
|
"path",
|
|
557
599
|
[".env", "README.md"],
|
|
558
600
|
rules,
|
|
601
|
+
"linux",
|
|
559
602
|
);
|
|
560
603
|
expect(result).not.toBeNull();
|
|
561
604
|
expect(result!.rule.action).toBe("deny");
|
|
@@ -568,6 +611,7 @@ describe("evaluateMostRestrictive", () => {
|
|
|
568
611
|
"path",
|
|
569
612
|
["/home/user/.ssh/id_rsa", "README.md"],
|
|
570
613
|
rules,
|
|
614
|
+
"linux",
|
|
571
615
|
);
|
|
572
616
|
expect(result).not.toBeNull();
|
|
573
617
|
expect(result!.rule.action).toBe("ask");
|
|
@@ -580,13 +624,14 @@ describe("evaluateMostRestrictive", () => {
|
|
|
580
624
|
"path",
|
|
581
625
|
["README.md", "src/index.ts"],
|
|
582
626
|
rules,
|
|
627
|
+
"linux",
|
|
583
628
|
);
|
|
584
629
|
expect(result).toBeNull();
|
|
585
630
|
});
|
|
586
631
|
|
|
587
632
|
test("empty values: returns null", () => {
|
|
588
633
|
const rules: Ruleset = [allowAll, denyEnv];
|
|
589
|
-
const result = evaluateMostRestrictive("path", [], rules);
|
|
634
|
+
const result = evaluateMostRestrictive("path", [], rules, "linux");
|
|
590
635
|
expect(result).toBeNull();
|
|
591
636
|
});
|
|
592
637
|
|
|
@@ -596,6 +641,7 @@ describe("evaluateMostRestrictive", () => {
|
|
|
596
641
|
"path",
|
|
597
642
|
["/home/user/.ssh/id_rsa", ".env"],
|
|
598
643
|
rules,
|
|
644
|
+
"linux",
|
|
599
645
|
);
|
|
600
646
|
expect(result).not.toBeNull();
|
|
601
647
|
expect(result!.rule.action).toBe("deny");
|