diff --git a/docs/cli/settings.md b/docs/cli/settings.md index ba6e0ed316..cd2eef281f 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -131,6 +131,7 @@ they appear in the UI. | UI Label | Setting | Description | Default | | -------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| Sandbox Set Hostname | `tools.sandboxSetHostname` | Whether to set the hostname in the sandbox container. Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments. | `true` | | Sandbox Allowed Paths | `tools.sandboxAllowedPaths` | List of additional paths that the sandbox is allowed to access. | `[]` | | Sandbox Network Access | `tools.sandboxNetworkAccess` | Whether the sandbox is allowed to access the network. | `false` | | Enable Interactive Shell | `tools.shell.enableInteractiveShell` | Use node-pty for an interactive shell experience. Fallback to child_process still applies. | `true` | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index ad44eb1256..15a0dabb5f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1553,6 +1553,13 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `undefined` - **Requires restart:** Yes +- **`tools.sandboxSetHostname`** (boolean): + - **Description:** Whether to set the hostname in the sandbox container. Set + to false to avoid Mutating UTS namespace, which is required for some + rootless container environments. + - **Default:** `true` + - **Requires restart:** Yes + - **`tools.sandboxAllowedPaths`** (array): - **Description:** List of additional paths that the sandbox is allowed to access. diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index 07685e9bea..eb0b5f3da4 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -132,6 +132,16 @@ export async function loadSandboxConfig( let sandboxValue: boolean | string | null | undefined; let allowedPaths: string[] = []; let networkAccess = true; + let setHostname = settings.tools?.sandboxSetHostname ?? true; + + // Environment variable override + const envSetHostname = process.env['GEMINI_CLI_SANDBOX_SET_HOSTNAME']?.toLowerCase().trim(); + if (envSetHostname === 'false') { + setHostname = false; + } else if (envSetHostname === 'true') { + setHostname = true; + } + let customImage: string | undefined; if ( @@ -143,6 +153,9 @@ export async function loadSandboxConfig( sandboxValue = config.enabled ? (config.command ?? true) : false; allowedPaths = config.allowedPaths ?? []; networkAccess = config.networkAccess ?? true; + if (config.setHostname !== undefined) { + setHostname = config.setHostname; + } customImage = config.image; } else if (typeof sandboxOption !== 'object' || sandboxOption === null) { sandboxValue = sandboxOption; @@ -163,6 +176,13 @@ export async function loadSandboxConfig( command === 'lxc'; return command && (image || isNative) - ? { enabled: true, allowedPaths, networkAccess, command, image } + ? { + enabled: true, + allowedPaths, + networkAccess, + command, + image, + setHostname, + } : undefined; } diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index e3d68415e9..a639f60001 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1552,6 +1552,18 @@ const SETTINGS_SCHEMA = { `, showInDialog: false, }, + sandboxSetHostname: { + type: 'boolean', + label: 'Sandbox Set Hostname', + category: 'Tools', + requiresRestart: true, + default: true, + description: oneLine` + Whether to set the hostname in the sandbox container. + Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments. + `, + showInDialog: true, + }, sandboxAllowedPaths: { type: 'array', label: 'Sandbox Allowed Paths', diff --git a/packages/cli/src/utils/sandbox.test.ts b/packages/cli/src/utils/sandbox.test.ts index 79bf8d5bdc..7d38f3b5d1 100644 --- a/packages/cli/src/utils/sandbox.test.ts +++ b/packages/cli/src/utils/sandbox.test.ts @@ -419,6 +419,56 @@ describe('sandbox', () => { ); }); + it('should skip setting hostname if setHostname is false', async () => { + const config: SandboxConfig = createMockSandboxConfig({ + command: 'docker', + image: 'gemini-cli-sandbox', + setHostname: false, + }); + + interface MockProcessWithStdout extends EventEmitter { + stdout: EventEmitter; + } + const mockImageCheckProcess = new EventEmitter() as MockProcessWithStdout; + mockImageCheckProcess.stdout = new EventEmitter(); + vi.mocked(spawn).mockImplementationOnce(() => { + setTimeout(() => { + mockImageCheckProcess.stdout.emit('data', Buffer.from('image-id')); + mockImageCheckProcess.emit('close', 0); + }, 1); + return mockImageCheckProcess as unknown as ReturnType; + }); + + const mockSpawnProcess = new EventEmitter() as unknown as ReturnType< + typeof spawn + >; + mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => { + if (event === 'close') { + setTimeout(() => cb(0), 10); + } + return mockSpawnProcess; + }); + vi.mocked(spawn).mockImplementationOnce(() => mockSpawnProcess); + + await expect( + start_sandbox(config, [], undefined, ['arg1']), + ).resolves.toBe(0); + + const containerName = 'gemini-cli-sandbox-a1b2c3d4e5f6'; + expect(spawn).toHaveBeenNthCalledWith( + 2, + 'docker', + expect.not.arrayContaining(['--hostname']), + expect.objectContaining({ stdio: 'inherit' }), + ); + expect(spawn).toHaveBeenNthCalledWith( + 2, + 'docker', + expect.arrayContaining(['--name', containerName]), + expect.anything(), + ); + }); + it('should pull image if missing', async () => { const config: SandboxConfig = createMockSandboxConfig({ command: 'docker', diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index abefd101d4..4a812719cc 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -509,7 +509,10 @@ export async function start_sandbox( 'hex', )}`; debugLogger.log(`ContainerName: ${containerName}`); - args.push('--name', containerName, '--hostname', containerName); + args.push('--name', containerName); + if (config.setHostname !== false) { + args.push('--hostname', containerName); + } // copy GEMINI_CLI_TEST_VAR for integration tests if (process.env['GEMINI_CLI_TEST_VAR']) { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 4429ce5de1..77194b6d5d 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -523,6 +523,7 @@ export interface SandboxConfig { allowedPaths?: string[]; includeDirectories?: string[]; networkAccess?: boolean; + setHostname?: boolean; command?: | 'docker' | 'podman' @@ -540,6 +541,7 @@ export const ConfigSchema = z.object({ allowedPaths: z.array(z.string()).default([]), includeDirectories: z.array(z.string()).default([]), networkAccess: z.boolean().default(false), + setHostname: z.boolean().default(true), command: z .enum([ 'docker', diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 248bef489d..31945d5bf4 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -2730,6 +2730,13 @@ "markdownDescription": "Legacy full-process sandbox execution environment. Set to a boolean to enable or disable the sandbox, provide a string path to a sandbox profile, or specify an explicit sandbox command (e.g., \"docker\", \"podman\", \"lxc\", \"windows-native\").\n\n- Category: `Tools`\n- Requires restart: `yes`", "$ref": "#/$defs/BooleanOrStringOrObject" }, + "sandboxSetHostname": { + "title": "Sandbox Set Hostname", + "description": "Whether to set the hostname in the sandbox container. Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments.", + "markdownDescription": "Whether to set the hostname in the sandbox container. Set to false to avoid Mutating UTS namespace, which is required for some rootless container environments.\n\n- Category: `Tools`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, + "type": "boolean" + }, "sandboxAllowedPaths": { "title": "Sandbox Allowed Paths", "description": "List of additional paths that the sandbox is allowed to access.",