@google/gemini-cli-core 0.33.0-preview.4 → 0.33.0-preview.5
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/dist/google-gemini-cli-core-0.33.0-preview.4.tgz +0 -0
- package/dist/src/agents/local-executor.test.js +2 -2
- package/dist/src/agents/local-executor.test.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.test.js +9 -10
- package/dist/src/core/loggingContentGenerator.test.js.map +1 -1
- package/dist/src/core/prompts.test.js +4 -4
- package/dist/src/core/prompts.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/policy/config.js +12 -4
- package/dist/src/policy/config.js.map +1 -1
- package/dist/src/policy/config.test.js +17 -23
- package/dist/src/policy/config.test.js.map +1 -1
- package/dist/src/policy/policy-engine.js +80 -161
- package/dist/src/policy/policy-engine.js.map +1 -1
- package/dist/src/policy/policy-engine.test.js +180 -110
- package/dist/src/policy/policy-engine.test.js.map +1 -1
- package/dist/src/policy/toml-loader.d.ts +1 -0
- package/dist/src/policy/toml-loader.js +26 -19
- package/dist/src/policy/toml-loader.js.map +1 -1
- package/dist/src/policy/toml-loader.test.js +36 -18
- package/dist/src/policy/toml-loader.test.js.map +1 -1
- package/dist/src/policy/types.d.ts +10 -0
- package/dist/src/policy/types.js.map +1 -1
- package/dist/src/prompts/promptProvider.test.js +1 -1
- package/dist/src/prompts/promptProvider.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.d.ts +38 -4
- package/dist/src/tools/mcp-tool.js +72 -12
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +34 -17
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/tool-registry.js +9 -7
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tool-registry.test.js +27 -30
- package/dist/src/tools/tool-registry.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/google-gemini-cli-core-0.33.0-preview.3.tgz +0 -0
|
@@ -101,19 +101,15 @@ describe('PolicyEngine', () => {
|
|
|
101
101
|
it('should match unqualified tool names with qualified rules when serverName is provided', async () => {
|
|
102
102
|
const rules = [
|
|
103
103
|
{
|
|
104
|
-
toolName: '
|
|
104
|
+
toolName: 'mcp_my-server_tool',
|
|
105
|
+
mcpName: 'my-server',
|
|
105
106
|
decision: PolicyDecision.ALLOW,
|
|
106
107
|
},
|
|
107
108
|
];
|
|
108
109
|
engine = new PolicyEngine({ rules });
|
|
109
110
|
// Match with qualified name (standard)
|
|
110
|
-
expect((await engine.check({ name: '
|
|
111
|
-
|
|
112
|
-
expect((await engine.check({ name: 'tool' }, 'my-server')).decision).toBe(PolicyDecision.ALLOW);
|
|
113
|
-
// Should NOT match with unqualified name but NO serverName
|
|
114
|
-
expect((await engine.check({ name: 'tool' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
|
|
115
|
-
// Should NOT match with unqualified name but WRONG serverName
|
|
116
|
-
expect((await engine.check({ name: 'tool' }, 'wrong-server')).decision).toBe(PolicyDecision.ASK_USER);
|
|
111
|
+
expect((await engine.check({ name: 'mcp_my-server_tool' }, 'my-server'))
|
|
112
|
+
.decision).toBe(PolicyDecision.ALLOW);
|
|
117
113
|
});
|
|
118
114
|
it('should match by args pattern', async () => {
|
|
119
115
|
const rules = [
|
|
@@ -328,58 +324,45 @@ describe('PolicyEngine', () => {
|
|
|
328
324
|
],
|
|
329
325
|
});
|
|
330
326
|
expect((await engine.check({ name: 'read_file' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
|
|
331
|
-
expect((await engine.check({ name: '
|
|
327
|
+
expect((await engine.check({ name: 'mcp_my-server_tool' }, 'my-server'))
|
|
328
|
+
.decision).toBe(PolicyDecision.ALLOW);
|
|
332
329
|
});
|
|
333
|
-
it('should match any MCP tool when toolName is *
|
|
330
|
+
it('should match any MCP tool when toolName is mcp_*', async () => {
|
|
334
331
|
engine = new PolicyEngine({
|
|
335
332
|
rules: [
|
|
336
|
-
{ toolName: '*
|
|
333
|
+
{ toolName: 'mcp_*', decision: PolicyDecision.ALLOW, priority: 10 },
|
|
337
334
|
],
|
|
338
335
|
defaultDecision: PolicyDecision.DENY,
|
|
339
336
|
});
|
|
340
|
-
expect((await engine.check({ name: '
|
|
341
|
-
expect((await engine.check({ name: '
|
|
337
|
+
expect((await engine.check({ name: 'mcp_mcp_tool' }, 'mcp')).decision).toBe(PolicyDecision.ALLOW);
|
|
338
|
+
expect((await engine.check({ name: 'mcp_other_tool' }, 'other')).decision).toBe(PolicyDecision.ALLOW);
|
|
342
339
|
expect((await engine.check({ name: 'read_file' }, undefined)).decision).toBe(PolicyDecision.DENY);
|
|
343
340
|
});
|
|
344
|
-
it('should match specific tool across all servers when using *__tool', async () => {
|
|
345
|
-
engine = new PolicyEngine({
|
|
346
|
-
rules: [
|
|
347
|
-
{
|
|
348
|
-
toolName: '*__search',
|
|
349
|
-
decision: PolicyDecision.ALLOW,
|
|
350
|
-
priority: 10,
|
|
351
|
-
},
|
|
352
|
-
],
|
|
353
|
-
defaultDecision: PolicyDecision.DENY,
|
|
354
|
-
});
|
|
355
|
-
expect((await engine.check({ name: 'ws__search' }, 'ws')).decision).toBe(PolicyDecision.ALLOW);
|
|
356
|
-
expect((await engine.check({ name: 'gh__search' }, 'gh')).decision).toBe(PolicyDecision.ALLOW);
|
|
357
|
-
expect((await engine.check({ name: 'gh__list' }, 'gh')).decision).toBe(PolicyDecision.DENY);
|
|
358
|
-
});
|
|
359
341
|
it('should match MCP server wildcard patterns', async () => {
|
|
360
342
|
const rules = [
|
|
361
343
|
{
|
|
362
|
-
toolName: '
|
|
344
|
+
toolName: 'mcp_my-server_*',
|
|
345
|
+
mcpName: 'my-server',
|
|
363
346
|
decision: PolicyDecision.ALLOW,
|
|
364
347
|
priority: 10,
|
|
365
348
|
},
|
|
366
349
|
{
|
|
367
|
-
toolName: '
|
|
350
|
+
toolName: 'mcp_blocked-server_*',
|
|
351
|
+
mcpName: 'blocked-server',
|
|
368
352
|
decision: PolicyDecision.DENY,
|
|
369
353
|
priority: 20,
|
|
370
354
|
},
|
|
371
355
|
];
|
|
372
356
|
engine = new PolicyEngine({ rules });
|
|
373
357
|
// Should match my-server tools
|
|
374
|
-
expect((await engine.check({ name: '
|
|
375
|
-
.decision).toBe(PolicyDecision.ALLOW);
|
|
376
|
-
expect((await engine.check({ name: 'my-server__another_tool' }, 'my-server'))
|
|
358
|
+
expect((await engine.check({ name: 'mcp_my-server_tool1' }, 'my-server'))
|
|
377
359
|
.decision).toBe(PolicyDecision.ALLOW);
|
|
360
|
+
expect((await engine.check({ name: 'mcp_my-server_another_tool' }, 'my-server')).decision).toBe(PolicyDecision.ALLOW);
|
|
378
361
|
// Should match blocked-server tools
|
|
379
|
-
expect((await engine.check({ name: '
|
|
380
|
-
expect((await engine.check({ name: '
|
|
362
|
+
expect((await engine.check({ name: 'mcp_blocked-server_tool1' }, 'blocked-server')).decision).toBe(PolicyDecision.DENY);
|
|
363
|
+
expect((await engine.check({ name: 'mcp_blocked-server_dangerous' }, 'blocked-server')).decision).toBe(PolicyDecision.DENY);
|
|
381
364
|
// Should not match other patterns
|
|
382
|
-
expect((await engine.check({ name: '
|
|
365
|
+
expect((await engine.check({ name: 'mcp_other-server_tool' }, 'other-server'))
|
|
383
366
|
.decision).toBe(PolicyDecision.ASK_USER);
|
|
384
367
|
expect((await engine.check({ name: 'my-server-tool' }, undefined)).decision).toBe(PolicyDecision.ASK_USER); // No __ separator
|
|
385
368
|
expect((await engine.check({ name: 'my-server' }, undefined)).decision).toBe(PolicyDecision.ASK_USER); // No tool name
|
|
@@ -387,61 +370,65 @@ describe('PolicyEngine', () => {
|
|
|
387
370
|
it('should prioritize specific tool rules over server wildcards', async () => {
|
|
388
371
|
const rules = [
|
|
389
372
|
{
|
|
390
|
-
toolName: '
|
|
373
|
+
toolName: 'mcp_my-server_*',
|
|
374
|
+
mcpName: 'my-server',
|
|
391
375
|
decision: PolicyDecision.ALLOW,
|
|
392
376
|
priority: 10,
|
|
393
377
|
},
|
|
394
378
|
{
|
|
395
|
-
toolName: '
|
|
379
|
+
toolName: 'mcp_my-server_dangerous-tool',
|
|
380
|
+
mcpName: 'my-server',
|
|
396
381
|
decision: PolicyDecision.DENY,
|
|
397
382
|
priority: 20,
|
|
398
383
|
},
|
|
399
384
|
];
|
|
400
385
|
engine = new PolicyEngine({ rules });
|
|
401
386
|
// Specific tool deny should override server allow
|
|
402
|
-
expect((await engine.check({ name: '
|
|
403
|
-
|
|
404
|
-
expect((await engine.check({ name: 'my-server__safe-tool' }, 'my-server'))
|
|
387
|
+
expect((await engine.check({ name: 'mcp_my-server_dangerous-tool' }, 'my-server')).decision).toBe(PolicyDecision.DENY);
|
|
388
|
+
expect((await engine.check({ name: 'mcp_my-server_safe-tool' }, 'my-server'))
|
|
405
389
|
.decision).toBe(PolicyDecision.ALLOW);
|
|
406
390
|
});
|
|
407
391
|
it('should NOT match spoofed server names when using wildcards', async () => {
|
|
408
|
-
// Vulnerability: A rule for '
|
|
409
|
-
// effectively allowing a server named '
|
|
392
|
+
// Vulnerability: A rule for 'mcp_prefix_*' matches 'mcp_prefix__suffix_tool'
|
|
393
|
+
// effectively allowing a server named 'mcp_prefix_suffix' to spoof 'prefix'.
|
|
410
394
|
const rules = [
|
|
411
395
|
{
|
|
412
|
-
toolName: '
|
|
396
|
+
toolName: 'mcp_safe_server_*',
|
|
397
|
+
mcpName: 'safe_server',
|
|
413
398
|
decision: PolicyDecision.ALLOW,
|
|
414
399
|
},
|
|
415
400
|
];
|
|
416
401
|
engine = new PolicyEngine({ rules });
|
|
417
|
-
// A tool from a different server '
|
|
418
|
-
const spoofedToolCall = { name: '
|
|
402
|
+
// A tool from a different server 'mcp_safe_server_malicious'
|
|
403
|
+
const spoofedToolCall = { name: 'mcp_mcp_safe_server_malicious_tool' };
|
|
419
404
|
// CURRENT BEHAVIOR (FIXED): Matches because it starts with 'safe_server__' BUT serverName doesn't match 'safe_server'
|
|
420
405
|
// We expect this to FAIL matching the ALLOW rule, thus falling back to default (ASK_USER)
|
|
421
|
-
expect((await engine.check(spoofedToolCall, '
|
|
406
|
+
expect((await engine.check(spoofedToolCall, 'mcp_safe_server_malicious'))
|
|
422
407
|
.decision).toBe(PolicyDecision.ASK_USER);
|
|
423
408
|
});
|
|
424
409
|
it('should verify tool name prefix even if serverName matches', async () => {
|
|
425
410
|
const rules = [
|
|
426
411
|
{
|
|
427
|
-
toolName: '
|
|
412
|
+
toolName: 'mcp_safe_server_*',
|
|
413
|
+
mcpName: 'safe_server',
|
|
428
414
|
decision: PolicyDecision.ALLOW,
|
|
429
415
|
},
|
|
430
416
|
];
|
|
431
417
|
engine = new PolicyEngine({ rules });
|
|
432
418
|
// serverName matches, but tool name does not start with prefix
|
|
433
|
-
const invalidToolCall = { name: '
|
|
419
|
+
const invalidToolCall = { name: 'mcp_other_server_tool' };
|
|
434
420
|
expect((await engine.check(invalidToolCall, 'safe_server')).decision).toBe(PolicyDecision.ASK_USER);
|
|
435
421
|
});
|
|
436
422
|
it('should allow when both serverName and tool name prefix match', async () => {
|
|
437
423
|
const rules = [
|
|
438
424
|
{
|
|
439
|
-
toolName: '
|
|
425
|
+
toolName: 'mcp_safe_server_*',
|
|
426
|
+
mcpName: 'safe_server',
|
|
440
427
|
decision: PolicyDecision.ALLOW,
|
|
441
428
|
},
|
|
442
429
|
];
|
|
443
430
|
engine = new PolicyEngine({ rules });
|
|
444
|
-
const validToolCall = { name: '
|
|
431
|
+
const validToolCall = { name: 'mcp_safe_server_tool' };
|
|
445
432
|
expect((await engine.check(validToolCall, 'safe_server')).decision).toBe(PolicyDecision.ALLOW);
|
|
446
433
|
});
|
|
447
434
|
});
|
|
@@ -1409,17 +1396,22 @@ describe('PolicyEngine', () => {
|
|
|
1409
1396
|
});
|
|
1410
1397
|
it('should support wildcard patterns for checkers', async () => {
|
|
1411
1398
|
const rules = [
|
|
1412
|
-
{
|
|
1399
|
+
{
|
|
1400
|
+
toolName: 'mcp_server_tool',
|
|
1401
|
+
mcpName: 'server',
|
|
1402
|
+
decision: PolicyDecision.ALLOW,
|
|
1403
|
+
},
|
|
1413
1404
|
];
|
|
1414
1405
|
const wildcardChecker = {
|
|
1415
1406
|
checker: { type: 'external', name: 'wildcard' },
|
|
1416
|
-
toolName: '
|
|
1407
|
+
toolName: 'mcp_server_*',
|
|
1408
|
+
mcpName: 'server',
|
|
1417
1409
|
};
|
|
1418
1410
|
engine = new PolicyEngine({ rules, checkers: [wildcardChecker] }, mockCheckerRunner);
|
|
1419
1411
|
vi.mocked(mockCheckerRunner.runChecker).mockResolvedValue({
|
|
1420
1412
|
decision: SafetyCheckDecision.ALLOW,
|
|
1421
1413
|
});
|
|
1422
|
-
await engine.check({ name: '
|
|
1414
|
+
await engine.check({ name: 'mcp_server_tool' }, 'server');
|
|
1423
1415
|
expect(mockCheckerRunner.runChecker).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ name: 'wildcard' }));
|
|
1424
1416
|
});
|
|
1425
1417
|
it('should run safety checkers when decision is ASK_USER and downgrade to DENY on failure', async () => {
|
|
@@ -1509,11 +1501,13 @@ describe('PolicyEngine', () => {
|
|
|
1509
1501
|
{
|
|
1510
1502
|
name: 'should return empty set when no rules provided',
|
|
1511
1503
|
rules: [],
|
|
1504
|
+
allToolNames: ['tool1'],
|
|
1512
1505
|
expected: [],
|
|
1513
1506
|
},
|
|
1514
1507
|
{
|
|
1515
1508
|
name: 'should apply rules without explicit modes to all modes',
|
|
1516
1509
|
rules: [{ toolName: 'tool1', decision: PolicyDecision.DENY }],
|
|
1510
|
+
allToolNames: ['tool1', 'tool2'],
|
|
1517
1511
|
expected: ['tool1'],
|
|
1518
1512
|
},
|
|
1519
1513
|
{
|
|
@@ -1533,6 +1527,7 @@ describe('PolicyEngine', () => {
|
|
|
1533
1527
|
modes: [ApprovalMode.DEFAULT],
|
|
1534
1528
|
},
|
|
1535
1529
|
],
|
|
1530
|
+
allToolNames: ['tool1'],
|
|
1536
1531
|
expected: [],
|
|
1537
1532
|
},
|
|
1538
1533
|
{
|
|
@@ -1549,6 +1544,7 @@ describe('PolicyEngine', () => {
|
|
|
1549
1544
|
modes: [ApprovalMode.DEFAULT],
|
|
1550
1545
|
},
|
|
1551
1546
|
],
|
|
1547
|
+
allToolNames: ['tool1', 'tool2', 'tool3'],
|
|
1552
1548
|
expected: ['tool1'],
|
|
1553
1549
|
},
|
|
1554
1550
|
{
|
|
@@ -1567,6 +1563,7 @@ describe('PolicyEngine', () => {
|
|
|
1567
1563
|
modes: [ApprovalMode.DEFAULT],
|
|
1568
1564
|
},
|
|
1569
1565
|
],
|
|
1566
|
+
allToolNames: ['tool1'],
|
|
1570
1567
|
expected: ['tool1'],
|
|
1571
1568
|
},
|
|
1572
1569
|
{
|
|
@@ -1585,6 +1582,7 @@ describe('PolicyEngine', () => {
|
|
|
1585
1582
|
modes: [ApprovalMode.DEFAULT],
|
|
1586
1583
|
},
|
|
1587
1584
|
],
|
|
1585
|
+
allToolNames: ['tool1'],
|
|
1588
1586
|
expected: [],
|
|
1589
1587
|
},
|
|
1590
1588
|
{
|
|
@@ -1597,7 +1595,8 @@ describe('PolicyEngine', () => {
|
|
|
1597
1595
|
},
|
|
1598
1596
|
],
|
|
1599
1597
|
nonInteractive: true,
|
|
1600
|
-
|
|
1598
|
+
allToolNames: ['tool1'],
|
|
1599
|
+
expected: ['tool1'],
|
|
1601
1600
|
},
|
|
1602
1601
|
{
|
|
1603
1602
|
name: 'should ignore rules with argsPattern',
|
|
@@ -1609,6 +1608,7 @@ describe('PolicyEngine', () => {
|
|
|
1609
1608
|
modes: [ApprovalMode.DEFAULT],
|
|
1610
1609
|
},
|
|
1611
1610
|
],
|
|
1611
|
+
allToolNames: ['tool1'],
|
|
1612
1612
|
expected: [],
|
|
1613
1613
|
},
|
|
1614
1614
|
{
|
|
@@ -1621,6 +1621,7 @@ describe('PolicyEngine', () => {
|
|
|
1621
1621
|
},
|
|
1622
1622
|
],
|
|
1623
1623
|
approvalMode: ApprovalMode.PLAN,
|
|
1624
|
+
allToolNames: ['tool1'],
|
|
1624
1625
|
expected: ['tool1'],
|
|
1625
1626
|
},
|
|
1626
1627
|
{
|
|
@@ -1633,6 +1634,7 @@ describe('PolicyEngine', () => {
|
|
|
1633
1634
|
},
|
|
1634
1635
|
],
|
|
1635
1636
|
approvalMode: ApprovalMode.DEFAULT,
|
|
1637
|
+
allToolNames: ['tool1'],
|
|
1636
1638
|
expected: [],
|
|
1637
1639
|
},
|
|
1638
1640
|
{
|
|
@@ -1651,36 +1653,55 @@ describe('PolicyEngine', () => {
|
|
|
1651
1653
|
},
|
|
1652
1654
|
],
|
|
1653
1655
|
approvalMode: ApprovalMode.YOLO,
|
|
1656
|
+
allToolNames: ['dangerous-tool', 'safe-tool'],
|
|
1654
1657
|
expected: [],
|
|
1655
1658
|
},
|
|
1656
1659
|
{
|
|
1657
1660
|
name: 'should respect server wildcard DENY',
|
|
1658
1661
|
rules: [
|
|
1659
1662
|
{
|
|
1660
|
-
toolName: '
|
|
1663
|
+
toolName: 'mcp_server_*',
|
|
1664
|
+
mcpName: 'server',
|
|
1661
1665
|
decision: PolicyDecision.DENY,
|
|
1662
1666
|
modes: [ApprovalMode.DEFAULT],
|
|
1663
1667
|
},
|
|
1664
1668
|
],
|
|
1665
|
-
|
|
1669
|
+
allToolNames: [
|
|
1670
|
+
'mcp_server_tool1',
|
|
1671
|
+
'mcp_server_tool2',
|
|
1672
|
+
'mcp_other_tool',
|
|
1673
|
+
],
|
|
1674
|
+
metadata: new Map([
|
|
1675
|
+
['mcp_server_tool1', { _serverName: 'server' }],
|
|
1676
|
+
['mcp_server_tool2', { _serverName: 'server' }],
|
|
1677
|
+
['mcp_other_tool', { _serverName: 'other' }],
|
|
1678
|
+
]),
|
|
1679
|
+
expected: ['mcp_server_tool1', 'mcp_server_tool2'],
|
|
1666
1680
|
},
|
|
1667
1681
|
{
|
|
1668
1682
|
name: 'should expand server wildcard for specific tools if already processed',
|
|
1669
1683
|
rules: [
|
|
1670
1684
|
{
|
|
1671
|
-
toolName: '
|
|
1685
|
+
toolName: 'mcp_server_*',
|
|
1686
|
+
mcpName: 'server',
|
|
1672
1687
|
decision: PolicyDecision.DENY,
|
|
1673
1688
|
priority: 100,
|
|
1674
1689
|
modes: [ApprovalMode.DEFAULT],
|
|
1675
1690
|
},
|
|
1676
1691
|
{
|
|
1677
|
-
toolName: '
|
|
1678
|
-
|
|
1692
|
+
toolName: 'mcp_server_tool1',
|
|
1693
|
+
mcpName: 'server',
|
|
1694
|
+
decision: PolicyDecision.DENY, // redundant but tests ordering
|
|
1679
1695
|
priority: 10,
|
|
1680
1696
|
modes: [ApprovalMode.DEFAULT],
|
|
1681
1697
|
},
|
|
1682
1698
|
],
|
|
1683
|
-
|
|
1699
|
+
allToolNames: ['mcp_server_tool1', 'mcp_server_tool2'],
|
|
1700
|
+
metadata: new Map([
|
|
1701
|
+
['mcp_server_tool1', { _serverName: 'server' }],
|
|
1702
|
+
['mcp_server_tool2', { _serverName: 'server' }],
|
|
1703
|
+
]),
|
|
1704
|
+
expected: ['mcp_server_tool1', 'mcp_server_tool2'],
|
|
1684
1705
|
},
|
|
1685
1706
|
{
|
|
1686
1707
|
name: 'should exclude run_shell_command but NOT write_file in simulated Plan Mode',
|
|
@@ -1707,24 +1728,29 @@ describe('PolicyEngine', () => {
|
|
|
1707
1728
|
priority: 10,
|
|
1708
1729
|
},
|
|
1709
1730
|
],
|
|
1710
|
-
|
|
1731
|
+
allToolNames: ['write_file', 'run_shell_command', 'read_file'],
|
|
1732
|
+
expected: ['run_shell_command', 'read_file'],
|
|
1711
1733
|
},
|
|
1712
1734
|
{
|
|
1713
1735
|
name: 'should NOT exclude tool if covered by a higher priority wildcard ALLOW',
|
|
1714
1736
|
rules: [
|
|
1715
1737
|
{
|
|
1716
|
-
toolName: '
|
|
1738
|
+
toolName: 'mcp_server_*',
|
|
1739
|
+
mcpName: 'server',
|
|
1717
1740
|
decision: PolicyDecision.ALLOW,
|
|
1718
1741
|
priority: 100,
|
|
1719
1742
|
modes: [ApprovalMode.DEFAULT],
|
|
1720
1743
|
},
|
|
1721
1744
|
{
|
|
1722
|
-
toolName: '
|
|
1745
|
+
toolName: 'mcp_server_tool1',
|
|
1746
|
+
mcpName: 'server',
|
|
1723
1747
|
decision: PolicyDecision.DENY,
|
|
1724
1748
|
priority: 10,
|
|
1725
1749
|
modes: [ApprovalMode.DEFAULT],
|
|
1726
1750
|
},
|
|
1727
1751
|
],
|
|
1752
|
+
allToolNames: ['mcp_server_tool1'],
|
|
1753
|
+
metadata: new Map([['mcp_server_tool1', { _serverName: 'server' }]]),
|
|
1728
1754
|
expected: [],
|
|
1729
1755
|
},
|
|
1730
1756
|
{
|
|
@@ -1736,38 +1762,53 @@ describe('PolicyEngine', () => {
|
|
|
1736
1762
|
priority: 10,
|
|
1737
1763
|
},
|
|
1738
1764
|
],
|
|
1739
|
-
|
|
1765
|
+
allToolNames: ['toolA', 'toolB', 'mcp_server_toolC'],
|
|
1766
|
+
expected: ['toolA', 'toolB', 'mcp_server_toolC'], // all tools denied by *
|
|
1740
1767
|
},
|
|
1741
1768
|
{
|
|
1742
1769
|
name: 'should handle MCP category wildcard *__* in getExcludedTools',
|
|
1743
1770
|
rules: [
|
|
1744
1771
|
{
|
|
1745
|
-
toolName: '*
|
|
1772
|
+
toolName: 'mcp_*',
|
|
1746
1773
|
decision: PolicyDecision.DENY,
|
|
1747
1774
|
priority: 10,
|
|
1748
1775
|
},
|
|
1749
1776
|
],
|
|
1750
|
-
|
|
1777
|
+
allToolNames: ['localTool', 'mcp_myserver_mytool'],
|
|
1778
|
+
metadata: new Map([
|
|
1779
|
+
['mcp_myserver_mytool', { _serverName: 'myserver' }],
|
|
1780
|
+
]),
|
|
1781
|
+
expected: ['mcp_myserver_mytool'],
|
|
1751
1782
|
},
|
|
1752
1783
|
{
|
|
1753
|
-
name: 'should handle tool wildcard *
|
|
1784
|
+
name: 'should handle tool wildcard mcp_server_* in getExcludedTools',
|
|
1754
1785
|
rules: [
|
|
1755
1786
|
{
|
|
1756
|
-
toolName: '*
|
|
1787
|
+
toolName: 'mcp_server_*',
|
|
1757
1788
|
decision: PolicyDecision.DENY,
|
|
1758
1789
|
priority: 10,
|
|
1759
1790
|
},
|
|
1760
1791
|
],
|
|
1761
|
-
|
|
1792
|
+
allToolNames: [
|
|
1793
|
+
'localTool',
|
|
1794
|
+
'mcp_server_search',
|
|
1795
|
+
'mcp_otherserver_read',
|
|
1796
|
+
],
|
|
1797
|
+
metadata: new Map([
|
|
1798
|
+
['mcp_server_search', { _serverName: 'server' }],
|
|
1799
|
+
['mcp_otherserver_read', { _serverName: 'otherserver' }],
|
|
1800
|
+
]),
|
|
1801
|
+
expected: ['mcp_server_search'],
|
|
1762
1802
|
},
|
|
1763
1803
|
];
|
|
1764
|
-
it.each(testCases)('$name', ({ rules, approvalMode, nonInteractive, expected }) => {
|
|
1804
|
+
it.each(testCases)('$name', ({ rules, approvalMode, nonInteractive, allToolNames, metadata, expected, }) => {
|
|
1765
1805
|
engine = new PolicyEngine({
|
|
1766
1806
|
rules,
|
|
1767
1807
|
approvalMode: approvalMode ?? ApprovalMode.DEFAULT,
|
|
1768
1808
|
nonInteractive: nonInteractive ?? false,
|
|
1769
1809
|
});
|
|
1770
|
-
const
|
|
1810
|
+
const toolsSet = allToolNames ? new Set(allToolNames) : undefined;
|
|
1811
|
+
const excluded = engine.getExcludedTools(metadata, toolsSet);
|
|
1771
1812
|
expect(Array.from(excluded).sort()).toEqual(expected.sort());
|
|
1772
1813
|
});
|
|
1773
1814
|
it('should skip annotation-based rules when no metadata is provided', () => {
|
|
@@ -1780,7 +1821,7 @@ describe('PolicyEngine', () => {
|
|
|
1780
1821
|
},
|
|
1781
1822
|
],
|
|
1782
1823
|
});
|
|
1783
|
-
const excluded = engine.getExcludedTools();
|
|
1824
|
+
const excluded = engine.getExcludedTools(undefined, new Set(['dangerous_tool']));
|
|
1784
1825
|
expect(Array.from(excluded)).toEqual([]);
|
|
1785
1826
|
});
|
|
1786
1827
|
it('should exclude tools matching annotation-based DENY rule when metadata is provided', () => {
|
|
@@ -1797,7 +1838,7 @@ describe('PolicyEngine', () => {
|
|
|
1797
1838
|
['dangerous_tool', { destructiveHint: true }],
|
|
1798
1839
|
['safe_tool', { readOnlyHint: true }],
|
|
1799
1840
|
]);
|
|
1800
|
-
const excluded = engine.getExcludedTools(metadata);
|
|
1841
|
+
const excluded = engine.getExcludedTools(metadata, new Set(['dangerous_tool', 'safe_tool']));
|
|
1801
1842
|
expect(Array.from(excluded)).toEqual(['dangerous_tool']);
|
|
1802
1843
|
});
|
|
1803
1844
|
it('should NOT exclude tools whose annotations do not match', () => {
|
|
@@ -1813,14 +1854,15 @@ describe('PolicyEngine', () => {
|
|
|
1813
1854
|
const metadata = new Map([
|
|
1814
1855
|
['safe_tool', { readOnlyHint: true }],
|
|
1815
1856
|
]);
|
|
1816
|
-
const excluded = engine.getExcludedTools(metadata);
|
|
1857
|
+
const excluded = engine.getExcludedTools(metadata, new Set(['safe_tool']));
|
|
1817
1858
|
expect(Array.from(excluded)).toEqual([]);
|
|
1818
1859
|
});
|
|
1819
1860
|
it('should exclude tools matching both toolName pattern AND annotations', () => {
|
|
1820
1861
|
engine = new PolicyEngine({
|
|
1821
1862
|
rules: [
|
|
1822
1863
|
{
|
|
1823
|
-
toolName: '
|
|
1864
|
+
toolName: 'mcp_server_*',
|
|
1865
|
+
mcpName: 'server',
|
|
1824
1866
|
toolAnnotations: { destructiveHint: true },
|
|
1825
1867
|
decision: PolicyDecision.DENY,
|
|
1826
1868
|
priority: 10,
|
|
@@ -1828,12 +1870,22 @@ describe('PolicyEngine', () => {
|
|
|
1828
1870
|
],
|
|
1829
1871
|
});
|
|
1830
1872
|
const metadata = new Map([
|
|
1831
|
-
[
|
|
1832
|
-
|
|
1833
|
-
|
|
1873
|
+
[
|
|
1874
|
+
'mcp_server_dangerous_tool',
|
|
1875
|
+
{ destructiveHint: true, _serverName: 'server' },
|
|
1876
|
+
],
|
|
1877
|
+
[
|
|
1878
|
+
'mcp_other_dangerous_tool',
|
|
1879
|
+
{ destructiveHint: true, _serverName: 'other' },
|
|
1880
|
+
],
|
|
1881
|
+
['mcp_server_safe_tool', { readOnlyHint: true, _serverName: 'server' }],
|
|
1834
1882
|
]);
|
|
1835
|
-
const excluded = engine.getExcludedTools(metadata
|
|
1836
|
-
|
|
1883
|
+
const excluded = engine.getExcludedTools(metadata, new Set([
|
|
1884
|
+
'mcp_server_dangerous_tool',
|
|
1885
|
+
'mcp_other_dangerous_tool',
|
|
1886
|
+
'mcp_server_safe_tool',
|
|
1887
|
+
]));
|
|
1888
|
+
expect(Array.from(excluded)).toEqual(['mcp_server_dangerous_tool']);
|
|
1837
1889
|
});
|
|
1838
1890
|
it('should exclude unprocessed tools from allToolNames when global DENY is active', () => {
|
|
1839
1891
|
engine = new PolicyEngine({
|
|
@@ -1849,8 +1901,8 @@ describe('PolicyEngine', () => {
|
|
|
1849
1901
|
priority: 70,
|
|
1850
1902
|
},
|
|
1851
1903
|
{
|
|
1852
|
-
// Simulates plan.toml: mcpName="*" → toolName="*
|
|
1853
|
-
toolName: '*
|
|
1904
|
+
// Simulates plan.toml: mcpName="*" → toolName="mcp_*"
|
|
1905
|
+
toolName: 'mcp_*',
|
|
1854
1906
|
toolAnnotations: { readOnlyHint: true },
|
|
1855
1907
|
decision: PolicyDecision.ASK_USER,
|
|
1856
1908
|
priority: 70,
|
|
@@ -1861,35 +1913,41 @@ describe('PolicyEngine', () => {
|
|
|
1861
1913
|
},
|
|
1862
1914
|
],
|
|
1863
1915
|
});
|
|
1864
|
-
// MCP tools are registered with
|
|
1916
|
+
// MCP tools are registered with qualified names in ToolRegistry
|
|
1865
1917
|
const allToolNames = new Set([
|
|
1866
1918
|
'glob',
|
|
1867
1919
|
'read_file',
|
|
1868
1920
|
'shell',
|
|
1869
1921
|
'web_fetch',
|
|
1870
|
-
'
|
|
1871
|
-
'
|
|
1922
|
+
'mcp_my-server_read_mcp_tool',
|
|
1923
|
+
'mcp_my-server_write_mcp_tool',
|
|
1872
1924
|
]);
|
|
1873
1925
|
// buildToolMetadata() includes _serverName for MCP tools
|
|
1874
1926
|
const toolMetadata = new Map([
|
|
1875
|
-
[
|
|
1876
|
-
|
|
1927
|
+
[
|
|
1928
|
+
'mcp_my-server_read_mcp_tool',
|
|
1929
|
+
{ readOnlyHint: true, _serverName: 'my-server' },
|
|
1930
|
+
],
|
|
1931
|
+
[
|
|
1932
|
+
'mcp_my-server_write_mcp_tool',
|
|
1933
|
+
{ readOnlyHint: false, _serverName: 'my-server' },
|
|
1934
|
+
],
|
|
1877
1935
|
]);
|
|
1878
1936
|
const excluded = engine.getExcludedTools(toolMetadata, allToolNames);
|
|
1879
1937
|
expect(excluded.has('shell')).toBe(true);
|
|
1880
1938
|
expect(excluded.has('web_fetch')).toBe(true);
|
|
1881
1939
|
// Non-read-only MCP tool excluded by catch-all DENY
|
|
1882
|
-
expect(excluded.has('
|
|
1940
|
+
expect(excluded.has('mcp_my-server_write_mcp_tool')).toBe(true);
|
|
1883
1941
|
expect(excluded.has('glob')).toBe(false);
|
|
1884
1942
|
expect(excluded.has('read_file')).toBe(false);
|
|
1885
1943
|
// Read-only MCP tool allowed by annotation rule
|
|
1886
|
-
expect(excluded.has('
|
|
1944
|
+
expect(excluded.has('mcp_my-server_read_mcp_tool')).toBe(false);
|
|
1887
1945
|
});
|
|
1888
|
-
it('should match
|
|
1946
|
+
it('should match MCP wildcard rules when explicitly mapped with _serverName', () => {
|
|
1889
1947
|
engine = new PolicyEngine({
|
|
1890
1948
|
rules: [
|
|
1891
1949
|
{
|
|
1892
|
-
toolName: '*
|
|
1950
|
+
toolName: 'mcp_*',
|
|
1893
1951
|
toolAnnotations: { readOnlyHint: true },
|
|
1894
1952
|
decision: PolicyDecision.ASK_USER,
|
|
1895
1953
|
priority: 70,
|
|
@@ -1902,17 +1960,23 @@ describe('PolicyEngine', () => {
|
|
|
1902
1960
|
});
|
|
1903
1961
|
// Tool registered with qualified name (collision case)
|
|
1904
1962
|
const allToolNames = new Set([
|
|
1905
|
-
'
|
|
1906
|
-
'
|
|
1963
|
+
'mcp_myserver_read_tool',
|
|
1964
|
+
'mcp_myserver_write_tool',
|
|
1907
1965
|
]);
|
|
1908
1966
|
const toolMetadata = new Map([
|
|
1909
|
-
[
|
|
1910
|
-
|
|
1967
|
+
[
|
|
1968
|
+
'mcp_myserver_read_tool',
|
|
1969
|
+
{ readOnlyHint: true, _serverName: 'myserver' },
|
|
1970
|
+
],
|
|
1971
|
+
[
|
|
1972
|
+
'mcp_myserver_write_tool',
|
|
1973
|
+
{ readOnlyHint: false, _serverName: 'myserver' },
|
|
1974
|
+
],
|
|
1911
1975
|
]);
|
|
1912
1976
|
const excluded = engine.getExcludedTools(toolMetadata, allToolNames);
|
|
1913
|
-
// Qualified name
|
|
1914
|
-
expect(excluded.has('
|
|
1915
|
-
expect(excluded.has('
|
|
1977
|
+
// Qualified name matched using explicit _serverName
|
|
1978
|
+
expect(excluded.has('mcp_myserver_read_tool')).toBe(false);
|
|
1979
|
+
expect(excluded.has('mcp_myserver_write_tool')).toBe(true);
|
|
1916
1980
|
});
|
|
1917
1981
|
it('should not exclude unprocessed tools when allToolNames is not provided (backward compat)', () => {
|
|
1918
1982
|
engine = new PolicyEngine({
|
|
@@ -2001,7 +2065,7 @@ describe('PolicyEngine', () => {
|
|
|
2001
2065
|
modes: [ApprovalMode.PLAN],
|
|
2002
2066
|
},
|
|
2003
2067
|
{
|
|
2004
|
-
toolName: '*
|
|
2068
|
+
toolName: 'mcp_*',
|
|
2005
2069
|
toolAnnotations: { readOnlyHint: true },
|
|
2006
2070
|
decision: PolicyDecision.ASK_USER,
|
|
2007
2071
|
priority: 70,
|
|
@@ -2032,13 +2096,19 @@ describe('PolicyEngine', () => {
|
|
|
2032
2096
|
'write_todos',
|
|
2033
2097
|
'memory',
|
|
2034
2098
|
'save_memory',
|
|
2035
|
-
'
|
|
2036
|
-
'
|
|
2099
|
+
'mcp_mcp-server_read_tool',
|
|
2100
|
+
'mcp_mcp-server_write_tool',
|
|
2037
2101
|
]);
|
|
2038
2102
|
// buildToolMetadata() includes _serverName for MCP tools
|
|
2039
2103
|
const toolMetadata = new Map([
|
|
2040
|
-
[
|
|
2041
|
-
|
|
2104
|
+
[
|
|
2105
|
+
'mcp_mcp-server_read_tool',
|
|
2106
|
+
{ readOnlyHint: true, _serverName: 'mcp-server' },
|
|
2107
|
+
],
|
|
2108
|
+
[
|
|
2109
|
+
'mcp_mcp-server_write_tool',
|
|
2110
|
+
{ readOnlyHint: false, _serverName: 'mcp-server' },
|
|
2111
|
+
],
|
|
2042
2112
|
]);
|
|
2043
2113
|
const excluded = engine.getExcludedTools(toolMetadata, allToolNames);
|
|
2044
2114
|
// These should be excluded (caught by catch-all DENY)
|
|
@@ -2051,7 +2121,7 @@ describe('PolicyEngine', () => {
|
|
|
2051
2121
|
expect(excluded.has('write_file')).toBe(true);
|
|
2052
2122
|
expect(excluded.has('replace')).toBe(true);
|
|
2053
2123
|
// Non-read-only MCP tool excluded by catch-all DENY
|
|
2054
|
-
expect(excluded.has('
|
|
2124
|
+
expect(excluded.has('mcp_mcp-server_write_tool')).toBe(true);
|
|
2055
2125
|
// These should NOT be excluded (explicitly allowed)
|
|
2056
2126
|
expect(excluded.has('glob')).toBe(false);
|
|
2057
2127
|
expect(excluded.has('grep_search')).toBe(false);
|
|
@@ -2063,7 +2133,7 @@ describe('PolicyEngine', () => {
|
|
|
2063
2133
|
expect(excluded.has('exit_plan_mode')).toBe(false);
|
|
2064
2134
|
expect(excluded.has('save_memory')).toBe(false);
|
|
2065
2135
|
// Read-only MCP tool allowed by annotation rule (matched via _serverName)
|
|
2066
|
-
expect(excluded.has('
|
|
2136
|
+
expect(excluded.has('mcp_mcp-server_read_tool')).toBe(false);
|
|
2067
2137
|
});
|
|
2068
2138
|
});
|
|
2069
2139
|
describe('YOLO mode with ask_user tool', () => {
|
|
@@ -2339,22 +2409,22 @@ describe('PolicyEngine', () => {
|
|
|
2339
2409
|
engine = new PolicyEngine({
|
|
2340
2410
|
rules: [
|
|
2341
2411
|
{
|
|
2342
|
-
toolName: '*
|
|
2412
|
+
toolName: 'mcp_*',
|
|
2343
2413
|
toolAnnotations: { experimental: true },
|
|
2344
2414
|
decision: PolicyDecision.DENY,
|
|
2345
2415
|
priority: 20,
|
|
2346
2416
|
},
|
|
2347
2417
|
{
|
|
2348
|
-
toolName: '*
|
|
2418
|
+
toolName: 'mcp_*',
|
|
2349
2419
|
decision: PolicyDecision.ALLOW,
|
|
2350
2420
|
priority: 10,
|
|
2351
2421
|
},
|
|
2352
2422
|
],
|
|
2353
2423
|
});
|
|
2354
|
-
expect((await engine.check({ name: '
|
|
2424
|
+
expect((await engine.check({ name: 'mcp_mcp_test' }, 'mcp', {
|
|
2355
2425
|
experimental: true,
|
|
2356
2426
|
})).decision).toBe(PolicyDecision.DENY);
|
|
2357
|
-
expect((await engine.check({ name: '
|
|
2427
|
+
expect((await engine.check({ name: 'mcp_mcp_stable' }, 'mcp', {
|
|
2358
2428
|
experimental: false,
|
|
2359
2429
|
})).decision).toBe(PolicyDecision.ALLOW);
|
|
2360
2430
|
});
|