* 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 apps/cloud/src (1 of 35 files). Show all
Unified · Split
| @@ -207,9 +207,15 @@ describe("sources api (HTTP)", () => { · 206 unmodified lines | ||
| 207 | 207 | expect(execution.structured).toMatchObject({ |
| 208 | 208 | status: "completed", |
| 209 | 209 | result: { |
| 210 | − message: "hello", | |
| 211 | − suffix: "world", | |
| 212 | − path: "/echo/hello", | |
| 210 | + ok: true, | |
| 211 | + data: { | |
| 212 | + status: 200, | |
| 213 | + data: { | |
| 214 | + message: "hello", | |
| 215 | + suffix: "world", | |
| 216 | + path: "/echo/hello", | |
| 217 | + }, | |
| 218 | + }, | |
| 213 | 219 | }, |
| 214 | 220 | logs: [], |
| 215 | 221 | }); |
| @@ -315,7 +321,7 @@ describe("sources api (HTTP)", () => { · 99 unmodified lines | ||
| 315 | 321 | expect(execution.isError).toBe(false); |
| 316 | 322 | expect(execution.structured).toMatchObject({ |
| 317 | 323 | status: "completed", |
| 318 | − result: { hello: "Hello Ada" }, | |
| 324 | + result: { ok: true, data: { hello: "Hello Ada" } }, | |
| 319 | 325 | }); |
| 320 | 326 | const requests = yield* server.requests; |
| 321 | 327 | expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true); |
| @@ -391,7 +397,8 @@ describe("sources api (HTTP)", () => { · 69 unmodified lines | ||
| 391 | 397 | expect(execution.structured).toMatchObject({ |
| 392 | 398 | status: "completed", |
| 393 | 399 | result: { |
| 394 | − content: [{ type: "text", text: "cloud-mcp-ok" }], | |
| 400 | + ok: true, | |
| 401 | + data: { content: [{ type: "text", text: "cloud-mcp-ok" }] }, | |
| 395 | 402 | }, |
| 396 | 403 | }); |
| 397 | 404 | expect((yield* server.requests).length).toBeGreaterThanOrEqual(2); |