Files
Last updated October 7, 2026
People attach files to conversations and records: the composer's paperclip, a pasted screenshot, a file dropped on the thread. Agents read them under the same governance as everything else, and can leave files of their own (a CSV export, a drafted document). This page covers where files are kept, who may see them, how they are served, the limits, and backups. How agents read and make files, and how search finds them, are in Asking an agent and The brain.
Who sees a file#
A file is visible exactly where its place is, checked again on every request: nothing is cached and there are no public links.
| A file | Who sees, downloads and previews it |
|---|---|
| An upload not yet posted | Its uploader |
| In a conversation | Whoever sees the conversation: its owner, its participants, anyone mentioned, admins. Someone taken out of it loses its files at once |
| On a record | Whoever may read the record (the collection's policy and its row rule). A deleted record's files are admins' only, and come back if the record is restored |
A file someone may not see answers "not found", never "forbidden".
An agent sees a conversation's files only where both it and the person it
acts for see the conversation, so one agent two people delegate to never
carries one person's files to the other. An agent is shown a file's name
and class (in lists, GET /api/files/:id, MCP's kernel_files) only when
the gate would let it read the file now: its data classes, deny rules and
kill switches apply to what it is shown, not only to downloads.
Uploading needs what posting does: to be in the conversation as a
contributor or owner, and the conversation open (a closed one keeps its
files and takes no more, 409 thread_closed). Uploading to a record needs
the right to update it. A file is removed by its uploader, the
conversation's owner, whoever may update the record, or an admin. An
agent's removal, or a change of a file's class by an agent, passes the gate
as file.write first, so deny rules, approvals and kill switches apply;
since a removal cannot be undone, a policy that would ask for approval
refuses it.
Classes#
A file takes its place's data class: the conversation's (unset reads as
internal; the owner may raise it, an admin may set any), its record
collection's, or internal for an upload not yet posted, raised to the
conversation's when it is posted there. Its uploader may raise a file's
class; only an admin may lower it, also below its place's (422
class_too_low otherwise). A file an agent makes starts at the higher of
its conversation's class and the class of what its run has read. Agents
read a file only through the gate (file.read at the file's class), and
every file an agent stores goes through it too (file.write), so a policy
can refuse or ask for approval of either. An agent's file op that needs
approval waits for it and makes the file once approved; an upload over the
API or MCP that would need approval is refused instead, since its bytes are
not kept for later. Every read raises the class of the run's next model
call to the file's: a step that names a lower class for its call is held
at what the run has read.
How files are kept#
Every file has its own random 256-bit key, sealed with the secrets key and kept in the state file. Its bytes, its two image previews and its extracted text are each sealed with that key (XChaCha20-Poly1305 in 64 KiB segments, bound to the file and the variant) before they reach storage:
- On this server, in
files/beside the state file, one folder per shard, files 0600 in folders 0700. Under--tenants, each tenant's in its own folder. - In an S3-compatible bucket the operator names (
OK_FILES_STORE=s3, see Operations), still sealed.
Removing a file removes its key and its blobs, so a copy of a blob left anywhere does not open. Rotating the secrets key re-seals every file's key in the same step as the other secrets; the blobs are not rewritten.
The text extracted for search sits in the state file's search index, as
protected at rest as records are: by the disk's own protection. A removed
file's row keeps only its id, its place, its class and when it was
removed: no name, type, size, fingerprint or uploader, and the update runs
with SQLite's secure_delete on.
Text is read from a PDF or a Word document in a child process of the
server (orchkernel internal extract), which starts with an empty
environment (none of the server's keys or secrets reach a parser reading
an uploaded file), is killed after 30 seconds, and has its CPU time and
open files limited on Linux and macOS, its memory to 1 GiB on Linux only
(other systems set no memory limit for it). At most two run at once; a
file waiting for one is read on a later tick.
How files are served#
| Answer | How |
|---|---|
Download (/content) |
Always a download (Content-Disposition: attachment), never cached |
| Type | Found from the content, never the name. HTML, SVG, XML and script are always served as application/octet-stream: an invoice.png that is really a web page downloads and is never shown |
| Previews | Thumbnails and the viewer's image are made by the server from the pixels of PNG, JPEG, GIF and WebP: turned upright, with no EXIF, no GPS position, nothing but pixels. Download gives the original file. An image over 45 megapixels gets no preview (its size is read from its header, before it is decoded), and the server decodes two images at a time |
| A PDF's pages | Drawn in the browser from the original, which the page asks for as a preview: checked like a download, and not logged as one |
| CSV an agent makes | A cell that starts with =, +, -, @, a tab or a carriage return (and is not a number) gets a ' in front, so a spreadsheet shows it as text instead of running it as a formula |
| Every file answer | X-Content-Type-Options: nosniff, Content-Security-Policy: sandbox; default-src 'none', Cross-Origin-Resource-Policy: same-origin |
HEIC, TIFF, BMP and SVG files show as a card with Download.
Limits#
Files can be up to 25 MB (OK_FILE_MAX_MB); an admin may set a lower
limit on File settings. A company's storage is 20 GB
(OK_FILES_QUOTA_GB), every preview counted; a tenant's 5 GB unless the
operator sets its own. Past either, the upload is refused in words: "This
file is 31 MB. Files can be up to 25 MB.", or "Your company's file storage
is full: 20 GB of 20 GB used. Remove files you no longer need, or ask
whoever runs OrchKernel for more space." Admins get one notification a day
while storage is over 90 % full. A person may try 120 uploads an hour
(refused ones count), with four arriving at once (sixteen for everyone).
Everything that can refuse an upload (where it goes, who sends it, the
gate, these limits, a declared size against the storage left) is asked
before a byte of it is read.
Upkeep#
| What | What happens to its files |
|---|---|
| A conversation closes | Nothing: they stay readable |
| A file is removed | It shows as "Attachment removed"; its key, blobs and search text go |
| A record is deleted | Its files stay, for admins only |
| A record is purged (retention, opt-out, forget) | Its files go in the same step, their search text inside the purge itself |
| An upload is never posted | Removed after 24 hours |
| A tenant is purged | Its folder goes, and its prefix in the bucket is emptied |
Once a day the server also removes blobs no file owns and search text of removed files (left by a crash between a removal and its clean-up).
Backups#
A backup holds every live file's blobs as they are sealed on disk, and a restore puts them back with the state: restore an earlier backup and a file removed since is back and opens. With files in a bucket, the bucket is the store: the backup lists its objects, and staging a restore says which are missing (turn on the bucket's versioning). See Backups and restore.
The API#
| Route | What |
|---|---|
POST /api/files?name=<n>[&thread=<id>|&record=<collection>:<id>][&data_class=<label>] |
The raw bytes as the body. 201 with the file; pending until a message posts it, attached on a record |
GET /api/files/limits |
{ max_file_bytes, quota_full } |
GET /api/files/:id |
The file |
GET /api/files/:id/content |
The original |
GET /api/files/:id/preview/thumb, /preview/view |
An image's previews |
GET /api/files/:id/text?from=0&chars=40000 |
Its extracted text, a page at a time |
PATCH /api/files/:id |
{ "data_class": "confidential" } |
DELETE /api/files/:id |
Remove it: 204 |
GET /api/threads/:id/files?kind=&cursor= |
A conversation's files, newest first |
GET /api/brain/collections/:name/records/:id/files |
A record's files |
PUT /api/threads/:id/data-class |
{ "data_class": "restricted" } |
GET, PUT /api/settings/files |
Admins: image reading, the per-file limit, storage used |
POST /api/threads/:id/messages and POST /api/threads/:id/ask take
attachments: [<file id>, ...] (at most 10, each a pending upload of the
caller); the message may then have no text. GET /api/threads/:id carries
files, every attached or removed file of the conversation.
A read token reaches the GET routes; uploading and removing need an
unscoped token, and no token scope reaches the settings. An outside agent
uses the same routes with its agent token and acting_for=<person>, under
the gate.
| Error | When |
|---|---|
404 not_found |
No such file, or one the caller may not see |
410 file_removed |
The file was removed |
413 file_too_large |
Over the per-file limit, with bytes and max_file_bytes |
507 quota_exceeded |
The company's storage is full, with used_bytes and quota_bytes |
422 class_too_low |
A class below what the place needs, without an admin |
422 invalid |
An empty file, no name, or an attachment that is not the caller's pending upload |
409 thread_closed |
Uploading to a closed conversation |
429 rate_limited |
Over 120 uploads in an hour, or too many arriving at once |