From 8974bcd502bd337c6284b6f22901c248819d66c4 Mon Sep 17 00:00:00 2001 From: "roboeve[bot]" <311457182+roboeve[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:58:21 +0000 Subject: [PATCH 1/2] fix(eve): fill connection call defaults once, when the call runs A connection call was checked before approval and again at execution, and the second check validated the first one's default-filled output. A default its own schema rejects, such as Pydantic's null for x: str = None, failed that second check. The pre-approval check now returns the model's input without provided arguments, and only the execution check fills defaults. Fixes #5509 Co-authored-by: ruiconti <1834568+ruiconti@users.noreply.github.com> --- .changeset/connection-null-default.md | 5 +++ .../catalog/connection-entry.test.ts | 5 +-- .../src/execution/catalog/connection-entry.ts | 40 +++++++++++-------- .../execution/catalog/step-catalog.test.ts | 4 +- 4 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 .changeset/connection-null-default.md diff --git a/.changeset/connection-null-default.md b/.changeset/connection-null-default.md new file mode 100644 index 0000000000..56087e3aac --- /dev/null +++ b/.changeset/connection-null-default.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +A connection tool call no longer fails with `Instance type "null" is invalid` when the model omits an argument whose schema declares a `null` default it does not allow, such as Pydantic's `{ "type": "string", "default": null }` for `x: str = None`. eve fills schema defaults once, when the call runs, instead of filling them before approval and rejecting them on the second check. diff --git a/packages/eve/src/execution/catalog/connection-entry.test.ts b/packages/eve/src/execution/catalog/connection-entry.test.ts index aa1a9f9fb6..128f62365e 100644 --- a/packages/eve/src/execution/catalog/connection-entry.test.ts +++ b/packages/eve/src/execution/catalog/connection-entry.test.ts @@ -106,8 +106,7 @@ describe("connection approval context", () => { }); describe("connection tool calls", () => { - // Expected failure until #5509 is fixed: the call rejects the `null` default its own check filled in. - it.fails("runs a call whose omitted argument defaults to null", async () => { + it("runs a call whose omitted argument defaults to null", async () => { // Pydantic publishes `query: str = None` as a string property that defaults to null. const search: ConnectionToolMetadata = { description: "Search issues.", @@ -142,7 +141,7 @@ describe("connection tool calls", () => { // The harness validates the model's input, then runs the tool on the validated value. await contextStorage.run(new ContextContainer(), async () => { const checked = await (entry.inputSchema as ToolSchema)["~standard"].validate({}); - expect(checked).toEqual({ value: { query: null } }); + expect(checked).toEqual({ value: {} }); await entry.execute!((checked as { value: unknown }).value, { messages: [], toolCallId: "call-search", diff --git a/packages/eve/src/execution/catalog/connection-entry.ts b/packages/eve/src/execution/catalog/connection-entry.ts index 8e3c020113..9b82c6140e 100644 --- a/packages/eve/src/execution/catalog/connection-entry.ts +++ b/packages/eve/src/execution/catalog/connection-entry.ts @@ -153,7 +153,8 @@ async function callConnectionTool( } // Validation may have run before a sign-in made the tools listable. const isEventAction = client.eventActions?.ownsName(toolName) === true; - const checked = await checkCall(connection, listing.tools, toolName, rawInput, !isEventAction); + const modelInput = withoutProvidedArguments(connection, rawInput, !isEventAction); + const checked = await checkCall(connection, listing.tools, toolName, modelInput); if ("issues" in checked) throw new Error(issuesMessage(checked.issues)); const { input, tool } = checked; @@ -224,39 +225,44 @@ async function checkInput( if (isConnectionAuthorizationRequiredError(error)) return { value: input }; return { issues: [{ message: listingFailureMessage(connection.connectionName, error) }] }; } - const checked = await checkCall( - connection, - tools, - toolName, - input, - client.eventActions?.ownsName(toolName) !== true, - ); - return "issues" in checked ? { issues: checked.issues } : { value: checked.input }; + const remote = client.eventActions?.ownsName(toolName) !== true; + const modelInput = withoutProvidedArguments(connection, input, remote); + const checked = await checkCall(connection, tools, toolName, modelInput); + // The call validates this value again when it runs. Defaults are filled only there, + // because a filled default its own schema rejects, such as Pydantic's `null` for + // `x: str = None`, would fail that second check. + return "issues" in checked ? { issues: checked.issues } : { value: modelInput }; } type CallCheck = | { readonly input: JsonObject; readonly tool: ConnectionToolMetadata } | { readonly issues: readonly StandardSchemaV1.Issue[] }; +/** Provided values replace these keys at execution, so a model value for one is dropped, not rejected. */ +function withoutProvidedArguments( + connection: ResolvedConnectionDefinition, + input: unknown, + remote: boolean, +): Record { + const provided = remote ? (connection.toolCall?.providedArguments ?? {}) : {}; + return Object.fromEntries( + Object.entries(isObject(input) ? input : {}).filter(([key]) => !Object.hasOwn(provided, key)), + ); +} + /** The tool a call runs and its input with schema defaults filled in, or why it cannot run. */ async function checkCall( connection: ResolvedConnectionDefinition, tools: readonly ConnectionToolMetadata[], toolName: string, - input: unknown, - remote = true, + input: Record, ): Promise { const tool = tools.find((entry) => entry.name === toolName); if (tool === undefined) { return { issues: [{ message: unknownToolMessage(connection, toolName, tools) }] }; } - // Provided values replace these keys at execution, so a model value for one is dropped, not rejected. - const provided = remote ? (connection.toolCall?.providedArguments ?? {}) : {}; - const modelInput = Object.fromEntries( - Object.entries(isObject(input) ? input : {}).filter(([key]) => !Object.hasOwn(provided, key)), - ); const result = await defineJsonSchema(tool.inputSchema as JsonObject)["~standard"].validate( - modelInput, + input, ); if (result.issues === undefined) return { input: result.value as JsonObject, tool }; return { diff --git a/packages/eve/src/execution/catalog/step-catalog.test.ts b/packages/eve/src/execution/catalog/step-catalog.test.ts index 47da1e26bb..6080e970f5 100644 --- a/packages/eve/src/execution/catalog/step-catalog.test.ts +++ b/packages/eve/src/execution/catalog/step-catalog.test.ts @@ -510,9 +510,9 @@ describe("buildStepCatalog", () => { return { linear, ...catalogContext({ connections: [linear], tools: [inlineTool("add")] }) }; } - it("resolves a name under a connection's prefix to that connection's tool, filling schema defaults", async () => { + it("resolves a name under a connection's prefix to that connection's tool, which fills schema defaults when it runs", async () => { const { catalog, linear, run } = linearCatalog(); - const validated = { input: { limit: 20, team: "core" }, name: "linear__list_issues" }; + const validated = { input: { team: "core" }, name: "linear__list_issues" }; expect( await validateTool(catalog, { input: { team: "core" }, name: "linear__list_issues" }), From 00444ef3968cbff4fd42b9007891d0b546c3c186 Mon Sep 17 00:00:00 2001 From: "roboeve[bot]" <311457182+roboeve[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:07:42 +0000 Subject: [PATCH 2/2] test(e2e): check the kennel feedings default through the server echo The call input now holds the model's arguments, so the eval reads the schema default from the day the fixture server echoes. Co-authored-by: ruiconti <1834568+ruiconti@users.noreply.github.com> --- .../agent-openapi-swagger/evals/kennel-mcp-results.eval.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/fixtures/agent-openapi-swagger/evals/kennel-mcp-results.eval.ts b/e2e/fixtures/agent-openapi-swagger/evals/kennel-mcp-results.eval.ts index 15cba69499..9060b3788d 100644 --- a/e2e/fixtures/agent-openapi-swagger/evals/kennel-mcp-results.eval.ts +++ b/e2e/fixtures/agent-openapi-swagger/evals/kennel-mcp-results.eval.ts @@ -19,11 +19,11 @@ export default defineEval({ count: 1, output: { petId: 4217, name: "Biscuit", kennel: "B3" }, }); - // A text result without an output schema is parsed as JSON, and the - // omitted `day` reaches the server as its schema default. + // A text result without an output schema is parsed as JSON. The call keeps + // the model's input, and the server echoes the schema default it received + // for the omitted `day`. turn.calledTool("kennel__list_feedings", { count: 1, - input: { day: "today" }, output: (value) => Array.isArray(value) && value.length === 2 &&