Surface OpenAPI argument errors to the caller instead of redacting them#1243
Conversation
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.
Cloudflare previewTorn down — the PR is closed. |
Greptile SummaryThis 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
Confidence Score: 5/5Safe 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
Reviews (1): Last reviewed commit: "Validate OpenAPI args before pausing for..." | Re-trigger Greptile |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 6e78677 | Jul 01 2026, 11:29 PM |
Deploying with
|
| 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 |
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 classifiedToolNotFoundError,ToolBlockedError, and validation-issueToolInvocationErroras expected failures, so these caller-argument errors fell into the defect-redaction branch and the calling model saw an opaqueInternal 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)expectedToolFailurenow classifiesOpenApiInvocationErrorraised during request building as{ code: "invalid_tool_arguments", message: <real message> }, flowing through the sameToolResult.failchannel astool_not_foundand upstream HTTP errors. The discriminator isstatusCode: Noneand nocause: that set is exactly the locally-generated, spec-derived messages (path params, request body, base64 payloads). Transport failures sharestatusCode: Nonebut wrap the raw client error ascause(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)invokeis split so its entire pre-send phase (path resolution, body assembly, base64 validation) is a separate purebuildRequest; the real invocation now calls it, so pre-approval validation and invocation share one code path by construction.validateToolArgsplugin hook. Core calls it only when the call is about to pause for approval, using anapprovalRequiredpredicate extracted fromenforceApprovalso the two cannot drift. Non-pausing calls skip it:invokeToolraises the identical failure moments later without the extra pass.buildRequestagainst 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 asinvalid_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 withMissing required request bodyand the elicitation handler is never invoked.Full gates pass:
format:check,lint,typecheck,test.