feat(sandbox): dynamic macOS sandbox expansion and worktree support (#23301)

This commit is contained in:
Gal Zahavi
2026-03-23 21:48:13 -07:00
committed by GitHub
parent 7fdf708e1a
commit fe7baee18b
40 changed files with 2201 additions and 183 deletions
+2 -1
View File
@@ -77,7 +77,8 @@ export async function checkPolicy(
// confirmation prompt if the policy engine's decision is 'ASK_USER'.
if (
decision === PolicyDecision.ASK_USER &&
toolCall.request.isClientInitiated
toolCall.request.isClientInitiated &&
!toolCall.request.args?.['additional_permissions']
) {
return {
decision: PolicyDecision.ALLOW,
+104
View File
@@ -792,6 +792,110 @@ export class Scheduler {
return true;
}
let isSandboxError = false;
let sandboxDetailsStr = '';
if (
result.status === CoreToolCallStatus.Error &&
result.response.errorType === 'sandbox_expansion_required'
) {
isSandboxError = true;
sandboxDetailsStr = result.response.error?.message || '';
}
if (isSandboxError) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const parsedError = JSON.parse(sandboxDetailsStr) as {
rootCommand: string;
additionalPermissions: import('../services/sandboxManager.js').SandboxPermissions;
};
const confirmationDetails: SerializableConfirmationDetails = {
type: 'sandbox_expansion',
title: 'Sandbox Expansion Request',
command: String(
activeCall.request.args['command'] ?? parsedError.rootCommand,
),
rootCommand: parsedError.rootCommand,
additionalPermissions: parsedError.additionalPermissions,
};
const correlationId = crypto.randomUUID();
// Mutate the active call so resolveConfirmation generates the correct Sandbox Expansion details
activeCall.request.args['additional_permissions'] =
parsedError.additionalPermissions;
activeCall.invocation = activeCall.tool.build(activeCall.request.args);
// CRITICAL: We must push the new args and invocation into the state manager
// before calling resolveConfirmation, because resolveConfirmation fetches
// the tool call directly from the state manager!
this.state.updateArgs(
callId,
activeCall.request.args,
activeCall.invocation,
);
this.state.updateStatus(callId, CoreToolCallStatus.AwaitingApproval, {
confirmationDetails,
correlationId,
});
const validatingCall = {
...activeCall,
status: CoreToolCallStatus.Validating,
} as ValidatingToolCall;
const confResult = await resolveConfirmation(validatingCall, signal, {
config: this.config,
messageBus: this.messageBus,
state: this.state,
modifier: this.modifier,
getPreferredEditor: this.getPreferredEditor,
schedulerId: this.schedulerId,
onWaitingForConfirmation: this.onWaitingForConfirmation,
});
if (confResult.outcome === ToolConfirmationOutcome.Cancel) {
type LegacyHack = ToolCallResponseInfo & {
llmContent?: string;
returnDisplay?: string;
};
const errorResponse = { ...result.response } as LegacyHack;
errorResponse.llmContent =
'User cancelled sandbox expansion. The command failed with a sandbox denial. Shell output:\n' +
String(errorResponse.returnDisplay);
this.state.updateStatus(
callId,
CoreToolCallStatus.Error,
errorResponse,
);
return false;
}
activeCall.request.args['additional_permissions'] =
parsedError.additionalPermissions;
// Reset the output stream visual so it replaces the error text
this.state.updateStatus(callId, CoreToolCallStatus.Executing, {
liveOutput: undefined,
});
// Call _execute synchronously and properly return its promise to loop internally!
return await this._execute(
{
...activeCall,
status: CoreToolCallStatus.Scheduled,
} as ScheduledToolCall,
signal,
);
} catch (_e) {
// Fallback to normal error handling if parsing/looping fails
}
}
if (result.status === CoreToolCallStatus.Success) {
this.state.updateStatus(
callId,