Where the Agent’s Files Actually Live

Cloudflare Computer’s filesystem question is more precise than “where can the agent save a file?” The question is how a pathname becomes durable state that can be read by application code, exposed to an execution backend, and later moved across a container boundary. The answer explains the Workspace’s absolute paths, stream-oriented API, SQLite data model, and the trade-offs that make it suitable for agent-scale working sets rather than every possible filesystem workload.

A useful example is an agent that receives a project, writes notes, changes a configuration file, and produces a report. The agent needs more than a place to put bytes. It needs names that remain stable, metadata that tells tools what each entry is, a way to read or write without loading an entire large file unnecessarily, and a commit boundary that does not leave half a rename or half a file visible. Cloudflare Computer builds those properties in layers.

A filesystem designed for a Durable Object

The Workspace exposes one absolute, POSIX-style path namespace rooted at /. Paths passed to the filesystem are asynchronous and must use forward slashes. /workspace is the conventional location for user data and the default container mount point, but it is not a special host-side directory that appears automatically. An application can create it like any other directory, or use another path when its deployment requires one.

This choice is easy to underestimate. A normal local program can rely on its current working directory and on synchronous filesystem calls. A Durable Object cannot assume that the code using a Workspace shares a process, a disk, or even a runtime with the code that will later execute against it. Absolute paths remove an implicit process-local reference point. Asynchronous methods make the storage boundary visible. The API resembles Node’s fs/promises where that familiarity helps, but it does not claim to be a complete local filesystem.

The difference is clearest in readFile. A binary read returns a stream by default; a caller asks for UTF-8 when it wants a string in memory. A ranged read can begin at a byte offset and limit the number of bytes returned. That default favors the common agent operation of passing a file to another service, a response, or a model-facing tool without first assembling the whole file in one JavaScript object. A small text file can still be read as a string when that is the useful representation.

Writes follow the same principle. The Workspace accepts text, byte arrays, or a readable stream. A request body or an object body from external storage can flow into the Workspace without an intermediate whole-file buffer. The stream is not magic: the system still has to hash its content and record the file’s structure. But the input can be consumed incrementally, which changes the peak memory profile and makes the API fit uploads and generated artifacts more naturally.

The surface also favors a few operations that agents use repeatedly: create directories, list entries, search with a glob, grep text, change mode, remove a path, and rename an output. Errors use familiar POSIX-style codes. A missing path is different from a non-directory parent; a directory is different from a file; deleting the root is forbidden. Those distinctions matter when an agent is deciding whether to seed a default file, retry a cleanup, or report a real storage failure instead of papering over it.

A rename illustrates the value of a real filesystem boundary. An agent can write a new site to a temporary path and then rename it over the published path. The move occurs in one transaction, so a failure cannot leave the entry simultaneously at both locations or copy only part of a directory tree. The operation preserves the moved inode, its bytes, and its mode. This is a stronger primitive than asking an agent to read one file, write another, and delete the first in three unrelated calls.

From a pathname to durable bytes

The Workspace does not store a file as one opaque value attached directly to a string path. It separates the namespace from inode metadata and from content. The separation is what lets a local rename be cheap, lets multiple names refer to one local inode, and lets synchronization reason about file content in pieces.

erDiagram DIR_ENTRY }o--|| INODE : resolves INODE ||--o{ CHUNK_REF : owns CHUNK_REF }o--|| BLOB : points_to INODE }o--o| MANIFEST : may_have TOMBSTONE }o..o{ DIR_ENTRY : describes_missing_path

A directory entry maps a parent inode and a name to a child inode. The inode carries the node type—file, directory, or symbolic link—along with mode, modification time, size, and a revision. Resolving /workspace/reports/today.md therefore means walking names through the directory-entry graph until the filesystem reaches the inode for the final component. The path is the user-facing identity; the inode is the local object that holds its metadata and content relationship.

For a regular file, the content is divided into fixed windows of at most 512 KiB. Each window is hashed with SHA-256 and stored as a content-addressed blob. A separate chunk-reference row says that a particular inode uses a particular hash at a particular index. The bytes are therefore immutable from the perspective of that hash: changing a file creates or reuses blob content and updates the file’s ordered references rather than mutating a blob in place.

A per-file manifest can summarize the ordered list of chunk hashes and sizes. Identical files can share a manifest, just as identical chunks can share blob rows. More importantly, the manifest and chunk references give the synchronization layer a compact answer to a practical question: which pieces of this file does the other side already have? The later transfer protocol can exchange hashes first and bytes only for missing objects.

The cached file size on the inode is another small but meaningful choice. A tool asking for metadata should not have to reconstruct the answer by summing every chunk row each time. The write paths update the size with the mode, modification time, and revision. As a result, stat can answer the ordinary question—how large is this file?—without turning metadata inspection into a content scan.

That structure also makes revisions useful. A mutation receives a monotonically increasing revision, and the live inode or a deletion record carries the revision that describes its latest change. Deleting a path removes its live inode from the namespace, but a tombstone records that the path existed and was deleted. Without that record, an incremental peer would have no way to learn that an entry disappeared rather than simply assuming it had never existed.

The design has a local and a distributed consequence. Locally, an inode graph is a natural representation for renames and hardlinks. On the eventual sync wire, however, a directory move is represented as new live paths and old-path tombstones. Local identity and transfer identity are related but not identical. That distinction is one reason the filesystem model must be understood before the container protocol: the protocol is built around the durable state representation rather than around a fictional stream of POSIX system calls.

Why streams do not mean “unlimited”

Consider a generated CSV that is larger than the model context but still appropriate for a Workspace. A binary read can capture the relevant range and stream it onward. When the caller asks for UTF-8, the selected range is materialized so it can be decoded as one string. The choice is explicit: string convenience spends memory; a binary stream preserves lazy transfer and backpressure.

On a streamed write, the source is read one piece at a time and re-windowed into the fixed chunk size. Each completed window is hashed and staged in the blob store. Only after the source drains does the system commit the inode or dirent changes and the ordered chunk list. The final transaction is short relative to transferring the whole source, and a failure before it completes does not publish a partially described file. Staged blob rows that no live inode references can later be reclaimed by garbage collection.

The stream path still has boundaries. A file must fit the system’s storage limits, and the chunk-reference list grows with file size. The container FUSE path has its own per-file in-memory cap. Streaming prevents an unnecessary whole-file buffer at the API boundary; it does not make a multi-gigabyte data set an agent-scale workload or remove the cost of hashing every new chunk.

Fixed chunking is especially good for edits that stay near the tail of a file. If an agent appends a line to a large log, most earlier windows keep their hashes. If it changes a small region, the affected windows can be replaced while untouched windows remain reusable. The same property helps when two paths contain identical package content: content addressing can store and transfer that repeated content once.

The weakness appears when an edit shifts all later boundaries. Imagine a 50 MB file divided into roughly 100 windows. Prepending one line changes the bytes at the first boundary, then shifts the boundary of every later window. Even if almost all of the logical content is unchanged, the later fixed windows can receive different hashes. The current design then loses much of its deduplication advantage for that edit and may need to transfer the file’s later chunks again. Content-defined chunking is discussed as a possible future improvement, not as a current guarantee.

This is a good example of why “incremental” needs a qualifier. The Workspace is incremental with respect to the representation it actually stores. It can avoid rewriting or transferring unchanged fixed windows, but it cannot infer semantic sameness from two files whose byte boundaries have shifted. The system chooses deterministic fixed windows because they are straightforward to address, read positionally, validate, and synchronize. The cost is less resilience to insertion-heavy edits.

Transactions protect meaning, not every future effect

A local transaction protects the integrity of a filesystem mutation. Creating a file, replacing its chunks, updating its metadata, and advancing its revision are treated as one coherent state change. Renaming a tree moves the namespace relationship without copying each file’s bytes. A deletion records the necessary tombstones as part of the mutation that removes the live entries.

That protection has a precise scope. It does not make an external provider transactional with the Workspace, and it does not make a later container command atomic with the DO-side write that preceded it. It also does not mean a failed agent run rolls back writes that already completed. The transaction ensures that the local SQLite representation is not left in an impossible halfway shape; coordination across runtimes is a separate problem.

The same distinction explains why the Workspace can serve both a filesystem-only application and a container-backed agent. In the filesystem-only case, the SQLite tree is the whole operating surface. In the container case, the tree becomes the durable source that another environment must mirror. The bytes, names, and revisions are already organized for that handoff, but the handoff still has its own acknowledgments and failure modes.

The practical model is therefore neither “a database pretending to be a disk” nor “a disk hidden behind an API.” It is a durable namespace with a content-addressed file representation and explicit boundaries around memory, transactions, and transfer. An agent can use it as a working directory because the API is familiar, but the reasons behind the API are distributed: paths must remain stable across runtimes, bytes may cross a network, and progress must be recoverable.

That last point leads directly to the container question. Once a file is represented as revisions, chunk references, immutable objects, and deletion records, the system has the ingredients to send only the state a peer lacks. The next issue is how Cloudflare Computer orders that exchange, decides when a change is acknowledged, and behaves when the exchange is interrupted.

References