mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-14 04:49:14 +00:00
Compare commits
1 Commits
feat/suppo
...
fix/nonint
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cb4371373 |
@@ -49,8 +49,6 @@ Cross-platform sandboxing with complete process isolation.
|
||||
|
||||
By default, Qwen Code uses a published sandbox image (configured in the CLI package) and will pull it as needed.
|
||||
|
||||
The container sandbox mounts your workspace and your `~/.qwen` directory into the container so auth and settings persist between runs.
|
||||
|
||||
**Best for**: Strong isolation on any OS, consistent tooling inside a known image.
|
||||
|
||||
### Choosing a method
|
||||
@@ -159,7 +157,7 @@ For a working allowlist-style proxy example, see: [Example Proxy Script](/develo
|
||||
|
||||
## Linux UID/GID handling
|
||||
|
||||
On Linux, Qwen Code defaults to enabling UID/GID mapping so the sandbox runs as your user (and reuses the mounted `~/.qwen`). Override with:
|
||||
The sandbox automatically handles user permissions on Linux. Override these permissions with:
|
||||
|
||||
```bash
|
||||
export SANDBOX_SET_UID_GID=true # Force host UID/GID
|
||||
|
||||
@@ -1826,7 +1826,7 @@ describe('runNonInteractive', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should print tool output to console in text mode (non-Task tools)', async () => {
|
||||
it('should print tool description and output to console in text mode (non-Task tools)', async () => {
|
||||
// Test that tool output is printed to stdout in text mode
|
||||
const toolCallEvent: ServerGeminiStreamEvent = {
|
||||
type: GeminiEventType.ToolCallRequest,
|
||||
@@ -1839,6 +1839,21 @@ describe('runNonInteractive', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the tool registry to return a tool with displayName and build method
|
||||
const mockTool = {
|
||||
displayName: 'Shell',
|
||||
build: (args: Record<string, unknown>) => {
|
||||
// @ts-expect-error - accessing indexed property for test mock
|
||||
const command: string = args.command || '';
|
||||
return {
|
||||
getDescription: () => String(command),
|
||||
};
|
||||
},
|
||||
};
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(
|
||||
mockTool as unknown as ReturnType<typeof mockToolRegistry.getTool>,
|
||||
);
|
||||
|
||||
// Mock tool execution with outputUpdateHandler being called
|
||||
mockCoreExecuteToolCall.mockImplementation(
|
||||
async (_config, _request, _signal, options) => {
|
||||
@@ -1901,8 +1916,15 @@ describe('runNonInteractive', () => {
|
||||
);
|
||||
|
||||
// Verify tool output was written to stdout
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Package outdated\n');
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('npm@1.0.0 -> npm@2.0.0\n');
|
||||
// First call should be tool description
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Shell: npm outdated');
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('\n');
|
||||
// Then the actual tool output
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Package outdated');
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('npm@1.0.0 -> npm@2.0.0');
|
||||
// Final newline after tool execution
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('\n');
|
||||
// And the model's response
|
||||
expect(processStdoutSpy).toHaveBeenCalledWith('Dependencies checked');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -351,19 +351,51 @@ export async function runNonInteractive(
|
||||
const taskToolProgressHandler = taskToolProgress?.handler;
|
||||
|
||||
// Create output handler for non-Task tools in text mode (for console output)
|
||||
const toolOutputLines: string[] = [];
|
||||
const nonTaskOutputHandler =
|
||||
!isTaskTool && !adapter
|
||||
? (callId: string, outputChunk: ToolResultDisplay) => {
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
const tool = toolRegistry.getTool(finalRequestInfo.name);
|
||||
if (tool) {
|
||||
try {
|
||||
const invocation = tool.build(finalRequestInfo.args);
|
||||
const description = invocation.getDescription();
|
||||
toolOutputLines.push(
|
||||
`${tool.displayName}: ${description}`,
|
||||
);
|
||||
toolOutputLines.push('\n');
|
||||
} catch {
|
||||
// If we can't build invocation, just show tool name
|
||||
toolOutputLines.push(`${tool.displayName}`);
|
||||
toolOutputLines.push('\n');
|
||||
}
|
||||
}
|
||||
// Print tool output to console in text mode
|
||||
if (typeof outputChunk === 'string') {
|
||||
process.stdout.write(outputChunk);
|
||||
// Indent output lines to show they're part of the tool execution
|
||||
const lines = outputChunk.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (i === lines.length - 1 && lines[i] === '') {
|
||||
// Skip trailing empty line
|
||||
continue;
|
||||
}
|
||||
toolOutputLines.push(lines[i]);
|
||||
}
|
||||
} else if (
|
||||
outputChunk &&
|
||||
typeof outputChunk === 'object' &&
|
||||
'ansiOutput' in outputChunk
|
||||
) {
|
||||
// Handle ANSI output - just print as string for now
|
||||
process.stdout.write(String(outputChunk.ansiOutput));
|
||||
// Handle ANSI output - indent it similarly
|
||||
const ansiStr = String(outputChunk.ansiOutput);
|
||||
const lines = ansiStr.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (i === lines.length - 1 && lines[i] === '') {
|
||||
continue;
|
||||
}
|
||||
toolOutputLines.push(lines[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
@@ -386,6 +418,11 @@ export async function runNonInteractive(
|
||||
: undefined,
|
||||
);
|
||||
|
||||
if (toolOutputLines.length > 0) {
|
||||
toolOutputLines.forEach((line) => process.stdout.write(line));
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
|
||||
// Note: In JSON mode, subagent messages are automatically added to the main
|
||||
// adapter's messages array and will be output together on emitResult()
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { exec, execSync, spawn, type ChildProcess } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { quote, parse } from 'shell-quote';
|
||||
import {
|
||||
@@ -49,16 +50,16 @@ const BUILTIN_SEATBELT_PROFILES = [
|
||||
|
||||
/**
|
||||
* Determines whether the sandbox container should be run with the current user's UID and GID.
|
||||
* This is often necessary on Linux systems when using rootful Docker without userns-remap
|
||||
* configured, to avoid permission issues with
|
||||
* This is often necessary on Linux systems (especially Debian/Ubuntu based) when using
|
||||
* rootful Docker without userns-remap configured, to avoid permission issues with
|
||||
* mounted volumes.
|
||||
*
|
||||
* The behavior is controlled by the `SANDBOX_SET_UID_GID` environment variable:
|
||||
* - If `SANDBOX_SET_UID_GID` is "1" or "true", this function returns `true`.
|
||||
* - If `SANDBOX_SET_UID_GID` is "0" or "false", this function returns `false`.
|
||||
* - If `SANDBOX_SET_UID_GID` is not set:
|
||||
* - On Linux, it defaults to `true`.
|
||||
* - On other OSes, it defaults to `false`.
|
||||
* - On Debian/Ubuntu Linux, it defaults to `true`.
|
||||
* - On other OSes, or if OS detection fails, it defaults to `false`.
|
||||
*
|
||||
* For more context on running Docker containers as non-root, see:
|
||||
* https://medium.com/redbubble/running-a-docker-container-as-a-non-root-user-7d2e00f8ee15
|
||||
@@ -75,20 +76,31 @@ async function shouldUseCurrentUserInSandbox(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If environment variable is not explicitly set, check for Debian/Ubuntu Linux
|
||||
if (os.platform() === 'linux') {
|
||||
const debugEnv = [process.env['DEBUG'], process.env['DEBUG_MODE']].some(
|
||||
(v) => v === 'true' || v === '1',
|
||||
);
|
||||
if (debugEnv) {
|
||||
// Use stderr so it doesn't clutter normal STDOUT output (e.g. in `--prompt` runs).
|
||||
console.error(
|
||||
'INFO: Using current user UID/GID in Linux sandbox. Set SANDBOX_SET_UID_GID=false to disable.',
|
||||
try {
|
||||
const osReleaseContent = await readFile('/etc/os-release', 'utf8');
|
||||
if (
|
||||
osReleaseContent.includes('ID=debian') ||
|
||||
osReleaseContent.includes('ID=ubuntu') ||
|
||||
osReleaseContent.match(/^ID_LIKE=.*debian.*/m) || // Covers derivatives
|
||||
osReleaseContent.match(/^ID_LIKE=.*ubuntu.*/m) // Covers derivatives
|
||||
) {
|
||||
// note here and below we use console.error for informational messages on stderr
|
||||
console.error(
|
||||
'INFO: Defaulting to use current user UID/GID for Debian/Ubuntu-based Linux.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} catch (_err) {
|
||||
// Silently ignore if /etc/os-release is not found or unreadable.
|
||||
// The default (false) will be applied in this case.
|
||||
console.warn(
|
||||
'Warning: Could not read /etc/os-release to auto-detect Debian/Ubuntu for UID/GID default.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false; // Default to false if no other condition is met
|
||||
}
|
||||
|
||||
// docker does not allow container names to contain ':' or '/', so we
|
||||
|
||||
@@ -818,7 +818,7 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
});
|
||||
|
||||
describe('Platform-Specific Behavior', () => {
|
||||
it('should use cmd.exe and hide window on Windows', async () => {
|
||||
it('should use cmd.exe on Windows', async () => {
|
||||
mockPlatform.mockReturnValue('win32');
|
||||
await simulateExecution('dir "foo bar"', (cp) =>
|
||||
cp.emit('exit', 0, null),
|
||||
@@ -829,8 +829,7 @@ describe('ShellExecutionService child_process fallback', () => {
|
||||
[],
|
||||
expect.objectContaining({
|
||||
shell: true,
|
||||
detached: false,
|
||||
windowsHide: true,
|
||||
detached: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -229,8 +229,7 @@ export class ShellExecutionService {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsVerbatimArguments: true,
|
||||
shell: isWindows ? true : 'bash',
|
||||
detached: !isWindows,
|
||||
windowsHide: isWindows,
|
||||
detached: true,
|
||||
env: {
|
||||
...process.env,
|
||||
QWEN_CODE: '1',
|
||||
|
||||
Reference in New Issue
Block a user