refactor(core): delegate sandbox denial parsing to SandboxManager (#23928)

This commit is contained in:
Tommaso Sciortino
2026-03-26 15:10:15 -07:00
committed by GitHub
parent 73dd7328df
commit 8868b34c75
10 changed files with 272 additions and 148 deletions

View File

@@ -478,162 +478,113 @@ export class ShellToolInvocation extends BaseToolInvocation<
}
// Heuristic Sandbox Denial Detection
const lowerOutput = (
(result.output || '') +
' ' +
(result.error?.message || '')
).toLowerCase();
const isFileDenial = [
'operation not permitted',
'vim:e303',
'should be read/write',
'sandbox_apply',
'sandbox: ',
].some((keyword) => lowerOutput.includes(keyword));
const isNetworkDenial = [
'error connecting to',
'network is unreachable',
'could not resolve host',
'connection refused',
'no address associated with hostname',
].some((keyword) => lowerOutput.includes(keyword));
// Only trigger heuristic if the command actually failed (exit code != 0 or aborted)
const failed =
if (
!!result.error ||
!!result.signal ||
(result.exitCode !== undefined && result.exitCode !== 0) ||
result.aborted;
result.aborted
) {
const sandboxDenial =
this.context.config.sandboxManager.parseDenials(result);
if (sandboxDenial) {
const strippedCommand = stripShellWrapper(this.params.command);
const rootCommands = getCommandRoots(strippedCommand).filter(
(r) => r !== 'shopt',
);
const rootCommandDisplay =
rootCommands.length > 0 ? rootCommands[0] : 'shell';
if (failed && (isFileDenial || isNetworkDenial)) {
const strippedCommand = stripShellWrapper(this.params.command);
const rootCommands = getCommandRoots(strippedCommand).filter(
(r) => r !== 'shopt',
);
const rootCommandDisplay =
rootCommands.length > 0 ? rootCommands[0] : 'shell';
// Extract denied paths
const deniedPaths = new Set<string>();
const regex =
/(?:^|\s)['"]?(\/[\w.-/]+)['"]?:\s*[Oo]peration not permitted/gi;
let match;
while ((match = regex.exec(result.output || '')) !== null) {
deniedPaths.add(match[1]);
}
while ((match = regex.exec(result.error?.message || '')) !== null) {
deniedPaths.add(match[1]);
}
const readPaths = new Set(
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.read || [],
);
const writePaths = new Set(
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.write || [],
);
if (isFileDenial && deniedPaths.size === 0) {
// Fallback heuristic: look for any absolute path in the output
// Avoid matching simple commands like /bin/sh
const fallbackRegex =
/(?:^|[\s"'[\]])(\/[a-zA-Z0-9_.-]+(?:\/[a-zA-Z0-9_.-]+)+)(?:$|[\s"'[\]:])/gi;
let m;
while ((m = fallbackRegex.exec(result.output || '')) !== null) {
const p = m[1];
if (p && !p.startsWith('/bin/') && !p.startsWith('/usr/bin/')) {
deniedPaths.add(p);
}
}
while (
(m = fallbackRegex.exec(result.error?.message || '')) !== null
) {
const p = m[1];
if (p && !p.startsWith('/bin/') && !p.startsWith('/usr/bin/')) {
deniedPaths.add(p);
}
}
}
const readPaths = new Set(
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.read || [],
);
const writePaths = new Set(
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.write || [],
);
for (const p of deniedPaths) {
try {
// Find an existing parent directory to add instead of a non-existent file
let currentPath = p;
try {
if (
fs.existsSync(currentPath) &&
fs.statSync(currentPath).isFile()
) {
currentPath = path.dirname(currentPath);
}
} catch (_e) {
/* ignore */
}
while (currentPath.length > 1) {
if (fs.existsSync(currentPath)) {
writePaths.add(currentPath);
readPaths.add(currentPath);
break;
}
currentPath = path.dirname(currentPath);
}
} catch (_e) {
// ignore
}
}
const additionalPermissions = {
network:
isNetworkDenial ||
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.network ||
undefined,
fileSystem:
isFileDenial || writePaths.size > 0
? {
read: Array.from(readPaths),
write: Array.from(writePaths),
if (sandboxDenial.filePaths) {
for (const p of sandboxDenial.filePaths) {
try {
// Find an existing parent directory to add instead of a non-existent file
let currentPath = p;
try {
if (
fs.existsSync(currentPath) &&
fs.statSync(currentPath).isFile()
) {
currentPath = path.dirname(currentPath);
}
} catch (_e) {
/* ignore */
}
: undefined,
};
while (currentPath.length > 1) {
if (fs.existsSync(currentPath)) {
writePaths.add(currentPath);
readPaths.add(currentPath);
break;
}
currentPath = path.dirname(currentPath);
}
} catch (_e) {
// ignore
}
}
}
const originalReadSize =
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.read?.length ||
0;
const originalWriteSize =
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.write
?.length || 0;
const originalNetwork =
!!this.params[PARAM_ADDITIONAL_PERMISSIONS]?.network;
const newReadSize = additionalPermissions.fileSystem?.read?.length || 0;
const newWriteSize =
additionalPermissions.fileSystem?.write?.length || 0;
const newNetwork = !!additionalPermissions.network;
const hasNewPermissions =
newReadSize > originalReadSize ||
newWriteSize > originalWriteSize ||
(!originalNetwork && newNetwork);
if (hasNewPermissions) {
const confirmationDetails = {
type: 'sandbox_expansion',
title: 'Sandbox Expansion Request',
command: this.params.command,
rootCommand: rootCommandDisplay,
additionalPermissions,
const additionalPermissions = {
network:
sandboxDenial.network ||
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.network ||
undefined,
fileSystem:
sandboxDenial.filePaths?.length || writePaths.size > 0
? {
read: Array.from(readPaths),
write: Array.from(writePaths),
}
: undefined,
};
return {
llmContent: 'Sandbox expansion required',
returnDisplay: returnDisplayMessage,
error: {
type: ToolErrorType.SANDBOX_EXPANSION_REQUIRED,
message: JSON.stringify(confirmationDetails),
},
};
const originalReadSize =
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.read
?.length || 0;
const originalWriteSize =
this.params[PARAM_ADDITIONAL_PERMISSIONS]?.fileSystem?.write
?.length || 0;
const originalNetwork =
!!this.params[PARAM_ADDITIONAL_PERMISSIONS]?.network;
const newReadSize =
additionalPermissions.fileSystem?.read?.length || 0;
const newWriteSize =
additionalPermissions.fileSystem?.write?.length || 0;
const newNetwork = !!additionalPermissions.network;
const hasNewPermissions =
newReadSize > originalReadSize ||
newWriteSize > originalWriteSize ||
(!originalNetwork && newNetwork);
if (hasNewPermissions) {
const confirmationDetails = {
type: 'sandbox_expansion',
title: 'Sandbox Expansion Request',
command: this.params.command,
rootCommand: rootCommandDisplay,
additionalPermissions,
};
return {
llmContent: 'Sandbox expansion required',
returnDisplay: returnDisplayMessage,
error: {
type: ToolErrorType.SANDBOX_EXPANSION_REQUIRED,
message: JSON.stringify(confirmationDetails),
},
};
}
// If no new permissions were found by heuristic, do not intercept.
// Just return the normal execution error so the LLM can try providing explicit paths itself.
}
// If no new permissions were found by heuristic, do not intercept.
// Just return the normal execution error so the LLM can try providing explicit paths itself.
}
const summarizeConfig =