Xeonr Developer Docs

Container files

Read and write a running workload's own filesystem — the whole container, not the workspace mounted into it.

FilesystemService is a file API over a running workload's own container filesystem. Where Workspaces give you durable storage that outlives every pod, this gives you the container as it exists right now: what the image actually shipped, what the workload wrote outside its mounts, a log under /var/log after a job failed.

const sandbox = await cp.sandboxes.get(id);

await sandbox.fs.readText("/etc/os-release");     // what the image shipped
await sandbox.fs.list("/var/log");                // what the workload left behind
await sandbox.fs.write("/tmp/config.json", body); // writable, because /tmp is a mount

.fs and .files are not the same thing

They share verbs and sit next to each other, so the distinction is worth stating plainly:

sandbox.filessandbox.fs
What it addressesThe durable workspace mounted into the sandboxThe whole container filesystem
LifetimeOutlives every pod; snapshottableGone when the pod is
copy / moveMetadata-only — no bytes moveReal bytes; a move across filesystems is not atomic
Backed byContent-addressed object storageThe container's actual filesystem
sha256 on statPopulated — it's the addressing keyEmpty; hashing would put a full read in every listing

Reach for .fs when you want something that is not in a workspace. Reach for .files for anything you want to keep.

An absolute path does not mean the container's root

Both APIs take absolute-looking paths, and neither rejects the other's. What differs is what the leading / is relative to:

await sandbox.files.write("/tmp/x.txt", body);  // -> /workspace/tmp/x.txt
await sandbox.fs.write("/tmp/x.txt", body);     // -> /tmp/x.txt

A workspace is its own tree. / means the root of the workspace, which the container sees at its mount path — so files.write("/tmp/x.txt") quietly creates a tmp/ directory inside the workspace rather than writing to the container's /tmp. It does not error, and the matching files.read("/tmp/x.txt") succeeds, so nothing looks wrong until something else goes looking in the real /tmp and finds nothing.

The same applies to reading a file a command produced. A bash or python step runs in the container, so a script writing to /tmp/chart.png has written to the container's /tmp. Reading it back needs fs.read("/tmp/chart.png"); files.read("/tmp/chart.png") looks for <workspace>/tmp/chart.png and reports not-found.

Rule of thumb: if a command produced it, .fs reads it; if you want it to survive the pod, put it in .files. Paths under the mount point (/workspace/...) are the one place both agree, because there they genuinely are the same bytes.

Writes usually fail, and that is deliberate

The platform's hardened default is a read-only root filesystem. That means:

  • Reads cover the whole tree.
  • Writes land only in mounted volumes — /tmp, and any workspace mounts — and fail everywhere else with FAILED_PRECONDITION and a message naming EROFS.

This is the isolation posture working, not a fault. If a workload genuinely needs a writable root, register its environment with writable_rootfs — it is a per-environment decision, made once, rather than a per-call flag.

EROFS is deliberately kept distinguishable from a permission failure. "Nothing may write here" and "you may not write here" have different fixes, and an API that collapsed them would send you looking at your role when the answer is the environment.

Permissions

Two layers, and the second is why there is no third:

  1. Namespace RBAC decides whether you may address the workload at all — VIEWER to read, OPERATOR to write.
  2. POSIX, as the workload's own user. The handler runs inside the container as the workload's user, so the container's own file permissions bound everything past that. You can never read or write anything the workload itself could not.

No path allowlist is needed to make that true, and none is offered. One exception: /.cp is not visible — it holds the platform's own staged binaries, not part of your container, and it reports as absent rather than forbidden.

A workspace-scoped credential (the key a workload's own agent holds) is refused outright. It is granted for specific trees, and a whole container filesystem is more than that.

Sandboxes and jobs

A sandbox answers whenever it is running. Nothing to opt into.

A job must ask for it, and only answers while it runs:

const outcome = await cp.jobs.run({
  environment: envId,
  argv: ["./build.sh"],
  filesystemApi: true,       // forces the sidecar + supervisor this needs
});

// while it is running, from another task:
await cp.jobs.fs(jobId).readText("/work/build.log");

Two things follow from a job's pod being torn down at completion:

  • The API is reachable only in RUNNING. Calling it on a finished job returns FAILED_PRECONDITION, not an empty result.
  • It does not replace outputs. Files that must survive the run belong in an output spec, which is collected before the pod goes away.

filesystemApi is opt-in because serving it forces the agent sidecar onto a job that may need no egress, no workspace and no outputs, and runs the workload under the supervisor. Most jobs need neither and should not pay for both.

Apps are not addressable. An app's replicas are interchangeable and individually disposable, so a file API aimed at one would write to a pod that may vanish mid-call and read state that differs between replicas.

The calls

CallNotes
StatOne FileInfo. Symlinks are reported as symlinks, never followed
ListFilesDirectory listing, optionally recursive. Unreadable subtrees are skipped, not fatal
ReadFileServer stream; offset + length for partial reads
WriteFileClient stream: a header then data frames. OVERWRITE, APPEND, PATCH_AT_OFFSET
MakeDirparents: true is mkdir -p
RemoveFileFile or directory (recursive). Removing / is refused
CopyFileReal bytes. Directories recurse; symlinks stay symlinks
MoveFileA rename where it can be; a copy+unlink across filesystems, which is not atomic
TruncateSet size
SetAttrMode, mtime, symlink target. Only the fields you pass are applied
ImportZipUnpack into the container. Entries are clamped to the destination — ../ cannot escape it
ExportZipA subtree as a zip, behind a byte ceiling

ExportZip refuses rather than truncates: a subtree over the ceiling raises RESOURCE_EXHAUSTED before a byte is written, because a half archive that looks whole is the worse failure. exportZip("/") on a real rootfs will refuse — pass a subtree.

There is no fresh option here. sandbox.files has one because /workspace is a materialised copy reconciled on a tick; this talks to the live container synchronously, so there is nothing to flush first.

From the console

The sandbox Files tab browses every filesystem the sandbox has — each workspace mount, and the container itself — one tree at a time. Selecting a text file opens it in an editor; anything binary, or over 2 MiB, offers a download instead. Files can be uploaded into the directory you are looking at.

Whether you can save depends on where you are:

  • A workspace mount — yes, it's durable storage.
  • A snapshot mount — no, and the Save control is absent rather than disabled: a snapshot is immutable by nature, not by permission.
  • The container — wherever the container itself allows it. /tmp and the mounts work; most of the rest returns the read-only-filesystem error above. The console lets the attempt happen and shows what the kernel said, rather than guessing which paths are writable — that depends on the mounts and on writable_rootfs, and guessing wrong would hide an upload that would have worked.

Errors

CodeMeans
NOT_FOUNDNo such path — or a hidden one
ALREADY_EXISTSDestination exists and overwrite was not set
PERMISSION_DENIEDThe workload's user may not do this
FAILED_PRECONDITIONRead-only filesystem (EROFS), not a directory, directory not empty
RESOURCE_EXHAUSTEDOut of space, or an export over the ceiling
UNAVAILABLEThe handler is not up — a job that did not set filesystemApi

On this page