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
@@ -362,16 +362,21 @@ describe('LinuxSandboxManager', () => {
});
vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
const bwrapArgs = await getBwrapArgs({
command: 'ls',
args: ['-la'],
cwd: workspace,
env: {},
policy: {
forbiddenPaths: ['/tmp/cache', '/opt/secret.txt'],
},
const customManager = new LinuxSandboxManager({
workspace,
forbiddenPaths: ['/tmp/cache', '/opt/secret.txt'],
});
const bwrapArgs = await getBwrapArgs(
{
command: 'ls',
args: ['-la'],
cwd: workspace,
env: {},
},
customManager,
);
const cacheIndex = bwrapArgs.indexOf('/tmp/cache');
expect(bwrapArgs[cacheIndex - 1]).toBe('--tmpfs');
@@ -389,16 +394,21 @@ describe('LinuxSandboxManager', () => {
return p.toString();
});
const bwrapArgs = await getBwrapArgs({
command: 'ls',
args: ['-la'],
cwd: workspace,
env: {},
policy: {
forbiddenPaths: ['/tmp/forbidden-symlink'],
},
const customManager = new LinuxSandboxManager({
workspace,
forbiddenPaths: ['/tmp/forbidden-symlink'],
});
const bwrapArgs = await getBwrapArgs(
{
command: 'ls',
args: ['-la'],
cwd: workspace,
env: {},
},
customManager,
);
const secretIndex = bwrapArgs.indexOf('/opt/real-target.txt');
expect(bwrapArgs[secretIndex - 2]).toBe('--ro-bind');
expect(bwrapArgs[secretIndex - 1]).toBe('/dev/null');
@@ -412,16 +422,21 @@ describe('LinuxSandboxManager', () => {
});
vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
const bwrapArgs = await getBwrapArgs({
command: 'ls',
args: [],
cwd: workspace,
env: {},
policy: {
forbiddenPaths: ['/tmp/not-here.txt'],
},
const customManager = new LinuxSandboxManager({
workspace,
forbiddenPaths: ['/tmp/not-here.txt'],
});
const bwrapArgs = await getBwrapArgs(
{
command: 'ls',
args: [],
cwd: workspace,
env: {},
},
customManager,
);
const idx = bwrapArgs.indexOf('/tmp/not-here.txt');
expect(bwrapArgs[idx - 2]).toBe('--symlink');
expect(bwrapArgs[idx - 1]).toBe('/dev/null');
@@ -436,16 +451,21 @@ describe('LinuxSandboxManager', () => {
return p.toString();
});
const bwrapArgs = await getBwrapArgs({
command: 'ls',
args: [],
cwd: workspace,
env: {},
policy: {
forbiddenPaths: ['/tmp/dir-link'],
},
const customManager = new LinuxSandboxManager({
workspace,
forbiddenPaths: ['/tmp/dir-link'],
});
const bwrapArgs = await getBwrapArgs(
{
command: 'ls',
args: [],
cwd: workspace,
env: {},
},
customManager,
);
const idx = bwrapArgs.indexOf('/opt/real-dir');
expect(bwrapArgs[idx - 1]).toBe('--tmpfs');
});
@@ -456,17 +476,24 @@ describe('LinuxSandboxManager', () => {
);
vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
const bwrapArgs = await getBwrapArgs({
command: 'ls',
args: ['-la'],
cwd: workspace,
env: {},
policy: {
allowedPaths: ['/tmp/conflict'],
forbiddenPaths: ['/tmp/conflict'],
},
const customManager = new LinuxSandboxManager({
workspace,
forbiddenPaths: ['/tmp/conflict'],
});
const bwrapArgs = await getBwrapArgs(
{
command: 'ls',
args: ['-la'],
cwd: workspace,
env: {},
policy: {
allowedPaths: ['/tmp/conflict'],
},
},
customManager,
);
const bindTryIdx = bwrapArgs.indexOf('--bind-try');
const tmpfsIdx = bwrapArgs.lastIndexOf('--tmpfs');
@@ -25,7 +25,6 @@ import {
} from '../../services/environmentSanitization.js';
import { debugLogger } from '../../utils/debugLogger.js';
import { spawnAsync } from '../../utils/shell-utils.js';
import { type SandboxPolicyManager } from '../../policy/sandboxPolicyManager.js';
import {
isStrictlyApproved,
verifySandboxOverrides,
@@ -134,20 +133,10 @@ function touch(filePath: string, isDirectory: boolean) {
* A SandboxManager implementation for Linux that uses Bubblewrap (bwrap).
*/
export interface LinuxSandboxOptions extends GlobalSandboxOptions {
modeConfig?: {
readonly?: boolean;
network?: boolean;
approvedTools?: string[];
allowOverrides?: boolean;
};
policyManager?: SandboxPolicyManager;
}
export class LinuxSandboxManager implements SandboxManager {
private static maskFilePath: string | undefined;
constructor(private readonly options: LinuxSandboxOptions) {}
constructor(private readonly options: GlobalSandboxOptions) {}
isKnownSafeCommand(args: string[]): boolean {
return isKnownSafeCommand(args);
@@ -333,7 +322,7 @@ export class LinuxSandboxManager implements SandboxManager {
}
}
const forbiddenPaths = sanitizePaths(req.policy?.forbiddenPaths) || [];
const forbiddenPaths = sanitizePaths(this.options.forbiddenPaths) || [];
for (const p of forbiddenPaths) {
let resolved: string;
try {