mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
Improve code coverage for cli package (#13724)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user