mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: consolidate sandbox configurations into a single object (#1154)
This commit is contained in:
@@ -28,6 +28,7 @@ import * as dotenv from 'dotenv';
|
|||||||
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 { loadSandboxConfig } from './sandboxConfig.js';
|
||||||
|
|
||||||
// Simple console logger for now - replace with actual logger if available
|
// Simple console logger for now - replace with actual logger if available
|
||||||
const logger = {
|
const logger = {
|
||||||
@@ -42,6 +43,7 @@ const logger = {
|
|||||||
interface CliArgs {
|
interface CliArgs {
|
||||||
model: string | undefined;
|
model: string | undefined;
|
||||||
sandbox: boolean | string | undefined;
|
sandbox: boolean | string | undefined;
|
||||||
|
'sandbox-image': string | undefined;
|
||||||
debug: boolean | undefined;
|
debug: boolean | undefined;
|
||||||
prompt: string | undefined;
|
prompt: string | undefined;
|
||||||
all_files: boolean | undefined;
|
all_files: boolean | undefined;
|
||||||
@@ -72,6 +74,10 @@ async function parseArguments(): Promise<CliArgs> {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Run in sandbox?',
|
description: 'Run in sandbox?',
|
||||||
})
|
})
|
||||||
|
.option('sandbox-image', {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Sandbox image URI.',
|
||||||
|
})
|
||||||
.option('debug', {
|
.option('debug', {
|
||||||
alias: 'd',
|
alias: 'd',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@@ -192,11 +198,13 @@ export async function loadCliConfig(
|
|||||||
|
|
||||||
const mcpServers = mergeMcpServers(settings, extensions);
|
const mcpServers = mergeMcpServers(settings, extensions);
|
||||||
|
|
||||||
|
const sandboxConfig = await loadSandboxConfig(settings, argv);
|
||||||
|
|
||||||
return new Config({
|
return new Config({
|
||||||
sessionId,
|
sessionId,
|
||||||
contentGeneratorConfig,
|
contentGeneratorConfig,
|
||||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||||
sandbox: argv.sandbox ?? settings.sandbox,
|
sandbox: sandboxConfig,
|
||||||
targetDir: process.cwd(),
|
targetDir: process.cwd(),
|
||||||
debugMode,
|
debugMode,
|
||||||
question: argv.prompt || '',
|
question: argv.prompt || '',
|
||||||
|
|||||||
102
packages/cli/src/config/sandboxConfig.ts
Normal file
102
packages/cli/src/config/sandboxConfig.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SandboxConfig } from '@gemini-cli/core';
|
||||||
|
import commandExists from 'command-exists';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import { getPackageJson } from '../utils/package.js';
|
||||||
|
import { Settings } from './settings.js';
|
||||||
|
|
||||||
|
// This is a stripped-down version of the CliArgs interface from config.ts
|
||||||
|
// to avoid circular dependencies.
|
||||||
|
interface SandboxCliArgs {
|
||||||
|
sandbox?: boolean | string;
|
||||||
|
'sandbox-image'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
|
||||||
|
'docker',
|
||||||
|
'podman',
|
||||||
|
'sandbox-exec',
|
||||||
|
];
|
||||||
|
|
||||||
|
function isSandboxCommand(value: string): value is SandboxConfig['command'] {
|
||||||
|
return (VALID_SANDBOX_COMMANDS as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSandboxCommand(
|
||||||
|
sandbox?: boolean | string,
|
||||||
|
): SandboxConfig['command'] | '' {
|
||||||
|
// note environment variable takes precedence over argument (from command line or settings)
|
||||||
|
sandbox = process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? sandbox;
|
||||||
|
if (sandbox === '1' || sandbox === 'true') sandbox = true;
|
||||||
|
else if (sandbox === '0' || sandbox === 'false') sandbox = false;
|
||||||
|
|
||||||
|
if (sandbox === false) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof sandbox === 'string' && sandbox !== '') {
|
||||||
|
if (!isSandboxCommand(sandbox)) {
|
||||||
|
console.error(
|
||||||
|
`ERROR: invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
|
||||||
|
', ',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
// confirm that specfied command exists
|
||||||
|
if (commandExists.sync(sandbox)) {
|
||||||
|
return sandbox;
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for seatbelt, docker, or podman, in that order
|
||||||
|
// for container-based sandboxing, require sandbox to be enabled explicitly
|
||||||
|
if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
|
||||||
|
return 'sandbox-exec';
|
||||||
|
} else if (commandExists.sync('docker') && sandbox === true) {
|
||||||
|
return 'docker';
|
||||||
|
} else if (commandExists.sync('podman') && sandbox === true) {
|
||||||
|
return 'podman';
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw an error if user requested sandbox but no command was found
|
||||||
|
if (sandbox === true) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
|
||||||
|
'install docker or podman or specify command in GEMINI_SANDBOX',
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSandboxConfig(
|
||||||
|
settings: Settings,
|
||||||
|
argv: SandboxCliArgs,
|
||||||
|
): Promise<SandboxConfig | undefined> {
|
||||||
|
const sandboxOption = argv.sandbox ?? settings.sandbox;
|
||||||
|
const sandboxCommand = getSandboxCommand(sandboxOption);
|
||||||
|
if (!sandboxCommand) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJson = await getPackageJson();
|
||||||
|
return {
|
||||||
|
command: sandboxCommand,
|
||||||
|
image:
|
||||||
|
argv['sandbox-image'] ??
|
||||||
|
process.env.GEMINI_SANDBOX_IMAGE ??
|
||||||
|
packageJson?.config?.sandboxImageUri ??
|
||||||
|
'gemini-cli-sandbox',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import { AppWrapper } from './ui/App.js';
|
|||||||
import { loadCliConfig } from './config/config.js';
|
import { loadCliConfig } from './config/config.js';
|
||||||
import { readStdin } from './utils/readStdin.js';
|
import { readStdin } from './utils/readStdin.js';
|
||||||
import { basename } from 'node:path';
|
import { basename } from 'node:path';
|
||||||
import { sandbox_command, start_sandbox } from './utils/sandbox.js';
|
import { start_sandbox } from './utils/sandbox.js';
|
||||||
import { LoadedSettings, loadSettings } from './config/settings.js';
|
import { LoadedSettings, loadSettings } from './config/settings.js';
|
||||||
import { themeManager } from './ui/themes/theme-manager.js';
|
import { themeManager } from './ui/themes/theme-manager.js';
|
||||||
import { getStartupWarnings } from './utils/startupWarnings.js';
|
import { getStartupWarnings } from './utils/startupWarnings.js';
|
||||||
@@ -72,9 +72,9 @@ export async function main() {
|
|||||||
|
|
||||||
// hop into sandbox if we are outside and sandboxing is enabled
|
// hop into sandbox if we are outside and sandboxing is enabled
|
||||||
if (!process.env.SANDBOX) {
|
if (!process.env.SANDBOX) {
|
||||||
const sandbox = sandbox_command(config.getSandbox());
|
const sandboxConfig = config.getSandbox();
|
||||||
if (sandbox) {
|
if (sandboxConfig) {
|
||||||
await start_sandbox(sandbox);
|
await start_sandbox(sandboxConfig);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
ToolRegistry,
|
ToolRegistry,
|
||||||
AccessibilitySettings,
|
AccessibilitySettings,
|
||||||
|
SandboxConfig,
|
||||||
} from '@gemini-cli/core';
|
} from '@gemini-cli/core';
|
||||||
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
@@ -21,7 +22,7 @@ import process from 'node:process';
|
|||||||
interface MockServerConfig {
|
interface MockServerConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model: string;
|
model: string;
|
||||||
sandbox: boolean | string;
|
sandbox?: SandboxConfig;
|
||||||
targetDir: string;
|
targetDir: string;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
question?: string;
|
question?: string;
|
||||||
@@ -42,7 +43,7 @@ interface MockServerConfig {
|
|||||||
|
|
||||||
getApiKey: Mock<() => string>;
|
getApiKey: Mock<() => string>;
|
||||||
getModel: Mock<() => string>;
|
getModel: Mock<() => string>;
|
||||||
getSandbox: Mock<() => boolean | string>;
|
getSandbox: Mock<() => SandboxConfig | undefined>;
|
||||||
getTargetDir: Mock<() => string>;
|
getTargetDir: Mock<() => string>;
|
||||||
getToolRegistry: Mock<() => ToolRegistry>; // Use imported ToolRegistry type
|
getToolRegistry: Mock<() => ToolRegistry>; // Use imported ToolRegistry type
|
||||||
getDebugMode: Mock<() => boolean>;
|
getDebugMode: Mock<() => boolean>;
|
||||||
@@ -78,7 +79,7 @@ vi.mock('@gemini-cli/core', async (importOriginal) => {
|
|||||||
return {
|
return {
|
||||||
apiKey: opts.apiKey || 'test-key',
|
apiKey: opts.apiKey || 'test-key',
|
||||||
model: opts.model || 'test-model-in-mock-factory',
|
model: opts.model || 'test-model-in-mock-factory',
|
||||||
sandbox: typeof opts.sandbox === 'boolean' ? opts.sandbox : false,
|
sandbox: opts.sandbox,
|
||||||
targetDir: opts.targetDir || '/test/dir',
|
targetDir: opts.targetDir || '/test/dir',
|
||||||
debugMode: opts.debugMode || false,
|
debugMode: opts.debugMode || false,
|
||||||
question: opts.question,
|
question: opts.question,
|
||||||
@@ -99,9 +100,7 @@ vi.mock('@gemini-cli/core', async (importOriginal) => {
|
|||||||
|
|
||||||
getApiKey: vi.fn(() => opts.apiKey || 'test-key'),
|
getApiKey: vi.fn(() => opts.apiKey || 'test-key'),
|
||||||
getModel: vi.fn(() => opts.model || 'test-model-in-mock-factory'),
|
getModel: vi.fn(() => opts.model || 'test-model-in-mock-factory'),
|
||||||
getSandbox: vi.fn(() =>
|
getSandbox: vi.fn(() => opts.sandbox),
|
||||||
typeof opts.sandbox === 'boolean' ? opts.sandbox : false,
|
|
||||||
),
|
|
||||||
getTargetDir: vi.fn(() => opts.targetDir || '/test/dir'),
|
getTargetDir: vi.fn(() => opts.targetDir || '/test/dir'),
|
||||||
getToolRegistry: vi.fn(() => ({}) as ToolRegistry), // Simple mock
|
getToolRegistry: vi.fn(() => ({}) as ToolRegistry), // Simple mock
|
||||||
getDebugMode: vi.fn(() => opts.debugMode || false),
|
getDebugMode: vi.fn(() => opts.debugMode || false),
|
||||||
@@ -190,7 +189,7 @@ describe('App UI', () => {
|
|||||||
model: 'test-model',
|
model: 'test-model',
|
||||||
},
|
},
|
||||||
embeddingModel: 'test-embedding-model',
|
embeddingModel: 'test-embedding-model',
|
||||||
sandbox: false,
|
sandbox: undefined,
|
||||||
targetDir: '/test/dir',
|
targetDir: '/test/dir',
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
userMemory: '',
|
userMemory: '',
|
||||||
|
|||||||
@@ -10,13 +10,12 @@ import path from 'node:path';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { quote } from 'shell-quote';
|
import { quote } from 'shell-quote';
|
||||||
import { getPackageJson } from './package.js';
|
|
||||||
import commandExists from 'command-exists';
|
|
||||||
import {
|
import {
|
||||||
USER_SETTINGS_DIR,
|
USER_SETTINGS_DIR,
|
||||||
SETTINGS_DIRECTORY_NAME,
|
SETTINGS_DIRECTORY_NAME,
|
||||||
} from '../config/settings.js';
|
} from '../config/settings.js';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import { SandboxConfig } from '@gemini-cli/core';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -99,62 +98,6 @@ async function shouldUseCurrentUserInSandbox(): Promise<boolean> {
|
|||||||
return false; // Default to false if no other condition is met
|
return false; // Default to false if no other condition is met
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSandboxImageName(
|
|
||||||
isCustomProjectSandbox: boolean,
|
|
||||||
): Promise<string> {
|
|
||||||
const packageJson = await getPackageJson();
|
|
||||||
return (
|
|
||||||
process.env.GEMINI_SANDBOX_IMAGE ??
|
|
||||||
packageJson?.config?.sandboxImageUri ??
|
|
||||||
(isCustomProjectSandbox
|
|
||||||
? LOCAL_DEV_SANDBOX_IMAGE_NAME + '-' + path.basename(path.resolve())
|
|
||||||
: LOCAL_DEV_SANDBOX_IMAGE_NAME)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sandbox_command(sandbox?: string | boolean): string {
|
|
||||||
// note environment variable takes precedence over argument (from command line or settings)
|
|
||||||
sandbox = process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? sandbox;
|
|
||||||
if (sandbox === '1' || sandbox === 'true') sandbox = true;
|
|
||||||
else if (sandbox === '0' || sandbox === 'false') sandbox = false;
|
|
||||||
|
|
||||||
if (sandbox === false) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof sandbox === 'string' && sandbox !== '') {
|
|
||||||
// confirm that specfied command exists
|
|
||||||
if (commandExists.sync(sandbox)) {
|
|
||||||
return sandbox;
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
`ERROR: missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for seatbelt, docker, or podman, in that order
|
|
||||||
// for container-based sandboxing, require sandbox to be enabled explicitly
|
|
||||||
if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
|
|
||||||
return 'sandbox-exec';
|
|
||||||
} else if (commandExists.sync('docker') && sandbox === true) {
|
|
||||||
return 'docker';
|
|
||||||
} else if (commandExists.sync('podman') && sandbox === true) {
|
|
||||||
return 'podman';
|
|
||||||
}
|
|
||||||
|
|
||||||
// throw an error if user requested sandbox but no command was found
|
|
||||||
if (sandbox === true) {
|
|
||||||
console.error(
|
|
||||||
'ERROR: GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
|
|
||||||
'install docker or podman or specify command in GEMINI_SANDBOX',
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// docker does not allow container names to contain ':' or '/', so we
|
// docker does not allow container names to contain ':' or '/', so we
|
||||||
// parse those out and make the name a little shorter
|
// parse those out and make the name a little shorter
|
||||||
function parseImageName(image: string): string {
|
function parseImageName(image: string): string {
|
||||||
@@ -237,8 +180,8 @@ function entrypoint(workdir: string): string[] {
|
|||||||
return ['bash', '-c', args.join(' ')];
|
return ['bash', '-c', args.join(' ')];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function start_sandbox(sandbox: string) {
|
export async function start_sandbox(config: SandboxConfig) {
|
||||||
if (sandbox === 'sandbox-exec') {
|
if (config.command === 'sandbox-exec') {
|
||||||
// disallow BUILD_SANDBOX
|
// disallow BUILD_SANDBOX
|
||||||
if (process.env.BUILD_SANDBOX) {
|
if (process.env.BUILD_SANDBOX) {
|
||||||
console.error('ERROR: cannot BUILD_SANDBOX when using MacOS Seatbelt');
|
console.error('ERROR: cannot BUILD_SANDBOX when using MacOS Seatbelt');
|
||||||
@@ -340,14 +283,14 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// spawn child and let it inherit stdio
|
// spawn child and let it inherit stdio
|
||||||
sandboxProcess = spawn(sandbox, args, {
|
sandboxProcess = spawn(config.command, args, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
await new Promise((resolve) => sandboxProcess?.on('close', resolve));
|
await new Promise((resolve) => sandboxProcess?.on('close', resolve));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`hopping into sandbox (command: ${sandbox}) ...`);
|
console.error(`hopping into sandbox (command: ${config.command}) ...`);
|
||||||
|
|
||||||
// determine full path for gemini-cli to distinguish linked vs installed setting
|
// determine full path for gemini-cli to distinguish linked vs installed setting
|
||||||
const gcPath = fs.realpathSync(process.argv[1]);
|
const gcPath = fs.realpathSync(process.argv[1]);
|
||||||
@@ -358,7 +301,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
);
|
);
|
||||||
const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile);
|
const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile);
|
||||||
|
|
||||||
const image = await getSandboxImageName(isCustomProjectSandbox);
|
const image = config.image;
|
||||||
const workdir = path.resolve(process.cwd());
|
const workdir = path.resolve(process.cwd());
|
||||||
const containerWorkdir = getContainerPath(workdir);
|
const containerWorkdir = getContainerPath(workdir);
|
||||||
|
|
||||||
@@ -391,7 +334,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
GEMINI_SANDBOX: sandbox, // in case sandbox is enabled via flags (see config.ts under cli package)
|
GEMINI_SANDBOX: config.command, // in case sandbox is enabled via flags (see config.ts under cli package)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -399,7 +342,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stop if image is missing
|
// stop if image is missing
|
||||||
if (!(await ensureSandboxImageIsPresent(sandbox, image))) {
|
if (!(await ensureSandboxImageIsPresent(config.command, image))) {
|
||||||
const remedy =
|
const remedy =
|
||||||
image === LOCAL_DEV_SANDBOX_IMAGE_NAME
|
image === LOCAL_DEV_SANDBOX_IMAGE_NAME
|
||||||
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
|
? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
|
||||||
@@ -529,7 +472,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
// if using proxy, switch to internal networking through proxy
|
// if using proxy, switch to internal networking through proxy
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
execSync(
|
execSync(
|
||||||
`${sandbox} network inspect ${SANDBOX_NETWORK_NAME} || ${sandbox} network create --internal ${SANDBOX_NETWORK_NAME}`,
|
`${config.command} network inspect ${SANDBOX_NETWORK_NAME} || ${config.command} network create --internal ${SANDBOX_NETWORK_NAME}`,
|
||||||
);
|
);
|
||||||
args.push('--network', SANDBOX_NETWORK_NAME);
|
args.push('--network', SANDBOX_NETWORK_NAME);
|
||||||
// if proxy command is set, create a separate network w/ host access (i.e. non-internal)
|
// if proxy command is set, create a separate network w/ host access (i.e. non-internal)
|
||||||
@@ -537,7 +480,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
// this allows proxy to work even on rootless podman on macos with host<->vm<->container isolation
|
// this allows proxy to work even on rootless podman on macos with host<->vm<->container isolation
|
||||||
if (proxyCommand) {
|
if (proxyCommand) {
|
||||||
execSync(
|
execSync(
|
||||||
`${sandbox} network inspect ${SANDBOX_PROXY_NAME} || ${sandbox} network create ${SANDBOX_PROXY_NAME}`,
|
`${config.command} network inspect ${SANDBOX_PROXY_NAME} || ${config.command} network create ${SANDBOX_PROXY_NAME}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,7 +489,9 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
// name container after image, plus numeric suffix to avoid conflicts
|
// name container after image, plus numeric suffix to avoid conflicts
|
||||||
const imageName = parseImageName(image);
|
const imageName = parseImageName(image);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const containerNameCheck = execSync(`${sandbox} ps -a --format "{{.Names}}"`)
|
const containerNameCheck = execSync(
|
||||||
|
`${config.command} ps -a --format "{{.Names}}"`,
|
||||||
|
)
|
||||||
.toString()
|
.toString()
|
||||||
.trim();
|
.trim();
|
||||||
while (containerNameCheck.includes(`${imageName}-${index}`)) {
|
while (containerNameCheck.includes(`${imageName}-${index}`)) {
|
||||||
@@ -650,7 +595,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
args.push('--env', `SANDBOX=${containerName}`);
|
args.push('--env', `SANDBOX=${containerName}`);
|
||||||
|
|
||||||
// for podman only, use empty --authfile to skip unnecessary auth refresh overhead
|
// for podman only, use empty --authfile to skip unnecessary auth refresh overhead
|
||||||
if (sandbox === 'podman') {
|
if (config.command === 'podman') {
|
||||||
const emptyAuthFilePath = path.join(os.tmpdir(), 'empty_auth.json');
|
const emptyAuthFilePath = path.join(os.tmpdir(), 'empty_auth.json');
|
||||||
fs.writeFileSync(emptyAuthFilePath, '{}', 'utf-8');
|
fs.writeFileSync(emptyAuthFilePath, '{}', 'utf-8');
|
||||||
args.push('--authfile', emptyAuthFilePath);
|
args.push('--authfile', emptyAuthFilePath);
|
||||||
@@ -683,7 +628,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
|
|
||||||
if (proxyCommand) {
|
if (proxyCommand) {
|
||||||
// run proxyCommand in its own container
|
// run proxyCommand in its own container
|
||||||
const proxyContainerCommand = `${sandbox} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
|
const proxyContainerCommand = `${config.command} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
|
||||||
proxyProcess = spawn(proxyContainerCommand, {
|
proxyProcess = spawn(proxyContainerCommand, {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
shell: true,
|
shell: true,
|
||||||
@@ -692,7 +637,7 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
// install handlers to stop proxy on exit/signal
|
// install handlers to stop proxy on exit/signal
|
||||||
const stopProxy = () => {
|
const stopProxy = () => {
|
||||||
console.log('stopping proxy container ...');
|
console.log('stopping proxy container ...');
|
||||||
execSync(`${sandbox} rm -f ${SANDBOX_PROXY_NAME}`);
|
execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`);
|
||||||
};
|
};
|
||||||
process.on('exit', stopProxy);
|
process.on('exit', stopProxy);
|
||||||
process.on('SIGINT', stopProxy);
|
process.on('SIGINT', stopProxy);
|
||||||
@@ -721,12 +666,12 @@ export async function start_sandbox(sandbox: string) {
|
|||||||
// connect proxy container to sandbox network
|
// connect proxy container to sandbox network
|
||||||
// (workaround for older versions of docker that don't support multiple --network args)
|
// (workaround for older versions of docker that don't support multiple --network args)
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`${sandbox} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`,
|
`${config.command} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// spawn child and let it inherit stdio
|
// spawn child and let it inherit stdio
|
||||||
sandboxProcess = spawn(sandbox, args, {
|
sandboxProcess = spawn(config.command, args, {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { Config, ConfigParameters } from './config.js';
|
import { Config, ConfigParameters, SandboxConfig } from './config.js';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
|
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
|
||||||
import {
|
import {
|
||||||
@@ -53,7 +53,10 @@ vi.mock('../telemetry/index.js', async (importOriginal) => {
|
|||||||
describe('Server Config (config.ts)', () => {
|
describe('Server Config (config.ts)', () => {
|
||||||
const API_KEY = 'server-api-key';
|
const API_KEY = 'server-api-key';
|
||||||
const MODEL = 'gemini-pro';
|
const MODEL = 'gemini-pro';
|
||||||
const SANDBOX = false;
|
const SANDBOX: SandboxConfig = {
|
||||||
|
command: 'docker',
|
||||||
|
image: 'gemini-cli-sandbox',
|
||||||
|
};
|
||||||
const TARGET_DIR = '/path/to/target';
|
const TARGET_DIR = '/path/to/target';
|
||||||
const DEBUG_MODE = false;
|
const DEBUG_MODE = false;
|
||||||
const QUESTION = 'test question';
|
const QUESTION = 'test question';
|
||||||
|
|||||||
@@ -73,11 +73,16 @@ export class MCPServerConfig {
|
|||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SandboxConfig {
|
||||||
|
command: 'docker' | 'podman' | 'sandbox-exec';
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigParameters {
|
export interface ConfigParameters {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
contentGeneratorConfig: ContentGeneratorConfig;
|
contentGeneratorConfig: ContentGeneratorConfig;
|
||||||
embeddingModel?: string;
|
embeddingModel?: string;
|
||||||
sandbox?: boolean | string;
|
sandbox?: SandboxConfig;
|
||||||
targetDir: string;
|
targetDir: string;
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
question?: string;
|
question?: string;
|
||||||
@@ -108,7 +113,7 @@ export class Config {
|
|||||||
private readonly sessionId: string;
|
private readonly sessionId: string;
|
||||||
private readonly contentGeneratorConfig: ContentGeneratorConfig;
|
private readonly contentGeneratorConfig: ContentGeneratorConfig;
|
||||||
private readonly embeddingModel: string;
|
private readonly embeddingModel: string;
|
||||||
private readonly sandbox: boolean | string | undefined;
|
private readonly sandbox: SandboxConfig | undefined;
|
||||||
private readonly targetDir: string;
|
private readonly targetDir: string;
|
||||||
private readonly debugMode: boolean;
|
private readonly debugMode: boolean;
|
||||||
private readonly question: string | undefined;
|
private readonly question: string | undefined;
|
||||||
@@ -198,7 +203,7 @@ export class Config {
|
|||||||
return this.embeddingModel;
|
return this.embeddingModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSandbox(): boolean | string | undefined {
|
getSandbox(): SandboxConfig | undefined {
|
||||||
return this.sandbox;
|
return this.sandbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,7 @@ export function logCliConfiguration(config: Config): void {
|
|||||||
'event.timestamp': new Date().toISOString(),
|
'event.timestamp': new Date().toISOString(),
|
||||||
model: config.getModel(),
|
model: config.getModel(),
|
||||||
embedding_model: config.getEmbeddingModel(),
|
embedding_model: config.getEmbeddingModel(),
|
||||||
sandbox_enabled:
|
sandbox_enabled: !!config.getSandbox(),
|
||||||
typeof config.getSandbox() === 'string' ? true : config.getSandbox(),
|
|
||||||
core_tools_enabled: (config.getCoreTools() ?? []).join(','),
|
core_tools_enabled: (config.getCoreTools() ?? []).join(','),
|
||||||
approval_mode: config.getApprovalMode(),
|
approval_mode: config.getApprovalMode(),
|
||||||
api_key_enabled: !!generatorConfig.apiKey,
|
api_key_enabled: !!generatorConfig.apiKey,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const baseConfigParams: ConfigParameters = {
|
|||||||
vertexai: false,
|
vertexai: false,
|
||||||
},
|
},
|
||||||
embeddingModel: 'test-embedding-model',
|
embeddingModel: 'test-embedding-model',
|
||||||
sandbox: false,
|
sandbox: undefined,
|
||||||
targetDir: '/test/dir',
|
targetDir: '/test/dir',
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
userMemory: '',
|
userMemory: '',
|
||||||
|
|||||||
Reference in New Issue
Block a user