mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-09 00:16:57 -07:00
fix(a2a-server): enforce workspace trust and task isolation to prevent RCE (#28470)
This commit is contained in:
@@ -678,6 +678,7 @@ export interface ConfigParameters {
|
||||
truncateToolOutputThreshold?: number;
|
||||
eventEmitter?: EventEmitter;
|
||||
useWriteTodos?: boolean;
|
||||
env?: Record<string, string>;
|
||||
workspacePoliciesDir?: string;
|
||||
policyEngineConfig?: PolicyEngineConfig;
|
||||
directWebFetch?: boolean;
|
||||
@@ -896,6 +897,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly useTerminalBuffer: boolean;
|
||||
private readonly useRenderProcess: boolean;
|
||||
private shellExecutionConfig: ShellExecutionConfig;
|
||||
readonly env?: Record<string, string>;
|
||||
private readonly extensionManagement: boolean = true;
|
||||
private readonly extensionRegistryURI: string | undefined;
|
||||
private readonly truncateToolOutputThreshold: number;
|
||||
@@ -1119,6 +1121,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.checkpointing = params.checkpointing ?? false;
|
||||
this.proxy = params.proxy;
|
||||
this.cwd = params.cwd ?? process.cwd();
|
||||
this.env = params.env;
|
||||
this.fileDiscoveryService = params.fileDiscoveryService ?? null;
|
||||
this.bugCommand = params.bugCommand;
|
||||
this.model = params.model;
|
||||
|
||||
@@ -154,6 +154,13 @@ export async function createContentGeneratorConfig(
|
||||
vertexAiRouting,
|
||||
};
|
||||
|
||||
const getEnv = (key: string) => {
|
||||
if (config?.env && config.env[key] !== undefined) {
|
||||
return config.env[key];
|
||||
}
|
||||
return process.env[key];
|
||||
};
|
||||
|
||||
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now.
|
||||
// Return before touching the API-key keychain: on Linux without a Secret Service
|
||||
// (WSL/SSH/Docker/CI) keytar can block indefinitely on its functional probe.
|
||||
@@ -165,16 +172,13 @@ export async function createContentGeneratorConfig(
|
||||
}
|
||||
|
||||
const geminiApiKey =
|
||||
apiKey ||
|
||||
process.env['GEMINI_API_KEY'] ||
|
||||
(await loadApiKey()) ||
|
||||
undefined;
|
||||
const googleApiKey = process.env['GOOGLE_API_KEY'] || undefined;
|
||||
apiKey || getEnv('GEMINI_API_KEY') || (await loadApiKey()) || undefined;
|
||||
const googleApiKey = getEnv('GOOGLE_API_KEY') || undefined;
|
||||
const googleCloudProject =
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] ||
|
||||
process.env['GOOGLE_CLOUD_PROJECT_ID'] ||
|
||||
getEnv('GOOGLE_CLOUD_PROJECT') ||
|
||||
getEnv('GOOGLE_CLOUD_PROJECT_ID') ||
|
||||
undefined;
|
||||
const googleCloudLocation = process.env['GOOGLE_CLOUD_LOCATION'] || undefined;
|
||||
const googleCloudLocation = getEnv('GOOGLE_CLOUD_LOCATION') || undefined;
|
||||
|
||||
if (authType === AuthType.USE_GEMINI && geminiApiKey) {
|
||||
contentGeneratorConfig.apiKey = geminiApiKey;
|
||||
@@ -194,8 +198,7 @@ export async function createContentGeneratorConfig(
|
||||
}
|
||||
|
||||
if (authType === AuthType.GATEWAY) {
|
||||
contentGeneratorConfig.apiKey =
|
||||
apiKey || process.env['GEMINI_API_KEY'] || '';
|
||||
contentGeneratorConfig.apiKey = apiKey || getEnv('GEMINI_API_KEY') || '';
|
||||
contentGeneratorConfig.vertexai = false;
|
||||
|
||||
return contentGeneratorConfig;
|
||||
|
||||
@@ -34,6 +34,10 @@ describe('CheckerRunner', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
mockContextBuilder = new ContextBuilder({} as Config);
|
||||
vi.spyOn(mockContextBuilder, 'config', 'get').mockReturnValue({
|
||||
env: {},
|
||||
getWorkingDir: vi.fn().mockReturnValue('/mock/cwd'),
|
||||
} as unknown as Config);
|
||||
mockRegistry = new CheckerRegistry('/mock/dist');
|
||||
CheckerRegistry.prototype.resolveInProcess = vi.fn();
|
||||
|
||||
|
||||
@@ -168,6 +168,8 @@ export class CheckerRunner {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(checkerPath, [], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: this.contextBuilder.config.getWorkingDir(),
|
||||
env: { ...process.env, ...this.contextBuilder.config.env },
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
|
||||
@@ -15,6 +15,10 @@ import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
export class ContextBuilder {
|
||||
constructor(private readonly context: AgentLoopContext) {}
|
||||
|
||||
get config() {
|
||||
return this.context.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full context object with all available data.
|
||||
*/
|
||||
|
||||
@@ -139,6 +139,7 @@ export interface ShellExecutionConfig {
|
||||
backgroundCompletionBehavior?: 'inject' | 'notify' | 'silent';
|
||||
originalCommand?: string;
|
||||
sessionId?: string;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -461,9 +462,10 @@ export class ShellExecutionService {
|
||||
const spawnArgs = [...argsPrefix, finalCommand];
|
||||
|
||||
// 2. Prepare Environment
|
||||
const sourceEnv = shellExecutionConfig.env ?? process.env;
|
||||
const gitConfigKeys: string[] = [];
|
||||
if (!isInteractive) {
|
||||
for (const key in process.env) {
|
||||
for (const key in sourceEnv) {
|
||||
if (key.startsWith('GIT_CONFIG_')) {
|
||||
gitConfigKeys.push(key);
|
||||
}
|
||||
@@ -479,7 +481,7 @@ export class ShellExecutionService {
|
||||
],
|
||||
};
|
||||
|
||||
const sanitizedEnv = sanitizeEnvironment(process.env, sanitizationConfig);
|
||||
const sanitizedEnv = sanitizeEnvironment(sourceEnv, sanitizationConfig);
|
||||
|
||||
const baseEnv: Record<string, string | undefined> = {
|
||||
...sanitizedEnv,
|
||||
@@ -493,7 +495,7 @@ export class ShellExecutionService {
|
||||
if (!isInteractive) {
|
||||
// Ensure all GIT_CONFIG_* variables are preserved even if they were redacted
|
||||
for (const key of gitConfigKeys) {
|
||||
baseEnv[key] = process.env[key];
|
||||
baseEnv[key] = sourceEnv[key];
|
||||
}
|
||||
|
||||
const gitConfigCount = parseInt(baseEnv['GIT_CONFIG_COUNT'] || '0', 10);
|
||||
|
||||
@@ -656,6 +656,7 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
||||
this.context.config.isInteractiveShellEnabled(),
|
||||
{
|
||||
...shellExecutionConfig,
|
||||
env: this.context.config.env,
|
||||
sessionId: this.context.config?.getSessionId?.() ?? 'default',
|
||||
pager: 'cat',
|
||||
sanitizationConfig:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { Config } from '../config/config.js';
|
||||
import { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import type { PolicyEngine } from '../policy/policy-engine.js';
|
||||
@@ -30,6 +30,7 @@ describe('Tracker Tools Integration', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tracker-tools-test-'));
|
||||
vi.stubEnv('GEMINI_CLI_HOME', tempDir);
|
||||
config = new Config({
|
||||
sessionId: `test-session-${Math.random().toString(36).substring(7)}`,
|
||||
targetDir: tempDir,
|
||||
@@ -42,6 +43,7 @@ describe('Tracker Tools Integration', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ vi.mock('child_process', () => ({
|
||||
spawnSync: vi.fn(() => ({ error: null, status: 0 })),
|
||||
}));
|
||||
|
||||
vi.mock('./headless.js', () => ({
|
||||
isHeadlessMode: vi.fn(() => false),
|
||||
}));
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
describe('editor utils', () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { promisify } from 'node:util';
|
||||
import { once } from 'node:events';
|
||||
import { debugLogger } from './debugLogger.js';
|
||||
import { coreEvents, CoreEvent, type EditorSelectedPayload } from './events.js';
|
||||
import { isHeadlessMode } from './headless.js';
|
||||
|
||||
const GUI_EDITORS = [
|
||||
'vscode',
|
||||
@@ -404,6 +405,13 @@ export async function openDiff(
|
||||
newPath: string,
|
||||
editor: EditorType,
|
||||
): Promise<void> {
|
||||
if (isHeadlessMode()) {
|
||||
debugLogger.warn(
|
||||
'External editor spawning is disabled in headless/server mode.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const diffCommand = getDiffCommand(oldPath, newPath, editor);
|
||||
if (!diffCommand) {
|
||||
debugLogger.error('No diff tool available. Install a supported editor.');
|
||||
|
||||
Reference in New Issue
Block a user