2026-03-11 14:42:50 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
2026-03-13 14:11:51 -07:00
|
|
|
* Copyright 2026 Google LLC
|
2026-03-11 14:42:50 -07:00
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-24 21:23:51 -04:00
|
|
|
import fs from 'node:fs/promises';
|
2026-03-23 11:43:58 -04:00
|
|
|
import os from 'node:os';
|
|
|
|
|
import path from 'node:path';
|
2026-03-25 17:54:45 +00:00
|
|
|
import {
|
|
|
|
|
isKnownSafeCommand as isMacSafeCommand,
|
|
|
|
|
isDangerousCommand as isMacDangerousCommand,
|
2026-03-25 18:58:45 -07:00
|
|
|
} from '../sandbox/utils/commandSafety.js';
|
2026-03-25 17:54:45 +00:00
|
|
|
import {
|
|
|
|
|
isKnownSafeCommand as isWindowsSafeCommand,
|
|
|
|
|
isDangerousCommand as isWindowsDangerousCommand,
|
|
|
|
|
} from '../sandbox/windows/commandSafety.js';
|
2026-03-24 21:23:51 -04:00
|
|
|
import { isNodeError } from '../utils/errors.js';
|
2026-03-11 14:42:50 -07:00
|
|
|
import {
|
|
|
|
|
sanitizeEnvironment,
|
2026-03-16 21:34:48 +00:00
|
|
|
getSecureSanitizationConfig,
|
2026-03-11 14:42:50 -07:00
|
|
|
type EnvironmentSanitizationConfig,
|
|
|
|
|
} from './environmentSanitization.js';
|
2026-03-23 21:48:13 -07:00
|
|
|
export interface SandboxPermissions {
|
|
|
|
|
/** Filesystem permissions. */
|
|
|
|
|
fileSystem?: {
|
|
|
|
|
/** Paths that should be readable by the command. */
|
|
|
|
|
read?: string[];
|
|
|
|
|
/** Paths that should be writable by the command. */
|
|
|
|
|
write?: string[];
|
|
|
|
|
};
|
|
|
|
|
/** Whether the command should have network access. */
|
|
|
|
|
network?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 11:43:58 -04:00
|
|
|
/**
|
|
|
|
|
* Security boundaries and permissions applied to a specific sandboxed execution.
|
|
|
|
|
*/
|
|
|
|
|
export interface ExecutionPolicy {
|
|
|
|
|
/** Additional absolute paths to grant full read/write access to. */
|
|
|
|
|
allowedPaths?: string[];
|
|
|
|
|
/** Absolute paths to explicitly deny read/write access to (overrides allowlists). */
|
|
|
|
|
forbiddenPaths?: string[];
|
|
|
|
|
/** Whether network access is allowed. */
|
|
|
|
|
networkAccess?: boolean;
|
|
|
|
|
/** Rules for scrubbing sensitive environment variables. */
|
|
|
|
|
sanitizationConfig?: Partial<EnvironmentSanitizationConfig>;
|
2026-03-23 21:48:13 -07:00
|
|
|
/** Additional granular permissions to grant to this command. */
|
|
|
|
|
additionalPermissions?: SandboxPermissions;
|
2026-03-23 11:43:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Global configuration options used to initialize a SandboxManager.
|
|
|
|
|
*/
|
|
|
|
|
export interface GlobalSandboxOptions {
|
|
|
|
|
/**
|
|
|
|
|
* The primary workspace path the sandbox is anchored to.
|
|
|
|
|
* This directory is granted full read and write access.
|
|
|
|
|
*/
|
|
|
|
|
workspace: string;
|
|
|
|
|
}
|
2026-03-11 14:42:50 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Request for preparing a command to run in a sandbox.
|
|
|
|
|
*/
|
|
|
|
|
export interface SandboxRequest {
|
|
|
|
|
/** The program to execute. */
|
|
|
|
|
command: string;
|
|
|
|
|
/** Arguments for the program. */
|
|
|
|
|
args: string[];
|
|
|
|
|
/** The working directory. */
|
|
|
|
|
cwd: string;
|
|
|
|
|
/** Environment variables to be passed to the program. */
|
|
|
|
|
env: NodeJS.ProcessEnv;
|
2026-03-23 11:43:58 -04:00
|
|
|
/** Policy to use for this request. */
|
|
|
|
|
policy?: ExecutionPolicy;
|
2026-03-11 14:42:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A command that has been prepared for sandboxed execution.
|
|
|
|
|
*/
|
|
|
|
|
export interface SandboxedCommand {
|
|
|
|
|
/** The program or wrapper to execute. */
|
|
|
|
|
program: string;
|
|
|
|
|
/** Final arguments for the program. */
|
|
|
|
|
args: string[];
|
|
|
|
|
/** Sanitized environment variables. */
|
|
|
|
|
env: NodeJS.ProcessEnv;
|
2026-03-13 14:11:51 -07:00
|
|
|
/** The working directory. */
|
|
|
|
|
cwd?: string;
|
2026-03-11 14:42:50 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Interface for a service that prepares commands for sandboxed execution.
|
|
|
|
|
*/
|
|
|
|
|
export interface SandboxManager {
|
|
|
|
|
/**
|
|
|
|
|
* Prepares a command to run in a sandbox, including environment sanitization.
|
|
|
|
|
*/
|
|
|
|
|
prepareCommand(req: SandboxRequest): Promise<SandboxedCommand>;
|
2026-03-25 17:54:45 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if a command with its arguments is known to be safe for this sandbox.
|
|
|
|
|
*/
|
|
|
|
|
isKnownSafeCommand(args: string[]): boolean;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if a command with its arguments is explicitly known to be dangerous for this sandbox.
|
|
|
|
|
*/
|
|
|
|
|
isDangerousCommand(args: string[]): boolean;
|
2026-03-11 14:42:50 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 04:04:17 +00:00
|
|
|
/**
|
|
|
|
|
* Files that represent the governance or "constitution" of the repository
|
|
|
|
|
* and should be write-protected in any sandbox.
|
|
|
|
|
*/
|
|
|
|
|
export const GOVERNANCE_FILES = [
|
|
|
|
|
{ path: '.gitignore', isDirectory: false },
|
|
|
|
|
{ path: '.geminiignore', isDirectory: false },
|
|
|
|
|
{ path: '.git', isDirectory: true },
|
|
|
|
|
] as const;
|
|
|
|
|
|
2026-03-11 14:42:50 -07:00
|
|
|
/**
|
|
|
|
|
* A no-op implementation of SandboxManager that silently passes commands
|
|
|
|
|
* through while applying environment sanitization.
|
|
|
|
|
*/
|
|
|
|
|
export class NoopSandboxManager implements SandboxManager {
|
|
|
|
|
/**
|
|
|
|
|
* Prepares a command by sanitizing the environment and passing through
|
|
|
|
|
* the original program and arguments.
|
|
|
|
|
*/
|
|
|
|
|
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
|
2026-03-16 21:34:48 +00:00
|
|
|
const sanitizationConfig = getSecureSanitizationConfig(
|
2026-03-23 11:43:58 -04:00
|
|
|
req.policy?.sanitizationConfig,
|
2026-03-16 21:34:48 +00:00
|
|
|
);
|
2026-03-11 14:42:50 -07:00
|
|
|
|
|
|
|
|
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
program: req.command,
|
|
|
|
|
args: req.args,
|
|
|
|
|
env: sanitizedEnv,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-25 17:54:45 +00:00
|
|
|
|
|
|
|
|
isKnownSafeCommand(args: string[]): boolean {
|
|
|
|
|
return os.platform() === 'win32'
|
|
|
|
|
? isWindowsSafeCommand(args)
|
|
|
|
|
: isMacSafeCommand(args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isDangerousCommand(args: string[]): boolean {
|
|
|
|
|
return os.platform() === 'win32'
|
|
|
|
|
? isWindowsDangerousCommand(args)
|
|
|
|
|
: isMacDangerousCommand(args);
|
|
|
|
|
}
|
2026-03-11 14:42:50 -07:00
|
|
|
}
|
2026-03-13 14:11:51 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* SandboxManager that implements actual sandboxing.
|
|
|
|
|
*/
|
|
|
|
|
export class LocalSandboxManager implements SandboxManager {
|
|
|
|
|
async prepareCommand(_req: SandboxRequest): Promise<SandboxedCommand> {
|
|
|
|
|
throw new Error('Tool sandboxing is not yet implemented.');
|
|
|
|
|
}
|
2026-03-25 17:54:45 +00:00
|
|
|
|
|
|
|
|
isKnownSafeCommand(_args: string[]): boolean {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isDangerousCommand(_args: string[]): boolean {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-13 14:11:51 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 11:43:58 -04:00
|
|
|
/**
|
|
|
|
|
* Sanitizes an array of paths by deduplicating them and ensuring they are absolute.
|
|
|
|
|
*/
|
|
|
|
|
export function sanitizePaths(paths?: string[]): string[] | undefined {
|
|
|
|
|
if (!paths) return undefined;
|
|
|
|
|
|
|
|
|
|
// We use a Map to deduplicate paths based on their normalized,
|
|
|
|
|
// platform-specific identity e.g. handling case-insensitivity on Windows)
|
|
|
|
|
// while preserving the original string casing.
|
|
|
|
|
const uniquePathsMap = new Map<string, string>();
|
|
|
|
|
for (const p of paths) {
|
|
|
|
|
if (!path.isAbsolute(p)) {
|
|
|
|
|
throw new Error(`Sandbox path must be absolute: ${p}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize the path (resolves slashes and redundant components)
|
|
|
|
|
let key = path.normalize(p);
|
|
|
|
|
|
|
|
|
|
// Windows file systems are case-insensitive, so we lowercase the key for
|
|
|
|
|
// deduplication
|
|
|
|
|
if (os.platform() === 'win32') {
|
|
|
|
|
key = key.toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!uniquePathsMap.has(key)) {
|
|
|
|
|
uniquePathsMap.set(key, p);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(uniquePathsMap.values());
|
|
|
|
|
}
|
2026-03-24 21:23:51 -04:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolves symlinks for a given path to prevent sandbox escapes.
|
|
|
|
|
* If a file does not exist (ENOENT), it recursively resolves the parent directory.
|
|
|
|
|
* Other errors (e.g. EACCES) are re-thrown.
|
|
|
|
|
*/
|
|
|
|
|
export async function tryRealpath(p: string): Promise<string> {
|
|
|
|
|
try {
|
|
|
|
|
return await fs.realpath(p);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (isNodeError(e) && e.code === 'ENOENT') {
|
|
|
|
|
const parentDir = path.dirname(p);
|
|
|
|
|
if (parentDir === p) {
|
|
|
|
|
return p;
|
|
|
|
|
}
|
|
|
|
|
return path.join(await tryRealpath(parentDir), path.basename(p));
|
|
|
|
|
}
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 15:25:22 -07:00
|
|
|
export { createSandboxManager } from './sandboxManagerFactory.js';
|