Primitives

Filesystem

Named virtual filesystems with glob, full-text search and directory forks that share large files.

A filesystem is a virtual workspace inside a group. A group can hold as many as you need, each with its own immutable id and a name: a workspace for the agent, a docs corpus, an uploads area. Read and write them through the SDK, REST or MCP, search them with globs and full-text queries, fork a directory in one call for a sub-agent, and snapshot, fork or pause each filesystem on its own.

Creating a filesystem

const workspace = await group.fs.create('workspace');  // 409 already_exists if taken; workspace.id is its id
const docs = group.fs('docs');                          // by name in the group, resolved on first use
const same = p.fs(workspace.id);                        // the same filesystem, by id
workspace = await group.fs.create("workspace")
same = p.fs(workspace.id)

From the CLI: primitives primitive create workspace --type fs --group agent-42 (prints the id).

Reading and writing

fs.ts
await workspace.write('/repo/plan.md', plan);
const text = await workspace.readText('/repo/plan.md');
const bytes = await workspace.read('/repo/logo.png');        // Uint8Array

const entries = await workspace.list('/repo');                // directories first, then by path
const info = await workspace.stat('/repo/plan.md');           // { path, kind, size, hash, mode, mtime }
fs.py
await workspace.write("/repo/plan.md", plan)
text = await workspace.read_text("/repo/plan.md")
entries = await workspace.list("/repo", recursive=True)
MethodDescription
write(path, content, { mode? })Create or replace a file. Missing parent directories are created. mode defaults to the existing file's, else 0o644.
read(path) / readText(path)A file's bytes, or its contents as UTF-8 text.
list(dir = '/', { recursive? })Entries of a directory, or all descendants with recursive.
stat(path)Metadata of a file or directory.
mkdir(path)Create a directory and its parents. Idempotent.
move(from, to, { overwrite? })Rename a file or a directory with its subtree.
remove(path, { recursive? })Delete a file, or a directory. A non-empty directory needs recursive.

Each entry is { path, kind: 'file' | 'dir', size, hash, mode, mtime }, where hash is the BLAKE3 hash of a file's contents and mtime is in milliseconds.

Paths are absolute and normalized: relative paths start at /, and ., .. and empty segments are rejected. Paths are at most 1,024 bytes and files at most 32 MiB. Writing over a directory, or a file where a directory is expected, fails with 409 fs_conflict.

const files = await workspace.glob('/repo/src/**/*.ts');          // ['/repo/src/index.ts', …]
const hits = await workspace.search('lease expired', { path: '/repo' });
// [{ path: '/repo/src/queue.ts', snippet: '…returns **lease** **expired**…' }]
  • glob(pattern, { limit? }) matches absolute paths. * stays within one path segment, ** crosses segments, and ?, [ab] and {a,b} work as usual.
  • search(query, { path?, limit? }) is full-text search over text files up to 1 MiB, best matches first. Every word in the query must match. Matched terms in snippet are wrapped in **.

Both return 50 results by default and at most 1,000.

Directory forks

forkDir copies a directory and its subtree to a new path for a sub-agent. Files over 64 KiB share their contents with the source instead of being copied, so forking a large repository is cheap:

const fork = await workspace.forkDir({ from: '/repo' });            // { path: '/repo-fork-x1y2z3', files: 128 }
await workspace.forkDir({ from: '/repo', to: '/scratch/attempt-1' });
fork = await workspace.fork_dir("/repo")
await workspace.move(fork["path"], "/scratch/attempt-1")

The fork is an ordinary directory: work in it, then move the files you want to keep and remove the rest. Forking onto an existing path fails with 409 fs_conflict.

Snapshots, forks and pause

Each filesystem has its own lifecycle, independent of the other primitives in the group:

await workspace.snapshot('before-refactor');                        // this filesystem only
await workspace.restore('before-refactor');
const attempt = await workspace.fork({ name: 'workspace-try' });    // a whole new filesystem, new id
const sub = await workspace.fork({ group: 'agent-42-sub' });        // into another group; 404 not_found if it doesn't exist
await workspace.pause();                                            // data calls fail with 423 paused
await workspace.resume();

A filesystem fork shares files over 64 KiB with its source instead of copying them; smaller files are copied. Use forkDir to try something inside one filesystem, and fork to hand a separate filesystem to another agent. See Groups.

Mounts

Coming soon: mounting a group's filesystem into a sandbox or container over FUSE or NFS. Today, sandboxes reach the filesystem through the REST API, the SDKs or MCP. See Use with a sandbox.

Over REST

Every filesystem endpoint lives under the filesystem's id, as returned by POST /v1/primitives:

curl -X PUT http://localhost:7777/v1/primitives/$ID/files/repo/plan.md \
  --data-binary @plan.md
curl http://localhost:7777/v1/primitives/$ID/files/repo/plan.md

A GET on a file returns its raw bytes with X-Primitives-Hash, X-Primitives-Size and X-Primitives-Mtime headers. On a directory, or with ?list=1, it returns { "data": [entries] }. Add ?stat=1 for metadata only. Set X-Primitives-Mode: 755 on a PUT to choose permission bits. All endpoints are in the REST API reference.

MCP tools

ToolRead-onlyArguments
fs_readyesprimitive?, path, list?, recursive?
fs_writenoprimitive?; action: write (default), move, delete or mkdir; path; content?, encoding? (utf8 or base64), to?, recursive?
fs_searchyesprimitive?; glob, or query with path?; limit?
fs_forknoprimitive?, from, to?

primitive is the filesystem to use: its id or its name from primitive_list. It can be left out when the group has exactly one filesystem. fs_read returns at most 256 KiB of a file's content and sets truncated beyond that. Binary files come back base64-encoded.