Skip to content

Validate client tool input against the tool's schema - #330684

Open
Ryan Ewen (RyanEwen) wants to merge 3 commits into
microsoft:mainfrom
RyanEwen:fix/browser-page-link-missing-id
Open

Validate client tool input against the tool's schema#330684
Ryan Ewen (RyanEwen) wants to merge 3 commits into
microsoft:mainfrom
RyanEwen:fix/browser-page-link-missing-id

Conversation

@RyanEwen

@RyanEwen Ryan Ewen (RyanEwen) commented Aug 13, 2026

Copy link
Copy Markdown

Problem

A client tool call whose arguments never streamed reaches the workbench with no toolInput. resolveToolInput turns that into the string '{}' (agentHostSessionHandler.ts:446-449), which parses into an empty object and is then handed to the tool as ordinary parameters. The tool runs with nothing.

For the browser tools that means pageId is undefined and preparation throws, which is what this PR originally reported. All nine of them declare required: ['pageId'], so the input was already known to be invalid before the tool was ever called.

Fix

Validate the parsed parameters against the tool's own inputSchema at _executeClientTool, documented as "the one place a client tool is actually invoked", and fail the call when a declared required property is absent.

The failure goes through the existing fail(...) path, so the model receives an attributable invalidInput error naming the missing property rather than a crash or a silently degraded run. Tools that declare no schema, or no required properties, are unaffected.

This supersedes the first revision of this PR, which made createBrowserPageLink return a default label for a missing page id. That guarded one call site and still let the invalid invocation proceed; the helper's signature is restored to requiring a page id.

Suggested by Kyle Cutler (@kycutler) in review. Root cause of the empty-argument path is tracked in #330899, along with two sibling defects from the same seam (#330683, #330730) that this does not address, since those are ordering rather than payload problems.

Verification

New test in agentHostClientTools.test.ts: a tool declaring required: ['pageId'], driven with toolInput: '{}', is not invoked and completes with success: false and code invalidInput naming the property. Verified to fail without the change (the tool is invoked with empty parameters).

1128 agent-session tests and 143 browserView tests pass.

`createBrowserPageLink` takes `string | URI`, and when it receives
`undefined` the `typeof pageId === 'string'` check is false, so the value
falls through to `pageId.toString()` and throws "Cannot read properties of
undefined (reading 'toString')".

All ten browser tools call it from `prepareToolInvocation` without a guard,
so any of them invoked with empty parameters throws before the tool's own
validation runs. The tools already handle the case correctly in `invoke`
(for example `clickBrowserTool` returns an error result when `pageId` is
absent), so the guard exists one layer too late: throwing out of
`prepareToolInvocation` leaves the tool call awaiting a completion that
never arrives.

Return the plain unlinked label when there is no page id, so the call
reaches `invoke` and is reported as a normal tool error.
Copilot AI balanced review requested due to automatic review settings August 13, 2026 16:35
@vs-code-engineering

Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

Kyle Cutler (@kycutler)

Matched files:

  • src/vs/workbench/contrib/browserView/electron-browser/tools/browserToolHelpers.ts
  • src/vs/workbench/contrib/browserView/test/electron-browser/tools/browserToolHelpers.test.ts

Joaquín Ruales (@jruales)

Matched files:

  • src/vs/workbench/contrib/browserView/electron-browser/tools/browserToolHelpers.ts
  • src/vs/workbench/contrib/browserView/test/electron-browser/tools/browserToolHelpers.test.ts

@RyanEwen

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Prevents browser tool preparation from throwing when pageId is missing.

Changes:

  • Returns an unlinked browser label for missing page IDs.
  • Adds regression coverage for empty tool parameters.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
browserToolHelpers.ts Adds the missing-page fallback.
browserToolHelpers.test.ts Tests helper and tool behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Trim the helper's JSDoc to the contract itself; the disconnect sequence that
produces empty parameters belongs in the PR description rather than coupling
a generic helper to agent-host details.

Assert the exact `toolResultError` in the invoke test. The previous
assertion only checked that a result object was returned, which a success
would also satisfy.
@RyanEwen

Copy link
Copy Markdown
Author

Related: these three share a root cause. Noting it so they can be triaged together rather than as three unrelated patches.

The workbench executes a client tool off the stream-mapper chat/toolCallReady (emitted at content_block_stop with confirmed: "not-needed"), independently of whether the SDK has invoked that tool through its in-process MCP server. Three defects fall out of that one seam:

If client tools executed only on the SDK-driven invocation, with the stream-mapper ready used for rendering only, all three would be structurally impossible: one authoritative trigger, never without real input, and registration ordered before execution.

I have not proposed that as a PR because I do not know whether the stream-mapper ready is deliberately actionable. Starting the tool as soon as its arguments finish streaming, instead of waiting for the SDK round trip, is a plausible latency win, and the permission flow may already depend on the current ordering. That is a call for whoever owns the design.

Each of the three fixes a reproducible bug on its own and carries a test that fails without it, so they are safe to land in the meantime. If the structural fix is preferred, #330683 and #330684 can reasonably be closed in its favour; #330730 is worth keeping either way, since buffering a result that arrives before its handler is the same defensive behaviour CopilotAgentSession already has on this path.

@kycutler

Copy link
Copy Markdown
Collaborator

Hi Ryan Ewen (@RyanEwen), can you open an issue with the above findings? Then we can triage it through the normal process. At a glance it's likely that a better fix would be somewhere within the agent host or protocol -- e.g. for this issue, performing validation of the arguments against the tool schema should catch an undefined page ID.

@RyanEwen

Copy link
Copy Markdown
Author

Opened #330899 with the findings.

Included your point about validating arguments against the tool schema: that looks like the right boundary for the empty-toolInput class specifically, though it would not cover the double invocation or the dropped result, which are ordering rather than payload problems.

The open design question there is whether the stream-mapper ready is meant to be actionable at all, or whether it should drive rendering only and leave execution to the SDK invocation.

A client tool call whose arguments never streamed reaches the workbench
with no toolInput, which resolveToolInput turns into '{}'. That empty
object then looks like valid parameters, so the tool runs with nothing:
the browser tools throw during preparation because pageId is undefined,
and every one of them declares it required.

Check the parsed parameters against the tool's own inputSchema at the
single point a client tool is invoked, and fail the call with an
attributable error when a required property is absent. The model sees
why the call failed instead of the tool crashing or acting on empty
input, and this covers every client tool that declares a schema rather
than one helper.

Replaces the defensive guard in createBrowserPageLink, whose signature
goes back to requiring a page id.
@RyanEwen Ryan Ewen (RyanEwen) changed the title Do not throw from browser tool preparation when the page id is missing Validate client tool input against the tool's schema Aug 14, 2026
@RyanEwen

Copy link
Copy Markdown
Author

Reworked along the lines suggested: validation now happens against the tool's inputSchema rather than guarding one helper.

Two things the code confirmed while doing it:

  • All nine browser tools already declare required: ['pageId'], and agentHostToolUtils already forwards inputSchema when registering client tools, so nothing new had to be plumbed.
  • The empty object is manufactured on the client: resolveToolInput returns '{}' when toolInput is undefined, which then parses into valid-looking parameters. Validation is what stops that becoming a tool invocation. Keeping the default is still right for tools that legitimately take no arguments, since those declare no required properties and pass.

Validation is at _executeClientTool, which the code comments call the one place a client tool is actually invoked, and it reuses the existing fail(...) path so the model gets an invalidInput error naming the missing property.

As noted on #330899, this covers the payload class only. The double invocation (#330683) and the dropped result (#330730) are ordering problems and are unaffected.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants