feat(cli): hide workspace policy update dialog and auto-accept by default (#20351)

This commit is contained in:
Abhijit Balaji
2026-02-25 20:05:19 -08:00
committed by GitHub
parent 37903d5dbb
commit d128fb0f7b
3 changed files with 151 additions and 54 deletions
+43 -14
View File
@@ -8,7 +8,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import * as os from 'node:os'; import * as os from 'node:os';
import { resolveWorkspacePolicyState } from './policy.js'; import {
resolveWorkspacePolicyState,
autoAcceptWorkspacePolicies,
setAutoAcceptWorkspacePolicies,
} from './policy.js';
import { writeToStderr } from '@google/gemini-cli-core'; import { writeToStderr } from '@google/gemini-cli-core';
// Mock debugLogger to avoid noise in test output // Mock debugLogger to avoid noise in test output
@@ -68,24 +72,18 @@ describe('resolveWorkspacePolicyState', () => {
fs.mkdirSync(policiesDir, { recursive: true }); fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
// First call to establish integrity (interactive accept) // First call to establish integrity (interactive auto-accept)
const firstResult = await resolveWorkspacePolicyState({ const firstResult = await resolveWorkspacePolicyState({
cwd: workspaceDir, cwd: workspaceDir,
trustedFolder: true, trustedFolder: true,
interactive: true, interactive: true,
}); });
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined(); expect(firstResult.workspacePoliciesDir).toBe(policiesDir);
expect(firstResult.policyUpdateConfirmationRequest).toBeUndefined();
// Establish integrity manually as if accepted expect(writeToStderr).not.toHaveBeenCalled();
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 match
const result = await resolveWorkspacePolicyState({ const result = await resolveWorkspacePolicyState({
cwd: workspaceDir, cwd: workspaceDir,
trustedFolder: true, trustedFolder: true,
@@ -107,7 +105,11 @@ describe('resolveWorkspacePolicyState', () => {
expect(result.policyUpdateConfirmationRequest).toBeUndefined(); expect(result.policyUpdateConfirmationRequest).toBeUndefined();
}); });
it('should return confirmation request if changed in interactive mode', async () => { it('should return confirmation request if changed in interactive mode when AUTO_ACCEPT is false', async () => {
const originalValue = autoAcceptWorkspacePolicies;
setAutoAcceptWorkspacePolicies(false);
try {
fs.mkdirSync(policiesDir, { recursive: true }); fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
@@ -124,9 +126,12 @@ describe('resolveWorkspacePolicyState', () => {
policyDir: policiesDir, policyDir: policiesDir,
newHash: expect.any(String), newHash: expect.any(String),
}); });
} finally {
setAutoAcceptWorkspacePolicies(originalValue);
}
}); });
it('should warn and auto-accept if changed in non-interactive mode', async () => { it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is true', async () => {
fs.mkdirSync(policiesDir, { recursive: true }); fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []'); fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
@@ -143,6 +148,30 @@ describe('resolveWorkspacePolicyState', () => {
); );
}); });
it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is false', async () => {
const originalValue = autoAcceptWorkspacePolicies;
setAutoAcceptWorkspacePolicies(false);
try {
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
const result = await resolveWorkspacePolicyState({
cwd: workspaceDir,
trustedFolder: true,
interactive: false,
});
expect(result.workspacePoliciesDir).toBe(policiesDir);
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
expect(writeToStderr).toHaveBeenCalledWith(
expect.stringContaining('Automatically accepting and loading'),
);
} finally {
setAutoAcceptWorkspacePolicies(originalValue);
}
});
it('should not return workspace policies if cwd is the home directory', async () => { it('should not return workspace policies if cwd is the home directory', async () => {
const policiesDir = path.join(tempDir, '.gemini', 'policies'); const policiesDir = path.join(tempDir, '.gemini', 'policies');
fs.mkdirSync(policiesDir, { recursive: true }); fs.mkdirSync(policiesDir, { recursive: true });
+26 -5
View File
@@ -17,9 +17,24 @@ import {
Storage, Storage,
type PolicyUpdateConfirmationRequest, type PolicyUpdateConfirmationRequest,
writeToStderr, writeToStderr,
debugLogger,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { type Settings } from './settings.js'; import { type Settings } from './settings.js';
/**
* Temporary flag to automatically accept workspace policies to reduce friction.
* Exported as 'let' to allow monkey patching in tests via the setter.
*/
export let autoAcceptWorkspacePolicies = true;
/**
* Sets the autoAcceptWorkspacePolicies flag.
* Used primarily for testing purposes.
*/
export function setAutoAcceptWorkspacePolicies(value: boolean) {
autoAcceptWorkspacePolicies = value;
}
export async function createPolicyEngineConfig( export async function createPolicyEngineConfig(
settings: Settings, settings: Settings,
approvalMode: ApprovalMode, approvalMode: ApprovalMode,
@@ -91,8 +106,8 @@ export async function resolveWorkspacePolicyState(options: {
) { ) {
// No workspace policies found // No workspace policies found
workspacePoliciesDir = undefined; workspacePoliciesDir = undefined;
} else if (interactive) { } else if (interactive && !autoAcceptWorkspacePolicies) {
// Policies changed or are new, and we are in interactive mode // Policies changed or are new, and we are in interactive mode and auto-accept is disabled
policyUpdateConfirmationRequest = { policyUpdateConfirmationRequest = {
scope: 'workspace', scope: 'workspace',
identifier: cwd, identifier: cwd,
@@ -100,17 +115,23 @@ export async function resolveWorkspacePolicyState(options: {
newHash: integrityResult.hash, newHash: integrityResult.hash,
}; };
} else { } else {
// Non-interactive mode: warn and automatically accept/load // Non-interactive mode or auto-accept is enabled: automatically accept/load
await integrityManager.acceptIntegrity( await integrityManager.acceptIntegrity(
'workspace', 'workspace',
cwd, cwd,
integrityResult.hash, integrityResult.hash,
); );
workspacePoliciesDir = potentialWorkspacePoliciesDir; workspacePoliciesDir = potentialWorkspacePoliciesDir;
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
if (!interactive) {
writeToStderr( writeToStderr(
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n', 'WARNING: Workspace policies changed or are new. Automatically accepting and loading them.\n',
); );
} else {
debugLogger.warn(
'Workspace policies changed or are new. Automatically accepting and loading them.',
);
}
} }
} }
@@ -10,6 +10,7 @@ import { loadCliConfig, type CliArgs } from './config.js';
import { createTestMergedSettings } from './settings.js'; import { createTestMergedSettings } from './settings.js';
import * as ServerConfig from '@google/gemini-cli-core'; import * as ServerConfig from '@google/gemini-cli-core';
import { isWorkspaceTrusted } from './trustedFolders.js'; import { isWorkspaceTrusted } from './trustedFolders.js';
import * as Policy from './policy.js';
// Mock dependencies // Mock dependencies
vi.mock('./trustedFolders.js', () => ({ vi.mock('./trustedFolders.js', () => ({
@@ -164,7 +165,86 @@ describe('Workspace-Level Policy CLI Integration', () => {
); );
}); });
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => { it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in interactive mode when AUTO_ACCEPT is true', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'mismatch',
hash: 'new-hash',
fileCount: 1,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = {
query: 'test',
promptInteractive: 'test',
} as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
'workspace',
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should automatically accept and load workspacePoliciesDir if integrity is NEW in interactive mode when AUTO_ACCEPT is true', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'new',
hash: 'new-hash',
fileCount: 5,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
'workspace',
MOCK_CWD,
'new-hash',
);
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: expect.stringContaining(
path.join('.gemini', 'policies'),
),
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode when AUTO_ACCEPT is false', async () => {
// Monkey patch autoAcceptWorkspacePolicies using setter
const originalValue = Policy.autoAcceptWorkspacePolicies;
Policy.setAutoAcceptWorkspacePolicies(false);
try {
vi.mocked(isWorkspaceTrusted).mockReturnValue({ vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true, isTrusted: true,
source: 'file', source: 'file',
@@ -192,48 +272,15 @@ describe('Workspace-Level Policy CLI Integration', () => {
policyDir: expect.stringContaining(path.join('.gemini', 'policies')), policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash', 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(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({
workspacePoliciesDir: undefined,
}),
expect.anything(),
);
});
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: true,
source: 'file',
});
mockCheckIntegrity.mockResolvedValue({
status: 'new',
hash: 'new-hash',
fileCount: 5,
});
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
const settings = createTestMergedSettings();
const argv = { query: 'test' } as unknown as CliArgs;
const config = await loadCliConfig(settings, 'test-session', argv, {
cwd: MOCK_CWD,
});
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
scope: 'workspace',
identifier: MOCK_CWD,
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
newHash: 'new-hash',
});
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith( expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
workspacePoliciesDir: undefined, workspacePoliciesDir: undefined,
}), }),
expect.anything(), expect.anything(),
); );
} finally {
// Restore for other tests
Policy.setAutoAcceptWorkspacePolicies(originalValue);
}
}); });
}); });