diff --git a/docs/core/policy-engine.md b/docs/core/policy-engine.md index 54da64fcf1..352c34be99 100644 --- a/docs/core/policy-engine.md +++ b/docs/core/policy-engine.md @@ -95,8 +95,8 @@ has a designated number that forms the base of the final priority calculation. | Tier | Base | Description | | :------ | :--- | :------------------------------------------------------------------------- | | Default | 1 | Built-in policies that ship with the Gemini CLI. | -| User | 2 | Custom policies defined by the user. | -| Project | 3 | Policies defined in the current project's configuration directory. | +| Project | 2 | Policies defined in the current project's configuration directory. | +| User | 3 | Custom policies defined by the user. | | Admin | 4 | Policies managed by an administrator (e.g., in an enterprise environment). | Within a TOML policy file, you assign a priority value from **0 to 999**. The @@ -106,16 +106,16 @@ engine transforms this into a final priority using the following formula: This system guarantees that: -- Admin policies always override Project, User, and Default policies. -- Project policies override User and Default policies. -- User policies always override Default policies. +- Admin policies always override User, Project, and Default policies. +- User policies override Project and Default policies. +- Project policies override Default policies. - You can still order rules within a single tier with fine-grained control. For example: - A `priority: 50` rule in a Default policy file becomes `1.050`. -- A `priority: 100` rule in a User policy file becomes `2.100`. -- A `priority: 10` rule in a Project policy file becomes `3.010`. +- A `priority: 10` rule in a Project policy file becomes `2.010`. +- A `priority: 100` rule in a User policy file becomes `3.100`. - A `priority: 20` rule in an Admin policy file becomes `4.020`. ### Approval modes diff --git a/packages/cli/src/config/policy-engine.integration.test.ts b/packages/cli/src/config/policy-engine.integration.test.ts index 2c7ce599da..dbc7f6a415 100644 --- a/packages/cli/src/config/policy-engine.integration.test.ts +++ b/packages/cli/src/config/policy-engine.integration.test.ts @@ -148,13 +148,13 @@ describe('Policy Engine Integration Tests', () => { ); const engine = new PolicyEngine(config); - // MCP server allowed (priority 2.1) provides general allow for server - // MCP server allowed (priority 2.1) provides general allow for server + // MCP server allowed (priority 3.1) provides general allow for server + // MCP server allowed (priority 3.1) provides general allow for server expect( (await engine.check({ name: 'my-server__safe-tool' }, undefined)) .decision, ).toBe(PolicyDecision.ALLOW); - // But specific tool exclude (priority 2.4) wins over server allow + // But specific tool exclude (priority 3.4) wins over server allow expect( (await engine.check({ name: 'my-server__dangerous-tool' }, undefined)) .decision, @@ -412,25 +412,25 @@ describe('Policy Engine Integration Tests', () => { // Find rules and verify their priorities const blockedToolRule = rules.find((r) => r.toolName === 'blocked-tool'); - expect(blockedToolRule?.priority).toBe(2.4); // Command line exclude + expect(blockedToolRule?.priority).toBe(3.4); // Command line exclude const blockedServerRule = rules.find( (r) => r.toolName === 'blocked-server__*', ); - expect(blockedServerRule?.priority).toBe(2.9); // MCP server exclude + expect(blockedServerRule?.priority).toBe(3.9); // MCP server exclude const specificToolRule = rules.find( (r) => r.toolName === 'specific-tool', ); - expect(specificToolRule?.priority).toBe(2.3); // Command line allow + expect(specificToolRule?.priority).toBe(3.3); // Command line allow const trustedServerRule = rules.find( (r) => r.toolName === 'trusted-server__*', ); - expect(trustedServerRule?.priority).toBe(2.2); // MCP trusted server + expect(trustedServerRule?.priority).toBe(3.2); // MCP trusted server const mcpServerRule = rules.find((r) => r.toolName === 'mcp-server__*'); - expect(mcpServerRule?.priority).toBe(2.1); // MCP allowed server + expect(mcpServerRule?.priority).toBe(3.1); // MCP allowed server const readOnlyToolRule = rules.find((r) => r.toolName === 'glob'); // Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny) @@ -577,16 +577,16 @@ describe('Policy Engine Integration Tests', () => { // Verify each rule has the expected priority const tool3Rule = rules.find((r) => r.toolName === 'tool3'); - expect(tool3Rule?.priority).toBe(2.4); // Excluded tools (user tier) + expect(tool3Rule?.priority).toBe(3.4); // Excluded tools (user tier) const server2Rule = rules.find((r) => r.toolName === 'server2__*'); - expect(server2Rule?.priority).toBe(2.9); // Excluded servers (user tier) + expect(server2Rule?.priority).toBe(3.9); // Excluded servers (user tier) const tool1Rule = rules.find((r) => r.toolName === 'tool1'); - expect(tool1Rule?.priority).toBe(2.3); // Allowed tools (user tier) + expect(tool1Rule?.priority).toBe(3.3); // Allowed tools (user tier) const server1Rule = rules.find((r) => r.toolName === 'server1__*'); - expect(server1Rule?.priority).toBe(2.1); // Allowed servers (user tier) + expect(server1Rule?.priority).toBe(3.1); // Allowed servers (user tier) const globRule = rules.find((r) => r.toolName === 'glob'); // Priority 70 in default tier → 1.07 diff --git a/packages/core/src/policy/config.test.ts b/packages/core/src/policy/config.test.ts index 32a5287113..a9fae7a1fa 100644 --- a/packages/core/src/policy/config.test.ts +++ b/packages/core/src/policy/config.test.ts @@ -169,7 +169,7 @@ describe('createPolicyEngineConfig', () => { r.decision === PolicyDecision.ALLOW, ); expect(rule).toBeDefined(); - expect(rule?.priority).toBeCloseTo(2.3, 5); // Command line allow + expect(rule?.priority).toBeCloseTo(3.3, 5); // Command line allow }); it('should deny tools in tools.exclude', async () => { @@ -188,7 +188,7 @@ describe('createPolicyEngineConfig', () => { r.decision === PolicyDecision.DENY, ); expect(rule).toBeDefined(); - expect(rule?.priority).toBeCloseTo(2.4, 5); // Command line exclude + expect(rule?.priority).toBeCloseTo(3.4, 5); // Command line exclude }); it('should allow tools from allowed MCP servers', async () => { @@ -206,7 +206,7 @@ describe('createPolicyEngineConfig', () => { r.toolName === 'my-server__*' && r.decision === PolicyDecision.ALLOW, ); expect(rule).toBeDefined(); - expect(rule?.priority).toBe(2.1); // MCP allowed server + expect(rule?.priority).toBe(3.1); // MCP allowed server }); it('should deny tools from excluded MCP servers', async () => { @@ -224,7 +224,7 @@ describe('createPolicyEngineConfig', () => { r.toolName === 'my-server__*' && r.decision === PolicyDecision.DENY, ); expect(rule).toBeDefined(); - expect(rule?.priority).toBe(2.9); // MCP excluded server + expect(rule?.priority).toBe(3.9); // MCP excluded server }); it('should allow tools from trusted MCP servers', async () => { @@ -251,7 +251,7 @@ describe('createPolicyEngineConfig', () => { r.decision === PolicyDecision.ALLOW, ); expect(trustedRule).toBeDefined(); - expect(trustedRule?.priority).toBe(2.2); // MCP trusted server + expect(trustedRule?.priority).toBe(3.2); // MCP trusted server // Untrusted server should not have an allow rule const untrustedRule = config.rules?.find( @@ -288,7 +288,7 @@ describe('createPolicyEngineConfig', () => { r.decision === PolicyDecision.ALLOW, ); expect(allowedRule).toBeDefined(); - expect(allowedRule?.priority).toBe(2.1); // MCP allowed server + expect(allowedRule?.priority).toBe(3.1); // MCP allowed server // Check trusted server const trustedRule = config.rules?.find( @@ -297,7 +297,7 @@ describe('createPolicyEngineConfig', () => { r.decision === PolicyDecision.ALLOW, ); expect(trustedRule).toBeDefined(); - expect(trustedRule?.priority).toBe(2.2); // MCP trusted server + expect(trustedRule?.priority).toBe(3.2); // MCP trusted server // Check excluded server const excludedRule = config.rules?.find( @@ -306,7 +306,7 @@ describe('createPolicyEngineConfig', () => { r.decision === PolicyDecision.DENY, ); expect(excludedRule).toBeDefined(); - expect(excludedRule?.priority).toBe(2.9); // MCP excluded server + expect(excludedRule?.priority).toBe(3.9); // MCP excluded server }); it('should allow all tools in YOLO mode', async () => { @@ -387,11 +387,11 @@ describe('createPolicyEngineConfig', () => { ); expect(serverDenyRule).toBeDefined(); - expect(serverDenyRule?.priority).toBe(2.9); // MCP excluded server + expect(serverDenyRule?.priority).toBe(3.9); // MCP excluded server expect(toolAllowRule).toBeDefined(); - expect(toolAllowRule?.priority).toBeCloseTo(2.3, 5); // Command line allow + expect(toolAllowRule?.priority).toBeCloseTo(3.3, 5); // Command line allow - // Server deny (2.9) has higher priority than tool allow (2.3), + // Server deny (3.9) has higher priority than tool allow (3.3), // so server deny wins (this is expected behavior - server-level blocks are security critical) }); @@ -424,7 +424,7 @@ describe('createPolicyEngineConfig', () => { expect(serverAllowRule).toBeDefined(); expect(toolDenyRule).toBeDefined(); - // Command line exclude (2.4) has higher priority than MCP server trust (2.2) + // Command line exclude (3.4) has higher priority than MCP server trust (3.2) // This is the correct behavior - specific exclusions should beat general server trust expect(toolDenyRule!.priority).toBeGreaterThan(serverAllowRule!.priority!); }); @@ -432,16 +432,16 @@ describe('createPolicyEngineConfig', () => { it('should handle complex priority scenarios correctly', async () => { const settings: PolicySettings = { tools: { - allowed: ['my-server__tool1', 'other-tool'], // Priority 2.3 - exclude: ['my-server__tool2', 'glob'], // Priority 2.4 + allowed: ['my-server__tool1', 'other-tool'], // Priority 3.3 + exclude: ['my-server__tool2', 'glob'], // Priority 3.4 }, mcp: { - allowed: ['allowed-server'], // Priority 2.1 - excluded: ['excluded-server'], // Priority 2.9 + allowed: ['allowed-server'], // Priority 3.1 + excluded: ['excluded-server'], // Priority 3.9 }, mcpServers: { 'trusted-server': { - trust: true, // Priority 90 -> 2.2 + trust: true, // Priority 90 -> 3.2 }, }, }; @@ -517,7 +517,7 @@ describe('createPolicyEngineConfig', () => { expect(globDenyRule).toBeDefined(); expect(globAllowRule).toBeDefined(); // Deny from settings (user tier) - expect(globDenyRule!.priority).toBeCloseTo(2.4, 5); // Command line exclude + expect(globDenyRule!.priority).toBeCloseTo(3.4, 5); // Command line exclude // Allow from default TOML: 1 + 50/1000 = 1.05 expect(globAllowRule!.priority).toBeCloseTo(1.05, 5); @@ -530,11 +530,11 @@ describe('createPolicyEngineConfig', () => { })) .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); - // Check that the highest priority items are the excludes (user tier: 2.4 and 2.9) + // Check that the highest priority items are the excludes (user tier: 3.4 and 3.9) const highestPriorityExcludes = priorities?.filter( (p) => - Math.abs(p.priority! - 2.4) < 0.01 || - Math.abs(p.priority! - 2.9) < 0.01, + Math.abs(p.priority! - 3.4) < 0.01 || + Math.abs(p.priority! - 3.9) < 0.01, ); expect( highestPriorityExcludes?.every((p) => p.decision === PolicyDecision.DENY), @@ -626,7 +626,7 @@ describe('createPolicyEngineConfig', () => { r.toolName === 'dangerous-tool' && r.decision === PolicyDecision.DENY, ); expect(excludeRule).toBeDefined(); - expect(excludeRule?.priority).toBeCloseTo(2.4, 5); // Command line exclude + expect(excludeRule?.priority).toBeCloseTo(3.4, 5); // Command line exclude }); it('should support argsPattern in policy rules', async () => { @@ -733,8 +733,8 @@ priority = 150 r.decision === PolicyDecision.ALLOW, ); expect(rule).toBeDefined(); - // Priority 150 in user tier → 2.150 - expect(rule?.priority).toBeCloseTo(2.15, 5); + // Priority 150 in user tier → 3.150 + expect(rule?.priority).toBeCloseTo(3.15, 5); expect(rule?.argsPattern).toBeInstanceOf(RegExp); expect(rule?.argsPattern?.test('{"command":"git status"}')).toBe(true); expect(rule?.argsPattern?.test('{"command":"git diff"}')).toBe(true); @@ -1046,7 +1046,7 @@ name = "invalid-name" r.decision === PolicyDecision.ALLOW, ); expect(rule).toBeDefined(); - expect(rule?.priority).toBeCloseTo(2.3, 5); // Command line allow + expect(rule?.priority).toBeCloseTo(3.3, 5); // Command line allow vi.doUnmock('node:fs/promises'); }); @@ -1188,7 +1188,7 @@ modes = ["plan"] r.modes?.includes(ApprovalMode.PLAN), ); expect(subagentRule).toBeDefined(); - expect(subagentRule?.priority).toBeCloseTo(2.1, 5); + expect(subagentRule?.priority).toBeCloseTo(3.1, 5); vi.doUnmock('node:fs/promises'); }); diff --git a/packages/core/src/policy/config.ts b/packages/core/src/policy/config.ts index 3bb324a38e..6a0b6c4145 100644 --- a/packages/core/src/policy/config.ts +++ b/packages/core/src/policy/config.ts @@ -38,8 +38,8 @@ export const DEFAULT_CORE_POLICIES_DIR = path.join(__dirname, 'policies'); // Policy tier constants for priority calculation export const DEFAULT_POLICY_TIER = 1; -export const USER_POLICY_TIER = 2; -export const PROJECT_POLICY_TIER = 3; +export const PROJECT_POLICY_TIER = 2; +export const USER_POLICY_TIER = 3; export const ADMIN_POLICY_TIER = 4; /** @@ -222,20 +222,20 @@ export async function createPolicyEngineConfig( // // Priority bands (tiers): // - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100) - // - User policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) - // - Project policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) + // - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) + // - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) // - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100) // - // This ensures Admin > Project > User > Default hierarchy is always preserved, + // This ensures Admin > User > Project > Default hierarchy is always preserved, // while allowing user-specified priorities to work within each tier. // - // Settings-based and dynamic rules (all in user tier 2.x): - // 2.95: Tools that the user has selected as "Always Allow" in the interactive UI - // 2.9: MCP servers excluded list (security: persistent server blocks) - // 2.4: Command line flag --exclude-tools (explicit temporary blocks) - // 2.3: Command line flag --allowed-tools (explicit temporary allows) - // 2.2: MCP servers with trust=true (persistent trusted servers) - // 2.1: MCP servers allowed list (persistent general server allows) + // Settings-based and dynamic rules (all in user tier 3.x): + // 3.95: Tools that the user has selected as "Always Allow" in the interactive UI + // 3.9: MCP servers excluded list (security: persistent server blocks) + // 3.4: Command line flag --exclude-tools (explicit temporary blocks) + // 3.3: Command line flag --allowed-tools (explicit temporary allows) + // 3.2: MCP servers with trust=true (persistent trusted servers) + // 3.1: MCP servers allowed list (persistent general server allows) // // TOML policy priorities (before transformation): // 10: Write tools default to ASK_USER (becomes 1.010 in default tier) @@ -246,33 +246,33 @@ export async function createPolicyEngineConfig( // 999: YOLO mode allow-all (becomes 1.999 in default tier) // MCP servers that are explicitly excluded in settings.mcp.excluded - // Priority: 2.9 (highest in user tier for security - persistent server blocks) + // Priority: 3.9 (highest in user tier for security - persistent server blocks) if (settings.mcp?.excluded) { for (const serverName of settings.mcp.excluded) { rules.push({ toolName: `${serverName}__*`, decision: PolicyDecision.DENY, - priority: 2.9, + priority: 3.9, source: 'Settings (MCP Excluded)', }); } } // Tools that are explicitly excluded in the settings. - // Priority: 2.4 (user tier - explicit temporary blocks) + // Priority: 3.4 (user tier - explicit temporary blocks) if (settings.tools?.exclude) { for (const tool of settings.tools.exclude) { rules.push({ toolName: tool, decision: PolicyDecision.DENY, - priority: 2.4, + priority: 3.4, source: 'Settings (Tools Excluded)', }); } } // Tools that are explicitly allowed in the settings. - // Priority: 2.3 (user tier - explicit temporary allows) + // Priority: 3.3 (user tier - explicit temporary allows) if (settings.tools?.allowed) { for (const tool of settings.tools.allowed) { // Check for legacy format: toolName(args) @@ -292,7 +292,7 @@ export async function createPolicyEngineConfig( rules.push({ toolName, decision: PolicyDecision.ALLOW, - priority: 2.3, + priority: 3.3, argsPattern: new RegExp(pattern), source: 'Settings (Tools Allowed)', }); @@ -304,7 +304,7 @@ export async function createPolicyEngineConfig( rules.push({ toolName, decision: PolicyDecision.ALLOW, - priority: 2.3, + priority: 3.3, source: 'Settings (Tools Allowed)', }); } @@ -316,7 +316,7 @@ export async function createPolicyEngineConfig( rules.push({ toolName, decision: PolicyDecision.ALLOW, - priority: 2.3, + priority: 3.3, source: 'Settings (Tools Allowed)', }); } @@ -324,7 +324,7 @@ export async function createPolicyEngineConfig( } // MCP servers that are trusted in the settings. - // Priority: 2.2 (user tier - persistent trusted servers) + // Priority: 3.2 (user tier - persistent trusted servers) if (settings.mcpServers) { for (const [serverName, serverConfig] of Object.entries( settings.mcpServers, @@ -335,7 +335,7 @@ export async function createPolicyEngineConfig( rules.push({ toolName: `${serverName}__*`, decision: PolicyDecision.ALLOW, - priority: 2.2, + priority: 3.2, source: 'Settings (MCP Trusted)', }); } @@ -343,13 +343,13 @@ export async function createPolicyEngineConfig( } // MCP servers that are explicitly allowed in settings.mcp.allowed - // Priority: 2.1 (user tier - persistent general server allows) + // Priority: 3.1 (user tier - persistent general server allows) if (settings.mcp?.allowed) { for (const serverName of settings.mcp.allowed) { rules.push({ toolName: `${serverName}__*`, decision: PolicyDecision.ALLOW, - priority: 2.1, + priority: 3.1, source: 'Settings (MCP Allowed)', }); } @@ -396,10 +396,10 @@ export function createPolicyUpdater( policyEngine.addRule({ toolName, decision: PolicyDecision.ALLOW, - // User tier (2) + high priority (950/1000) = 2.95 + // User tier (3) + high priority (950/1000) = 3.95 // This ensures user "always allow" selections are high priority - // but still lose to admin policies (3.xxx) and settings excludes (200) - priority: 2.95, + // but still lose to admin policies (4.xxx) and settings excludes (300) + priority: 3.95, argsPattern: new RegExp(pattern), source: 'Dynamic (Confirmed)', }); @@ -421,10 +421,10 @@ export function createPolicyUpdater( policyEngine.addRule({ toolName, decision: PolicyDecision.ALLOW, - // User tier (2) + high priority (950/1000) = 2.95 + // User tier (3) + high priority (950/1000) = 3.95 // This ensures user "always allow" selections are high priority - // but still lose to admin policies (3.xxx) and settings excludes (200) - priority: 2.95, + // but still lose to admin policies (4.xxx) and settings excludes (300) + priority: 3.95, argsPattern, source: 'Dynamic (Confirmed)', }); diff --git a/packages/core/src/policy/persistence.test.ts b/packages/core/src/policy/persistence.test.ts index 7d80b41893..3acf7c714d 100644 --- a/packages/core/src/policy/persistence.test.ts +++ b/packages/core/src/policy/persistence.test.ts @@ -136,7 +136,7 @@ describe('createPolicyUpdater', () => { const rules = policyEngine.getRules(); const addedRule = rules.find((r) => r.toolName === toolName); expect(addedRule).toBeDefined(); - expect(addedRule?.priority).toBe(2.95); + expect(addedRule?.priority).toBe(3.95); expect(addedRule?.argsPattern).toEqual( new RegExp(`"command":"git\\ status(?:[\\s"]|\\\\")`), ); diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index 12648fec5f..2bd18554d0 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -5,19 +5,20 @@ # # Priority bands (tiers): # - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100) -# - User policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) -# - Admin policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) +# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100) # -# This ensures Admin > User > Default hierarchy is always preserved, +# This ensures Admin > User > Project > Default hierarchy is always preserved, # while allowing user-specified priorities to work within each tier. # -# Settings-based and dynamic rules (all in user tier 2.x): -# 2.95: Tools that the user has selected as "Always Allow" in the interactive UI -# 2.9: MCP servers excluded list (security: persistent server blocks) -# 2.4: Command line flag --exclude-tools (explicit temporary blocks) -# 2.3: Command line flag --allowed-tools (explicit temporary allows) -# 2.2: MCP servers with trust=true (persistent trusted servers) -# 2.1: MCP servers allowed list (persistent general server allows) +# Settings-based and dynamic rules (all in user tier 3.x): +# 3.95: Tools that the user has selected as "Always Allow" in the interactive UI +# 3.9: MCP servers excluded list (security: persistent server blocks) +# 3.4: Command line flag --exclude-tools (explicit temporary blocks) +# 3.3: Command line flag --allowed-tools (explicit temporary allows) +# 3.2: MCP servers with trust=true (persistent trusted servers) +# 3.1: MCP servers allowed list (persistent general server allows) # # TOML policy priorities (before transformation): # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) diff --git a/packages/core/src/policy/policies/read-only.toml b/packages/core/src/policy/policies/read-only.toml index b608a87904..41f6b2205b 100644 --- a/packages/core/src/policy/policies/read-only.toml +++ b/packages/core/src/policy/policies/read-only.toml @@ -5,19 +5,20 @@ # # Priority bands (tiers): # - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100) -# - User policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) -# - Admin policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) +# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100) # -# This ensures Admin > User > Default hierarchy is always preserved, +# This ensures Admin > User > Project > Default hierarchy is always preserved, # while allowing user-specified priorities to work within each tier. # -# Settings-based and dynamic rules (all in user tier 2.x): -# 2.95: Tools that the user has selected as "Always Allow" in the interactive UI -# 2.9: MCP servers excluded list (security: persistent server blocks) -# 2.4: Command line flag --exclude-tools (explicit temporary blocks) -# 2.3: Command line flag --allowed-tools (explicit temporary allows) -# 2.2: MCP servers with trust=true (persistent trusted servers) -# 2.1: MCP servers allowed list (persistent general server allows) +# Settings-based and dynamic rules (all in user tier 3.x): +# 3.95: Tools that the user has selected as "Always Allow" in the interactive UI +# 3.9: MCP servers excluded list (security: persistent server blocks) +# 3.4: Command line flag --exclude-tools (explicit temporary blocks) +# 3.3: Command line flag --allowed-tools (explicit temporary allows) +# 3.2: MCP servers with trust=true (persistent trusted servers) +# 3.1: MCP servers allowed list (persistent general server allows) # # TOML policy priorities (before transformation): # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) diff --git a/packages/core/src/policy/policies/write.toml b/packages/core/src/policy/policies/write.toml index 991424cebc..8f1e3d33e1 100644 --- a/packages/core/src/policy/policies/write.toml +++ b/packages/core/src/policy/policies/write.toml @@ -5,19 +5,20 @@ # # Priority bands (tiers): # - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100) -# - User policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) -# - Admin policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) +# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100) # -# This ensures Admin > User > Default hierarchy is always preserved, +# This ensures Admin > User > Project > Default hierarchy is always preserved, # while allowing user-specified priorities to work within each tier. # -# Settings-based and dynamic rules (all in user tier 2.x): -# 2.95: Tools that the user has selected as "Always Allow" in the interactive UI -# 2.9: MCP servers excluded list (security: persistent server blocks) -# 2.4: Command line flag --exclude-tools (explicit temporary blocks) -# 2.3: Command line flag --allowed-tools (explicit temporary allows) -# 2.2: MCP servers with trust=true (persistent trusted servers) -# 2.1: MCP servers allowed list (persistent general server allows) +# Settings-based and dynamic rules (all in user tier 3.x): +# 3.95: Tools that the user has selected as "Always Allow" in the interactive UI +# 3.9: MCP servers excluded list (security: persistent server blocks) +# 3.4: Command line flag --exclude-tools (explicit temporary blocks) +# 3.3: Command line flag --allowed-tools (explicit temporary allows) +# 3.2: MCP servers with trust=true (persistent trusted servers) +# 3.1: MCP servers allowed list (persistent general server allows) # # TOML policy priorities (before transformation): # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) diff --git a/packages/core/src/policy/policies/yolo.toml b/packages/core/src/policy/policies/yolo.toml index 95c3b411f1..174099d7ee 100644 --- a/packages/core/src/policy/policies/yolo.toml +++ b/packages/core/src/policy/policies/yolo.toml @@ -5,19 +5,20 @@ # # Priority bands (tiers): # - Default policies (TOML): 1 + priority/1000 (e.g., priority 100 → 1.100) -# - User policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) -# - Admin policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Project policies (TOML): 2 + priority/1000 (e.g., priority 100 → 2.100) +# - User policies (TOML): 3 + priority/1000 (e.g., priority 100 → 3.100) +# - Admin policies (TOML): 4 + priority/1000 (e.g., priority 100 → 4.100) # -# This ensures Admin > User > Default hierarchy is always preserved, +# This ensures Admin > User > Project > Default hierarchy is always preserved, # while allowing user-specified priorities to work within each tier. # -# Settings-based and dynamic rules (all in user tier 2.x): -# 2.95: Tools that the user has selected as "Always Allow" in the interactive UI -# 2.9: MCP servers excluded list (security: persistent server blocks) -# 2.4: Command line flag --exclude-tools (explicit temporary blocks) -# 2.3: Command line flag --allowed-tools (explicit temporary allows) -# 2.2: MCP servers with trust=true (persistent trusted servers) -# 2.1: MCP servers allowed list (persistent general server allows) +# Settings-based and dynamic rules (all in user tier 3.x): +# 3.95: Tools that the user has selected as "Always Allow" in the interactive UI +# 3.9: MCP servers excluded list (security: persistent server blocks) +# 3.4: Command line flag --exclude-tools (explicit temporary blocks) +# 3.3: Command line flag --allowed-tools (explicit temporary allows) +# 3.2: MCP servers with trust=true (persistent trusted servers) +# 3.1: MCP servers allowed list (persistent general server allows) # # TOML policy priorities (before transformation): # 10: Write tools default to ASK_USER (becomes 1.010 in default tier) diff --git a/packages/core/src/policy/project-policy.test.ts b/packages/core/src/policy/project-policy.test.ts index 73018b0821..dbbc7b759e 100644 --- a/packages/core/src/policy/project-policy.test.ts +++ b/packages/core/src/policy/project-policy.test.ts @@ -34,7 +34,7 @@ describe('Project-Level Policies', () => { vi.doUnmock('node:fs/promises'); }); - it('should load project policies with correct priority (Tier 3)', async () => { + it('should load project policies with correct priority (Tier 2)', async () => { const projectPoliciesDir = '/mock/project/policies'; const defaultPoliciesDir = '/mock/default/policies'; @@ -88,14 +88,14 @@ priority = 10 toolName = "test_tool" decision = "deny" priority = 10 -`; // Tier 2 -> 2.010 +`; // Tier 3 -> 3.010 } if (path.includes('project.toml')) { return `[[rule]] toolName = "test_tool" decision = "allow" priority = 10 -`; // Tier 3 -> 3.010 +`; // Tier 2 -> 2.010 } if (path.includes('admin.toml')) { return `[[rule]] @@ -116,7 +116,7 @@ priority = 10 const { createPolicyEngineConfig } = await import('./config.js'); - // Test 1: Project vs User (Project should win) + // Test 1: Project vs User (User should win) const config = await createPolicyEngineConfig( {}, ApprovalMode.DEFAULT, @@ -129,8 +129,8 @@ priority = 10 // Check for all 4 rules const defaultRule = rules?.find((r) => r.priority === 1.01); - const userRule = rules?.find((r) => r.priority === 2.01); - const projectRule = rules?.find((r) => r.priority === 3.01); + const projectRule = rules?.find((r) => r.priority === 2.01); + const userRule = rules?.find((r) => r.priority === 3.01); const adminRule = rules?.find((r) => r.priority === 4.01); expect(defaultRule).toBeDefined(); @@ -138,10 +138,10 @@ priority = 10 expect(projectRule).toBeDefined(); expect(adminRule).toBeDefined(); - // Verify Hierarchy: Admin > Project > User > Default - expect(adminRule!.priority).toBeGreaterThan(projectRule!.priority!); - expect(projectRule!.priority).toBeGreaterThan(userRule!.priority!); - expect(userRule!.priority).toBeGreaterThan(defaultRule!.priority!); + // Verify Hierarchy: Admin > User > Project > Default + expect(adminRule!.priority).toBeGreaterThan(userRule!.priority!); + expect(userRule!.priority).toBeGreaterThan(projectRule!.priority!); + expect(projectRule!.priority).toBeGreaterThan(defaultRule!.priority!); }); it('should ignore project policies if projectPoliciesDir is undefined', async () => { @@ -192,7 +192,7 @@ priority=10`, expect(rules![0].priority).toBe(1.01); }); - it('should load project policies and correctly transform to Tier 3', async () => { + it('should load project policies and correctly transform to Tier 2', async () => { const projectPoliciesDir = '/mock/project/policies'; // Mock FS @@ -236,7 +236,7 @@ priority=500`, const rule = config.rules?.find((r) => r.toolName === 'p_tool'); expect(rule).toBeDefined(); - // Project Tier (3) + 500/1000 = 3.5 - expect(rule?.priority).toBe(3.5); + // Project Tier (2) + 500/1000 = 2.5 + expect(rule?.priority).toBe(2.5); }); }); diff --git a/packages/core/src/policy/toml-loader.test.ts b/packages/core/src/policy/toml-loader.test.ts index c627f6d049..115c758063 100644 --- a/packages/core/src/policy/toml-loader.test.ts +++ b/packages/core/src/policy/toml-loader.test.ts @@ -228,14 +228,18 @@ modes = ["autoEdit"] `, ); - const getPolicyTier = (_dir: string) => 2; // Tier 2 - const result = await loadPoliciesFromToml([tempDir], getPolicyTier); + const getPolicyTier2 = (_dir: string) => 2; // Tier 2 + const result2 = await loadPoliciesFromToml([tempDir], getPolicyTier2); - expect(result.rules).toHaveLength(1); - expect(result.rules[0].toolName).toBe('tier2-tool'); - expect(result.rules[0].modes).toEqual(['autoEdit']); - expect(result.rules[0].source).toBe('User: tier2.toml'); - expect(result.errors).toHaveLength(0); + expect(result2.rules).toHaveLength(1); + expect(result2.rules[0].toolName).toBe('tier2-tool'); + expect(result2.rules[0].modes).toEqual(['autoEdit']); + expect(result2.rules[0].source).toBe('Project: tier2.toml'); + + const getPolicyTier3 = (_dir: string) => 3; // Tier 3 + const result3 = await loadPoliciesFromToml([tempDir], getPolicyTier3); + expect(result3.rules[0].source).toBe('User: tier2.toml'); + expect(result3.errors).toHaveLength(0); }); it('should handle TOML parse errors', async () => { diff --git a/packages/core/src/policy/toml-loader.ts b/packages/core/src/policy/toml-loader.ts index 022bddc048..b23128a990 100644 --- a/packages/core/src/policy/toml-loader.ts +++ b/packages/core/src/policy/toml-loader.ts @@ -127,8 +127,8 @@ export interface PolicyLoadResult { */ function getTierName(tier: number): 'default' | 'user' | 'project' | 'admin' { if (tier === 1) return 'default'; - if (tier === 2) return 'user'; - if (tier === 3) return 'project'; + if (tier === 2) return 'project'; + if (tier === 3) return 'user'; if (tier === 4) return 'admin'; return 'default'; }