* feat(tool-result): add ToolResult union and dual-shape invoker Adds `ToolResult<T>` / `ToolError` to @executor-js/sdk and a `ToolResult.ok` / `ToolResult.fail` constructor pair. Updates the sandbox tool dispatcher to accept both the new typed union (passes through unchanged) and the legacy `{ data, error }` envelope. The legacy branch now walks known upstream shapes (Microsoft Graph, DealCloud, JSON:API, plain message-bearing bodies) before falling back to a clamped JSON.stringify of the body, so structured 4xx payloads no longer collapse to 'Tool execution failed'. The cause/defect branch keeps its strict .message-only discipline. * feat(plugins): emit ToolResult from openapi, graphql, mcp, google-discovery Migrates each dynamic plugin's invokeTool to wrap its result in the typed ToolResult<T> union. OpenAPI and Google Discovery map non-2xx responses to ToolResult.fail with an extracted upstream message and the raw body in error.details; 2xx responses to ToolResult.ok with { status, headers, data }. GraphQL maps 200-with-errors bodies to ToolResult.fail({ code: 'graphql_errors', ... }) and bare data to ToolResult.ok. MCP maps { isError: true } to ToolResult.fail and content arrays to ToolResult.ok. Static plugin tools (executor.openapi.previewSpec / addSource, executor.graphql.addSource) now also wrap their plain values in ToolResult.ok for shape consistency. Updates plugin tests that asserted on the old { status, headers, data, error } envelope or the bare MCP { content } shape. Tests that consume the OpenAPI plugin's invocation result use the new unwrapInvocation helper from @executor-js/plugin-openapi/testing. * feat(tool-result): make ToolResult mandatory and route defects opaquely Removes the legacy { data, error } envelope shim from the sandbox tool dispatcher. The invoker now passes ToolResult<T> through unchanged and wraps any other plain-value plugin return in ToolResult.ok so the sandbox surface is uniform. Plugin/infra defects no longer pass their message into the sandbox. The dispatcher generates a short hex correlation id, logs the full cause with that id under executor.correlation_id, and rejects with `Internal tool error [<corrId>]`. The QuickJS bridge and the dynamic-worker module template defensively re-stamp the same opaque shape; the MCP host server's top-level execute failure path does the same. ExecutionToolError in-band messages from the execution package's built-in validators (tools.search arg checks, etc.) are still passed through at the QuickJS bridge so model-facing input errors keep their useful diagnostic. Tests: - ToolResult.ok / fail / isToolResult constructor unit tests. - repro tests assert structured upstream payloads now reach the sandbox through ToolResult.error.details (not through .message). - leak tests pin the new invariant: plugin defects only escape as the opaque generic + correlation id; no token / connection string / file path leaks into Error.message. - QuickJS end-to-end defect test confirms the same shape at the sandbox boundary. - Cloud HTTP integration tests, MCP host tests, dynamic-worker invocation tests, and plugin tests updated for the new ToolResult wire shape and the opaque-generic defect contract. * Fix tool result contracts
Showing the changes to packages/plugins/openapi/src/testing (1 of 35 files). Show all
Unified · Split
| @@ -1,4 +1,4 @@ | |||
| 1 | import { Context, Data, Effect, Layer, Predicate, Ref, Schema, Scope } from "effect"; | 1 | import { Context, Data, Effect, Layer, Option, Predicate, Ref, Schema, Scope } from "effect"; |
| 2 | import { | 2 | import { |
| 3 | HttpClient, | 3 | HttpClient, |
| 4 | HttpRouter, | 4 | HttpRouter, |
| @@ -13,7 +13,7 @@ import { · 8 unmodified lines | |||
| 13 | OpenApi, | 13 | OpenApi, |
| 14 | } from "effect/unstable/httpapi"; | 14 | } from "effect/unstable/httpapi"; |
| 15 | import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing"; | 15 | import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing"; |
| 16 | import type { ScopeId } from "@executor-js/sdk/core"; | 16 | import { isToolResult, type ScopeId } from "@executor-js/sdk/core"; |
| 17 | import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; | 17 | import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; |
| 18 | 18 | ||
| 19 | export class OpenApiTestServerAddressError extends Data.TaggedError( | 19 | export class OpenApiTestServerAddressError extends Data.TaggedError( |
| @@ -618,3 +618,55 @@ export const TestLayers = { · 598 unmodified lines | |||
| 618 | echo: OpenApiEchoTestServer.layer, | 618 | echo: OpenApiEchoTestServer.layer, |
| 619 | echoWithOAuth: OpenApiEchoTestServer.layerWithOAuth, | 619 | echoWithOAuth: OpenApiEchoTestServer.layerWithOAuth, |
| 620 | }; | 620 | }; |
| 621 | |||
| 622 | const OpenApiTransportEnvelope = Schema.Struct({ | ||
| 623 | status: Schema.Number, | ||
| 624 | headers: Schema.Record(Schema.String, Schema.String), | ||
| 625 | data: Schema.Unknown, | ||
| 626 | }); | ||
| 627 | |||
| 628 | const decodeOpenApiTransportEnvelope = Schema.decodeUnknownOption(OpenApiTransportEnvelope); | ||
| 629 | |||
| 630 | export interface OpenApiInvocationResult<TData = Record<string, unknown> | unknown[] | null> { | ||
| 631 | readonly status: number | null; | ||
| 632 | readonly headers: Record<string, string> | null; | ||
| 633 | readonly data: TData; | ||
| 634 | readonly error: unknown; | ||
| 635 | } | ||
| 636 | |||
| 637 | export const unwrapInvocation = <TData = Record<string, unknown> | null>( | ||
| 638 | raw: unknown, | ||
| 639 | ): OpenApiInvocationResult<TData> => { | ||
| 640 | if (!isToolResult(raw)) { | ||
| 641 | return { | ||
| 642 | status: null, | ||
| 643 | headers: null, | ||
| 644 | data: raw as TData, | ||
| 645 | error: null, | ||
| 646 | }; | ||
| 647 | } | ||
| 648 | if (raw.ok) { | ||
| 649 | const inner = raw.data; | ||
| 650 | const wrapped = Option.getOrUndefined(decodeOpenApiTransportEnvelope(inner)); | ||
| 651 | if (wrapped !== undefined) { | ||
| 652 | return { | ||
| 653 | status: wrapped.status, | ||
| 654 | headers: wrapped.headers, | ||
| 655 | data: wrapped.data as TData, | ||
| 656 | error: null, | ||
| 657 | }; | ||
| 658 | } | ||
| 659 | return { | ||
| 660 | status: null, | ||
| 661 | headers: null, | ||
| 662 | data: inner as TData, | ||
| 663 | error: null, | ||
| 664 | }; | ||
| 665 | } | ||
| 666 | return { | ||
| 667 | status: raw.error.status ?? null, | ||
| 668 | headers: null, | ||
| 669 | data: null as TData, | ||
| 670 | error: raw.error.details ?? raw.error, | ||
| 671 | }; | ||
| 672 | }; | ||