2025-09-18 13:44:23 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
type PolicyEngineConfig,
|
|
|
|
|
PolicyDecision,
|
|
|
|
|
type PolicyRule,
|
2025-10-28 09:20:57 -07:00
|
|
|
type ApprovalMode,
|
2025-10-21 11:45:33 -07:00
|
|
|
type PolicyEngine,
|
|
|
|
|
type MessageBus,
|
|
|
|
|
MessageBusType,
|
|
|
|
|
type UpdatePolicy,
|
2025-10-28 09:20:57 -07:00
|
|
|
Storage,
|
2025-09-18 13:44:23 -07:00
|
|
|
} from '@google/gemini-cli-core';
|
2025-10-28 09:20:57 -07:00
|
|
|
import { type Settings, getSystemSettingsPath } from './settings.js';
|
|
|
|
|
import path from 'node:path';
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
import {
|
|
|
|
|
loadPoliciesFromToml,
|
|
|
|
|
type PolicyFileError,
|
|
|
|
|
} from './policy-toml-loader.js';
|
|
|
|
|
|
|
|
|
|
// Get the directory name of the current module
|
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
|
|
|
|
|
// Store policy loading errors to be displayed after UI is ready
|
|
|
|
|
let storedPolicyErrors: string[] = [];
|
|
|
|
|
|
|
|
|
|
function getPolicyDirectories(): string[] {
|
|
|
|
|
const DEFAULT_POLICIES_DIR = path.resolve(__dirname, 'policies');
|
|
|
|
|
const USER_POLICIES_DIR = Storage.getUserPoliciesDir();
|
|
|
|
|
const systemSettingsPath = getSystemSettingsPath();
|
|
|
|
|
const ADMIN_POLICIES_DIR = path.join(
|
|
|
|
|
path.dirname(systemSettingsPath),
|
|
|
|
|
'policies',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
DEFAULT_POLICIES_DIR,
|
|
|
|
|
USER_POLICIES_DIR,
|
|
|
|
|
ADMIN_POLICIES_DIR,
|
|
|
|
|
].reverse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines the policy tier (1=default, 2=user, 3=admin) for a given directory.
|
|
|
|
|
* This is used by the TOML loader to assign priority bands.
|
|
|
|
|
*/
|
|
|
|
|
function getPolicyTier(dir: string): number {
|
|
|
|
|
const DEFAULT_POLICIES_DIR = path.resolve(__dirname, 'policies');
|
|
|
|
|
const USER_POLICIES_DIR = Storage.getUserPoliciesDir();
|
|
|
|
|
const systemSettingsPath = getSystemSettingsPath();
|
|
|
|
|
const ADMIN_POLICIES_DIR = path.join(
|
|
|
|
|
path.dirname(systemSettingsPath),
|
|
|
|
|
'policies',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Normalize paths for comparison
|
|
|
|
|
const normalizedDir = path.resolve(dir);
|
|
|
|
|
const normalizedDefault = path.resolve(DEFAULT_POLICIES_DIR);
|
|
|
|
|
const normalizedUser = path.resolve(USER_POLICIES_DIR);
|
|
|
|
|
const normalizedAdmin = path.resolve(ADMIN_POLICIES_DIR);
|
|
|
|
|
|
|
|
|
|
if (normalizedDir === normalizedDefault) return 1;
|
|
|
|
|
if (normalizedDir === normalizedUser) return 2;
|
|
|
|
|
if (normalizedDir === normalizedAdmin) return 3;
|
|
|
|
|
|
|
|
|
|
// Default to tier 1 if unknown
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Formats a policy file error for console logging.
|
|
|
|
|
*/
|
|
|
|
|
function formatPolicyError(error: PolicyFileError): string {
|
|
|
|
|
const tierLabel = error.tier.toUpperCase();
|
|
|
|
|
let message = `[${tierLabel}] Policy file error in ${error.fileName}:\n`;
|
|
|
|
|
message += ` ${error.message}`;
|
|
|
|
|
if (error.details) {
|
|
|
|
|
message += `\n${error.details}`;
|
|
|
|
|
}
|
|
|
|
|
if (error.suggestion) {
|
|
|
|
|
message += `\n Suggestion: ${error.suggestion}`;
|
|
|
|
|
}
|
|
|
|
|
return message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createPolicyEngineConfig(
|
2025-09-18 13:44:23 -07:00
|
|
|
settings: Settings,
|
|
|
|
|
approvalMode: ApprovalMode,
|
2025-10-28 09:20:57 -07:00
|
|
|
): Promise<PolicyEngineConfig> {
|
|
|
|
|
const policyDirs = getPolicyDirectories();
|
|
|
|
|
|
|
|
|
|
// Load policies from TOML files
|
|
|
|
|
const { rules: tomlRules, errors } = await loadPoliciesFromToml(
|
|
|
|
|
approvalMode,
|
|
|
|
|
policyDirs,
|
|
|
|
|
getPolicyTier,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Store any errors encountered during TOML loading
|
|
|
|
|
// These will be emitted by getPolicyErrorsForUI() after the UI is ready.
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
storedPolicyErrors = errors.map((error) => formatPolicyError(error));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rules: PolicyRule[] = [...tomlRules];
|
2025-09-18 13:44:23 -07:00
|
|
|
|
|
|
|
|
// Priority system for policy rules:
|
|
|
|
|
// - Higher priority numbers win over lower priority numbers
|
|
|
|
|
// - When multiple rules match, the highest priority rule is applied
|
|
|
|
|
// - Rules are evaluated in order of priority (highest first)
|
|
|
|
|
//
|
2025-10-28 09:20:57 -07:00
|
|
|
// 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)
|
|
|
|
|
//
|
|
|
|
|
// This ensures Admin > User > 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)
|
|
|
|
|
//
|
|
|
|
|
// TOML policy priorities (before transformation):
|
|
|
|
|
// 10: Write tools default to ASK_USER (becomes 1.010 in default tier)
|
|
|
|
|
// 15: Auto-edit tool override (becomes 1.015 in default tier)
|
|
|
|
|
// 50: Read-only tools (becomes 1.050 in default tier)
|
|
|
|
|
// 999: YOLO mode allow-all (becomes 1.999 in default tier)
|
2025-09-18 13:44:23 -07:00
|
|
|
|
2025-10-28 09:20:57 -07:00
|
|
|
// MCP servers that are explicitly excluded in settings.mcp.excluded
|
|
|
|
|
// Priority: 2.9 (highest in user tier for security - persistent server blocks)
|
|
|
|
|
if (settings.mcp?.excluded) {
|
|
|
|
|
for (const serverName of settings.mcp.excluded) {
|
2025-09-18 13:44:23 -07:00
|
|
|
rules.push({
|
|
|
|
|
toolName: `${serverName}__*`,
|
2025-10-28 09:20:57 -07:00
|
|
|
decision: PolicyDecision.DENY,
|
|
|
|
|
priority: 2.9,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tools that are explicitly excluded in the settings.
|
|
|
|
|
// Priority: 2.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,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tools that are explicitly allowed in the settings.
|
|
|
|
|
// Priority: 2.3 (user tier - explicit temporary allows)
|
|
|
|
|
if (settings.tools?.allowed) {
|
|
|
|
|
for (const tool of settings.tools.allowed) {
|
|
|
|
|
rules.push({
|
|
|
|
|
toolName: tool,
|
2025-09-18 13:44:23 -07:00
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-10-28 09:20:57 -07:00
|
|
|
priority: 2.3,
|
2025-09-18 13:44:23 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MCP servers that are trusted in the settings.
|
2025-10-28 09:20:57 -07:00
|
|
|
// Priority: 2.2 (user tier - persistent trusted servers)
|
2025-09-18 13:44:23 -07:00
|
|
|
if (settings.mcpServers) {
|
|
|
|
|
for (const [serverName, serverConfig] of Object.entries(
|
|
|
|
|
settings.mcpServers,
|
|
|
|
|
)) {
|
|
|
|
|
if (serverConfig.trust) {
|
|
|
|
|
// Trust all tools from this MCP server
|
|
|
|
|
// Using pattern matching for MCP tool names which are formatted as "serverName__toolName"
|
|
|
|
|
rules.push({
|
|
|
|
|
toolName: `${serverName}__*`,
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-10-28 09:20:57 -07:00
|
|
|
priority: 2.2,
|
2025-09-18 13:44:23 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 09:20:57 -07:00
|
|
|
// MCP servers that are explicitly allowed in settings.mcp.allowed
|
|
|
|
|
// Priority: 2.1 (user tier - persistent general server allows)
|
|
|
|
|
if (settings.mcp?.allowed) {
|
|
|
|
|
for (const serverName of settings.mcp.allowed) {
|
2025-09-18 13:44:23 -07:00
|
|
|
rules.push({
|
|
|
|
|
toolName: `${serverName}__*`,
|
2025-10-28 09:20:57 -07:00
|
|
|
decision: PolicyDecision.ALLOW,
|
|
|
|
|
priority: 2.1,
|
2025-09-18 13:44:23 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rules,
|
|
|
|
|
defaultDecision: PolicyDecision.ASK_USER,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-10-21 11:45:33 -07:00
|
|
|
|
|
|
|
|
export function createPolicyUpdater(
|
|
|
|
|
policyEngine: PolicyEngine,
|
|
|
|
|
messageBus: MessageBus,
|
|
|
|
|
) {
|
|
|
|
|
messageBus.subscribe(
|
|
|
|
|
MessageBusType.UPDATE_POLICY,
|
|
|
|
|
(message: UpdatePolicy) => {
|
|
|
|
|
const toolName = message.toolName;
|
|
|
|
|
|
|
|
|
|
policyEngine.addRule({
|
|
|
|
|
toolName,
|
|
|
|
|
decision: PolicyDecision.ALLOW,
|
2025-10-28 09:20:57 -07:00
|
|
|
// User tier (2) + high priority (950/1000) = 2.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,
|
2025-10-21 11:45:33 -07:00
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-28 09:20:57 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets and clears any policy errors that were stored during config loading.
|
|
|
|
|
* This should be called once the UI is ready to display errors.
|
|
|
|
|
*
|
|
|
|
|
* @returns Array of formatted error messages, or empty array if no errors
|
|
|
|
|
*/
|
|
|
|
|
export function getPolicyErrorsForUI(): string[] {
|
|
|
|
|
const errors = [...storedPolicyErrors];
|
|
|
|
|
storedPolicyErrors = []; // Clear after retrieving
|
|
|
|
|
return errors;
|
|
|
|
|
}
|