Xeonr Developer Docs

Workspaces

A durable file API over content-addressed object storage — writable before a pod exists, readable after it's gone.

A workspace is the user's files. It is a file API, not blob get/put: real directories, Stat, partial reads and writes, rename, snapshots. Underneath it's a content-addressed blob store over object storage plus a path index, which is what makes CopyFile, MoveFile and SnapshotWorkspace metadata-only operations rather than byte movement.

It is deliberately not a PVC. A workspace outlives any pod, resumes on any node, has no volume-count ceiling, and costs only object bytes while nothing is running.

Lifecycle

const ws = await cp.workspaces.create({
  label: "#general channel",
  fromSnapshot: snapshotId,   // optional: fork a snapshot — metadata-only, instant
});

cp.workspaces.get(id) returns a handle for an id you already hold, with no round trip.

CallNotes
GetWorkspace / ListWorkspacesSize, file count, last update; paginated
UpdateWorkspaceLabel only — everything else is immutable
DeleteWorkspaceIndependent of any sandbox that used it

The file API

Every call works with or without a live sandbox. That's the property that makes "pre-load files → run later → retrieve after" work at all.

CallNotes
StatOne FileInfo: size, mode, is_dir, content sha256, mtime, symlink target
ListFilesDirectory listing, optionally recursive
ReadFileServer stream; offset + length for partial reads (length: 0 = to EOF)
WriteFileClient stream: a WriteHeader then data frames
MakeDirparents: true is mkdir -p
RemoveFileFile or directory (recursive)
CopyFile / MoveFileMetadata-only — no bytes move, however large the file
TruncateSet size
SetAttrMode, mtime, symlink target
ImportZip / ExportZipSeed or retrieve a subtree
CreateUploadPresigned PUT so a browser uploads straight to object storage

Paths are validated on every call: no traversal, namespace-scoped, confined to the workspace.

await ws.files.write("src/main.py", source);          // string, bytes, or a stream
const report = await ws.files.readText("out/report.json");

await ws.files.edit("src/main.py", "3000", "8080");   // one exact occurrence
await ws.files.list("/", { recursive: true });
await ws.files.move("a.txt", "b.txt");
await ws.files.remove("tmp", { recursive: true });

Reads fail at the call for a missing path, not part-way through the body — a lazily-opened stream would let a caller write a 200 before discovering the file was never there. Large bodies stream both ways: ws.files.reader(path) and passing a stream to write.

Write modes are OVERWRITE, APPEND and PATCH_AT_OFFSET (write length bytes starting at offset, leaving the rest intact).

Zip import

Two ways in. Stream the bytes, or hand the server a URL — the console's path, where the browser uploads straight to object storage and never through the API:

const { uploadUrl, objectRef } = await cp.workspaces.createUpload({
  filename: "repo.zip",
  size: bytes.length,
});
await fetch(uploadUrl, { method: "PUT", body: bytes });

await ws.files.importZipFrom(objectRef, { dest: "repo" });

For a zip you already hold, ws.files.importZip(bytes, { dest: "repo" }) streams it directly; ws.files.exportZip("dist") is the way back out.

Entry paths are traversal-checked and expansion is size-capped, so a zip bomb fails the import rather than the platform.

Snapshots

SnapshotWorkspace freezes an immutable, copy-on-write view. It's content-addressed, so it's cheap and dedupes against the workspace it came from.

const snapshot = await ws.snapshot({ label: "v1" });
const frozen = await snapshot.files.readText("src/main.py");

A snapshot handle exposes the read half of the file API and nothing else — snapshot.files.write does not exist, because a snapshot is immutable and that is better enforced by the type than by a runtime error.

Snapshots are useful three ways:

  • Fork or templateCreateWorkspace({ fromSnapshotId }) gives an independent workspace instantly.
  • Read directlyStat, ListFiles and ReadFile accept snapshot_id instead of workspace_id, so a snapshot id plus a path names one set of bytes forever, with no fork.
  • Ship it — an app version mounts a snapshot read-only, and a built environment can use one as its build context.

ListSnapshots and DeleteSnapshot manage them.

Inside a sandbox

Attach a workspace to a sandbox (or a job) and the agent presents it as POSIX /workspace. The workload sees an ordinary directory tree — it never calls the file API, and it holds no credential for it. The agent materialises the tree on start and syncs changes back.

Cache.policy picks the durability/latency trade:

PolicyBehaviour
WRITE_THROUGHChanges flushed promptly — durable, slower
WRITE_BACKFlushed on writeback_sec or on SyncWorkspace — fast, loses un-flushed writes on a hard kill

While a sandbox is attached, the live copy is authoritative: file-API calls are routed through that sandbox's agent, so a WriteFile lands in the tree the process sees and a ReadFile reflects writes that haven't been flushed. Same call, same result — the caller doesn't choose, and there is no split-brain against the write-back cache.

SyncWorkspace forces a flush and reports conflicts — paths that changed on both sides since the last sync. The live container wins; the overwritten remote change is named so you can reconcile it.

Sizing

Cold start is proportional to file count (the agent fetches per file) and the periodic scan re-reads the tree, so the comfortable shape today is source trees and artifacts — thousands of files, not hundreds of thousands. Keep node_modules-scale dependency trees in the environment image, where they're layer-cached and shared, rather than in the workspace. That split is the intended one: environment for deps, workspace for your files.

On this page