@gotgenes/pi-permission-system 16.0.2 → 16.2.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 +20 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +57 -0
- package/src/access-intent/access-path.ts +88 -0
- package/src/access-intent/bash/cwd-projection.ts +9 -4
- package/src/access-intent/bash/program.ts +8 -8
- package/src/handlers/gates/bash-command.ts +18 -3
- package/src/handlers/gates/bash-external-directory.ts +16 -30
- package/src/handlers/gates/bash-path-extractor.ts +9 -3
- package/src/handlers/gates/bash-path.ts +6 -4
- package/src/handlers/gates/external-directory-policy.ts +66 -0
- package/src/handlers/gates/external-directory.ts +8 -9
- package/src/handlers/gates/path.ts +6 -5
- package/src/handlers/gates/runner.ts +6 -5
- package/src/handlers/gates/tool-call-gate-pipeline.ts +6 -5
- package/src/path-utils.ts +0 -20
- package/src/permission-event-rpc.ts +4 -6
- package/src/permission-manager.ts +39 -55
- package/src/permission-resolver.ts +45 -57
- package/src/permissions-service.ts +2 -4
- package/src/skill-prompt-sanitizer.ts +2 -2
- package/test/access-intent/access-path.test.ts +107 -0
- package/test/access-intent/bash/program.test.ts +30 -12
- package/test/handlers/before-agent-start.test.ts +6 -9
- package/test/handlers/external-directory-session-dedup.test.ts +16 -40
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -3
- package/test/handlers/gates/bash-command.test.ts +26 -23
- package/test/handlers/gates/bash-external-directory.test.ts +36 -32
- package/test/handlers/gates/bash-path.test.ts +12 -8
- package/test/handlers/gates/external-directory-policy.test.ts +124 -0
- package/test/handlers/gates/external-directory.test.ts +11 -5
- package/test/handlers/gates/path.test.ts +30 -25
- package/test/handlers/gates/runner.test.ts +6 -1
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +4 -3
- package/test/handlers/input.test.ts +1 -1
- package/test/helpers/gate-fixtures.ts +12 -24
- package/test/helpers/handler-fixtures.ts +21 -15
- package/test/helpers/session-fixtures.ts +3 -18
- package/test/path-utils.test.ts +0 -34
- package/test/permission-event-rpc.test.ts +24 -20
- package/test/permission-manager-unified.test.ts +445 -209
- package/test/permission-resolver.test.ts +112 -89
- package/test/permissions-service.test.ts +10 -7
- package/test/skill-prompt-sanitizer.test.ts +30 -7
|
@@ -8,6 +8,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
import { describe, expect, it, test } from "vitest";
|
|
11
|
+
import type { ResolvedAccessIntent } from "#src/access-intent/access-intent";
|
|
11
12
|
import {
|
|
12
13
|
getGlobalConfigPath,
|
|
13
14
|
getProjectAgentsDir,
|
|
@@ -70,6 +71,35 @@ const sessionAllow = (surface: string, pattern: string): Rule => ({
|
|
|
70
71
|
origin: "session",
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
// Adapters that build an AccessIntent and call the unified `check` entry point,
|
|
75
|
+
// so these tests exercise the single resolution path (#478) without a
|
|
76
|
+
// production-class wrapper used only by tests.
|
|
77
|
+
function checkTool(
|
|
78
|
+
manager: PermissionManager,
|
|
79
|
+
toolName: string,
|
|
80
|
+
input: unknown,
|
|
81
|
+
agentName?: string,
|
|
82
|
+
sessionRules?: Ruleset,
|
|
83
|
+
): PermissionCheckResult {
|
|
84
|
+
return manager.check(
|
|
85
|
+
{ kind: "tool", surface: toolName, input, agentName },
|
|
86
|
+
sessionRules,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkPathValues(
|
|
91
|
+
manager: PermissionManager,
|
|
92
|
+
values: readonly string[],
|
|
93
|
+
agentName?: string,
|
|
94
|
+
sessionRules?: Ruleset,
|
|
95
|
+
surface = "path",
|
|
96
|
+
): PermissionCheckResult {
|
|
97
|
+
return manager.check(
|
|
98
|
+
{ kind: "path-values", surface, values, agentName },
|
|
99
|
+
sessionRules,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
73
103
|
// ---------------------------------------------------------------------------
|
|
74
104
|
// Step 5: session rules concatenated — wins over config/default
|
|
75
105
|
// ---------------------------------------------------------------------------
|
|
@@ -80,7 +110,8 @@ describe("checkPermission — session rules", () => {
|
|
|
80
110
|
const sessionRules: Ruleset = [
|
|
81
111
|
sessionAllow("external_directory", "/other/project"),
|
|
82
112
|
];
|
|
83
|
-
const result =
|
|
113
|
+
const result = checkTool(
|
|
114
|
+
manager,
|
|
84
115
|
"external_directory",
|
|
85
116
|
{ path: "/other/project" },
|
|
86
117
|
undefined,
|
|
@@ -94,7 +125,8 @@ describe("checkPermission — session rules", () => {
|
|
|
94
125
|
it("session rule wins over the universal default (skill)", () => {
|
|
95
126
|
const manager = makeManager();
|
|
96
127
|
const sessionRules: Ruleset = [sessionAllow("skill", "librarian")];
|
|
97
|
-
const result =
|
|
128
|
+
const result = checkTool(
|
|
129
|
+
manager,
|
|
98
130
|
"skill",
|
|
99
131
|
{ name: "librarian" },
|
|
100
132
|
undefined,
|
|
@@ -108,7 +140,8 @@ describe("checkPermission — session rules", () => {
|
|
|
108
140
|
it("session rule wins over the universal default (bash)", () => {
|
|
109
141
|
const manager = makeManager();
|
|
110
142
|
const sessionRules: Ruleset = [sessionAllow("bash", "git status")];
|
|
111
|
-
const result =
|
|
143
|
+
const result = checkTool(
|
|
144
|
+
manager,
|
|
112
145
|
"bash",
|
|
113
146
|
{ command: "git status" },
|
|
114
147
|
undefined,
|
|
@@ -122,7 +155,7 @@ describe("checkPermission — session rules", () => {
|
|
|
122
155
|
it("session rule wins over the universal default (tool — read)", () => {
|
|
123
156
|
const manager = makeManager();
|
|
124
157
|
const sessionRules: Ruleset = [sessionAllow("read", "*")];
|
|
125
|
-
const result = manager
|
|
158
|
+
const result = checkTool(manager, "read", {}, undefined, sessionRules);
|
|
126
159
|
expect(result.state).toBe("allow");
|
|
127
160
|
expect(result.source).toBe("session");
|
|
128
161
|
});
|
|
@@ -130,14 +163,14 @@ describe("checkPermission — session rules", () => {
|
|
|
130
163
|
it("session rule wins over the universal default (mcp)", () => {
|
|
131
164
|
const manager = makeManager();
|
|
132
165
|
const sessionRules: Ruleset = [sessionAllow("mcp", "mcp_status")];
|
|
133
|
-
const result = manager
|
|
166
|
+
const result = checkTool(manager, "mcp", {}, undefined, sessionRules);
|
|
134
167
|
expect(result.state).toBe("allow");
|
|
135
168
|
expect(result.source).toBe("session");
|
|
136
169
|
});
|
|
137
170
|
|
|
138
171
|
it("no session rules — falls through to default (ask)", () => {
|
|
139
172
|
const manager = makeManager();
|
|
140
|
-
const result = manager
|
|
173
|
+
const result = checkTool(manager, "read", {}, undefined, []);
|
|
141
174
|
expect(result.state).toBe("ask");
|
|
142
175
|
expect(result.source).not.toBe("session");
|
|
143
176
|
});
|
|
@@ -146,7 +179,8 @@ describe("checkPermission — session rules", () => {
|
|
|
146
179
|
const manager = makeManager();
|
|
147
180
|
// Only "git status" is session-approved; "git push" should fall through to default.
|
|
148
181
|
const sessionRules: Ruleset = [sessionAllow("bash", "git status")];
|
|
149
|
-
const result =
|
|
182
|
+
const result = checkTool(
|
|
183
|
+
manager,
|
|
150
184
|
"bash",
|
|
151
185
|
{ command: "git push origin main" },
|
|
152
186
|
undefined,
|
|
@@ -159,13 +193,15 @@ describe("checkPermission — session rules", () => {
|
|
|
159
193
|
it("session wildcard pattern matches multiple commands", () => {
|
|
160
194
|
const manager = makeManager();
|
|
161
195
|
const sessionRules: Ruleset = [sessionAllow("bash", "git *")];
|
|
162
|
-
const push =
|
|
196
|
+
const push = checkTool(
|
|
197
|
+
manager,
|
|
163
198
|
"bash",
|
|
164
199
|
{ command: "git push origin main" },
|
|
165
200
|
undefined,
|
|
166
201
|
sessionRules,
|
|
167
202
|
);
|
|
168
|
-
const status =
|
|
203
|
+
const status = checkTool(
|
|
204
|
+
manager,
|
|
169
205
|
"bash",
|
|
170
206
|
{ command: "git status" },
|
|
171
207
|
undefined,
|
|
@@ -190,7 +226,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
190
226
|
external_directory: { "/trusted/*": "allow" },
|
|
191
227
|
});
|
|
192
228
|
try {
|
|
193
|
-
const result = manager
|
|
229
|
+
const result = checkTool(manager, "external_directory", {
|
|
194
230
|
path: "/trusted/repo",
|
|
195
231
|
});
|
|
196
232
|
expect(result.state).toBe("allow");
|
|
@@ -203,7 +239,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
203
239
|
|
|
204
240
|
it("source is 'special' even for a default match (no config rule)", () => {
|
|
205
241
|
const manager = makeManager();
|
|
206
|
-
const result = manager
|
|
242
|
+
const result = checkTool(manager, "external_directory", {
|
|
207
243
|
path: "/some/path",
|
|
208
244
|
});
|
|
209
245
|
expect(result.state).toBe("ask");
|
|
@@ -213,7 +249,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
213
249
|
|
|
214
250
|
it("matchedPattern is undefined for a default match", () => {
|
|
215
251
|
const manager = makeManager();
|
|
216
|
-
const result = manager
|
|
252
|
+
const result = checkTool(manager, "external_directory", {
|
|
217
253
|
path: "/unknown",
|
|
218
254
|
});
|
|
219
255
|
expect(result.matchedPattern).toBeUndefined();
|
|
@@ -227,7 +263,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
227
263
|
skill: { librarian: "allow" },
|
|
228
264
|
});
|
|
229
265
|
try {
|
|
230
|
-
const result = manager
|
|
266
|
+
const result = checkTool(manager, "skill", { name: "librarian" });
|
|
231
267
|
expect(result.state).toBe("allow");
|
|
232
268
|
expect(result.source).toBe("skill");
|
|
233
269
|
expect(result.matchedPattern).toBe("librarian");
|
|
@@ -238,7 +274,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
238
274
|
|
|
239
275
|
it("source is 'skill' even for a default match", () => {
|
|
240
276
|
const manager = makeManager();
|
|
241
|
-
const result = manager
|
|
277
|
+
const result = checkTool(manager, "skill", { name: "unknown" });
|
|
242
278
|
expect(result.state).toBe("ask");
|
|
243
279
|
expect(result.source).toBe("skill");
|
|
244
280
|
});
|
|
@@ -251,7 +287,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
251
287
|
bash: { "git *": "allow" },
|
|
252
288
|
});
|
|
253
289
|
try {
|
|
254
|
-
const result = manager
|
|
290
|
+
const result = checkTool(manager, "bash", {
|
|
255
291
|
command: "git status",
|
|
256
292
|
});
|
|
257
293
|
expect(result.state).toBe("allow");
|
|
@@ -265,7 +301,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
265
301
|
|
|
266
302
|
it("source is 'bash' even for a default match, command is empty string", () => {
|
|
267
303
|
const manager = makeManager();
|
|
268
|
-
const result = manager
|
|
304
|
+
const result = checkTool(manager, "bash", {});
|
|
269
305
|
expect(result.source).toBe("bash");
|
|
270
306
|
expect(result.command).toBe("");
|
|
271
307
|
expect(result.matchedPattern).toBeUndefined();
|
|
@@ -279,7 +315,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
279
315
|
["exa"],
|
|
280
316
|
);
|
|
281
317
|
try {
|
|
282
|
-
const result = manager
|
|
318
|
+
const result = checkTool(manager, "mcp", {
|
|
283
319
|
tool: "exa:search",
|
|
284
320
|
server: "exa",
|
|
285
321
|
});
|
|
@@ -294,7 +330,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
294
330
|
|
|
295
331
|
it("source is 'default' when all targets match only the synthesized default", () => {
|
|
296
332
|
const manager = makeManager();
|
|
297
|
-
const result = manager
|
|
333
|
+
const result = checkTool(manager, "mcp", { tool: "exa:search" });
|
|
298
334
|
expect(result.state).toBe("ask");
|
|
299
335
|
expect(result.source).toBe("default");
|
|
300
336
|
expect(result.matchedPattern).toBeUndefined();
|
|
@@ -306,7 +342,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
306
342
|
[],
|
|
307
343
|
);
|
|
308
344
|
try {
|
|
309
|
-
const result = manager
|
|
345
|
+
const result = checkTool(manager, "mcp", {});
|
|
310
346
|
expect(result.target).toBeDefined();
|
|
311
347
|
expect(result.source).toBe("mcp");
|
|
312
348
|
} finally {
|
|
@@ -322,7 +358,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
322
358
|
read: "allow",
|
|
323
359
|
});
|
|
324
360
|
try {
|
|
325
|
-
const result = manager
|
|
361
|
+
const result = checkTool(manager, "read", {});
|
|
326
362
|
expect(result.state).toBe("allow");
|
|
327
363
|
expect(result.source).toBe("tool");
|
|
328
364
|
} finally {
|
|
@@ -332,14 +368,14 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
332
368
|
|
|
333
369
|
it("built-in tool: source is 'tool' even for a default match", () => {
|
|
334
370
|
const manager = makeManager();
|
|
335
|
-
const result = manager
|
|
371
|
+
const result = checkTool(manager, "read", {});
|
|
336
372
|
expect(result.state).toBe("ask");
|
|
337
373
|
expect(result.source).toBe("tool");
|
|
338
374
|
});
|
|
339
375
|
|
|
340
376
|
it("extension tool: source is 'default' when no config rule matches", () => {
|
|
341
377
|
const manager = makeManager();
|
|
342
|
-
const result = manager
|
|
378
|
+
const result = checkTool(manager, "my_custom_tool", {});
|
|
343
379
|
expect(result.state).toBe("ask");
|
|
344
380
|
expect(result.source).toBe("default");
|
|
345
381
|
});
|
|
@@ -350,7 +386,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
350
386
|
my_custom_tool: "allow",
|
|
351
387
|
});
|
|
352
388
|
try {
|
|
353
|
-
const result = manager
|
|
389
|
+
const result = checkTool(manager, "my_custom_tool", {});
|
|
354
390
|
expect(result.state).toBe("allow");
|
|
355
391
|
expect(result.source).toBe("tool");
|
|
356
392
|
} finally {
|
|
@@ -363,7 +399,8 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
363
399
|
it("matchedPattern is the session rule pattern for a session match (bash)", () => {
|
|
364
400
|
const manager = makeManager();
|
|
365
401
|
const sessionRules: Ruleset = [sessionAllow("bash", "git *")];
|
|
366
|
-
const result =
|
|
402
|
+
const result = checkTool(
|
|
403
|
+
manager,
|
|
367
404
|
"bash",
|
|
368
405
|
{ command: "git status" },
|
|
369
406
|
undefined,
|
|
@@ -376,7 +413,8 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
376
413
|
it("matchedPattern is the session rule pattern for a session match (skill)", () => {
|
|
377
414
|
const manager = makeManager();
|
|
378
415
|
const sessionRules: Ruleset = [sessionAllow("skill", "librarian")];
|
|
379
|
-
const result =
|
|
416
|
+
const result = checkTool(
|
|
417
|
+
manager,
|
|
380
418
|
"skill",
|
|
381
419
|
{ name: "librarian" },
|
|
382
420
|
undefined,
|
|
@@ -398,7 +436,7 @@ describe("checkPermission — home path expansion in external_directory rules",
|
|
|
398
436
|
external_directory: { "~/trusted/*": "allow" },
|
|
399
437
|
});
|
|
400
438
|
try {
|
|
401
|
-
const result = manager
|
|
439
|
+
const result = checkTool(manager, "external_directory", {
|
|
402
440
|
path: join(homedir(), "trusted/repo"),
|
|
403
441
|
});
|
|
404
442
|
expect(result.state).toBe("allow");
|
|
@@ -415,7 +453,7 @@ describe("checkPermission — home path expansion in external_directory rules",
|
|
|
415
453
|
external_directory: { "$HOME/trusted/*": "allow" },
|
|
416
454
|
});
|
|
417
455
|
try {
|
|
418
|
-
const result = manager
|
|
456
|
+
const result = checkTool(manager, "external_directory", {
|
|
419
457
|
path: join(homedir(), "trusted/repo"),
|
|
420
458
|
});
|
|
421
459
|
expect(result.state).toBe("allow");
|
|
@@ -432,7 +470,7 @@ describe("checkPermission — home path expansion in external_directory rules",
|
|
|
432
470
|
external_directory: { "~/private/*": "deny" },
|
|
433
471
|
});
|
|
434
472
|
try {
|
|
435
|
-
const result = manager
|
|
473
|
+
const result = checkTool(manager, "external_directory", {
|
|
436
474
|
path: join(homedir(), "private/secrets.txt"),
|
|
437
475
|
});
|
|
438
476
|
expect(result.state).toBe("deny");
|
|
@@ -448,7 +486,7 @@ describe("checkPermission — home path expansion in external_directory rules",
|
|
|
448
486
|
external_directory: { "~/trusted/*": "allow" },
|
|
449
487
|
});
|
|
450
488
|
try {
|
|
451
|
-
const result = manager
|
|
489
|
+
const result = checkTool(manager, "external_directory", {
|
|
452
490
|
path: "/tmp/not-home/file",
|
|
453
491
|
});
|
|
454
492
|
// Falls back to the "*": "ask" default — no allow from the ~/trusted/* rule.
|
|
@@ -505,7 +543,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
505
543
|
it("single-scope global: config rule has origin 'global'", () => {
|
|
506
544
|
const { manager, cleanup } = makeManagerWithScopes({ read: "allow" });
|
|
507
545
|
try {
|
|
508
|
-
const result = manager
|
|
546
|
+
const result = checkTool(manager, "read", {});
|
|
509
547
|
expect(result.state).toBe("allow");
|
|
510
548
|
expect(result.origin).toBe("global");
|
|
511
549
|
} finally {
|
|
@@ -518,7 +556,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
518
556
|
bash: { "git *": "allow" },
|
|
519
557
|
});
|
|
520
558
|
try {
|
|
521
|
-
const result = manager
|
|
559
|
+
const result = checkTool(manager, "bash", { command: "git status" });
|
|
522
560
|
expect(result.state).toBe("allow");
|
|
523
561
|
expect(result.origin).toBe("global");
|
|
524
562
|
} finally {
|
|
@@ -532,7 +570,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
532
570
|
{ read: "allow" },
|
|
533
571
|
);
|
|
534
572
|
try {
|
|
535
|
-
const result = manager
|
|
573
|
+
const result = checkTool(manager, "read", {});
|
|
536
574
|
expect(result.state).toBe("allow");
|
|
537
575
|
expect(result.origin).toBe("project");
|
|
538
576
|
} finally {
|
|
@@ -548,13 +586,13 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
548
586
|
{ bash: { "rm *": "deny" } },
|
|
549
587
|
);
|
|
550
588
|
try {
|
|
551
|
-
const gitResult = manager
|
|
589
|
+
const gitResult = checkTool(manager, "bash", {
|
|
552
590
|
command: "git status",
|
|
553
591
|
});
|
|
554
592
|
expect(gitResult.state).toBe("allow");
|
|
555
593
|
expect(gitResult.origin).toBe("global");
|
|
556
594
|
|
|
557
|
-
const rmResult = manager
|
|
595
|
+
const rmResult = checkTool(manager, "bash", {
|
|
558
596
|
command: "rm -rf /",
|
|
559
597
|
});
|
|
560
598
|
expect(rmResult.state).toBe("deny");
|
|
@@ -571,7 +609,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
571
609
|
{ bash: { "git *": "allow" } },
|
|
572
610
|
);
|
|
573
611
|
try {
|
|
574
|
-
const result = manager
|
|
612
|
+
const result = checkTool(manager, "bash", {
|
|
575
613
|
command: "git status",
|
|
576
614
|
});
|
|
577
615
|
expect(result.state).toBe("allow");
|
|
@@ -589,7 +627,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
589
627
|
);
|
|
590
628
|
try {
|
|
591
629
|
// The catch-all "*" now comes from the project scope.
|
|
592
|
-
const result = manager
|
|
630
|
+
const result = checkTool(manager, "bash", {
|
|
593
631
|
command: "anything",
|
|
594
632
|
});
|
|
595
633
|
expect(result.state).toBe("allow");
|
|
@@ -606,7 +644,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
606
644
|
{ read: { "*": "allow" } },
|
|
607
645
|
);
|
|
608
646
|
try {
|
|
609
|
-
const result = manager
|
|
647
|
+
const result = checkTool(manager, "read", {});
|
|
610
648
|
expect(result.state).toBe("allow");
|
|
611
649
|
expect(result.origin).toBe("project");
|
|
612
650
|
} finally {
|
|
@@ -617,7 +655,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
617
655
|
it("no config match: origin is 'builtin' (default layer)", () => {
|
|
618
656
|
// No config — falls back to synthesized default.
|
|
619
657
|
const manager = makeManager();
|
|
620
|
-
const result = manager
|
|
658
|
+
const result = checkTool(manager, "read", {});
|
|
621
659
|
expect(result.state).toBe("ask");
|
|
622
660
|
expect(result.origin).toBe("builtin");
|
|
623
661
|
});
|
|
@@ -633,7 +671,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
633
671
|
origin: "session",
|
|
634
672
|
},
|
|
635
673
|
];
|
|
636
|
-
const result = manager
|
|
674
|
+
const result = checkTool(manager, "read", {}, undefined, sessionRules);
|
|
637
675
|
expect(result.state).toBe("allow");
|
|
638
676
|
expect(result.source).toBe("session");
|
|
639
677
|
expect(result.origin).toBe("session");
|
|
@@ -643,7 +681,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
643
681
|
const { manager, cleanup } = makeManagerWithScopes({ "*": "allow" });
|
|
644
682
|
try {
|
|
645
683
|
// No explicit surface rule — hits the synthesized default derived from "*".
|
|
646
|
-
const result = manager
|
|
684
|
+
const result = checkTool(manager, "read", {});
|
|
647
685
|
expect(result.state).toBe("allow");
|
|
648
686
|
expect(result.origin).toBe("global");
|
|
649
687
|
} finally {
|
|
@@ -657,7 +695,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
657
695
|
{ "*": "allow" },
|
|
658
696
|
);
|
|
659
697
|
try {
|
|
660
|
-
const result = manager
|
|
698
|
+
const result = checkTool(manager, "read", {});
|
|
661
699
|
expect(result.state).toBe("allow");
|
|
662
700
|
expect(result.origin).toBe("project");
|
|
663
701
|
} finally {
|
|
@@ -668,7 +706,7 @@ describe("checkPermission — rule origin provenance", () => {
|
|
|
668
706
|
it("built-in fallback (no * in any config): origin is 'builtin'", () => {
|
|
669
707
|
// Manager with no config file — built-in "ask" default.
|
|
670
708
|
const manager = makeManager();
|
|
671
|
-
const result = manager
|
|
709
|
+
const result = checkTool(manager, "read", {});
|
|
672
710
|
expect(result.state).toBe("ask");
|
|
673
711
|
expect(result.origin).toBe("builtin");
|
|
674
712
|
});
|
|
@@ -734,7 +772,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
734
772
|
describe("universal fallback", () => {
|
|
735
773
|
it("defaults to ask when no config is provided", () => {
|
|
736
774
|
const manager = makeInMemoryManager();
|
|
737
|
-
const result = manager
|
|
775
|
+
const result = checkTool(manager, "read", {});
|
|
738
776
|
expect(result.state).toBe("ask");
|
|
739
777
|
expect(result.origin).toBe("builtin");
|
|
740
778
|
});
|
|
@@ -743,7 +781,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
743
781
|
const manager = makeInMemoryManager({
|
|
744
782
|
global: { permission: { "*": "allow" } },
|
|
745
783
|
});
|
|
746
|
-
const result = manager
|
|
784
|
+
const result = checkTool(manager, "read", {});
|
|
747
785
|
expect(result.state).toBe("allow");
|
|
748
786
|
expect(result.origin).toBe("global");
|
|
749
787
|
});
|
|
@@ -752,7 +790,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
752
790
|
const manager = makeInMemoryManager({
|
|
753
791
|
global: { permission: { "*": "deny" } },
|
|
754
792
|
});
|
|
755
|
-
const result = manager
|
|
793
|
+
const result = checkTool(manager, "write", {});
|
|
756
794
|
expect(result.state).toBe("deny");
|
|
757
795
|
});
|
|
758
796
|
});
|
|
@@ -764,7 +802,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
764
802
|
permission: { "*": "ask", bash: { "git *": "allow" } },
|
|
765
803
|
},
|
|
766
804
|
});
|
|
767
|
-
const result = manager
|
|
805
|
+
const result = checkTool(manager, "bash", {
|
|
768
806
|
command: "git status",
|
|
769
807
|
});
|
|
770
808
|
expect(result.state).toBe("allow");
|
|
@@ -776,7 +814,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
776
814
|
const manager = makeInMemoryManager({
|
|
777
815
|
global: { permission: { "*": "deny", read: "allow" } },
|
|
778
816
|
});
|
|
779
|
-
const result = manager
|
|
817
|
+
const result = checkTool(manager, "read", {});
|
|
780
818
|
expect(result.state).toBe("allow");
|
|
781
819
|
expect(result.source).toBe("tool");
|
|
782
820
|
});
|
|
@@ -787,7 +825,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
787
825
|
permission: { "*": "ask", skill: { librarian: "allow" } },
|
|
788
826
|
},
|
|
789
827
|
});
|
|
790
|
-
const result = manager
|
|
828
|
+
const result = checkTool(manager, "skill", { name: "librarian" });
|
|
791
829
|
expect(result.state).toBe("allow");
|
|
792
830
|
expect(result.source).toBe("skill");
|
|
793
831
|
});
|
|
@@ -801,7 +839,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
801
839
|
},
|
|
802
840
|
["exa"],
|
|
803
841
|
);
|
|
804
|
-
const result = manager
|
|
842
|
+
const result = checkTool(manager, "mcp", {
|
|
805
843
|
tool: "exa:search",
|
|
806
844
|
server: "exa",
|
|
807
845
|
});
|
|
@@ -818,7 +856,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
818
856
|
},
|
|
819
857
|
},
|
|
820
858
|
});
|
|
821
|
-
const result = manager
|
|
859
|
+
const result = checkTool(manager, "external_directory", {
|
|
822
860
|
path: "/trusted/repo",
|
|
823
861
|
});
|
|
824
862
|
expect(result.state).toBe("allow");
|
|
@@ -829,7 +867,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
829
867
|
const manager = makeInMemoryManager({
|
|
830
868
|
global: { permission: { "*": "ask" } },
|
|
831
869
|
});
|
|
832
|
-
const result = manager
|
|
870
|
+
const result = checkTool(manager, "my_custom_tool", {});
|
|
833
871
|
expect(result.state).toBe("ask");
|
|
834
872
|
expect(result.source).toBe("default");
|
|
835
873
|
});
|
|
@@ -841,7 +879,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
841
879
|
global: { permission: { read: "ask" } },
|
|
842
880
|
project: { permission: { read: "allow" } },
|
|
843
881
|
});
|
|
844
|
-
const result = manager
|
|
882
|
+
const result = checkTool(manager, "read", {});
|
|
845
883
|
expect(result.state).toBe("allow");
|
|
846
884
|
expect(result.origin).toBe("project");
|
|
847
885
|
});
|
|
@@ -852,7 +890,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
852
890
|
project: { permission: { read: "allow" } },
|
|
853
891
|
agent: { coder: { permission: { read: "deny" } } },
|
|
854
892
|
});
|
|
855
|
-
const result = manager
|
|
893
|
+
const result = checkTool(manager, "read", {}, "coder");
|
|
856
894
|
expect(result.state).toBe("deny");
|
|
857
895
|
expect(result.origin).toBe("agent");
|
|
858
896
|
});
|
|
@@ -863,7 +901,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
863
901
|
agent: { coder: { permission: { read: "deny" } } },
|
|
864
902
|
projectAgent: { coder: { permission: { read: "allow" } } },
|
|
865
903
|
});
|
|
866
|
-
const result = manager
|
|
904
|
+
const result = checkTool(manager, "read", {}, "coder");
|
|
867
905
|
expect(result.state).toBe("allow");
|
|
868
906
|
expect(result.origin).toBe("project-agent");
|
|
869
907
|
});
|
|
@@ -873,13 +911,13 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
873
911
|
global: { permission: { bash: { "git *": "allow" } } },
|
|
874
912
|
project: { permission: { bash: { "rm *": "deny" } } },
|
|
875
913
|
});
|
|
876
|
-
const gitResult = manager
|
|
914
|
+
const gitResult = checkTool(manager, "bash", {
|
|
877
915
|
command: "git status",
|
|
878
916
|
});
|
|
879
917
|
expect(gitResult.state).toBe("allow");
|
|
880
918
|
expect(gitResult.origin).toBe("global");
|
|
881
919
|
|
|
882
|
-
const rmResult = manager
|
|
920
|
+
const rmResult = checkTool(manager, "bash", {
|
|
883
921
|
command: "rm -rf /",
|
|
884
922
|
});
|
|
885
923
|
expect(rmResult.state).toBe("deny");
|
|
@@ -893,7 +931,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
893
931
|
},
|
|
894
932
|
project: { permission: { bash: "allow" } },
|
|
895
933
|
});
|
|
896
|
-
const result = manager
|
|
934
|
+
const result = checkTool(manager, "bash", { command: "anything" });
|
|
897
935
|
expect(result.state).toBe("allow");
|
|
898
936
|
expect(result.origin).toBe("project");
|
|
899
937
|
});
|
|
@@ -905,12 +943,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
905
943
|
global: { permission: { "*": "deny" } },
|
|
906
944
|
});
|
|
907
945
|
const sessionRules: Ruleset = [sessionAllow("read", "*")];
|
|
908
|
-
const result = manager
|
|
909
|
-
"read",
|
|
910
|
-
{},
|
|
911
|
-
undefined,
|
|
912
|
-
sessionRules,
|
|
913
|
-
);
|
|
946
|
+
const result = checkTool(manager, "read", {}, undefined, sessionRules);
|
|
914
947
|
expect(result.state).toBe("allow");
|
|
915
948
|
expect(result.source).toBe("session");
|
|
916
949
|
});
|
|
@@ -920,7 +953,8 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
920
953
|
global: { permission: { "*": "ask" } },
|
|
921
954
|
});
|
|
922
955
|
const sessionRules: Ruleset = [sessionAllow("bash", "git *")];
|
|
923
|
-
const bashResult =
|
|
956
|
+
const bashResult = checkTool(
|
|
957
|
+
manager,
|
|
924
958
|
"bash",
|
|
925
959
|
{ command: "git status" },
|
|
926
960
|
undefined,
|
|
@@ -928,7 +962,8 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
928
962
|
);
|
|
929
963
|
expect(bashResult.state).toBe("allow");
|
|
930
964
|
|
|
931
|
-
const readResult =
|
|
965
|
+
const readResult = checkTool(
|
|
966
|
+
manager,
|
|
932
967
|
"read",
|
|
933
968
|
{},
|
|
934
969
|
undefined,
|
|
@@ -944,7 +979,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
944
979
|
global: { permission: { "*": "ask" } },
|
|
945
980
|
project: { permission: { "*": "allow" } },
|
|
946
981
|
});
|
|
947
|
-
const result = manager
|
|
982
|
+
const result = checkTool(manager, "read", {});
|
|
948
983
|
expect(result.state).toBe("allow");
|
|
949
984
|
expect(result.origin).toBe("project");
|
|
950
985
|
});
|
|
@@ -952,12 +987,7 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
|
|
|
952
987
|
it("session origin is 'session'", () => {
|
|
953
988
|
const manager = makeInMemoryManager();
|
|
954
989
|
const sessionRules: Ruleset = [sessionAllow("read", "*")];
|
|
955
|
-
const result = manager
|
|
956
|
-
"read",
|
|
957
|
-
{},
|
|
958
|
-
undefined,
|
|
959
|
-
sessionRules,
|
|
960
|
-
);
|
|
990
|
+
const result = checkTool(manager, "read", {}, undefined, sessionRules);
|
|
961
991
|
expect(result.origin).toBe("session");
|
|
962
992
|
});
|
|
963
993
|
});
|
|
@@ -1005,7 +1035,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1005
1035
|
read: { "*": "allow", "*.env": "deny" },
|
|
1006
1036
|
});
|
|
1007
1037
|
try {
|
|
1008
|
-
const result = manager
|
|
1038
|
+
const result = checkTool(manager, "read", { path: ".env" });
|
|
1009
1039
|
expect(result.state).toBe("deny");
|
|
1010
1040
|
expect(result.matchedPattern).toBe("*.env");
|
|
1011
1041
|
} finally {
|
|
@@ -1018,7 +1048,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1018
1048
|
read: { "*": "allow", "*.env": "deny" },
|
|
1019
1049
|
});
|
|
1020
1050
|
try {
|
|
1021
|
-
const result = manager
|
|
1051
|
+
const result = checkTool(manager, "read", {
|
|
1022
1052
|
path: "src/main.ts",
|
|
1023
1053
|
});
|
|
1024
1054
|
expect(result.state).toBe("allow");
|
|
@@ -1032,7 +1062,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1032
1062
|
write: { "*": "deny", "src/*": "allow" },
|
|
1033
1063
|
});
|
|
1034
1064
|
try {
|
|
1035
|
-
const result = manager
|
|
1065
|
+
const result = checkTool(manager, "write", {
|
|
1036
1066
|
path: "src/main.ts",
|
|
1037
1067
|
});
|
|
1038
1068
|
expect(result.state).toBe("allow");
|
|
@@ -1047,7 +1077,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1047
1077
|
write: { "*": "deny", "src/*": "allow" },
|
|
1048
1078
|
});
|
|
1049
1079
|
try {
|
|
1050
|
-
const result = manager
|
|
1080
|
+
const result = checkTool(manager, "write", {
|
|
1051
1081
|
path: "vendor/lib.ts",
|
|
1052
1082
|
});
|
|
1053
1083
|
expect(result.state).toBe("deny");
|
|
@@ -1061,7 +1091,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1061
1091
|
read: "allow",
|
|
1062
1092
|
});
|
|
1063
1093
|
try {
|
|
1064
|
-
const result = manager
|
|
1094
|
+
const result = checkTool(manager, "read", { path: ".env" });
|
|
1065
1095
|
expect(result.state).toBe("allow");
|
|
1066
1096
|
} finally {
|
|
1067
1097
|
cleanup();
|
|
@@ -1073,7 +1103,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1073
1103
|
read: "deny",
|
|
1074
1104
|
});
|
|
1075
1105
|
try {
|
|
1076
|
-
const result = manager
|
|
1106
|
+
const result = checkTool(manager, "read", {
|
|
1077
1107
|
path: "src/main.ts",
|
|
1078
1108
|
});
|
|
1079
1109
|
expect(result.state).toBe("deny");
|
|
@@ -1088,7 +1118,8 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1088
1118
|
});
|
|
1089
1119
|
try {
|
|
1090
1120
|
const sessionRules: Ruleset = [sessionAllow("read", ".env")];
|
|
1091
|
-
const result =
|
|
1121
|
+
const result = checkTool(
|
|
1122
|
+
manager,
|
|
1092
1123
|
"read",
|
|
1093
1124
|
{ path: ".env" },
|
|
1094
1125
|
undefined,
|
|
@@ -1106,7 +1137,7 @@ describe("checkPermission — per-tool path patterns", () => {
|
|
|
1106
1137
|
read: { "*": "allow", "*.env": "deny" },
|
|
1107
1138
|
});
|
|
1108
1139
|
try {
|
|
1109
|
-
const result = manager
|
|
1140
|
+
const result = checkTool(manager, "read", {});
|
|
1110
1141
|
expect(result.state).toBe("allow");
|
|
1111
1142
|
} finally {
|
|
1112
1143
|
cleanup();
|
|
@@ -1137,7 +1168,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1137
1168
|
read: "allow",
|
|
1138
1169
|
});
|
|
1139
1170
|
try {
|
|
1140
|
-
const result = manager
|
|
1171
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1141
1172
|
expect(result.state).toBe("deny");
|
|
1142
1173
|
expect(result.matchedPattern).toBe("*.env");
|
|
1143
1174
|
} finally {
|
|
@@ -1151,7 +1182,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1151
1182
|
read: "allow",
|
|
1152
1183
|
});
|
|
1153
1184
|
try {
|
|
1154
|
-
const result = manager
|
|
1185
|
+
const result = checkTool(manager, "path", { path: "README.md" });
|
|
1155
1186
|
expect(result.state).toBe("allow");
|
|
1156
1187
|
} finally {
|
|
1157
1188
|
cleanup();
|
|
@@ -1165,12 +1196,12 @@ describe("cross-cutting path surface", () => {
|
|
|
1165
1196
|
});
|
|
1166
1197
|
try {
|
|
1167
1198
|
// path surface allows, per-tool denies
|
|
1168
|
-
const readResult = manager
|
|
1199
|
+
const readResult = checkTool(manager, "read", {
|
|
1169
1200
|
path: "data.secret",
|
|
1170
1201
|
});
|
|
1171
1202
|
expect(readResult.state).toBe("deny");
|
|
1172
1203
|
// path surface also allows
|
|
1173
|
-
const pathResult = manager
|
|
1204
|
+
const pathResult = checkTool(manager, "path", {
|
|
1174
1205
|
path: "data.secret",
|
|
1175
1206
|
});
|
|
1176
1207
|
expect(pathResult.state).toBe("allow");
|
|
@@ -1197,7 +1228,8 @@ describe("cross-cutting path surface", () => {
|
|
|
1197
1228
|
});
|
|
1198
1229
|
try {
|
|
1199
1230
|
const sessionRules: Ruleset = [sessionAllow("path", "/project/.env")];
|
|
1200
|
-
const result =
|
|
1231
|
+
const result = checkTool(
|
|
1232
|
+
manager,
|
|
1201
1233
|
"path",
|
|
1202
1234
|
{ path: "/project/.env" },
|
|
1203
1235
|
undefined,
|
|
@@ -1216,7 +1248,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1216
1248
|
});
|
|
1217
1249
|
try {
|
|
1218
1250
|
// path surface falls through to universal default
|
|
1219
|
-
const result = manager
|
|
1251
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1220
1252
|
expect(result.state).toBe("ask");
|
|
1221
1253
|
} finally {
|
|
1222
1254
|
cleanup();
|
|
@@ -1232,14 +1264,14 @@ describe("cross-cutting path surface", () => {
|
|
|
1232
1264
|
try {
|
|
1233
1265
|
// No explicit "path" key → matchedPattern must be undefined so the
|
|
1234
1266
|
// path gate skips (describePathGate returns null).
|
|
1235
|
-
const result = manager
|
|
1267
|
+
const result = checkTool(manager, "path", {
|
|
1236
1268
|
path: "src/main.ts",
|
|
1237
1269
|
});
|
|
1238
1270
|
expect(result.state).toBe("ask");
|
|
1239
1271
|
expect(result.matchedPattern).toBeUndefined();
|
|
1240
1272
|
|
|
1241
1273
|
// Meanwhile the tool-level check should allow read.
|
|
1242
|
-
const readResult = manager
|
|
1274
|
+
const readResult = checkTool(manager, "read", {
|
|
1243
1275
|
path: "src/main.ts",
|
|
1244
1276
|
});
|
|
1245
1277
|
expect(readResult.state).toBe("allow");
|
|
@@ -1256,7 +1288,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1256
1288
|
bash: { "npm *": { action: "deny", reason: "Use pnpm instead" } },
|
|
1257
1289
|
});
|
|
1258
1290
|
try {
|
|
1259
|
-
const result = manager
|
|
1291
|
+
const result = checkTool(manager, "bash", {
|
|
1260
1292
|
command: "npm install",
|
|
1261
1293
|
});
|
|
1262
1294
|
expect(result.state).toBe("deny");
|
|
@@ -1272,7 +1304,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1272
1304
|
bash: { "rm -rf *": "deny" },
|
|
1273
1305
|
});
|
|
1274
1306
|
try {
|
|
1275
|
-
const result = manager
|
|
1307
|
+
const result = checkTool(manager, "bash", { command: "rm -rf /" });
|
|
1276
1308
|
expect(result.state).toBe("deny");
|
|
1277
1309
|
expect(result.reason).toBeUndefined();
|
|
1278
1310
|
} finally {
|
|
@@ -1290,7 +1322,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1290
1322
|
},
|
|
1291
1323
|
});
|
|
1292
1324
|
try {
|
|
1293
|
-
const result = manager
|
|
1325
|
+
const result = checkTool(manager, "read", { path: ".env" });
|
|
1294
1326
|
expect(result.state).toBe("deny");
|
|
1295
1327
|
expect(result.reason).toBe("Environment files contain secrets");
|
|
1296
1328
|
expect(result.matchedPattern).toBe("*.env");
|
|
@@ -1304,7 +1336,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1304
1336
|
bash: { "npm *": { action: "deny", reason: 42 } },
|
|
1305
1337
|
});
|
|
1306
1338
|
try {
|
|
1307
|
-
const result = manager
|
|
1339
|
+
const result = checkTool(manager, "bash", {
|
|
1308
1340
|
command: "npm install",
|
|
1309
1341
|
});
|
|
1310
1342
|
expect(result.state).toBe("ask");
|
|
@@ -1322,7 +1354,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1322
1354
|
path: { "*.env": "deny", "*": "allow" },
|
|
1323
1355
|
});
|
|
1324
1356
|
try {
|
|
1325
|
-
const result = manager
|
|
1357
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1326
1358
|
// "*" is last and matches .env → allow (the deny is shadowed)
|
|
1327
1359
|
expect(result.state).toBe("allow");
|
|
1328
1360
|
} finally {
|
|
@@ -1336,7 +1368,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1336
1368
|
path: { "*": "allow", "*.env": "deny" },
|
|
1337
1369
|
});
|
|
1338
1370
|
try {
|
|
1339
|
-
const result = manager
|
|
1371
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1340
1372
|
expect(result.state).toBe("deny");
|
|
1341
1373
|
} finally {
|
|
1342
1374
|
cleanup();
|
|
@@ -1355,22 +1387,20 @@ describe("cross-cutting path surface", () => {
|
|
|
1355
1387
|
},
|
|
1356
1388
|
});
|
|
1357
1389
|
try {
|
|
1358
|
-
expect(manager
|
|
1390
|
+
expect(checkTool(manager, "path", { path: ".env" }).state).toBe("deny");
|
|
1391
|
+
expect(checkTool(manager, "path", { path: ".env.local" }).state).toBe(
|
|
1359
1392
|
"deny",
|
|
1360
1393
|
);
|
|
1361
1394
|
expect(
|
|
1362
|
-
manager
|
|
1363
|
-
).toBe("deny");
|
|
1364
|
-
expect(
|
|
1365
|
-
manager.checkPermission("path", { path: ".env.production" }).state,
|
|
1395
|
+
checkTool(manager, "path", { path: ".env.production" }).state,
|
|
1366
1396
|
).toBe("deny");
|
|
1367
|
-
expect(manager
|
|
1397
|
+
expect(checkTool(manager, "path", { path: "src/.env" }).state).toBe(
|
|
1368
1398
|
"deny",
|
|
1369
1399
|
);
|
|
1370
|
-
expect(
|
|
1371
|
-
|
|
1372
|
-
)
|
|
1373
|
-
expect(manager
|
|
1400
|
+
expect(checkTool(manager, "path", { path: ".env.example" }).state).toBe(
|
|
1401
|
+
"allow",
|
|
1402
|
+
);
|
|
1403
|
+
expect(checkTool(manager, "path", { path: "README.md" }).state).toBe(
|
|
1374
1404
|
"allow",
|
|
1375
1405
|
);
|
|
1376
1406
|
} finally {
|
|
@@ -1385,7 +1415,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1385
1415
|
"*": "allow",
|
|
1386
1416
|
});
|
|
1387
1417
|
try {
|
|
1388
|
-
const result = manager
|
|
1418
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1389
1419
|
expect(result.state).toBe("allow");
|
|
1390
1420
|
} finally {
|
|
1391
1421
|
cleanup();
|
|
@@ -1397,7 +1427,7 @@ describe("cross-cutting path surface", () => {
|
|
|
1397
1427
|
"*": "deny",
|
|
1398
1428
|
});
|
|
1399
1429
|
try {
|
|
1400
|
-
const result = manager
|
|
1430
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1401
1431
|
expect(result.state).toBe("deny");
|
|
1402
1432
|
} finally {
|
|
1403
1433
|
cleanup();
|
|
@@ -1413,11 +1443,11 @@ describe("cross-cutting path surface", () => {
|
|
|
1413
1443
|
});
|
|
1414
1444
|
try {
|
|
1415
1445
|
// path gate passes (allow), but tool gate denies
|
|
1416
|
-
const pathResult = manager
|
|
1446
|
+
const pathResult = checkTool(manager, "path", {
|
|
1417
1447
|
path: "secret.txt",
|
|
1418
1448
|
});
|
|
1419
1449
|
expect(pathResult.state).toBe("allow");
|
|
1420
|
-
const readResult = manager
|
|
1450
|
+
const readResult = checkTool(manager, "read", {
|
|
1421
1451
|
path: "secret.txt",
|
|
1422
1452
|
});
|
|
1423
1453
|
expect(readResult.state).toBe("deny");
|
|
@@ -1437,7 +1467,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
|
|
|
1437
1467
|
path: { "*": "allow", "~/.ssh/*": "deny" },
|
|
1438
1468
|
});
|
|
1439
1469
|
try {
|
|
1440
|
-
const result = manager
|
|
1470
|
+
const result = checkTool(manager, "path", {
|
|
1441
1471
|
path: "~/.ssh/config",
|
|
1442
1472
|
});
|
|
1443
1473
|
expect(result.state).toBe("deny");
|
|
@@ -1452,7 +1482,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
|
|
|
1452
1482
|
path: { "*": "allow", "~/.ssh/*": "deny" },
|
|
1453
1483
|
});
|
|
1454
1484
|
try {
|
|
1455
|
-
const result = manager
|
|
1485
|
+
const result = checkTool(manager, "path", {
|
|
1456
1486
|
path: `${homedir()}/.ssh/config`,
|
|
1457
1487
|
});
|
|
1458
1488
|
expect(result.state).toBe("deny");
|
|
@@ -1467,7 +1497,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
|
|
|
1467
1497
|
path: { "*": "allow", "$HOME/.ssh/*": "deny" },
|
|
1468
1498
|
});
|
|
1469
1499
|
try {
|
|
1470
|
-
const result = manager
|
|
1500
|
+
const result = checkTool(manager, "path", {
|
|
1471
1501
|
path: "$HOME/.ssh/config",
|
|
1472
1502
|
});
|
|
1473
1503
|
expect(result.state).toBe("deny");
|
|
@@ -1482,7 +1512,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
|
|
|
1482
1512
|
path: { "*": "allow", "~/.ssh/*": "deny" },
|
|
1483
1513
|
});
|
|
1484
1514
|
try {
|
|
1485
|
-
const result = manager
|
|
1515
|
+
const result = checkTool(manager, "path", {
|
|
1486
1516
|
path: `${homedir()}/.ssh/config`,
|
|
1487
1517
|
});
|
|
1488
1518
|
expect(result.state).toBe("deny");
|
|
@@ -1496,7 +1526,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
|
|
|
1496
1526
|
path: { "*": "allow", "*.env": "deny" },
|
|
1497
1527
|
});
|
|
1498
1528
|
try {
|
|
1499
|
-
const result = manager
|
|
1529
|
+
const result = checkTool(manager, "path", { path: ".env" });
|
|
1500
1530
|
expect(result.state).toBe("deny");
|
|
1501
1531
|
expect(result.matchedPattern).toBe("*.env");
|
|
1502
1532
|
} finally {
|
|
@@ -1510,7 +1540,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
|
|
|
1510
1540
|
read: { "*": "allow", "~/.ssh/*": "deny" },
|
|
1511
1541
|
});
|
|
1512
1542
|
try {
|
|
1513
|
-
const result = manager
|
|
1543
|
+
const result = checkTool(manager, "read", {
|
|
1514
1544
|
path: "~/.ssh/config",
|
|
1515
1545
|
});
|
|
1516
1546
|
expect(result.state).toBe("deny");
|
|
@@ -1583,7 +1613,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1583
1613
|
});
|
|
1584
1614
|
const scoped: ScopedPermissionManager = manager;
|
|
1585
1615
|
expect(typeof scoped.configureForCwd).toBe("function");
|
|
1586
|
-
expect(typeof scoped.
|
|
1616
|
+
expect(typeof scoped.check).toBe("function");
|
|
1587
1617
|
expect(typeof scoped.getToolPermission).toBe("function");
|
|
1588
1618
|
expect(typeof scoped.getConfigIssues).toBe("function");
|
|
1589
1619
|
});
|
|
@@ -1594,7 +1624,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1594
1624
|
});
|
|
1595
1625
|
try {
|
|
1596
1626
|
const manager = new PermissionManager({ agentDir });
|
|
1597
|
-
const result = manager
|
|
1627
|
+
const result = checkTool(manager, "read", { path: "foo.txt" });
|
|
1598
1628
|
expect(result.state).toBe("deny");
|
|
1599
1629
|
} finally {
|
|
1600
1630
|
cleanup();
|
|
@@ -1609,14 +1639,14 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1609
1639
|
try {
|
|
1610
1640
|
const manager = new PermissionManager({ agentDir });
|
|
1611
1641
|
// Before configureForCwd: global policy applies
|
|
1612
|
-
expect(manager
|
|
1642
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1613
1643
|
"deny",
|
|
1614
1644
|
);
|
|
1615
1645
|
|
|
1616
1646
|
manager.configureForCwd(cwd);
|
|
1617
1647
|
|
|
1618
1648
|
// After configureForCwd: project override applies (last-match-wins)
|
|
1619
|
-
expect(manager
|
|
1649
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1620
1650
|
"allow",
|
|
1621
1651
|
);
|
|
1622
1652
|
} finally {
|
|
@@ -1632,14 +1662,14 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1632
1662
|
try {
|
|
1633
1663
|
const manager = new PermissionManager({ agentDir });
|
|
1634
1664
|
manager.configureForCwd(cwd);
|
|
1635
|
-
expect(manager
|
|
1665
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1636
1666
|
"allow",
|
|
1637
1667
|
);
|
|
1638
1668
|
|
|
1639
1669
|
manager.configureForCwd(undefined);
|
|
1640
1670
|
|
|
1641
1671
|
// After reverting: global policy applies again
|
|
1642
|
-
expect(manager
|
|
1672
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1643
1673
|
"deny",
|
|
1644
1674
|
);
|
|
1645
1675
|
} finally {
|
|
@@ -1654,7 +1684,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1654
1684
|
try {
|
|
1655
1685
|
const manager = new PermissionManager({ agentDir });
|
|
1656
1686
|
// Warm the cache
|
|
1657
|
-
expect(manager
|
|
1687
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1658
1688
|
"allow",
|
|
1659
1689
|
);
|
|
1660
1690
|
// Update global config on disk to deny read
|
|
@@ -1665,7 +1695,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1665
1695
|
// configureForCwd clears cache + rebuilds loader
|
|
1666
1696
|
manager.configureForCwd(undefined);
|
|
1667
1697
|
// Should pick up the changed global config
|
|
1668
|
-
expect(manager
|
|
1698
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1669
1699
|
"deny",
|
|
1670
1700
|
);
|
|
1671
1701
|
} finally {
|
|
@@ -1708,12 +1738,12 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
|
1708
1738
|
manager.configureForCwd(cwd);
|
|
1709
1739
|
|
|
1710
1740
|
// Without an agent name: global allow applies.
|
|
1711
|
-
expect(manager
|
|
1741
|
+
expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
|
|
1712
1742
|
"allow",
|
|
1713
1743
|
);
|
|
1714
1744
|
// With the "coder" agent: project-agent deny overrides global allow.
|
|
1715
1745
|
expect(
|
|
1716
|
-
manager
|
|
1746
|
+
checkTool(manager, "read", { path: "foo.txt" }, "coder").state,
|
|
1717
1747
|
).toBe("deny");
|
|
1718
1748
|
} finally {
|
|
1719
1749
|
cleanup();
|
|
@@ -1742,13 +1772,13 @@ test("Project-level config overrides base bash patterns", () => {
|
|
|
1742
1772
|
);
|
|
1743
1773
|
|
|
1744
1774
|
try {
|
|
1745
|
-
const allowed = manager
|
|
1775
|
+
const allowed = checkTool(manager, "bash", {
|
|
1746
1776
|
command: "rm -rf build",
|
|
1747
1777
|
});
|
|
1748
1778
|
expect(allowed.state).toBe("allow");
|
|
1749
1779
|
expect(allowed.matchedPattern).toBe("rm -rf build");
|
|
1750
1780
|
|
|
1751
|
-
const denied = manager
|
|
1781
|
+
const denied = checkTool(manager, "bash", {
|
|
1752
1782
|
command: "rm -rf node_modules",
|
|
1753
1783
|
});
|
|
1754
1784
|
expect(denied.state).toBe("deny");
|
|
@@ -1780,7 +1810,8 @@ permission:
|
|
|
1780
1810
|
);
|
|
1781
1811
|
|
|
1782
1812
|
try {
|
|
1783
|
-
const allowed =
|
|
1813
|
+
const allowed = checkTool(
|
|
1814
|
+
manager,
|
|
1784
1815
|
"bash",
|
|
1785
1816
|
{ command: "git log --oneline" },
|
|
1786
1817
|
"reviewer",
|
|
@@ -1788,7 +1819,8 @@ permission:
|
|
|
1788
1819
|
expect(allowed.state).toBe("allow");
|
|
1789
1820
|
expect(allowed.matchedPattern).toBe("git log *");
|
|
1790
1821
|
|
|
1791
|
-
const denied =
|
|
1822
|
+
const denied = checkTool(
|
|
1823
|
+
manager,
|
|
1792
1824
|
"bash",
|
|
1793
1825
|
{ command: "git status" },
|
|
1794
1826
|
"reviewer",
|
|
@@ -1826,7 +1858,7 @@ permission:
|
|
|
1826
1858
|
);
|
|
1827
1859
|
|
|
1828
1860
|
try {
|
|
1829
|
-
const result = manager
|
|
1861
|
+
const result = checkTool(manager, "read", {}, "reviewer");
|
|
1830
1862
|
expect(result.state).toBe("allow");
|
|
1831
1863
|
expect(result.source).toBe("tool");
|
|
1832
1864
|
} finally {
|
|
@@ -1863,7 +1895,8 @@ permission:
|
|
|
1863
1895
|
);
|
|
1864
1896
|
|
|
1865
1897
|
try {
|
|
1866
|
-
const reviewerResult =
|
|
1898
|
+
const reviewerResult = checkTool(
|
|
1899
|
+
manager,
|
|
1867
1900
|
"custom_extension_tool",
|
|
1868
1901
|
{},
|
|
1869
1902
|
"reviewer",
|
|
@@ -1871,7 +1904,7 @@ permission:
|
|
|
1871
1904
|
expect(reviewerResult.state).toBe("deny");
|
|
1872
1905
|
expect(reviewerResult.source).toBe("default");
|
|
1873
1906
|
|
|
1874
|
-
const globalResult = manager
|
|
1907
|
+
const globalResult = checkTool(manager, "custom_extension_tool", {});
|
|
1875
1908
|
expect(globalResult.state).toBe("allow");
|
|
1876
1909
|
expect(globalResult.source).toBe("default");
|
|
1877
1910
|
} finally {
|
|
@@ -1898,11 +1931,11 @@ permission:
|
|
|
1898
1931
|
);
|
|
1899
1932
|
|
|
1900
1933
|
try {
|
|
1901
|
-
const agentResult = manager
|
|
1934
|
+
const agentResult = checkTool(manager, "read", {}, "reviewer");
|
|
1902
1935
|
expect(agentResult.state).toBe("deny");
|
|
1903
1936
|
expect(agentResult.source).toBe("tool");
|
|
1904
1937
|
|
|
1905
|
-
const globalResult = manager
|
|
1938
|
+
const globalResult = checkTool(manager, "read", {});
|
|
1906
1939
|
expect(globalResult.state).toBe("allow");
|
|
1907
1940
|
expect(globalResult.source).toBe("tool");
|
|
1908
1941
|
} finally {
|
|
@@ -1920,11 +1953,11 @@ test("PermissionManager canonical built-in permission checking", () => {
|
|
|
1920
1953
|
});
|
|
1921
1954
|
|
|
1922
1955
|
try {
|
|
1923
|
-
const readResult = manager
|
|
1956
|
+
const readResult = checkTool(manager, "read", {});
|
|
1924
1957
|
expect(readResult.state).toBe("allow");
|
|
1925
1958
|
expect(readResult.source).toBe("tool");
|
|
1926
1959
|
|
|
1927
|
-
const writeResult = manager
|
|
1960
|
+
const writeResult = checkTool(manager, "write", {});
|
|
1928
1961
|
expect(writeResult.state).toBe("deny");
|
|
1929
1962
|
expect(writeResult.source).toBe("tool");
|
|
1930
1963
|
} finally {
|
|
@@ -1946,7 +1979,7 @@ test("multiline bash command resolves to allow via universal fallback", () => {
|
|
|
1946
1979
|
try {
|
|
1947
1980
|
const command =
|
|
1948
1981
|
"node -e \"\nimport('x').then(() => {\n console.log('done');\n});\n\"";
|
|
1949
|
-
const result = manager
|
|
1982
|
+
const result = checkTool(manager, "bash", { command });
|
|
1950
1983
|
expect(result.state).toBe("allow");
|
|
1951
1984
|
} finally {
|
|
1952
1985
|
cleanup();
|
|
@@ -1964,14 +1997,14 @@ test("Bash specific deny patterns override catch-all within the same config", ()
|
|
|
1964
1997
|
});
|
|
1965
1998
|
|
|
1966
1999
|
try {
|
|
1967
|
-
const denied = manager
|
|
2000
|
+
const denied = checkTool(manager, "bash", {
|
|
1968
2001
|
command: "rm -rf build",
|
|
1969
2002
|
});
|
|
1970
2003
|
expect(denied.state).toBe("deny");
|
|
1971
2004
|
expect(denied.source).toBe("bash");
|
|
1972
2005
|
expect(denied.matchedPattern).toBe("rm -rf *");
|
|
1973
2006
|
|
|
1974
|
-
const allowed = manager
|
|
2007
|
+
const allowed = checkTool(manager, "bash", { command: "echo hello" });
|
|
1975
2008
|
expect(allowed.state).toBe("allow");
|
|
1976
2009
|
expect(allowed.source).toBe("bash");
|
|
1977
2010
|
expect(allowed.matchedPattern).toBe("*");
|
|
@@ -1989,7 +2022,7 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
|
|
|
1989
2022
|
});
|
|
1990
2023
|
|
|
1991
2024
|
try {
|
|
1992
|
-
const queryDocs = manager
|
|
2025
|
+
const queryDocs = checkTool(manager, "mcp", {
|
|
1993
2026
|
tool: "research:query-docs",
|
|
1994
2027
|
});
|
|
1995
2028
|
expect(queryDocs.state).toBe("allow");
|
|
@@ -1997,14 +2030,14 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
|
|
|
1997
2030
|
expect(queryDocs.matchedPattern).toBe("research_query-*");
|
|
1998
2031
|
expect(queryDocs.target).toBe("research_query-docs");
|
|
1999
2032
|
|
|
2000
|
-
const resolve2 = manager
|
|
2033
|
+
const resolve2 = checkTool(manager, "mcp", {
|
|
2001
2034
|
tool: "research:resolve-context",
|
|
2002
2035
|
});
|
|
2003
2036
|
expect(resolve2.state).toBe("ask");
|
|
2004
2037
|
expect(resolve2.matchedPattern).toBe("research_*");
|
|
2005
2038
|
expect(resolve2.target).toBe("research_resolve-context");
|
|
2006
2039
|
|
|
2007
|
-
const unknown = manager
|
|
2040
|
+
const unknown = checkTool(manager, "mcp", {
|
|
2008
2041
|
tool: "search:provider",
|
|
2009
2042
|
});
|
|
2010
2043
|
expect(unknown.state).toBe("deny");
|
|
@@ -2025,13 +2058,13 @@ test("Arbitrary extension tools use exact-name tool permissions instead of MCP f
|
|
|
2025
2058
|
});
|
|
2026
2059
|
|
|
2027
2060
|
try {
|
|
2028
|
-
const allowed = manager
|
|
2061
|
+
const allowed = checkTool(manager, "third_party_tool", {});
|
|
2029
2062
|
expect(allowed.state).toBe("allow");
|
|
2030
2063
|
expect(allowed.source).toBe("tool");
|
|
2031
2064
|
|
|
2032
2065
|
// another_extension_tool has no explicit rule — falls through to the
|
|
2033
2066
|
// universal default (permission["*"] = "deny") with source "default".
|
|
2034
|
-
const fallback = manager
|
|
2067
|
+
const fallback = checkTool(manager, "another_extension_tool", {});
|
|
2035
2068
|
expect(fallback.state).toBe("deny");
|
|
2036
2069
|
expect(fallback.source).toBe("default");
|
|
2037
2070
|
} finally {
|
|
@@ -2052,20 +2085,20 @@ test("Skill permission matching", () => {
|
|
|
2052
2085
|
});
|
|
2053
2086
|
|
|
2054
2087
|
try {
|
|
2055
|
-
const allowed = manager
|
|
2088
|
+
const allowed = checkTool(manager, "skill", {
|
|
2056
2089
|
name: "requesting-code-review",
|
|
2057
2090
|
});
|
|
2058
2091
|
expect(allowed.state).toBe("allow");
|
|
2059
2092
|
expect(allowed.matchedPattern).toBe("requesting-code-review");
|
|
2060
2093
|
expect(allowed.source).toBe("skill");
|
|
2061
2094
|
|
|
2062
|
-
const denied = manager
|
|
2095
|
+
const denied = checkTool(manager, "skill", {
|
|
2063
2096
|
name: "web-design-guidelines",
|
|
2064
2097
|
});
|
|
2065
2098
|
expect(denied.state).toBe("deny");
|
|
2066
2099
|
expect(denied.matchedPattern).toBe("web-*");
|
|
2067
2100
|
|
|
2068
|
-
const fallback = manager
|
|
2101
|
+
const fallback = checkTool(manager, "skill", {
|
|
2069
2102
|
name: "unknown-skill",
|
|
2070
2103
|
});
|
|
2071
2104
|
expect(fallback.state).toBe("ask");
|
|
@@ -2088,7 +2121,7 @@ test("MCP proxy tool infers server-prefixed aliases from configured server names
|
|
|
2088
2121
|
);
|
|
2089
2122
|
|
|
2090
2123
|
try {
|
|
2091
|
-
const result = manager
|
|
2124
|
+
const result = checkTool(manager, "mcp", {
|
|
2092
2125
|
tool: "get_code_context_exa",
|
|
2093
2126
|
});
|
|
2094
2127
|
expect(result.state).toBe("allow");
|
|
@@ -2131,7 +2164,7 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
2131
2164
|
});
|
|
2132
2165
|
|
|
2133
2166
|
try {
|
|
2134
|
-
const result = manager
|
|
2167
|
+
const result = checkTool(manager, "mcp", {
|
|
2135
2168
|
tool: "some_tool_legacy-server",
|
|
2136
2169
|
});
|
|
2137
2170
|
expect(result.state).toBe("ask");
|
|
@@ -2153,7 +2186,7 @@ test("MCP describe mode normalizes qualified tool names without duplicating serv
|
|
|
2153
2186
|
);
|
|
2154
2187
|
|
|
2155
2188
|
try {
|
|
2156
|
-
const result = manager
|
|
2189
|
+
const result = checkTool(manager, "mcp", {
|
|
2157
2190
|
describe: "exa:web_search_exa",
|
|
2158
2191
|
server: "exa",
|
|
2159
2192
|
});
|
|
@@ -2172,11 +2205,11 @@ test("Canonical tools map directly without legacy aliases", () => {
|
|
|
2172
2205
|
});
|
|
2173
2206
|
|
|
2174
2207
|
try {
|
|
2175
|
-
const findResult = manager
|
|
2208
|
+
const findResult = checkTool(manager, "find", {});
|
|
2176
2209
|
expect(findResult.state).toBe("allow");
|
|
2177
2210
|
expect(findResult.source).toBe("tool");
|
|
2178
2211
|
|
|
2179
|
-
const lsResult = manager
|
|
2212
|
+
const lsResult = checkTool(manager, "ls", {});
|
|
2180
2213
|
expect(lsResult.state).toBe("deny");
|
|
2181
2214
|
expect(lsResult.source).toBe("tool");
|
|
2182
2215
|
} finally {
|
|
@@ -2200,7 +2233,8 @@ permission:
|
|
|
2200
2233
|
);
|
|
2201
2234
|
|
|
2202
2235
|
try {
|
|
2203
|
-
const result =
|
|
2236
|
+
const result = checkTool(
|
|
2237
|
+
manager,
|
|
2204
2238
|
"mcp",
|
|
2205
2239
|
{ tool: "exa:web_search_exa" },
|
|
2206
2240
|
"reviewer",
|
|
@@ -2232,7 +2266,8 @@ permission:
|
|
|
2232
2266
|
);
|
|
2233
2267
|
|
|
2234
2268
|
try {
|
|
2235
|
-
const result =
|
|
2269
|
+
const result = checkTool(
|
|
2270
|
+
manager,
|
|
2236
2271
|
"mcp",
|
|
2237
2272
|
{ tool: "web_search_exa" },
|
|
2238
2273
|
"reviewer",
|
|
@@ -2265,7 +2300,8 @@ permission:
|
|
|
2265
2300
|
);
|
|
2266
2301
|
|
|
2267
2302
|
try {
|
|
2268
|
-
const allowed =
|
|
2303
|
+
const allowed = checkTool(
|
|
2304
|
+
manager,
|
|
2269
2305
|
"mcp",
|
|
2270
2306
|
{ tool: "web_search_exa" },
|
|
2271
2307
|
"reviewer",
|
|
@@ -2275,7 +2311,8 @@ permission:
|
|
|
2275
2311
|
expect(allowed.matchedPattern).toBe("exa_web_search_exa");
|
|
2276
2312
|
expect(allowed.target).toBe("exa_web_search_exa");
|
|
2277
2313
|
|
|
2278
|
-
const fallback =
|
|
2314
|
+
const fallback = checkTool(
|
|
2315
|
+
manager,
|
|
2279
2316
|
"mcp",
|
|
2280
2317
|
{ tool: "other_exa" },
|
|
2281
2318
|
"reviewer",
|
|
@@ -2304,11 +2341,12 @@ permission:
|
|
|
2304
2341
|
);
|
|
2305
2342
|
|
|
2306
2343
|
try {
|
|
2307
|
-
const readResult = manager
|
|
2344
|
+
const readResult = checkTool(manager, "read", {}, "reviewer");
|
|
2308
2345
|
expect(readResult.state).toBe("deny");
|
|
2309
2346
|
expect(readResult.source).toBe("tool");
|
|
2310
2347
|
|
|
2311
|
-
const mcpResult =
|
|
2348
|
+
const mcpResult = checkTool(
|
|
2349
|
+
manager,
|
|
2312
2350
|
"mcp",
|
|
2313
2351
|
{ tool: "exa:web_search_exa" },
|
|
2314
2352
|
"reviewer",
|
|
@@ -2337,11 +2375,11 @@ permission:
|
|
|
2337
2375
|
);
|
|
2338
2376
|
|
|
2339
2377
|
try {
|
|
2340
|
-
const findResult = manager
|
|
2378
|
+
const findResult = checkTool(manager, "find", {}, "reviewer");
|
|
2341
2379
|
expect(findResult.state).toBe("allow");
|
|
2342
2380
|
expect(findResult.source).toBe("tool");
|
|
2343
2381
|
|
|
2344
|
-
const lsResult = manager
|
|
2382
|
+
const lsResult = checkTool(manager, "ls", {}, "reviewer");
|
|
2345
2383
|
expect(lsResult.state).toBe("deny");
|
|
2346
2384
|
expect(lsResult.source).toBe("tool");
|
|
2347
2385
|
} finally {
|
|
@@ -2367,15 +2405,16 @@ permission:
|
|
|
2367
2405
|
);
|
|
2368
2406
|
|
|
2369
2407
|
try {
|
|
2370
|
-
const findResult = manager
|
|
2408
|
+
const findResult = checkTool(manager, "find", {}, "reviewer");
|
|
2371
2409
|
expect(findResult.state).toBe("allow");
|
|
2372
2410
|
expect(findResult.source).toBe("tool");
|
|
2373
2411
|
|
|
2374
|
-
const taskResult = manager
|
|
2412
|
+
const taskResult = checkTool(manager, "task", {}, "reviewer");
|
|
2375
2413
|
expect(taskResult.state).toBe("allow");
|
|
2376
2414
|
expect(taskResult.source).toBe("tool");
|
|
2377
2415
|
|
|
2378
|
-
const mcpResult =
|
|
2416
|
+
const mcpResult = checkTool(
|
|
2417
|
+
manager,
|
|
2379
2418
|
"mcp",
|
|
2380
2419
|
{ tool: "exa:web_search_exa" },
|
|
2381
2420
|
"reviewer",
|
|
@@ -2392,7 +2431,7 @@ test("task uses exact-name tool permissions like any registered extension tool",
|
|
|
2392
2431
|
});
|
|
2393
2432
|
|
|
2394
2433
|
try {
|
|
2395
|
-
const taskResult = manager
|
|
2434
|
+
const taskResult = checkTool(manager, "task", {});
|
|
2396
2435
|
expect(taskResult.state).toBe("allow");
|
|
2397
2436
|
expect(taskResult.source).toBe("tool");
|
|
2398
2437
|
} finally {
|
|
@@ -2471,7 +2510,7 @@ test("external_directory permission falls back to universal default when not exp
|
|
|
2471
2510
|
const { manager, cleanup } = createManager({ permission: {} });
|
|
2472
2511
|
|
|
2473
2512
|
try {
|
|
2474
|
-
const result = manager
|
|
2513
|
+
const result = checkTool(manager, "external_directory", {});
|
|
2475
2514
|
expect(result.state).toBe("ask");
|
|
2476
2515
|
expect(result.source).toBe("special");
|
|
2477
2516
|
expect(result.matchedPattern).toBe(undefined);
|
|
@@ -2486,7 +2525,7 @@ test("external_directory permission respects explicit deny", () => {
|
|
|
2486
2525
|
});
|
|
2487
2526
|
|
|
2488
2527
|
try {
|
|
2489
|
-
const result = manager
|
|
2528
|
+
const result = checkTool(manager, "external_directory", {});
|
|
2490
2529
|
expect(result.state).toBe("deny");
|
|
2491
2530
|
expect(result.source).toBe("special");
|
|
2492
2531
|
expect(result.matchedPattern).toBe("*");
|
|
@@ -2501,7 +2540,7 @@ test("external_directory permission can be explicitly allowed", () => {
|
|
|
2501
2540
|
});
|
|
2502
2541
|
|
|
2503
2542
|
try {
|
|
2504
|
-
const result = manager
|
|
2543
|
+
const result = checkTool(manager, "external_directory", {});
|
|
2505
2544
|
expect(result.state).toBe("allow");
|
|
2506
2545
|
expect(result.source).toBe("special");
|
|
2507
2546
|
expect(result.matchedPattern).toBe("*");
|
|
@@ -2526,14 +2565,10 @@ permission:
|
|
|
2526
2565
|
);
|
|
2527
2566
|
|
|
2528
2567
|
try {
|
|
2529
|
-
const globalResult = manager
|
|
2568
|
+
const globalResult = checkTool(manager, "external_directory", {});
|
|
2530
2569
|
expect(globalResult.state).toBe("deny");
|
|
2531
2570
|
|
|
2532
|
-
const agentResult = manager
|
|
2533
|
-
"external_directory",
|
|
2534
|
-
{},
|
|
2535
|
-
"trusted",
|
|
2536
|
-
);
|
|
2571
|
+
const agentResult = checkTool(manager, "external_directory", {}, "trusted");
|
|
2537
2572
|
expect(agentResult.state).toBe("allow");
|
|
2538
2573
|
expect(agentResult.source).toBe("special");
|
|
2539
2574
|
} finally {
|
|
@@ -2547,7 +2582,7 @@ test("external_directory permission is not affected by unrelated surface keys",
|
|
|
2547
2582
|
});
|
|
2548
2583
|
|
|
2549
2584
|
try {
|
|
2550
|
-
const extResult = manager
|
|
2585
|
+
const extResult = checkTool(manager, "external_directory", {});
|
|
2551
2586
|
expect(extResult.state).toBe("allow");
|
|
2552
2587
|
expect(extResult.matchedPattern).toBe("*");
|
|
2553
2588
|
} finally {
|
|
@@ -2573,7 +2608,8 @@ permission:
|
|
|
2573
2608
|
);
|
|
2574
2609
|
|
|
2575
2610
|
try {
|
|
2576
|
-
const allowed =
|
|
2611
|
+
const allowed = checkTool(
|
|
2612
|
+
manager,
|
|
2577
2613
|
"skill",
|
|
2578
2614
|
{ name: "pi-code-review" },
|
|
2579
2615
|
"reviewer",
|
|
@@ -2582,7 +2618,8 @@ permission:
|
|
|
2582
2618
|
expect(allowed.matchedPattern).toBe("pi-*");
|
|
2583
2619
|
expect(allowed.source).toBe("skill");
|
|
2584
2620
|
|
|
2585
|
-
const asked =
|
|
2621
|
+
const asked = checkTool(
|
|
2622
|
+
manager,
|
|
2586
2623
|
"skill",
|
|
2587
2624
|
{ name: "other-skill" },
|
|
2588
2625
|
"reviewer",
|
|
@@ -2590,7 +2627,7 @@ permission:
|
|
|
2590
2627
|
expect(asked.state).toBe("ask");
|
|
2591
2628
|
expect(asked.matchedPattern).toBe("*");
|
|
2592
2629
|
|
|
2593
|
-
const denied = manager
|
|
2630
|
+
const denied = checkTool(manager, "skill", { name: "pi-code-review" });
|
|
2594
2631
|
expect(denied.state).toBe("deny");
|
|
2595
2632
|
expect(denied.source).toBe("skill");
|
|
2596
2633
|
} finally {
|
|
@@ -2616,7 +2653,8 @@ permission:
|
|
|
2616
2653
|
);
|
|
2617
2654
|
|
|
2618
2655
|
try {
|
|
2619
|
-
const allowed =
|
|
2656
|
+
const allowed = checkTool(
|
|
2657
|
+
manager,
|
|
2620
2658
|
"external_directory",
|
|
2621
2659
|
{ path: `${homedir()}/Downloads/file.txt` },
|
|
2622
2660
|
"trusted",
|
|
@@ -2625,7 +2663,8 @@ permission:
|
|
|
2625
2663
|
expect(allowed.matchedPattern).toBe("~/Downloads/*");
|
|
2626
2664
|
expect(allowed.source).toBe("special");
|
|
2627
2665
|
|
|
2628
|
-
const denied =
|
|
2666
|
+
const denied = checkTool(
|
|
2667
|
+
manager,
|
|
2629
2668
|
"external_directory",
|
|
2630
2669
|
{ path: `${homedir()}/Documents/secret.txt` },
|
|
2631
2670
|
"trusted",
|
|
@@ -2633,7 +2672,7 @@ permission:
|
|
|
2633
2672
|
expect(denied.state).toBe("deny");
|
|
2634
2673
|
expect(denied.matchedPattern).toBe("*");
|
|
2635
2674
|
|
|
2636
|
-
const globalDenied = manager
|
|
2675
|
+
const globalDenied = checkTool(manager, "external_directory", {});
|
|
2637
2676
|
expect(globalDenied.state).toBe("deny");
|
|
2638
2677
|
expect(globalDenied.source).toBe("special");
|
|
2639
2678
|
} finally {
|
|
@@ -2670,7 +2709,8 @@ permission:
|
|
|
2670
2709
|
);
|
|
2671
2710
|
|
|
2672
2711
|
try {
|
|
2673
|
-
const allowed =
|
|
2712
|
+
const allowed = checkTool(
|
|
2713
|
+
manager,
|
|
2674
2714
|
"skill",
|
|
2675
2715
|
{ name: "pi-code-review" },
|
|
2676
2716
|
"analyst",
|
|
@@ -2678,7 +2718,8 @@ permission:
|
|
|
2678
2718
|
expect(allowed.state).toBe("allow");
|
|
2679
2719
|
expect(allowed.matchedPattern).toBe("pi-*");
|
|
2680
2720
|
|
|
2681
|
-
const denied =
|
|
2721
|
+
const denied = checkTool(
|
|
2722
|
+
manager,
|
|
2682
2723
|
"skill",
|
|
2683
2724
|
{ name: "other-skill" },
|
|
2684
2725
|
"analyst",
|
|
@@ -2716,11 +2757,11 @@ permission:
|
|
|
2716
2757
|
);
|
|
2717
2758
|
|
|
2718
2759
|
try {
|
|
2719
|
-
const result = manager
|
|
2760
|
+
const result = checkTool(manager, "external_directory", {}, "analyst");
|
|
2720
2761
|
expect(result.state).toBe("allow");
|
|
2721
2762
|
expect(result.source).toBe("special");
|
|
2722
2763
|
|
|
2723
|
-
const globalResult = manager
|
|
2764
|
+
const globalResult = checkTool(manager, "external_directory", {});
|
|
2724
2765
|
expect(globalResult.state).toBe("deny");
|
|
2725
2766
|
} finally {
|
|
2726
2767
|
cleanup();
|
|
@@ -2747,10 +2788,10 @@ test("PermissionManager reads config from PI_CODING_AGENT_DIR when set", () => {
|
|
|
2747
2788
|
process.env.PI_CODING_AGENT_DIR = baseDir;
|
|
2748
2789
|
try {
|
|
2749
2790
|
const manager = new PermissionManager();
|
|
2750
|
-
const result = manager
|
|
2791
|
+
const result = checkTool(manager, "read", {});
|
|
2751
2792
|
expect(result.state).toBe("allow");
|
|
2752
2793
|
|
|
2753
|
-
const result2 = manager
|
|
2794
|
+
const result2 = checkTool(manager, "write", {});
|
|
2754
2795
|
expect(result2.state).toBe("deny");
|
|
2755
2796
|
} finally {
|
|
2756
2797
|
if (original !== undefined) {
|
|
@@ -2809,7 +2850,8 @@ test("checkPermission returns source 'session' when session rules cover the exte
|
|
|
2809
2850
|
},
|
|
2810
2851
|
];
|
|
2811
2852
|
|
|
2812
|
-
const result =
|
|
2853
|
+
const result = checkTool(
|
|
2854
|
+
manager,
|
|
2813
2855
|
"external_directory",
|
|
2814
2856
|
{ path: "/other/project/src/foo.ts" },
|
|
2815
2857
|
undefined,
|
|
@@ -2839,7 +2881,8 @@ test("checkPermission falls back to config policy when session rules do not cove
|
|
|
2839
2881
|
},
|
|
2840
2882
|
];
|
|
2841
2883
|
|
|
2842
|
-
const result =
|
|
2884
|
+
const result = checkTool(
|
|
2885
|
+
manager,
|
|
2843
2886
|
"external_directory",
|
|
2844
2887
|
{ path: "/completely/different/path.ts" },
|
|
2845
2888
|
undefined,
|
|
@@ -2858,13 +2901,14 @@ test("checkPermission with empty session rules is identical to call without sess
|
|
|
2858
2901
|
});
|
|
2859
2902
|
|
|
2860
2903
|
try {
|
|
2861
|
-
const withEmpty =
|
|
2904
|
+
const withEmpty = checkTool(
|
|
2905
|
+
manager,
|
|
2862
2906
|
"external_directory",
|
|
2863
2907
|
{ path: "/other/project/foo.ts" },
|
|
2864
2908
|
undefined,
|
|
2865
2909
|
[],
|
|
2866
2910
|
);
|
|
2867
|
-
const withoutArg = manager
|
|
2911
|
+
const withoutArg = checkTool(manager, "external_directory", {
|
|
2868
2912
|
path: "/other/project/foo.ts",
|
|
2869
2913
|
});
|
|
2870
2914
|
const expected: PermissionCheckResult = {
|
|
@@ -2895,7 +2939,8 @@ test("session rules for one surface do not affect checks on other surfaces", ()
|
|
|
2895
2939
|
},
|
|
2896
2940
|
];
|
|
2897
2941
|
|
|
2898
|
-
const bashResult =
|
|
2942
|
+
const bashResult = checkTool(
|
|
2943
|
+
manager,
|
|
2899
2944
|
"bash",
|
|
2900
2945
|
{ command: "git status" },
|
|
2901
2946
|
undefined,
|
|
@@ -2904,7 +2949,8 @@ test("session rules for one surface do not affect checks on other surfaces", ()
|
|
|
2904
2949
|
expect(bashResult.state).toBe("ask");
|
|
2905
2950
|
expect(bashResult.source).toBe("bash");
|
|
2906
2951
|
|
|
2907
|
-
const mcpResult =
|
|
2952
|
+
const mcpResult = checkTool(
|
|
2953
|
+
manager,
|
|
2908
2954
|
"mcp",
|
|
2909
2955
|
{ tool: "exa:search" },
|
|
2910
2956
|
undefined,
|
|
@@ -2933,7 +2979,8 @@ test("session rules override config deny for external_directory", () => {
|
|
|
2933
2979
|
},
|
|
2934
2980
|
];
|
|
2935
2981
|
|
|
2936
|
-
const result =
|
|
2982
|
+
const result = checkTool(
|
|
2983
|
+
manager,
|
|
2937
2984
|
"external_directory",
|
|
2938
2985
|
{ path: "/other/project/src/foo.ts" },
|
|
2939
2986
|
undefined,
|
|
@@ -2960,7 +3007,8 @@ test("checkPermission returns source 'session' for bash when session rules match
|
|
|
2960
3007
|
},
|
|
2961
3008
|
];
|
|
2962
3009
|
|
|
2963
|
-
const result =
|
|
3010
|
+
const result = checkTool(
|
|
3011
|
+
manager,
|
|
2964
3012
|
"bash",
|
|
2965
3013
|
{ command: "git status --short" },
|
|
2966
3014
|
undefined,
|
|
@@ -2988,7 +3036,8 @@ test("checkPermission returns source 'session' for bash when session rule is exa
|
|
|
2988
3036
|
},
|
|
2989
3037
|
];
|
|
2990
3038
|
|
|
2991
|
-
const result =
|
|
3039
|
+
const result = checkTool(
|
|
3040
|
+
manager,
|
|
2992
3041
|
"bash",
|
|
2993
3042
|
{ command: "ls" },
|
|
2994
3043
|
undefined,
|
|
@@ -3015,7 +3064,8 @@ test("checkPermission falls back to config for bash when session rules do not ma
|
|
|
3015
3064
|
},
|
|
3016
3065
|
];
|
|
3017
3066
|
|
|
3018
|
-
const result =
|
|
3067
|
+
const result = checkTool(
|
|
3068
|
+
manager,
|
|
3019
3069
|
"bash",
|
|
3020
3070
|
{ command: "npm run build" },
|
|
3021
3071
|
undefined,
|
|
@@ -3042,7 +3092,8 @@ test("checkPermission returns source 'session' for mcp when session rules match
|
|
|
3042
3092
|
},
|
|
3043
3093
|
];
|
|
3044
3094
|
|
|
3045
|
-
const result =
|
|
3095
|
+
const result = checkTool(
|
|
3096
|
+
manager,
|
|
3046
3097
|
"mcp",
|
|
3047
3098
|
{ tool: "exa:search" },
|
|
3048
3099
|
undefined,
|
|
@@ -3069,7 +3120,8 @@ test("checkPermission returns source 'session' for skill when session rules matc
|
|
|
3069
3120
|
},
|
|
3070
3121
|
];
|
|
3071
3122
|
|
|
3072
|
-
const result =
|
|
3123
|
+
const result = checkTool(
|
|
3124
|
+
manager,
|
|
3073
3125
|
"skill",
|
|
3074
3126
|
{ name: "librarian" },
|
|
3075
3127
|
undefined,
|
|
@@ -3097,7 +3149,7 @@ test("checkPermission returns source 'session' for tool surface when session rul
|
|
|
3097
3149
|
},
|
|
3098
3150
|
];
|
|
3099
3151
|
|
|
3100
|
-
const result = manager
|
|
3152
|
+
const result = checkTool(manager, "read", {}, undefined, sessionRules);
|
|
3101
3153
|
expect(result.state).toBe("allow");
|
|
3102
3154
|
expect(result.source).toBe("session");
|
|
3103
3155
|
} finally {
|
|
@@ -3119,7 +3171,8 @@ test("bash session rules do not bleed into mcp checks", () => {
|
|
|
3119
3171
|
},
|
|
3120
3172
|
];
|
|
3121
3173
|
|
|
3122
|
-
const result =
|
|
3174
|
+
const result = checkTool(
|
|
3175
|
+
manager,
|
|
3123
3176
|
"mcp",
|
|
3124
3177
|
{ tool: "exa:search" },
|
|
3125
3178
|
undefined,
|
|
@@ -3206,7 +3259,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
|
|
|
3206
3259
|
});
|
|
3207
3260
|
try {
|
|
3208
3261
|
manager.configureForCwd(cwd);
|
|
3209
|
-
const result = manager
|
|
3262
|
+
const result = checkTool(manager, "read", { path: "src/App.jsx" });
|
|
3210
3263
|
expect(result.state).toBe("allow");
|
|
3211
3264
|
expect(result.matchedPattern).toBe(`${cwd}/*`);
|
|
3212
3265
|
} finally {
|
|
@@ -3220,7 +3273,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
|
|
|
3220
3273
|
});
|
|
3221
3274
|
try {
|
|
3222
3275
|
manager.configureForCwd(cwd);
|
|
3223
|
-
const result = manager
|
|
3276
|
+
const result = checkTool(manager, "read", { path: "src/App.jsx" });
|
|
3224
3277
|
expect(result.state).toBe("deny");
|
|
3225
3278
|
expect(result.matchedPattern).toBe("src/*");
|
|
3226
3279
|
} finally {
|
|
@@ -3238,7 +3291,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
|
|
|
3238
3291
|
});
|
|
3239
3292
|
try {
|
|
3240
3293
|
manager.configureForCwd(cwd);
|
|
3241
|
-
const result = manager
|
|
3294
|
+
const result = checkTool(manager, "read", { path: "src/App.jsx" });
|
|
3242
3295
|
// The later "src/*" deny wins over the earlier absolute allow.
|
|
3243
3296
|
expect(result.state).toBe("deny");
|
|
3244
3297
|
expect(result.matchedPattern).toBe("src/*");
|
|
@@ -3253,7 +3306,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
|
|
|
3253
3306
|
});
|
|
3254
3307
|
try {
|
|
3255
3308
|
manager.configureForCwd(cwd);
|
|
3256
|
-
const result = manager
|
|
3309
|
+
const result = checkTool(manager, "path", { path: "src/App.jsx" });
|
|
3257
3310
|
expect(result.state).toBe("allow");
|
|
3258
3311
|
expect(result.matchedPattern).toBe(`${cwd}/*`);
|
|
3259
3312
|
} finally {
|
|
@@ -3270,7 +3323,7 @@ describe("checkPathPolicy", () => {
|
|
|
3270
3323
|
path: { "*": "ask", [`${cwd}/*`]: "allow" },
|
|
3271
3324
|
});
|
|
3272
3325
|
try {
|
|
3273
|
-
const result = manager
|
|
3326
|
+
const result = checkPathValues(manager, [
|
|
3274
3327
|
`${cwd}/src/App.jsx`,
|
|
3275
3328
|
"src/App.jsx",
|
|
3276
3329
|
]);
|
|
@@ -3288,7 +3341,7 @@ describe("checkPathPolicy", () => {
|
|
|
3288
3341
|
path: { "*": "ask", [`${cwd}/*`]: "allow", "src/*": "deny" },
|
|
3289
3342
|
});
|
|
3290
3343
|
try {
|
|
3291
|
-
const result = manager
|
|
3344
|
+
const result = checkPathValues(manager, [
|
|
3292
3345
|
`${cwd}/src/App.jsx`,
|
|
3293
3346
|
"src/App.jsx",
|
|
3294
3347
|
]);
|
|
@@ -3305,7 +3358,8 @@ describe("checkPathPolicy", () => {
|
|
|
3305
3358
|
});
|
|
3306
3359
|
try {
|
|
3307
3360
|
const sessionRules: Ruleset = [sessionAllow("path", "src/*")];
|
|
3308
|
-
const result =
|
|
3361
|
+
const result = checkPathValues(
|
|
3362
|
+
manager,
|
|
3309
3363
|
["src/App.jsx"],
|
|
3310
3364
|
undefined,
|
|
3311
3365
|
sessionRules,
|
|
@@ -3322,7 +3376,7 @@ describe("checkPathPolicy", () => {
|
|
|
3322
3376
|
path: { "*": "deny" },
|
|
3323
3377
|
});
|
|
3324
3378
|
try {
|
|
3325
|
-
const result = manager
|
|
3379
|
+
const result = checkPathValues(manager, []);
|
|
3326
3380
|
expect(result.state).toBe("deny");
|
|
3327
3381
|
expect(result.matchedPattern).toBe("*");
|
|
3328
3382
|
} finally {
|
|
@@ -3335,7 +3389,8 @@ describe("checkPathPolicy", () => {
|
|
|
3335
3389
|
external_directory: { "*": "ask", "/tmp/*": "allow" },
|
|
3336
3390
|
});
|
|
3337
3391
|
try {
|
|
3338
|
-
const result =
|
|
3392
|
+
const result = checkPathValues(
|
|
3393
|
+
manager,
|
|
3339
3394
|
["/tmp/x"],
|
|
3340
3395
|
undefined,
|
|
3341
3396
|
undefined,
|
|
@@ -3357,7 +3412,7 @@ describe("checkPathPolicy", () => {
|
|
|
3357
3412
|
});
|
|
3358
3413
|
try {
|
|
3359
3414
|
// No path rule denies; the external_directory allow must NOT apply here.
|
|
3360
|
-
const result = manager
|
|
3415
|
+
const result = checkPathValues(manager, ["/tmp/x"]);
|
|
3361
3416
|
expect(result.toolName).toBe("path");
|
|
3362
3417
|
expect(result.state).toBe("allow");
|
|
3363
3418
|
expect(result.matchedPattern).toBe("*");
|
|
@@ -3366,3 +3421,184 @@ describe("checkPathPolicy", () => {
|
|
|
3366
3421
|
}
|
|
3367
3422
|
});
|
|
3368
3423
|
});
|
|
3424
|
+
|
|
3425
|
+
// ---------------------------------------------------------------------------
|
|
3426
|
+
// check(intent) — unified entry point (Step 1 of #478)
|
|
3427
|
+
// ---------------------------------------------------------------------------
|
|
3428
|
+
|
|
3429
|
+
describe("check — tool intent", () => {
|
|
3430
|
+
it("resolves a tool call on the bash surface", () => {
|
|
3431
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3432
|
+
bash: { "*": "allow", "git push": "deny" },
|
|
3433
|
+
});
|
|
3434
|
+
try {
|
|
3435
|
+
const intent: ResolvedAccessIntent = {
|
|
3436
|
+
kind: "tool",
|
|
3437
|
+
surface: "bash",
|
|
3438
|
+
input: { command: "git push" },
|
|
3439
|
+
};
|
|
3440
|
+
const result = manager.check(intent);
|
|
3441
|
+
expect(result.state).toBe("deny");
|
|
3442
|
+
expect(result.toolName).toBe("bash");
|
|
3443
|
+
expect(result.source).toBe("bash");
|
|
3444
|
+
} finally {
|
|
3445
|
+
cleanup();
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
it("resolves a tool call on the read surface", () => {
|
|
3450
|
+
const { manager, cleanup } = makeManagerWithConfig({ read: "deny" });
|
|
3451
|
+
try {
|
|
3452
|
+
const intent: ResolvedAccessIntent = {
|
|
3453
|
+
kind: "tool",
|
|
3454
|
+
surface: "read",
|
|
3455
|
+
input: { path: "/some/file.txt" },
|
|
3456
|
+
};
|
|
3457
|
+
const result = manager.check(intent);
|
|
3458
|
+
expect(result.state).toBe("deny");
|
|
3459
|
+
expect(result.toolName).toBe("read");
|
|
3460
|
+
expect(result.source).toBe("tool");
|
|
3461
|
+
} finally {
|
|
3462
|
+
cleanup();
|
|
3463
|
+
}
|
|
3464
|
+
});
|
|
3465
|
+
|
|
3466
|
+
it("applies session rules via the tool intent", () => {
|
|
3467
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3468
|
+
bash: { "*": "deny" },
|
|
3469
|
+
});
|
|
3470
|
+
try {
|
|
3471
|
+
const sessionRules: Ruleset = [sessionAllow("bash", "echo *")];
|
|
3472
|
+
const intent: ResolvedAccessIntent = {
|
|
3473
|
+
kind: "tool",
|
|
3474
|
+
surface: "bash",
|
|
3475
|
+
input: { command: "echo hello" },
|
|
3476
|
+
};
|
|
3477
|
+
const result = manager.check(intent, sessionRules);
|
|
3478
|
+
expect(result.state).toBe("allow");
|
|
3479
|
+
expect(result.source).toBe("session");
|
|
3480
|
+
} finally {
|
|
3481
|
+
cleanup();
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
|
|
3485
|
+
it("threads agentName through the tool intent", () => {
|
|
3486
|
+
const { manager, cleanup } = createManager(
|
|
3487
|
+
{ permission: { bash: "deny" } },
|
|
3488
|
+
{
|
|
3489
|
+
"agent-a": `---\nname: agent-a\npermission:\n bash: allow\n---\n`,
|
|
3490
|
+
},
|
|
3491
|
+
);
|
|
3492
|
+
try {
|
|
3493
|
+
const intent: ResolvedAccessIntent = {
|
|
3494
|
+
kind: "tool",
|
|
3495
|
+
surface: "bash",
|
|
3496
|
+
input: { command: "echo hi" },
|
|
3497
|
+
agentName: "agent-a",
|
|
3498
|
+
};
|
|
3499
|
+
const result = manager.check(intent);
|
|
3500
|
+
expect(result.state).toBe("allow");
|
|
3501
|
+
} finally {
|
|
3502
|
+
cleanup();
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3505
|
+
});
|
|
3506
|
+
|
|
3507
|
+
describe("check — path-values intent", () => {
|
|
3508
|
+
const cwd = "/workspace/project";
|
|
3509
|
+
|
|
3510
|
+
it("evaluates precomputed policy values against the path surface", () => {
|
|
3511
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3512
|
+
path: { "*": "ask", [`${cwd}/*`]: "allow" },
|
|
3513
|
+
});
|
|
3514
|
+
try {
|
|
3515
|
+
const intent: ResolvedAccessIntent = {
|
|
3516
|
+
kind: "path-values",
|
|
3517
|
+
surface: "path",
|
|
3518
|
+
values: [`${cwd}/src/App.jsx`, "src/App.jsx"],
|
|
3519
|
+
};
|
|
3520
|
+
const result = manager.check(intent);
|
|
3521
|
+
expect(result.state).toBe("allow");
|
|
3522
|
+
expect(result.matchedPattern).toBe(`${cwd}/*`);
|
|
3523
|
+
expect(result.source).toBe("special");
|
|
3524
|
+
expect(result.toolName).toBe("path");
|
|
3525
|
+
} finally {
|
|
3526
|
+
cleanup();
|
|
3527
|
+
}
|
|
3528
|
+
});
|
|
3529
|
+
|
|
3530
|
+
it("evaluates against the external_directory surface", () => {
|
|
3531
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3532
|
+
external_directory: { "*": "ask", "/tmp/*": "allow" },
|
|
3533
|
+
});
|
|
3534
|
+
try {
|
|
3535
|
+
const intent: ResolvedAccessIntent = {
|
|
3536
|
+
kind: "path-values",
|
|
3537
|
+
surface: "external_directory",
|
|
3538
|
+
values: ["/tmp/x"],
|
|
3539
|
+
};
|
|
3540
|
+
const result = manager.check(intent);
|
|
3541
|
+
expect(result.state).toBe("allow");
|
|
3542
|
+
expect(result.matchedPattern).toBe("/tmp/*");
|
|
3543
|
+
expect(result.source).toBe("special");
|
|
3544
|
+
expect(result.toolName).toBe("external_directory");
|
|
3545
|
+
} finally {
|
|
3546
|
+
cleanup();
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
3549
|
+
|
|
3550
|
+
it("falls back to the catch-all for an empty value list", () => {
|
|
3551
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3552
|
+
path: { "*": "deny" },
|
|
3553
|
+
});
|
|
3554
|
+
try {
|
|
3555
|
+
const intent: ResolvedAccessIntent = {
|
|
3556
|
+
kind: "path-values",
|
|
3557
|
+
surface: "path",
|
|
3558
|
+
values: [],
|
|
3559
|
+
};
|
|
3560
|
+
const result = manager.check(intent);
|
|
3561
|
+
expect(result.state).toBe("deny");
|
|
3562
|
+
expect(result.matchedPattern).toBe("*");
|
|
3563
|
+
} finally {
|
|
3564
|
+
cleanup();
|
|
3565
|
+
}
|
|
3566
|
+
});
|
|
3567
|
+
|
|
3568
|
+
it("applies session rules via the path-values intent", () => {
|
|
3569
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3570
|
+
path: { "*": "ask", "src/*": "deny" },
|
|
3571
|
+
});
|
|
3572
|
+
try {
|
|
3573
|
+
const sessionRules: Ruleset = [sessionAllow("path", "src/*")];
|
|
3574
|
+
const intent: ResolvedAccessIntent = {
|
|
3575
|
+
kind: "path-values",
|
|
3576
|
+
surface: "path",
|
|
3577
|
+
values: ["src/App.jsx"],
|
|
3578
|
+
};
|
|
3579
|
+
const result = manager.check(intent, sessionRules);
|
|
3580
|
+
expect(result.state).toBe("allow");
|
|
3581
|
+
expect(result.source).toBe("session");
|
|
3582
|
+
} finally {
|
|
3583
|
+
cleanup();
|
|
3584
|
+
}
|
|
3585
|
+
});
|
|
3586
|
+
|
|
3587
|
+
it("last-match-wins across the provided aliases", () => {
|
|
3588
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
3589
|
+
path: { "*": "ask", [`${cwd}/*`]: "allow", "src/*": "deny" },
|
|
3590
|
+
});
|
|
3591
|
+
try {
|
|
3592
|
+
const intent: ResolvedAccessIntent = {
|
|
3593
|
+
kind: "path-values",
|
|
3594
|
+
surface: "path",
|
|
3595
|
+
values: [`${cwd}/src/App.jsx`, "src/App.jsx"],
|
|
3596
|
+
};
|
|
3597
|
+
const result = manager.check(intent);
|
|
3598
|
+
expect(result.state).toBe("deny");
|
|
3599
|
+
expect(result.matchedPattern).toBe("src/*");
|
|
3600
|
+
} finally {
|
|
3601
|
+
cleanup();
|
|
3602
|
+
}
|
|
3603
|
+
});
|
|
3604
|
+
});
|