Core Package
Use @rivet-dev/agentos-core standalone for direct VM control without the Rivet Actor runtime.
agentOS vs agentOS Core
The agentOS() actor (from @rivet-dev/agentos) wraps the core package and adds:
Core (@rivet-dev/agentos-core) | Actor (@rivet-dev/agentos) | |
|---|---|---|
| Persistence | In-memory by default (pluggable via mounts) | Persistent filesystem and sessions |
| Distributed state | Manage yourself | Built-in distributed statefulness |
| Stateful VMs | Complex to run yourself | Built into Rivet |
| Sleep/wake | Manual dispose() / create() | Automatic |
| Events | Direct callbacks | Broadcasted to all connected clients |
| Preview URLs | None | Built-in signed URL server |
| Multiplayer | N/A | Multiple clients on same actor |
| Orchestration | N/A | Workflows, queues, cron |
| Agent-to-agent communication | Custom | Built into Rivet Actors |
| Authentication | Set up yourself | Documentation |
We recommend using Rivet Actors because they provide a portable way to run agentOS() on any infrastructure with built-in persistence, networking, and orchestration. Use the core package if you need the most bare-bones implementation possible.
Install
npm install @rivet-dev/agentos-core
Boot a VM
Define the actor on the server:
import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "./software/pi";
const vm = agentOS({
software: [pi],
});
export const registry = setup({ use: { vm } });
registry.start();
Then drive it from a typed client:
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";
const client = createClient<typeof registry>({ endpoint: "http://localhost:6420" });
const handle = client.vm.getOrCreate("my-agent");
const result = await handle.exec("echo hello");
console.log(result.stdout); // "hello\n"
Sidecar process
Every VM runs inside a shared sidecar process rather than a process of its own. By default all VMs are tenants of a single, process-global sidecar (the default pool), so each additional VM only adds its marginal cost — a V8 isolate plus its kernel state — instead of a whole OS process. This is what keeps per-VM memory in the tens of MB and warm VM creation in the single-digit milliseconds (see Benchmarks).
This is automatic — agentOS() and AgentOs.create() use the shared default sidecar with no configuration, and the same applies to Rivet Actors (each actor’s VM is a tenant of the shared process). Disposing a VM tears down only that VM; the shared sidecar process is reused across VMs and stays alive for the lifetime of the host process.
For advanced cases the core package exposes explicit sidecar handles so you can isolate a group of VMs in their own process:
import { AgentOs } from "@rivet-dev/agentos-core";
// One dedicated sidecar process hosting multiple VMs.
const sidecar = await AgentOs.createSidecar();
const a = await AgentOs.create({ sidecar: { kind: "explicit", handle: sidecar } });
const b = await AgentOs.create({ sidecar: { kind: "explicit", handle: sidecar } });
await a.dispose(); // tears down VM a only
await b.dispose();
await sidecar.dispose(); // tears down the shared process
Filesystem
await handle.writeFile("/home/agentos/hello.txt", "Hello, world!");
const content = await handle.readFile("/home/agentos/hello.txt");
console.log(new TextDecoder().decode(content));
await handle.mkdir("/home/agentos/src");
await handle.writeFiles([
{ path: "/home/agentos/src/index.ts", content: "console.log('hi');" },
{ path: "/home/agentos/src/utils.ts", content: "export const add = (a: number, b: number) => a + b;" },
]);
const entries = await handle.readdirRecursive("/home/agentos");
for (const entry of entries) {
console.log(entry.type, entry.path);
}
Processes
Long-running process output is delivered over the live processOutput / processExit events on a connection rather than per-pid callbacks:
// One-shot execution
const result = await handle.exec("ls -la /home/agentos");
console.log(result.stdout);
// Long-running process with streaming output
await handle.writeFile(
"/tmp/server.mjs",
'import http from "http"; http.createServer((req, res) => res.end("ok")).listen(3000); console.log("listening");',
);
const { pid } = await handle.spawn("node", ["/tmp/server.mjs"]);
const conn = handle.connect();
conn.on("processOutput", (data) => {
if (data.pid === pid && data.stream === "stdout") {
console.log("stdout:", new TextDecoder().decode(data.data));
}
});
conn.on("processExit", (data) => {
if (data.pid === pid) console.log("exited:", data.exitCode);
});
// Write to stdin
await handle.writeProcessStdin(pid, "some input\n");
// Stop or kill
await handle.stopProcess(pid);
Agent sessions
createSession returns a session record. All session operations take its sessionId. Session events and permission requests are delivered over the live connection (sessionEvent / permissionRequest):
const conn = handle.connect();
// Stream session events (each event is a JSON-RPC notification)
conn.on("sessionEvent", (data) => {
console.log(data.sessionId, data.event.method, data.event.params);
});
// Observe permission requests from the agent
conn.on("permissionRequest", (data) => {
console.log("Permission:", data.sessionId, data.request.description);
});
// createSession() resolves to the session ID string.
const sessionId = await handle.createSession("pi", {
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
// Send a prompt. sendPrompt() resolves to { response, text }, where `text` is
// the accumulated agent message text and `response` is the raw JSON-RPC response.
const { text } = await handle.sendPrompt(sessionId, "Write a hello world script");
console.log(text);
await handle.closeSession(sessionId);
Subscribe to sessionEvent before sending a prompt so you do not miss the live stream. Persisted history can be read back later with getSessionEvents().
Networking
// Start a server inside the VM
await handle.writeFile(
"/tmp/app.mjs",
'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);',
);
await handle.spawn("node", ["/tmp/app.mjs"]);
// Fetch from it
const response = await handle.vmFetch(3000, "/");
console.log(new TextDecoder().decode(response.body));
Cron jobs
Cron jobs run an "exec" command or a "session" prompt on a schedule. Fired jobs are surfaced over the live cronEvent connection:
const { id } = await handle.scheduleCron({
id: "cleanup",
schedule: "0 * * * *",
action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache"] },
});
console.log("Scheduled:", id);
// Run an agent session on a schedule
await handle.scheduleCron({
schedule: "0 9 * * *",
action: {
type: "session",
agentType: "pi",
prompt: "Review the logs and summarize any errors",
cwd: "/workspace",
},
});
const conn = handle.connect();
conn.on("cronEvent", (data) => {
console.log("Cron event:", data.event.type, data.event.jobId);
});
console.log(await handle.listCronJobs());
Mounts
Configure filesystem backends at boot time.
Native mount plugins (host directories, S3, etc.) are passed via plugin, each
identified by an id and a config object.
import { agentOS, setup } from "@rivet-dev/agentos";
const vm = agentOS({
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/" } },
},
],
});
export const registry = setup({ use: { vm } });
registry.start();
agentOS() configuration reference
When you use the agentOS() actor, all VM configuration is passed to the factory as a single flat object. This is the consolidated config block to copy and adapt:
import { agentOS, nodeModulesMount, setup } from "@rivet-dev/agentos";
import pi from "./software/pi";
const vm = agentOS({
// Filesystems to mount at boot. Use nodeModulesMount() to expose a host
// node_modules tree at /root/node_modules.
mounts: [nodeModulesMount("/path/to/project/node_modules")],
// Software packages to install in the VM (see /docs/software)
software: [pi],
// Ports exempt from SSRF checks
loopbackExemptPorts: [3000],
// Extra instructions appended to agent system prompts
additionalInstructions: "Always write tests first.",
// Preview URL token lifetimes
preview: {
defaultExpiresInSeconds: 3600, // 1 hour (default)
maxExpiresInSeconds: 86400, // 24 hours (default)
},
// Lifecycle hooks (see below)
onSessionEvent: async (sessionId, event) => {
console.log("Session event:", sessionId, event.method);
},
onPermissionRequest: async (sessionId, request) => {
console.log("Permission request:", sessionId, request.permissionId);
},
});
export const registry = setup({ use: { vm } });
registry.start();
The top-level fields are documented inline above. See Mounts, Software, and (for the hooks) Approvals.
Lifecycle hooks
onPermissionRequest(sessionId, request) fires when an agent requests permission. onSessionEvent(sessionId, event) is a server-side hook called once for every session event: unlike the client-side sessionEvent connection subscription, it runs in the actor for every event regardless of connected clients, making it the place for server-side logging, persistence, or side effects.
import { agentOS } from "@rivet-dev/agentos";
export const vm = agentOS({
// Runs once per session event, server-side, for every session.
onSessionEvent: async (sessionId, event) => {
console.log("Session event:", sessionId, event.method);
},
});
Timeouts
| Setting | Default | Description |
|---|---|---|
| Action timeout | 15 minutes | Maximum time for any single action |
| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |
These are set internally by the agentOS() factory and cannot be overridden per-call. See Persistence & Sleep for details on the sleep lifecycle.