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.
200 lines (185 loc) • 6.82 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 { isDeepEqualData } from '../util/is-deep-equal-data';
import type { CollectedToolApprovals } from './collect-tool-approvals';
import { refineParsedToolCallInput } from './parse-tool-call';
import { resolveToolApproval } from './resolve-tool-approval';
import { verifyToolApprovalSignature } from './tool-approval-signature';
import type { ToolApprovalConfiguration } from './tool-approval-configuration';
import type { ToolInputRefinement } from './tool-input-refinement';
/**
* 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,
refineToolInput,
}: {
approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
tools: TOOLS | undefined;
toolApproval: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT> | undefined;
messages: ModelMessage[];
toolsContext: InferToolSetContext<TOOLS>;
runtimeContext: RUNTIME_CONTEXT;
toolApprovalSecret?: string | Uint8Array;
refineToolInput?: ToolInputRefinement<TOOLS>;
}): Promise<{
approvedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
deniedToolApprovals: Array<CollectedToolApprovals<TOOLS>>;
invalidToolApprovals: Array<
CollectedToolApprovals<TOOLS> & { error: InvalidToolInputError }
>;
}> {
const approved: Array<CollectedToolApprovals<TOOLS>> = [];
const denied: Array<CollectedToolApprovals<TOOLS>> = [];
const invalid: Array<
CollectedToolApprovals<TOOLS> & { error: InvalidToolInputError }
> = [];
for (const collectedApproval of approvedToolApprovals) {
// 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, collectedApproval.toolCall.toolName);
// History can omit or alter these fields. Rebuild them from the current
// server-side definition before input refinement and approval policy checks,
// just as parsing a new model tool call does.
const {
toolMetadata: _toolMetadata,
dynamic: _dynamic,
...toolCallFromHistory
} = collectedApproval.toolCall;
const approval: CollectedToolApprovals<TOOLS> = {
...collectedApproval,
toolCall: {
...toolCallFromHistory,
...(tool?.metadata != null ? { toolMetadata: tool.metadata } : {}),
...(tool?.type === 'dynamic' ? { dynamic: true } : {}),
title: tool?.title,
} as typeof collectedApproval.toolCall,
};
const { approvalRequest, toolCall } = approval;
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) {
let validationError: unknown;
try {
// Preserve the approved input before validators or refiners can mutate
// it, and give them an independent copy of the schema input.
const approvedInput = structuredClone(toolCall.input);
const hasInputSchemaInput = Object.prototype.hasOwnProperty.call(
approvalRequest,
'inputSchemaInput',
);
const validation = await safeValidateTypes({
value: structuredClone(
hasInputSchemaInput
? approvalRequest.inputSchemaInput
: toolCall.input,
),
schema: asSchema(tool.inputSchema),
});
if (!validation.success) {
validationError = validation.error;
} else {
const revalidatedToolCall = await refineParsedToolCallInput({
toolCall: {
...toolCall,
input: validation.value,
},
refineToolInput,
});
// Normalize the revalidated input to the snapshot's realm.
const revalidatedInput = structuredClone(revalidatedToolCall.input);
// Revalidation must never change the operation that was approved,
// including when older or projected history omits the schema input.
if (!isDeepEqualData(revalidatedInput, approvedInput)) {
validationError = new Error(
'Approved tool input does not match the validated schema output.',
);
}
}
} catch (error) {
validationError = error;
}
if (validationError != null) {
invalid.push({
...approval,
error: new InvalidToolInputError({
toolName: toolCall.toolName,
toolInput: JSON.stringify(toolCall.input),
cause: validationError,
}),
});
continue;
}
}
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,
invalidToolApprovals: invalid,
};
}