* 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 (2 of 35 files). Show all
Unified · Split
| @@ -207,9 +207,15 @@ describe("sources api (HTTP)", () => { · 206 unmodified lines | |||
| 207 | expect(execution.structured).toMatchObject({ | 207 | expect(execution.structured).toMatchObject({ |
| 208 | status: "completed", | 208 | status: "completed", |
| 209 | result: { | 209 | result: { |
| 210 | message: "hello", | 210 | ok: true, |
| 211 | suffix: "world", | 211 | data: { |
| 212 | path: "/echo/hello", | 212 | status: 200, |
| 213 | data: { | ||
| 214 | message: "hello", | ||
| 215 | suffix: "world", | ||
| 216 | path: "/echo/hello", | ||
| 217 | }, | ||
| 218 | }, | ||
| 213 | }, | 219 | }, |
| 214 | logs: [], | 220 | logs: [], |
| 215 | }); | 221 | }); |
| @@ -315,7 +321,7 @@ describe("sources api (HTTP)", () => { · 99 unmodified lines | |||
| 315 | expect(execution.isError).toBe(false); | 321 | expect(execution.isError).toBe(false); |
| 316 | expect(execution.structured).toMatchObject({ | 322 | expect(execution.structured).toMatchObject({ |
| 317 | status: "completed", | 323 | status: "completed", |
| 318 | result: { hello: "Hello Ada" }, | 324 | result: { ok: true, data: { hello: "Hello Ada" } }, |
| 319 | }); | 325 | }); |
| 320 | const requests = yield* server.requests; | 326 | const requests = yield* server.requests; |
| 321 | expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true); | 327 | expect(requests.some((request) => request.payload.query?.includes("__schema"))).toBe(true); |
| @@ -391,7 +397,8 @@ describe("sources api (HTTP)", () => { · 69 unmodified lines | |||
| 391 | expect(execution.structured).toMatchObject({ | 397 | expect(execution.structured).toMatchObject({ |
| 392 | status: "completed", | 398 | status: "completed", |
| 393 | result: { | 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 | expect((yield* server.requests).length).toBeGreaterThanOrEqual(2); | 404 | expect((yield* server.requests).length).toBeGreaterThanOrEqual(2); |
| @@ -159,7 +159,7 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 158 unmodified lines | |||
| 159 | `); | 159 | `); |
| 160 | 160 | ||
| 161 | db.prepare("INSERT INTO `openapi_source` (id, scope_id, oauth2) VALUES (?, ?, ?)").run( | 161 | db.prepare("INSERT INTO `openapi_source` (id, scope_id, oauth2) VALUES (?, ?, ?)").run( |
| 162 | "dealcloud_api", | 162 | "example_api", |
| 163 | "org-1", | 163 | "org-1", |
| 164 | JSON.stringify({ | 164 | JSON.stringify({ |
| 165 | kind: "oauth2", | 165 | kind: "oauth2", |
| @@ -174,27 +174,27 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 8 unmodified lines | |||
| 174 | "INSERT INTO `connection` (id, scope_id, provider, provider_state, scope, updated_at) VALUES (?, ?, ?, ?, ?, ?)", | 174 | "INSERT INTO `connection` (id, scope_id, provider, provider_state, scope, updated_at) VALUES (?, ?, ?, ?, ?, ?)", |
| 175 | ); | 175 | ); |
| 176 | insertConnection.run( | 176 | insertConnection.run( |
| 177 | "openapi-oauth2-app-dealcloud_api", | 177 | "openapi-oauth2-app-example_api", |
| 178 | "org-1", | 178 | "org-1", |
| 179 | "oauth2", | 179 | "oauth2", |
| 180 | JSON.stringify({ | 180 | JSON.stringify({ |
| 181 | kind: "client-credentials", | 181 | kind: "client-credentials", |
| 182 | tokenEndpoint: "https://resolve.dealcloud.com/oauth/token", | 182 | tokenEndpoint: "https://auth.example.test/oauth/token", |
| 183 | clientIdSecretId: "dealcloud-client-id", | 183 | clientIdSecretId: "example-client-id", |
| 184 | clientSecretSecretId: "dealcloud-client-secret", | 184 | clientSecretSecretId: "example-client-secret", |
| 185 | }), | 185 | }), |
| 186 | null, | 186 | null, |
| 187 | now, | 187 | now, |
| 188 | ); | 188 | ); |
| 189 | insertConnection.run( | 189 | insertConnection.run( |
| 190 | "openapi-oauth2-app-dealcloud_api", | 190 | "openapi-oauth2-app-example_api", |
| 191 | "user-org:user-jd:org-1", | 191 | "user-org:user-jd:org-1", |
| 192 | "openapi:oauth2", | 192 | "openapi:oauth2", |
| 193 | JSON.stringify({ | 193 | JSON.stringify({ |
| 194 | kind: "client-credentials", | 194 | kind: "client-credentials", |
| 195 | tokenEndpoint: "https://resolve.dealcloud.com/oauth/token", | 195 | tokenEndpoint: "https://auth.example.test/oauth/token", |
| 196 | clientIdSecretId: "dealcloud-client-id-jd", | 196 | clientIdSecretId: "example-client-id-jd", |
| 197 | clientSecretSecretId: "dealcloud-client-secret-jd", | 197 | clientSecretSecretId: "example-client-secret-jd", |
| 198 | }), | 198 | }), |
| 199 | null, | 199 | null, |
| 200 | now, | 200 | now, |
| @@ -207,12 +207,12 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 6 unmodified lines | |||
| 207 | "org-client-id", | 207 | "org-client-id", |
| 208 | "org-1", | 208 | "org-1", |
| 209 | "openapi", | 209 | "openapi", |
| 210 | "dealcloud_api", | 210 | "example_api", |
| 211 | "org-1", | 211 | "org-1", |
| 212 | "oauth2:oauth2:client-id", | 212 | "oauth2:oauth2:client-id", |
| 213 | "secret", | 213 | "secret", |
| 214 | null, | 214 | null, |
| 215 | "dealcloud-client-id-jd", | 215 | "example-client-id-jd", |
| 216 | null, | 216 | null, |
| 217 | now, | 217 | now, |
| 218 | now, | 218 | now, |
| @@ -221,12 +221,12 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 2 unmodified lines | |||
| 221 | "org-client-secret", | 221 | "org-client-secret", |
| 222 | "org-1", | 222 | "org-1", |
| 223 | "openapi", | 223 | "openapi", |
| 224 | "dealcloud_api", | 224 | "example_api", |
| 225 | "org-1", | 225 | "org-1", |
| 226 | "oauth2:oauth2:client-secret", | 226 | "oauth2:oauth2:client-secret", |
| 227 | "secret", | 227 | "secret", |
| 228 | null, | 228 | null, |
| 229 | "dealcloud-client-secret-jd", | 229 | "example-client-secret-jd", |
| 230 | null, | 230 | null, |
| 231 | now, | 231 | now, |
| 232 | now, | 232 | now, |
| @@ -235,13 +235,13 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 2 unmodified lines | |||
| 235 | "org-connection", | 235 | "org-connection", |
| 236 | "org-1", | 236 | "org-1", |
| 237 | "openapi", | 237 | "openapi", |
| 238 | "dealcloud_api", | 238 | "example_api", |
| 239 | "org-1", | 239 | "org-1", |
| 240 | "oauth2:oauth2:connection", | 240 | "oauth2:oauth2:connection", |
| 241 | "connection", | 241 | "connection", |
| 242 | null, | 242 | null, |
| 243 | null, | 243 | null, |
| 244 | "openapi-oauth2-app-dealcloud_api", | 244 | "openapi-oauth2-app-example_api", |
| 245 | now, | 245 | now, |
| 246 | now, | 246 | now, |
| 247 | ); | 247 | ); |
| @@ -249,13 +249,13 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 1 unmodified line | |||
| 249 | "jd-connection", | 249 | "jd-connection", |
| 250 | "user-org:user-jd:org-1", | 250 | "user-org:user-jd:org-1", |
| 251 | "openapi", | 251 | "openapi", |
| 252 | "dealcloud_api", | 252 | "example_api", |
| 253 | "org-1", | 253 | "org-1", |
| 254 | "oauth2:oauth2:connection", | 254 | "oauth2:oauth2:connection", |
| 255 | "connection", | 255 | "connection", |
| 256 | null, | 256 | null, |
| 257 | null, | 257 | null, |
| 258 | "openapi-oauth2-app-dealcloud_api", | 258 | "openapi-oauth2-app-example_api", |
| 259 | now, | 259 | now, |
| 260 | now, | 260 | now, |
| 261 | ); | 261 | ); |
| @@ -271,20 +271,20 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 9 unmodified lines | |||
| 271 | .prepare( | 271 | .prepare( |
| 272 | "SELECT scope_id, slot_key, kind, secret_id, connection_id FROM `credential_binding` WHERE source_id = ? ORDER BY scope_id, slot_key", | 272 | "SELECT scope_id, slot_key, kind, secret_id, connection_id FROM `credential_binding` WHERE source_id = ? ORDER BY scope_id, slot_key", |
| 273 | ) | 273 | ) |
| 274 | .all("dealcloud_api"); | 274 | .all("example_api"); |
| 275 | expect(bindings).toEqual([ | 275 | expect(bindings).toEqual([ |
| 276 | { | 276 | { |
| 277 | scope_id: "org-1", | 277 | scope_id: "org-1", |
| 278 | slot_key: "oauth2:oauth2:client-id", | 278 | slot_key: "oauth2:oauth2:client-id", |
| 279 | kind: "secret", | 279 | kind: "secret", |
| 280 | secret_id: "dealcloud-client-id", | 280 | secret_id: "example-client-id", |
| 281 | connection_id: null, | 281 | connection_id: null, |
| 282 | }, | 282 | }, |
| 283 | { | 283 | { |
| 284 | scope_id: "org-1", | 284 | scope_id: "org-1", |
| 285 | slot_key: "oauth2:oauth2:client-secret", | 285 | slot_key: "oauth2:oauth2:client-secret", |
| 286 | kind: "secret", | 286 | kind: "secret", |
| 287 | secret_id: "dealcloud-client-secret", | 287 | secret_id: "example-client-secret", |
| 288 | connection_id: null, | 288 | connection_id: null, |
| 289 | }, | 289 | }, |
| 290 | { | 290 | { |
| @@ -292,20 +292,20 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 1 unmodified line | |||
| 292 | slot_key: "oauth2:oauth2:connection", | 292 | slot_key: "oauth2:oauth2:connection", |
| 293 | kind: "connection", | 293 | kind: "connection", |
| 294 | secret_id: null, | 294 | secret_id: null, |
| 295 | connection_id: "openapi-oauth2-app-dealcloud_api", | 295 | connection_id: "openapi-oauth2-app-example_api", |
| 296 | }, | 296 | }, |
| 297 | { | 297 | { |
| 298 | scope_id: "user-org:user-jd:org-1", | 298 | scope_id: "user-org:user-jd:org-1", |
| 299 | slot_key: "oauth2:oauth2:client-id", | 299 | slot_key: "oauth2:oauth2:client-id", |
| 300 | kind: "secret", | 300 | kind: "secret", |
| 301 | secret_id: "dealcloud-client-id-jd", | 301 | secret_id: "example-client-id-jd", |
| 302 | connection_id: null, | 302 | connection_id: null, |
| 303 | }, | 303 | }, |
| 304 | { | 304 | { |
| 305 | scope_id: "user-org:user-jd:org-1", | 305 | scope_id: "user-org:user-jd:org-1", |
| 306 | slot_key: "oauth2:oauth2:client-secret", | 306 | slot_key: "oauth2:oauth2:client-secret", |
| 307 | kind: "secret", | 307 | kind: "secret", |
| 308 | secret_id: "dealcloud-client-secret-jd", | 308 | secret_id: "example-client-secret-jd", |
| 309 | connection_id: null, | 309 | connection_id: null, |
| 310 | }, | 310 | }, |
| 311 | { | 311 | { |
| @@ -313,7 +313,7 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 1 unmodified line | |||
| 313 | slot_key: "oauth2:oauth2:connection", | 313 | slot_key: "oauth2:oauth2:connection", |
| 314 | kind: "connection", | 314 | kind: "connection", |
| 315 | secret_id: null, | 315 | secret_id: null, |
| 316 | connection_id: "openapi-oauth2-app-dealcloud_api", | 316 | connection_id: "openapi-oauth2-app-example_api", |
| 317 | }, | 317 | }, |
| 318 | ]); | 318 | ]); |
| 319 | }); | 319 | }); |