diff --git a/.changeset/avoid-sandbox-file-decoding.md b/.changeset/avoid-sandbox-file-decoding.md new file mode 100644 index 000000000..6a1e2ad81 --- /dev/null +++ b/.changeset/avoid-sandbox-file-decoding.md @@ -0,0 +1,5 @@ +--- +"executor": patch +--- + +Prevent execute agents from attempting unsupported base64 decoding by directing file payloads through ToolFile emission and bodyBase64 forwarding. diff --git a/packages/core/execution/src/skills.ts b/packages/core/execution/src/skills.ts index 473e18ca0..1036ae697 100644 --- a/packages/core/execution/src/skills.ts +++ b/packages/core/execution/src/skills.ts @@ -47,11 +47,12 @@ const EXECUTE_SKILL_BODY = [ "- Tool calls return a value union: `{ ok: true, data }` for success or `{ ok: false, error: { code, message, status?, details?, retryable? } }` for expected tool/domain failures. Branch on `result.ok`.", "- `data` is the upstream payload itself. HTTP-backed tools (OpenAPI) also set `http: { status, headers }` beside `data` — read `result.http?.headers` for pagination (Link) or rate-limit headers.", "- Use `emit(value)` to append user-visible output. Plain values become MCP text content. MCP content blocks are forwarded as-is. `ToolFile` values are rendered by MIME. Emitting and returning compose: emitted items come first in the tool result, the returned value follows, and the envelope reports an `emitted` count so you can confirm the items landed.", - '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. Emit any attachment with `emit(result.data)`.', + '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. A "file-returning tool" includes APIs that return file bytes inside a JSON field, such as Base64-encoded `content`; wrap those payloads in a `ToolFile` and `emit()` them. Emit any attachment with `emit(result.data)`.', + "- Never decode or transcode bytes yourself — the sandbox has no `Buffer`, `atob`, `btoa`, `TextDecoder`, or `TextEncoder`. For base64-encoded bytes in a JSON field, wrap the payload in a `ToolFile` with its `mimeType` and `emit()` it; to forward them to an upload tool, pass the `ToolFile`'s base64 `data` as that tool's `bodyBase64` (both sides speak base64, so nothing is decoded).", '- To emit MCP-native content directly, pass an MCP content block to `emit(...)`, such as `{ type: "image", data, mimeType }`, `{ type: "audio", data, mimeType }`, `{ type: "text", text }`, `{ type: "resource", resource }`, or `{ type: "resource_link", uri, name, ... }`.', "- `emit(ToolFile)` is MIME-based: `image/*` becomes MCP image content, `audio/*` becomes MCP audio content, text-like files become decoded text, and other binary files become embedded MCP resources.", "- `return` is only for ordinary structured data. Returning a `ToolFile`, a `ToolResult`, an MCP content block, or a bare base64 string does not emit content to the MCP client.", - "- Some providers, including Gmail, return attachment bytes without a public URL. To send that attachment to another API from code, decode `ToolFile.data` from base64 and pass the bytes to that API's upload/file input.", + "- Some providers, including Gmail, return attachment bytes as a `ToolFile` with no public URL to hand off — the bytes themselves are the payload, so `emit(result.data)` to display it, or pass its base64 `data` as another tool's `bodyBase64` to forward it.", "- If `tools.search()` returns `hasMore: true` and you didn't find what you need, fetch the next page: `tools.search({ query, offset: nextOffset, limit })`.", "- Always use the full address when calling tools: `tools....(args)`. The `path` returned by `tools.search()` / `tools.describe.tool()` is already the exact path under `tools` — call `tools[path]` rather than guessing segments.", "- The `tools` object is a lazy proxy — enumerating it (`Object.keys(tools)`, spread, `for...in`) throws. Use `tools.search()` or `tools.executor.coreTools.connections.list({})` instead.",