From 4d4cf0f2f95a002e0b5bf23b2e4f5a446a0612ac Mon Sep 17 00:00:00 2001 From: Tolik Malibroda <54813212+tolikmalibroda@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:46:54 +0200 Subject: [PATCH] feat: Add multi-stage docker build support for custom sandbox.Dockerfile (#746) --- docs/cli/configuration.md | 26 ++++++++++++++ packages/cli/src/utils/sandbox.ts | 28 ++++++++++----- scripts/build_sandbox.sh | 58 +++++++++++++++++++++---------- 3 files changed, 85 insertions(+), 27 deletions(-) diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index 4367e73e..9f2120e3 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -77,6 +77,7 @@ When you create a `.gemini/settings.json` file for project-specific settings, or - See the [Theming section in README.md](../../README.md#theming) for available theme names. - **`sandbox`** (boolean or string): - Controls whether and how to use sandboxing for tool execution. + - If a `.gemini/sandbox.Dockerfile` exists in your project, it will be used to build a custom sandbox image based on `gemini-cli-sandbox`. - `true`: Enable default sandbox (see [README](../../README.md) for behavior). - `false`: Disable sandboxing (WARNING: this is inherently unsafe). - `"docker"` or `"podman"`: Explicitly choose container-based sandboxing command. @@ -276,6 +277,31 @@ This example demonstrates how you can provide general project context, specific By understanding and utilizing these configuration layers and the hierarchical nature of context files, you can effectively manage the AI's memory and tailor the Gemini CLI's responses to your specific needs and projects. +## Sandboxing + +The Gemini CLI can execute potentially unsafe operations (like shell commands and file modifications) within a sandboxed environment to protect your system. + +Sandboxing is disabled by default, but you can enable it in a few ways: + +- Using `--sandbox` or `-s` flag. +- Setting `GEMINI_SANDBOX` environment variable. +- Sandbox is enabled in `--yolo` mode by default. + +By default, it uses a pre-built `gemini-cli-sandbox` Docker image. + +For project-specific sandboxing needs, you can create a custom Dockerfile at `.gemini/sandbox.Dockerfile` in your project's root directory. This Dockerfile should be based on the base sandbox image: + +```dockerfile +FROM gemini-cli-sandbox + +# Add your custom dependencies or configurations here +# For example: +# RUN apt-get update && apt-get install -y some-package +# COPY ./my-config /app/my-config +``` + +When `.gemini/sandbox.Dockerfile` exists, the CLI will automatically build and use a custom image for your project. + ## Theming The Gemini CLI supports theming to customize its color scheme and appearance. Themes define colors for text, backgrounds, syntax highlighting, and other UI elements. diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index e583f0ff..9fb3da69 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -73,7 +73,9 @@ async function shouldUseCurrentUserInSandbox(): Promise { return false; // Default to false if no other condition is met } -async function getSandboxImageName(): Promise { +async function getSandboxImageName( + isCustomProjectSandbox: boolean, +): Promise { const packageJsonResult = await readPackageUp(); const packageJsonConfig = packageJsonResult?.packageJson.config as | { sandboxImageUri?: string } @@ -81,7 +83,9 @@ async function getSandboxImageName(): Promise { return ( process.env.GEMINI_SANDBOX_IMAGE ?? packageJsonConfig?.sandboxImageUri ?? - LOCAL_DEV_SANDBOX_IMAGE_NAME + (isCustomProjectSandbox + ? LOCAL_DEV_SANDBOX_IMAGE_NAME + '-' + path.basename(path.resolve()) + : LOCAL_DEV_SANDBOX_IMAGE_NAME) ); } @@ -272,15 +276,23 @@ export async function start_sandbox(sandbox: string) { // determine full path for gemini-cli to distinguish linked vs installed setting const gcPath = execSync(`realpath $(which gemini)`).toString().trim(); - const image = await getSandboxImageName(); + const projectSandboxDockerfile = path.join( + SETTINGS_DIRECTORY_NAME, + 'sandbox.Dockerfile', + ); + const isCustomProjectSandbox = fs.existsSync(projectSandboxDockerfile); + + const image = await getSandboxImageName(isCustomProjectSandbox); const workdir = process.cwd(); - // if BUILD_SANDBOX is set, then call scripts/build_sandbox.sh under gemini-cli repo + // if BUILD_SANDBOX is set or project-specific sandbox.Dockerfile provided, + // then call scripts/build_sandbox.sh under gemini-cli repo + // // note this can only be done with binary linked from gemini-cli repo - if (process.env.BUILD_SANDBOX) { + if (process.env.BUILD_SANDBOX || isCustomProjectSandbox) { if (!gcPath.includes('gemini-cli/packages/')) { console.error( - 'ERROR: cannot BUILD_SANDBOX using installed gemini binary; ' + + '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); @@ -293,9 +305,9 @@ export async function start_sandbox(sandbox: string) { SETTINGS_DIRECTORY_NAME, 'sandbox.Dockerfile', ); - if (fs.existsSync(projectSandboxDockerfile)) { + if (isCustomProjectSandbox) { console.error(`using ${projectSandboxDockerfile} for sandbox`); - buildArgs += `-f ${path.resolve(projectSandboxDockerfile)}`; + buildArgs += `-s -f ${path.resolve(projectSandboxDockerfile)} -i ${image}`; } spawnSync(`cd ${gcRoot} && scripts/build_sandbox.sh ${buildArgs}`, { stdio: 'inherit', diff --git a/scripts/build_sandbox.sh b/scripts/build_sandbox.sh index 76847f90..4366cd45 100755 --- a/scripts/build_sandbox.sh +++ b/scripts/build_sandbox.sh @@ -26,20 +26,26 @@ fi CMD=$(scripts/sandbox_command.sh) echo "using $CMD for sandboxing" -IMAGE=gemini-cli-sandbox -DOCKERFILE=Dockerfile +BASE_IMAGE=gemini-cli-sandbox +CUSTOM_IMAGE='' +BASE_DOCKERFILE=Dockerfile +CUSTOM_DOCKERFILE='' SKIP_NPM_INSTALL_BUILD=false -while getopts "sf:" opt; do +while getopts "sf:i:" opt; do case ${opt} in s) SKIP_NPM_INSTALL_BUILD=true ;; f) - DOCKERFILE=$OPTARG + CUSTOM_DOCKERFILE=$OPTARG + ;; + i) + CUSTOM_IMAGE=$OPTARG ;; \?) echo "usage: $(basename "$0") [-s] [-f ]" echo " -s: skip npm install + npm run build" - echo " -f : use " + echo " -f : use for custom image" + echo " -i : use name for custom image" exit 1 ;; esac @@ -64,9 +70,6 @@ npm pack -w @gemini-code/core --pack-destination ./packages/core/dist &>/dev/nul # give node user (used during installation, see Dockerfile) access to these files chmod 755 packages/*/dist/gemini-code-*.tgz -# build container image & prune older unused images -echo "building $IMAGE ... (can be slow first time)" - # redirect build output to /dev/null unless VERBOSE is set BUILD_STDOUT="/dev/null" if [ -n "${VERBOSE:-}" ]; then @@ -76,17 +79,34 @@ fi # initialize build arg array from BUILD_SANDBOX_FLAGS read -r -a build_args <<<"${BUILD_SANDBOX_FLAGS:-}" -# append common build args -build_args+=(-f "$DOCKERFILE" -t "$IMAGE" .) +build_image() { + local -n build_args=$1 -if [[ "$CMD" == "podman" ]]; then - # use empty --authfile to skip unnecessary auth refresh overhead - $CMD build --authfile=<(echo '{}') "${build_args[@]}" >$BUILD_STDOUT -elif [[ "$CMD" == "docker" ]]; then - # use config directory to skip unnecessary auth refresh overhead - $CMD --config=".docker" buildx build "${build_args[@]}" >$BUILD_STDOUT -else - $CMD build "${build_args[@]}" >$BUILD_STDOUT + if [[ "$CMD" == "podman" ]]; then + # use empty --authfile to skip unnecessary auth refresh overhead + $CMD build --authfile=<(echo '{}') "${build_args[@]}" >$BUILD_STDOUT + elif [[ "$CMD" == "docker" ]]; then + # use config directory to skip unnecessary auth refresh overhead + $CMD --config=".docker" buildx build "${build_args[@]}" >$BUILD_STDOUT + else + $CMD build "${build_args[@]}" >$BUILD_STDOUT + fi +} + +# build container images & prune older unused images + +echo "building $BASE_IMAGE ... (can be slow first time)" +base_image_build_args=(${build_args[@]}) +base_image_build_args+=(-f "$BASE_DOCKERFILE" -t "$BASE_IMAGE" .) +build_image base_image_build_args +echo "built $BASE_IMAGE" + +if [[ -n "$CUSTOM_DOCKERFILE" && -n "$CUSTOM_IMAGE" ]]; then + echo "building $CUSTOM_IMAGE ... (can be slow first time)" + custom_image_build_args=(${build_args[@]}) + custom_image_build_args+=(-f "$CUSTOM_DOCKERFILE" -t "$CUSTOM_IMAGE" .) + build_image custom_image_build_args + echo "built $CUSTOM_IMAGE" fi + $CMD image prune -f >/dev/null -echo "built $IMAGE" \ No newline at end of file