File size: 5,350 Bytes
84aa3bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  getPackageJson,
  type SandboxConfig,
  FatalSandboxError,
} from '@google/gemini-cli-core';
import commandExists from 'command-exists';
import * as os from 'node:os';
import type { Settings } from './settings.js';
import { fileURLToPath } from 'node:url';
import path from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// This is a stripped-down version of the CliArgs interface from config.ts
// to avoid circular dependencies.
interface SandboxCliArgs {
  sandbox?: boolean | string | null;
}
const VALID_SANDBOX_COMMANDS = [
  'docker',
  'podman',
  'sandbox-exec',
  'runsc',
  'lxc',
  'windows-native',
];

function isSandboxCommand(
  value: string,
): value is Exclude<SandboxConfig['command'], undefined> {
  return (VALID_SANDBOX_COMMANDS as ReadonlyArray<string | undefined>).includes(
    value,
  );
}

function getSandboxCommand(
  sandbox?: boolean | string | null,
): 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)
  const environmentConfiguredSandbox =
    process.env['GEMINI_SANDBOX']?.toLowerCase().trim() ?? '';
  sandbox =
    environmentConfiguredSandbox?.length > 0
      ? environmentConfiguredSandbox
      : sandbox;
  if (sandbox === '1' || sandbox === 'true') sandbox = true;
  else if (sandbox === '0' || sandbox === 'false' || !sandbox) sandbox = false;

  if (sandbox === false) {
    return '';
  }

  if (typeof sandbox === 'string' && sandbox) {
    if (!isSandboxCommand(sandbox)) {
      throw new FatalSandboxError(
        `Invalid sandbox command '${sandbox}'. Must be one of ${VALID_SANDBOX_COMMANDS.join(
          ', ',
        )}`,
      );
    }
    // runsc (gVisor) is only supported on Linux
    if (sandbox === 'runsc' && os.platform() !== 'linux') {
      throw new FatalSandboxError(
        'gVisor (runsc) sandboxing is only supported on Linux',
      );
    }
    // windows-native is only supported on Windows
    if (sandbox === 'windows-native' && os.platform() !== 'win32') {
      throw new FatalSandboxError(
        'Windows native sandboxing is only supported on Windows',
      );
    }

    // confirm that specified command exists (unless it's built-in)
    if (sandbox !== 'windows-native' && !commandExists.sync(sandbox)) {
      throw new FatalSandboxError(
        `Missing sandbox command '${sandbox}' (from GEMINI_SANDBOX)`,
      );
    }
    // runsc uses Docker with --runtime=runsc; both must be available (prioritize runsc when explicitly chosen)
    if (sandbox === 'runsc' && !commandExists.sync('docker')) {
      throw new FatalSandboxError(
        "runsc (gVisor) requires Docker. Install Docker, or use sandbox: 'docker'.",
      );
    }
    return sandbox;
  }

  // look for seatbelt, docker, or podman, in that order
  // for container-based sandboxing, require sandbox to be enabled explicitly
  // note: runsc is NOT auto-detected, it must be explicitly specified
  if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
    return 'sandbox-exec';
  } else if (commandExists.sync('docker') && sandbox === true) {
    return 'docker';
  } else if (commandExists.sync('podman') && sandbox === true) {
    return 'podman';
  }

  // throw an error if user requested sandbox but no command was found
  if (sandbox === true) {
    throw new FatalSandboxError(
      'GEMINI_SANDBOX is true but failed to determine command for sandbox; ' +
        'install docker or podman or specify command in GEMINI_SANDBOX',
    );
  }

  return '';
  // Note: 'lxc' is intentionally not auto-detected because it requires a
  // pre-existing, running container managed by the user. Use
  // GEMINI_SANDBOX=lxc or sandbox: "lxc" in settings to enable it.
}

export async function loadSandboxConfig(
  settings: Settings,
  argv: SandboxCliArgs,
): Promise<SandboxConfig | undefined> {
  const sandboxOption = argv.sandbox ?? settings.tools?.sandbox;

  let sandboxValue: boolean | string | null | undefined;
  let allowedPaths: string[] = [];
  let networkAccess = true;
  let customImage: string | undefined;

  if (
    typeof sandboxOption === 'object' &&
    sandboxOption !== null &&
    !Array.isArray(sandboxOption)
  ) {
    const config = sandboxOption;
    sandboxValue = config.enabled ? (config.command ?? true) : false;
    allowedPaths = config.allowedPaths ?? [];
    networkAccess = config.networkAccess ?? true;
    customImage = config.image;
  } else if (typeof sandboxOption !== 'object' || sandboxOption === null) {
    sandboxValue = sandboxOption;
  }

  const command = getSandboxCommand(sandboxValue);

  const packageJson = await getPackageJson(__dirname);
  const image =
    process.env['GEMINI_SANDBOX_IMAGE'] ??
    process.env['GEMINI_SANDBOX_IMAGE_DEFAULT'] ??
    customImage ??
    packageJson?.config?.sandboxImageUri;

  const isNative =
    command === 'windows-native' ||
    command === 'sandbox-exec' ||
    command === 'lxc';

  return command && (image || isNative)
    ? { enabled: true, allowedPaths, networkAccess, command, image }
    : undefined;
}