Skip to content

Commit 8553c38

Browse files
gemini-cli-robotAbhijit-2592galdawave
authored
fix(patch): cherry-pick 32e777f to release/v0.31.0-preview.2-pr-20531 to patch version v0.31.0-preview.2 and create version 0.31.0-preview.3 (#20607)
Co-authored-by: Abhijit Balaji <abhijitbalaji@google.com> Co-authored-by: galz10 <galzahavi@google.com>
1 parent 5ec13e8 commit 8553c38

7 files changed

Lines changed: 212 additions & 98 deletions

File tree

packages/cli/src/config/policy.test.ts

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
88
import * as fs from 'node:fs';
99
import * as path from 'node:path';
1010
import * as os from 'node:os';
11-
import { resolveWorkspacePolicyState } from './policy.js';
11+
import {
12+
resolveWorkspacePolicyState,
13+
autoAcceptWorkspacePolicies,
14+
setAutoAcceptWorkspacePolicies,
15+
disableWorkspacePolicies,
16+
setDisableWorkspacePolicies,
17+
} from './policy.js';
1218
import { writeToStderr } from '@google/gemini-cli-core';
1319

1420
// Mock debugLogger to avoid noise in test output
@@ -41,6 +47,9 @@ describe('resolveWorkspacePolicyState', () => {
4147
fs.mkdirSync(workspaceDir);
4248
policiesDir = path.join(workspaceDir, '.gemini', 'policies');
4349

50+
// Enable policies for these tests to verify loading logic
51+
setDisableWorkspacePolicies(false);
52+
4453
vi.clearAllMocks();
4554
});
4655

@@ -63,29 +72,30 @@ describe('resolveWorkspacePolicyState', () => {
6372
});
6473
});
6574

75+
it('should have disableWorkspacePolicies set to true by default', () => {
76+
// We explicitly set it to false in beforeEach for other tests,
77+
// so here we test that setting it to true works.
78+
setDisableWorkspacePolicies(true);
79+
expect(disableWorkspacePolicies).toBe(true);
80+
});
81+
6682
it('should return policy directory if integrity matches', async () => {
6783
// Set up policies directory with a file
6884
fs.mkdirSync(policiesDir, { recursive: true });
6985
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
7086

71-
// First call to establish integrity (interactive accept)
87+
// First call to establish integrity (interactive auto-accept)
7288
const firstResult = await resolveWorkspacePolicyState({
7389
cwd: workspaceDir,
7490
trustedFolder: true,
7591
interactive: true,
7692
});
77-
expect(firstResult.policyUpdateConfirmationRequest).toBeDefined();
78-
79-
// Establish integrity manually as if accepted
80-
const { PolicyIntegrityManager } = await import('@google/gemini-cli-core');
81-
const integrityManager = new PolicyIntegrityManager();
82-
await integrityManager.acceptIntegrity(
83-
'workspace',
84-
workspaceDir,
85-
firstResult.policyUpdateConfirmationRequest!.newHash,
86-
);
93+
expect(firstResult.workspacePoliciesDir).toBe(policiesDir);
94+
expect(firstResult.policyUpdateConfirmationRequest).toBeUndefined();
95+
expect(writeToStderr).not.toHaveBeenCalled();
8796

8897
// Second call should match
98+
8999
const result = await resolveWorkspacePolicyState({
90100
cwd: workspaceDir,
91101
trustedFolder: true,
@@ -107,26 +117,33 @@ describe('resolveWorkspacePolicyState', () => {
107117
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
108118
});
109119

110-
it('should return confirmation request if changed in interactive mode', async () => {
111-
fs.mkdirSync(policiesDir, { recursive: true });
112-
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
120+
it('should return confirmation request if changed in interactive mode when AUTO_ACCEPT is false', async () => {
121+
const originalValue = autoAcceptWorkspacePolicies;
122+
setAutoAcceptWorkspacePolicies(false);
113123

114-
const result = await resolveWorkspacePolicyState({
115-
cwd: workspaceDir,
116-
trustedFolder: true,
117-
interactive: true,
118-
});
124+
try {
125+
fs.mkdirSync(policiesDir, { recursive: true });
126+
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
119127

120-
expect(result.workspacePoliciesDir).toBeUndefined();
121-
expect(result.policyUpdateConfirmationRequest).toEqual({
122-
scope: 'workspace',
123-
identifier: workspaceDir,
124-
policyDir: policiesDir,
125-
newHash: expect.any(String),
126-
});
128+
const result = await resolveWorkspacePolicyState({
129+
cwd: workspaceDir,
130+
trustedFolder: true,
131+
interactive: true,
132+
});
133+
134+
expect(result.workspacePoliciesDir).toBeUndefined();
135+
expect(result.policyUpdateConfirmationRequest).toEqual({
136+
scope: 'workspace',
137+
identifier: workspaceDir,
138+
policyDir: policiesDir,
139+
newHash: expect.any(String),
140+
});
141+
} finally {
142+
setAutoAcceptWorkspacePolicies(originalValue);
143+
}
127144
});
128145

129-
it('should warn and auto-accept if changed in non-interactive mode', async () => {
146+
it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is true', async () => {
130147
fs.mkdirSync(policiesDir, { recursive: true });
131148
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
132149

@@ -143,6 +160,30 @@ describe('resolveWorkspacePolicyState', () => {
143160
);
144161
});
145162

163+
it('should warn and auto-accept if changed in non-interactive mode when AUTO_ACCEPT is false', async () => {
164+
const originalValue = autoAcceptWorkspacePolicies;
165+
setAutoAcceptWorkspacePolicies(false);
166+
167+
try {
168+
fs.mkdirSync(policiesDir, { recursive: true });
169+
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
170+
171+
const result = await resolveWorkspacePolicyState({
172+
cwd: workspaceDir,
173+
trustedFolder: true,
174+
interactive: false,
175+
});
176+
177+
expect(result.workspacePoliciesDir).toBe(policiesDir);
178+
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
179+
expect(writeToStderr).toHaveBeenCalledWith(
180+
expect.stringContaining('Automatically accepting and loading'),
181+
);
182+
} finally {
183+
setAutoAcceptWorkspacePolicies(originalValue);
184+
}
185+
});
186+
146187
it('should not return workspace policies if cwd is the home directory', async () => {
147188
const policiesDir = path.join(tempDir, '.gemini', 'policies');
148189
fs.mkdirSync(policiesDir, { recursive: true });
@@ -159,7 +200,26 @@ describe('resolveWorkspacePolicyState', () => {
159200
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
160201
});
161202

162-
it('should not return workspace policies if cwd is a symlink to the home directory', async () => {
203+
it('should return empty state if disableWorkspacePolicies is true even if folder is trusted', async () => {
204+
setDisableWorkspacePolicies(true);
205+
206+
// Set up policies directory with a file
207+
fs.mkdirSync(policiesDir, { recursive: true });
208+
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
209+
210+
const result = await resolveWorkspacePolicyState({
211+
cwd: workspaceDir,
212+
trustedFolder: true,
213+
interactive: true,
214+
});
215+
216+
expect(result).toEqual({
217+
workspacePoliciesDir: undefined,
218+
policyUpdateConfirmationRequest: undefined,
219+
});
220+
});
221+
222+
it('should return empty state if cwd is a symlink to the home directory', async () => {
163223
const policiesDir = path.join(tempDir, '.gemini', 'policies');
164224
fs.mkdirSync(policiesDir, { recursive: true });
165225
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');

packages/cli/src/config/policy.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,38 @@ import {
1717
Storage,
1818
type PolicyUpdateConfirmationRequest,
1919
writeToStderr,
20+
debugLogger,
2021
} from '@google/gemini-cli-core';
2122
import { type Settings } from './settings.js';
2223

24+
/**
25+
* Temporary flag to automatically accept workspace policies to reduce friction.
26+
* Exported as 'let' to allow monkey patching in tests via the setter.
27+
*/
28+
export let autoAcceptWorkspacePolicies = true;
29+
30+
/**
31+
* Sets the autoAcceptWorkspacePolicies flag.
32+
* Used primarily for testing purposes.
33+
*/
34+
export function setAutoAcceptWorkspacePolicies(value: boolean) {
35+
autoAcceptWorkspacePolicies = value;
36+
}
37+
38+
/**
39+
* Temporary flag to disable workspace level policies altogether.
40+
* Exported as 'let' to allow monkey patching in tests via the setter.
41+
*/
42+
export let disableWorkspacePolicies = true;
43+
44+
/**
45+
* Sets the disableWorkspacePolicies flag.
46+
* Used primarily for testing purposes.
47+
*/
48+
export function setDisableWorkspacePolicies(value: boolean) {
49+
disableWorkspacePolicies = value;
50+
}
51+
2352
export async function createPolicyEngineConfig(
2453
settings: Settings,
2554
approvalMode: ApprovalMode,
@@ -66,7 +95,7 @@ export async function resolveWorkspacePolicyState(options: {
6695
| PolicyUpdateConfirmationRequest
6796
| undefined;
6897

69-
if (trustedFolder) {
98+
if (trustedFolder && !disableWorkspacePolicies) {
7099
const storage = new Storage(cwd);
71100

72101
// If we are in the home directory (or rather, our target Gemini dir is the global one),
@@ -91,26 +120,32 @@ export async function resolveWorkspacePolicyState(options: {
91120
) {
92121
// No workspace policies found
93122
workspacePoliciesDir = undefined;
94-
} else if (interactive) {
95-
// Policies changed or are new, and we are in interactive mode
123+
} else if (interactive && !autoAcceptWorkspacePolicies) {
124+
// Policies changed or are new, and we are in interactive mode and auto-accept is disabled
96125
policyUpdateConfirmationRequest = {
97126
scope: 'workspace',
98127
identifier: cwd,
99128
policyDir: potentialWorkspacePoliciesDir,
100129
newHash: integrityResult.hash,
101130
};
102131
} else {
103-
// Non-interactive mode: warn and automatically accept/load
132+
// Non-interactive mode or auto-accept is enabled: automatically accept/load
104133
await integrityManager.acceptIntegrity(
105134
'workspace',
106135
cwd,
107136
integrityResult.hash,
108137
);
109138
workspacePoliciesDir = potentialWorkspacePoliciesDir;
110-
// debugLogger.warn here doesn't show up in the terminal. It is showing up only in debug mode on the debug console
111-
writeToStderr(
112-
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them in non-interactive mode.\n',
113-
);
139+
140+
if (!interactive) {
141+
writeToStderr(
142+
'WARNING: Workspace policies changed or are new. Automatically accepting and loading them.\n',
143+
);
144+
} else {
145+
debugLogger.warn(
146+
'Workspace policies changed or are new. Automatically accepting and loading them.',
147+
);
148+
}
114149
}
115150
}
116151

packages/cli/src/config/workspace-policy-cli.test.ts

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { loadCliConfig, type CliArgs } from './config.js';
1010
import { createTestMergedSettings } from './settings.js';
1111
import * as ServerConfig from '@google/gemini-cli-core';
1212
import { isWorkspaceTrusted } from './trustedFolders.js';
13+
import * as Policy from './policy.js';
1314

1415
// Mock dependencies
1516
vi.mock('./trustedFolders.js', () => ({
@@ -53,6 +54,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
5354

5455
beforeEach(() => {
5556
vi.clearAllMocks();
57+
Policy.setDisableWorkspacePolicies(false);
5658
// Default to MATCH for existing tests
5759
mockCheckIntegrity.mockResolvedValue({
5860
status: 'match',
@@ -164,7 +166,7 @@ describe('Workspace-Level Policy CLI Integration', () => {
164166
);
165167
});
166168

167-
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode', async () => {
169+
it('should automatically accept and load workspacePoliciesDir if integrity MISMATCH in interactive mode when AUTO_ACCEPT is true', async () => {
168170
vi.mocked(isWorkspaceTrusted).mockReturnValue({
169171
isTrusted: true,
170172
source: 'file',
@@ -186,24 +188,23 @@ describe('Workspace-Level Policy CLI Integration', () => {
186188
cwd: MOCK_CWD,
187189
});
188190

189-
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
190-
scope: 'workspace',
191-
identifier: MOCK_CWD,
192-
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
193-
newHash: 'new-hash',
194-
});
195-
// In interactive mode without accept flag, it waits for user confirmation (handled by UI),
196-
// so it currently DOES NOT pass the directory to createPolicyEngineConfig yet.
197-
// The UI will handle the confirmation and reload/update.
191+
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
192+
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
193+
'workspace',
194+
MOCK_CWD,
195+
'new-hash',
196+
);
198197
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
199198
expect.objectContaining({
200-
workspacePoliciesDir: undefined,
199+
workspacePoliciesDir: expect.stringContaining(
200+
path.join('.gemini', 'policies'),
201+
),
201202
}),
202203
expect.anything(),
203204
);
204205
});
205206

206-
it('should set policyUpdateConfirmationRequest if integrity is NEW with files (first time seen) in interactive mode', async () => {
207+
it('should automatically accept and load workspacePoliciesDir if integrity is NEW in interactive mode when AUTO_ACCEPT is true', async () => {
207208
vi.mocked(isWorkspaceTrusted).mockReturnValue({
208209
isTrusted: true,
209210
source: 'file',
@@ -222,18 +223,65 @@ describe('Workspace-Level Policy CLI Integration', () => {
222223
cwd: MOCK_CWD,
223224
});
224225

225-
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
226-
scope: 'workspace',
227-
identifier: MOCK_CWD,
228-
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
229-
newHash: 'new-hash',
230-
});
226+
expect(config.getPolicyUpdateConfirmationRequest()).toBeUndefined();
227+
expect(mockAcceptIntegrity).toHaveBeenCalledWith(
228+
'workspace',
229+
MOCK_CWD,
230+
'new-hash',
231+
);
231232

232233
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
233234
expect.objectContaining({
234-
workspacePoliciesDir: undefined,
235+
workspacePoliciesDir: expect.stringContaining(
236+
path.join('.gemini', 'policies'),
237+
),
235238
}),
236239
expect.anything(),
237240
);
238241
});
242+
243+
it('should set policyUpdateConfirmationRequest if integrity MISMATCH in interactive mode when AUTO_ACCEPT is false', async () => {
244+
// Monkey patch autoAcceptWorkspacePolicies using setter
245+
const originalValue = Policy.autoAcceptWorkspacePolicies;
246+
Policy.setAutoAcceptWorkspacePolicies(false);
247+
248+
try {
249+
vi.mocked(isWorkspaceTrusted).mockReturnValue({
250+
isTrusted: true,
251+
source: 'file',
252+
});
253+
mockCheckIntegrity.mockResolvedValue({
254+
status: 'mismatch',
255+
hash: 'new-hash',
256+
fileCount: 1,
257+
});
258+
vi.mocked(ServerConfig.isHeadlessMode).mockReturnValue(false); // Interactive
259+
260+
const settings = createTestMergedSettings();
261+
const argv = {
262+
query: 'test',
263+
promptInteractive: 'test',
264+
} as unknown as CliArgs;
265+
266+
const config = await loadCliConfig(settings, 'test-session', argv, {
267+
cwd: MOCK_CWD,
268+
});
269+
270+
expect(config.getPolicyUpdateConfirmationRequest()).toEqual({
271+
scope: 'workspace',
272+
identifier: MOCK_CWD,
273+
policyDir: expect.stringContaining(path.join('.gemini', 'policies')),
274+
newHash: 'new-hash',
275+
});
276+
expect(ServerConfig.createPolicyEngineConfig).toHaveBeenCalledWith(
277+
expect.objectContaining({
278+
workspacePoliciesDir: undefined,
279+
}),
280+
expect.anything(),
281+
);
282+
} finally {
283+
// Restore for other tests
284+
Policy.setAutoAcceptWorkspacePolicies(originalValue);
285+
}
286+
});
239287
});

0 commit comments

Comments
 (0)