Skip to main content
Operating System

Filesystem

Read, write, mount, and manage files inside agentOS, all backed by a virtual filesystem isolated from the host disk.

Each VM has its own filesystem that the agent works in. Guest fs calls never touch the host disk, and it persists automatically across sleep/wake with no setup. See Persistence for the details.

Mounts

Back a guest path with external storage by adding it to the mounts config. Each mount takes a path and an optional readOnly flag, and the guest only ever sees the mounted subtree, never the wider host.

Host directory

Project a real host directory into the filesystem, Docker-style. The guest sees only the mounted subtree, never the wider host filesystem. Path-escape attempts (symlinks, .., path aliasing) are confined to the mount root.

import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({
  software: [pi],
  mounts: [
    {
      path: "/mnt/code",
      plugin: { id: "host_dir", config: { hostPath: "/path/to/repo" } },
      readOnly: true,
    },
  ],
});

export const registry = setup({ use: { vm } });
registry.start();

S3

Mount an S3 bucket with the built-in s3 plugin. Pass an optional prefix to scope storage to a key path within the bucket, useful for sharing one bucket across multiple agents.

The backend is a block store, not a one-object-per-file mapping: file contents are split into fixed-size chunks (4 MB by default) stored as individual S3 objects, with a separate metadata layer mapping each file to its chunks. This keeps large files, partial reads and writes, and snapshots efficient without rewriting whole objects.

import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({
  software: [pi],
  mounts: [
    {
      path: "/mnt/data",
      plugin: {
        id: "s3",
        config: {
          bucket: "my-bucket",
          prefix: "agent-data/",
          region: "us-east-1",
        },
      },
    },
  ],
});

export const registry = setup({ use: { vm } });
registry.start();

The s3 plugin config also accepts credentials ({ accessKeyId, secretAccessKey }) and a custom endpoint for S3-compatible providers.

Google Drive

Mount a Google Drive folder with the built-in google_drive plugin.

import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({
  software: [pi],
  mounts: [
    {
      path: "/mnt/drive",
      plugin: {
        id: "google_drive",
        config: {
          credentials: {
            clientEmail: process.env.GOOGLE_DRIVE_CLIENT_EMAIL!,
            privateKey: process.env.GOOGLE_DRIVE_PRIVATE_KEY!,
          },
          folderId: process.env.GOOGLE_DRIVE_FOLDER_ID!,
        },
      },
    },
  ],
});

export const registry = setup({ use: { vm } });
registry.start();

In-memory

Use the built-in memory plugin for an ephemeral mounted directory in the RivetKit agentOS() actor.

import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({
  software: [pi],
  mounts: [
    {
      path: "/home/agentos/scratch",
      plugin: { id: "memory", config: {} },
    },
  ],
});

export const registry = setup({ use: { vm } });
registry.start();

Dynamic mount

Use mountFs() with a serializable, sidecar-owned plugin descriptor. The same descriptor works through Core and RivetKit; the actor persists dynamic descriptors in SQLite and replays them on wake. mountFs() resolves only once the mount is visible to guest code, and unmountFs(path) removes it.

import { AgentOs } from "@rivet-dev/agentos-core";

const vm = await AgentOs.create({ defaultSoftware: false });
await vm.filesystem.mount({
	path: "/home/agentos/scratch",
	plugin: { id: "memory", config: {} },
});
await vm.filesystem.writeFile("/home/agentos/scratch/hello.txt", "hello");

The actor’s durable root is handled separately: the sidecar connects directly to Rivet’s actor SQLite UDS, so root filesystem reads and writes never pass through JavaScript. listMounts() returns live sanitized metadata without plugin configuration.

File operations

These operations are primarily what the agent uses inside the VM, and are also available from the client to seed inputs and read results. For large or read-only inputs (a repo, a dataset), a read-only host mount is faster than copying files in. Programs that need stdin or live output use exec instead (see Processes & Shell).

Read and write

import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const agent = client.vm.getOrCreate("my-agent");

// Write a file (string or Uint8Array)
await agent.filesystem.writeFile("/home/agentos/hello.txt", "Hello, world!");

// Read a file (returns Uint8Array)
const content = await agent.filesystem.readFile("/home/agentos/hello.txt");
console.log(new TextDecoder().decode(content));

Batch read and write

// Batch write (creates parent directories automatically)
const writeResults = await agent.filesystem.writeFiles([
  { path: "/home/agentos/src/index.ts", content: "console.log('hello');" },
  { path: "/home/agentos/src/utils.ts", content: "export function add(a: number, b: number) { return a + b; }" },
]);

// Batch read
const readResults = await agent.filesystem.readFiles([
  "/home/agentos/src/index.ts",
  "/home/agentos/src/utils.ts",
]);
for (const result of readResults) {
  console.log(result.path, new TextDecoder().decode(result.content ?? new Uint8Array()));
}

Directories

// Create a directory
await agent.filesystem.mkdir("/home/agentos/projects");

// List directory contents
const entries = await agent.filesystem.readdir("/home/agentos/projects");

// Recursive listing (entries carry path, type, and size)
const tree = await agent.filesystem.readdirRecursive("/home/agentos");
for (const entry of tree) {
  const name = entry.path.split("/").pop() ?? entry.path;
  console.log(entry.type, entry.path, name);
}

File metadata

// Check if a path exists
const fileExists = await agent.filesystem.exists("/home/agentos/hello.txt");

// Get file metadata
const info = await agent.filesystem.stat("/home/agentos/hello.txt");
console.log(info.size, info.isDirectory, info.mtimeMs);

Move and delete

// Move/rename
await agent.filesystem.move("/home/agentos/old.txt", "/home/agentos/new.txt");

// Delete a file
await agent.filesystem.remove("/home/agentos/new.txt");

// Delete a directory recursively
await agent.filesystem.remove("/home/agentos/temp", { recursive: true });

Permissions

Filesystem access is governed by the VM permission policy. The filesystem scope is granted by default; restrict it by path, for example to deny a sensitive directory:

// Allow the filesystem everywhere, but deny anything under /home/agentos/vault.
const denyVault = {
	fs: {
		default: "allow",
		rules: [{ mode: "deny", operations: ["*"], paths: ["/home/agentos/vault/**"] }],
	},
} satisfies Permissions;

See Permissions for the full configuration.

Sandboxes

For heavier workloads, run a full Linux external sandbox alongside the VM and mount its filesystem into agentOS. The agent then reads and writes the sandbox’s files through the same fs APIs while the sandbox handles execution.

Default layout

With no mounts configured, every VM boots an Alpine-based root filesystem with the standard POSIX directories:

  • /home/agentos: the agent’s home directory ($HOME) and default working directory (pwd) when spawned, where it reads and writes (mounts land under it, e.g. /home/agentos/data).
  • /bin, /sbin, /usr: installed commands (common POSIX utilities by default, plus any software you add).
  • /etc, /lib, /opt, /root, /run, /srv, /tmp, /var, /mnt: standard system paths.

It is backed by the VM’s own filesystem and persisted across sleep/wake. Nothing comes from or touches the host disk.

Embedded API

Filesystem methods and mount descriptors are identical. Configure database when the root filesystem must survive dispose(); mounts persist through their own backends.

import { mkdirSync } from "node:fs";
import { resolve } from "node:path";
import { AgentOs, type AgentOsOptions } from "@rivet-dev/agentos-core";

mkdirSync(".agentos", { recursive: true });
const options = {
	database: {
		type: "sqlite_file",
		path: resolve(".agentos/agentos.sqlite"),
	},
} satisfies AgentOsOptions;

let vm = await AgentOs.create(options);
await vm.filesystem.writeFile("/home/agentos/result.txt", "persistent");
await vm.dispose();

vm = await AgentOs.create(options);
const result = await vm.filesystem.readFile("/home/agentos/result.txt");
console.log(new TextDecoder().decode(result));
await vm.dispose();
import { AgentOs } from "@rivet-dev/agentos-core";

// Embedded applications configure filesystem backends at boot. Native mount plugins (host
// directories, S3, etc.) are passed via `plugin`, each identified by an `id`
// and a `config` object.
const vm = await AgentOs.create({
	mounts: [
		// Host directory (read-only)
		{
			path: "/mnt/code",
			plugin: { id: "host_dir", config: { hostPath: "/path/to/repo" } },
			readOnly: true,
		},
		// S3 bucket
		{
			path: "/mnt/data",
			plugin: { id: "s3", config: { bucket: "my-bucket", prefix: "agent/" } },
		},
	],
});

Read more in the embedded API quickstart.