Fix sandbox env var loading

This commit is contained in:
Christine Betts
2026-04-01 13:57:59 -04:00
parent dcf5afafda
commit 60d40f3d4e
5 changed files with 153 additions and 27 deletions
+23
View File
@@ -227,6 +227,29 @@ export SANDBOX_FLAGS="--flag1 --flag2=value"
$env:SANDBOX_FLAGS="--flag1 --flag2=value"
```
### Route custom environment variables
Use the `SANDBOX_ENV` environment variable to explicitly route custom
environment variables into the sandbox. This is a comma-separated list of
`KEY=VALUE` pairs.
**macOS/Linux**
```bash
export SANDBOX_ENV="MY_VAR=hello,ANOTHER_VAR=world"
gemini -p "echo \$MY_VAR \$ANOTHER_VAR"
```
**Windows (PowerShell)**
```powershell
$env:SANDBOX_ENV="MY_VAR=hello,ANOTHER_VAR=world"
gemini -p "echo %MY_VAR% %ANOTHER_VAR%"
```
Environment variables listed in `SANDBOX_ENV` are also available to tools run by
the agent.
## Linux UID/GID handling
The sandbox automatically handles user permissions on Linux. Override these
+1
View File
@@ -85,6 +85,7 @@ const AUTH_ENV_VAR_WHITELIST = [
'GOOGLE_API_KEY',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_CLOUD_LOCATION',
'SANDBOX_ENV',
];
/**
+35
View File
@@ -514,6 +514,41 @@ describe('sandbox', () => {
);
});
it('should handle SANDBOX_ENV in macOS seatbelt', async () => {
vi.mocked(os.platform).mockReturnValue('darwin');
process.env['SANDBOX_ENV'] = 'MY_VAR=hello,ANOTHER_VAR=world';
const config: SandboxConfig = createMockSandboxConfig({
command: 'sandbox-exec',
image: 'some-image',
});
interface MockProcess extends EventEmitter {
stdout: EventEmitter;
stderr: EventEmitter;
}
const mockSpawnProcess = new EventEmitter() as MockProcess;
mockSpawnProcess.stdout = new EventEmitter();
mockSpawnProcess.stderr = new EventEmitter();
vi.mocked(spawn).mockReturnValue(
mockSpawnProcess as unknown as ReturnType<typeof spawn>,
);
const promise = start_sandbox(config);
setTimeout(() => mockSpawnProcess.emit('close', 0), 10);
await promise;
// Check that SANDBOX_ENV variables are passed in the sh -c command
expect(spawn).toHaveBeenCalledWith(
'sandbox-exec',
expect.arrayContaining([
'sh',
'-c',
expect.stringContaining('MY_VAR=hello ANOTHER_VAR=world'),
]),
expect.any(Object),
);
});
it('should pass through GOOGLE_GEMINI_BASE_URL and GOOGLE_VERTEX_BASE_URL', async () => {
const config: SandboxConfig = createMockSandboxConfig({
command: 'docker',
+80 -27
View File
@@ -141,18 +141,48 @@ export async function start_sandbox(
}
const finalArgv = cliArgs;
const shCommandParts = [
`SANDBOX=sandbox-exec`,
`NODE_OPTIONS="${nodeOptions}"`,
];
args.push(
'-f',
profileFile,
'sh',
'-c',
[
`SANDBOX=sandbox-exec`,
`NODE_OPTIONS="${nodeOptions}"`,
...finalArgv.map((arg) => quote([arg])),
].join(' '),
);
// copy additional environment variables from SANDBOX_ENV
if (process.env['SANDBOX_ENV']) {
let currentEnv = '';
for (let part of process.env['SANDBOX_ENV'].split(',')) {
part = part.trim();
if (!part) continue;
if (part.includes('=')) {
if (currentEnv) {
debugLogger.log(`SANDBOX_ENV: ${currentEnv}`);
const [k, ...vParts] = currentEnv.split('=');
const v = vParts.join('=');
shCommandParts.push(`${k}=${quote([v])}`);
}
currentEnv = part;
} else {
if (currentEnv) {
currentEnv += ',' + part;
} else {
debugLogger.log(`SANDBOX_ENV: ${part} (forwarded)`);
const val = process.env[part];
if (val !== undefined) {
shCommandParts.push(`${part}=${quote([val])}`);
}
}
}
}
if (currentEnv) {
debugLogger.log(`SANDBOX_ENV: ${currentEnv}`);
const [k, ...vParts] = currentEnv.split('=');
const v = vParts.join('=');
shCommandParts.push(`${k}=${quote([v])}`);
}
}
shCommandParts.push(...finalArgv.map((arg) => quote([arg])));
args.push('-f', profileFile, 'sh', '-c', shCommandParts.join(' '));
// start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set
const proxyCommand = process.env['GEMINI_SANDBOX_PROXY_COMMAND'];
let proxyProcess: ChildProcess | undefined = undefined;
@@ -616,18 +646,29 @@ export async function start_sandbox(
// 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('=')) {
debugLogger.log(`SANDBOX_ENV: ${env}`);
args.push('--env', env);
let currentEnv = '';
for (let part of process.env['SANDBOX_ENV'].split(',')) {
part = part.trim();
if (!part) continue;
if (part.includes('=')) {
if (currentEnv) {
debugLogger.log(`SANDBOX_ENV: ${currentEnv}`);
args.push('--env', currentEnv);
}
currentEnv = part;
} else {
if (currentEnv) {
currentEnv += ',' + part;
} else {
throw new FatalSandboxError(
'SANDBOX_ENV must be a comma-separated list of key=value pairs',
);
debugLogger.log(`SANDBOX_ENV: ${part} (forwarded)`);
args.push('--env', part);
}
}
}
if (currentEnv) {
debugLogger.log(`SANDBOX_ENV: ${currentEnv}`);
args.push('--env', currentEnv);
}
}
// copy NODE_OPTIONS
@@ -979,19 +1020,31 @@ async function start_lxc_sandbox(
// Forward SANDBOX_ENV key=value pairs
if (process.env['SANDBOX_ENV']) {
for (let env of process.env['SANDBOX_ENV'].split(',')) {
if ((env = env.trim())) {
if (env.includes('=')) {
envArgs.push('--env', env);
let currentEnv = '';
for (let part of process.env['SANDBOX_ENV'].split(',')) {
part = part.trim();
if (!part) continue;
if (part.includes('=')) {
if (currentEnv) {
envArgs.push('--env', currentEnv);
}
currentEnv = part;
} else {
if (currentEnv) {
currentEnv += ',' + part;
} else {
throw new FatalSandboxError(
'SANDBOX_ENV must be a comma-separated list of key=value pairs',
);
// LXC doesn't automatically forward from host, so we look it up
const val = process.env[part];
if (val !== undefined) {
envArgs.push('--env', `${part}=${val}`);
}
}
}
}
if (currentEnv) {
envArgs.push('--env', currentEnv);
}
}
// Forward NODE_OPTIONS (e.g. from --inspect flags)
const existingNodeOptions = process.env['NODE_OPTIONS'] || '';
const allNodeOptions = [
@@ -425,6 +425,20 @@ export class ShellExecutionService {
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
};
// Forward SANDBOX_ENV key=value pairs
if (process.env['SANDBOX_ENV']) {
for (let env of process.env['SANDBOX_ENV'].split(',')) {
if ((env = env.trim())) {
const index = env.indexOf('=');
if (index > 0) {
const key = env.substring(0, index);
const value = env.substring(index + 1);
baseEnv[key] = value;
}
}
}
}
if (!isInteractive) {
// Ensure all GIT_CONFIG_* variables are preserved even if they were redacted
for (const key of gitConfigKeys) {