feat(core): add forbiddenPaths to GlobalSandboxOptions and refactor createSandboxManager (#23936)

This commit is contained in:
Emily Hedlund
2026-03-27 12:57:26 -04:00
committed by GitHub
parent 33cf2da1df
commit 535667baf6
11 changed files with 170 additions and 155 deletions
@@ -142,7 +142,7 @@ function ensureSandboxAvailable(): boolean {
describe('SandboxManager Integration', () => {
const workspace = process.cwd();
const manager = createSandboxManager({ enabled: true }, workspace);
const manager = createSandboxManager({ enabled: true }, { workspace });
// Skip if we are on an unsupported platform or if it's a NoopSandboxManager
const shouldSkip =
@@ -235,7 +235,7 @@ describe('SandboxManager Integration', () => {
try {
const osManager = createSandboxManager(
{ enabled: true },
tempWorkspace,
{ workspace: tempWorkspace, forbiddenPaths: [forbiddenDir] },
);
const { command, args } = Platform.touch(testFile);
@@ -244,7 +244,6 @@ describe('SandboxManager Integration', () => {
args,
cwd: tempWorkspace,
env: process.env,
policy: { forbiddenPaths: [forbiddenDir] },
});
const result = await runCommand(sandboxed);
@@ -268,7 +267,7 @@ describe('SandboxManager Integration', () => {
try {
const osManager = createSandboxManager(
{ enabled: true },
tempWorkspace,
{ workspace: tempWorkspace, forbiddenPaths: [forbiddenDir] },
);
const { command, args } = Platform.cat(nestedFile);
@@ -277,7 +276,6 @@ describe('SandboxManager Integration', () => {
args,
cwd: tempWorkspace,
env: process.env,
policy: { forbiddenPaths: [forbiddenDir] },
});
const result = await runCommand(sandboxed);
@@ -298,7 +296,7 @@ describe('SandboxManager Integration', () => {
try {
const osManager = createSandboxManager(
{ enabled: true },
tempWorkspace,
{ workspace: tempWorkspace, forbiddenPaths: [conflictDir] },
);
const { command, args } = Platform.touch(testFile);
@@ -309,7 +307,6 @@ describe('SandboxManager Integration', () => {
env: process.env,
policy: {
allowedPaths: [conflictDir],
forbiddenPaths: [conflictDir],
},
});
@@ -329,7 +326,7 @@ describe('SandboxManager Integration', () => {
try {
const osManager = createSandboxManager(
{ enabled: true },
tempWorkspace,
{ workspace: tempWorkspace, forbiddenPaths: [nonExistentPath] },
);
const { command, args } = Platform.echo('survived');
const sandboxed = await osManager.prepareCommand({
@@ -339,7 +336,6 @@ describe('SandboxManager Integration', () => {
env: process.env,
policy: {
allowedPaths: [nonExistentPath],
forbiddenPaths: [nonExistentPath],
},
});
const result = await runCommand(sandboxed);
@@ -362,7 +358,7 @@ describe('SandboxManager Integration', () => {
try {
const osManager = createSandboxManager(
{ enabled: true },
tempWorkspace,
{ workspace: tempWorkspace, forbiddenPaths: [nonExistentFile] },
);
// We use touch to attempt creation of the file
@@ -374,7 +370,6 @@ describe('SandboxManager Integration', () => {
args: argsTouch,
cwd: tempWorkspace,
env: process.env,
policy: { forbiddenPaths: [nonExistentFile] },
});
// Execute the command, we expect it to fail (permission denied or read-only file system)
@@ -402,7 +397,7 @@ describe('SandboxManager Integration', () => {
try {
const osManager = createSandboxManager(
{ enabled: true },
tempWorkspace,
{ workspace: tempWorkspace, forbiddenPaths: [symlinkFile] },
);
// Attempt to read the target file directly
@@ -413,7 +408,6 @@ describe('SandboxManager Integration', () => {
args: argsTarget,
cwd: tempWorkspace,
env: process.env,
policy: { forbiddenPaths: [symlinkFile] }, // Forbid the symlink
});
const resultTarget = await runCommand(commandTarget);
expect(resultTarget.status).not.toBe(0);
@@ -426,7 +420,6 @@ describe('SandboxManager Integration', () => {
args: argsLink,
cwd: tempWorkspace,
env: process.env,
policy: { forbiddenPaths: [symlinkFile] }, // Forbid the symlink
});
const resultLink = await runCommand(commandLink);
expect(resultLink.status).not.toBe(0);
@@ -364,7 +364,10 @@ describe('SandboxManager', () => {
describe('createSandboxManager', () => {
it('should return NoopSandboxManager if sandboxing is disabled', () => {
const manager = createSandboxManager({ enabled: false }, '/workspace');
const manager = createSandboxManager(
{ enabled: false },
{ workspace: '/workspace' },
);
expect(manager).toBeInstanceOf(NoopSandboxManager);
});
@@ -375,7 +378,10 @@ describe('SandboxManager', () => {
'should return $expected.name if sandboxing is enabled and platform is $platform',
({ platform, expected }) => {
vi.spyOn(os, 'platform').mockReturnValue(platform);
const manager = createSandboxManager({ enabled: true }, '/workspace');
const manager = createSandboxManager(
{ enabled: true },
{ workspace: '/workspace' },
);
expect(manager).toBeInstanceOf(expected);
},
);
@@ -384,7 +390,7 @@ describe('SandboxManager', () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
const manager = createSandboxManager(
{ enabled: true, command: 'windows-native' },
'/workspace',
{ workspace: '/workspace' },
);
expect(manager).toBeInstanceOf(WindowsSandboxManager);
});
@@ -393,7 +399,7 @@ describe('SandboxManager', () => {
vi.spyOn(os, 'platform').mockReturnValue('win32');
const manager = createSandboxManager(
{ enabled: true, command: 'docker' as unknown as 'windows-native' },
'/workspace',
{ workspace: '/workspace' },
);
expect(manager).toBeInstanceOf(LocalSandboxManager);
});
+17 -2
View File
@@ -22,6 +22,7 @@ import {
type EnvironmentSanitizationConfig,
} from './environmentSanitization.js';
import type { ShellExecutionResult } from './shellExecutionService.js';
import type { SandboxPolicyManager } from '../policy/sandboxPolicyManager.js';
export interface SandboxPermissions {
/** Filesystem permissions. */
fileSystem?: {
@@ -40,8 +41,6 @@ export interface SandboxPermissions {
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. */
@@ -50,6 +49,16 @@ export interface ExecutionPolicy {
additionalPermissions?: SandboxPermissions;
}
/**
* Configuration for the sandbox mode behavior.
*/
export interface SandboxModeConfig {
readonly?: boolean;
network?: boolean;
approvedTools?: string[];
allowOverrides?: boolean;
}
/**
* Global configuration options used to initialize a SandboxManager.
*/
@@ -59,6 +68,12 @@ export interface GlobalSandboxOptions {
* This directory is granted full read and write access.
*/
workspace: string;
/** Absolute paths to explicitly deny read/write access to (overrides allowlists). */
forbiddenPaths?: string[];
/** The current sandbox mode behavior from config. */
modeConfig?: SandboxModeConfig;
/** The policy manager for persistent approvals. */
policyManager?: SandboxPolicyManager;
}
/**
@@ -9,50 +9,36 @@ import {
type SandboxManager,
NoopSandboxManager,
LocalSandboxManager,
type GlobalSandboxOptions,
} from './sandboxManager.js';
import { LinuxSandboxManager } from '../sandbox/linux/LinuxSandboxManager.js';
import { MacOsSandboxManager } from '../sandbox/macos/MacOsSandboxManager.js';
import { WindowsSandboxManager } from '../sandbox/windows/WindowsSandboxManager.js';
import type { SandboxConfig } from '../config/config.js';
import { type SandboxPolicyManager } from '../policy/sandboxPolicyManager.js';
/**
* Creates a sandbox manager based on the provided settings.
*/
export function createSandboxManager(
sandbox: SandboxConfig | undefined,
workspace: string,
policyManager?: SandboxPolicyManager,
options: GlobalSandboxOptions,
approvalMode?: string,
): SandboxManager {
if (approvalMode === 'yolo') {
return new NoopSandboxManager();
}
const modeConfig =
policyManager && approvalMode
? policyManager.getModeConfig(approvalMode)
: undefined;
if (!options.modeConfig && options.policyManager && approvalMode) {
options.modeConfig = options.policyManager.getModeConfig(approvalMode);
}
if (sandbox?.enabled) {
if (os.platform() === 'win32' && sandbox?.command === 'windows-native') {
return new WindowsSandboxManager({
workspace,
modeConfig,
policyManager,
});
return new WindowsSandboxManager(options);
} else if (os.platform() === 'linux') {
return new LinuxSandboxManager({
workspace,
modeConfig,
policyManager,
});
return new LinuxSandboxManager(options);
} else if (os.platform() === 'darwin') {
return new MacOsSandboxManager({
workspace,
modeConfig,
policyManager,
});
return new MacOsSandboxManager(options);
}
return new LocalSandboxManager();
}