Your IP : 216.73.216.74


Current Path : /proc/self/root/usr/local/lib/node_modules/@google/gemini-cli/dist/src/utils/
Upload File :
Current File : //proc/self/root/usr/local/lib/node_modules/@google/gemini-cli/dist/src/utils/sandbox.js

/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { exec, execSync, spawn } 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 { quote } from 'shell-quote';
import { USER_SETTINGS_DIR, SETTINGS_DIRECTORY_NAME, } from '../config/settings.js';
import { promisify } from 'util';
const execAsync = promisify(exec);
function getContainerPath(hostPath) {
    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() {
    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
            ) {
                // 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 false; // Default to false if no other condition is met
}
// docker does not allow container names to contain ':' or '/', so we
// parse those out and make the name a little shorter
function parseImageName(image) {
    const [fullName, tag] = image.split(':');
    const name = fullName.split('/').at(-1) ?? 'unknown-image';
    return tag ? `${name}-${tag}` : name;
}
function ports() {
    return (process.env.SANDBOX_PORTS ?? '')
        .split(',')
        .filter((p) => p.trim())
        .map((p) => p.trim());
}
function entrypoint(workdir) {
    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(SETTINGS_DIRECTORY_NAME, '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 cliArgs = process.argv.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, ...cliArgs];
    return ['bash', '-c', args.join(' ')];
}
export async function start_sandbox(config, nodeArgs = []) {
    if (config.command === 'sandbox-exec') {
        // disallow BUILD_SANDBOX
        if (process.env.BUILD_SANDBOX) {
            console.error('ERROR: cannot BUILD_SANDBOX when using MacOS Seatbelt');
            process.exit(1);
        }
        const profile = (process.env.SEATBELT_PROFILE ??= 'permissive-open');
        let profileFile = new URL(`sandbox-macos-${profile}.sb`, import.meta.url)
            .pathname;
        // if profile name is not recognized, then look for file under project settings directory
        if (!BUILTIN_SEATBELT_PROFILES.includes(profile)) {
            profileFile = path.join(SETTINGS_DIRECTORY_NAME, `sandbox-macos-${profile}.sb`);
        }
        if (!fs.existsSync(profileFile)) {
            console.error(`ERROR: missing macos seatbelt profile file '${profileFile}'`);
            process.exit(1);
        }
        // Log on STDERR so it doesn't clutter the output on STDOUT
        console.error(`using macos seatbelt (profile: ${profile}) ...`);
        // if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS
        const nodeOptions = [
            ...(process.env.DEBUG ? ['--inspect-brk'] : []),
            ...nodeArgs,
        ].join(' ');
        const args = [
            '-D',
            `TARGET_DIR=${fs.realpathSync(process.cwd())}`,
            '-D',
            `TMP_DIR=${fs.realpathSync(os.tmpdir())}`,
            '-D',
            `HOME_DIR=${fs.realpathSync(os.homedir())}`,
            '-D',
            `CACHE_DIR=${fs.realpathSync(execSync(`getconf DARWIN_USER_CACHE_DIR`).toString().trim())}`,
            '-f',
            profileFile,
            'sh',
            '-c',
            [
                `SANDBOX=sandbox-exec`,
                `NODE_OPTIONS="${nodeOptions}"`,
                ...process.argv.map((arg) => quote([arg])),
            ].join(' '),
        ];
        // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
        const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
        let proxyProcess = undefined;
        let sandboxProcess = undefined;
        const sandboxEnv = { ...process.env };
        if (proxyCommand) {
            const proxy = process.env.HTTPS_PROXY ||
                process.env.https_proxy ||
                process.env.HTTP_PROXY ||
                process.env.http_proxy ||
                'http://localhost:8877';
            sandboxEnv['HTTPS_PROXY'] = proxy;
            sandboxEnv['https_proxy'] = proxy; // lower-case can be required, e.g. for curl
            sandboxEnv['HTTP_PROXY'] = proxy;
            sandboxEnv['http_proxy'] = proxy;
            const noProxy = process.env.NO_PROXY || process.env.no_proxy;
            if (noProxy) {
                sandboxEnv['NO_PROXY'] = noProxy;
                sandboxEnv['no_proxy'] = noProxy;
            }
            proxyProcess = spawn(proxyCommand, {
                stdio: ['ignore', 'pipe', 'pipe'],
                shell: true,
                detached: true,
            });
            // install handlers to stop proxy on exit/signal
            const stopProxy = () => {
                console.log('stopping proxy ...');
                if (proxyProcess?.pid) {
                    process.kill(-proxyProcess.pid, 'SIGTERM');
                }
            };
            process.on('exit', stopProxy);
            process.on('SIGINT', stopProxy);
            process.on('SIGTERM', stopProxy);
            // commented out as it disrupts ink rendering
            // proxyProcess.stdout?.on('data', (data) => {
            //   console.info(data.toString());
            // });
            proxyProcess.stderr?.on('data', (data) => {
                console.error(data.toString());
            });
            proxyProcess.on('close', (code, signal) => {
                console.error(`ERROR: proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`);
                if (sandboxProcess?.pid) {
                    process.kill(-sandboxProcess.pid, 'SIGTERM');
                }
                process.exit(1);
            });
            console.log('waiting for proxy to start ...');
            await execAsync(`until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`);
        }
        // spawn child and let it inherit stdio
        sandboxProcess = spawn(config.command, args, {
            stdio: 'inherit',
        });
        await new Promise((resolve) => sandboxProcess?.on('close', resolve));
        return;
    }
    console.error(`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 projectSandboxDockerfile = path.join(SETTINGS_DIRECTORY_NAME, 'sandbox.Dockerfile');
    const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile);
    const image = config.image;
    const workdir = path.resolve(process.cwd());
    const containerWorkdir = getContainerPath(workdir);
    // if BUILD_SANDBOX is set, then call scripts/build_sandbox.js under gemini-cli repo
    //
    // note this can only be done with binary linked from gemini-cli repo
    if (process.env.BUILD_SANDBOX) {
        if (!gcPath.includes('gemini-cli/packages/')) {
            console.error('ERROR: cannot build sandbox using installed gemini binary; ' +
                'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.');
            process.exit(1);
        }
        else {
            console.error('building sandbox ...');
            const gcRoot = gcPath.split('/packages/')[0];
            // if project folder has sandbox.Dockerfile under project settings folder, use that
            let buildArgs = '';
            const projectSandboxDockerfile = path.join(SETTINGS_DIRECTORY_NAME, 'sandbox.Dockerfile');
            if (isCustomProjectSandbox) {
                console.error(`using ${projectSandboxDockerfile} for sandbox`);
                buildArgs += `-f ${path.resolve(projectSandboxDockerfile)} -i ${image}`;
            }
            execSync(`cd ${gcRoot} && node scripts/build_sandbox.js -s ${buildArgs}`, {
                stdio: 'inherit',
                env: {
                    ...process.env,
                    GEMINI_SANDBOX: config.command, // in case sandbox is enabled via flags (see config.ts under cli package)
                },
            });
        }
    }
    // stop if image is missing
    if (!(await ensureSandboxImageIsPresent(config.command, image))) {
        const remedy = image === LOCAL_DEV_SANDBOX_IMAGE_NAME
            ? 'Try running `npm run build:all` or `npm run build:sandbox` under the gemini-cli repo to build it locally, or check the image name and your network connection.'
            : 'Please check the image name, your network connection, or notify gemini-cli-dev@google.com if the issue persists.';
        console.error(`ERROR: Sandbox image '${image}' is missing or could not be pulled. ${remedy}`);
        process.exit(1);
    }
    // use interactive mode and auto-remove container on exit
    // run init binary inside container to forward signals & reap zombies
    const args = ['run', '-i', '--rm', '--init', '--workdir', containerWorkdir];
    // add TTY only if stdin is TTY as well, i.e. for piped input don't init TTY in container
    if (process.stdin.isTTY) {
        args.push('-t');
    }
    // mount current directory as working directory in sandbox (set via --workdir)
    args.push('--volume', `${workdir}:${containerWorkdir}`);
    // mount user settings directory inside container, after creating if missing
    // note user/home changes inside sandbox and we mount at BOTH paths for consistency
    const userSettingsDirOnHost = USER_SETTINGS_DIR;
    const userSettingsDirInSandbox = getContainerPath(`/home/node/${SETTINGS_DIRECTORY_NAME}`);
    if (!fs.existsSync(userSettingsDirOnHost)) {
        fs.mkdirSync(userSettingsDirOnHost);
    }
    args.push('--volume', `${userSettingsDirOnHost}:${userSettingsDirInSandbox}`);
    if (userSettingsDirInSandbox !== userSettingsDirOnHost) {
        args.push('--volume', `${userSettingsDirOnHost}:${getContainerPath(userSettingsDirOnHost)}`);
    }
    // mount os.tmpdir() as os.tmpdir() inside container
    args.push('--volume', `${os.tmpdir()}:${getContainerPath(os.tmpdir())}`);
    // mount gcloud config directory if it exists
    const gcloudConfigDir = path.join(os.homedir(), '.config', 'gcloud');
    if (fs.existsSync(gcloudConfigDir)) {
        args.push('--volume', `${gcloudConfigDir}:${getContainerPath(gcloudConfigDir)}:ro`);
    }
    // mount ADC file if GOOGLE_APPLICATION_CREDENTIALS is set
    if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
        const adcFile = process.env.GOOGLE_APPLICATION_CREDENTIALS;
        if (fs.existsSync(adcFile)) {
            args.push('--volume', `${adcFile}:${getContainerPath(adcFile)}:ro`);
            args.push('--env', `GOOGLE_APPLICATION_CREDENTIALS=${getContainerPath(adcFile)}`);
        }
    }
    // mount paths listed in SANDBOX_MOUNTS
    if (process.env.SANDBOX_MOUNTS) {
        for (let mount of process.env.SANDBOX_MOUNTS.split(',')) {
            if (mount.trim()) {
                // parse mount as from:to:opts
                let [from, to, opts] = mount.trim().split(':');
                to = to || from; // default to mount at same path inside container
                opts = opts || 'ro'; // default to read-only
                mount = `${from}:${to}:${opts}`;
                // check that from path is absolute
                if (!path.isAbsolute(from)) {
                    console.error(`ERROR: path '${from}' listed in SANDBOX_MOUNTS must be absolute`);
                    process.exit(1);
                }
                // check that from path exists on host
                if (!fs.existsSync(from)) {
                    console.error(`ERROR: missing mount path '${from}' listed in SANDBOX_MOUNTS`);
                    process.exit(1);
                }
                console.error(`SANDBOX_MOUNTS: ${from} -> ${to} (${opts})`);
                args.push('--volume', mount);
            }
        }
    }
    // expose env-specified ports on the sandbox
    ports().forEach((p) => args.push('--publish', `${p}:${p}`));
    // if DEBUG is set, expose debugging port
    if (process.env.DEBUG) {
        const debugPort = process.env.DEBUG_PORT || '9229';
        args.push(`--publish`, `${debugPort}:${debugPort}`);
    }
    // copy proxy environment variables, replacing localhost with SANDBOX_PROXY_NAME
    // copy as both upper-case and lower-case as is required by some utilities
    // GEMINI_SANDBOX_PROXY_COMMAND implies HTTPS_PROXY unless HTTP_PROXY is set
    const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND;
    if (proxyCommand) {
        let proxy = process.env.HTTPS_PROXY ||
            process.env.https_proxy ||
            process.env.HTTP_PROXY ||
            process.env.http_proxy ||
            'http://localhost:8877';
        proxy = proxy.replace('localhost', SANDBOX_PROXY_NAME);
        if (proxy) {
            args.push('--env', `HTTPS_PROXY=${proxy}`);
            args.push('--env', `https_proxy=${proxy}`); // lower-case can be required, e.g. for curl
            args.push('--env', `HTTP_PROXY=${proxy}`);
            args.push('--env', `http_proxy=${proxy}`);
        }
        const noProxy = process.env.NO_PROXY || process.env.no_proxy;
        if (noProxy) {
            args.push('--env', `NO_PROXY=${noProxy}`);
            args.push('--env', `no_proxy=${noProxy}`);
        }
        // if using proxy, switch to internal networking through proxy
        if (proxy) {
            execSync(`${config.command} network inspect ${SANDBOX_NETWORK_NAME} || ${config.command} network create --internal ${SANDBOX_NETWORK_NAME}`);
            args.push('--network', SANDBOX_NETWORK_NAME);
            // if proxy command is set, create a separate network w/ host access (i.e. non-internal)
            // we will run proxy in its own container connected to both host network and internal network
            // this allows proxy to work even on rootless podman on macos with host<->vm<->container isolation
            if (proxyCommand) {
                execSync(`${config.command} network inspect ${SANDBOX_PROXY_NAME} || ${config.command} network create ${SANDBOX_PROXY_NAME}`);
            }
        }
    }
    // name container after image, plus numeric suffix to avoid conflicts
    const imageName = parseImageName(image);
    let index = 0;
    const containerNameCheck = execSync(`${config.command} ps -a --format "{{.Names}}"`)
        .toString()
        .trim();
    while (containerNameCheck.includes(`${imageName}-${index}`)) {
        index++;
    }
    const containerName = `${imageName}-${index}`;
    args.push('--name', containerName, '--hostname', containerName);
    // copy GEMINI_API_KEY(s)
    if (process.env.GEMINI_API_KEY) {
        args.push('--env', `GEMINI_API_KEY=${process.env.GEMINI_API_KEY}`);
    }
    if (process.env.GOOGLE_API_KEY) {
        args.push('--env', `GOOGLE_API_KEY=${process.env.GOOGLE_API_KEY}`);
    }
    // copy GOOGLE_GENAI_USE_VERTEXAI
    if (process.env.GOOGLE_GENAI_USE_VERTEXAI) {
        args.push('--env', `GOOGLE_GENAI_USE_VERTEXAI=${process.env.GOOGLE_GENAI_USE_VERTEXAI}`);
    }
    // copy GOOGLE_CLOUD_PROJECT
    if (process.env.GOOGLE_CLOUD_PROJECT) {
        args.push('--env', `GOOGLE_CLOUD_PROJECT=${process.env.GOOGLE_CLOUD_PROJECT}`);
    }
    // copy GOOGLE_CLOUD_LOCATION
    if (process.env.GOOGLE_CLOUD_LOCATION) {
        args.push('--env', `GOOGLE_CLOUD_LOCATION=${process.env.GOOGLE_CLOUD_LOCATION}`);
    }
    // copy GEMINI_MODEL
    if (process.env.GEMINI_MODEL) {
        args.push('--env', `GEMINI_MODEL=${process.env.GEMINI_MODEL}`);
    }
    // copy TERM and COLORTERM to try to maintain terminal setup
    if (process.env.TERM) {
        args.push('--env', `TERM=${process.env.TERM}`);
    }
    if (process.env.COLORTERM) {
        args.push('--env', `COLORTERM=${process.env.COLORTERM}`);
    }
    // copy VIRTUAL_ENV if under working directory
    // also mount-replace VIRTUAL_ENV directory with <project_settings>/sandbox.venv
    // sandbox can then set up this new VIRTUAL_ENV directory using sandbox.bashrc (see below)
    // directory will be empty if not set up, which is still preferable to having host binaries
    if (process.env.VIRTUAL_ENV?.toLowerCase().startsWith(workdir.toLowerCase())) {
        const sandboxVenvPath = path.resolve(SETTINGS_DIRECTORY_NAME, 'sandbox.venv');
        if (!fs.existsSync(sandboxVenvPath)) {
            fs.mkdirSync(sandboxVenvPath, { recursive: true });
        }
        args.push('--volume', `${sandboxVenvPath}:${getContainerPath(process.env.VIRTUAL_ENV)}`);
        args.push('--env', `VIRTUAL_ENV=${getContainerPath(process.env.VIRTUAL_ENV)}`);
    }
    // copy additional environment variables from SANDBOX_ENV
    if (process.env.SANDBOX_ENV) {
        for (let env of process.env.SANDBOX_ENV.split(',')) {
            if ((env = env.trim())) {
                if (env.includes('=')) {
                    console.error(`SANDBOX_ENV: ${env}`);
                    args.push('--env', env);
                }
                else {
                    console.error('ERROR: SANDBOX_ENV must be a comma-separated list of key=value pairs');
                    process.exit(1);
                }
            }
        }
    }
    // copy NODE_OPTIONS
    const existingNodeOptions = process.env.NODE_OPTIONS || '';
    const allNodeOptions = [
        ...(existingNodeOptions ? [existingNodeOptions] : []),
        ...nodeArgs,
    ].join(' ');
    if (allNodeOptions.length > 0) {
        args.push('--env', `NODE_OPTIONS="${allNodeOptions}"`);
    }
    // set SANDBOX as container name
    args.push('--env', `SANDBOX=${containerName}`);
    // for podman only, use empty --authfile to skip unnecessary auth refresh overhead
    if (config.command === 'podman') {
        const emptyAuthFilePath = path.join(os.tmpdir(), 'empty_auth.json');
        fs.writeFileSync(emptyAuthFilePath, '{}', 'utf-8');
        args.push('--authfile', emptyAuthFilePath);
    }
    // Determine if the current user's UID/GID should be passed to the sandbox.
    // See shouldUseCurrentUserInSandbox for more details.
    let userFlag = '';
    const finalEntrypoint = entrypoint(workdir);
    if (process.env.GEMINI_CLI_INTEGRATION_TEST === 'true') {
        args.push('--user', 'root');
        userFlag = '--user root';
    }
    else if (await shouldUseCurrentUserInSandbox()) {
        // For the user-creation logic to work, the container must start as root.
        // 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();
        // 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
        // finally switch to that user to run the gemini process. This is
        // necessary on Linux to ensure the user exists within the
        // container's /etc/passwd file, which is required by os.userInfo().
        const username = 'gemini';
        const homeDir = getContainerPath(os.homedir());
        const setupUserCommands = [
            // Use -f with groupadd to avoid errors if the group already exists.
            `groupadd -f -g ${gid} ${username}`,
            // Create user only if it doesn't exist. Use -o for non-unique UID.
            `id -u ${username} &>/dev/null || useradd -o -u ${uid} -g ${gid} -d ${homeDir} -s /bin/bash ${username}`,
        ].join(' && ');
        const originalCommand = finalEntrypoint[2];
        const escapedOriginalCommand = originalCommand.replace(/'/g, "'\\''");
        // Use `su -p` to preserve the environment.
        const suCommand = `su -p ${username} -c '${escapedOriginalCommand}'`;
        // The entrypoint is always `['bash', '-c', '<command>']`, so we modify the command part.
        finalEntrypoint[2] = `${setupUserCommands} && ${suCommand}`;
        // We still need userFlag for the simpler proxy container, which does not have this issue.
        userFlag = `--user ${uid}:${gid}`;
        // When forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well.
        args.push('--env', `HOME=${os.homedir()}`);
    }
    // push container image name
    args.push(image);
    // push container entrypoint (including args)
    args.push(...finalEntrypoint);
    // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
    let proxyProcess = undefined;
    let sandboxProcess = undefined;
    if (proxyCommand) {
        // run proxyCommand in its own container
        const proxyContainerCommand = `${config.command} run --rm --init ${userFlag} --name ${SANDBOX_PROXY_NAME} --network ${SANDBOX_PROXY_NAME} -p 8877:8877 -v ${process.cwd()}:${workdir} --workdir ${workdir} ${image} ${proxyCommand}`;
        proxyProcess = spawn(proxyContainerCommand, {
            stdio: ['ignore', 'pipe', 'pipe'],
            shell: true,
            detached: true,
        });
        // install handlers to stop proxy on exit/signal
        const stopProxy = () => {
            console.log('stopping proxy container ...');
            execSync(`${config.command} rm -f ${SANDBOX_PROXY_NAME}`);
        };
        process.on('exit', stopProxy);
        process.on('SIGINT', stopProxy);
        process.on('SIGTERM', stopProxy);
        // commented out as it disrupts ink rendering
        // proxyProcess.stdout?.on('data', (data) => {
        //   console.info(data.toString());
        // });
        proxyProcess.stderr?.on('data', (data) => {
            console.error(data.toString().trim());
        });
        proxyProcess.on('close', (code, signal) => {
            console.error(`ERROR: proxy container command '${proxyContainerCommand}' exited with code ${code}, signal ${signal}`);
            if (sandboxProcess?.pid) {
                process.kill(-sandboxProcess.pid, 'SIGTERM');
            }
            process.exit(1);
        });
        console.log('waiting for proxy to start ...');
        await execAsync(`until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`);
        // connect proxy container to sandbox network
        // (workaround for older versions of docker that don't support multiple --network args)
        await execAsync(`${config.command} network connect ${SANDBOX_NETWORK_NAME} ${SANDBOX_PROXY_NAME}`);
    }
    // spawn child and let it inherit stdio
    sandboxProcess = spawn(config.command, args, {
        stdio: 'inherit',
    });
    sandboxProcess.on('error', (err) => {
        console.error('Sandbox process error:', err);
    });
    await new Promise((resolve) => {
        sandboxProcess?.on('close', (code, signal) => {
            if (code !== 0) {
                console.log(`Sandbox process exited with code: ${code}, signal: ${signal}`);
            }
            resolve();
        });
    });
}
// Helper functions to ensure sandbox image is present
async function imageExists(sandbox, image) {
    return new Promise((resolve) => {
        const args = ['images', '-q', image];
        const checkProcess = spawn(sandbox, args);
        let stdoutData = '';
        if (checkProcess.stdout) {
            checkProcess.stdout.on('data', (data) => {
                stdoutData += data.toString();
            });
        }
        checkProcess.on('error', (err) => {
            console.warn(`Failed to start '${sandbox}' command for image check: ${err.message}`);
            resolve(false);
        });
        checkProcess.on('close', (code) => {
            // Non-zero code might indicate docker daemon not running, etc.
            // The primary success indicator is non-empty stdoutData.
            if (code !== 0) {
                // console.warn(`'${sandbox} images -q ${image}' exited with code ${code}.`);
            }
            resolve(stdoutData.trim() !== '');
        });
    });
}
async function pullImage(sandbox, image) {
    console.info(`Attempting to pull image ${image} using ${sandbox}...`);
    return new Promise((resolve) => {
        const args = ['pull', image];
        const pullProcess = spawn(sandbox, args, { stdio: 'pipe' });
        let stderrData = '';
        const onStdoutData = (data) => {
            console.info(data.toString().trim()); // Show pull progress
        };
        const onStderrData = (data) => {
            stderrData += data.toString();
            console.error(data.toString().trim()); // Show pull errors/info from the command itself
        };
        const onError = (err) => {
            console.warn(`Failed to start '${sandbox} pull ${image}' command: ${err.message}`);
            cleanup();
            resolve(false);
        };
        const onClose = (code) => {
            if (code === 0) {
                console.info(`Successfully pulled image ${image}.`);
                cleanup();
                resolve(true);
            }
            else {
                console.warn(`Failed to pull image ${image}. '${sandbox} pull ${image}' exited with code ${code}.`);
                if (stderrData.trim()) {
                    // Details already printed by the stderr listener above
                }
                cleanup();
                resolve(false);
            }
        };
        const cleanup = () => {
            if (pullProcess.stdout) {
                pullProcess.stdout.removeListener('data', onStdoutData);
            }
            if (pullProcess.stderr) {
                pullProcess.stderr.removeListener('data', onStderrData);
            }
            pullProcess.removeListener('error', onError);
            pullProcess.removeListener('close', onClose);
            if (pullProcess.connected) {
                pullProcess.disconnect();
            }
        };
        if (pullProcess.stdout) {
            pullProcess.stdout.on('data', onStdoutData);
        }
        if (pullProcess.stderr) {
            pullProcess.stderr.on('data', onStderrData);
        }
        pullProcess.on('error', onError);
        pullProcess.on('close', onClose);
    });
}
async function ensureSandboxImageIsPresent(sandbox, image) {
    console.info(`Checking for sandbox image: ${image}`);
    if (await imageExists(sandbox, image)) {
        console.info(`Sandbox image ${image} found locally.`);
        return true;
    }
    console.info(`Sandbox image ${image} not found locally.`);
    if (image === LOCAL_DEV_SANDBOX_IMAGE_NAME) {
        // user needs to build the image themself
        return false;
    }
    if (await pullImage(sandbox, image)) {
        // After attempting to pull, check again to be certain
        if (await imageExists(sandbox, image)) {
            console.info(`Sandbox image ${image} is now available after pulling.`);
            return true;
        }
        else {
            console.warn(`Sandbox image ${image} still not found after a pull attempt. This might indicate an issue with the image name or registry, or the pull command reported success but failed to make the image available.`);
            return false;
        }
    }
    console.error(`Failed to obtain sandbox image ${image} after check and pull attempt.`);
    return false; // Pull command failed or image still not present
}
//# sourceMappingURL=sandbox.js.map