source dump of claude code
at main 159 lines 5.5 kB view raw
1import { feature } from 'bun:bundle' 2import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs' 3import type { PendingClassifierCheck } from '../../../types/permissions.js' 4import { isAgentSwarmsEnabled } from '../../../utils/agentSwarmsEnabled.js' 5import { toError } from '../../../utils/errors.js' 6import { logError } from '../../../utils/log.js' 7import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js' 8import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' 9import { 10 createPermissionRequest, 11 isSwarmWorker, 12 sendPermissionRequestViaMailbox, 13} from '../../../utils/swarm/permissionSync.js' 14import { registerPermissionCallback } from '../../useSwarmPermissionPoller.js' 15import type { PermissionContext } from '../PermissionContext.js' 16import { createResolveOnce } from '../PermissionContext.js' 17 18type SwarmWorkerPermissionParams = { 19 ctx: PermissionContext 20 description: string 21 pendingClassifierCheck?: PendingClassifierCheck | undefined 22 updatedInput: Record<string, unknown> | undefined 23 suggestions: PermissionUpdate[] | undefined 24} 25 26/** 27 * Handles the swarm worker permission flow. 28 * 29 * When running as a swarm worker: 30 * 1. Tries classifier auto-approval for bash commands 31 * 2. Forwards the permission request to the leader via mailbox 32 * 3. Registers callbacks for when the leader responds 33 * 4. Sets the pending indicator while waiting 34 * 35 * Returns a PermissionDecision if the classifier auto-approves, 36 * or a Promise that resolves when the leader responds. 37 * Returns null if swarms are not enabled or this is not a swarm worker, 38 * so the caller can fall through to interactive handling. 39 */ 40async function handleSwarmWorkerPermission( 41 params: SwarmWorkerPermissionParams, 42): Promise<PermissionDecision | null> { 43 if (!isAgentSwarmsEnabled() || !isSwarmWorker()) { 44 return null 45 } 46 47 const { ctx, description, updatedInput, suggestions } = params 48 49 // For bash commands, try classifier auto-approval before forwarding to 50 // the leader. Agents await the classifier result (rather than racing it 51 // against user interaction like the main agent). 52 const classifierResult = feature('BASH_CLASSIFIER') 53 ? await ctx.tryClassifier?.(params.pendingClassifierCheck, updatedInput) 54 : null 55 if (classifierResult) { 56 return classifierResult 57 } 58 59 // Forward permission request to the leader via mailbox 60 try { 61 const clearPendingRequest = (): void => 62 ctx.toolUseContext.setAppState(prev => ({ 63 ...prev, 64 pendingWorkerRequest: null, 65 })) 66 67 const decision = await new Promise<PermissionDecision>(resolve => { 68 const { resolve: resolveOnce, claim } = createResolveOnce(resolve) 69 70 // Create the permission request 71 const request = createPermissionRequest({ 72 toolName: ctx.tool.name, 73 toolUseId: ctx.toolUseID, 74 input: ctx.input, 75 description, 76 permissionSuggestions: suggestions, 77 }) 78 79 // Register callback BEFORE sending the request to avoid race condition 80 // where leader responds before callback is registered 81 registerPermissionCallback({ 82 requestId: request.id, 83 toolUseId: ctx.toolUseID, 84 async onAllow( 85 allowedInput: Record<string, unknown> | undefined, 86 permissionUpdates: PermissionUpdate[], 87 feedback?: string, 88 contentBlocks?: ContentBlockParam[], 89 ) { 90 if (!claim()) return // atomic check-and-mark before await 91 clearPendingRequest() 92 93 // Merge the updated input with the original input 94 const finalInput = 95 allowedInput && Object.keys(allowedInput).length > 0 96 ? allowedInput 97 : ctx.input 98 99 resolveOnce( 100 await ctx.handleUserAllow( 101 finalInput, 102 permissionUpdates, 103 feedback, 104 undefined, 105 contentBlocks, 106 ), 107 ) 108 }, 109 onReject(feedback?: string, contentBlocks?: ContentBlockParam[]) { 110 if (!claim()) return 111 clearPendingRequest() 112 113 ctx.logDecision({ 114 decision: 'reject', 115 source: { type: 'user_reject', hasFeedback: !!feedback }, 116 }) 117 118 resolveOnce(ctx.cancelAndAbort(feedback, undefined, contentBlocks)) 119 }, 120 }) 121 122 // Now that callback is registered, send the request to the leader 123 void sendPermissionRequestViaMailbox(request) 124 125 // Show visual indicator that we're waiting for leader approval 126 ctx.toolUseContext.setAppState(prev => ({ 127 ...prev, 128 pendingWorkerRequest: { 129 toolName: ctx.tool.name, 130 toolUseId: ctx.toolUseID, 131 description, 132 }, 133 })) 134 135 // If the abort signal fires while waiting for the leader response, 136 // resolve the promise with a cancel decision so it does not hang. 137 ctx.toolUseContext.abortController.signal.addEventListener( 138 'abort', 139 () => { 140 if (!claim()) return 141 clearPendingRequest() 142 ctx.logCancelled() 143 resolveOnce(ctx.cancelAndAbort(undefined, true)) 144 }, 145 { once: true }, 146 ) 147 }) 148 149 return decision 150 } catch (error) { 151 // If swarm permission submission fails, fall back to local handling 152 logError(toError(error)) 153 // Continue to local UI handling below 154 return null 155 } 156} 157 158export { handleSwarmWorkerPermission } 159export type { SwarmWorkerPermissionParams }