Skip to content

Surface OpenAPI argument errors to the caller instead of redacting them#1243

Merged
RhysSullivan merged 2 commits into
mainfrom
fix/openapi-preflight-tool-errors
Jul 1, 2026
Merged

Surface OpenAPI argument errors to the caller instead of redacting them#1243
RhysSullivan merged 2 commits into
mainfrom
fix/openapi-preflight-tool-errors

Conversation

@RhysSullivan

Copy link
Copy Markdown
Collaborator

Problem

When an OpenAPI-backed tool is called with a missing required path parameter or request body, the invocation fails before any HTTP request is sent, with a message generated locally from the spec (e.g. Missing required path parameter: datasourceId). The dispatcher only classified ToolNotFoundError, ToolBlockedError, and validation-issue ToolInvocationError as expected failures, so these caller-argument errors fell into the defect-redaction branch and the calling model saw an opaque Internal tool error [id] with no hint that the fix is renaming or adding an argument.

Worse, approval enforcement ran before any argument validation, so a call guaranteed to fail paused for user approval first: the user approved, then the call failed on the exact check the approval could never satisfy.

Changes

Surface request-build argument errors as expected tool failures (packages/core/execution/src/tool-invoker.ts)

expectedToolFailure now classifies OpenApiInvocationError raised during request building as { code: "invalid_tool_arguments", message: <real message> }, flowing through the same ToolResult.fail channel as tool_not_found and upstream HTTP errors. The discriminator is statusCode: None and no cause: that set is exactly the locally-generated, spec-derived messages (path params, request body, base64 payloads). Transport failures share statusCode: None but wrap the raw client error as cause (which can hold internal URLs), and post-response failures carry a status code; both keep flowing through the opaque-defect redaction. Matched structurally by tag since the execution package does not depend on the openapi plugin.

Validate args before pausing for approval (packages/core/sdk, packages/plugins/openapi)

  • invoke is split so its entire pre-send phase (path resolution, body assembly, base64 validation) is a separate pure buildRequest; the real invocation now calls it, so pre-approval validation and invocation share one code path by construction.
  • New optional validateToolArgs plugin hook. Core calls it only when the call is about to pause for approval, using an approvalRequired predicate extracted from enforceApproval so the two cannot drift. Non-pausing calls skip it: invokeTool raises the identical failure moments later without the extra pass.
  • The openapi plugin implements the hook by running buildRequest against the persisted binding and discarding the built request. Legacy rows without a persisted binding skip validation rather than re-parsing the spec.

Tests

  • tool-invoker.leak.test.ts: missing-path-param and missing-body messages pass through verbatim as invalid_tool_arguments; transport (statusCode: None + cause) and post-response (statusCode: Some) failures stay opaque with a correlation id; existing redaction cases unchanged.
  • plugin.test.ts: a body-less POST that requires approval fails with Missing required request body and the elicitation handler is never invoked.

Full gates pass: format:check, lint, typecheck, test.

Pre-flight OpenApiInvocationError raises (missing/unresolved path params,
missing or malformed request body) happen before any HTTP request and their
messages are generated locally from the spec, but the dispatcher only
classified ToolNotFoundError, ToolBlockedError, and validation-issue
ToolInvocationError as expected failures. These caller-argument errors fell
into the defect-redaction branch, so the model saw an opaque
"Internal tool error [id]" with no hint that the fix is renaming or adding
an argument.

Classify them as invalid_tool_arguments with the real message, keyed on the
pre-flight shape: statusCode None and no cause. Transport failures (None +
cause carrying the raw client error) and post-response failures (Some status)
keep flowing through redaction.
Approval elicitation ran before any argument validation, so a call with a
missing required path parameter or request body paused for user approval and
then failed on the exact pre-flight check the approval could never satisfy
(observed with queryCreate: approved, then "Missing required request body").

Add an optional validateToolArgs plugin hook that core calls only when the
call is about to pause for approval (same predicate enforceApproval applies,
now shared as approvalRequired). The openapi plugin implements it by running
the request-building pass extracted from invoke (buildRequest: path params,
request body, base64 payloads) and discarding the built request, so the
validation semantics are exactly the ones invocation applies rather than a
parallel re-implementation. Legacy rows without a persisted binding skip
validation instead of re-parsing the spec.
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes two related problems with OpenAPI-backed tool invocations: locally-generated argument errors (missing path params, missing request body) were being swallowed into opaque Internal tool error [id] messages, and the approval elicitation was firing before any argument validation, causing the user to approve calls that could only fail.

  • tool-invoker.ts: Adds openApiPreflightMessage(), which structurally matches OpenApiInvocationError by checking _tag, statusCode: None, and absence of cause — the exact set of locally-generated spec errors. Transport failures (statusCode: None + cause) and post-response failures (statusCode: Some) remain on the opaque-defect path.
  • invoke.ts: Splits the monolithic invoke() into a pure buildRequest() and an invoke() that annotates telemetry, calls buildRequest, and executes the HTTP request, guaranteeing a single code path for validation.
  • executor.ts / plugin.ts: Extracts approvalRequired() predicate, adds optional validateToolArgs plugin hook, and calls it before enforceApproval so invalid-arg calls are rejected without consuming an approval.

Confidence Score: 5/5

Safe to merge — changes are well-scoped, the structural discriminator is carefully specified with three independent conditions, the refactoring of enforceApproval is semantically equivalent, and both unit and integration tests cover the critical paths including the redaction boundary.

The structural discriminator correctly separates locally-generated errors from transport and post-response failures — all three test cases in the leak audit confirm the redaction boundary holds. The approvalRequired extraction is an exact equivalence refactor. The validateToolArgs hook is optional and only fires for approval-gated calls, so plugins that don't implement it and non-approval paths are completely unaffected.

No files require special attention. The most complex pieces are the discriminator in tool-invoker.ts and the buildRequest split in invoke.ts, both covered by targeted tests.

Important Files Changed

Filename Overview
packages/core/execution/src/tool-invoker.ts Adds openApiPreflightMessage() structural discriminator and wires it into expectedToolFailure(), correctly distinguishing pre-flight argument errors (statusCode:None, no cause) from transport errors (statusCode:None + cause) and post-response errors (statusCode:Some).
packages/core/sdk/src/executor.ts Extracts approvalRequired() predicate from enforceApproval and uses it to gate pre-approval validateToolArgs call; refactoring is semantically equivalent to the original two-return-statement guard.
packages/plugins/openapi/src/sdk/invoke.ts Splits invoke() into buildRequest() + invoke(), where buildRequest handles all pre-flight arg validation and HttpClientRequest construction; invoke() adds telemetry spans, calls buildRequest, then executes the HTTP request. One code path enforced by construction.
packages/plugins/openapi/src/sdk/backing.ts Adds validateOpenApiBackedToolArgs which looks up the persisted operation binding and runs buildRequest discarding the result; silently skips legacy rows without a persisted binding.
packages/core/execution/src/tool-invoker.leak.test.ts Adds four fake-OpenApiInvocationError tool handlers and four test cases covering: path-param preflight (surfaces verbatim), body preflight (surfaces verbatim), transport failure (stays opaque), post-response failure (stays opaque).
packages/plugins/openapi/src/sdk/plugin.test.ts Adds integration test: body-less POST that requires approval fails pre-flight with 'Missing required request body' and the elicitation handler is never invoked.

Reviews (1): Last reviewed commit: "Validate OpenAPI args before pausing for..." | Re-trigger Greptile

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1243

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1243

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1243

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1243

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1243

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1243

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1243

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1243

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1243

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1243

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1243

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1243

executor

npm i https://pkg.pr.new/executor@1243

commit: 6e78677

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 6e78677 Jul 01 2026, 11:29 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 1, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 6e78677 Commit Preview URL

Branch Preview URL
Jul 01 2026, 11:28 PM

@RhysSullivan
RhysSullivan merged commit e39dbcf into main Jul 1, 2026
17 checks passed
@RhysSullivan
RhysSullivan deleted the fix/openapi-preflight-tool-errors branch July 1, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant