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-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-26 15:10:15 -07:00
|
|
|
import type { ShellExecutionResult } from './shellExecutionService.js';
|
2026-03-27 12:57:26 -04:00
|
|
|
import type { SandboxPolicyManager } from '../policy/sandboxPolicyManager.js';
|
2026-04-13 11:43:13 -07:00
|
|
|
import {
|
|
|
|
|
toPathKey,
|
|
|
|
|
deduplicateAbsolutePaths,
|
|
|
|
|
resolveToRealPath,
|
|
|
|
|
} from '../utils/paths.js';
|
2026-04-09 15:04:16 -07:00
|
|
|
import { resolveGitWorktreePaths } from '../sandbox/utils/fsUtils.js';
|
2026-04-08 15:00:50 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A structured result of fully resolved sandbox paths.
|
|
|
|
|
* All paths in this object are absolute, deduplicated, and expanded to include
|
|
|
|
|
* both the original path and its real target (if it is a symlink).
|
|
|
|
|
*/
|
|
|
|
|
export interface ResolvedSandboxPaths {
|
|
|
|
|
/** The primary workspace directory. */
|
|
|
|
|
workspace: {
|
|
|
|
|
/** The original path provided in the sandbox options. */
|
|
|
|
|
original: string;
|
|
|
|
|
/** The real path. */
|
|
|
|
|
resolved: string;
|
|
|
|
|
};
|
|
|
|
|
/** Explicitly denied paths. */
|
|
|
|
|
forbidden: string[];
|
|
|
|
|
/** Directories included globally across all commands in this sandbox session. */
|
|
|
|
|
globalIncludes: string[];
|
|
|
|
|
/** Paths explicitly allowed by the policy of the currently executing command. */
|
|
|
|
|
policyAllowed: string[];
|
|
|
|
|
/** Paths granted temporary read access by the current command's dynamic permissions. */
|
|
|
|
|
policyRead: string[];
|
|
|
|
|
/** Paths granted temporary write access by the current command's dynamic permissions. */
|
|
|
|
|
policyWrite: string[];
|
2026-04-09 15:04:16 -07:00
|
|
|
/** Auto-detected paths for git worktrees/submodules. */
|
|
|
|
|
gitWorktree?: {
|
|
|
|
|
/** The actual .git directory for this worktree. */
|
|
|
|
|
worktreeGitDir: string;
|
|
|
|
|
/** The main repository's .git directory (if applicable). */
|
|
|
|
|
mainGitDir?: string;
|
|
|
|
|
};
|
2026-04-08 15:00:50 -07:00
|
|
|
}
|
|
|
|
|
|
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[];
|
|
|
|
|
/** 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
|
|
|
}
|
|
|
|
|
|
2026-03-27 12:57:26 -04:00
|
|
|
/**
|
|
|
|
|
* Configuration for the sandbox mode behavior.
|
|
|
|
|
*/
|
|
|
|
|
export interface SandboxModeConfig {
|
|
|
|
|
readonly?: boolean;
|
|
|
|
|
network?: boolean;
|
|
|
|
|
approvedTools?: string[];
|
|
|
|
|
allowOverrides?: boolean;
|
2026-04-01 16:51:06 -07:00
|
|
|
yolo?: boolean;
|
2026-03-27 12:57:26 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 11:43:58 -04:00
|
|
|
/**
|
|
|
|
|
* Global configuration options used to initialize a SandboxManager.
|
|
|
|
|
*/
|
|
|
|
|
export interface GlobalSandboxOptions {
|
2026-04-01 12:27:55 -04:00
|
|
|
/** The absolute path to the primary workspace directory, granted full read/write access. */
|
2026-03-23 11:43:58 -04:00
|
|
|
workspace: string;
|
2026-03-31 22:06:50 +00:00
|
|
|
/** Absolute paths to explicitly include in the workspace context. */
|
|
|
|
|
includeDirectories?: string[];
|
2026-04-01 12:27:55 -04:00
|
|
|
/** An optional asynchronous resolver function for paths that should be explicitly denied. */
|
|
|
|
|
forbiddenPaths?: () => Promise<string[]>;
|
2026-03-27 12:57:26 -04:00
|
|
|
/** The current sandbox mode behavior from config. */
|
|
|
|
|
modeConfig?: SandboxModeConfig;
|
|
|
|
|
/** The policy manager for persistent approvals. */
|
|
|
|
|
policyManager?: SandboxPolicyManager;
|
2026-03-23 11:43:58 -04:00
|
|
|
}
|
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-31 13:35:13 -04:00
|
|
|
/** An optional cleanup function to be called after the command terminates. */
|
|
|
|
|
cleanup?: () => void;
|
2026-03-11 14:42:50 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 15:10:15 -07:00
|
|
|
/**
|
|
|
|
|
* A structured result from parsing sandbox denials.
|
|
|
|
|
*/
|
|
|
|
|
export interface ParsedSandboxDenial {
|
|
|
|
|
/** If the denial is related to file system access, these are the paths that were blocked. */
|
|
|
|
|
filePaths?: string[];
|
|
|
|
|
/** If the denial is related to network access. */
|
|
|
|
|
network?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
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-26 15:10:15 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parses the output of a command to detect sandbox denials.
|
|
|
|
|
*/
|
|
|
|
|
parseDenials(result: ShellExecutionResult): ParsedSandboxDenial | undefined;
|
2026-04-01 16:51:06 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the primary workspace directory for this sandbox.
|
|
|
|
|
*/
|
|
|
|
|
getWorkspace(): string;
|
2026-04-03 17:23:27 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the global sandbox options for this sandbox.
|
|
|
|
|
*/
|
|
|
|
|
getOptions(): GlobalSandboxOptions | undefined;
|
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-26 20:35:21 +00:00
|
|
|
/**
|
|
|
|
|
* Files that contain sensitive secrets or credentials and should be
|
|
|
|
|
* completely hidden (deny read/write) in any sandbox.
|
|
|
|
|
*/
|
|
|
|
|
export const SECRET_FILES = [
|
|
|
|
|
{ pattern: '.env' },
|
|
|
|
|
{ pattern: '.env.*' },
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if a given file name matches any of the secret file patterns.
|
|
|
|
|
*/
|
|
|
|
|
export function isSecretFile(fileName: string): boolean {
|
|
|
|
|
return SECRET_FILES.some((s) => {
|
|
|
|
|
if (s.pattern.endsWith('*')) {
|
|
|
|
|
const prefix = s.pattern.slice(0, -1);
|
|
|
|
|
return fileName.startsWith(prefix);
|
|
|
|
|
}
|
|
|
|
|
return fileName === s.pattern;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns arguments for the Linux 'find' command to locate secret files.
|
|
|
|
|
*/
|
|
|
|
|
export function getSecretFileFindArgs(): string[] {
|
|
|
|
|
const args: string[] = ['('];
|
|
|
|
|
SECRET_FILES.forEach((s, i) => {
|
|
|
|
|
if (i > 0) args.push('-o');
|
|
|
|
|
args.push('-name', s.pattern);
|
|
|
|
|
});
|
|
|
|
|
args.push(')');
|
|
|
|
|
return args;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Finds all secret files in a directory up to a certain depth.
|
|
|
|
|
* Default is shallow scan (depth 1) for performance.
|
|
|
|
|
*/
|
|
|
|
|
export async function findSecretFiles(
|
|
|
|
|
baseDir: string,
|
|
|
|
|
maxDepth = 1,
|
|
|
|
|
): Promise<string[]> {
|
|
|
|
|
const secrets: string[] = [];
|
|
|
|
|
const skipDirs = new Set([
|
|
|
|
|
'node_modules',
|
|
|
|
|
'.git',
|
|
|
|
|
'.venv',
|
|
|
|
|
'__pycache__',
|
|
|
|
|
'dist',
|
|
|
|
|
'build',
|
|
|
|
|
'.next',
|
|
|
|
|
'.idea',
|
|
|
|
|
'.vscode',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
async function walk(dir: string, depth: number) {
|
|
|
|
|
if (depth > maxDepth) return;
|
|
|
|
|
try {
|
|
|
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const fullPath = path.join(dir, entry.name);
|
|
|
|
|
if (entry.isDirectory()) {
|
|
|
|
|
if (!skipDirs.has(entry.name)) {
|
|
|
|
|
await walk(fullPath, depth + 1);
|
|
|
|
|
}
|
|
|
|
|
} else if (entry.isFile()) {
|
|
|
|
|
if (isSecretFile(entry.name)) {
|
|
|
|
|
secrets.push(fullPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore read errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await walk(baseDir, 1);
|
|
|
|
|
return secrets;
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
2026-04-01 16:51:06 -07:00
|
|
|
constructor(private options?: GlobalSandboxOptions) {}
|
|
|
|
|
|
2026-03-11 14:42:50 -07:00
|
|
|
/**
|
|
|
|
|
* 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-26 15:10:15 -07:00
|
|
|
|
|
|
|
|
parseDenials(): undefined {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-04-01 16:51:06 -07:00
|
|
|
|
|
|
|
|
getWorkspace(): string {
|
|
|
|
|
return this.options?.workspace ?? process.cwd();
|
|
|
|
|
}
|
2026-04-03 17:23:27 -07:00
|
|
|
|
|
|
|
|
getOptions(): GlobalSandboxOptions | undefined {
|
|
|
|
|
return this.options;
|
|
|
|
|
}
|
2026-03-11 14:42:50 -07:00
|
|
|
}
|
2026-03-13 14:11:51 -07:00
|
|
|
|
|
|
|
|
/**
|
2026-03-26 15:10:15 -07:00
|
|
|
* A SandboxManager implementation that just runs locally (no sandboxing yet).
|
2026-03-13 14:11:51 -07:00
|
|
|
*/
|
|
|
|
|
export class LocalSandboxManager implements SandboxManager {
|
2026-04-01 16:51:06 -07:00
|
|
|
constructor(private options?: GlobalSandboxOptions) {}
|
|
|
|
|
|
2026-03-13 14:11:51 -07:00
|
|
|
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-26 15:10:15 -07:00
|
|
|
|
|
|
|
|
parseDenials(): undefined {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2026-04-01 16:51:06 -07:00
|
|
|
|
|
|
|
|
getWorkspace(): string {
|
|
|
|
|
return this.options?.workspace ?? process.cwd();
|
|
|
|
|
}
|
2026-04-03 17:23:27 -07:00
|
|
|
|
|
|
|
|
getOptions(): GlobalSandboxOptions | undefined {
|
|
|
|
|
return this.options;
|
|
|
|
|
}
|
2026-03-13 14:11:51 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-01 12:27:55 -04:00
|
|
|
/**
|
2026-04-08 15:00:50 -07:00
|
|
|
* Resolves and sanitizes all path categories for a sandbox request.
|
2026-04-01 12:27:55 -04:00
|
|
|
*/
|
|
|
|
|
export async function resolveSandboxPaths(
|
|
|
|
|
options: GlobalSandboxOptions,
|
|
|
|
|
req: SandboxRequest,
|
2026-04-08 15:00:50 -07:00
|
|
|
overridePermissions?: SandboxPermissions,
|
|
|
|
|
): Promise<ResolvedSandboxPaths> {
|
|
|
|
|
/**
|
|
|
|
|
* Helper that expands each path to include its realpath (if it's a symlink)
|
2026-04-13 11:43:13 -07:00
|
|
|
* and pipes the result through deduplicateAbsolutePaths for deduplication and absolute path enforcement.
|
2026-04-08 15:00:50 -07:00
|
|
|
*/
|
|
|
|
|
const expand = (paths?: string[] | null): string[] => {
|
|
|
|
|
if (!paths || paths.length === 0) return [];
|
|
|
|
|
const expanded = paths.flatMap((p) => {
|
|
|
|
|
try {
|
|
|
|
|
const resolved = resolveToRealPath(p);
|
|
|
|
|
return resolved === p ? [p] : [p, resolved];
|
|
|
|
|
} catch {
|
|
|
|
|
return [p];
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-13 11:43:13 -07:00
|
|
|
return deduplicateAbsolutePaths(expanded);
|
2026-04-08 15:00:50 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const forbidden = expand(await options.forbiddenPaths?.());
|
|
|
|
|
|
|
|
|
|
const globalIncludes = expand(options.includeDirectories);
|
|
|
|
|
const policyAllowed = expand(req.policy?.allowedPaths);
|
|
|
|
|
|
|
|
|
|
const policyRead = expand(overridePermissions?.fileSystem?.read);
|
|
|
|
|
const policyWrite = expand(overridePermissions?.fileSystem?.write);
|
2026-04-01 12:27:55 -04:00
|
|
|
|
2026-04-08 15:00:50 -07:00
|
|
|
const resolvedWorkspace = resolveToRealPath(options.workspace);
|
|
|
|
|
|
|
|
|
|
const workspaceIdentities = new Set(
|
2026-04-13 11:43:13 -07:00
|
|
|
[options.workspace, resolvedWorkspace].map(toPathKey),
|
2026-04-08 15:00:50 -07:00
|
|
|
);
|
2026-04-13 11:43:13 -07:00
|
|
|
const forbiddenIdentities = new Set(forbidden.map(toPathKey));
|
2026-04-01 12:27:55 -04:00
|
|
|
|
2026-04-09 15:04:16 -07:00
|
|
|
const { worktreeGitDir, mainGitDir } =
|
|
|
|
|
await resolveGitWorktreePaths(resolvedWorkspace);
|
|
|
|
|
const gitWorktree = worktreeGitDir
|
|
|
|
|
? { gitWorktree: { worktreeGitDir, mainGitDir } }
|
|
|
|
|
: undefined;
|
|
|
|
|
|
2026-04-16 14:18:09 -07:00
|
|
|
if (worktreeGitDir) {
|
|
|
|
|
const gitIdentities = new Set(
|
|
|
|
|
[
|
|
|
|
|
path.join(options.workspace, '.git'),
|
|
|
|
|
path.join(resolvedWorkspace, '.git'),
|
|
|
|
|
].map(toPathKey),
|
|
|
|
|
);
|
|
|
|
|
if (policyRead.some((p) => gitIdentities.has(toPathKey(p)))) {
|
|
|
|
|
policyRead.push(worktreeGitDir);
|
|
|
|
|
if (mainGitDir) policyRead.push(mainGitDir);
|
|
|
|
|
}
|
|
|
|
|
if (policyWrite.some((p) => gitIdentities.has(toPathKey(p)))) {
|
|
|
|
|
policyWrite.push(worktreeGitDir);
|
|
|
|
|
if (mainGitDir) policyWrite.push(mainGitDir);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 15:00:50 -07:00
|
|
|
/**
|
|
|
|
|
* Filters out any paths that are explicitly forbidden or match the workspace root (original or resolved).
|
|
|
|
|
*/
|
|
|
|
|
const filter = (paths: string[]) =>
|
|
|
|
|
paths.filter((p) => {
|
2026-04-13 11:43:13 -07:00
|
|
|
const identity = toPathKey(p);
|
2026-04-08 15:00:50 -07:00
|
|
|
return (
|
|
|
|
|
!workspaceIdentities.has(identity) && !forbiddenIdentities.has(identity)
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-04-01 12:27:55 -04:00
|
|
|
|
|
|
|
|
return {
|
2026-04-08 15:00:50 -07:00
|
|
|
workspace: {
|
|
|
|
|
original: options.workspace,
|
|
|
|
|
resolved: resolvedWorkspace,
|
|
|
|
|
},
|
2026-04-01 12:27:55 -04:00
|
|
|
forbidden,
|
2026-04-08 15:00:50 -07:00
|
|
|
globalIncludes: filter(globalIncludes),
|
|
|
|
|
policyAllowed: filter(policyAllowed),
|
|
|
|
|
policyRead: filter(policyRead),
|
|
|
|
|
policyWrite: filter(policyWrite),
|
2026-04-09 15:04:16 -07:00
|
|
|
...gitWorktree,
|
2026-04-01 12:27:55 -04:00
|
|
|
};
|
|
|
|
|
}
|
2026-04-09 15:04:16 -07:00
|
|
|
|
2026-03-19 15:25:22 -07:00
|
|
|
export { createSandboxManager } from './sandboxManagerFactory.js';
|