Skip to content

Outbound attachments are inline-base64 only — back them with AgentDrive #738

Description

@jiashuoz

Summary

Outbound attachments can only be supplied as inline base64. For an agent caller, that means the file's bytes must pass through the model's context window on every send — there is no upload handle, no URL, and no path. The read and forward paths already avoid this; only the send path does not.

Direction: back outbound attachments with AgentDrive so callers reference stored objects instead of inlining bytes.

Current interface

send_message and forward_message accept attachments[]:

{
  "filename":     "report.pdf",        // 1–255 chars
  "content_type": "application/pdf",   // ^[a-zA-Z][a-zA-Z0-9.+-]*\/[a-zA-Z0-9.+-]+$
  "data":         "JVBERi0xLjQK…"      // ^[A-Za-z0-9+/]+={0,2}$  — base64 only
}

maxItems: 10, ≤10 MB each decoded, ≤25 MB combined.

The asymmetry

Path Indirection Bytes through caller context
get_attachment (read) download_url + expires_at, explicitly "no size limit" No
forward_message originals carried over server-side by default No
send_message (originate) none Yes, always

The read path caps opt-in inline base64 at 256 KB precisely because inlining is expensive. The write path accepts 10 MB of the same encoding — a 40× larger ceiling in the direction that was deliberately constrained elsewhere.

Why the documented limits are not reachable

Base64 expands by ~4/3, so the stated ceilings imply an agent emitting ~13.3 MB (single) or ~33 MB (combined) of high-entropy text in one tool call. In practice the binding constraint is per-call output, not the documented size limit.

Measured on a real send: an 11,549-byte zip became a 15,400-character data string, and emitting it was the slowest step of the task by a wide margin. Extrapolating, a 1 MB PDF is ~1.4M characters — roughly two to three orders of magnitude below the documented cap.

Integrity

There is no checksum field anywhere in the attachment interface, so nothing validates that what the caller emitted is what the server stored.

The data pattern ^[A-Za-z0-9+/]+={0,2}$ constrains the alphabet only. A dropped or inserted character usually changes the decoded length or fails the decode, so it tends to surface. A substitution of one valid base64 character for another preserves length, decodes cleanly, and silently yields different bytes. Nothing in the API detects this.

Whether that corruption is noticed today depends entirely on the container the caller happened to pick:

  • zip / tar.gz — per-entry CRC32, so the recipient gets a loud error
  • PDF, plain text, most images — no internal check, corrupts silently

That is luck, not a guarantee.

Proposed

  1. AgentDrive-backed handles. Accept a stored-object reference as an alternative to data in attachments[] — one of data or the handle, not both. Mirrors what get_attachment's download_url already does for the read path, and lets an agent that produced a file in AgentDrive attach it without ever materializing the bytes in context.
  2. Optional sha256 per attachment entry. Server rejects on mismatch rather than mailing a corrupted file. Cheap, and it makes integrity assertable end to end for the inline path that will remain for small files.
  3. Reconcile the inline ceilings. If 256 KB is the right inline limit for reads, the 10 MB write limit deserves the same reasoning applied to it — or documentation stating the practical limit for agent callers.

Notes

  • Discovered while sending a ~11 KB zip attachment through the MCP surface; the send itself succeeded and delivered intact (server-reported size_bytes matched the local file exactly).
  • Not urgent for correctness of small sends today. It is a blocker for any workflow that originates a non-trivial file — reports, exports, generated documents — which is a plausible thing to want from email infrastructure for agents.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions