160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
import { feature } from 'bun:bundle'
|
|
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
|
import type { PendingClassifierCheck } from '../../../types/permissions.js'
|
|
import { isAgentSwarmsEnabled } from '../../../utils/agentSwarmsEnabled.js'
|
|
import { toError } from '../../../utils/errors.js'
|
|
import { logError } from '../../../utils/log.js'
|
|
import type { PermissionDecision } from '../../../utils/permissions/PermissionResult.js'
|
|
import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js'
|
|
import {
|
|
createPermissionRequest,
|
|
isSwarmWorker,
|
|
sendPermissionRequestViaMailbox,
|
|
} from '../../../utils/swarm/permissionSync.js'
|
|
import { registerPermissionCallback } from '../../useSwarmPermissionPoller.js'
|
|
import type { PermissionContext } from '../PermissionContext.js'
|
|
import { createResolveOnce } from '../PermissionContext.js'
|
|
|
|
type SwarmWorkerPermissionParams = {
|
|
ctx: PermissionContext
|
|
description: string
|
|
pendingClassifierCheck?: PendingClassifierCheck | undefined
|
|
updatedInput: Record<string, unknown> | undefined
|
|
suggestions: PermissionUpdate[] | undefined
|
|
}
|
|
|
|
/**
|
|
* Handles the swarm worker permission flow.
|
|
*
|
|
* When running as a swarm worker:
|
|
* 1. Tries classifier auto-approval for bash commands
|
|
* 2. Forwards the permission request to the leader via mailbox
|
|
* 3. Registers callbacks for when the leader responds
|
|
* 4. Sets the pending indicator while waiting
|
|
*
|
|
* Returns a PermissionDecision if the classifier auto-approves,
|
|
* or a Promise that resolves when the leader responds.
|
|
* Returns null if swarms are not enabled or this is not a swarm worker,
|
|
* so the caller can fall through to interactive handling.
|
|
*/
|
|
async function handleSwarmWorkerPermission(
|
|
params: SwarmWorkerPermissionParams,
|
|
): Promise<PermissionDecision | null> {
|
|
if (!isAgentSwarmsEnabled() || !isSwarmWorker()) {
|
|
return null
|
|
}
|
|
|
|
const { ctx, description, updatedInput, suggestions } = params
|
|
|
|
// For bash commands, try classifier auto-approval before forwarding to
|
|
// the leader. Agents await the classifier result (rather than racing it
|
|
// against user interaction like the main agent).
|
|
const classifierResult = feature('BASH_CLASSIFIER')
|
|
? await ctx.tryClassifier?.(params.pendingClassifierCheck, updatedInput)
|
|
: null
|
|
if (classifierResult) {
|
|
return classifierResult
|
|
}
|
|
|
|
// Forward permission request to the leader via mailbox
|
|
try {
|
|
const clearPendingRequest = (): void =>
|
|
ctx.toolUseContext.setAppState(prev => ({
|
|
...prev,
|
|
pendingWorkerRequest: null,
|
|
}))
|
|
|
|
const decision = await new Promise<PermissionDecision>(resolve => {
|
|
const { resolve: resolveOnce, claim } = createResolveOnce(resolve)
|
|
|
|
// Create the permission request
|
|
const request = createPermissionRequest({
|
|
toolName: ctx.tool.name,
|
|
toolUseId: ctx.toolUseID,
|
|
input: ctx.input,
|
|
description,
|
|
permissionSuggestions: suggestions,
|
|
})
|
|
|
|
// Register callback BEFORE sending the request to avoid race condition
|
|
// where leader responds before callback is registered
|
|
registerPermissionCallback({
|
|
requestId: request.id,
|
|
toolUseId: ctx.toolUseID,
|
|
async onAllow(
|
|
allowedInput: Record<string, unknown> | undefined,
|
|
permissionUpdates: PermissionUpdate[],
|
|
feedback?: string,
|
|
contentBlocks?: ContentBlockParam[],
|
|
) {
|
|
if (!claim()) return // atomic check-and-mark before await
|
|
clearPendingRequest()
|
|
|
|
// Merge the updated input with the original input
|
|
const finalInput =
|
|
allowedInput && Object.keys(allowedInput).length > 0
|
|
? allowedInput
|
|
: ctx.input
|
|
|
|
resolveOnce(
|
|
await ctx.handleUserAllow(
|
|
finalInput,
|
|
permissionUpdates,
|
|
feedback,
|
|
undefined,
|
|
contentBlocks,
|
|
),
|
|
)
|
|
},
|
|
onReject(feedback?: string, contentBlocks?: ContentBlockParam[]) {
|
|
if (!claim()) return
|
|
clearPendingRequest()
|
|
|
|
ctx.logDecision({
|
|
decision: 'reject',
|
|
source: { type: 'user_reject', hasFeedback: !!feedback },
|
|
})
|
|
|
|
resolveOnce(ctx.cancelAndAbort(feedback, undefined, contentBlocks))
|
|
},
|
|
})
|
|
|
|
// Now that callback is registered, send the request to the leader
|
|
void sendPermissionRequestViaMailbox(request)
|
|
|
|
// Show visual indicator that we're waiting for leader approval
|
|
ctx.toolUseContext.setAppState(prev => ({
|
|
...prev,
|
|
pendingWorkerRequest: {
|
|
toolName: ctx.tool.name,
|
|
toolUseId: ctx.toolUseID,
|
|
description,
|
|
},
|
|
}))
|
|
|
|
// If the abort signal fires while waiting for the leader response,
|
|
// resolve the promise with a cancel decision so it does not hang.
|
|
ctx.toolUseContext.abortController.signal.addEventListener(
|
|
'abort',
|
|
() => {
|
|
if (!claim()) return
|
|
clearPendingRequest()
|
|
ctx.logCancelled()
|
|
resolveOnce(ctx.cancelAndAbort(undefined, true))
|
|
},
|
|
{ once: true },
|
|
)
|
|
})
|
|
|
|
return decision
|
|
} catch (error) {
|
|
// If swarm permission submission fails, fall back to local handling
|
|
logError(toError(error))
|
|
// Continue to local UI handling below
|
|
return null
|
|
}
|
|
}
|
|
|
|
export { handleSwarmWorkerPermission }
|
|
export type { SwarmWorkerPermissionParams }
|