The problem
An agent that has produced or received binary bytes has no tool that can put them in its own file store. upload_file takes text only, so a Word document, an image, a spreadsheet or a filled PDF coming from anywhere other than a hardcoded platform path is unreachable to the model.
This surfaced in #756 (fill an emailed Word form and reply with it): once the attachment is in the store, any tool or connector that produces a new binary version of it has nowhere to put it.
Priority — P2: correctness: the capability exists at every layer below the tool; only the declaration is missing, and no path is silently producing a wrong result today.
What is actually missing — VERIFIED, and it is one parameter
The bytes path is complete from the HTTP route down. It is only the two tool declarations that do not expose it.
The DO route already accepts base64 — workers/api/src/agent-do-storage-routes.ts:143-154:
contentBase64?: string;
…
if (!body.name || (!body.content && !body.contentBase64))
return json({ error: "name and content or contentBase64 required" }, 400);
const data = body.contentBase64
? bytesFromBase64(body.contentBase64).slice().buffer
: body.content;
The API route forwards the whole body verbatim, so contentBase64 already reaches the DO over HTTP today — workers/api/src/routes/storage.ts:167-175 (agent) and :341-347 (instance):
return proxyDO(c, agent.id, "/files", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(await c.req.json()),
});
And one caller already uses it in production — the Gmail attachment downloader, workers/api/src/lib/connectors/gmail.ts:226:
contentBase64: base64UrlToBase64(base64url),
What does not expose it:
upload_file, workers/api/src/lib/storage-tools.ts:81-91 — content: { type: "string", description: "File content (text)", required: true } (:86), no base64 parameter. Its handler (storage-tools.ts:325-336) passes data: content straight through as a string.
upload_agent_file over MCP, workers/mcp/src/storage-tools.ts:264-292 — content: z.string().describe("File content (text)") (:271), and it POSTs to the very /v1/agents/:id/files route above.
engine.fileUpload already accepts string | ArrayBuffer | Uint8Array (workers/api/src/agent-storage-utils.ts:160-164, fileBytes), and guessMimeType already knows the Office types (storage-tools.ts:815-816: doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document").
Grep used for the absence: grep -rn "contentBase64" workers/api/src | grep -v '\.test\.' → five hits, all in the three files above, none in a tool declaration.
What to do
- Add
content_base64 to upload_file (storage-tools.ts:81-91) and require exactly one of content / content_base64. Decode with the existing bytesFromBase64 and hand fileUpload a Uint8Array.
- Same for MCP
upload_agent_file — pass contentBase64 through; the route already relays it.
- Cap the decoded size explicitly and refuse over it with a sentence naming the limit, rather than letting the isolate decide.
MAX_ATTACHMENT_BYTES = 12 * 1024 * 1024 (connectors/gmail.ts:43) is the existing precedent for "bytes we will pull through a Worker".
- Default
mime_type from guessMimeType(name) on the base64 branch — a base64 upload defaulting to text/plain would poison extractFileText's dispatch (agent-storage-utils.ts:141-149).
Alternatives considered and rejected
- A separate
upload_binary_file tool. Rejected: it doubles a tool the model must choose between on a distinction (encoding) that is not about intent, and every gate — builtin-tool-policy.ts:105, tool-reach.ts:135, the mutation report — would need a second entry.
- Accept a URL and have the platform fetch it. Rejected: that is a new SSRF surface for a problem that is a parameter, and
fetch_url/http_request already exist behind lib/ssrf.ts for the cases where fetching is the point.
- Route binary writes through the multipart endpoints (
/files/multipart/*). Rejected for the tool surface: multipart exists for 2GB uploads from a browser that can disconnect. A model handing over a 200KB form is a single request.
Acceptance criteria
Regression risk
- Existing text callers must be untouched.
storage-tools.test.ts:187-190 already covers upload_file with { name, content }; that test must keep passing unchanged.
extractText and vectorisation. fileUpload auto-extracts and vectorises; a binary upload with a text mime type would push binary noise into Vectorize. Criterion 4 is the guard.
- Tool-count and policy tables.
builtin-tool-policy.ts, tool-reach.ts and tool-mutation-report.test.ts key off tool NAMES; adding a parameter touches none of them, which is precisely why a parameter beats a second tool.
Parent: #756. Related: #755 (the download that produces the first such file).
The problem
An agent that has produced or received binary bytes has no tool that can put them in its own file store.
upload_filetakes text only, so a Word document, an image, a spreadsheet or a filled PDF coming from anywhere other than a hardcoded platform path is unreachable to the model.This surfaced in #756 (fill an emailed Word form and reply with it): once the attachment is in the store, any tool or connector that produces a new binary version of it has nowhere to put it.
Priority —
P2: correctness: the capability exists at every layer below the tool; only the declaration is missing, and no path is silently producing a wrong result today.What is actually missing — VERIFIED, and it is one parameter
The bytes path is complete from the HTTP route down. It is only the two tool declarations that do not expose it.
The DO route already accepts base64 —
workers/api/src/agent-do-storage-routes.ts:143-154:The API route forwards the whole body verbatim, so
contentBase64already reaches the DO over HTTP today —workers/api/src/routes/storage.ts:167-175(agent) and:341-347(instance):And one caller already uses it in production — the Gmail attachment downloader,
workers/api/src/lib/connectors/gmail.ts:226:What does not expose it:
upload_file,workers/api/src/lib/storage-tools.ts:81-91—content: { type: "string", description: "File content (text)", required: true }(:86), no base64 parameter. Its handler (storage-tools.ts:325-336) passesdata: contentstraight through as a string.upload_agent_fileover MCP,workers/mcp/src/storage-tools.ts:264-292—content: z.string().describe("File content (text)")(:271), and it POSTs to the very/v1/agents/:id/filesroute above.engine.fileUploadalready acceptsstring | ArrayBuffer | Uint8Array(workers/api/src/agent-storage-utils.ts:160-164,fileBytes), andguessMimeTypealready knows the Office types (storage-tools.ts:815-816:doc: "application/msword",docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document").Grep used for the absence:
grep -rn "contentBase64" workers/api/src | grep -v '\.test\.'→ five hits, all in the three files above, none in a tool declaration.What to do
content_base64toupload_file(storage-tools.ts:81-91) and require exactly one ofcontent/content_base64. Decode with the existingbytesFromBase64and handfileUploadaUint8Array.upload_agent_file— passcontentBase64through; the route already relays it.MAX_ATTACHMENT_BYTES = 12 * 1024 * 1024(connectors/gmail.ts:43) is the existing precedent for "bytes we will pull through a Worker".mime_typefromguessMimeType(name)on the base64 branch — a base64 upload defaulting totext/plainwould poisonextractFileText's dispatch (agent-storage-utils.ts:141-149).Alternatives considered and rejected
upload_binary_filetool. Rejected: it doubles a tool the model must choose between on a distinction (encoding) that is not about intent, and every gate —builtin-tool-policy.ts:105,tool-reach.ts:135, the mutation report — would need a second entry.fetch_url/http_requestalready exist behindlib/ssrf.tsfor the cases where fetching is the point./files/multipart/*). Rejected for the tool surface: multipart exists for 2GB uploads from a browser that can disconnect. A model handing over a 200KB form is a single request.Acceptance criteria
upload_filewithcontent_base64and nocontentstores a file whose bytes round-trip byte-for-byte throughlist_files→ the file-download route.upload_filewith bothcontentandcontent_base64is refused, naming which to use.upload_filewith neither is refused (today!name || !contentalready refuses; the message must name both options).x.docxwith nomime_typeis stored asapplication/vnd.openxmlformats-officedocument.wordprocessingml.document, nottext/plain.upload_agent_fileaccepts the same parameter and the round-trip test covers that surface too.Regression risk
storage-tools.test.ts:187-190already coversupload_filewith{ name, content }; that test must keep passing unchanged.extractTextand vectorisation.fileUploadauto-extracts and vectorises; a binary upload with a text mime type would push binary noise into Vectorize. Criterion 4 is the guard.builtin-tool-policy.ts,tool-reach.tsandtool-mutation-report.test.tskey off tool NAMES; adding a parameter touches none of them, which is precisely why a parameter beats a second tool.Parent: #756. Related: #755 (the download that produces the first such file).