Skip to main content
Drop a file. Build on it.

The picture

Attachments used to ride one message and end with it. A file dropped in chat is now saved — into that user’s own files, private to them, still there in next week’s conversation. The agent reads it and answers from it, and can build an app on top of it. The message that follows carries only a reference to the file. That is what keeps a transcript light: a spreadsheet lands once, and the conversation about it stays a conversation, not a copy of the file repeated on every turn. Images are the deliberate exception. They still ride the message itself, because that is how a model sees a picture at all.

Where files go

Each user gets /user/files/ inside their own workspace — the same filesystem the agent works in, scoped to that one person. Nothing is shared between users, and nothing here belongs to a thread, so a file survives every conversation that comes after it. POST /files is the door a browser uses:
The body is the file’s own raw bytes under its own media type — there is no multipart form to assemble, so the name rides the query string, percent-encoded. x-vendo-upload is required and its value is not read. A raw body cannot be application/json, so this door sits outside the wire’s CSRF floor and asks for a header a cross-site form post cannot set. Without it the upload comes back a 400. The client below sends it for you.
Same name replaces. Upload sales-2026.csv again and the new file is sales-2026.csv — last write wins, no -v2 suffix, no second copy. This is deliberate: re-sending a corrected export is the common case, and a drawer that silently accumulated four near-identical spreadsheets would be worse than one that keeps the newest.
A name is a file name, never a path. nested/report.csv and ../escape.csv are refused rather than quietly rewritten, so nothing can address anything outside the user’s own files.

From the browser

The client does it in one call — no upload state to manage:
The built-in chat surface already does this for you: dropping a file on the thread, or picking one with the paperclip, uploads it and sends the reference.

From your own code

putUserFile is the same write, called server-side — for pushing a file at a user without waiting for them to bring it:
It writes exactly what the chat door writes, so nothing downstream can tell the two apart. It delivers nothing and starts no turn — the file is simply there, and the user reaches it the next time they chat.

What the agent does with it

Three tools come with every deployment — no adapter, no key, no configuration: All three run through the same guard, audit trail and approval rules as every other tool, so on the cautious preset the write parks for the person. None of them takes a path — only a file name — so nothing they are asked for can address anything outside that one user’s files. There is no subject argument either: each call opens the drawer of the principal that made it and no other. A long file is read a window at a time — 200 lines, or 12,000 characters, whichever comes first — so a spreadsheet gets walked rather than truncated. The result says truncated and hands back a nextOffset to pass as offset on the next call. offset counts lines, never characters. Because the tools are on the ordinary registry, the listing is also how the agent finds a file in a later conversation. Nothing about last month’s upload is in this conversation’s history — the agent looks, finds it, and reads it. Being on the ordinary registry is also why they are at the MCP door: an outside agent holding a user-bound token gets the same three tools against that same user’s files.

What reads back

Any file can be saved. Only these read back as text: csv · tsv · txt · log · sql · md · json · ndjson · xml · html · yaml · yml Anything else — a PDF, an image, an .xlsx workbook, a .parquet export, or any extension Vendo does not recognize — comes back with its name, size and media type, readable: false, and this:
sales.parquet is saved, but its contents cannot be read back yet. Only these read back as text: csv, tsv, txt, log, sql, md, json, ndjson, xml, html, yaml, yml. Tell the user what the file is and ask them for one of those if you need what is inside it.
That is an ok, not an error. The bytes are safe, and the sentence tells the agent what to ask the person for instead of leaving it to narrate an empty result.
The extension is the whole evidence. Nothing stores a media type, so a perfectly good text file saved as notes.dat does not read back. Renaming it notes.txt is what fixes it.

Building on a file

There is nothing to wire here. Once your users can drop files (the upload door above), everything in this section happens on its own — no API to call, no configuration. This is what the agent does with a file, automatically. The whole flow, as your user experiences it:
  1. They drop sales-2026.csv into the chat and ask: “make me a dashboard of this.”
  2. The agent reads the file and builds an app. The rows it needs are copied into the app’s own saved items — the app’s private storage, separate from the user’s files. The dashboard renders from those saved rows.
  3. The file and the app now live separately. The copy is a snapshot, not a live link — the app doesn’t read the user’s files afterward, and nothing re-reads them on its behalf.
  4. In December they drop the updated sales-2026.csv. Same name, so it replaces the old copy — no duplicates. The dashboard still shows what it was built from —
  5. — until they ask. “Refresh my dashboard.” The agent reads the new file, rewrites the app’s saved items, and says what it did: file replaced, app updated.
No watcher, no polling, no background sync — a file sitting in the drawer changes nothing until the user talks to the agent. That’s deliberate: everything the agent does is visible in the conversation.
What is yours to wire is already above on this page: the upload door (POST /files), client.files.upload in your UI, and putUserFile from your backend. The agent’s three file tools ship with every deployment.
In this release a PDF or an image lands in the drawer and can be read about, but does not reach an app. Only tabular data is copied into an app. Putting a document into an app’s own file storage is a follow-up.

Size

An upload is capped at 5 MiB (5242880 bytes), and createVendo({ uploadMaxBytes }) moves it. That is a cap on the door — on what one browser request through POST /files, or one vendo_user_files_put call, may carry. Both doors read the same number, so a file refused in chat cannot be admitted by asking over MCP instead. An over-cap upload is a validation refusal — a 400 on POST /files, before anything is written — and it names both the knob and where the bytes would have landed:
“sales-2026.csv” is 8388608 bytes and the upload door allows at most 5242880: send a smaller file, or raise createVendo(). These bytes land in this deployment’s store, which caps one file at 5242880 bytes — wire createVendo() with a FilesAdapter (s3Files) before raising the door past it.
putUserFile is a trusted server caller and the door’s cap does not apply to it. What bounds it instead is the backing: with no files: adapter configured, files are kept in your store’s own blobs, up to 5 MiB each. Raising uploadMaxBytes past that without a bucket only moves the failure one step later.

Your own bucket

s3Files is the ready-made files: adapter. It signs SigV4 itself over WebCrypto, so it runs on an edge target, and it talks to anything S3-compatible: Cloudflare R2, AWS S3, Supabase Storage, MinIO.
endpoint is the origin your provider’s dashboard gives you — R2 https://<account>.r2.cloudflarestorage.com, AWS https://s3.<region>.amazonaws.com, Supabase https://<ref>.supabase.co/storage/v1/s3, MinIO your own. region defaults to "auto", which is what R2 requires and MinIO ignores; AWS and Supabase need their real one. prefix scopes keys inside the bucket so one bucket can hold several deployments, and credentials takes an optional sessionToken for temporary credentials. The adapter reads no environment of its own: which credentials reach it is your composition’s question. There is one backing for every file — no tiering, no spillover. Unset, files are store blobs; set, every file is in your bucket. Nothing about this requires a Vendo Cloud key. vendo doctor prints where a deployment’s uploads land, and the boot block adds a files row when you have wired an adapter of your own.