* 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 | 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); |
| @@ -159,7 +159,7 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 158 unmodified lines | ||
| 159 | 159 | `); |
| 160 | 160 | |
| 161 | 161 | db.prepare("INSERT INTO `openapi_source` (id, scope_id, oauth2) VALUES (?, ?, ?)").run( |
| 162 | − "dealcloud_api", | |
| 162 | + "example_api", | |
| 163 | 163 | "org-1", |
| 164 | 164 | JSON.stringify({ |
| 165 | 165 | kind: "oauth2", |
| @@ -174,27 +174,27 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 8 unmodified lines | ||
| 174 | 174 | "INSERT INTO `connection` (id, scope_id, provider, provider_state, scope, updated_at) VALUES (?, ?, ?, ?, ?, ?)", |
| 175 | 175 | ); |
| 176 | 176 | insertConnection.run( |
| 177 | − "openapi-oauth2-app-dealcloud_api", | |
| 177 | + "openapi-oauth2-app-example_api", | |
| 178 | 178 | "org-1", |
| 179 | 179 | "oauth2", |
| 180 | 180 | JSON.stringify({ |
| 181 | 181 | kind: "client-credentials", |
| 182 | − tokenEndpoint: "https://resolve.dealcloud.com/oauth/token", | |
| 183 | − clientIdSecretId: "dealcloud-client-id", | |
| 184 | − clientSecretSecretId: "dealcloud-client-secret", | |
| 182 | + tokenEndpoint: "https://auth.example.test/oauth/token", | |
| 183 | + clientIdSecretId: "example-client-id", | |
| 184 | + clientSecretSecretId: "example-client-secret", | |
| 185 | 185 | }), |
| 186 | 186 | null, |
| 187 | 187 | now, |
| 188 | 188 | ); |
| 189 | 189 | insertConnection.run( |
| 190 | − "openapi-oauth2-app-dealcloud_api", | |
| 190 | + "openapi-oauth2-app-example_api", | |
| 191 | 191 | "user-org:user-jd:org-1", |
| 192 | 192 | "openapi:oauth2", |
| 193 | 193 | JSON.stringify({ |
| 194 | 194 | kind: "client-credentials", |
| 195 | − tokenEndpoint: "https://resolve.dealcloud.com/oauth/token", | |
| 196 | − clientIdSecretId: "dealcloud-client-id-jd", | |
| 197 | − clientSecretSecretId: "dealcloud-client-secret-jd", | |
| 195 | + tokenEndpoint: "https://auth.example.test/oauth/token", | |
| 196 | + clientIdSecretId: "example-client-id-jd", | |
| 197 | + clientSecretSecretId: "example-client-secret-jd", | |
| 198 | 198 | }), |
| 199 | 199 | null, |
| 200 | 200 | now, |
| @@ -207,12 +207,12 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 6 unmodified lines | ||
| 207 | 207 | "org-client-id", |
| 208 | 208 | "org-1", |
| 209 | 209 | "openapi", |
| 210 | − "dealcloud_api", | |
| 210 | + "example_api", | |
| 211 | 211 | "org-1", |
| 212 | 212 | "oauth2:oauth2:client-id", |
| 213 | 213 | "secret", |
| 214 | 214 | null, |
| 215 | − "dealcloud-client-id-jd", | |
| 215 | + "example-client-id-jd", | |
| 216 | 216 | null, |
| 217 | 217 | now, |
| 218 | 218 | now, |
| @@ -221,12 +221,12 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 2 unmodified lines | ||
| 221 | 221 | "org-client-secret", |
| 222 | 222 | "org-1", |
| 223 | 223 | "openapi", |
| 224 | − "dealcloud_api", | |
| 224 | + "example_api", | |
| 225 | 225 | "org-1", |
| 226 | 226 | "oauth2:oauth2:client-secret", |
| 227 | 227 | "secret", |
| 228 | 228 | null, |
| 229 | − "dealcloud-client-secret-jd", | |
| 229 | + "example-client-secret-jd", | |
| 230 | 230 | null, |
| 231 | 231 | now, |
| 232 | 232 | now, |
| @@ -235,13 +235,13 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 2 unmodified lines | ||
| 235 | 235 | "org-connection", |
| 236 | 236 | "org-1", |
| 237 | 237 | "openapi", |
| 238 | − "dealcloud_api", | |
| 238 | + "example_api", | |
| 239 | 239 | "org-1", |
| 240 | 240 | "oauth2:oauth2:connection", |
| 241 | 241 | "connection", |
| 242 | 242 | null, |
| 243 | 243 | null, |
| 244 | − "openapi-oauth2-app-dealcloud_api", | |
| 244 | + "openapi-oauth2-app-example_api", | |
| 245 | 245 | now, |
| 246 | 246 | now, |
| 247 | 247 | ); |
| @@ -249,13 +249,13 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 1 unmodified line | ||
| 249 | 249 | "jd-connection", |
| 250 | 250 | "user-org:user-jd:org-1", |
| 251 | 251 | "openapi", |
| 252 | − "dealcloud_api", | |
| 252 | + "example_api", | |
| 253 | 253 | "org-1", |
| 254 | 254 | "oauth2:oauth2:connection", |
| 255 | 255 | "connection", |
| 256 | 256 | null, |
| 257 | 257 | null, |
| 258 | − "openapi-oauth2-app-dealcloud_api", | |
| 258 | + "openapi-oauth2-app-example_api", | |
| 259 | 259 | now, |
| 260 | 260 | now, |
| 261 | 261 | ); |
| @@ -271,20 +271,20 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 9 unmodified lines | ||
| 271 | 271 | .prepare( |
| 272 | 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 | 275 | expect(bindings).toEqual([ |
| 276 | 276 | { |
| 277 | 277 | scope_id: "org-1", |
| 278 | 278 | slot_key: "oauth2:oauth2:client-id", |
| 279 | 279 | kind: "secret", |
| 280 | − secret_id: "dealcloud-client-id", | |
| 280 | + secret_id: "example-client-id", | |
| 281 | 281 | connection_id: null, |
| 282 | 282 | }, |
| 283 | 283 | { |
| 284 | 284 | scope_id: "org-1", |
| 285 | 285 | slot_key: "oauth2:oauth2:client-secret", |
| 286 | 286 | kind: "secret", |
| 287 | − secret_id: "dealcloud-client-secret", | |
| 287 | + secret_id: "example-client-secret", | |
| 288 | 288 | connection_id: null, |
| 289 | 289 | }, |
| 290 | 290 | { |
| @@ -292,20 +292,20 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 1 unmodified line | ||
| 292 | 292 | slot_key: "oauth2:oauth2:connection", |
| 293 | 293 | kind: "connection", |
| 294 | 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 | 298 | scope_id: "user-org:user-jd:org-1", |
| 299 | 299 | slot_key: "oauth2:oauth2:client-id", |
| 300 | 300 | kind: "secret", |
| 301 | − secret_id: "dealcloud-client-id-jd", | |
| 301 | + secret_id: "example-client-id-jd", | |
| 302 | 302 | connection_id: null, |
| 303 | 303 | }, |
| 304 | 304 | { |
| 305 | 305 | scope_id: "user-org:user-jd:org-1", |
| 306 | 306 | slot_key: "oauth2:oauth2:client-secret", |
| 307 | 307 | kind: "secret", |
| 308 | − secret_id: "dealcloud-client-secret-jd", | |
| 308 | + secret_id: "example-client-secret-jd", | |
| 309 | 309 | connection_id: null, |
| 310 | 310 | }, |
| 311 | 311 | { |
| @@ -313,7 +313,7 @@ describe("0009_repair_openapi_oauth_cutover_residue", () => { · 1 unmodified line | ||
| 313 | 313 | slot_key: "oauth2:oauth2:connection", |
| 314 | 314 | kind: "connection", |
| 315 | 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 | }); |