fix(core): enhance sandbox usability and fix build error (#24460)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Gal Zahavi
2026-04-01 16:51:06 -07:00
committed by GitHub
parent ca78a0f177
commit 13ccc16457
22 changed files with 1285 additions and 53 deletions
@@ -144,6 +144,10 @@ export class LinuxSandboxManager implements SandboxManager {
return parsePosixSandboxDenials(result);
}
getWorkspace(): string {
return this.options.workspace;
}
private getMaskFilePath(): string {
if (
LinuxSandboxManager.maskFilePath &&
@@ -193,9 +197,11 @@ export class LinuxSandboxManager implements SandboxManager {
this.options.modeConfig?.approvedTools,
)
: false;
const workspaceWrite = !isReadonlyMode || isApproved;
const isYolo = this.options.modeConfig?.yolo ?? false;
const workspaceWrite = !isReadonlyMode || isApproved || isYolo;
const networkAccess =
this.options.modeConfig?.network || req.policy?.networkAccess || false;
this.options.modeConfig?.network || req.policy?.networkAccess || isYolo;
const persistentPermissions = allowOverrides
? this.options.policyManager?.getCommandPermissions(commandName)
@@ -140,6 +140,31 @@ describe('MacOsSandboxManager', () => {
);
});
it('should NOT whitelist root in YOLO mode', async () => {
manager = new MacOsSandboxManager({
workspace: mockWorkspace,
modeConfig: { readonly: false, allowOverrides: true, yolo: true },
});
await manager.prepareCommand({
command: 'ls',
args: ['/'],
cwd: mockWorkspace,
env: {},
});
expect(seatbeltArgsBuilder.buildSeatbeltProfile).toHaveBeenCalledWith(
expect.objectContaining({
additionalPermissions: expect.objectContaining({
fileSystem: expect.objectContaining({
read: expect.not.arrayContaining(['/']),
write: expect.not.arrayContaining(['/']),
}),
}),
}),
);
});
describe('virtual commands', () => {
it('should translate __read to /bin/cat', async () => {
const testFile = path.join(mockWorkspace, 'file.txt');
@@ -55,6 +55,10 @@ export class MacOsSandboxManager implements SandboxManager {
return parsePosixSandboxDenials(result);
}
getWorkspace(): string {
return this.options.workspace;
}
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
await initializeShellParsers();
const sanitizationConfig = getSecureSanitizationConfig(
@@ -90,9 +94,11 @@ export class MacOsSandboxManager implements SandboxManager {
)
: false;
const workspaceWrite = !isReadonlyMode || isApproved;
const isYolo = this.options.modeConfig?.yolo ?? false;
const workspaceWrite = !isReadonlyMode || isApproved || isYolo;
const defaultNetwork =
this.options.modeConfig?.network || req.policy?.networkAccess || false;
this.options.modeConfig?.network || req.policy?.networkAccess || isYolo;
const { allowed: allowedPaths, forbidden: forbiddenPaths } =
await resolveSandboxPaths(this.options, req);
@@ -103,7 +109,6 @@ export class MacOsSandboxManager implements SandboxManager {
? this.options.policyManager?.getCommandPermissions(commandName)
: undefined;
// Merge all permissions
const mergedAdditional: SandboxPermissions = {
fileSystem: {
read: [
+28 -4
View File
@@ -23,6 +23,15 @@ export const BASE_SEATBELT_PROFILE = `(version 1)
(allow signal (target same-sandbox))
(allow process-info*)
; Map system frameworks + dylibs for loader.
(allow file-map-executable
(subpath "/System/Library/Frameworks")
(subpath "/System/Library/PrivateFrameworks")
(subpath "/usr/lib")
(subpath "/bin")
(subpath "/usr/bin")
)
(allow file-write-data
(require-all
(path "/dev/null")
@@ -86,16 +95,22 @@ export const BASE_SEATBELT_PROFILE = `(version 1)
(allow mach-lookup
(global-name "com.apple.sysmond")
(global-name "com.apple.system.opendirectoryd.libinfo")
(global-name "com.apple.system.opendirectoryd.membership")
(global-name "com.apple.system.logger")
(global-name "com.apple.system.notification_center")
(global-name "com.apple.logd")
(global-name "com.apple.secinitd")
(global-name "com.apple.trustd.agent")
(global-name "com.apple.trustd")
(global-name "com.apple.analyticsd")
(global-name "com.apple.analyticsd.messagetracer")
)
\n; IOKit
(allow iokit-open
(iokit-registry-entry-class "RootDomainUserClient")
)
(allow mach-lookup
(global-name "com.apple.system.opendirectoryd.libinfo")
)
; Needed for python multiprocessing on MacOS for the SemLock
(allow ipc-posix-sem)
@@ -132,10 +147,19 @@ export const BASE_SEATBELT_PROFILE = `(version 1)
(allow file-read* file-write*
(literal "/dev/null")
(literal "/dev/zero")
(literal "/dev/tty")
(subpath "/dev/fd")
(subpath "/tmp")
(subpath "/private/tmp")
)
(allow file-read-metadata
(literal "/")
(subpath "/var")
(subpath "/private/var")
(subpath "/dev")
)
`;
/**
@@ -0,0 +1,208 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
getProactiveToolSuggestions,
isNetworkReliantCommand,
} from './proactivePermissions.js';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
vi.mock('node:os');
vi.mock('node:fs', () => ({
default: {
promises: {
access: vi.fn(),
},
constants: {
F_OK: 0,
},
},
promises: {
access: vi.fn(),
},
constants: {
F_OK: 0,
},
}));
describe('proactivePermissions', () => {
const homeDir = '/Users/testuser';
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(os.homedir).mockReturnValue(homeDir);
vi.mocked(os.platform).mockReturnValue('darwin');
});
describe('isNetworkReliantCommand', () => {
it('should return true for always-network tools', () => {
expect(isNetworkReliantCommand('ssh')).toBe(true);
expect(isNetworkReliantCommand('git')).toBe(true);
expect(isNetworkReliantCommand('curl')).toBe(true);
});
it('should return true for network-heavy node subcommands', () => {
expect(isNetworkReliantCommand('npm', 'install')).toBe(true);
expect(isNetworkReliantCommand('yarn', 'add')).toBe(true);
expect(isNetworkReliantCommand('bun', '')).toBe(true);
});
it('should return false for local node subcommands', () => {
expect(isNetworkReliantCommand('npm', 'test')).toBe(false);
expect(isNetworkReliantCommand('yarn', 'run')).toBe(false);
});
it('should return false for unknown tools', () => {
expect(isNetworkReliantCommand('ls')).toBe(false);
});
});
describe('getProactiveToolSuggestions', () => {
it('should return undefined for unknown tools', async () => {
expect(await getProactiveToolSuggestions('ls')).toBeUndefined();
expect(await getProactiveToolSuggestions('node')).toBeUndefined();
});
it('should return permissions for npm if paths exist', async () => {
vi.mocked(fs.promises.access).mockImplementation(
(p: fs.PathLike, _mode?: number) => {
const pathStr = p.toString();
if (
pathStr === path.join(homeDir, '.npm') ||
pathStr === path.join(homeDir, '.cache') ||
pathStr === path.join(homeDir, '.npmrc')
) {
return Promise.resolve();
}
return Promise.reject(new Error('ENOENT'));
},
);
const permissions = await getProactiveToolSuggestions('npm');
expect(permissions).toBeDefined();
expect(permissions?.network).toBe(true);
// .npmrc should be read-only
expect(permissions?.fileSystem?.read).toContain(
path.join(homeDir, '.npmrc'),
);
expect(permissions?.fileSystem?.write).not.toContain(
path.join(homeDir, '.npmrc'),
);
// .npm should be read-write
expect(permissions?.fileSystem?.read).toContain(
path.join(homeDir, '.npm'),
);
expect(permissions?.fileSystem?.write).toContain(
path.join(homeDir, '.npm'),
);
// .cache should be read-write
expect(permissions?.fileSystem?.write).toContain(
path.join(homeDir, '.cache'),
);
// should NOT contain .ssh or .gitconfig for npm
expect(permissions?.fileSystem?.read).not.toContain(
path.join(homeDir, '.ssh'),
);
});
it('should grant network access and suggest primary cache paths even if they do not exist', async () => {
vi.mocked(fs.promises.access).mockRejectedValue(new Error('ENOENT'));
const permissions = await getProactiveToolSuggestions('npm');
expect(permissions).toBeDefined();
expect(permissions?.network).toBe(true);
expect(permissions?.fileSystem?.write).toContain(
path.join(homeDir, '.npm'),
);
// .cache is optional and should NOT be included if it doesn't exist
expect(permissions?.fileSystem?.write).not.toContain(
path.join(homeDir, '.cache'),
);
});
it('should suggest .ssh and .gitconfig only for git', async () => {
vi.mocked(fs.promises.access).mockImplementation(
(p: fs.PathLike, _mode?: number) => {
const pathStr = p.toString();
if (
pathStr === path.join(homeDir, '.ssh') ||
pathStr === path.join(homeDir, '.gitconfig')
) {
return Promise.resolve();
}
return Promise.reject(new Error('ENOENT'));
},
);
const permissions = await getProactiveToolSuggestions('git');
expect(permissions?.network).toBe(true);
expect(permissions?.fileSystem?.read).toContain(
path.join(homeDir, '.ssh'),
);
expect(permissions?.fileSystem?.read).toContain(
path.join(homeDir, '.gitconfig'),
);
});
it('should suggest .ssh but NOT .gitconfig for ssh', async () => {
vi.mocked(fs.promises.access).mockImplementation(
(p: fs.PathLike, _mode?: number) => {
const pathStr = p.toString();
if (pathStr === path.join(homeDir, '.ssh')) {
return Promise.resolve();
}
return Promise.reject(new Error('ENOENT'));
},
);
const permissions = await getProactiveToolSuggestions('ssh');
expect(permissions?.network).toBe(true);
expect(permissions?.fileSystem?.read).toContain(
path.join(homeDir, '.ssh'),
);
expect(permissions?.fileSystem?.read).not.toContain(
path.join(homeDir, '.gitconfig'),
);
});
it('should handle Windows specific paths', async () => {
vi.mocked(os.platform).mockReturnValue('win32');
const appData = 'C:\\Users\\testuser\\AppData\\Roaming';
vi.stubEnv('AppData', appData);
vi.mocked(fs.promises.access).mockImplementation(
(p: fs.PathLike, _mode?: number) => {
const pathStr = p.toString();
if (pathStr === path.join(appData, 'npm')) {
return Promise.resolve();
}
return Promise.reject(new Error('ENOENT'));
},
);
const permissions = await getProactiveToolSuggestions('npm.exe');
expect(permissions).toBeDefined();
expect(permissions?.fileSystem?.read).toContain(
path.join(appData, 'npm'),
);
vi.unstubAllEnvs();
});
it('should include bun, pnpm, and yarn specific paths', async () => {
vi.mocked(fs.promises.access).mockResolvedValue(undefined);
const bun = await getProactiveToolSuggestions('bun');
expect(bun?.fileSystem?.read).toContain(path.join(homeDir, '.bun'));
expect(bun?.fileSystem?.read).not.toContain(path.join(homeDir, '.yarn'));
const yarn = await getProactiveToolSuggestions('yarn');
expect(yarn?.fileSystem?.read).toContain(path.join(homeDir, '.yarn'));
});
});
});
@@ -0,0 +1,189 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { type SandboxPermissions } from '../../services/sandboxManager.js';
const NETWORK_RELIANT_TOOLS = new Set([
'npm',
'npx',
'yarn',
'pnpm',
'bun',
'git',
'ssh',
'scp',
'sftp',
'curl',
'wget',
]);
const NODE_ECOSYSTEM_TOOLS = new Set(['npm', 'npx', 'yarn', 'pnpm', 'bun']);
const NETWORK_HEAVY_SUBCOMMANDS = new Set([
'install',
'i',
'ci',
'update',
'up',
'publish',
'add',
'remove',
'outdated',
'audit',
]);
/**
* Returns true if the command or subcommand is known to be network-reliant.
*/
export function isNetworkReliantCommand(
commandName: string,
subCommand?: string,
): boolean {
const normalizedCommand = commandName.toLowerCase().replace(/\.exe$/, '');
if (!NETWORK_RELIANT_TOOLS.has(normalizedCommand)) {
return false;
}
// Node ecosystem tools only need network for specific subcommands
if (NODE_ECOSYSTEM_TOOLS.has(normalizedCommand)) {
// Bare yarn/bun/pnpm is an alias for install
if (
!subCommand &&
(normalizedCommand === 'yarn' ||
normalizedCommand === 'bun' ||
normalizedCommand === 'pnpm')
) {
return true;
}
return (
!!subCommand && NETWORK_HEAVY_SUBCOMMANDS.has(subCommand.toLowerCase())
);
}
// Other tools (ssh, git, curl, etc.) are always network-reliant
return true;
}
/**
* Returns suggested additional permissions for network-reliant tools
* based on common configuration and cache directories.
*/
/**
* Returns suggested additional permissions for network-reliant tools
* based on common configuration and cache directories.
*/
export async function getProactiveToolSuggestions(
commandName: string,
): Promise<SandboxPermissions | undefined> {
const normalizedCommand = commandName.toLowerCase().replace(/\.exe$/, '');
if (!NETWORK_RELIANT_TOOLS.has(normalizedCommand)) {
return undefined;
}
const home = os.homedir();
const readOnlyPaths: string[] = [];
const primaryCachePaths: string[] = [];
const optionalCachePaths: string[] = [];
if (normalizedCommand === 'npm' || normalizedCommand === 'npx') {
readOnlyPaths.push(path.join(home, '.npmrc'));
primaryCachePaths.push(path.join(home, '.npm'));
optionalCachePaths.push(path.join(home, '.node-gyp'));
optionalCachePaths.push(path.join(home, '.cache'));
} else if (normalizedCommand === 'yarn') {
readOnlyPaths.push(path.join(home, '.yarnrc'));
readOnlyPaths.push(path.join(home, '.yarnrc.yml'));
primaryCachePaths.push(path.join(home, '.yarn'));
primaryCachePaths.push(path.join(home, '.config', 'yarn'));
optionalCachePaths.push(path.join(home, '.cache'));
} else if (normalizedCommand === 'pnpm') {
readOnlyPaths.push(path.join(home, '.npmrc'));
primaryCachePaths.push(path.join(home, '.pnpm-store'));
primaryCachePaths.push(path.join(home, '.config', 'pnpm'));
optionalCachePaths.push(path.join(home, '.cache'));
} else if (normalizedCommand === 'bun') {
readOnlyPaths.push(path.join(home, '.bunfig.toml'));
primaryCachePaths.push(path.join(home, '.bun'));
optionalCachePaths.push(path.join(home, '.cache'));
} else if (normalizedCommand === 'git') {
readOnlyPaths.push(path.join(home, '.ssh'));
readOnlyPaths.push(path.join(home, '.gitconfig'));
optionalCachePaths.push(path.join(home, '.cache'));
} else if (
normalizedCommand === 'ssh' ||
normalizedCommand === 'scp' ||
normalizedCommand === 'sftp'
) {
readOnlyPaths.push(path.join(home, '.ssh'));
}
// Windows specific paths
if (os.platform() === 'win32') {
const appData = process.env['AppData'];
const localAppData = process.env['LocalAppData'];
if (normalizedCommand === 'npm' || normalizedCommand === 'npx') {
if (appData) {
primaryCachePaths.push(path.join(appData, 'npm'));
optionalCachePaths.push(path.join(appData, 'npm-cache'));
}
if (localAppData) {
optionalCachePaths.push(path.join(localAppData, 'npm-cache'));
}
}
}
const finalReadOnly: string[] = [];
const finalReadWrite: string[] = [];
const checkExists = async (p: string): Promise<boolean> => {
try {
await fs.promises.access(p, fs.constants.F_OK);
return true;
} catch {
return false;
}
};
const readOnlyChecks = await Promise.all(
readOnlyPaths.map(async (p) => ({ path: p, exists: await checkExists(p) })),
);
for (const { path: p, exists } of readOnlyChecks) {
if (exists) {
finalReadOnly.push(p);
}
}
for (const p of primaryCachePaths) {
finalReadWrite.push(p);
}
const optionalChecks = await Promise.all(
optionalCachePaths.map(async (p) => ({
path: p,
exists: await checkExists(p),
})),
);
for (const { path: p, exists } of optionalChecks) {
if (exists) {
finalReadWrite.push(p);
}
}
return {
fileSystem:
finalReadOnly.length > 0 || finalReadWrite.length > 0
? {
read: [...finalReadOnly, ...finalReadWrite],
write: finalReadWrite,
}
: undefined,
network: true,
};
}
@@ -40,4 +40,80 @@ describe('parsePosixSandboxDenials', () => {
} as unknown as ShellExecutionResult);
expect(parsed).toBeUndefined();
});
it('should detect npm specific file system denials', () => {
const output = `
npm verbose logfile could not be created: Error: EPERM: operation not permitted, open '/Users/galzahavi/.npm/_logs/2026-04-01T02_47_18_624Z-debug-0.log'
`;
const parsed = parsePosixSandboxDenials({
output,
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.filePaths).toContain(
'/Users/galzahavi/.npm/_logs/2026-04-01T02_47_18_624Z-debug-0.log',
);
});
it('should detect npm specific path errors', () => {
const output = `
npm error code EPERM
npm error syscall open
npm error path /Users/galzahavi/.npm/_cacache/tmp/ccf579a2
`;
const parsed = parsePosixSandboxDenials({
output,
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.filePaths).toContain(
'/Users/galzahavi/.npm/_cacache/tmp/ccf579a2',
);
});
it('should detect network denials with ENOTFOUND', () => {
const output = `
npm http fetch GET https://registry.npmjs.org/2 attempt 1 failed with ENOTFOUND
`;
const parsed = parsePosixSandboxDenials({
output,
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.network).toBe(true);
});
it('should detect non-verbose npm path errors', () => {
const output = `
npm ERR! code EPERM
npm ERR! syscall open
npm ERR! path /Users/galzahavi/.npm/_cacache/tmp/ccf579a2
`;
const parsed = parsePosixSandboxDenials({
output,
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.filePaths).toContain(
'/Users/galzahavi/.npm/_cacache/tmp/ccf579a2',
);
});
it('should detect pnpm specific network errors', () => {
const output = `
ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/nonexistent: Not Found
`;
const parsed = parsePosixSandboxDenials({
output,
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.network).toBe(true);
});
it('should detect pnpm specific file system errors', () => {
const output = `
EACCES: permission denied, mkdir '/Users/galzahavi/.pnpm-store/v3'
`;
const parsed = parsePosixSandboxDenials({
output,
} as unknown as ShellExecutionResult);
expect(parsed).toBeDefined();
expect(parsed?.filePaths).toContain('/Users/galzahavi/.pnpm-store/v3');
});
});
@@ -20,6 +20,9 @@ export function parsePosixSandboxDenials(
const isFileDenial = [
'operation not permitted',
'permission denied',
'eperm',
'eacces',
'vim:e303',
'should be read/write',
'sandbox_apply',
@@ -32,6 +35,17 @@ export function parsePosixSandboxDenials(
'could not resolve host',
'connection refused',
'no address associated with hostname',
'econnrefused',
'enotfound',
'etimedout',
'econnreset',
'network error',
'getaddrinfo',
'socket hang up',
'connect-timeout',
'err_pnpm_fetch',
'err_pnpm_no_matching_version',
"syscall: 'listen'",
].some((keyword) => combined.includes(keyword));
if (!isFileDenial && !isNetworkDenial) {
@@ -40,17 +54,31 @@ export function parsePosixSandboxDenials(
const filePaths = new Set<string>();
// Extract denied paths (POSIX absolute paths)
const regex =
/(?:^|\s)['"]?(\/[\w.-/]+)['"]?:\s*[Oo]peration not permitted/gi;
let match;
while ((match = regex.exec(output)) !== null) {
filePaths.add(match[1]);
}
if (errorOutput) {
while ((match = regex.exec(errorOutput)) !== null) {
// Extract denied paths (POSIX absolute paths or home-relative paths starting with ~)
const regexes = [
// format: /path: operation not permitted
/(?:^|\s)['"]?((?:\/|~)[\w.\-/:~]+)['"]?:\s*[Oo]peration not permitted/gi,
// format: operation not permitted, open '/path'
/[Oo]peration not permitted,\s*open\s*['"]?((?:\/|~)[\w.\-/:~]+)['"]?/gi,
// format: permission denied, open '/path'
/[Pp]ermission denied,\s*open\s*['"]?((?:\/|~)[\w.\-/:~]+)['"]?/gi,
// format: npm error path /path or npm ERR! path /path
/npm\s+(?:error|ERR!)\s+path\s+((?:\/|~)[\w.\-/:~]+)/gi,
// format: EACCES: permission denied, mkdir '/path'
/EACCES:\s*permission denied,\s*\w+\s*['"]?((?:\/|~)[\w.\-/:~]+)['"]?/gi,
];
for (const regex of regexes) {
let match;
while ((match = regex.exec(output)) !== null) {
filePaths.add(match[1]);
}
if (errorOutput) {
regex.lastIndex = 0; // Reset for next use
while ((match = regex.exec(errorOutput)) !== null) {
filePaths.add(match[1]);
}
}
}
// Fallback heuristic: look for any absolute path in the output if it was a file denial
@@ -86,6 +86,35 @@ describe('WindowsSandboxManager', () => {
expect(result.args[0]).toBe('1');
});
it('should NOT whitelist drive roots in YOLO mode', async () => {
manager = new WindowsSandboxManager({
workspace: testCwd,
modeConfig: { readonly: false, allowOverrides: true, yolo: true },
forbiddenPaths: async () => [],
});
const req: SandboxRequest = {
command: 'whoami',
args: [],
cwd: testCwd,
env: {},
};
await manager.prepareCommand(req);
// Verify spawnAsync was called for icacls
const icaclsCalls = vi
.mocked(spawnAsync)
.mock.calls.filter((call) => call[0] === 'icacls');
// Should NOT have called icacls for C:\, D:\, etc.
const driveRootCalls = icaclsCalls.filter(
(call) =>
typeof call[1]?.[0] === 'string' && /^[A-Z]:\\$/.test(call[1][0]),
);
expect(driveRootCalls).toHaveLength(0);
});
it('should handle network access from additionalPermissions', async () => {
const req: SandboxRequest = {
command: 'whoami',
@@ -72,6 +72,10 @@ export class WindowsSandboxManager implements SandboxManager {
return parseWindowsSandboxDenials(result);
}
getWorkspace(): string {
return this.options.workspace;
}
/**
* Ensures a file or directory exists.
*/
@@ -240,6 +244,8 @@ export class WindowsSandboxManager implements SandboxManager {
];
}
const isYolo = this.options.modeConfig?.yolo ?? false;
// Fetch persistent approvals for this command
const commandName = await getCommandName(command, args);
const persistentPermissions = allowOverrides
@@ -259,6 +265,7 @@ export class WindowsSandboxManager implements SandboxManager {
],
},
network:
isYolo ||
persistentPermissions?.network ||
req.policy?.additionalPermissions?.network ||
false,
@@ -301,7 +308,9 @@ export class WindowsSandboxManager implements SandboxManager {
// Grant "Low Mandatory Level" read/write access to allowedPaths.
for (const allowedPath of allowedPaths) {
const resolved = await tryRealpath(allowedPath);
if (!fs.existsSync(resolved)) {
try {
await fs.promises.access(resolved, fs.constants.F_OK);
} catch {
throw new Error(
`Sandbox request rejected: Allowed path does not exist: ${resolved}. ` +
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',
@@ -316,7 +325,9 @@ export class WindowsSandboxManager implements SandboxManager {
);
for (const writePath of additionalWritePaths) {
const resolved = await tryRealpath(writePath);
if (!fs.existsSync(resolved)) {
try {
await fs.promises.access(resolved, fs.constants.F_OK);
} catch {
throw new Error(
`Sandbox request rejected: Additional write path does not exist: ${resolved}. ` +
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',