feat(cli): support tools.sandboxImage in settings (#3146)

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
This commit is contained in:
jinye 2026-04-13 09:43:34 +08:00 committed by GitHub
parent 116796b2a4
commit 1557d93043
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 146 additions and 19 deletions

View file

@ -111,10 +111,21 @@ vi.mock('open', () => ({
vi.mock('read-package-up', () => ({
readPackageUp: vi.fn(() =>
Promise.resolve({ packageJson: { version: 'test-version' } }),
Promise.resolve({
packageJson: {
version: 'test-version',
config: { sandboxImageUri: 'pkg-default-image' },
},
}),
),
}));
vi.mock('command-exists', () => ({
default: {
sync: vi.fn(() => true),
},
}));
vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actualServer = await importOriginal<typeof ServerConfig>();
const SkillManagerMock = vi.fn();
@ -2441,6 +2452,83 @@ describe('Telemetry configuration via environment variables', () => {
});
});
describe('sandbox image resolution precedence', () => {
const originalArgv = process.argv;
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
delete process.env['QWEN_SANDBOX_IMAGE'];
});
afterEach(() => {
process.argv = originalArgv;
vi.unstubAllEnvs();
vi.restoreAllMocks();
delete process.env['QWEN_SANDBOX_IMAGE'];
});
it('uses --sandbox-image over env and settings', async () => {
vi.stubEnv('QWEN_SANDBOX_IMAGE', 'env-image');
process.argv = [
'node',
'script.js',
'--sandbox',
'--sandbox-image',
'cli-image',
];
const argv = await parseArguments();
const settings: Settings = {
tools: {
sandbox: true,
sandboxImage: 'settings-image',
},
};
const config = await loadCliConfig(settings, argv, undefined, []);
expect(config.getSandbox()?.image).toBe('cli-image');
});
it('uses QWEN_SANDBOX_IMAGE over tools.sandboxImage', async () => {
vi.stubEnv('QWEN_SANDBOX_IMAGE', 'env-image');
process.argv = ['node', 'script.js', '--sandbox'];
const argv = await parseArguments();
const settings: Settings = {
tools: {
sandbox: true,
sandboxImage: 'settings-image',
},
};
const config = await loadCliConfig(settings, argv, undefined, []);
expect(config.getSandbox()?.image).toBe('env-image');
});
it('uses tools.sandboxImage when cli and env are absent', async () => {
process.argv = ['node', 'script.js', '--sandbox'];
const argv = await parseArguments();
const settings: Settings = {
tools: {
sandbox: true,
sandboxImage: 'settings-image',
},
};
const config = await loadCliConfig(settings, argv, undefined, []);
expect(config.getSandbox()?.image).toBe('settings-image');
});
it('falls back to package default image when no explicit source is provided', async () => {
process.argv = ['node', 'script.js', '--sandbox'];
const argv = await parseArguments();
const settings: Settings = {
tools: {
sandbox: true,
},
};
const config = await loadCliConfig(settings, argv, undefined, []);
expect(config.getSandbox()?.image).toBe('pkg-default-image');
});
});
describe('loadCliConfig runtimeOutputDir', () => {
const originalArgv = process.argv;
const originalRuntimeEnv = process.env['QWEN_RUNTIME_DIR'];

View file

@ -514,7 +514,7 @@ export async function parseArguments(): Promise<CliArgs> {
})
.deprecateOption(
'sandbox-image',
'Use the "tools.sandbox" setting in settings.json instead. This flag will be removed in a future version.',
'Use the "tools.sandboxImage" setting in settings.json instead. This flag will be removed in a future version.',
)
.deprecateOption(
'checkpointing',

View file

@ -99,6 +99,7 @@ export async function loadSandboxConfig(
const image =
argv.sandboxImage ??
process.env['QWEN_SANDBOX_IMAGE'] ??
settings.tools?.sandboxImage ??
packageJson?.config?.sandboxImageUri;
return command && image ? { command, image } : undefined;

View file

@ -110,6 +110,16 @@ describe('SettingsSchema', () => {
).toBeDefined();
});
it('should have sandboxImage setting under tools', () => {
expect(getSettingsSchema().tools.properties.sandboxImage).toBeDefined();
expect(getSettingsSchema().tools.properties.sandboxImage.type).toBe(
'string',
);
expect(getSettingsSchema().tools.properties.sandboxImage.default).toBe(
undefined,
);
});
it('should have unique categories', () => {
const categories = new Set();

View file

@ -1014,6 +1014,16 @@ const SETTINGS_SCHEMA = {
'Sandbox execution environment (can be a boolean or a path string).',
showInDialog: false,
},
sandboxImage: {
type: 'string',
label: 'Sandbox Image',
category: 'Tools',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Sandbox image URI used by Docker/Podman when --sandbox-image and QWEN_SANDBOX_IMAGE are not set.',
showInDialog: false,
},
shell: {
type: 'object',
label: 'Shell',