ai
Version:
AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.
124 lines (113 loc) • 4.03 kB
text/typescript
import {
asSchema,
isExecutableTool,
safeValidateTypes,
type Context,
type InferToolSetContext,
type ModelMessage,
type ToolSet,
} from '@ai-sdk/provider-utils';
import { InvalidToolApprovalSignatureError } from '../error/invalid-tool-approval-signature-error';
import { InvalidToolInputError } from '../error/invalid-tool-input-error';
import { getOwn } from '../util/get-own';
import type { CollectedToolApprovals } from './collect-tool-approvals';
import { resolveToolApproval } from './resolve-tool-approval';
import { verifyToolApprovalSignature } from './tool-approval-signature';
import type { ToolApprovalConfiguration } from './tool-approval-configuration';
/**
* Re-validates approved tool approvals reconstructed from client-supplied
* message history before they are executed. Checks HMAC signature (when
* configured), input schema, and approval policy.
*/
export async function validateApprovedToolApprovals<
TOOLS extends ToolSet,
RUNTIME_CONTEXT extends Context | unknown | never,
>({
approvedToolApprovals,
tools,
toolApproval,
messages,
toolsContext,
runtimeContext,
toolApprovalSecret,
}: {
approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
tools: TOOLS | undefined;
toolApproval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> | undefined;
messages: ModelMessage[];
toolsContext: InferToolSetContext<TOOLS>;
runtimeContext: RUNTIME_CONTEXT;
toolApprovalSecret?: string | Uint8Array;
}): Promise<{
approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
deniedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
}> {
const approved: Array<CollectedToolApprovals<TOOLS>> = [];
const denied: Array<CollectedToolApprovals<TOOLS>> = [];
for (const approval of approvedToolApprovals) {
const { toolCall, approvalRequest } = approval;
// Look up the tool by own property only: `toolName` comes from
// client-supplied history, so a name matching an inherited object property
// (e.g. `constructor`, `toString`) must resolve to "no such tool" rather
// than a prototype value that would silently skip input validation below.
const tool = getOwn(tools, toolCall.toolName);
if (toolApprovalSecret != null) {
if (approvalRequest.signature == null) {
throw new InvalidToolApprovalSignatureError({
approvalId: approvalRequest.approvalId,
toolCallId: toolCall.toolCallId,
reason: 'missing signature',
});
}
const valid = await verifyToolApprovalSignature({
secret: toolApprovalSecret,
signature: approvalRequest.signature,
approvalId: approvalRequest.approvalId,
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: toolCall.input,
});
if (!valid) {
throw new InvalidToolApprovalSignatureError({
approvalId: approvalRequest.approvalId,
toolCallId: toolCall.toolCallId,
reason: 'invalid signature',
});
}
}
if (isExecutableTool(tool) && tool.inputSchema != null) {
const validation = await safeValidateTypes({
value: toolCall.input,
schema: asSchema(tool.inputSchema),
});
if (!validation.success) {
throw new InvalidToolInputError({
toolName: toolCall.toolName,
toolInput: JSON.stringify(toolCall.input),
cause: validation.error,
});
}
}
const approvalStatus = await resolveToolApproval({
tools,
toolApproval,
toolCall,
messages,
toolsContext,
runtimeContext,
});
if (approvalStatus.type === 'denied') {
denied.push({
...approval,
approvalResponse: {
...approval.approvalResponse,
approved: false,
reason: approvalStatus.reason ?? approval.approvalResponse.reason,
},
});
} else {
approved.push(approval);
}
}
return { approvedToolApprovals: approved, deniedToolApprovals: denied };
}