mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-29 15:30:40 -07:00
feat(policy): implement project-level policy support
Introduces a new 'Project' tier (Tier 3) for policies, allowing users to define project-specific rules in `$PROJECT_ROOT/.gemini/policies`. Key Changes: - **Core**: Added `PROJECT_POLICY_TIER` (3) and bumped `ADMIN_POLICY_TIER` to 4. Updated `getPolicyDirectories`, `getPolicyTier`, and `createPolicyEngineConfig` to handle project-level policy directories. - **Storage**: Added `getProjectPoliciesDir()` to the `Storage` class. - **CLI**: Updated `loadCliConfig` to securely load project policies. Crucially, project policies are **only loaded if the workspace is trusted**. - **Tests**: Added comprehensive tests for both core policy logic and CLI integration, verifying priority hierarchy (Admin > Project > User > Default) and trust checks. This hierarchy ensures that project-specific rules override user defaults but are still subject to system-wide admin enforcement.
This commit is contained in:
@@ -3198,6 +3198,8 @@ describe('Policy Engine Integration in loadCliConfig', () => {
|
||||
}),
|
||||
}),
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3219,6 +3221,8 @@ describe('Policy Engine Integration in loadCliConfig', () => {
|
||||
}),
|
||||
}),
|
||||
expect.anything(),
|
||||
undefined,
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
Config,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
Storage,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
@@ -692,9 +693,15 @@ export async function loadCliConfig(
|
||||
policyPaths: argv.policy,
|
||||
};
|
||||
|
||||
let projectPoliciesDir: string | undefined;
|
||||
if (trustedFolder) {
|
||||
projectPoliciesDir = new Storage(cwd).getProjectPoliciesDir();
|
||||
}
|
||||
|
||||
const policyEngineConfig = await createPolicyEngineConfig(
|
||||
effectiveSettings,
|
||||
approvalMode,
|
||||
projectPoliciesDir,
|
||||
);
|
||||
policyEngineConfig.nonInteractive = !interactive;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { type Settings } from './settings.js';
|
||||
export async function createPolicyEngineConfig(
|
||||
settings: Settings,
|
||||
approvalMode: ApprovalMode,
|
||||
projectPoliciesDir?: string,
|
||||
): Promise<PolicyEngineConfig> {
|
||||
// Explicitly construct PolicySettings from Settings to ensure type safety
|
||||
// and avoid accidental leakage of other settings properties.
|
||||
@@ -28,7 +29,12 @@ export async function createPolicyEngineConfig(
|
||||
policyPaths: settings.policyPaths,
|
||||
};
|
||||
|
||||
return createCorePolicyEngineConfig(policySettings, approvalMode);
|
||||
return createCorePolicyEngineConfig(
|
||||
policySettings,
|
||||
approvalMode,
|
||||
undefined,
|
||||
projectPoliciesDir,
|
||||
);
|
||||
}
|
||||
|
||||
export function createPolicyUpdater(
|
||||
|
||||
91
packages/cli/src/config/project-policy-cli.test.ts
Normal file
91
packages/cli/src/config/project-policy-cli.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import { loadCliConfig, type CliArgs } from './config.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async () => {
|
||||
const actual = await vi.importActual<typeof ServerConfig>(
|
||||
'@google/gemini-cli-core',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
loadServerHierarchicalMemory: vi.fn().mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
}),
|
||||
createPolicyEngineConfig: vi.fn().mockResolvedValue({
|
||||
rules: [],
|
||||
checkers: [],
|
||||
}),
|
||||
getVersion: vi.fn().mockResolvedValue('test-version'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Project-Level Policy CLI Integration', () => {
|
||||
const MOCK_CWD = process.cwd();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have getProjectPoliciesDir on Storage class', () => {
|
||||
const storage = new ServerConfig.Storage(MOCK_CWD);
|
||||
expect(storage.getProjectPoliciesDir).toBeDefined();
|
||||
expect(typeof storage.getProjectPoliciesDir).toBe('function');
|
||||
});
|
||||
|
||||
it('should pass projectPoliciesDir to createPolicyEngineConfig when folder is trusted', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { query: 'test' } as unknown as CliArgs;
|
||||
|
||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||
|
||||
// The wrapper createPolicyEngineConfig in policy.ts calls createCorePolicyEngineConfig
|
||||
// We check if the core one was called with 4 arguments, the 4th being the project dir
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
undefined, // defaultPoliciesDir
|
||||
expect.stringContaining(path.join('.gemini', 'policies')),
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT pass projectPoliciesDir to createPolicyEngineConfig when folder is NOT trusted', async () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
});
|
||||
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = { query: 'test' } as unknown as CliArgs;
|
||||
|
||||
await loadCliConfig(settings, 'test-session', argv, { cwd: MOCK_CWD });
|
||||
|
||||
// The 4th argument (projectPoliciesDir) should be undefined
|
||||
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user