Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions packages/core/src/policy/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { Storage } from '../config/storage.js';
import * as tomlLoader from './toml-loader.js';
import { coreEvents } from '../utils/events.js';
import { MCPServerConfig } from '../config/config.js';

vi.unmock('../config/storage.js');

Expand Down Expand Up @@ -279,6 +280,145 @@ describe('createPolicyEngineConfig', () => {
expect(untrustedRule).toBeUndefined();
});

it('should NOT automatically allow configured MCP servers in non-interactive mode by default', async () => {
const config = await createPolicyEngineConfig(
{
mcpServers: {
'server-1': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);

const rule = config.rules?.find(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeUndefined();
});

it('should automatically allow configured MCP servers in non-interactive mode if opted-in', async () => {
const config = await createPolicyEngineConfig(
{
mcp: { autoAllowInHeadless: true },
mcpServers: {
'server-1': new MCPServerConfig('node', []),
'server-2': new MCPServerConfig('python', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);

const rule1 = config.rules?.find(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
const rule2 = config.rules?.find(
(r) => r.mcpName === 'server-2' && r.decision === PolicyDecision.ALLOW,
);

expect(rule1).toBeDefined();
expect(rule1?.source).toBe('Settings (Headless MCP Auto-Allow)');
expect(rule2).toBeDefined();
expect(rule2?.source).toBe('Settings (Headless MCP Auto-Allow)');
});

it('should NOT automatically allow configured MCP servers in interactive mode even if opted-in', async () => {
const config = await createPolicyEngineConfig(
{
mcp: { autoAllowInHeadless: true },
mcpServers: {
'server-1': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
true, // interactive
);

const rule = config.rules?.find(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeUndefined();
});

it('should NOT duplicate allow rules if an MCP server is already explicitly allowed, wildcard allowed, or trusted', async () => {
const config = await createPolicyEngineConfig(
{
mcp: {
autoAllowInHeadless: true,
allowed: ['server-1', '*'],
},
mcpServers: {
'server-1': new MCPServerConfig('node', []),
'server-2': new MCPServerConfig('node', []),
'server-3': { trust: true },
'server-4': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);

// server-1: already in mcp.allowed
const rules1 = config.rules?.filter(
(r) => r.mcpName === 'server-1' && r.decision === PolicyDecision.ALLOW,
);
expect(rules1).toHaveLength(1);
expect(rules1?.[0].source).toBe('Settings (MCP Allowed)');

// server-2: covered by '*' in mcp.allowed
// Note: the logic adds a rule for '*' which will match server-2 at runtime,
// but the loop in headless auto-allow should skip adding a specific rule for server-2.
const rules2 = config.rules?.filter(
(r) => r.mcpName === 'server-2' && r.decision === PolicyDecision.ALLOW,
);
expect(rules2).toHaveLength(0);

// server-3: already trusted
const rules3 = config.rules?.filter(
(r) => r.mcpName === 'server-3' && r.decision === PolicyDecision.ALLOW,
);
expect(rules3).toHaveLength(1);
expect(rules3?.[0].source).toBe('Settings (MCP Trusted)');

// server-4: NOT explicitly allowed or trusted, but SHOULD NOT be added because '*' exists in mcp.allowed
const rules4 = config.rules?.filter(
(r) => r.mcpName === 'server-4' && r.decision === PolicyDecision.ALLOW,
);
expect(rules4).toHaveLength(0);

// Verify the wildcard rule exists
const wildcardRule = config.rules?.find(
(r) => r.mcpName === '*' && r.decision === PolicyDecision.ALLOW,
);
expect(wildcardRule).toBeDefined();
expect(wildcardRule?.toolName).toBe('mcp_*');
});

it('should use correct tool name pattern for wildcard server in headless auto-allow', async () => {
const config = await createPolicyEngineConfig(
{
mcp: { autoAllowInHeadless: true },
mcpServers: {
'*': new MCPServerConfig('node', []),
},
},
ApprovalMode.DEFAULT,
MOCK_DEFAULT_DIR,
false, // non-interactive
);

const rule = config.rules?.find(
(r) => r.mcpName === '*' && r.decision === PolicyDecision.ALLOW,
);
expect(rule).toBeDefined();
expect(rule?.toolName).toBe('mcp_*');
});

it('should handle multiple MCP server configurations together', async () => {
const config = await createPolicyEngineConfig(
{
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/policy/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,38 @@ export async function createPolicyEngineConfig(
}
}

// In non-interactive mode, automatically allow all configured MCP servers if opted-in.
// This ensures that tools provided by these servers are available without
// requiring explicit entries in settings.mcp.allowed.
if (
!interactive &&
settings.mcp?.autoAllowInHeadless &&
settings.mcpServers
) {
for (const serverName of Object.keys(settings.mcpServers)) {
// Avoid duplicates if already explicitly allowed, allowed via wildcard, or trusted.
if (
settings.mcp?.allowed?.includes(serverName) ||
settings.mcp?.allowed?.includes('*') ||
settings.mcpServers[serverName].trust
) {
continue;
}

rules.push({
toolName:
serverName === '*'
? `${MCP_TOOL_PREFIX}*`
: `${MCP_TOOL_PREFIX}${serverName}_*`,
mcpName: serverName,
decision: PolicyDecision.ALLOW,
priority: ALLOWED_MCP_SERVER_PRIORITY,
source: 'Settings (Headless MCP Auto-Allow)',
modes: nonPlanModes,
});
}
}

return {
rules,
checkers,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/policy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ export interface PolicySettings {
mcp?: {
excluded?: string[];
allowed?: string[];
autoAllowInHeadless?: boolean;
};
tools?: {
core?: string[];
Expand Down
Loading