{"record":{"id":"212035be3359dd89","repo":"vercel/ai","slug":"tool-approval-signature-verification-failed-for-ap","errorCode":null,"errorMessage":"Tool approval signature verification failed for approval \"${approvalId}\" (tool call \"${toolCallId}\"): ${reason}","messagePattern":"Tool approval signature verification failed for approval \"(.+?)\" \\(tool call \"(.+?)\"\\): (.+?)","errorType":"exception","errorClass":"InvalidToolApprovalSignatureError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/generate-text/validate-tool-approvals.ts","lineNumber":65,"sourceCode":"  >;\n}> {\n  const approved: Array<CollectedToolApprovals<TOOLS>> = [];\n  const denied: Array<CollectedToolApprovals<TOOLS>> = [];\n  const invalid: Array<\n    CollectedToolApprovals<TOOLS> & { error: InvalidToolInputError }\n  > = [];\n\n  for (const approval of approvedToolApprovals) {\n    const { toolCall, approvalRequest } = approval;\n    // Look up the tool by own property only: `toolName` comes from\n    // client-supplied history, so a name matching an inherited object property\n    // (e.g. `constructor`, `toString`) must resolve to \"no such tool\" rather\n    // than a prototype value that would silently skip input validation below.\n    const tool = getOwn(tools, toolCall.toolName);\n\n    if (toolApprovalSecret != null) {\n      if (approvalRequest.signature == null) {\n        throw new InvalidToolApprovalSignatureError({\n          approvalId: approvalRequest.approvalId,\n          toolCallId: toolCall.toolCallId,\n          reason: 'missing signature',\n        });\n      }\n\n      const valid = await verifyToolApprovalSignature({\n        secret: toolApprovalSecret,\n        signature: approvalRequest.signature,\n        approvalId: approvalRequest.approvalId,\n        toolCallId: toolCall.toolCallId,\n        toolName: toolCall.toolName,\n        input: toolCall.input,\n      });\n\n      if (!valid) {\n        throw new InvalidToolApprovalSignatureError({\n          approvalId: approvalRequest.approvalId,","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/ai/src/generate-text/validate-tool-approvals.ts#L47-L83","documentation":"When a toolApprovalSecret is configured, every approved tool approval must carry a valid cryptographic signature. validateApprovedToolApprovals throws InvalidToolApprovalSignatureError when the approval request has no signature at all (reason 'missing signature', and other reasons for malformed payloads). This protects against tampered approvals being replayed into the tool loop.","triggerScenarios":"Passing tool approval responses (from `sendToolApprovals`/multi-step continue) whose approval request lacks a signature while `toolApprovalSecret` is set — e.g. approvals produced by a previous run without the secret, hand-constructed approvals, or approvals serialized before signing existed.","commonSituations":"Mixing conversations generated before enabling toolApprovalSecret with a run that enables it; manually crafting approval objects in tests/scripts; a proxy/client stripping the signature field; different secrets/versions across services sharing conversation state.","solutions":["Only send approval objects that came from the SDK's signed approval request (same run with toolApprovalSecret enabled).","Enable toolApprovalSecret consistently across all runs that share the conversation, including the run that produced the approval requests.","Re-issue the approval by resuming from the original provider response rather than reconstructing approvals manually.","Catch AI_InvalidToolApprovalSignatureError and surface a security warning; do not retry with the same payload."],"exampleFix":"// before\nawait result.sendToolApprovals([{ approvalId, approved: true }]); // hand-built, unsigned\n\n// after\nconst approvals = toolApprovalsFromResponse(originalResult, { approved: true }); // signed by SDK\nawait result.sendToolApprovals(approvals);","handlingStrategy":"try-catch","validationCode":"if (toolApprovalSecret != null && approvals.some(a => a.signature == null)) {\n  throw new Error('Refusing to send unsigned approvals while toolApprovalSecret is enabled');\n}","typeGuard":"function isSignedApproval(a: unknown): a is { approvalId: string; signature: string } {\n  return (\n    typeof a === 'object' && a !== null &&\n    typeof (a as any).approvalId === 'string' &&\n    typeof (a as any).signature === 'string'\n  );\n}","tryCatchPattern":"try {\n  await result.sendToolApprovals(approvals);\n} catch (error) {\n  if (InvalidToolApprovalSignatureError.isInstance(error)) {\n    // treat as security event: do not retry, re-issue approvals from the signed source\n    logger.security('unsigned-or-tampered tool approval', error);\n  } else throw error;\n}","preventionTips":["Enable toolApprovalSecret consistently across every service/run handling the conversation.","Never hand-construct approval objects; always resume from the SDK's signed response.","Version your approvals when changing the signing scheme and migrate old conversations."],"tags":["security","signature","tool-approval","validation"],"backgroundTag":"signature-verification-failed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}