2025-04-30 00:38:25 +00:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-06-11 10:50:31 -07:00
import { exec , execSync , spawn , type ChildProcess } from 'node:child_process' ;
2025-04-30 00:38:25 +00:00
import os from 'node:os' ;
import path from 'node:path' ;
import fs from 'node:fs' ;
2025-05-13 21:13:54 +00:00
import { readFile } from 'node:fs/promises' ;
2025-08-22 22:36:57 +01:00
import { fileURLToPath } from 'node:url' ;
2025-08-01 18:32:44 +02:00
import { quote , parse } from 'shell-quote' ;
2025-10-14 02:31:39 +09:00
import { USER_SETTINGS_DIR } from '../config/settings.js' ;
2025-08-25 22:11:27 +02:00
import { promisify } from 'node:util' ;
2025-08-26 00:04:53 +02:00
import type { Config , SandboxConfig } from '@google/gemini-cli-core' ;
2025-10-14 02:31:39 +09:00
import { FatalSandboxError , GEMINI_DIR } from '@google/gemini-cli-core' ;
2025-08-06 17:19:10 -07:00
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js' ;
2025-09-25 11:26:07 -07:00
import { randomBytes } from 'node:crypto' ;
2025-06-11 10:50:31 -07:00
const execAsync = promisify ( exec ) ;
2025-04-30 00:38:25 +00:00
2025-06-09 12:19:42 -07:00
function getContainerPath ( hostPath : string ) : string {
if ( os . platform ( ) !== 'win32' ) {
return hostPath ;
}
2025-08-20 16:52:27 -07:00
2025-06-09 12:19:42 -07:00
const withForwardSlashes = hostPath . replace ( /\\/g , '/' ) ;
const match = withForwardSlashes . match ( /^([A-Z]):\/(.*)/i ) ;
if ( match ) {
return ` / ${ match [ 1 ] . toLowerCase ( ) } / ${ match [ 2 ] } ` ;
}
return hostPath ;
}
2025-06-03 19:32:17 +00:00
const LOCAL_DEV_SANDBOX_IMAGE_NAME = 'gemini-cli-sandbox' ;
2025-06-10 08:58:37 -07:00
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' ,
] ;
2025-06-03 19:32:17 +00:00
2025-05-13 21:13:54 +00:00
/ * *
* 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 > {
2025-08-17 12:43:21 -04:00
const envVar = process . env [ 'SANDBOX_SET_UID_GID' ] ? . toLowerCase ( ) . trim ( ) ;
2025-05-13 21:13:54 +00:00
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
) {
2025-05-15 10:54:30 -07:00
// note here and below we use console.error for informational messages on stderr
console . error (
2025-05-13 21:13:54 +00:00
'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
}
2025-04-30 17:16:29 +00:00
// docker does not allow container names to contain ':' or '/', so we
2025-07-21 17:54:44 -04:00
// parse those out to shorten the name
2025-04-30 07:39:00 +00:00
function parseImageName ( image : string ) : string {
2025-04-30 17:16:29 +00:00
const [ fullName , tag ] = image . split ( ':' ) ;
const name = fullName . split ( '/' ) . at ( - 1 ) ? ? 'unknown-image' ;
return tag ? ` ${ name } - ${ tag } ` : name ;
2025-04-30 07:39:00 +00:00
}
2025-05-07 14:23:13 +00:00
function ports ( ) : string [ ] {
2025-08-17 12:43:21 -04:00
return ( process . env [ 'SANDBOX_PORTS' ] ? ? '' )
2025-05-07 14:23:13 +00:00
. split ( ',' )
. filter ( ( p ) = > p . trim ( ) )
. map ( ( p ) = > p . trim ( ) ) ;
}
2025-08-20 16:52:27 -07:00
function entrypoint ( workdir : string , cliArgs : string [ ] ) : string [ ] {
2025-06-09 12:19:42 -07:00
const isWindows = os . platform ( ) === 'win32' ;
const containerWorkdir = getContainerPath ( workdir ) ;
const shellCmds = [ ] ;
const pathSeparator = isWindows ? ';' : ':' ;
2025-05-07 14:23:13 +00:00
let pathSuffix = '' ;
2025-08-17 12:43:21 -04:00
if ( process . env [ 'PATH' ] ) {
const paths = process . env [ 'PATH' ] . split ( pathSeparator ) ;
2025-06-09 12:19:42 -07:00
for ( const p of paths ) {
const containerPath = getContainerPath ( p ) ;
if (
containerPath . toLowerCase ( ) . startsWith ( containerWorkdir . toLowerCase ( ) )
) {
pathSuffix += ` : ${ containerPath } ` ;
2025-05-07 14:23:13 +00:00
}
}
}
if ( pathSuffix ) {
2025-06-09 12:19:42 -07:00
shellCmds . push ( ` export PATH=" $ PATH ${ pathSuffix } "; ` ) ;
2025-05-07 14:23:13 +00:00
}
let pythonPathSuffix = '' ;
2025-08-17 12:43:21 -04:00
if ( process . env [ 'PYTHONPATH' ] ) {
const paths = process . env [ 'PYTHONPATH' ] . split ( pathSeparator ) ;
2025-06-09 12:19:42 -07:00
for ( const p of paths ) {
const containerPath = getContainerPath ( p ) ;
if (
containerPath . toLowerCase ( ) . startsWith ( containerWorkdir . toLowerCase ( ) )
) {
pythonPathSuffix += ` : ${ containerPath } ` ;
2025-05-07 14:23:13 +00:00
}
}
}
if ( pythonPathSuffix ) {
2025-06-09 12:19:42 -07:00
shellCmds . push ( ` export PYTHONPATH=" $ PYTHONPATH ${ pythonPathSuffix } "; ` ) ;
2025-05-07 14:23:13 +00:00
}
2025-10-14 02:31:39 +09:00
const projectSandboxBashrc = path . join ( GEMINI_DIR , 'sandbox.bashrc' ) ;
2025-05-07 14:23:13 +00:00
if ( fs . existsSync ( projectSandboxBashrc ) ) {
2025-06-09 12:19:42 -07:00
shellCmds . push ( ` source ${ getContainerPath ( projectSandboxBashrc ) } ; ` ) ;
2025-05-07 14:23:13 +00:00
}
ports ( ) . forEach ( ( p ) = >
2025-06-09 12:19:42 -07:00
shellCmds . push (
2025-05-07 14:23:13 +00:00
` socat TCP4-LISTEN: ${ p } ,bind= $ (hostname -i),fork,reuseaddr TCP4:127.0.0.1: ${ p } 2> /dev/null & ` ,
) ,
) ;
2025-08-20 16:52:27 -07:00
const quotedCliArgs = cliArgs . slice ( 2 ) . map ( ( arg ) = > quote ( [ arg ] ) ) ;
2025-05-07 14:23:13 +00:00
const cliCmd =
2025-08-17 12:43:21 -04:00
process . env [ 'NODE_ENV' ] === 'development'
? process . env [ 'DEBUG' ]
2025-05-07 14:23:13 +00:00
? 'npm run debug --'
2025-06-08 01:04:20 -04:00
: 'npm rebuild && npm run start --'
2025-08-17 12:43:21 -04:00
: process . env [ 'DEBUG' ]
? ` node --inspect-brk=0.0.0.0: ${ process . env [ 'DEBUG_PORT' ] || '9229' } $ (which gemini) `
2025-05-13 17:49:45 +00:00
: 'gemini' ;
2025-05-07 14:23:13 +00:00
2025-08-20 16:52:27 -07:00
const args = [ . . . shellCmds , cliCmd , . . . quotedCliArgs ] ;
2025-05-07 14:23:13 +00:00
return [ 'bash' , '-c' , args . join ( ' ' ) ] ;
}
2025-06-24 21:18:55 +00:00
export async function start_sandbox (
config : SandboxConfig ,
nodeArgs : string [ ] = [ ] ,
2025-07-31 05:38:20 +09:00
cliConfig? : Config ,
2025-08-20 16:52:27 -07:00
cliArgs : string [ ] = [ ] ,
2025-09-17 13:05:40 -07:00
) : Promise < number > {
2025-08-06 17:19:10 -07:00
const patcher = new ConsolePatcher ( {
2025-08-17 12:43:21 -04:00
debugMode : cliConfig?.getDebugMode ( ) || ! ! process . env [ 'DEBUG' ] ,
2025-08-06 17:19:10 -07:00
stderr : true ,
} ) ;
patcher . patch ( ) ;
2025-06-24 21:18:55 +00:00
2025-08-06 17:19:10 -07:00
try {
if ( config . command === 'sandbox-exec' ) {
// disallow BUILD_SANDBOX
2025-08-17 12:43:21 -04:00
if ( process . env [ 'BUILD_SANDBOX' ] ) {
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
'Cannot BUILD_SANDBOX when using macOS Seatbelt' ,
) ;
2025-08-06 17:19:10 -07:00
}
2025-08-22 22:36:57 +01:00
2025-08-17 12:43:21 -04:00
const profile = ( process . env [ 'SEATBELT_PROFILE' ] ? ? = 'permissive-open' ) ;
2025-08-22 22:36:57 +01:00
let profileFile = fileURLToPath (
new URL ( ` sandbox-macos- ${ profile } .sb ` , import . meta . url ) ,
) ;
2025-08-06 17:19:10 -07:00
// if profile name is not recognized, then look for file under project settings directory
if ( ! BUILTIN_SEATBELT_PROFILES . includes ( profile ) ) {
2025-10-14 02:31:39 +09:00
profileFile = path . join ( GEMINI_DIR , ` sandbox-macos- ${ profile } .sb ` ) ;
2025-08-06 17:19:10 -07:00
}
if ( ! fs . existsSync ( profileFile ) ) {
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
` Missing macos seatbelt profile file ' ${ profileFile } ' ` ,
2025-08-06 17:19:10 -07:00
) ;
}
// 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 = [
2025-08-17 12:43:21 -04:00
. . . ( process . env [ 'DEBUG' ] ? [ '--inspect-brk' ] : [ ] ) ,
2025-08-06 17:19:10 -07:00
. . . 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 ( ) ) } ` ,
] ;
// Add included directories from the workspace context
// Always add 5 INCLUDE_DIR parameters to ensure .sb files can reference them
const MAX_INCLUDE_DIRS = 5 ;
const targetDir = fs . realpathSync ( cliConfig ? . getTargetDir ( ) || '' ) ;
const includedDirs : string [ ] = [ ] ;
if ( cliConfig ) {
const workspaceContext = cliConfig . getWorkspaceContext ( ) ;
const directories = workspaceContext . getDirectories ( ) ;
// Filter out TARGET_DIR
for ( const dir of directories ) {
const realDir = fs . realpathSync ( dir ) ;
if ( realDir !== targetDir ) {
includedDirs . push ( realDir ) ;
}
2025-07-31 05:38:20 +09:00
}
}
2025-08-06 17:19:10 -07:00
for ( let i = 0 ; i < MAX_INCLUDE_DIRS ; i ++ ) {
let dirPath = '/dev/null' ; // Default to a safe path that won't cause issues
2025-07-31 05:38:20 +09:00
2025-08-06 17:19:10 -07:00
if ( i < includedDirs . length ) {
dirPath = includedDirs [ i ] ;
}
2025-07-31 05:38:20 +09:00
2025-08-06 17:19:10 -07:00
args . push ( '-D' , ` INCLUDE_DIR_ ${ i } = ${ dirPath } ` ) ;
2025-06-10 08:58:37 -07:00
}
2025-06-11 10:50:31 -07:00
2025-08-20 16:52:27 -07:00
const finalArgv = cliArgs ;
2025-08-06 17:19:10 -07:00
args . push (
'-f' ,
profileFile ,
'sh' ,
'-c' ,
[
` SANDBOX=sandbox-exec ` ,
` NODE_OPTIONS=" ${ nodeOptions } " ` ,
2025-08-20 16:52:27 -07:00
. . . finalArgv . map ( ( arg ) = > quote ( [ arg ] ) ) ,
2025-08-06 17:19:10 -07:00
] . join ( ' ' ) ,
) ;
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
2025-08-17 12:43:21 -04:00
const proxyCommand = process . env [ 'GEMINI_SANDBOX_PROXY_COMMAND' ] ;
2025-08-06 17:19:10 -07:00
let proxyProcess : ChildProcess | undefined = undefined ;
let sandboxProcess : ChildProcess | undefined = undefined ;
const sandboxEnv = { . . . process . env } ;
if ( proxyCommand ) {
const proxy =
2025-08-17 12:43:21 -04:00
process . env [ 'HTTPS_PROXY' ] ||
process . env [ 'https_proxy' ] ||
process . env [ 'HTTP_PROXY' ] ||
process . env [ 'http_proxy' ] ||
2025-08-06 17:19:10 -07:00
'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 ;
2025-08-17 12:43:21 -04:00
const noProxy = process . env [ 'NO_PROXY' ] || process . env [ 'no_proxy' ] ;
2025-08-06 17:19:10 -07:00
if ( noProxy ) {
sandboxEnv [ 'NO_PROXY' ] = noProxy ;
sandboxEnv [ 'no_proxy' ] = noProxy ;
2025-06-11 10:50:31 -07:00
}
2025-08-06 17:19:10 -07:00
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 ) = > {
if ( sandboxProcess ? . pid ) {
process . kill ( - sandboxProcess . pid , 'SIGTERM' ) ;
}
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
` Proxy command ' ${ proxyCommand } ' exited with code ${ code } , signal ${ signal } ` ,
) ;
2025-08-06 17:19:10 -07:00
} ) ;
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
2025-09-17 13:05:40 -07:00
process . stdin . pause ( ) ;
2025-08-06 17:19:10 -07:00
sandboxProcess = spawn ( config . command , args , {
stdio : 'inherit' ,
2025-06-10 08:58:37 -07:00
} ) ;
2025-09-17 13:05:40 -07:00
return new Promise ( ( resolve , reject ) = > {
sandboxProcess ? . on ( 'error' , reject ) ;
sandboxProcess ? . on ( 'close' , ( code ) = > {
process . stdin . resume ( ) ;
resolve ( code ? ? 1 ) ;
} ) ;
} ) ;
2025-06-10 08:58:37 -07:00
}
2025-05-07 20:03:29 -07:00
2025-08-06 17:19:10 -07:00
console . error ( ` hopping into sandbox (command: ${ config . command } ) ... ` ) ;
2025-05-08 14:50:35 -07:00
2025-08-06 17:19:10 -07:00
// determine full path for gemini-cli to distinguish linked vs installed setting
const gcPath = fs . realpathSync ( process . argv [ 1 ] ) ;
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
const projectSandboxDockerfile = path . join (
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-08-06 17:19:10 -07:00
'sandbox.Dockerfile' ,
) ;
const isCustomProjectSandbox = fs . existsSync ( projectSandboxDockerfile ) ;
2025-06-05 17:46:54 +02:00
2025-08-06 17:19:10 -07:00
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
2025-08-17 12:43:21 -04:00
if ( process . env [ 'BUILD_SANDBOX' ] ) {
2025-08-06 17:19:10 -07:00
if ( ! gcPath . includes ( 'gemini-cli/packages/' ) ) {
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
'Cannot build sandbox using installed gemini binary; ' +
2025-08-06 17:19:10 -07:00
'run `npm link ./packages/cli` under gemini-cli repo to switch to linked binary.' ,
) ;
} 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 (
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-08-06 17:19:10 -07:00
'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)
} ,
} ,
) ;
}
}
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// 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.' ;
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
` Sandbox image ' ${ image } ' is missing or could not be pulled. ${ remedy } ` ,
2025-04-30 00:38:25 +00:00
) ;
}
2025-08-06 17:19:10 -07:00
// 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 ] ;
2025-06-04 08:24:33 +02:00
2025-08-06 17:19:10 -07:00
// add custom flags from SANDBOX_FLAGS
2025-08-17 12:43:21 -04:00
if ( process . env [ 'SANDBOX_FLAGS' ] ) {
const flags = parse ( process . env [ 'SANDBOX_FLAGS' ] , process . env ) . filter (
2025-08-06 17:19:10 -07:00
( f ) : f is string = > typeof f === 'string' ,
) ;
args . push ( . . . flags ) ;
}
2025-08-01 18:32:44 +02:00
2025-08-06 17:19:10 -07:00
// 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' ) ;
}
2025-04-30 00:38:25 +00:00
2025-09-10 21:19:37 +02:00
// allow access to host.docker.internal
args . push ( '--add-host' , 'host.docker.internal:host-gateway' ) ;
2025-08-06 17:19:10 -07:00
// mount current directory as working directory in sandbox (set via --workdir)
args . push ( '--volume' , ` ${ workdir } : ${ containerWorkdir } ` ) ;
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// 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 (
2025-10-14 02:31:39 +09:00
` /home/node/ ${ GEMINI_DIR } ` ,
2025-05-01 12:08:24 -07:00
) ;
2025-08-06 17:19:10 -07:00
if ( ! fs . existsSync ( userSettingsDirOnHost ) ) {
fs . mkdirSync ( userSettingsDirOnHost ) ;
}
2025-06-16 10:55:13 +08:00
args . push (
'--volume' ,
2025-08-06 17:19:10 -07:00
` ${ userSettingsDirOnHost } : ${ userSettingsDirInSandbox } ` ,
2025-06-16 10:55:13 +08:00
) ;
2025-08-06 17:19:10 -07:00
if ( userSettingsDirInSandbox !== userSettingsDirOnHost ) {
args . push (
'--volume' ,
` ${ userSettingsDirOnHost } : ${ getContainerPath ( userSettingsDirOnHost ) } ` ,
) ;
}
2025-06-16 10:55:13 +08:00
2025-08-06 17:19:10 -07:00
// 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 ) ) {
2025-06-16 10:55:13 +08:00
args . push (
2025-08-06 17:19:10 -07:00
'--volume' ,
` ${ gcloudConfigDir } : ${ getContainerPath ( gcloudConfigDir ) } :ro ` ,
2025-06-16 10:55:13 +08:00
) ;
}
2025-08-06 17:19:10 -07:00
// mount ADC file if GOOGLE_APPLICATION_CREDENTIALS is set
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GOOGLE_APPLICATION_CREDENTIALS' ] ) {
const adcFile = process . env [ 'GOOGLE_APPLICATION_CREDENTIALS' ] ;
2025-08-06 17:19:10 -07:00
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
2025-08-17 12:43:21 -04:00
if ( process . env [ 'SANDBOX_MOUNTS' ] ) {
for ( let mount of process . env [ 'SANDBOX_MOUNTS' ] . split ( ',' ) ) {
2025-08-06 17:19:10 -07:00
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 ) ) {
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
` Path ' ${ from } ' listed in SANDBOX_MOUNTS must be absolute ` ,
2025-08-06 17:19:10 -07:00
) ;
}
// check that from path exists on host
if ( ! fs . existsSync ( from ) ) {
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
` Missing mount path ' ${ from } ' listed in SANDBOX_MOUNTS ` ,
2025-08-06 17:19:10 -07:00
) ;
}
console . error ( ` SANDBOX_MOUNTS: ${ from } -> ${ to } ( ${ opts } ) ` ) ;
args . push ( '--volume' , mount ) ;
2025-04-30 00:38:25 +00:00
}
}
}
2025-08-06 17:19:10 -07:00
// expose env-specified ports on the sandbox
ports ( ) . forEach ( ( p ) = > args . push ( '--publish' , ` ${ p } : ${ p } ` ) ) ;
2025-05-07 14:23:13 +00:00
2025-08-06 17:19:10 -07:00
// if DEBUG is set, expose debugging port
2025-08-17 12:43:21 -04:00
if ( process . env [ 'DEBUG' ] ) {
const debugPort = process . env [ 'DEBUG_PORT' ] || '9229' ;
2025-08-06 17:19:10 -07:00
args . push ( ` --publish ` , ` ${ debugPort } : ${ debugPort } ` ) ;
}
2025-05-07 14:23:13 +00:00
2025-08-06 17:19:10 -07:00
// 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
2025-08-17 12:43:21 -04:00
const proxyCommand = process . env [ 'GEMINI_SANDBOX_PROXY_COMMAND' ] ;
2025-08-06 17:19:10 -07:00
if ( proxyCommand ) {
let proxy =
2025-08-17 12:43:21 -04:00
process . env [ 'HTTPS_PROXY' ] ||
process . env [ 'https_proxy' ] ||
process . env [ 'HTTP_PROXY' ] ||
process . env [ 'http_proxy' ] ||
2025-08-06 17:19:10 -07:00
'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 } ` ) ;
}
2025-08-17 12:43:21 -04:00
const noProxy = process . env [ 'NO_PROXY' ] || process . env [ 'no_proxy' ] ;
2025-08-06 17:19:10 -07:00
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 ) {
2025-06-16 08:27:29 -07:00
execSync (
2025-08-06 17:19:10 -07:00
` ${ config . command } network inspect ${ SANDBOX_NETWORK_NAME } || ${ config . command } network create --internal ${ SANDBOX_NETWORK_NAME } ` ,
2025-06-16 08:27:29 -07:00
) ;
2025-08-06 17:19:10 -07:00
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 } ` ,
) ;
}
2025-06-16 08:27:29 -07:00
}
2025-06-10 08:58:37 -07:00
}
2025-09-25 11:26:07 -07:00
// name container after image, plus random suffix to avoid conflicts
2025-08-06 17:19:10 -07:00
const imageName = parseImageName ( image ) ;
2025-09-25 11:26:07 -07:00
const isIntegrationTest =
process . env [ 'GEMINI_CLI_INTEGRATION_TEST' ] === 'true' ;
let containerName ;
if ( isIntegrationTest ) {
containerName = ` gemini-cli-integration-test- ${ randomBytes ( 4 ) . toString (
'hex' ,
) } ` ;
console . log ( ` ContainerName: ${ containerName } ` ) ;
} else {
let index = 0 ;
const containerNameCheck = execSync (
` ${ config . command } ps -a --format "{{.Names}}" ` ,
)
. toString ( )
. trim ( ) ;
while ( containerNameCheck . includes ( ` ${ imageName } - ${ index } ` ) ) {
index ++ ;
}
containerName = ` ${ imageName } - ${ index } ` ;
console . log ( ` ContainerName (regular): ${ containerName } ` ) ;
2025-08-06 17:19:10 -07:00
}
args . push ( '--name' , containerName , '--hostname' , containerName ) ;
2025-04-30 00:38:25 +00:00
2025-09-25 17:32:40 -07:00
// copy GEMINI_CLI_TEST_VAR for integration tests
if ( process . env [ 'GEMINI_CLI_TEST_VAR' ] ) {
args . push (
'--env' ,
` GEMINI_CLI_TEST_VAR= ${ process . env [ 'GEMINI_CLI_TEST_VAR' ] } ` ,
) ;
}
2025-08-06 17:19:10 -07:00
// copy GEMINI_API_KEY(s)
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GEMINI_API_KEY' ] ) {
args . push ( '--env' , ` GEMINI_API_KEY= ${ process . env [ 'GEMINI_API_KEY' ] } ` ) ;
2025-08-06 17:19:10 -07:00
}
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GOOGLE_API_KEY' ] ) {
args . push ( '--env' , ` GOOGLE_API_KEY= ${ process . env [ 'GOOGLE_API_KEY' ] } ` ) ;
2025-08-06 17:19:10 -07:00
}
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// copy GOOGLE_GENAI_USE_VERTEXAI
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GOOGLE_GENAI_USE_VERTEXAI' ] ) {
2025-08-06 17:19:10 -07:00
args . push (
'--env' ,
2025-08-17 12:43:21 -04:00
` GOOGLE_GENAI_USE_VERTEXAI= ${ process . env [ 'GOOGLE_GENAI_USE_VERTEXAI' ] } ` ,
2025-08-06 17:19:10 -07:00
) ;
}
2025-06-16 10:55:13 +08:00
2025-08-06 17:19:10 -07:00
// copy GOOGLE_GENAI_USE_GCA
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GOOGLE_GENAI_USE_GCA' ] ) {
2025-08-06 17:19:10 -07:00
args . push (
'--env' ,
2025-08-17 12:43:21 -04:00
` GOOGLE_GENAI_USE_GCA= ${ process . env [ 'GOOGLE_GENAI_USE_GCA' ] } ` ,
2025-08-06 17:19:10 -07:00
) ;
}
2025-07-25 10:19:38 -07:00
2025-08-06 17:19:10 -07:00
// copy GOOGLE_CLOUD_PROJECT
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GOOGLE_CLOUD_PROJECT' ] ) {
2025-08-06 17:19:10 -07:00
args . push (
'--env' ,
2025-08-17 12:43:21 -04:00
` GOOGLE_CLOUD_PROJECT= ${ process . env [ 'GOOGLE_CLOUD_PROJECT' ] } ` ,
2025-08-06 17:19:10 -07:00
) ;
}
2025-06-16 10:55:13 +08:00
2025-08-06 17:19:10 -07:00
// copy GOOGLE_CLOUD_LOCATION
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GOOGLE_CLOUD_LOCATION' ] ) {
2025-08-06 17:19:10 -07:00
args . push (
'--env' ,
2025-08-17 12:43:21 -04:00
` GOOGLE_CLOUD_LOCATION= ${ process . env [ 'GOOGLE_CLOUD_LOCATION' ] } ` ,
2025-08-06 17:19:10 -07:00
) ;
}
2025-06-16 10:55:13 +08:00
2025-08-06 17:19:10 -07:00
// copy GEMINI_MODEL
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GEMINI_MODEL' ] ) {
args . push ( '--env' , ` GEMINI_MODEL= ${ process . env [ 'GEMINI_MODEL' ] } ` ) ;
2025-08-06 17:19:10 -07:00
}
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// copy TERM and COLORTERM to try to maintain terminal setup
2025-08-17 12:43:21 -04:00
if ( process . env [ 'TERM' ] ) {
args . push ( '--env' , ` TERM= ${ process . env [ 'TERM' ] } ` ) ;
2025-08-06 17:19:10 -07:00
}
2025-08-17 12:43:21 -04:00
if ( process . env [ 'COLORTERM' ] ) {
args . push ( '--env' , ` COLORTERM= ${ process . env [ 'COLORTERM' ] } ` ) ;
2025-08-06 17:19:10 -07:00
}
2025-04-30 00:38:25 +00:00
2025-08-08 15:35:47 +00:00
// Pass through IDE mode environment variables
for ( const envVar of [
'GEMINI_CLI_IDE_SERVER_PORT' ,
'GEMINI_CLI_IDE_WORKSPACE_PATH' ,
'TERM_PROGRAM' ,
] ) {
if ( process . env [ envVar ] ) {
args . push ( '--env' , ` ${ envVar } = ${ process . env [ envVar ] } ` ) ;
}
}
2025-08-06 17:19:10 -07:00
// 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 (
2025-08-17 12:43:21 -04:00
process . env [ 'VIRTUAL_ENV' ]
? . toLowerCase ( )
. startsWith ( workdir . toLowerCase ( ) )
2025-08-06 17:19:10 -07:00
) {
2025-10-14 02:31:39 +09:00
const sandboxVenvPath = path . resolve ( GEMINI_DIR , 'sandbox.venv' ) ;
2025-08-06 17:19:10 -07:00
if ( ! fs . existsSync ( sandboxVenvPath ) ) {
fs . mkdirSync ( sandboxVenvPath , { recursive : true } ) ;
}
args . push (
'--volume' ,
2025-08-17 12:43:21 -04:00
` ${ sandboxVenvPath } : ${ getContainerPath ( process . env [ 'VIRTUAL_ENV' ] ) } ` ,
2025-08-06 17:19:10 -07:00
) ;
args . push (
'--env' ,
2025-08-17 12:43:21 -04:00
` VIRTUAL_ENV= ${ getContainerPath ( process . env [ 'VIRTUAL_ENV' ] ) } ` ,
2025-08-06 17:19:10 -07:00
) ;
2025-05-03 00:39:31 -07:00
}
2025-08-06 17:19:10 -07:00
// copy additional environment variables from SANDBOX_ENV
2025-08-17 12:43:21 -04:00
if ( process . env [ 'SANDBOX_ENV' ] ) {
for ( let env of process . env [ 'SANDBOX_ENV' ] . split ( ',' ) ) {
2025-08-06 17:19:10 -07:00
if ( ( env = env . trim ( ) ) ) {
if ( env . includes ( '=' ) ) {
console . error ( ` SANDBOX_ENV: ${ env } ` ) ;
args . push ( '--env' , env ) ;
} else {
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
'SANDBOX_ENV must be a comma-separated list of key=value pairs' ,
2025-08-06 17:19:10 -07:00
) ;
}
2025-04-30 00:38:25 +00:00
}
}
}
2025-08-06 17:19:10 -07:00
// copy NODE_OPTIONS
2025-08-17 12:43:21 -04:00
const existingNodeOptions = process . env [ 'NODE_OPTIONS' ] || '' ;
2025-08-06 17:19:10 -07:00
const allNodeOptions = [
. . . ( existingNodeOptions ? [ existingNodeOptions ] : [ ] ) ,
. . . nodeArgs ,
] . join ( ' ' ) ;
2025-06-24 21:18:55 +00:00
2025-08-06 17:19:10 -07:00
if ( allNodeOptions . length > 0 ) {
args . push ( '--env' , ` NODE_OPTIONS=" ${ allNodeOptions } " ` ) ;
}
2025-05-08 14:50:35 -07:00
2025-08-06 17:19:10 -07:00
// set SANDBOX as container name
args . push ( '--env' , ` SANDBOX= ${ containerName } ` ) ;
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// 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 ) ;
}
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// Determine if the current user's UID/GID should be passed to the sandbox.
// See shouldUseCurrentUserInSandbox for more details.
let userFlag = '' ;
2025-08-20 16:52:27 -07:00
const finalEntrypoint = entrypoint ( workdir , cliArgs ) ;
2025-08-06 17:19:10 -07:00
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GEMINI_CLI_INTEGRATION_TEST' ] === 'true' ) {
2025-08-06 17:19:10 -07:00
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 ( ) } ` ) ;
}
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// push container image name
args . push ( image ) ;
2025-05-07 14:23:13 +00:00
2025-08-06 17:19:10 -07:00
// push container entrypoint (including args)
args . push ( . . . finalEntrypoint ) ;
2025-04-30 00:38:25 +00:00
2025-08-06 17:19:10 -07:00
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
let proxyProcess : ChildProcess | undefined = undefined ;
let sandboxProcess : ChildProcess | undefined = undefined ;
2025-06-16 08:27:29 -07:00
2025-08-06 17:19:10 -07:00
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 ) = > {
if ( sandboxProcess ? . pid ) {
process . kill ( - sandboxProcess . pid , 'SIGTERM' ) ;
}
2025-08-25 21:44:45 -07:00
throw new FatalSandboxError (
` Proxy container command ' ${ proxyContainerCommand } ' exited with code ${ code } , signal ${ signal } ` ,
) ;
2025-08-06 17:19:10 -07:00
} ) ;
console . log ( 'waiting for proxy to start ...' ) ;
await execAsync (
` until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done ` ,
2025-06-11 10:50:31 -07:00
) ;
2025-08-06 17:19:10 -07:00
// 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 } ` ,
) ;
}
2025-06-09 12:19:42 -07:00
2025-08-06 17:19:10 -07:00
// spawn child and let it inherit stdio
2025-09-17 13:05:40 -07:00
process . stdin . pause ( ) ;
2025-08-06 17:19:10 -07:00
sandboxProcess = spawn ( config . command , args , {
stdio : 'inherit' ,
} ) ;
2025-04-30 00:38:25 +00:00
2025-09-17 13:05:40 -07:00
return new Promise < number > ( ( resolve , reject ) = > {
sandboxProcess . on ( 'error' , ( err ) = > {
console . error ( 'Sandbox process error:' , err ) ;
reject ( err ) ;
} ) ;
2025-06-10 08:58:37 -07:00
2025-08-06 17:19:10 -07:00
sandboxProcess ? . on ( 'close' , ( code , signal ) = > {
2025-09-17 13:05:40 -07:00
process . stdin . resume ( ) ;
if ( code !== 0 && code !== null ) {
2025-08-06 17:19:10 -07:00
console . log (
` Sandbox process exited with code: ${ code } , signal: ${ signal } ` ,
) ;
}
2025-09-17 13:05:40 -07:00
resolve ( code ? ? 1 ) ;
2025-08-06 17:19:10 -07:00
} ) ;
2025-06-10 08:58:37 -07:00
} ) ;
2025-08-06 17:19:10 -07:00
} finally {
patcher . cleanup ( ) ;
}
2025-04-30 00:38:25 +00:00
}
2025-05-30 20:49:47 +00:00
// Helper functions to ensure sandbox image is present
async function imageExists ( sandbox : string , image : string ) : Promise < boolean > {
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 : string , image : string ) : Promise < boolean > {
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 = '' ;
2025-06-05 06:40:33 -07:00
const onStdoutData = ( data : Buffer ) = > {
console . info ( data . toString ( ) . trim ( ) ) ; // Show pull progress
} ;
const onStderrData = ( data : Buffer ) = > {
stderrData += data . toString ( ) ;
console . error ( data . toString ( ) . trim ( ) ) ; // Show pull errors/info from the command itself
} ;
const onError = ( err : Error ) = > {
2025-05-30 20:49:47 +00:00
console . warn (
` Failed to start ' ${ sandbox } pull ${ image } ' command: ${ err . message } ` ,
) ;
2025-06-05 06:40:33 -07:00
cleanup ( ) ;
2025-05-30 20:49:47 +00:00
resolve ( false ) ;
2025-06-05 06:40:33 -07:00
} ;
2025-05-30 20:49:47 +00:00
2025-06-05 06:40:33 -07:00
const onClose = ( code : number | null ) = > {
2025-05-30 20:49:47 +00:00
if ( code === 0 ) {
console . info ( ` Successfully pulled image ${ image } . ` ) ;
2025-06-05 06:40:33 -07:00
cleanup ( ) ;
2025-05-30 20:49:47 +00:00
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
}
2025-06-05 06:40:33 -07:00
cleanup ( ) ;
2025-05-30 20:49:47 +00:00
resolve ( false ) ;
}
2025-06-05 06:40:33 -07:00
} ;
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 ) ;
2025-05-30 20:49:47 +00:00
} ) ;
}
async function ensureSandboxImageIsPresent (
sandbox : string ,
image : string ,
) : Promise < boolean > {
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. ` ) ;
2025-06-03 19:32:17 +00:00
if ( image === LOCAL_DEV_SANDBOX_IMAGE_NAME ) {
2025-07-21 17:54:44 -04:00
// user needs to build the image themselves
2025-06-03 19:32:17 +00:00
return false ;
}
2025-05-30 20:49:47 +00:00
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
}