feat: Add multi-stage docker build support for custom sandbox.Dockerfile (#746)

This commit is contained in:
Tolik Malibroda 2025-06-05 17:46:54 +02:00 committed by GitHub
parent a8ac9b1fac
commit 4d4cf0f2f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 27 deletions

View File

@ -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. - See the [Theming section in README.md](../../README.md#theming) for available theme names.
- **`sandbox`** (boolean or string): - **`sandbox`** (boolean or string):
- Controls whether and how to use sandboxing for tool execution. - 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). - `true`: Enable default sandbox (see [README](../../README.md) for behavior).
- `false`: Disable sandboxing (WARNING: this is inherently unsafe). - `false`: Disable sandboxing (WARNING: this is inherently unsafe).
- `"docker"` or `"podman"`: Explicitly choose container-based sandboxing command. - `"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. 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 ## 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. The Gemini CLI supports theming to customize its color scheme and appearance. Themes define colors for text, backgrounds, syntax highlighting, and other UI elements.

View File

@ -73,7 +73,9 @@ async function shouldUseCurrentUserInSandbox(): Promise<boolean> {
return false; // Default to false if no other condition is met return false; // Default to false if no other condition is met
} }
async function getSandboxImageName(): Promise<string> { async function getSandboxImageName(
isCustomProjectSandbox: boolean,
): Promise<string> {
const packageJsonResult = await readPackageUp(); const packageJsonResult = await readPackageUp();
const packageJsonConfig = packageJsonResult?.packageJson.config as const packageJsonConfig = packageJsonResult?.packageJson.config as
| { sandboxImageUri?: string } | { sandboxImageUri?: string }
@ -81,7 +83,9 @@ async function getSandboxImageName(): Promise<string> {
return ( return (
process.env.GEMINI_SANDBOX_IMAGE ?? process.env.GEMINI_SANDBOX_IMAGE ??
packageJsonConfig?.sandboxImageUri ?? 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 // determine full path for gemini-cli to distinguish linked vs installed setting
const gcPath = execSync(`realpath $(which gemini)`).toString().trim(); 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(); 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 // 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/')) { if (!gcPath.includes('gemini-cli/packages/')) {
console.error( 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.', 'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.',
); );
process.exit(1); process.exit(1);
@ -293,9 +305,9 @@ export async function start_sandbox(sandbox: string) {
SETTINGS_DIRECTORY_NAME, SETTINGS_DIRECTORY_NAME,
'sandbox.Dockerfile', 'sandbox.Dockerfile',
); );
if (fs.existsSync(projectSandboxDockerfile)) { if (isCustomProjectSandbox) {
console.error(`using ${projectSandboxDockerfile} for sandbox`); 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}`, { spawnSync(`cd ${gcRoot} && scripts/build_sandbox.sh ${buildArgs}`, {
stdio: 'inherit', stdio: 'inherit',

View File

@ -26,20 +26,26 @@ fi
CMD=$(scripts/sandbox_command.sh) CMD=$(scripts/sandbox_command.sh)
echo "using $CMD for sandboxing" echo "using $CMD for sandboxing"
IMAGE=gemini-cli-sandbox BASE_IMAGE=gemini-cli-sandbox
DOCKERFILE=Dockerfile CUSTOM_IMAGE=''
BASE_DOCKERFILE=Dockerfile
CUSTOM_DOCKERFILE=''
SKIP_NPM_INSTALL_BUILD=false SKIP_NPM_INSTALL_BUILD=false
while getopts "sf:" opt; do while getopts "sf:i:" opt; do
case ${opt} in case ${opt} in
s) SKIP_NPM_INSTALL_BUILD=true ;; s) SKIP_NPM_INSTALL_BUILD=true ;;
f) f)
DOCKERFILE=$OPTARG CUSTOM_DOCKERFILE=$OPTARG
;;
i)
CUSTOM_IMAGE=$OPTARG
;; ;;
\?) \?)
echo "usage: $(basename "$0") [-s] [-f <dockerfile>]" echo "usage: $(basename "$0") [-s] [-f <dockerfile>]"
echo " -s: skip npm install + npm run build" echo " -s: skip npm install + npm run build"
echo " -f <dockerfile>: use <dockerfile>" echo " -f <dockerfile>: use <dockerfile> for custom image"
echo " -i <image>: use <image> name for custom image"
exit 1 exit 1
;; ;;
esac 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 # give node user (used during installation, see Dockerfile) access to these files
chmod 755 packages/*/dist/gemini-code-*.tgz 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 # redirect build output to /dev/null unless VERBOSE is set
BUILD_STDOUT="/dev/null" BUILD_STDOUT="/dev/null"
if [ -n "${VERBOSE:-}" ]; then if [ -n "${VERBOSE:-}" ]; then
@ -76,17 +79,34 @@ fi
# initialize build arg array from BUILD_SANDBOX_FLAGS # initialize build arg array from BUILD_SANDBOX_FLAGS
read -r -a build_args <<<"${BUILD_SANDBOX_FLAGS:-}" read -r -a build_args <<<"${BUILD_SANDBOX_FLAGS:-}"
# append common build args build_image() {
build_args+=(-f "$DOCKERFILE" -t "$IMAGE" .) local -n build_args=$1
if [[ "$CMD" == "podman" ]]; then if [[ "$CMD" == "podman" ]]; then
# use empty --authfile to skip unnecessary auth refresh overhead # use empty --authfile to skip unnecessary auth refresh overhead
$CMD build --authfile=<(echo '{}') "${build_args[@]}" >$BUILD_STDOUT $CMD build --authfile=<(echo '{}') "${build_args[@]}" >$BUILD_STDOUT
elif [[ "$CMD" == "docker" ]]; then elif [[ "$CMD" == "docker" ]]; then
# use config directory to skip unnecessary auth refresh overhead # use config directory to skip unnecessary auth refresh overhead
$CMD --config=".docker" buildx build "${build_args[@]}" >$BUILD_STDOUT $CMD --config=".docker" buildx build "${build_args[@]}" >$BUILD_STDOUT
else else
$CMD build "${build_args[@]}" >$BUILD_STDOUT $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 fi
$CMD image prune -f >/dev/null $CMD image prune -f >/dev/null
echo "built $IMAGE"