Skip to content

upload_file is text-only, so no tool can put bytes in the file store — the DO route, the API route and one live caller already take contentBase64 #762

Description

@serge-ivo

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 base64workers/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-91content: { 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-292content: 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

  1. 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.
  2. Same for MCP upload_agent_file — pass contentBase64 through; the route already relays it.
  3. 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".
  4. 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

  • upload_file with content_base64 and no content stores a file whose bytes round-trip byte-for-byte through list_files → the file-download route.
  • upload_file with both content and content_base64 is refused, naming which to use.
  • upload_file with neither is refused (today !name || !content already refuses; the message must name both options).
  • A base64 upload named x.docx with no mime_type is stored as application/vnd.openxmlformats-officedocument.wordprocessingml.document, not text/plain.
  • Over-cap base64 is refused before the decode allocates, with the limit in the message.
  • MCP upload_agent_file accepts the same parameter and the round-trip test covers that surface too.

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2: correctnessReal defect, no live harm today — inert fields, miscounts, missing guardsbackendBackend / Worker / API workbugSomething isn't workingconnectorsConnector + tool framework

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions