/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { type SandboxManager, type SandboxRequest, type SandboxedCommand, type SandboxPermissions, type GlobalSandboxOptions, type ParsedSandboxDenial, } from '../../services/sandboxManager.js'; import type { ShellExecutionResult } from '../../services/shellExecutionService.js'; import { sanitizeEnvironment, getSecureSanitizationConfig, } from '../../services/environmentSanitization.js'; import { buildSeatbeltArgs } from './seatbeltArgsBuilder.js'; import { initializeShellParsers, getCommandName, } from '../../utils/shell-utils.js'; import { isKnownSafeCommand, isDangerousCommand, isStrictlyApproved, } from '../utils/commandSafety.js'; import { verifySandboxOverrides } from '../utils/commandUtils.js'; import { parsePosixSandboxDenials } from '../utils/sandboxDenialUtils.js'; /** * A SandboxManager implementation for macOS that uses Seatbelt. */ export class MacOsSandboxManager implements SandboxManager { constructor(private readonly options: GlobalSandboxOptions) {} isKnownSafeCommand(args: string[]): boolean { const toolName = args[0]; const approvedTools = this.options.modeConfig?.approvedTools ?? []; if (toolName && approvedTools.includes(toolName)) { return true; } return isKnownSafeCommand(args); } isDangerousCommand(args: string[]): boolean { return isDangerousCommand(args); } parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined { return parsePosixSandboxDenials(result); } async prepareCommand(req: SandboxRequest): Promise { await initializeShellParsers(); const sanitizationConfig = getSecureSanitizationConfig( req.policy?.sanitizationConfig, ); const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig); const isReadonlyMode = this.options.modeConfig?.readonly ?? true; const allowOverrides = this.options.modeConfig?.allowOverrides ?? true; // Reject override attempts in plan mode verifySandboxOverrides(allowOverrides, req.policy); // If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes const isApproved = allowOverrides ? await isStrictlyApproved( req.command, req.args, this.options.modeConfig?.approvedTools, ) : false; const workspaceWrite = !isReadonlyMode || isApproved; const defaultNetwork = this.options.modeConfig?.network || req.policy?.networkAccess || false; // Fetch persistent approvals for this command const commandName = await getCommandName(req.command, req.args); const persistentPermissions = allowOverrides ? this.options.policyManager?.getCommandPermissions(commandName) : undefined; // Merge all permissions const mergedAdditional: SandboxPermissions = { fileSystem: { read: [ ...(persistentPermissions?.fileSystem?.read ?? []), ...(req.policy?.additionalPermissions?.fileSystem?.read ?? []), ], write: [ ...(persistentPermissions?.fileSystem?.write ?? []), ...(req.policy?.additionalPermissions?.fileSystem?.write ?? []), ], }, network: defaultNetwork || persistentPermissions?.network || req.policy?.additionalPermissions?.network || false, }; const sandboxArgs = buildSeatbeltArgs({ workspace: this.options.workspace, allowedPaths: [...(req.policy?.allowedPaths || [])], forbiddenPaths: this.options.forbiddenPaths, networkAccess: mergedAdditional.network, workspaceWrite, additionalPermissions: mergedAdditional, }); return { program: '/usr/bin/sandbox-exec', args: [...sandboxArgs, '--', req.command, ...req.args], env: sanitizedEnv, cwd: req.cwd, }; } }