feat(policy): change priority hierarchy to Admin > User > Project > Default

Updates the policy engine to prioritize User policies over Project-specific policies.
This change is a security measure to ensure that users maintain control over their
environment and are not inadvertently compromised by policies defined in a cloned
repository.

Key Changes:
- Swapped Tier 2 (now Project) and Tier 3 (now User).
- Updated documentation to reflect the new hierarchy.
- Updated all built-in policy TOML files with correct tier information.
- Adjusted all tests and integration test expectations to match new priority values.
This commit is contained in:
Abhijit Balaji
2026-02-09 16:17:10 -08:00
parent f34a3ee1df
commit 3a24b0c975
12 changed files with 146 additions and 138 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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');
});

View File

@@ -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)',
});

View File

@@ -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"]|\\\\")`),
);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);
});
});

View File

@@ -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 () => {

View File

@@ -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';
}