@gotgenes/pi-permission-system 5.18.1 → 5.18.2
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 +24 -0
- package/README.md +19 -19
- package/package.json +11 -16
- package/src/handlers/gates/bash-path.ts +8 -0
- package/src/handlers/gates/path.ts +12 -1
- package/src/handlers/gates/skill-read.ts +4 -0
- package/src/handlers/lifecycle.ts +1 -1
- package/src/handlers/permission-gate-handler.ts +1 -1
- package/tests/config-modal.test.ts +43 -58
- package/tests/config-reporter.test.ts +31 -34
- package/tests/handlers/external-directory-integration.test.ts +3 -3
- package/tests/handlers/gates/bash-path.test.ts +57 -3
- package/tests/handlers/gates/path.test.ts +82 -9
- package/tests/handlers/tool-call.test.ts +2 -2
- package/tests/permission-manager-unified.test.ts +26 -0
- package/tests/permission-system.test.ts +313 -358
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
1
|
import {
|
|
3
2
|
existsSync,
|
|
4
3
|
mkdirSync,
|
|
@@ -9,7 +8,7 @@ import {
|
|
|
9
8
|
} from "node:fs";
|
|
10
9
|
import { homedir, tmpdir } from "node:os";
|
|
11
10
|
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { test } from "vitest";
|
|
11
|
+
import { expect, test } from "vitest";
|
|
13
12
|
|
|
14
13
|
import {
|
|
15
14
|
createActiveToolsCacheKey,
|
|
@@ -230,7 +229,7 @@ async function runToolCall(
|
|
|
230
229
|
options: ExtensionHarnessOptions = {},
|
|
231
230
|
): Promise<Record<string, unknown>> {
|
|
232
231
|
const handler = harness.handlers.tool_call;
|
|
233
|
-
|
|
232
|
+
expect(handler).toBeTypeOf("function");
|
|
234
233
|
|
|
235
234
|
const result = await withIsolatedSubagentEnv(async () =>
|
|
236
235
|
Promise.resolve(
|
|
@@ -241,66 +240,58 @@ async function runToolCall(
|
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
test("Yolo mode only auto-approves ask-state permissions", () => {
|
|
244
|
-
|
|
243
|
+
expect(
|
|
245
244
|
shouldAutoApprovePermissionState("ask", DEFAULT_EXTENSION_CONFIG),
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
assert.equal(
|
|
245
|
+
).toBe(false);
|
|
246
|
+
expect(
|
|
249
247
|
shouldAutoApprovePermissionState("ask", {
|
|
250
248
|
...DEFAULT_EXTENSION_CONFIG,
|
|
251
249
|
yoloMode: true,
|
|
252
250
|
}),
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
assert.equal(
|
|
251
|
+
).toBe(true);
|
|
252
|
+
expect(
|
|
256
253
|
shouldAutoApprovePermissionState("deny", {
|
|
257
254
|
...DEFAULT_EXTENSION_CONFIG,
|
|
258
255
|
yoloMode: true,
|
|
259
256
|
}),
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
assert.equal(
|
|
257
|
+
).toBe(false);
|
|
258
|
+
expect(
|
|
263
259
|
shouldAutoApprovePermissionState("allow", {
|
|
264
260
|
...DEFAULT_EXTENSION_CONFIG,
|
|
265
261
|
yoloMode: true,
|
|
266
262
|
}),
|
|
267
|
-
|
|
268
|
-
);
|
|
263
|
+
).toBe(false);
|
|
269
264
|
});
|
|
270
265
|
|
|
271
266
|
test("Yolo mode resolves ask permissions without UI or delegation forwarding", () => {
|
|
272
|
-
|
|
267
|
+
expect(
|
|
273
268
|
canResolveAskPermissionRequest({
|
|
274
269
|
config: DEFAULT_EXTENSION_CONFIG,
|
|
275
270
|
hasUI: false,
|
|
276
271
|
isSubagent: false,
|
|
277
272
|
}),
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
assert.equal(
|
|
273
|
+
).toBe(false);
|
|
274
|
+
expect(
|
|
281
275
|
canResolveAskPermissionRequest({
|
|
282
276
|
config: { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true },
|
|
283
277
|
hasUI: false,
|
|
284
278
|
isSubagent: false,
|
|
285
279
|
}),
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
assert.equal(
|
|
280
|
+
).toBe(true);
|
|
281
|
+
expect(
|
|
289
282
|
canResolveAskPermissionRequest({
|
|
290
283
|
config: DEFAULT_EXTENSION_CONFIG,
|
|
291
284
|
hasUI: false,
|
|
292
285
|
isSubagent: true,
|
|
293
286
|
}),
|
|
294
|
-
|
|
295
|
-
);
|
|
287
|
+
).toBe(true);
|
|
296
288
|
});
|
|
297
289
|
|
|
298
290
|
test("Permission-system status is only exposed when yolo mode is enabled", () => {
|
|
299
|
-
|
|
300
|
-
|
|
291
|
+
expect(getPermissionSystemStatus(DEFAULT_EXTENSION_CONFIG)).toBe(undefined);
|
|
292
|
+
expect(
|
|
301
293
|
getPermissionSystemStatus({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: true }),
|
|
302
|
-
|
|
303
|
-
);
|
|
294
|
+
).toBe("yolo");
|
|
304
295
|
});
|
|
305
296
|
|
|
306
297
|
test("System prompt sanitizer removes the Available tools section and surrounding boilerplate", () => {
|
|
@@ -318,11 +309,11 @@ test("System prompt sanitizer removes the Available tools section and surroundin
|
|
|
318
309
|
|
|
319
310
|
const result = sanitizeAvailableToolsSection(prompt, ["read", "mcp"]);
|
|
320
311
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
312
|
+
expect(result.removed).toBe(true);
|
|
313
|
+
expect(result.prompt).not.toContain("Available tools:");
|
|
314
|
+
expect(result.prompt).not.toContain("In addition to the tools above");
|
|
315
|
+
expect(result.prompt).toMatch(/Guidelines:/);
|
|
316
|
+
expect(result.prompt).toMatch(/Use mcp for MCP discovery first/i);
|
|
326
317
|
});
|
|
327
318
|
|
|
328
319
|
test("System prompt sanitizer removes denied tool guidelines while keeping global guidance", () => {
|
|
@@ -337,13 +328,12 @@ test("System prompt sanitizer removes denied tool guidelines while keeping globa
|
|
|
337
328
|
|
|
338
329
|
const result = sanitizeAvailableToolsSection(prompt, ["bash", "grep", "mcp"]);
|
|
339
330
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
result.prompt,
|
|
331
|
+
expect(result.removed).toBe(true);
|
|
332
|
+
expect(result.prompt).not.toContain("Use task when work SHOULD");
|
|
333
|
+
expect(result.prompt).toMatch(/Use mcp for MCP discovery first/i);
|
|
334
|
+
expect(result.prompt).toMatch(/Prefer grep\/find\/ls tools over bash/i);
|
|
335
|
+
expect(result.prompt).toMatch(/Be concise in your responses/);
|
|
336
|
+
expect(result.prompt).toMatch(
|
|
347
337
|
/Show file paths clearly when working with files/,
|
|
348
338
|
);
|
|
349
339
|
});
|
|
@@ -358,16 +348,14 @@ test("System prompt sanitizer removes inactive built-in write guidance", () => {
|
|
|
358
348
|
|
|
359
349
|
const result = sanitizeAvailableToolsSection(prompt, ["read"]);
|
|
360
350
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
false,
|
|
351
|
+
expect(result.removed).toBe(true);
|
|
352
|
+
expect(result.prompt).not.toContain(
|
|
353
|
+
"Use write only for new files or complete rewrites",
|
|
365
354
|
);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
false,
|
|
355
|
+
expect(result.prompt).not.toContain(
|
|
356
|
+
"do NOT use cat or bash to display what you did",
|
|
369
357
|
);
|
|
370
|
-
|
|
358
|
+
expect(result.prompt).toMatch(/Be concise in your responses/);
|
|
371
359
|
});
|
|
372
360
|
|
|
373
361
|
test("Before-agent-start cache dedupes unchanged active-tool exposure and prompt state", () => {
|
|
@@ -381,14 +369,12 @@ test("Before-agent-start cache dedupes unchanged active-tool exposure and prompt
|
|
|
381
369
|
allowedToolNames: allowedTools,
|
|
382
370
|
});
|
|
383
371
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
shouldApplyCachedAgentStartState(activeToolsKey, activeToolsKey),
|
|
372
|
+
expect(shouldApplyCachedAgentStartState(null, activeToolsKey)).toBe(true);
|
|
373
|
+
expect(shouldApplyCachedAgentStartState(activeToolsKey, activeToolsKey)).toBe(
|
|
387
374
|
false,
|
|
388
375
|
);
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
shouldApplyCachedAgentStartState(promptStateKey, promptStateKey),
|
|
376
|
+
expect(shouldApplyCachedAgentStartState(null, promptStateKey)).toBe(true);
|
|
377
|
+
expect(shouldApplyCachedAgentStartState(promptStateKey, promptStateKey)).toBe(
|
|
392
378
|
false,
|
|
393
379
|
);
|
|
394
380
|
});
|
|
@@ -408,11 +394,10 @@ test("Before-agent-start prompt cache invalidates on permission changes while ru
|
|
|
408
394
|
allowedToolNames: ["read"],
|
|
409
395
|
});
|
|
410
396
|
|
|
411
|
-
|
|
412
|
-
shouldApplyCachedAgentStartState(baselineKey, baselineKey),
|
|
397
|
+
expect(shouldApplyCachedAgentStartState(baselineKey, baselineKey)).toBe(
|
|
413
398
|
false,
|
|
414
399
|
);
|
|
415
|
-
|
|
400
|
+
expect(manager.checkPermission("write", {}, undefined).state).toBe("deny");
|
|
416
401
|
|
|
417
402
|
const updatedConfig = `${JSON.stringify(
|
|
418
403
|
{ permission: { "*": "allow", write: "allow" } },
|
|
@@ -435,7 +420,7 @@ test("Before-agent-start prompt cache invalidates on permission changes while ru
|
|
|
435
420
|
updatedStamp = manager.getPolicyCacheStamp();
|
|
436
421
|
}
|
|
437
422
|
|
|
438
|
-
|
|
423
|
+
expect(updatedStamp).not.toBe(baselineStamp);
|
|
439
424
|
|
|
440
425
|
const invalidatedKey = createBeforeAgentStartPromptStateKey({
|
|
441
426
|
agentName: null,
|
|
@@ -445,14 +430,10 @@ test("Before-agent-start prompt cache invalidates on permission changes while ru
|
|
|
445
430
|
allowedToolNames: ["read", "write"],
|
|
446
431
|
});
|
|
447
432
|
|
|
448
|
-
|
|
449
|
-
shouldApplyCachedAgentStartState(baselineKey, invalidatedKey),
|
|
433
|
+
expect(shouldApplyCachedAgentStartState(baselineKey, invalidatedKey)).toBe(
|
|
450
434
|
true,
|
|
451
435
|
);
|
|
452
|
-
|
|
453
|
-
manager.checkPermission("write", {}, undefined).state,
|
|
454
|
-
"allow",
|
|
455
|
-
);
|
|
436
|
+
expect(manager.checkPermission("write", {}, undefined).state).toBe("allow");
|
|
456
437
|
} finally {
|
|
457
438
|
cleanup();
|
|
458
439
|
}
|
|
@@ -482,16 +463,16 @@ test("Permission-system logger respects debug toggle and keeps review log enable
|
|
|
482
463
|
toolName: "write",
|
|
483
464
|
});
|
|
484
465
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
466
|
+
expect(initialDebugWarning).toBe(undefined);
|
|
467
|
+
expect(reviewWarning).toBe(undefined);
|
|
468
|
+
expect(existsSync(debugLogPath)).toBe(false);
|
|
469
|
+
expect(existsSync(reviewLogPath)).toBe(true);
|
|
489
470
|
|
|
490
471
|
config.debugLog = true;
|
|
491
472
|
const enabledDebugWarning = logger.debug("debug.enabled", { sample: true });
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
473
|
+
expect(enabledDebugWarning).toBe(undefined);
|
|
474
|
+
expect(existsSync(debugLogPath)).toBe(true);
|
|
475
|
+
expect(readFileSync(debugLogPath, "utf8")).toMatch(/debug\.enabled/);
|
|
495
476
|
} finally {
|
|
496
477
|
rmSync(baseDir, { recursive: true, force: true });
|
|
497
478
|
}
|
|
@@ -504,12 +485,12 @@ test("PermissionManager canonical built-in permission checking", () => {
|
|
|
504
485
|
|
|
505
486
|
try {
|
|
506
487
|
const readResult = manager.checkPermission("read", {});
|
|
507
|
-
|
|
508
|
-
|
|
488
|
+
expect(readResult.state).toBe("allow");
|
|
489
|
+
expect(readResult.source).toBe("tool");
|
|
509
490
|
|
|
510
491
|
const writeResult = manager.checkPermission("write", {});
|
|
511
|
-
|
|
512
|
-
|
|
492
|
+
expect(writeResult.state).toBe("deny");
|
|
493
|
+
expect(writeResult.source).toBe("tool");
|
|
513
494
|
} finally {
|
|
514
495
|
cleanup();
|
|
515
496
|
}
|
|
@@ -530,7 +511,7 @@ test("multiline bash command resolves to allow via universal fallback", () => {
|
|
|
530
511
|
const command =
|
|
531
512
|
"node -e \"\nimport('x').then(() => {\n console.log('done');\n});\n\"";
|
|
532
513
|
const result = manager.checkPermission("bash", { command });
|
|
533
|
-
|
|
514
|
+
expect(result.state).toBe("allow");
|
|
534
515
|
} finally {
|
|
535
516
|
cleanup();
|
|
536
517
|
}
|
|
@@ -548,14 +529,14 @@ test("Bash specific deny patterns override catch-all within the same config", ()
|
|
|
548
529
|
|
|
549
530
|
try {
|
|
550
531
|
const denied = manager.checkPermission("bash", { command: "rm -rf build" });
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
532
|
+
expect(denied.state).toBe("deny");
|
|
533
|
+
expect(denied.source).toBe("bash");
|
|
534
|
+
expect(denied.matchedPattern).toBe("rm -rf *");
|
|
554
535
|
|
|
555
536
|
const allowed = manager.checkPermission("bash", { command: "echo hello" });
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
537
|
+
expect(allowed.state).toBe("allow");
|
|
538
|
+
expect(allowed.source).toBe("bash");
|
|
539
|
+
expect(allowed.matchedPattern).toBe("*");
|
|
559
540
|
} finally {
|
|
560
541
|
cleanup();
|
|
561
542
|
}
|
|
@@ -573,22 +554,22 @@ test("MCP wildcard matching uses the registered mcp tool", () => {
|
|
|
573
554
|
const queryDocs = manager.checkPermission("mcp", {
|
|
574
555
|
tool: "research:query-docs",
|
|
575
556
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
557
|
+
expect(queryDocs.state).toBe("allow");
|
|
558
|
+
expect(queryDocs.source).toBe("mcp");
|
|
559
|
+
expect(queryDocs.matchedPattern).toBe("research_query-*");
|
|
560
|
+
expect(queryDocs.target).toBe("research_query-docs");
|
|
580
561
|
|
|
581
562
|
const resolve2 = manager.checkPermission("mcp", {
|
|
582
563
|
tool: "research:resolve-context",
|
|
583
564
|
});
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
565
|
+
expect(resolve2.state).toBe("ask");
|
|
566
|
+
expect(resolve2.matchedPattern).toBe("research_*");
|
|
567
|
+
expect(resolve2.target).toBe("research_resolve-context");
|
|
587
568
|
|
|
588
569
|
const unknown = manager.checkPermission("mcp", { tool: "search:provider" });
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
570
|
+
expect(unknown.state).toBe("deny");
|
|
571
|
+
expect(unknown.matchedPattern).toBe("*");
|
|
572
|
+
expect(unknown.target).toBe("search_provider");
|
|
592
573
|
} finally {
|
|
593
574
|
cleanup();
|
|
594
575
|
}
|
|
@@ -605,14 +586,14 @@ test("Arbitrary extension tools use exact-name tool permissions instead of MCP f
|
|
|
605
586
|
|
|
606
587
|
try {
|
|
607
588
|
const allowed = manager.checkPermission("third_party_tool", {});
|
|
608
|
-
|
|
609
|
-
|
|
589
|
+
expect(allowed.state).toBe("allow");
|
|
590
|
+
expect(allowed.source).toBe("tool");
|
|
610
591
|
|
|
611
592
|
// another_extension_tool has no explicit rule — falls through to the
|
|
612
593
|
// universal default (permission["*"] = "deny") with source "default".
|
|
613
594
|
const fallback = manager.checkPermission("another_extension_tool", {});
|
|
614
|
-
|
|
615
|
-
|
|
595
|
+
expect(fallback.state).toBe("deny");
|
|
596
|
+
expect(fallback.source).toBe("default");
|
|
616
597
|
} finally {
|
|
617
598
|
cleanup();
|
|
618
599
|
}
|
|
@@ -634,21 +615,21 @@ test("Skill permission matching", () => {
|
|
|
634
615
|
const allowed = manager.checkPermission("skill", {
|
|
635
616
|
name: "requesting-code-review",
|
|
636
617
|
});
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
618
|
+
expect(allowed.state).toBe("allow");
|
|
619
|
+
expect(allowed.matchedPattern).toBe("requesting-code-review");
|
|
620
|
+
expect(allowed.source).toBe("skill");
|
|
640
621
|
|
|
641
622
|
const denied = manager.checkPermission("skill", {
|
|
642
623
|
name: "web-design-guidelines",
|
|
643
624
|
});
|
|
644
|
-
|
|
645
|
-
|
|
625
|
+
expect(denied.state).toBe("deny");
|
|
626
|
+
expect(denied.matchedPattern).toBe("web-*");
|
|
646
627
|
|
|
647
628
|
const fallback = manager.checkPermission("skill", {
|
|
648
629
|
name: "unknown-skill",
|
|
649
630
|
});
|
|
650
|
-
|
|
651
|
-
|
|
631
|
+
expect(fallback.state).toBe("ask");
|
|
632
|
+
expect(fallback.matchedPattern).toBe("*");
|
|
652
633
|
} finally {
|
|
653
634
|
cleanup();
|
|
654
635
|
}
|
|
@@ -672,10 +653,10 @@ test("MCP proxy tool infers server-prefixed aliases from configured server names
|
|
|
672
653
|
const result = manager.checkPermission("mcp", {
|
|
673
654
|
tool: "get_code_context_exa",
|
|
674
655
|
});
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
656
|
+
expect(result.state).toBe("allow");
|
|
657
|
+
expect(result.source).toBe("mcp");
|
|
658
|
+
expect(result.matchedPattern).toBe("exa_get_code_context_exa");
|
|
659
|
+
expect(result.target).toBe("exa_get_code_context_exa");
|
|
679
660
|
} finally {
|
|
680
661
|
cleanup();
|
|
681
662
|
}
|
|
@@ -715,7 +696,7 @@ test("MCP server names in settings.json are not used — only mcp.json is consul
|
|
|
715
696
|
const result = manager.checkPermission("mcp", {
|
|
716
697
|
tool: "some_tool_legacy-server",
|
|
717
698
|
});
|
|
718
|
-
|
|
699
|
+
expect(result.state).toBe("ask");
|
|
719
700
|
} finally {
|
|
720
701
|
rmSync(baseDir, { recursive: true, force: true });
|
|
721
702
|
}
|
|
@@ -740,10 +721,10 @@ test("MCP describe mode normalizes qualified tool names without duplicating serv
|
|
|
740
721
|
describe: "exa:web_search_exa",
|
|
741
722
|
server: "exa",
|
|
742
723
|
});
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
724
|
+
expect(result.state).toBe("allow");
|
|
725
|
+
expect(result.source).toBe("mcp");
|
|
726
|
+
expect(result.matchedPattern).toBe("exa_web_search_exa");
|
|
727
|
+
expect(result.target).toBe("exa_web_search_exa");
|
|
747
728
|
} finally {
|
|
748
729
|
cleanup();
|
|
749
730
|
}
|
|
@@ -756,12 +737,12 @@ test("Canonical tools map directly without legacy aliases", () => {
|
|
|
756
737
|
|
|
757
738
|
try {
|
|
758
739
|
const findResult = manager.checkPermission("find", {});
|
|
759
|
-
|
|
760
|
-
|
|
740
|
+
expect(findResult.state).toBe("allow");
|
|
741
|
+
expect(findResult.source).toBe("tool");
|
|
761
742
|
|
|
762
743
|
const lsResult = manager.checkPermission("ls", {});
|
|
763
|
-
|
|
764
|
-
|
|
744
|
+
expect(lsResult.state).toBe("deny");
|
|
745
|
+
expect(lsResult.source).toBe("tool");
|
|
765
746
|
} finally {
|
|
766
747
|
cleanup();
|
|
767
748
|
}
|
|
@@ -788,9 +769,9 @@ permission:
|
|
|
788
769
|
{ tool: "exa:web_search_exa" },
|
|
789
770
|
"reviewer",
|
|
790
771
|
);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
772
|
+
expect(result.state).toBe("allow");
|
|
773
|
+
expect(result.source).toBe("mcp");
|
|
774
|
+
expect(result.target).toBe("exa_web_search_exa");
|
|
794
775
|
} finally {
|
|
795
776
|
cleanup();
|
|
796
777
|
}
|
|
@@ -822,10 +803,10 @@ permission:
|
|
|
822
803
|
{ tool: "web_search_exa" },
|
|
823
804
|
"reviewer",
|
|
824
805
|
);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
806
|
+
expect(result.state).toBe("deny");
|
|
807
|
+
expect(result.source).toBe("mcp");
|
|
808
|
+
expect(result.matchedPattern).toBe("exa_web_search_exa");
|
|
809
|
+
expect(result.target).toBe("exa_web_search_exa");
|
|
829
810
|
} finally {
|
|
830
811
|
cleanup();
|
|
831
812
|
}
|
|
@@ -857,19 +838,19 @@ permission:
|
|
|
857
838
|
{ tool: "web_search_exa" },
|
|
858
839
|
"reviewer",
|
|
859
840
|
);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
841
|
+
expect(allowed.state).toBe("allow");
|
|
842
|
+
expect(allowed.source).toBe("mcp");
|
|
843
|
+
expect(allowed.matchedPattern).toBe("exa_web_search_exa");
|
|
844
|
+
expect(allowed.target).toBe("exa_web_search_exa");
|
|
864
845
|
|
|
865
846
|
const fallback = manager.checkPermission(
|
|
866
847
|
"mcp",
|
|
867
848
|
{ tool: "other_exa" },
|
|
868
849
|
"reviewer",
|
|
869
850
|
);
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
851
|
+
expect(fallback.state).toBe("deny");
|
|
852
|
+
expect(fallback.source).toBe("mcp");
|
|
853
|
+
expect(fallback.target).toBe("exa_other_exa");
|
|
873
854
|
} finally {
|
|
874
855
|
cleanup();
|
|
875
856
|
}
|
|
@@ -892,16 +873,16 @@ permission:
|
|
|
892
873
|
|
|
893
874
|
try {
|
|
894
875
|
const readResult = manager.checkPermission("read", {}, "reviewer");
|
|
895
|
-
|
|
896
|
-
|
|
876
|
+
expect(readResult.state).toBe("deny");
|
|
877
|
+
expect(readResult.source).toBe("tool");
|
|
897
878
|
|
|
898
879
|
const mcpResult = manager.checkPermission(
|
|
899
880
|
"mcp",
|
|
900
881
|
{ tool: "exa:web_search_exa" },
|
|
901
882
|
"reviewer",
|
|
902
883
|
);
|
|
903
|
-
|
|
904
|
-
|
|
884
|
+
expect(mcpResult.state).toBe("allow");
|
|
885
|
+
expect(mcpResult.source).toBe("mcp");
|
|
905
886
|
} finally {
|
|
906
887
|
cleanup();
|
|
907
888
|
}
|
|
@@ -925,12 +906,12 @@ permission:
|
|
|
925
906
|
|
|
926
907
|
try {
|
|
927
908
|
const findResult = manager.checkPermission("find", {}, "reviewer");
|
|
928
|
-
|
|
929
|
-
|
|
909
|
+
expect(findResult.state).toBe("allow");
|
|
910
|
+
expect(findResult.source).toBe("tool");
|
|
930
911
|
|
|
931
912
|
const lsResult = manager.checkPermission("ls", {}, "reviewer");
|
|
932
|
-
|
|
933
|
-
|
|
913
|
+
expect(lsResult.state).toBe("deny");
|
|
914
|
+
expect(lsResult.source).toBe("tool");
|
|
934
915
|
} finally {
|
|
935
916
|
cleanup();
|
|
936
917
|
}
|
|
@@ -955,13 +936,13 @@ permission:
|
|
|
955
936
|
|
|
956
937
|
try {
|
|
957
938
|
const findResult = manager.checkPermission("find", {}, "reviewer");
|
|
958
|
-
|
|
959
|
-
|
|
939
|
+
expect(findResult.state).toBe("allow");
|
|
940
|
+
expect(findResult.source).toBe("tool");
|
|
960
941
|
|
|
961
942
|
// In flat format any surface key works, including extension tools
|
|
962
943
|
const taskResult = manager.checkPermission("task", {}, "reviewer");
|
|
963
|
-
|
|
964
|
-
|
|
944
|
+
expect(taskResult.state).toBe("allow");
|
|
945
|
+
expect(taskResult.source).toBe("tool");
|
|
965
946
|
|
|
966
947
|
// mcp: allow catches all MCP targets
|
|
967
948
|
const mcpResult = manager.checkPermission(
|
|
@@ -969,7 +950,7 @@ permission:
|
|
|
969
950
|
{ tool: "exa:web_search_exa" },
|
|
970
951
|
"reviewer",
|
|
971
952
|
);
|
|
972
|
-
|
|
953
|
+
expect(mcpResult.state).toBe("allow");
|
|
973
954
|
} finally {
|
|
974
955
|
cleanup();
|
|
975
956
|
}
|
|
@@ -982,19 +963,19 @@ test("task uses exact-name tool permissions like any registered extension tool",
|
|
|
982
963
|
|
|
983
964
|
try {
|
|
984
965
|
const taskResult = manager.checkPermission("task", {});
|
|
985
|
-
|
|
986
|
-
|
|
966
|
+
expect(taskResult.state).toBe("allow");
|
|
967
|
+
expect(taskResult.source).toBe("tool");
|
|
987
968
|
} finally {
|
|
988
969
|
cleanup();
|
|
989
970
|
}
|
|
990
971
|
});
|
|
991
972
|
|
|
992
973
|
test("Tool registry resolves event tool names from string and object payloads", () => {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
974
|
+
expect(getToolNameFromValue(" read ")).toBe("read");
|
|
975
|
+
expect(getToolNameFromValue({ toolName: "write" })).toBe("write");
|
|
976
|
+
expect(getToolNameFromValue({ name: "find" })).toBe("find");
|
|
977
|
+
expect(getToolNameFromValue({ tool: "grep" })).toBe("grep");
|
|
978
|
+
expect(getToolNameFromValue({})).toBe(null);
|
|
998
979
|
});
|
|
999
980
|
|
|
1000
981
|
test("Tool registry blocks unregistered tools and handles aliases", () => {
|
|
@@ -1008,9 +989,9 @@ test("Tool registry blocks unregistered tools and handles aliases", () => {
|
|
|
1008
989
|
"third_party_tool",
|
|
1009
990
|
registeredTools,
|
|
1010
991
|
);
|
|
1011
|
-
|
|
992
|
+
expect(unknownCheck.status).toBe("unregistered");
|
|
1012
993
|
if (unknownCheck.status === "unregistered") {
|
|
1013
|
-
|
|
994
|
+
expect(unknownCheck.availableToolNames).toEqual(["bash", "mcp", "read"]);
|
|
1014
995
|
}
|
|
1015
996
|
|
|
1016
997
|
const aliasCheck = checkRequestedToolRegistration(
|
|
@@ -1018,13 +999,13 @@ test("Tool registry blocks unregistered tools and handles aliases", () => {
|
|
|
1018
999
|
registeredTools,
|
|
1019
1000
|
{ legacy_read: "read" },
|
|
1020
1001
|
);
|
|
1021
|
-
|
|
1002
|
+
expect(aliasCheck.status).toBe("registered");
|
|
1022
1003
|
|
|
1023
1004
|
const missingNameCheck = checkRequestedToolRegistration(
|
|
1024
1005
|
" ",
|
|
1025
1006
|
registeredTools,
|
|
1026
1007
|
);
|
|
1027
|
-
|
|
1008
|
+
expect(missingNameCheck.status).toBe("missing-tool-name");
|
|
1028
1009
|
});
|
|
1029
1010
|
|
|
1030
1011
|
test("getToolPermission returns tool-level policy for canonical and extension tools", () => {
|
|
@@ -1046,16 +1027,16 @@ permission:
|
|
|
1046
1027
|
|
|
1047
1028
|
try {
|
|
1048
1029
|
const bashPermission = manager.getToolPermission("bash", "reviewer");
|
|
1049
|
-
|
|
1030
|
+
expect(bashPermission).toBe("deny");
|
|
1050
1031
|
|
|
1051
1032
|
const taskPermission = manager.getToolPermission("task", "reviewer");
|
|
1052
|
-
|
|
1033
|
+
expect(taskPermission).toBe("allow");
|
|
1053
1034
|
|
|
1054
1035
|
const readPermission = manager.getToolPermission("read", "reviewer");
|
|
1055
|
-
|
|
1036
|
+
expect(readPermission).toBe("deny");
|
|
1056
1037
|
|
|
1057
1038
|
const defaultBashPermission = manager.getToolPermission("bash");
|
|
1058
|
-
|
|
1039
|
+
expect(defaultBashPermission).toBe("ask");
|
|
1059
1040
|
|
|
1060
1041
|
const { manager: manager2, cleanup: cleanup2 } = createManager({
|
|
1061
1042
|
permission: { "*": "deny", bash: "allow" },
|
|
@@ -1063,7 +1044,7 @@ permission:
|
|
|
1063
1044
|
|
|
1064
1045
|
try {
|
|
1065
1046
|
const globalBashPermission = manager2.getToolPermission("bash");
|
|
1066
|
-
|
|
1047
|
+
expect(globalBashPermission).toBe("allow");
|
|
1067
1048
|
} finally {
|
|
1068
1049
|
cleanup2();
|
|
1069
1050
|
}
|
|
@@ -1079,12 +1060,12 @@ test("getToolPermission supports arbitrary extension tool names", () => {
|
|
|
1079
1060
|
|
|
1080
1061
|
try {
|
|
1081
1062
|
const explicitPermission = manager.getToolPermission("third_party_tool");
|
|
1082
|
-
|
|
1063
|
+
expect(explicitPermission).toBe("allow");
|
|
1083
1064
|
|
|
1084
1065
|
const fallbackPermission = manager.getToolPermission(
|
|
1085
1066
|
"missing_extension_tool",
|
|
1086
1067
|
);
|
|
1087
|
-
|
|
1068
|
+
expect(fallbackPermission).toBe("deny");
|
|
1088
1069
|
} finally {
|
|
1089
1070
|
cleanup();
|
|
1090
1071
|
}
|
|
@@ -1098,22 +1079,20 @@ test("Yolo mode bypasses delegated ask routing when no parent forwarding target
|
|
|
1098
1079
|
env: {},
|
|
1099
1080
|
});
|
|
1100
1081
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1082
|
+
expect(targetSessionId).toBe(null);
|
|
1083
|
+
expect(
|
|
1103
1084
|
canResolveAskPermissionRequest({
|
|
1104
1085
|
config: { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true },
|
|
1105
1086
|
hasUI: false,
|
|
1106
1087
|
isSubagent: true,
|
|
1107
1088
|
}),
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
assert.equal(
|
|
1089
|
+
).toBe(true);
|
|
1090
|
+
expect(
|
|
1111
1091
|
shouldAutoApprovePermissionState("ask", {
|
|
1112
1092
|
...DEFAULT_EXTENSION_CONFIG,
|
|
1113
1093
|
yoloMode: true,
|
|
1114
1094
|
}),
|
|
1115
|
-
|
|
1116
|
-
);
|
|
1095
|
+
).toBe(true);
|
|
1117
1096
|
});
|
|
1118
1097
|
|
|
1119
1098
|
test("Permission forwarding resolves the parent interactive session from subagent runtime env", () => {
|
|
@@ -1126,7 +1105,7 @@ test("Permission forwarding resolves the parent interactive session from subagen
|
|
|
1126
1105
|
},
|
|
1127
1106
|
});
|
|
1128
1107
|
|
|
1129
|
-
|
|
1108
|
+
expect(targetSessionId).toBe("parent-session");
|
|
1130
1109
|
});
|
|
1131
1110
|
|
|
1132
1111
|
test("Permission forwarding does not guess a target session when subagent runtime env is missing", () => {
|
|
@@ -1137,7 +1116,7 @@ test("Permission forwarding does not guess a target session when subagent runtim
|
|
|
1137
1116
|
env: {},
|
|
1138
1117
|
});
|
|
1139
1118
|
|
|
1140
|
-
|
|
1119
|
+
expect(targetSessionId).toBe(null);
|
|
1141
1120
|
});
|
|
1142
1121
|
|
|
1143
1122
|
test("Permission forwarding uses session-scoped directories per interactive session", () => {
|
|
@@ -1151,26 +1130,24 @@ test("Permission forwarding uses session-scoped directories per interactive sess
|
|
|
1151
1130
|
"session-b",
|
|
1152
1131
|
);
|
|
1153
1132
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1133
|
+
expect(sessionA.sessionRootDir).not.toBe(sessionB.sessionRootDir);
|
|
1134
|
+
expect(sessionA.requestsDir).not.toBe(sessionB.requestsDir);
|
|
1135
|
+
expect(sessionA.responsesDir).not.toBe(sessionB.responsesDir);
|
|
1157
1136
|
});
|
|
1158
1137
|
|
|
1159
1138
|
test("Permission forwarding request routing only matches the intended UI session", () => {
|
|
1160
|
-
|
|
1139
|
+
expect(
|
|
1161
1140
|
isForwardedPermissionRequestForSession(
|
|
1162
1141
|
{ targetSessionId: "session-a" },
|
|
1163
1142
|
"session-a",
|
|
1164
1143
|
),
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
assert.equal(
|
|
1144
|
+
).toBe(true);
|
|
1145
|
+
expect(
|
|
1168
1146
|
isForwardedPermissionRequestForSession(
|
|
1169
1147
|
{ targetSessionId: "session-a" },
|
|
1170
1148
|
"session-b",
|
|
1171
1149
|
),
|
|
1172
|
-
|
|
1173
|
-
);
|
|
1150
|
+
).toBe(false);
|
|
1174
1151
|
});
|
|
1175
1152
|
|
|
1176
1153
|
test("Permission forwarding rejects unresolved sentinel session ids", () => {
|
|
@@ -1180,7 +1157,7 @@ test("Permission forwarding rejects unresolved sentinel session ids", () => {
|
|
|
1180
1157
|
currentSessionId: "unknown",
|
|
1181
1158
|
});
|
|
1182
1159
|
|
|
1183
|
-
|
|
1160
|
+
expect(targetSessionId).toBe(null);
|
|
1184
1161
|
});
|
|
1185
1162
|
|
|
1186
1163
|
// ---------------------------------------------------------------------------
|
|
@@ -1268,14 +1245,14 @@ test("Project-level config overrides base bash patterns", () => {
|
|
|
1268
1245
|
const allowed = manager.checkPermission("bash", {
|
|
1269
1246
|
command: "rm -rf build",
|
|
1270
1247
|
});
|
|
1271
|
-
|
|
1272
|
-
|
|
1248
|
+
expect(allowed.state).toBe("allow");
|
|
1249
|
+
expect(allowed.matchedPattern).toBe("rm -rf build");
|
|
1273
1250
|
|
|
1274
1251
|
const denied = manager.checkPermission("bash", {
|
|
1275
1252
|
command: "rm -rf node_modules",
|
|
1276
1253
|
});
|
|
1277
|
-
|
|
1278
|
-
|
|
1254
|
+
expect(denied.state).toBe("deny");
|
|
1255
|
+
expect(denied.matchedPattern).toBe("rm -rf *");
|
|
1279
1256
|
} finally {
|
|
1280
1257
|
cleanup();
|
|
1281
1258
|
}
|
|
@@ -1308,16 +1285,16 @@ permission:
|
|
|
1308
1285
|
{ command: "git log --oneline" },
|
|
1309
1286
|
"reviewer",
|
|
1310
1287
|
);
|
|
1311
|
-
|
|
1312
|
-
|
|
1288
|
+
expect(allowed.state).toBe("allow");
|
|
1289
|
+
expect(allowed.matchedPattern).toBe("git log *");
|
|
1313
1290
|
|
|
1314
1291
|
const denied = manager.checkPermission(
|
|
1315
1292
|
"bash",
|
|
1316
1293
|
{ command: "git status" },
|
|
1317
1294
|
"reviewer",
|
|
1318
1295
|
);
|
|
1319
|
-
|
|
1320
|
-
|
|
1296
|
+
expect(denied.state).toBe("deny");
|
|
1297
|
+
expect(denied.matchedPattern).toBe("git *");
|
|
1321
1298
|
} finally {
|
|
1322
1299
|
cleanup();
|
|
1323
1300
|
}
|
|
@@ -1350,8 +1327,8 @@ permission:
|
|
|
1350
1327
|
|
|
1351
1328
|
try {
|
|
1352
1329
|
const result = manager.checkPermission("read", {}, "reviewer");
|
|
1353
|
-
|
|
1354
|
-
|
|
1330
|
+
expect(result.state).toBe("allow");
|
|
1331
|
+
expect(result.source).toBe("tool");
|
|
1355
1332
|
} finally {
|
|
1356
1333
|
cleanup();
|
|
1357
1334
|
}
|
|
@@ -1391,12 +1368,12 @@ permission:
|
|
|
1391
1368
|
{},
|
|
1392
1369
|
"reviewer",
|
|
1393
1370
|
);
|
|
1394
|
-
|
|
1395
|
-
|
|
1371
|
+
expect(reviewerResult.state).toBe("deny");
|
|
1372
|
+
expect(reviewerResult.source).toBe("default");
|
|
1396
1373
|
|
|
1397
1374
|
const globalResult = manager.checkPermission("custom_extension_tool", {});
|
|
1398
|
-
|
|
1399
|
-
|
|
1375
|
+
expect(globalResult.state).toBe("allow");
|
|
1376
|
+
expect(globalResult.source).toBe("default");
|
|
1400
1377
|
} finally {
|
|
1401
1378
|
cleanup();
|
|
1402
1379
|
}
|
|
@@ -1422,12 +1399,12 @@ permission:
|
|
|
1422
1399
|
|
|
1423
1400
|
try {
|
|
1424
1401
|
const agentResult = manager.checkPermission("read", {}, "reviewer");
|
|
1425
|
-
|
|
1426
|
-
|
|
1402
|
+
expect(agentResult.state).toBe("deny");
|
|
1403
|
+
expect(agentResult.source).toBe("tool");
|
|
1427
1404
|
|
|
1428
1405
|
const globalResult = manager.checkPermission("read", {});
|
|
1429
|
-
|
|
1430
|
-
|
|
1406
|
+
expect(globalResult.state).toBe("allow");
|
|
1407
|
+
expect(globalResult.source).toBe("tool");
|
|
1431
1408
|
} finally {
|
|
1432
1409
|
cleanup();
|
|
1433
1410
|
}
|
|
@@ -1454,10 +1431,10 @@ test("PermissionManager reads config from PI_CODING_AGENT_DIR when set", () => {
|
|
|
1454
1431
|
try {
|
|
1455
1432
|
const manager = new PermissionManager();
|
|
1456
1433
|
const result = manager.checkPermission("read", {});
|
|
1457
|
-
|
|
1434
|
+
expect(result.state).toBe("allow");
|
|
1458
1435
|
|
|
1459
1436
|
const result2 = manager.checkPermission("write", {});
|
|
1460
|
-
|
|
1437
|
+
expect(result2.state).toBe("deny");
|
|
1461
1438
|
} finally {
|
|
1462
1439
|
if (original !== undefined) {
|
|
1463
1440
|
process.env.PI_CODING_AGENT_DIR = original;
|
|
@@ -1495,9 +1472,9 @@ test("parseAllSkillPromptSections finds every available_skills block", () => {
|
|
|
1495
1472
|
|
|
1496
1473
|
const sections = parseAllSkillPromptSections(prompt);
|
|
1497
1474
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1475
|
+
expect(sections.length).toBe(2);
|
|
1476
|
+
expect(sections[0].entries[0]?.name).toBe("skill-one");
|
|
1477
|
+
expect(sections[1].entries[0]?.name).toBe("skill-two");
|
|
1501
1478
|
});
|
|
1502
1479
|
|
|
1503
1480
|
test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills block", () => {
|
|
@@ -1536,26 +1513,12 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
1536
1513
|
|
|
1537
1514
|
const result = resolveSkillPromptEntries(prompt, manager, null, "/cwd");
|
|
1538
1515
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
result.prompt.includes("visible-skill"),
|
|
1546
|
-
true,
|
|
1547
|
-
"Visible skill should remain in the prompt",
|
|
1548
|
-
);
|
|
1549
|
-
assert.equal(
|
|
1550
|
-
(result.prompt.match(/<available_skills>/g) || []).length,
|
|
1551
|
-
1,
|
|
1552
|
-
"Fully denied blocks should be removed",
|
|
1553
|
-
);
|
|
1554
|
-
assert.deepEqual(
|
|
1555
|
-
result.entries.map((entry) => entry.name),
|
|
1556
|
-
["visible-skill"],
|
|
1557
|
-
"Tracked skill entries should exclude denied skills",
|
|
1558
|
-
);
|
|
1516
|
+
expect(result.prompt).not.toContain("denied-skill");
|
|
1517
|
+
expect(result.prompt).toContain("visible-skill");
|
|
1518
|
+
expect((result.prompt.match(/<available_skills>/g) || []).length).toBe(1);
|
|
1519
|
+
expect(result.entries.map((entry) => entry.name)).toEqual([
|
|
1520
|
+
"visible-skill",
|
|
1521
|
+
]);
|
|
1559
1522
|
} finally {
|
|
1560
1523
|
cleanup();
|
|
1561
1524
|
}
|
|
@@ -1602,12 +1565,8 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
|
|
|
1602
1565
|
result.entries,
|
|
1603
1566
|
);
|
|
1604
1567
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
matchedBlockedSkill,
|
|
1608
|
-
null,
|
|
1609
|
-
"Denied skills should not remain in tracked entries",
|
|
1610
|
-
);
|
|
1568
|
+
expect(matchedVisibleSkill?.name).toBe("visible-skill");
|
|
1569
|
+
expect(matchedBlockedSkill).toBe(null);
|
|
1611
1570
|
} finally {
|
|
1612
1571
|
cleanup();
|
|
1613
1572
|
}
|
|
@@ -1623,9 +1582,9 @@ test("external_directory permission falls back to universal default when not exp
|
|
|
1623
1582
|
|
|
1624
1583
|
try {
|
|
1625
1584
|
const result = manager.checkPermission("external_directory", {});
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1585
|
+
expect(result.state).toBe("ask");
|
|
1586
|
+
expect(result.source).toBe("special");
|
|
1587
|
+
expect(result.matchedPattern).toBe(undefined);
|
|
1629
1588
|
} finally {
|
|
1630
1589
|
cleanup();
|
|
1631
1590
|
}
|
|
@@ -1638,9 +1597,9 @@ test("external_directory permission respects explicit deny", () => {
|
|
|
1638
1597
|
|
|
1639
1598
|
try {
|
|
1640
1599
|
const result = manager.checkPermission("external_directory", {});
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1600
|
+
expect(result.state).toBe("deny");
|
|
1601
|
+
expect(result.source).toBe("special");
|
|
1602
|
+
expect(result.matchedPattern).toBe("*");
|
|
1644
1603
|
} finally {
|
|
1645
1604
|
cleanup();
|
|
1646
1605
|
}
|
|
@@ -1653,9 +1612,9 @@ test("external_directory permission can be explicitly allowed", () => {
|
|
|
1653
1612
|
|
|
1654
1613
|
try {
|
|
1655
1614
|
const result = manager.checkPermission("external_directory", {});
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1615
|
+
expect(result.state).toBe("allow");
|
|
1616
|
+
expect(result.source).toBe("special");
|
|
1617
|
+
expect(result.matchedPattern).toBe("*");
|
|
1659
1618
|
} finally {
|
|
1660
1619
|
cleanup();
|
|
1661
1620
|
}
|
|
@@ -1679,7 +1638,7 @@ permission:
|
|
|
1679
1638
|
try {
|
|
1680
1639
|
// Global policy denies external_directory
|
|
1681
1640
|
const globalResult = manager.checkPermission("external_directory", {});
|
|
1682
|
-
|
|
1641
|
+
expect(globalResult.state).toBe("deny");
|
|
1683
1642
|
|
|
1684
1643
|
// Trusted agent overrides to allow
|
|
1685
1644
|
const agentResult = manager.checkPermission(
|
|
@@ -1687,8 +1646,8 @@ permission:
|
|
|
1687
1646
|
{},
|
|
1688
1647
|
"trusted",
|
|
1689
1648
|
);
|
|
1690
|
-
|
|
1691
|
-
|
|
1649
|
+
expect(agentResult.state).toBe("allow");
|
|
1650
|
+
expect(agentResult.source).toBe("special");
|
|
1692
1651
|
} finally {
|
|
1693
1652
|
cleanup();
|
|
1694
1653
|
}
|
|
@@ -1704,8 +1663,8 @@ test("external_directory permission is not affected by unrelated surface keys",
|
|
|
1704
1663
|
try {
|
|
1705
1664
|
// external_directory still resolves from its own entry
|
|
1706
1665
|
const extResult = manager.checkPermission("external_directory", {});
|
|
1707
|
-
|
|
1708
|
-
|
|
1666
|
+
expect(extResult.state).toBe("allow");
|
|
1667
|
+
expect(extResult.matchedPattern).toBe("*");
|
|
1709
1668
|
} finally {
|
|
1710
1669
|
cleanup();
|
|
1711
1670
|
}
|
|
@@ -1735,9 +1694,9 @@ permission:
|
|
|
1735
1694
|
{ name: "pi-code-review" },
|
|
1736
1695
|
"reviewer",
|
|
1737
1696
|
);
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1697
|
+
expect(allowed.state).toBe("allow");
|
|
1698
|
+
expect(allowed.matchedPattern).toBe("pi-*");
|
|
1699
|
+
expect(allowed.source).toBe("skill");
|
|
1741
1700
|
|
|
1742
1701
|
// Falls through to agent frontmatter catch-all
|
|
1743
1702
|
const asked = manager.checkPermission(
|
|
@@ -1745,13 +1704,13 @@ permission:
|
|
|
1745
1704
|
{ name: "other-skill" },
|
|
1746
1705
|
"reviewer",
|
|
1747
1706
|
);
|
|
1748
|
-
|
|
1749
|
-
|
|
1707
|
+
expect(asked.state).toBe("ask");
|
|
1708
|
+
expect(asked.matchedPattern).toBe("*");
|
|
1750
1709
|
|
|
1751
1710
|
// No agent override — global deny applies
|
|
1752
1711
|
const denied = manager.checkPermission("skill", { name: "pi-code-review" });
|
|
1753
|
-
|
|
1754
|
-
|
|
1712
|
+
expect(denied.state).toBe("deny");
|
|
1713
|
+
expect(denied.source).toBe("skill");
|
|
1755
1714
|
} finally {
|
|
1756
1715
|
cleanup();
|
|
1757
1716
|
}
|
|
@@ -1781,9 +1740,9 @@ permission:
|
|
|
1781
1740
|
{ path: `${homedir()}/Downloads/file.txt` },
|
|
1782
1741
|
"trusted",
|
|
1783
1742
|
);
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1743
|
+
expect(allowed.state).toBe("allow");
|
|
1744
|
+
expect(allowed.matchedPattern).toBe("~/Downloads/*");
|
|
1745
|
+
expect(allowed.source).toBe("special");
|
|
1787
1746
|
|
|
1788
1747
|
// Falls through to agent frontmatter catch-all deny
|
|
1789
1748
|
const denied = manager.checkPermission(
|
|
@@ -1791,13 +1750,13 @@ permission:
|
|
|
1791
1750
|
{ path: `${homedir()}/Documents/secret.txt` },
|
|
1792
1751
|
"trusted",
|
|
1793
1752
|
);
|
|
1794
|
-
|
|
1795
|
-
|
|
1753
|
+
expect(denied.state).toBe("deny");
|
|
1754
|
+
expect(denied.matchedPattern).toBe("*");
|
|
1796
1755
|
|
|
1797
1756
|
// No agent override — global deny applies
|
|
1798
1757
|
const globalDenied = manager.checkPermission("external_directory", {});
|
|
1799
|
-
|
|
1800
|
-
|
|
1758
|
+
expect(globalDenied.state).toBe("deny");
|
|
1759
|
+
expect(globalDenied.source).toBe("special");
|
|
1801
1760
|
} finally {
|
|
1802
1761
|
cleanup();
|
|
1803
1762
|
}
|
|
@@ -1838,8 +1797,8 @@ permission:
|
|
|
1838
1797
|
{ name: "pi-code-review" },
|
|
1839
1798
|
"analyst",
|
|
1840
1799
|
);
|
|
1841
|
-
|
|
1842
|
-
|
|
1800
|
+
expect(allowed.state).toBe("allow");
|
|
1801
|
+
expect(allowed.matchedPattern).toBe("pi-*");
|
|
1843
1802
|
|
|
1844
1803
|
// Project-agent *: deny wins over global-agent *: ask
|
|
1845
1804
|
const denied = manager.checkPermission(
|
|
@@ -1847,8 +1806,8 @@ permission:
|
|
|
1847
1806
|
{ name: "other-skill" },
|
|
1848
1807
|
"analyst",
|
|
1849
1808
|
);
|
|
1850
|
-
|
|
1851
|
-
|
|
1809
|
+
expect(denied.state).toBe("deny");
|
|
1810
|
+
expect(denied.matchedPattern).toBe("*");
|
|
1852
1811
|
} finally {
|
|
1853
1812
|
cleanup();
|
|
1854
1813
|
}
|
|
@@ -1882,12 +1841,12 @@ permission:
|
|
|
1882
1841
|
try {
|
|
1883
1842
|
// Project-agent allow wins over global-agent ask
|
|
1884
1843
|
const result = manager.checkPermission("external_directory", {}, "analyst");
|
|
1885
|
-
|
|
1886
|
-
|
|
1844
|
+
expect(result.state).toBe("allow");
|
|
1845
|
+
expect(result.source).toBe("special");
|
|
1887
1846
|
|
|
1888
1847
|
// Without agent context, global config deny applies
|
|
1889
1848
|
const globalResult = manager.checkPermission("external_directory", {});
|
|
1890
|
-
|
|
1849
|
+
expect(globalResult.state).toBe("deny");
|
|
1891
1850
|
} finally {
|
|
1892
1851
|
cleanup();
|
|
1893
1852
|
}
|
|
@@ -1914,12 +1873,11 @@ test("tool_call blocks path-bearing tools outside cwd when external_directory is
|
|
|
1914
1873
|
input: { path: siblingPath },
|
|
1915
1874
|
});
|
|
1916
1875
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
String(result.reason),
|
|
1876
|
+
expect(result.block).toBe(true);
|
|
1877
|
+
expect(String(result.reason)).toMatch(
|
|
1920
1878
|
/external directory permission denial/i,
|
|
1921
1879
|
);
|
|
1922
|
-
|
|
1880
|
+
expect(String(result.reason)).toMatch(/repo-sibling/);
|
|
1923
1881
|
} finally {
|
|
1924
1882
|
await harness.cleanup();
|
|
1925
1883
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -1941,8 +1899,8 @@ test("tool_call allows path-bearing tools inside cwd without external_directory
|
|
|
1941
1899
|
input: { path: join(harness.cwd, "src", "index.ts") },
|
|
1942
1900
|
});
|
|
1943
1901
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1902
|
+
expect(result).toEqual({});
|
|
1903
|
+
expect(harness.prompts).toEqual([]);
|
|
1946
1904
|
} finally {
|
|
1947
1905
|
await harness.cleanup();
|
|
1948
1906
|
}
|
|
@@ -1966,9 +1924,8 @@ test("tool_call blocks external_directory ask when no confirmation channel is av
|
|
|
1966
1924
|
},
|
|
1967
1925
|
});
|
|
1968
1926
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
String(result.reason),
|
|
1927
|
+
expect(result.block).toBe(true);
|
|
1928
|
+
expect(String(result.reason)).toMatch(
|
|
1972
1929
|
/requires approval, but no interactive UI is available/i,
|
|
1973
1930
|
);
|
|
1974
1931
|
} finally {
|
|
@@ -1996,11 +1953,11 @@ test("tool_call prompts for external_directory and then falls through to normal
|
|
|
1996
1953
|
{ hasUI: true, selectResponse: "Yes" },
|
|
1997
1954
|
);
|
|
1998
1955
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
1956
|
+
expect(result).toEqual({});
|
|
1957
|
+
expect(harness.prompts.length).toBe(1);
|
|
1958
|
+
expect(harness.prompts[0]).toMatch(/external directory access/i);
|
|
1959
|
+
expect(harness.prompts[0]).toMatch(/grep/);
|
|
1960
|
+
expect(harness.prompts[0]).toMatch(/external-search-root/);
|
|
2004
1961
|
} finally {
|
|
2005
1962
|
await harness.cleanup();
|
|
2006
1963
|
}
|
|
@@ -2021,8 +1978,8 @@ test("tool_call skips external_directory checks for optional path tools without
|
|
|
2021
1978
|
input: { pattern: "*.ts" },
|
|
2022
1979
|
});
|
|
2023
1980
|
|
|
2024
|
-
|
|
2025
|
-
|
|
1981
|
+
expect(result).toEqual({});
|
|
1982
|
+
expect(harness.prompts).toEqual([]);
|
|
2026
1983
|
} finally {
|
|
2027
1984
|
await harness.cleanup();
|
|
2028
1985
|
}
|
|
@@ -2045,12 +2002,11 @@ test("tool_call blocks bash command with external path when external_directory i
|
|
|
2045
2002
|
input: { command: "cat /etc/hosts" },
|
|
2046
2003
|
});
|
|
2047
2004
|
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
String(result.reason),
|
|
2005
|
+
expect(result.block).toBe(true);
|
|
2006
|
+
expect(String(result.reason)).toMatch(
|
|
2051
2007
|
/external directory permission denial/i,
|
|
2052
2008
|
);
|
|
2053
|
-
|
|
2009
|
+
expect(String(result.reason)).toMatch(/\/etc\/hosts/);
|
|
2054
2010
|
} finally {
|
|
2055
2011
|
await harness.cleanup();
|
|
2056
2012
|
}
|
|
@@ -2071,7 +2027,7 @@ test("tool_call allows bash command with only internal paths when external_direc
|
|
|
2071
2027
|
input: { command: "cat src/index.ts" },
|
|
2072
2028
|
});
|
|
2073
2029
|
|
|
2074
|
-
|
|
2030
|
+
expect(result).toEqual({});
|
|
2075
2031
|
} finally {
|
|
2076
2032
|
await harness.cleanup();
|
|
2077
2033
|
}
|
|
@@ -2093,9 +2049,8 @@ test("tool_call prompts for bash command with external path when external_direct
|
|
|
2093
2049
|
});
|
|
2094
2050
|
|
|
2095
2051
|
// No UI available in default harness, so it should block
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
String(result.reason),
|
|
2052
|
+
expect(result.block).toBe(true);
|
|
2053
|
+
expect(String(result.reason)).toMatch(
|
|
2099
2054
|
/requires approval.*no interactive UI/i,
|
|
2100
2055
|
);
|
|
2101
2056
|
} finally {
|
|
@@ -2119,7 +2074,7 @@ test("tool_call allows bash command with external path when external_directory i
|
|
|
2119
2074
|
});
|
|
2120
2075
|
|
|
2121
2076
|
// Should pass through to normal bash permission (which is also allow)
|
|
2122
|
-
|
|
2077
|
+
expect(result).toEqual({});
|
|
2123
2078
|
} finally {
|
|
2124
2079
|
await harness.cleanup();
|
|
2125
2080
|
}
|
|
@@ -2145,8 +2100,8 @@ test("tool_call applies bash pattern permissions after external_directory allow"
|
|
|
2145
2100
|
});
|
|
2146
2101
|
|
|
2147
2102
|
// external_directory allows, but bash pattern denies
|
|
2148
|
-
|
|
2149
|
-
|
|
2103
|
+
expect(result.block).toBe(true);
|
|
2104
|
+
expect(String(result.reason)).toMatch(/not permitted/i);
|
|
2150
2105
|
} finally {
|
|
2151
2106
|
await harness.cleanup();
|
|
2152
2107
|
}
|
|
@@ -2171,10 +2126,10 @@ test("generic ask prompts include serialized tool input for informed approval",
|
|
|
2171
2126
|
{ hasUI: true, selectResponse: "No" },
|
|
2172
2127
|
);
|
|
2173
2128
|
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2129
|
+
expect(result.block).toBe(true);
|
|
2130
|
+
expect(harness.prompts.length).toBe(1);
|
|
2131
|
+
expect(harness.prompts[0]).toMatch(/weather_lookup/);
|
|
2132
|
+
expect(harness.prompts[0]).toMatch(/\{"city":"Chicago","units":"metric"\}/);
|
|
2178
2133
|
} finally {
|
|
2179
2134
|
await harness.cleanup();
|
|
2180
2135
|
}
|
|
@@ -2203,14 +2158,14 @@ test("getResolvedPolicyPaths returns correct paths and existence when files exis
|
|
|
2203
2158
|
|
|
2204
2159
|
const result = pm.getResolvedPolicyPaths();
|
|
2205
2160
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2161
|
+
expect(result.globalConfigPath).toBe(globalConfigPath);
|
|
2162
|
+
expect(result.globalConfigExists).toBe(true);
|
|
2163
|
+
expect(result.projectConfigPath).toBe(projectConfigPath);
|
|
2164
|
+
expect(result.projectConfigExists).toBe(true);
|
|
2165
|
+
expect(result.agentsDir).toBe(agentsDir);
|
|
2166
|
+
expect(result.agentsDirExists).toBe(true);
|
|
2167
|
+
expect(result.projectAgentsDir).toBe(projectAgentsDir);
|
|
2168
|
+
expect(result.projectAgentsDirExists).toBe(true);
|
|
2214
2169
|
} finally {
|
|
2215
2170
|
rmSync(tempDir, { recursive: true, force: true });
|
|
2216
2171
|
}
|
|
@@ -2229,14 +2184,14 @@ test("getResolvedPolicyPaths returns false for missing files and null for absent
|
|
|
2229
2184
|
|
|
2230
2185
|
const result = pm.getResolvedPolicyPaths();
|
|
2231
2186
|
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2187
|
+
expect(result.globalConfigPath).toBe(globalConfigPath);
|
|
2188
|
+
expect(result.globalConfigExists).toBe(false);
|
|
2189
|
+
expect(result.projectConfigPath).toBe(null);
|
|
2190
|
+
expect(result.projectConfigExists).toBe(false);
|
|
2191
|
+
expect(result.agentsDir).toBe(agentsDir);
|
|
2192
|
+
expect(result.agentsDirExists).toBe(false);
|
|
2193
|
+
expect(result.projectAgentsDir).toBe(null);
|
|
2194
|
+
expect(result.projectAgentsDirExists).toBe(false);
|
|
2240
2195
|
} finally {
|
|
2241
2196
|
rmSync(tempDir, { recursive: true, force: true });
|
|
2242
2197
|
}
|
|
@@ -2251,7 +2206,7 @@ test("PermissionManager.getConfigIssues returns empty array for clean config", (
|
|
|
2251
2206
|
const { manager, cleanup } = createManager(config);
|
|
2252
2207
|
try {
|
|
2253
2208
|
const issues = manager.getConfigIssues();
|
|
2254
|
-
|
|
2209
|
+
expect(issues.length).toBe(0);
|
|
2255
2210
|
} finally {
|
|
2256
2211
|
cleanup();
|
|
2257
2212
|
}
|
|
@@ -2261,7 +2216,7 @@ test("PermissionManager.getConfigIssues returns empty array for empty config", (
|
|
|
2261
2216
|
const { manager, cleanup } = createManager({});
|
|
2262
2217
|
try {
|
|
2263
2218
|
const issues = manager.getConfigIssues();
|
|
2264
|
-
|
|
2219
|
+
expect(issues.length).toBe(0);
|
|
2265
2220
|
} finally {
|
|
2266
2221
|
cleanup();
|
|
2267
2222
|
}
|
|
@@ -2297,8 +2252,8 @@ test("session approval: first prompt with 'Yes, for this session' skips subseque
|
|
|
2297
2252
|
},
|
|
2298
2253
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2299
2254
|
);
|
|
2300
|
-
|
|
2301
|
-
|
|
2255
|
+
expect(result1).toEqual({});
|
|
2256
|
+
expect(harness.prompts.length).toBe(1);
|
|
2302
2257
|
|
|
2303
2258
|
// Second access under same prefix — should skip prompt
|
|
2304
2259
|
const result2 = await runToolCall(
|
|
@@ -2310,9 +2265,9 @@ test("session approval: first prompt with 'Yes, for this session' skips subseque
|
|
|
2310
2265
|
},
|
|
2311
2266
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2312
2267
|
);
|
|
2313
|
-
|
|
2268
|
+
expect(result2).toEqual({});
|
|
2314
2269
|
// No new prompt — still just the original one
|
|
2315
|
-
|
|
2270
|
+
expect(harness.prompts.length).toBe(1);
|
|
2316
2271
|
|
|
2317
2272
|
// Third access with different tool under same prefix — also skipped
|
|
2318
2273
|
const result3 = await runToolCall(
|
|
@@ -2324,8 +2279,8 @@ test("session approval: first prompt with 'Yes, for this session' skips subseque
|
|
|
2324
2279
|
},
|
|
2325
2280
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2326
2281
|
);
|
|
2327
|
-
|
|
2328
|
-
|
|
2282
|
+
expect(result3).toEqual({});
|
|
2283
|
+
expect(harness.prompts.length).toBe(1);
|
|
2329
2284
|
} finally {
|
|
2330
2285
|
await harness.cleanup();
|
|
2331
2286
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -2360,7 +2315,7 @@ test("session approval: different directory prefix still prompts", async () => {
|
|
|
2360
2315
|
},
|
|
2361
2316
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2362
2317
|
);
|
|
2363
|
-
|
|
2318
|
+
expect(harness.prompts.length).toBe(1);
|
|
2364
2319
|
|
|
2365
2320
|
// Access sibling-b — different prefix, should prompt again
|
|
2366
2321
|
await runToolCall(
|
|
@@ -2372,7 +2327,7 @@ test("session approval: different directory prefix still prompts", async () => {
|
|
|
2372
2327
|
},
|
|
2373
2328
|
{ hasUI: true, selectResponse: "Yes" },
|
|
2374
2329
|
);
|
|
2375
|
-
|
|
2330
|
+
expect(harness.prompts.length).toBe(2);
|
|
2376
2331
|
} finally {
|
|
2377
2332
|
await harness.cleanup();
|
|
2378
2333
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -2405,7 +2360,7 @@ test("session approval: session_shutdown clears session approvals", async () =>
|
|
|
2405
2360
|
},
|
|
2406
2361
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2407
2362
|
);
|
|
2408
|
-
|
|
2363
|
+
expect(harness.prompts.length).toBe(1);
|
|
2409
2364
|
|
|
2410
2365
|
// Trigger session_shutdown (clears cache)
|
|
2411
2366
|
const shutdownCtx = createMockContext(cwd, harness.prompts, {
|
|
@@ -2424,8 +2379,8 @@ test("session approval: session_shutdown clears session approvals", async () =>
|
|
|
2424
2379
|
},
|
|
2425
2380
|
{ hasUI: true, selectResponse: "Yes" },
|
|
2426
2381
|
);
|
|
2427
|
-
|
|
2428
|
-
|
|
2382
|
+
expect(result).toEqual({});
|
|
2383
|
+
expect(harness.prompts.length).toBe(2);
|
|
2429
2384
|
} finally {
|
|
2430
2385
|
await harness.cleanup();
|
|
2431
2386
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -2457,8 +2412,8 @@ test("session approval: bash external directory with 'Yes, for this session' ski
|
|
|
2457
2412
|
},
|
|
2458
2413
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2459
2414
|
);
|
|
2460
|
-
|
|
2461
|
-
|
|
2415
|
+
expect(result1).toEqual({});
|
|
2416
|
+
expect(harness.prompts.length).toBe(1);
|
|
2462
2417
|
|
|
2463
2418
|
// Second bash command referencing path under same prefix — skips prompt
|
|
2464
2419
|
const result2 = await runToolCall(
|
|
@@ -2470,8 +2425,8 @@ test("session approval: bash external directory with 'Yes, for this session' ski
|
|
|
2470
2425
|
},
|
|
2471
2426
|
{ hasUI: true, selectResponse: "Yes, for this session" },
|
|
2472
2427
|
);
|
|
2473
|
-
|
|
2474
|
-
|
|
2428
|
+
expect(result2).toEqual({});
|
|
2429
|
+
expect(harness.prompts.length).toBe(1);
|
|
2475
2430
|
} finally {
|
|
2476
2431
|
await harness.cleanup();
|
|
2477
2432
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -2504,7 +2459,7 @@ test("session approval: regular 'Yes' does not create session approval", async (
|
|
|
2504
2459
|
},
|
|
2505
2460
|
{ hasUI: true, selectResponse: "Yes" },
|
|
2506
2461
|
);
|
|
2507
|
-
|
|
2462
|
+
expect(harness.prompts.length).toBe(1);
|
|
2508
2463
|
|
|
2509
2464
|
// Same prefix — should still prompt since we used "Yes" not session
|
|
2510
2465
|
await runToolCall(
|
|
@@ -2516,7 +2471,7 @@ test("session approval: regular 'Yes' does not create session approval", async (
|
|
|
2516
2471
|
},
|
|
2517
2472
|
{ hasUI: true, selectResponse: "Yes" },
|
|
2518
2473
|
);
|
|
2519
|
-
|
|
2474
|
+
expect(harness.prompts.length).toBe(2);
|
|
2520
2475
|
} finally {
|
|
2521
2476
|
await harness.cleanup();
|
|
2522
2477
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -2549,9 +2504,9 @@ test("checkPermission returns source 'session' when session rules cover the exte
|
|
|
2549
2504
|
undefined,
|
|
2550
2505
|
sessionRules,
|
|
2551
2506
|
);
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2507
|
+
expect(result.state).toBe("allow");
|
|
2508
|
+
expect(result.source).toBe("session");
|
|
2509
|
+
expect(result.matchedPattern).toBe("/other/project/*");
|
|
2555
2510
|
} finally {
|
|
2556
2511
|
cleanup();
|
|
2557
2512
|
}
|
|
@@ -2580,8 +2535,8 @@ test("checkPermission falls back to config policy when session rules do not cove
|
|
|
2580
2535
|
undefined,
|
|
2581
2536
|
sessionRules,
|
|
2582
2537
|
);
|
|
2583
|
-
|
|
2584
|
-
|
|
2538
|
+
expect(result.state).toBe("deny");
|
|
2539
|
+
expect(result.source).toBe("special");
|
|
2585
2540
|
} finally {
|
|
2586
2541
|
cleanup();
|
|
2587
2542
|
}
|
|
@@ -2609,8 +2564,8 @@ test("checkPermission with empty session rules is identical to call without sess
|
|
|
2609
2564
|
source: "special",
|
|
2610
2565
|
origin: "global",
|
|
2611
2566
|
};
|
|
2612
|
-
|
|
2613
|
-
|
|
2567
|
+
expect(withEmpty).toEqual(expected);
|
|
2568
|
+
expect(withoutArg).toEqual(expected);
|
|
2614
2569
|
} finally {
|
|
2615
2570
|
cleanup();
|
|
2616
2571
|
}
|
|
@@ -2640,8 +2595,8 @@ test("session rules for one surface do not affect checks on other surfaces", ()
|
|
|
2640
2595
|
undefined,
|
|
2641
2596
|
sessionRules,
|
|
2642
2597
|
);
|
|
2643
|
-
|
|
2644
|
-
|
|
2598
|
+
expect(bashResult.state).toBe("ask");
|
|
2599
|
+
expect(bashResult.source).toBe("bash");
|
|
2645
2600
|
|
|
2646
2601
|
// MCP check — session rules should not affect MCP decisions.
|
|
2647
2602
|
const mcpResult = manager.checkPermission(
|
|
@@ -2650,8 +2605,8 @@ test("session rules for one surface do not affect checks on other surfaces", ()
|
|
|
2650
2605
|
undefined,
|
|
2651
2606
|
sessionRules,
|
|
2652
2607
|
);
|
|
2653
|
-
|
|
2654
|
-
|
|
2608
|
+
expect(mcpResult.state).toBe("ask");
|
|
2609
|
+
expect(mcpResult.source).toBe("default");
|
|
2655
2610
|
} finally {
|
|
2656
2611
|
cleanup();
|
|
2657
2612
|
}
|
|
@@ -2680,8 +2635,8 @@ test("session rules override config deny for external_directory", () => {
|
|
|
2680
2635
|
undefined,
|
|
2681
2636
|
sessionRules,
|
|
2682
2637
|
);
|
|
2683
|
-
|
|
2684
|
-
|
|
2638
|
+
expect(result.state).toBe("allow");
|
|
2639
|
+
expect(result.source).toBe("session");
|
|
2685
2640
|
} finally {
|
|
2686
2641
|
cleanup();
|
|
2687
2642
|
}
|
|
@@ -2709,9 +2664,9 @@ test("checkPermission returns source 'session' for bash when session rules match
|
|
|
2709
2664
|
undefined,
|
|
2710
2665
|
sessionRules,
|
|
2711
2666
|
);
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2667
|
+
expect(result.state).toBe("allow");
|
|
2668
|
+
expect(result.source).toBe("session");
|
|
2669
|
+
expect(result.matchedPattern).toBe("git *");
|
|
2715
2670
|
} finally {
|
|
2716
2671
|
cleanup();
|
|
2717
2672
|
}
|
|
@@ -2737,8 +2692,8 @@ test("checkPermission returns source 'session' for bash when session rule is exa
|
|
|
2737
2692
|
undefined,
|
|
2738
2693
|
sessionRules,
|
|
2739
2694
|
);
|
|
2740
|
-
|
|
2741
|
-
|
|
2695
|
+
expect(result.state).toBe("allow");
|
|
2696
|
+
expect(result.source).toBe("session");
|
|
2742
2697
|
} finally {
|
|
2743
2698
|
cleanup();
|
|
2744
2699
|
}
|
|
@@ -2764,8 +2719,8 @@ test("checkPermission falls back to config for bash when session rules do not ma
|
|
|
2764
2719
|
undefined,
|
|
2765
2720
|
sessionRules,
|
|
2766
2721
|
);
|
|
2767
|
-
|
|
2768
|
-
|
|
2722
|
+
expect(result.state).toBe("deny");
|
|
2723
|
+
expect(result.source).toBe("bash");
|
|
2769
2724
|
} finally {
|
|
2770
2725
|
cleanup();
|
|
2771
2726
|
}
|
|
@@ -2791,8 +2746,8 @@ test("checkPermission returns source 'session' for mcp when session rules match
|
|
|
2791
2746
|
undefined,
|
|
2792
2747
|
sessionRules,
|
|
2793
2748
|
);
|
|
2794
|
-
|
|
2795
|
-
|
|
2749
|
+
expect(result.state).toBe("allow");
|
|
2750
|
+
expect(result.source).toBe("session");
|
|
2796
2751
|
} finally {
|
|
2797
2752
|
cleanup();
|
|
2798
2753
|
}
|
|
@@ -2818,9 +2773,9 @@ test("checkPermission returns source 'session' for skill when session rules matc
|
|
|
2818
2773
|
undefined,
|
|
2819
2774
|
sessionRules,
|
|
2820
2775
|
);
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2776
|
+
expect(result.state).toBe("allow");
|
|
2777
|
+
expect(result.source).toBe("session");
|
|
2778
|
+
expect(result.matchedPattern).toBe("librarian");
|
|
2824
2779
|
} finally {
|
|
2825
2780
|
cleanup();
|
|
2826
2781
|
}
|
|
@@ -2841,8 +2796,8 @@ test("checkPermission returns source 'session' for tool surface when session rul
|
|
|
2841
2796
|
];
|
|
2842
2797
|
|
|
2843
2798
|
const result = manager.checkPermission("read", {}, undefined, sessionRules);
|
|
2844
|
-
|
|
2845
|
-
|
|
2799
|
+
expect(result.state).toBe("allow");
|
|
2800
|
+
expect(result.source).toBe("session");
|
|
2846
2801
|
} finally {
|
|
2847
2802
|
cleanup();
|
|
2848
2803
|
}
|
|
@@ -2869,7 +2824,7 @@ test("bash session rules do not bleed into mcp checks", () => {
|
|
|
2869
2824
|
sessionRules,
|
|
2870
2825
|
);
|
|
2871
2826
|
// bash session rule must not affect mcp surface
|
|
2872
|
-
|
|
2827
|
+
expect(result.source).not.toBe("session");
|
|
2873
2828
|
} finally {
|
|
2874
2829
|
cleanup();
|
|
2875
2830
|
}
|