Skip to content

validateApprovedToolApprovals rejects identical JSON when the approved input comes from another realm #21805

Description

@Christophevvv

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

  • I agree to follow this project's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions