feat(cli): automatically accept workspace policy changes

This commit is contained in:
Abhijit Balaji
2026-02-25 11:44:43 -08:00
parent be12371380
commit 978399d900
4 changed files with 44 additions and 68 deletions
-1
View File
@@ -699,7 +699,6 @@ export async function loadCliConfig(
await resolveWorkspacePolicyState({
cwd,
trustedFolder,
interactive,
});
const policyEngineConfig = await createPolicyEngineConfig(
+12 -31
View File
@@ -9,7 +9,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { resolveWorkspacePolicyState } from './policy.js';
import { writeToStderr } from '@google/gemini-cli-core';
import { debugLogger } from '@google/gemini-cli-core';
// Mock debugLogger to avoid noise in test output
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
@@ -54,7 +54,6 @@ describe('resolveWorkspacePolicyState', () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: false,
interactive: true,
});
expect(result).toEqual({
@@ -68,28 +67,18 @@ describe('resolveWorkspacePolicyState', () => {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
// First call to establish integrity (interactive accept)
// First call will now auto-accept
const firstResult = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined();
expect(firstResult.workspacePoliciesDir).toBe(policiesDir);
expect(firstResult.policyUpdateConfirmationRequest).toBeUndefined();
// Establish integrity manually as if accepted
const { PolicyIntegrityManager } = await import('@google/gemini-cli-core');
const integrityManager = new PolicyIntegrityManager();
await integrityManager.acceptIntegrity(
'workspace',
workspaceDir,
firstResult.policyUpdateConfirmationRequest!.newHash,
);
// Second call should match
// Second call should also match
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
@@ -100,30 +89,26 @@ describe('resolveWorkspacePolicyState', () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
});
it('should return confirmation request if changed in interactive mode', async () => {
it('should auto-accept if changed in interactive mode', async () => {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toEqual({
scope: 'workspace',
identifier: workspaceDir,
policyDir: policiesDir,
newHash: expect.any(String),
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Automatically accepting and loading'),
);
});
it('should warn and auto-accept if changed in non-interactive mode', async () => {
@@ -133,12 +118,11 @@ describe('resolveWorkspacePolicyState', () => {
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: false,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
expect(writeToStderr).toHaveBeenCalledWith(
expect(debugLogger.warn).toHaveBeenCalledWith(
expect.stringContaining('Automatically accepting and loading'),
);
});
@@ -152,7 +136,6 @@ describe('resolveWorkspacePolicyState', () => {
const result = await resolveWorkspacePolicyState({
cwd: tempDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
@@ -176,9 +159,7 @@ describe('resolveWorkspacePolicyState', () => {
const result = await resolveWorkspacePolicyState({
cwd: symlinkDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
} finally {
+12 -17
View File
@@ -16,7 +16,7 @@ import {
IntegrityStatus,
Storage,
type PolicyUpdateConfirmationRequest,
writeToStderr,
debugLogger,
} from '@google/gemini-cli-core';
import { type Settings } from './settings.js';
@@ -57,14 +57,14 @@ export interface WorkspacePolicyState {
export async function resolveWorkspacePolicyState(options: {
cwd: string;
trustedFolder: boolean;
interactive: boolean;
}): Promise<WorkspacePolicyState> {
const { cwd, trustedFolder, interactive } = options;
const { cwd, trustedFolder } = options;
let workspacePoliciesDir: string | undefined;
let policyUpdateConfirmationRequest:
// TODO: Restore policyUpdateConfirmationRequest when re-enabling interactive policy acceptance.
const policyUpdateConfirmationRequest:
| PolicyUpdateConfirmationRequest
| undefined;
| undefined = undefined;
if (trustedFolder) {
const storage = new Storage(cwd);
@@ -91,25 +91,20 @@ export async function resolveWorkspacePolicyState(options: {
) {
// No workspace policies found
workspacePoliciesDir = undefined;
} else if (interactive) {
// Policies changed or are new, and we are in interactive mode
policyUpdateConfirmationRequest = {
scope: 'workspace',
identifier: cwd,
policyDir: potentialWorkspacePoliciesDir,
newHash: integrityResult.hash,
};
} else {
// Non-interactive mode: warn and automatically accept/load
// Policies changed or are new.
// Automatically accept and load for now to reduce friction.
// We keep the infrastructure (PolicyUpdateConfirmationRequest etc.)
// but bypass the interactive dialog.
await integrityManager.acceptIntegrity(
'workspace',
cwd,
integrityResult.hash,
);
workspacePoliciesDir = potentialWorkspacePoliciesDir;
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
writeToStderr(
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n',
debugLogger.warn(
'Workspace policies changed or are new. Automatically accepting and loading them.',
);
}
}
@@ -164,7 +164,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
);
});
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => {
it('should automatically accept if integrity MISMATCH in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
@@ -186,24 +186,23 @@ describe('Workspace-Level Policy CLI Integration', () => {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
// In interactive mode without accept flag, it waits for user confirmation (handled by UI),
// so it currently DOES NOT pass the directory to createPolicyEngineConfig yet.
// The UI will handle the confirmation and reload/update.
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
'workspace',
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
it('should automatically accept if integrity is NEW with files (first time seen) in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
@@ -222,16 +221,18 @@ describe('Workspace-Level Policy CLI Integration', () => {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
'workspace',
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);