From ac42fb0a24fe7349e9968e2359ef5232f1cb6e72 Mon Sep 17 00:00:00 2001 From: amelidev Date: Mon, 3 Aug 2026 13:31:48 -0600 Subject: [PATCH] fix(cli): fall back to embedded macOS seatbelt profiles if missing (#28551) --- .../src/commands/extensions/install.test.ts | 5 +- .../src/commands/extensions/validate.test.ts | 5 +- packages/cli/src/commands/mcp.test.ts | 2 +- packages/cli/src/utils/sandbox.test.ts | 134 +++++ packages/cli/src/utils/sandbox.ts | 361 +++++++----- .../cli/src/utils/sandboxBuiltinProfiles.ts | 555 ++++++++++++++++++ packages/cli/src/utils/sandboxUtils.ts | 2 + 7 files changed, 912 insertions(+), 152 deletions(-) create mode 100644 packages/cli/src/utils/sandboxBuiltinProfiles.ts diff --git a/packages/cli/src/commands/extensions/install.test.ts b/packages/cli/src/commands/extensions/install.test.ts index 8b3f8c5807..df0d162026 100644 --- a/packages/cli/src/commands/extensions/install.test.ts +++ b/packages/cli/src/commands/extensions/install.test.ts @@ -103,7 +103,10 @@ vi.mock('../utils.js', () => ({ describe('extensions install command', () => { it('should fail if no source is provided', () => { - const validationParser = yargs([]).command(installCommand).fail(false); + const validationParser = yargs([]) + .locale('en') + .command(installCommand) + .fail(false); expect(() => validationParser.parse('install')).toThrow( 'Not enough non-option arguments: got 0, need at least 1', ); diff --git a/packages/cli/src/commands/extensions/validate.test.ts b/packages/cli/src/commands/extensions/validate.test.ts index d574095eb6..6a475c16c9 100644 --- a/packages/cli/src/commands/extensions/validate.test.ts +++ b/packages/cli/src/commands/extensions/validate.test.ts @@ -27,7 +27,10 @@ vi.mock('../utils.js', () => ({ describe('extensions validate command', () => { it('should fail if no path is provided', () => { - const validationParser = yargs([]).command(validateCommand).fail(false); + const validationParser = yargs([]) + .locale('en') + .command(validateCommand) + .fail(false); expect(() => validationParser.parse('validate')).toThrow( 'Not enough non-option arguments: got 0, need at least 1', ); diff --git a/packages/cli/src/commands/mcp.test.ts b/packages/cli/src/commands/mcp.test.ts index eae9614cf3..d10898935b 100644 --- a/packages/cli/src/commands/mcp.test.ts +++ b/packages/cli/src/commands/mcp.test.ts @@ -17,7 +17,7 @@ describe('mcp command', () => { }); it('should show help when no subcommand is provided', async () => { - const yargsInstance = yargs(); + const yargsInstance = yargs().locale('en'); (mcpCommand.builder as (y: Argv) => Argv)(yargsInstance); const parser = yargsInstance.command(mcpCommand).help(); diff --git a/packages/cli/src/utils/sandbox.test.ts b/packages/cli/src/utils/sandbox.test.ts index 79bf8d5bdc..740def629f 100644 --- a/packages/cli/src/utils/sandbox.test.ts +++ b/packages/cli/src/utils/sandbox.test.ts @@ -292,6 +292,140 @@ describe('sandbox', () => { await expect(start_sandbox(config)).rejects.toThrow(FatalSandboxError); }); + it('should fall back to embedded profile if the .sb file is missing on disk', async () => { + vi.mocked(os.platform).mockReturnValue('darwin'); + vi.mocked(fs.existsSync).mockImplementation((p) => + String(p).includes( + 'gemini-sandbox-macos-permissive-open-a1b2c3d4e5f6.sb', + ), + ); + + const config: SandboxConfig = createMockSandboxConfig({ + command: 'sandbox-exec', + image: 'some-image', + }); + + const onSpy = vi.spyOn(process, 'on'); + const offSpy = vi.spyOn(process, 'off'); + + 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, + ); + + const promise = start_sandbox(config, [], undefined, ['arg1']); + + setTimeout(() => { + mockSpawnProcess.emit('close', 0); + }, 10); + + await expect(promise).resolves.toBe(0); + + // Verify fs.writeFileSync was called with the temp profile file, content, and 0o600 permissions + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining( + 'gemini-sandbox-macos-permissive-open-a1b2c3d4e5f6.sb', + ), + expect.stringContaining('deny default'), + expect.objectContaining({ + encoding: 'utf8', + mode: 0o600, + }), + ); + + // Verify spawn was called with the temp profile file + expect(spawn).toHaveBeenCalledWith( + 'sandbox-exec', + expect.arrayContaining([ + '-f', + expect.stringContaining( + 'gemini-sandbox-macos-permissive-open-a1b2c3d4e5f6.sb', + ), + ]), + expect.objectContaining({ stdio: 'inherit' }), + ); + + // Verify process on/off hooks were called for exit, SIGINT, and SIGTERM cleanups + expect(onSpy).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(onSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(onSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); + + expect(offSpy).toHaveBeenCalledWith('exit', expect.any(Function)); + expect(offSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(offSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); + + // Verify fs.unlinkSync was called to clean up the temp file + expect(fs.unlinkSync).toHaveBeenCalledWith( + expect.stringContaining( + 'gemini-sandbox-macos-permissive-open-a1b2c3d4e5f6.sb', + ), + ); + }); + + it.each([ + 'permissive-open', + 'permissive-closed', + 'permissive-proxied', + 'restrictive-open', + 'restrictive-closed', + 'restrictive-proxied', + 'strict-open', + 'strict-proxied', + ])( + 'should fall back to embedded content successfully for profile "%s"', + async (profile) => { + vi.mocked(os.platform).mockReturnValue('darwin'); + // Mock existsSync to return false for the profile file but true for temp directories + vi.mocked(fs.existsSync).mockImplementation((p) => + String(p).includes('gemini-sandbox-macos-'), + ); + + vi.stubEnv('SEATBELT_PROFILE', profile); + + 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, + ); + + const promise = start_sandbox(config, [], undefined, ['arg1']); + + setTimeout(() => { + mockSpawnProcess.emit('close', 0); + }, 10); + + await expect(promise).resolves.toBe(0); + + // Verify fs.writeFileSync was called with the correct file mode and content for the profile + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining(`gemini-sandbox-macos-${profile}-`), + expect.stringContaining('deny default'), + expect.objectContaining({ + encoding: 'utf8', + mode: 0o600, + }), + ); + + vi.unstubAllEnvs(); + }, + ); + it('should handle Docker execution', async () => { const config: SandboxConfig = createMockSandboxConfig({ command: 'docker', diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index bda3f073ea..cc314a85ca 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -39,6 +39,7 @@ import { SANDBOX_PROXY_NAME, BUILTIN_SEATBELT_PROFILES, } from './sandboxUtils.js'; +import { BUILTIN_SEATBELT_PROFILE_CONTENTS } from './sandboxBuiltinProfiles.js'; const execAsync = promisify(exec); const execFileAsync = promisify(execFile); @@ -56,6 +57,41 @@ export async function start_sandbox( patcher.patch(); let stopProxy: (() => void) | undefined = undefined; + let tempProfileFile: string | null = null; + + const cleanup = () => { + if (tempProfileFile && fs.existsSync(tempProfileFile)) { + try { + fs.unlinkSync(tempProfileFile); + } catch { + // ignore + } + tempProfileFile = null; + } + if (stopProxy) { + try { + stopProxy(); + } catch { + // ignore + } + } + }; + + const sigintHandler = () => { + cleanup(); + process.off('SIGINT', sigintHandler); + process.kill(process.pid, 'SIGINT'); + }; + + const sigtermHandler = () => { + cleanup(); + process.off('SIGTERM', sigtermHandler); + process.kill(process.pid, 'SIGTERM'); + }; + + process.on('exit', cleanup); + process.on('SIGINT', sigintHandler); + process.on('SIGTERM', sigtermHandler); try { if (config.command === 'sandbox-exec') { @@ -81,161 +117,193 @@ export async function start_sandbox( profileFile = fs.existsSync(userProfileFile) ? userProfileFile : projectProfileFile; - } - if (!fs.existsSync(profileFile)) { - throw new FatalSandboxError( - `Missing macos seatbelt profile file '${profileFile}'`, - ); - } - debugLogger.log(`using macos seatbelt (profile: ${profile}) ...`); - // if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS - const nodeOptions = [ - ...(process.env['DEBUG'] ? ['--inspect-brk'] : []), - ...nodeArgs, - ].join(' '); - - const args = [ - '-D', - `TARGET_DIR=${fs.realpathSync(process.cwd())}`, - '-D', - `TMP_DIR=${fs.realpathSync(os.tmpdir())}`, - '-D', - `HOME_DIR=${fs.realpathSync(homedir())}`, - '-D', - `CACHE_DIR=${fs.realpathSync((await execAsync('getconf DARWIN_USER_CACHE_DIR')).stdout.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); + } else { + // For builtin profiles, if the file doesn't exist on disk (e.g. bundled or bazel environments), + // write the embedded profile content to a temporary file. + if (!fs.existsSync(profileFile)) { + const content = BUILTIN_SEATBELT_PROFILE_CONTENTS[profile]; + if (content) { + try { + const tempDir = fs.realpathSync(os.tmpdir()); + const rand = randomBytes(8).toString('hex'); + tempProfileFile = path.join( + tempDir, + `gemini-sandbox-macos-${profile}-${rand}.sb`, + ); + fs.writeFileSync(tempProfileFile, content, { + encoding: 'utf8', + mode: 0o600, + }); + profileFile = tempProfileFile; + } catch (err) { + debugLogger.warn( + `Failed to write temporary seatbelt profile: ${err}`, + ); + } } } } - // Add custom allowed paths from config - if (config.allowedPaths) { - for (const hostPath of config.allowedPaths) { - if ( - hostPath && - path.isAbsolute(hostPath) && - fs.existsSync(hostPath) - ) { - const realDir = fs.realpathSync(hostPath); - if (!includedDirs.includes(realDir) && realDir !== targetDir) { + try { + if (!fs.existsSync(profileFile)) { + throw new FatalSandboxError( + `Missing macos seatbelt profile file '${profileFile}'`, + ); + } + debugLogger.log(`using macos seatbelt (profile: ${profile}) ...`); + // if DEBUG is set, convert to --inspect-brk in NODE_OPTIONS + const nodeOptions = [ + ...(process.env['DEBUG'] ? ['--inspect-brk'] : []), + ...nodeArgs, + ].join(' '); + + const args = [ + '-D', + `TARGET_DIR=${fs.realpathSync(process.cwd())}`, + '-D', + `TMP_DIR=${fs.realpathSync(os.tmpdir())}`, + '-D', + `HOME_DIR=${fs.realpathSync(homedir())}`, + '-D', + `CACHE_DIR=${fs.realpathSync((await execAsync('getconf DARWIN_USER_CACHE_DIR')).stdout.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); } } } - } - for (let i = 0; i < MAX_INCLUDE_DIRS; i++) { - let dirPath = '/dev/null'; // Default to a safe path that won't cause issues - - if (i < includedDirs.length) { - dirPath = includedDirs[i]; - } - - args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`); - } - - const finalArgv = cliArgs; - - args.push( - '-f', - profileFile, - 'sh', - '-c', - [ - `SANDBOX=sandbox-exec`, - `NODE_OPTIONS="${nodeOptions}"`, - ...finalArgv.map((arg) => quote([arg])), - ].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; - let sandboxProcess: ChildProcess | undefined = undefined; - const sandboxEnv = { ...process.env }; - if (proxyCommand) { - const proxy = - process.env['HTTPS_PROXY'] || - process.env['https_proxy'] || - process.env['HTTP_PROXY'] || - process.env['http_proxy'] || - '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; - const noProxy = process.env['NO_PROXY'] || process.env['no_proxy']; - if (noProxy) { - sandboxEnv['NO_PROXY'] = noProxy; - sandboxEnv['no_proxy'] = noProxy; - } - proxyProcess = spawn(proxyCommand, { - stdio: ['ignore', 'pipe', 'pipe'], - shell: true, - detached: true, - }); - // install handlers to stop proxy on exit/signal - stopProxy = () => { - debugLogger.log('stopping proxy ...'); - if (proxyProcess?.pid) { - try { - process.kill(-proxyProcess.pid, 'SIGTERM'); - } catch { - // ignore + // Add custom allowed paths from config + if (config.allowedPaths) { + for (const hostPath of config.allowedPaths) { + if ( + hostPath && + path.isAbsolute(hostPath) && + fs.existsSync(hostPath) + ) { + const realDir = fs.realpathSync(hostPath); + if (!includedDirs.includes(realDir) && realDir !== targetDir) { + includedDirs.push(realDir); + } } } - }; - 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) => { - debugLogger.debug(`[PROXY STDERR]: ${data.toString().trim()}`); - }); - proxyProcess.on('close', (code, signal) => { - if (sandboxProcess?.pid) { - process.kill(-sandboxProcess.pid, 'SIGTERM'); + for (let i = 0; i < MAX_INCLUDE_DIRS; i++) { + let dirPath = '/dev/null'; // Default to a safe path that won't cause issues + + if (i < includedDirs.length) { + dirPath = includedDirs[i]; } - throw new FatalSandboxError( - `Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`, - ); - }); - debugLogger.log('waiting for proxy to start ...'); - await execAsync( - `until timeout 0.25 curl -s http://localhost:8877; do sleep 0.25; done`, + + args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`); + } + + const finalArgv = cliArgs; + + args.push( + '-f', + profileFile, + 'sh', + '-c', + [ + `SANDBOX=sandbox-exec`, + 'NODE_OPTIONS=' + quote([nodeOptions]), + ...finalArgv.map((arg) => quote([arg])), + ].join(' '), ); - } - // spawn child and let it inherit stdio - process.stdin.pause(); - sandboxProcess = spawn(config.command, args, { - stdio: 'inherit', - }); - return await new Promise((resolve, reject) => { - sandboxProcess?.on('error', reject); - sandboxProcess?.on('close', (code) => { - process.stdin.resume(); - resolve(code ?? 1); + // 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; + let sandboxProcess: ChildProcess | undefined = undefined; + const sandboxEnv = { ...process.env }; + if (proxyCommand) { + const proxy = + process.env['HTTPS_PROXY'] || + process.env['https_proxy'] || + process.env['HTTP_PROXY'] || + process.env['http_proxy'] || + '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; + const noProxy = process.env['NO_PROXY'] || process.env['no_proxy']; + if (noProxy) { + sandboxEnv['NO_PROXY'] = noProxy; + sandboxEnv['no_proxy'] = noProxy; + } + proxyProcess = spawn(proxyCommand, { + stdio: ['ignore', 'pipe', 'pipe'], + shell: true, + detached: true, + }); + // install handlers to stop proxy on exit/signal + stopProxy = () => { + debugLogger.log('stopping proxy ...'); + if (proxyProcess?.pid) { + try { + process.kill(-proxyProcess.pid, 'SIGTERM'); + } catch { + // ignore + } + } + }; + + // commented out as it disrupts ink rendering + // proxyProcess.stdout?.on('data', (data) => { + // console.info(data.toString()); + // }); + proxyProcess.stderr?.on('data', (data) => { + debugLogger.debug(`[PROXY STDERR]: ${data.toString().trim()}`); + }); + proxyProcess.on('close', (code, signal) => { + if (sandboxProcess?.pid) { + process.kill(-sandboxProcess.pid, 'SIGTERM'); + } + throw new FatalSandboxError( + `Proxy command '${proxyCommand}' exited with code ${code}, signal ${signal}`, + ); + }); + debugLogger.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 + process.stdin.pause(); + sandboxProcess = spawn(config.command, args, { + stdio: 'inherit', }); - }); + return await new Promise((resolve, reject) => { + sandboxProcess?.on('error', (err) => { + cleanup(); + reject(err); + }); + sandboxProcess?.on('close', (code) => { + process.stdin.resume(); + cleanup(); + resolve(code ?? 1); + }); + }); + } catch (err) { + cleanup(); + throw err; + } } if (config.command === 'lxc') { @@ -768,9 +836,6 @@ export async function start_sandbox( // ignore } }; - process.on('exit', stopProxy); - process.on('SIGINT', stopProxy); - process.on('SIGTERM', stopProxy); // commented out as it disrupts ink rendering // proxyProcess.stdout?.on('data', (data) => { @@ -821,12 +886,10 @@ export async function start_sandbox( }); }); } finally { - if (stopProxy) { - stopProxy(); - process.off('exit', stopProxy); - process.off('SIGINT', stopProxy); - process.off('SIGTERM', stopProxy); - } + process.off('exit', cleanup); + process.off('SIGINT', sigintHandler); + process.off('SIGTERM', sigtermHandler); + cleanup(); patcher.cleanup(); } } diff --git a/packages/cli/src/utils/sandboxBuiltinProfiles.ts b/packages/cli/src/utils/sandboxBuiltinProfiles.ts new file mode 100644 index 0000000000..d95a05a65c --- /dev/null +++ b/packages/cli/src/utils/sandboxBuiltinProfiles.ts @@ -0,0 +1,555 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export const BUILTIN_SEATBELT_PROFILE_CONTENTS: Record = { + 'permissive-open': `(version 1) +(deny default) +(allow file-read*) +(allow process-exec) +(allow process-fork) +(allow signal (target self)) +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") + (literal "/dev/ptmx") + (regex #"^/dev/ttys[0-9]*$") +) +(allow mach-lookup + (global-name "com.apple.sysmond") + (global-name "com.apple.system.opendirectoryd.libinfo") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.SecurityServer") + (global-name "com.apple.networkd") + (global-name "com.apple.ocspd") + (global-name "com.apple.trustd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.mDNSResponder") + (global-name "com.apple.mDNSResponderHelper") + (global-name "com.apple.SystemConfiguration.DNSConfiguration") + (global-name "com.apple.SystemConfiguration.configd") +) +(allow system-socket + (require-all + (socket-domain AF_SYSTEM) + (socket-protocol 2) + ) +) +(allow file-ioctl (regex #"^/dev/tty.*")) +(allow network-inbound (local ip "*:*")) +(allow network-bind (local ip "*:*")) +(allow network-outbound)`, + + 'permissive-proxied': `(version 1) +(deny default) +(allow file-read*) +(allow process-exec) +(allow process-fork) +(allow signal (target self)) +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") + (literal "/dev/ptmx") + (regex #"^/dev/ttys[0-9]*$") +) +(allow mach-lookup + (global-name "com.apple.sysmond") + (global-name "com.apple.system.opendirectoryd.libinfo") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.SecurityServer") + (global-name "com.apple.networkd") + (global-name "com.apple.ocspd") + (global-name "com.apple.trustd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.mDNSResponder") + (global-name "com.apple.mDNSResponderHelper") + (global-name "com.apple.SystemConfiguration.DNSConfiguration") + (global-name "com.apple.SystemConfiguration.configd") +) +(allow system-socket + (require-all + (socket-domain AF_SYSTEM) + (socket-protocol 2) + ) +) +(allow file-ioctl (regex #"^/dev/tty.*")) +(allow network-inbound (local ip "localhost:9229")) +(allow network-bind (local ip "*:*")) +(allow network-outbound (remote tcp "localhost:8877"))`, + + 'restrictive-open': `(version 1) +(deny default) +(allow file-read*) +(allow process-exec) +(allow process-fork) +(allow signal (target self)) +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") +) +(allow mach-lookup (global-name "com.apple.sysmond")) +(allow file-ioctl (regex #"^/dev/tty.*")) +(allow network-inbound (local ip "localhost:9229")) +(allow network-outbound)`, + + 'restrictive-proxied': `(version 1) +(deny default) +(allow file-read*) +(allow process-exec) +(allow process-fork) +(allow signal (target self)) +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") +) +(allow mach-lookup (global-name "com.apple.sysmond")) +(allow file-ioctl (regex #"^/dev/tty.*")) +(allow network-inbound (local ip "localhost:9229")) +(allow network-outbound (remote tcp "localhost:8877"))`, + + 'strict-open': `(version 1) +(deny default) +(allow file-read* + (literal "/") + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (literal (string-append (param "HOME_DIR") "/.gitconfig")) + (subpath (string-append (param "HOME_DIR") "/.nvm")) + (subpath (string-append (param "HOME_DIR") "/.fnm")) + (subpath (string-append (param "HOME_DIR") "/.node")) + (subpath (string-append (param "HOME_DIR") "/.config")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (subpath "/usr") + (subpath "/bin") + (subpath "/sbin") + (subpath "/Library") + (subpath "/System") + (subpath "/private") + (subpath "/dev") + (subpath "/etc") + (subpath "/opt") + (subpath "/Applications") +) +(allow file-read-metadata) +(allow process-exec) +(allow process-fork) +(allow signal (target self)) +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") +) +(allow mach-lookup (global-name "com.apple.sysmond")) +(allow file-ioctl (regex #"^/dev/tty.*")) +(allow network-inbound (local ip "localhost:9229")) +(allow network-outbound)`, + + 'strict-proxied': `(version 1) +(deny default) +(allow file-read* + (literal "/") + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (literal (string-append (param "HOME_DIR") "/.gitconfig")) + (subpath (string-append (param "HOME_DIR") "/.nvm")) + (subpath (string-append (param "HOME_DIR") "/.fnm")) + (subpath (string-append (param "HOME_DIR") "/.node")) + (subpath (string-append (param "HOME_DIR") "/.config")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (subpath "/usr") + (subpath "/bin") + (subpath "/sbin") + (subpath "/Library") + (subpath "/System") + (subpath "/private") + (subpath "/dev") + (subpath "/etc") + (subpath "/opt") + (subpath "/Applications") +) +(allow file-read-metadata) +(allow process-exec) +(allow process-fork) +(allow signal (target self)) +(allow sysctl-read + (sysctl-name "hw.activecpu") + (sysctl-name "hw.busfrequency_compat") + (sysctl-name "hw.byteorder") + (sysctl-name "hw.cacheconfig") + (sysctl-name "hw.cachelinesize_compat") + (sysctl-name "hw.cpufamily") + (sysctl-name "hw.cpufrequency_compat") + (sysctl-name "hw.cputype") + (sysctl-name "hw.l1dcachesize_compat") + (sysctl-name "hw.l1icachesize_compat") + (sysctl-name "hw.l2cachesize_compat") + (sysctl-name "hw.l3cachesize_compat") + (sysctl-name "hw.logicalcpu_max") + (sysctl-name "hw.machine") + (sysctl-name "hw.ncpu") + (sysctl-name "hw.nperflevels") + (sysctl-name "hw.optional.arm.FEAT_BF16") + (sysctl-name "hw.optional.arm.FEAT_DotProd") + (sysctl-name "hw.optional.arm.FEAT_FCMA") + (sysctl-name "hw.optional.arm.FEAT_FHM") + (sysctl-name "hw.optional.arm.FEAT_FP16") + (sysctl-name "hw.optional.arm.FEAT_I8MM") + (sysctl-name "hw.optional.arm.FEAT_JSCVT") + (sysctl-name "hw.optional.arm.FEAT_LSE") + (sysctl-name "hw.optional.arm.FEAT_RDM") + (sysctl-name "hw.optional.arm.FEAT_SHA512") + (sysctl-name "hw.optional.armv8_2_sha512") + (sysctl-name "hw.packages") + (sysctl-name "hw.pagesize_compat") + (sysctl-name "hw.physicalcpu_max") + (sysctl-name "hw.tbfrequency_compat") + (sysctl-name "hw.vectorunit") + (sysctl-name "kern.hostname") + (sysctl-name "kern.maxfilesperproc") + (sysctl-name "kern.osproductversion") + (sysctl-name "kern.osrelease") + (sysctl-name "kern.ostype") + (sysctl-name "kern.osvariant_status") + (sysctl-name "kern.osversion") + (sysctl-name "kern.secure_kernel") + (sysctl-name "kern.usrstack64") + (sysctl-name "kern.version") + (sysctl-name "sysctl.proc_cputype") + (sysctl-name-prefix "hw.perflevel") +) +(allow file-write* + (subpath (param "TARGET_DIR")) + (subpath (param "TMP_DIR")) + (subpath (param "CACHE_DIR")) + (subpath (string-append (param "HOME_DIR") "/.gemini")) + (subpath (string-append (param "HOME_DIR") "/.npm")) + (subpath (string-append (param "HOME_DIR") "/.cache")) + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) + (literal "/dev/stdout") + (literal "/dev/stderr") + (literal "/dev/null") +) +(allow mach-lookup (global-name "com.apple.sysmond")) +(allow file-ioctl (regex #"^/dev/tty.*")) +(allow network-inbound (local ip "localhost:9229")) +(allow network-outbound (remote tcp "localhost:8877"))`, +}; + +// Map standard 'closed' profiles to their strict counterparts for backward compatibility and fallback support +BUILTIN_SEATBELT_PROFILE_CONTENTS['permissive-closed'] = + BUILTIN_SEATBELT_PROFILE_CONTENTS['strict-open']; +BUILTIN_SEATBELT_PROFILE_CONTENTS['restrictive-closed'] = + BUILTIN_SEATBELT_PROFILE_CONTENTS['strict-proxied']; diff --git a/packages/cli/src/utils/sandboxUtils.ts b/packages/cli/src/utils/sandboxUtils.ts index 439350a323..dcf9a8c2b3 100644 --- a/packages/cli/src/utils/sandboxUtils.ts +++ b/packages/cli/src/utils/sandboxUtils.ts @@ -15,8 +15,10 @@ export const SANDBOX_NETWORK_NAME = 'gemini-cli-sandbox'; export const SANDBOX_PROXY_NAME = 'gemini-cli-sandbox-proxy'; export const BUILTIN_SEATBELT_PROFILES = [ 'permissive-open', + 'permissive-closed', 'permissive-proxied', 'restrictive-open', + 'restrictive-closed', 'restrictive-proxied', 'strict-open', 'strict-proxied',