Improve code coverage for cli package (#13724)

This commit is contained in:
Megha Bansal
2025-11-24 23:11:46 +05:30
committed by GitHub
parent 569c6f1dd0
commit 95693e265e
47 changed files with 5115 additions and 489 deletions
+18 -167
View File
@@ -8,7 +8,6 @@ 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 { USER_SETTINGS_DIR } from '../config/settings.js';
@@ -22,166 +21,20 @@ import {
} from '@google/gemini-cli-core';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
import { randomBytes } from 'node:crypto';
import {
getContainerPath,
shouldUseCurrentUserInSandbox,
parseImageName,
ports,
entrypoint,
LOCAL_DEV_SANDBOX_IMAGE_NAME,
SANDBOX_NETWORK_NAME,
SANDBOX_PROXY_NAME,
BUILTIN_SEATBELT_PROFILES,
} from './sandboxUtils.js';
const execAsync = promisify(exec);
function getContainerPath(hostPath: string): string {
if (os.platform() !== 'win32') {
return hostPath;
}
const withForwardSlashes = hostPath.replace(/\\/g, '/');
const match = withForwardSlashes.match(/^([A-Z]):\/(.*)/i);
if (match) {
return `/${match[1].toLowerCase()}/${match[2]}`;
}
return hostPath;
}
const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'gemini-cli-sandbox';
const SANDBOX_NETWORK_NAME = 'gemini-cli-sandbox';
const SANDBOX_PROXY_NAME = 'gemini-cli-sandbox-proxy';
const BUILTIN_SEATBELT_PROFILES = [
'permissive-open',
'permissive-closed',
'permissive-proxied',
'restrictive-open',
'restrictive-closed',
'restrictive-proxied',
];
/**
* Determines whether the sandbox container should be run with the current user's UID and GID.
* 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 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
*
* @returns {Promise<boolean>} A promise that resolves to true if the current user's UID/GID should be used, false otherwise.
*/
async function shouldUseCurrentUserInSandbox(): Promise<boolean> {
const envVar = process.env['SANDBOX_SET_UID_GID']?.toLowerCase().trim();
if (envVar === '1' || envVar === 'true') {
return true;
}
if (envVar === '0' || envVar === 'false') {
return false;
}
// If environment variable is not explicitly set, check for Debian/Ubuntu Linux
if (os.platform() === 'linux') {
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
) {
debugLogger.log(
'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.
debugLogger.warn(
'Warning: Could not read /etc/os-release to auto-detect Debian/Ubuntu for UID/GID default.',
);
}
}
return false; // Default to false if no other condition is met
}
// docker does not allow container names to contain ':' or '/', so we
// parse those out to shorten the name
function parseImageName(image: string): string {
const [fullName, tag] = image.split(':');
const name = fullName.split('/').at(-1) ?? 'unknown-image';
return tag ? `${name}-${tag}` : name;
}
function ports(): string[] {
return (process.env['SANDBOX_PORTS'] ?? '')
.split(',')
.filter((p) => p.trim())
.map((p) => p.trim());
}
function entrypoint(workdir: string, cliArgs: string[]): string[] {
const isWindows = os.platform() === 'win32';
const containerWorkdir = getContainerPath(workdir);
const shellCmds = [];
const pathSeparator = isWindows ? ';' : ':';
let pathSuffix = '';
if (process.env['PATH']) {
const paths = process.env['PATH'].split(pathSeparator);
for (const p of paths) {
const containerPath = getContainerPath(p);
if (
containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase())
) {
pathSuffix += `:${containerPath}`;
}
}
}
if (pathSuffix) {
shellCmds.push(`export PATH="$PATH${pathSuffix}";`);
}
let pythonPathSuffix = '';
if (process.env['PYTHONPATH']) {
const paths = process.env['PYTHONPATH'].split(pathSeparator);
for (const p of paths) {
const containerPath = getContainerPath(p);
if (
containerPath.toLowerCase().startsWith(containerWorkdir.toLowerCase())
) {
pythonPathSuffix += `:${containerPath}`;
}
}
}
if (pythonPathSuffix) {
shellCmds.push(`export PYTHONPATH="$PYTHONPATH${pythonPathSuffix}";`);
}
const projectSandboxBashrc = path.join(GEMINI_DIR, 'sandbox.bashrc');
if (fs.existsSync(projectSandboxBashrc)) {
shellCmds.push(`source ${getContainerPath(projectSandboxBashrc)};`);
}
ports().forEach((p) =>
shellCmds.push(
`socat TCP4-LISTEN:${p},bind=$(hostname -i),fork,reuseaddr TCP4:127.0.0.1:${p} 2> /dev/null &`,
),
);
const quotedCliArgs = cliArgs.slice(2).map((arg) => quote([arg]));
const cliCmd =
process.env['NODE_ENV'] === 'development'
? process.env['DEBUG']
? 'npm run debug --'
: 'npm rebuild && npm run start --'
: process.env['DEBUG']
? `node --inspect-brk=0.0.0.0:${process.env['DEBUG_PORT'] || '9229'} $(which gemini)`
: 'gemini';
const args = [...shellCmds, cliCmd, ...quotedCliArgs];
return ['bash', '-c', args.join(' ')];
}
export async function start_sandbox(
config: SandboxConfig,
nodeArgs: string[] = [],
@@ -231,7 +84,7 @@ export async function start_sandbox(
'-D',
`HOME_DIR=${fs.realpathSync(os.homedir())}`,
'-D',
`CACHE_DIR=${fs.realpathSync(execSync(`getconf DARWIN_USER_CACHE_DIR`).toString().trim())}`,
`CACHE_DIR=${fs.realpathSync((await execAsync('getconf DARWIN_USER_CACHE_DIR')).stdout.trim())}`,
];
// Add included directories from the workspace context
@@ -350,7 +203,7 @@ export async function start_sandbox(
debugLogger.log(`hopping into sandbox (command: ${config.command}) ...`);
// determine full path for gemini-cli to distinguish linked vs installed setting
const gcPath = fs.realpathSync(process.argv[1]);
const gcPath = process.argv[1] ? fs.realpathSync(process.argv[1]) : '';
const projectSandboxDockerfile = path.join(
GEMINI_DIR,
@@ -565,11 +418,9 @@ export async function start_sandbox(
debugLogger.log(`ContainerName: ${containerName}`);
} else {
let index = 0;
const containerNameCheck = execSync(
`${config.command} ps -a --format "{{.Names}}"`,
)
.toString()
.trim();
const containerNameCheck = (
await execAsync(`${config.command} ps -a --format "{{.Names}}"`)
).stdout.trim();
while (containerNameCheck.includes(`${imageName}-${index}`)) {
index++;
}
@@ -723,8 +574,8 @@ export async function start_sandbox(
// The entrypoint script then handles dropping privileges to the correct user.
args.push('--user', 'root');
const uid = execSync('id -u').toString().trim();
const gid = execSync('id -g').toString().trim();
const uid = (await execAsync('id -u')).stdout.trim();
const gid = (await execAsync('id -g')).stdout.trim();
// Instead of passing --user to the main sandbox container, we let it
// start as root, then create a user with the host's UID/GID, and