Description
validateApprovedToolApprovals rejects a manual tool approval when the approved input and Zod's parsed output are structurally identical but were created in different JavaScript realms.
isDeepEqualData returns false when obj1.constructor !== obj2.constructor, before it compares fields. Zod's safeParse always allocates a new object. In one Node process both objects share Object, so an approval of unchanged JSON succeeds. After a value is revived in another realm (node:vm, or the QuickJS VM used by workflow / @ai-sdk/workflow), the revived input and Zod's copy do not share a constructor. The approval is marked invalid with:
Approved tool input does not match the validated schema output.
This regressed in ai@7.0.113 (a4b0940). A successful schema parse used to be sufficient. The new check is meant to reject a schema transform that changes the approved operation, such as "3" becoming 3. It also rejects JSON that did not change.
We hit this with @ai-sdk/workflow's WorkflowAgent on workflow@5.0.0-beta.57. The model messages are produced by a "use step" function and revived inside the workflow VM. WorkflowAgent.stream then calls validateApprovedToolApprovals. The tool schema has no transforms. The approved input and the parsed output have the same keys and values. The same approval succeeds under ToolLoopAgent in a single Node process.
Expected: an approval whose input already matches the schema output is executed. Actual: the approval is invalid, and the tool is not executed.
Reproduction
import vm from "node:vm";
import { tool } from "ai";
import { validateApprovedToolApprovals } from "ai/internal";
import { z } from "zod";
const input = {
name: "Joris Janssens",
birthDate: "1980-02-10",
};
const revived = vm.runInNewContext(
`({ name: "Joris Janssens", birthDate: "1980-02-10" })`,
);
const addPerson = tool({
inputSchema: z.object({
name: z.string(),
birthDate: z.string().nullish(),
}),
execute: async () => "ok",
});
function approval(value: unknown) {
return {
approvalRequest: {
type: "tool-approval-request" as const,
approvalId: "approval-1",
toolCallId: "call-1",
},
approvalResponse: {
type: "tool-approval-response" as const,
approvalId: "approval-1",
approved: true,
},
toolCall: {
type: "tool-call" as const,
toolCallId: "call-1",
toolName: "addPerson",
input: value,
},
};
}
const sameRealm = await validateApprovedToolApprovals({
approvedToolApprovals: [approval(input)],
tools: { addPerson },
messages: [],
toolsContext: {},
runtimeContext: {},
});
console.log("same realm invalid", sameRealm.invalidToolApprovals.length); // 0
const otherRealm = await validateApprovedToolApprovals({
approvedToolApprovals: [approval(revived)],
tools: { addPerson },
messages: [],
toolsContext: {},
runtimeContext: {},
});
console.log(otherRealm.invalidToolApprovals[0]?.error.message);
// Invalid input for tool addPerson: Error: Approved tool input does not match the validated schema output.
AI SDK Version
ai: 7.0.122
@ai-sdk/workflow: 2.0.53
workflow: 5.0.0-beta.57
Code of Conduct
Description
validateApprovedToolApprovals rejects a manual tool approval when the approved input and Zod's parsed output are structurally identical but were created in different JavaScript realms.
isDeepEqualData returns false when obj1.constructor !== obj2.constructor, before it compares fields. Zod's safeParse always allocates a new object. In one Node process both objects share Object, so an approval of unchanged JSON succeeds. After a value is revived in another realm (node:vm, or the QuickJS VM used by workflow / @ai-sdk/workflow), the revived input and Zod's copy do not share a constructor. The approval is marked invalid with:
Approved tool input does not match the validated schema output.
This regressed in ai@7.0.113 (a4b0940). A successful schema parse used to be sufficient. The new check is meant to reject a schema transform that changes the approved operation, such as "3" becoming 3. It also rejects JSON that did not change.
We hit this with @ai-sdk/workflow's WorkflowAgent on workflow@5.0.0-beta.57. The model messages are produced by a "use step" function and revived inside the workflow VM. WorkflowAgent.stream then calls validateApprovedToolApprovals. The tool schema has no transforms. The approved input and the parsed output have the same keys and values. The same approval succeeds under ToolLoopAgent in a single Node process.
Expected: an approval whose input already matches the schema output is executed. Actual: the approval is invalid, and the tool is not executed.
Reproduction
AI SDK Version
ai: 7.0.122
@ai-sdk/workflow: 2.0.53
workflow: 5.0.0-beta.57
Code of Conduct