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

@ -235,6 +235,7 @@ If you are experiencing performance issues with file searching (e.g., with `@` c
| Setting | Type | Description | Default | Notes |
| ------------------------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tools.sandbox` | boolean or string | Sandbox execution environment (can be a boolean or a path string). | `undefined` | |
| `tools.sandboxImage` | string | Sandbox image URI used by Docker/Podman when `--sandbox-image` and `QWEN_SANDBOX_IMAGE` are not set. | `undefined` | |
| `tools.shell.enableInteractiveShell` | boolean | Use `node-pty` for an interactive shell experience. Fallback to `child_process` still applies. | `false` | |
| `tools.core` | array of strings | **Deprecated.** Will be removed in next version. Use `permissions.allow` + `permissions.deny` instead. Restricts built-in tools to an allowlist. All tools not in the list are disabled. | `undefined` | |
| `tools.exclude` | array of strings | **Deprecated.** Use `permissions.deny` instead. Tool names to exclude from discovery. Automatically migrated to the `permissions` format on first load. | `undefined` | |
@ -442,6 +443,7 @@ Here is an example of a `settings.json` file with the nested structure, new as o
"tools": {
"approvalMode": "yolo",
"sandbox": "docker",
"sandboxImage": "ghcr.io/qwenlm/qwen-code:0.14.1",
"discoveryCommand": "bin/get_tools",
"callCommand": "bin/call_tool",
"exclude": ["write_file"]
@ -505,28 +507,32 @@ For authentication-related variables (like `OPENAI_*`) and the recommended `.qwe
### Environment Variables Table
| Variable | Description | Notes |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QWEN_TELEMETRY_ENABLED` | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. | Overrides the `telemetry.enabled` setting. |
| `QWEN_TELEMETRY_TARGET` | Sets the telemetry target (`local` or `gcp`). | Overrides the `telemetry.target` setting. |
| `QWEN_TELEMETRY_OTLP_ENDPOINT` | Sets the OTLP endpoint for telemetry. | Overrides the `telemetry.otlpEndpoint` setting. |
| `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. |
| `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. |
| `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to when the target is `local`. | Overrides the `telemetry.outfile` setting. |
| `QWEN_TELEMETRY_USE_COLLECTOR` | Set to `true` or `1` to enable or disable using an external OTLP collector. Any other value is treated as disabling it. | Overrides the `telemetry.useCollector` setting. |
| `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. |
| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). |
| `DEBUG` or `DEBUG_MODE` | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting. | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically. |
| `NO_COLOR` | Set to any value to disable all color output in the CLI. | |
| `CLI_TITLE` | Set to a string to customize the title of the CLI. | |
| `CODE_ASSIST_ENDPOINT` | Specifies the endpoint for the code assist server. | This is useful for development and testing. |
| Variable | Description | Notes |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `QWEN_TELEMETRY_ENABLED` | Set to `true` or `1` to enable telemetry. Any other value is treated as disabling it. | Overrides the `telemetry.enabled` setting. |
| `QWEN_TELEMETRY_TARGET` | Sets the telemetry target (`local` or `gcp`). | Overrides the `telemetry.target` setting. |
| `QWEN_TELEMETRY_OTLP_ENDPOINT` | Sets the OTLP endpoint for telemetry. | Overrides the `telemetry.otlpEndpoint` setting. |
| `QWEN_TELEMETRY_OTLP_PROTOCOL` | Sets the OTLP protocol (`grpc` or `http`). | Overrides the `telemetry.otlpProtocol` setting. |
| `QWEN_TELEMETRY_LOG_PROMPTS` | Set to `true` or `1` to enable or disable logging of user prompts. Any other value is treated as disabling it. | Overrides the `telemetry.logPrompts` setting. |
| `QWEN_TELEMETRY_OUTFILE` | Sets the file path to write telemetry to when the target is `local`. | Overrides the `telemetry.outfile` setting. |
| `QWEN_TELEMETRY_USE_COLLECTOR` | Set to `true` or `1` to enable or disable using an external OTLP collector. Any other value is treated as disabling it. | Overrides the `telemetry.useCollector` setting. |
| `QWEN_SANDBOX` | Alternative to the `sandbox` setting in `settings.json`. | Accepts `true`, `false`, `docker`, `podman`, or a custom command string. |
| `QWEN_SANDBOX_IMAGE` | Overrides sandbox image selection for Docker/Podman. | Takes precedence over `tools.sandboxImage`. |
| `SEATBELT_PROFILE` | (macOS specific) Switches the Seatbelt (`sandbox-exec`) profile on macOS. | `permissive-open`: (Default) Restricts writes to the project folder (and a few other folders, see `packages/cli/src/utils/sandbox-macos-permissive-open.sb`) but allows other operations. `strict`: Uses a strict profile that declines operations by default. `<profile_name>`: Uses a custom profile. To define a custom profile, create a file named `sandbox-macos-<profile_name>.sb` in your project's `.qwen/` directory (e.g., `my-project/.qwen/sandbox-macos-custom.sb`). |
| `DEBUG` or `DEBUG_MODE` | (often used by underlying libraries or the CLI itself) Set to `true` or `1` to enable verbose debug logging, which can be helpful for troubleshooting. | **Note:** These variables are automatically excluded from project `.env` files by default to prevent interference with the CLI behavior. Use `.qwen/.env` files if you need to set these for Qwen Code specifically. |
| `NO_COLOR` | Set to any value to disable all color output in the CLI. | |
| `CLI_TITLE` | Set to a string to customize the title of the CLI. | |
| `CODE_ASSIST_ENDPOINT` | Specifies the endpoint for the code assist server. | This is useful for development and testing. |
| `QWEN_CODE_MAX_OUTPUT_TOKENS` | Overrides the default maximum output tokens per response. When not set, Qwen Code uses an adaptive strategy: starts with 8K tokens and automatically retries with 64K if the response is truncated. Set this to a specific value (e.g., `16000`) to use a fixed limit instead. | Takes precedence over the capped default (8K) but is overridden by `samplingParams.max_tokens` in settings. Disables automatic escalation when set. Example: `export QWEN_CODE_MAX_OUTPUT_TOKENS=16000` |
| `TAVILY_API_KEY` | Your API key for the Tavily web search service. | Used to enable the `web_search` tool functionality. Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` |
| `TAVILY_API_KEY` | Your API key for the Tavily web search service. | Used to enable the `web_search` tool functionality. Example: `export TAVILY_API_KEY="tvly-your-api-key-here"` |
## Command-Line Arguments
Arguments passed directly when running the CLI can override other configurations for that specific session.
For sandbox image selection, precedence is:
`--sandbox-image` > `QWEN_SANDBOX_IMAGE` > `tools.sandboxImage` > built-in default image.
### Command-Line Arguments Table
| Argument | Alias | Description | Possible Values | Notes |

View file

@ -103,8 +103,16 @@ qwen -p "run the test suite"
- **CLI flag**: `--sandbox-image <image>`
- **Environment variable**: `QWEN_SANDBOX_IMAGE=<image>`
- **Settings file**: `tools.sandboxImage` in your `settings.json` (e.g., `{"tools": {"sandboxImage": "ghcr.io/qwenlm/qwen-code:0.14.1"}}`)
If you dont set either, Qwen Code uses the default image configured in the CLI package (for example `ghcr.io/qwenlm/qwen-code:<version>`).
Priority order (highest to lowest):
1. `--sandbox-image`
2. `QWEN_SANDBOX_IMAGE`
3. `tools.sandboxImage`
4. Built-in default image from the CLI package (for example `ghcr.io/qwenlm/qwen-code:<version>`)
`settings.env.QWEN_SANDBOX_IMAGE` also works as a generic env injection mechanism, but `tools.sandboxImage` is the preferred persistent setting.
### macOS Seatbelt profiles

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',

View file

@ -438,6 +438,10 @@
"type": "object",
"additionalProperties": true
},
"sandboxImage": {
"description": "Sandbox image URI used by Docker/Podman when --sandbox-image and QWEN_SANDBOX_IMAGE are not set.",
"type": "string"
},
"shell": {
"description": "Settings for shell execution.",
"type": "object",