Files
List, read, write, upload and download — with an encoding you name rather than one that is guessed.
| Call | What it does |
|---|---|
GET /machines/{id}/files |
list a directory |
GET /machines/{id}/files/content |
read a file, or a byte range |
PUT /machines/{id}/files/content |
write or append |
File access needs no input lease.
Encoding is named, never guessed
const text = await machine.files.read("/etc/hostname"); // utf8
const bytes = await machine.files.read("/tmp/logo.png", { encoding: "base64" });
A utf8 read of bytes that are not UTF-8 is an invalid_request telling you to ask for
base64. It is never silently decoded, because a decode-and-re-encode is exactly where a PNG
turns into a field of replacement characters — and the corruption shows up later, somewhere
else, as a mystery.
Reading
offset and length read a range. The response carries eof, which is what you loop on:
const png = await machine.files.download("/home/agent/logo.png");
download does that loop for you. The guest bounds an inline read, so a large file arrives as
several 4 MB ranges and is reassembled client-side. It ends on the guest’s own eof rather than
a size comparison, so a file that grows under the read does not truncate, and it stops at 64 MB
unless you raise maxBytes.
Writing
await machine.files.write("/home/agent/notes.md", "# notes\n");
await machine.files.write("/home/agent/notes.md", "more\n", { append: true });
await machine.files.upload("/home/agent/logo.png", pngBytes); // base64 on the wire
| Option | Default | Meaning |
|---|---|---|
encoding |
utf8 |
utf8 or base64 |
append |
false |
append rather than replace |
createDirs |
true |
create parent directories as needed |
mode |
— | file mode, e.g. "0644" |
upload is write with base64, which is an exact round trip rather than a guess.
What is refused
The machine is your own computer and the microVM is the boundary, so the default readable area is everything. What is denied is the small set of paths that are not yours: the substrate’s guest credentials, the machine agent’s own, and the host’s control channel into the guest.
await machine.files.read("/etc/machine-agent/credentials.json");
// RasterError: permission_denied
The denylist is fixed and not configurable. The check runs against every form the path takes
— the path as written, each symlink hop, and the fully resolved result — because a dangling link
is still a link (open(2) follows one whether or not it resolves today), and resolution can
rewrite a prefix out of the denylist where a parent directory is itself a link.
This is a property of the guest agent, so it holds however you reach the file: SDK, CLI, MCP tool or a vendor adapter. What it does not do is stop a process inside the machine from reading whatever that process can read — the denylist keeps the control plane’s own credentials out of the API surface, it does not sandbox the machine from itself.
Nothing is logged
File contents, paths, clipboard text, typed text and command lines all pass through the guest agent, and its request logger records route, status, size and duration — never a body, a header or a query parameter. There is an integration test asserting it.