diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index e0adf0ac..59e19a49 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -30,6 +30,11 @@ function isSandboxCommand(value: string): value is SandboxConfig['command'] { function getSandboxCommand( sandbox?: boolean | string, ): SandboxConfig['command'] | '' { + // If the SANDBOX env var is set, we're already inside the sandbox. + if (process.env.SANDBOX) { + return ''; + } + // note environment variable takes precedence over argument (from command line or settings) sandbox = process.env.GEMINI_SANDBOX?.toLowerCase().trim() ?? sandbox; if (sandbox === '1' || sandbox === 'true') sandbox = true; diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 48386357..6a08edef 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -605,15 +605,46 @@ export async function start_sandbox(config: SandboxConfig) { // 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(); - args.push('--user', `${uid}:${gid}`); + + // 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', '']`, 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 + // When forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well. args.push('--env', `HOME=${os.homedir()}`); } @@ -621,7 +652,7 @@ export async function start_sandbox(config: SandboxConfig) { args.push(image); // push container entrypoint (including args) - args.push(...entrypoint(workdir)); + args.push(...finalEntrypoint); // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set let proxyProcess: ChildProcess | undefined = undefined;