From f607add36e3754b98a58e5d601ffcf030310f6d3 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 23 Jun 2026 20:10:18 -0700 Subject: [PATCH 1/2] feat: native options schema + custom software/agents docs - Native AgentOs options schema (NativeAgentOsOptions) across core/agentos + crates - Re-export defineSoftware from @rivet-dev/agentos - Docs: Reference > Custom Software (Definition, Building Binaries) + Custom Agents guide; onAgentStderr (agent logs) in Sessions --- README.md | 8 +- crates/agentos-actor-plugin/src/config.rs | 5 + crates/client/src/config.rs | 3 + packages/agentos/src/actor.ts | 14 +- packages/agentos/src/config.ts | 57 +++- packages/agentos/src/index.ts | 27 +- packages/agentos/tests/actor.test.ts | 71 ++++- packages/core/package.json | 6 +- packages/core/src/agent-os.ts | 10 + packages/core/src/index.ts | 14 + packages/core/src/options-schema.ts | 296 ++++++++++++++++++ packages/core/tests/options-schema.test.ts | 22 ++ pnpm-lock.yaml | 6 +- website/docs.config.mjs | 9 + .../src/content/docs/docs/agents/custom.mdx | 152 +++++++++ website/src/content/docs/docs/agents/pi.mdx | 4 + .../src/content/docs/docs/architecture.mdx | 2 +- website/src/content/docs/docs/bindings.mdx | 2 + .../docs/custom-software/building-wasm.mdx | 86 +++++ .../docs/docs/custom-software/definition.mdx | 156 +++++++++ website/src/content/docs/docs/sessions.mdx | 25 ++ website/src/content/docs/docs/software.mdx | 4 +- .../src/content/docs/docs/system-prompt.mdx | 10 +- 23 files changed, 953 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/options-schema.ts create mode 100644 packages/core/tests/options-schema.test.ts create mode 100644 website/src/content/docs/docs/agents/custom.mdx create mode 100644 website/src/content/docs/docs/custom-software/building-wasm.mdx create mode 100644 website/src/content/docs/docs/custom-software/definition.mdx diff --git a/README.md b/README.md index 5a23aac33..a3b25e399 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ ## Why agentOS - **Runs inside your process**: No VMs to boot, no containers to pull. Agents start in milliseconds with minimal memory overhead. -- **Embeds in your backend**: Agents call your functions directly via [host tools](https://rivet.dev/docs/agent-os/tools). No network hops, no complex auth between services. +- **Embeds in your backend**: Agents call your functions directly via [bindings](https://agentos-sdk.dev/docs/bindings). No network hops, no complex auth between services. - **Granular security**: Deny-by-default permissions for filesystem, network, and process access. The same isolation technology trusted by browsers worldwide. - **Deploy anywhere**: Just an npm package. Works on your laptop, Rivet Cloud, Railway, Vercel, Kubernetes, or any container platform. - **Open source**: Apache 2.0 licensed. Self-host or use [Rivet Cloud](https://rivet.dev/docs/agent-os/deployment) for managed infrastructure. ### agentOS vs Sandbox -agentOS is a lightweight VM that runs inside your process. Sandboxes are full Linux environments. agentOS integrates agents into your backend with [host tools](https://rivet.dev/docs/agent-os/tools) and granular permissions. Sandboxes give you a full OS for browsers, native binaries, and dev servers. +agentOS is a lightweight VM that runs inside your process. Sandboxes are full Linux environments. agentOS integrates agents into your backend with [bindings](https://agentos-sdk.dev/docs/bindings) and granular permissions. Sandboxes give you a full OS for browsers, native binaries, and dev servers. You don't have to choose: agentOS works with sandboxes through the [sandbox extension](https://rivet.dev/docs/agent-os/sandbox), spinning up a full sandbox on demand and mounting the sandbox's file system when the workload needs it. @@ -114,13 +114,13 @@ All benchmarks compare agentOS against the fastest/cheapest mainstream sandbox p ### Infrastructure - **[Mount external storage as a filesystem](https://rivet.dev/docs/agent-os/filesystem)**: S3-compatible storage, Google Drive, host directories, overlay filesystems, or custom backends -- **[Host tools](https://rivet.dev/docs/agent-os/tools)**: Define JavaScript functions that agents call as CLI commands inside the VM +- **[Bindings](https://agentos-sdk.dev/docs/bindings)**: Define JavaScript functions that agents call as CLI commands inside the VM - **[Cron](https://rivet.dev/docs/agent-os/cron), [webhooks](https://rivet.dev/docs/agent-os/webhooks), and [queues](https://rivet.dev/docs/agent-os/queues)**: Schedule tasks, receive external events, and serialize work with built-in primitives - **[Sandbox extension](https://rivet.dev/docs/agent-os/sandbox)**: Pair with full sandboxes (E2B, Daytona, etc.) for heavy workloads like browsers or native compilation ### Orchestration - **[Multiplayer](https://rivet.dev/docs/agent-os/multiplayer)**: Multiple clients observe and collaborate with the same agent in real time -- **[Agent-to-agent](https://rivet.dev/docs/agent-os/agent-to-agent)**: Agents delegate work to other agents through host-defined tools +- **[Agent-to-agent](https://rivet.dev/docs/agent-os/agent-to-agent)**: Agents delegate work to other agents through host-defined bindings - **[Workflows](https://rivet.dev/docs/agent-os/workflows)**: Chain agent tasks into durable workflows with retries, branching, and resumable execution - **[Authentication](https://rivet.dev/docs/agent-os/authentication)**: Integrate with your existing auth model (API keys, OAuth, JWTs) diff --git a/crates/agentos-actor-plugin/src/config.rs b/crates/agentos-actor-plugin/src/config.rs index 08c2998cc..78ac30e88 100644 --- a/crates/agentos-actor-plugin/src/config.rs +++ b/crates/agentos-actor-plugin/src/config.rs @@ -17,6 +17,11 @@ use anyhow::{Context, Result}; /// Serializable mirror of [`AgentOsConfig`]. `deny_unknown_fields` enforces /// fail-loud behavior when callers pass fields outside this allow-list /// (including non-serializable fields like `schedule_driver`). +/// +/// Keep this struct in sync with +/// `packages/agentos/src/config.ts::nativeAgentOsOptionsSchema` and +/// `packages/agentos/src/actor.ts::buildConfigJson`; TS preflight validation +/// should reject the same native-boundary fields before this serde guard runs. #[derive(serde::Deserialize, Default, Clone)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub(crate) struct AgentOsConfigJson { diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 7a61f6f7d..fb8ddee43 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -15,6 +15,9 @@ use serde::{Deserialize, Serialize}; use crate::fs::VirtualFileSystem; /// Resolved client options (= TS `AgentOsOptions`). All fields optional with documented defaults. +/// +/// Keep this Rust mirror in sync with `packages/core/src/agent-os.ts::AgentOsOptions` +/// and `packages/core/src/options-schema.ts::agentOsOptionsSchema`. #[derive(Default)] pub struct AgentOsConfig { /// Software packages to install (flattened). Default `[]`. diff --git a/packages/agentos/src/actor.ts b/packages/agentos/src/actor.ts index 59dc68a92..056d39479 100644 --- a/packages/agentos/src/actor.ts +++ b/packages/agentos/src/actor.ts @@ -26,6 +26,7 @@ import { type AgentOsActorConfig, type AgentOsActorConfigInput, agentOsActorConfigSchema, + nativeAgentOsOptionsSchema, } from "./config.js"; import { getPluginPath } from "./plugin-binary.js"; import type { AgentOsActorState, AgentOsActorVars } from "./types.js"; @@ -191,19 +192,18 @@ function flattenSoftware(input: unknown, out: SoftwareDescriptorLike[]): void { export function buildConfigJson( parsed: AgentOsActorConfig, ): string { + const options = nativeAgentOsOptionsSchema.parse( + parsed.options ?? {}, + ) as Record; const descriptors: SoftwareDescriptorLike[] = []; - flattenSoftware( - (parsed.options as { software?: unknown })?.software, - descriptors, - ); + flattenSoftware(options.software, descriptors); // Auto-include the default software bundle (`@agentos-software/common`: `sh` + // coreutils + the standard CLI tools agents rely on) unless the caller opted // out with `defaultSoftware: false`. Anything already listed in `software` // (e.g. an explicit `common`) is not duplicated. Prepended so the baseline // tools come first, matching the previous explicit `[common, ...]` ordering. - const defaultSoftwareEnabled = - (parsed.options as { defaultSoftware?: unknown })?.defaultSoftware !== false; + const defaultSoftwareEnabled = options.defaultSoftware !== false; if (defaultSoftwareEnabled) { const defaults: SoftwareDescriptorLike[] = []; flattenSoftware(common, defaults); @@ -233,7 +233,6 @@ export function buildConfigJson( // no manual `nodeModulesMount(...)`: see `withAutoAgentNodeModulesMount`. An // explicit `/root/node_modules` mount in `options.mounts` always wins. The VM // module resolver reads the mounted tree through the kernel VFS. - const options = (parsed.options ?? {}) as Record; const mounts = withAutoAgentNodeModulesMount( serializeNativeMounts(options.mounts), descriptors, @@ -242,6 +241,7 @@ export function buildConfigJson( return JSON.stringify({ software, additionalInstructions: options.additionalInstructions, + moduleAccessCwd: options.moduleAccessCwd, loopbackExemptPorts: options.loopbackExemptPorts, allowedNodeBuiltins: options.allowedNodeBuiltins, permissions: options.permissions, diff --git a/packages/agentos/src/config.ts b/packages/agentos/src/config.ts index 575e7ac76..746fffbda 100644 --- a/packages/agentos/src/config.ts +++ b/packages/agentos/src/config.ts @@ -1,8 +1,14 @@ import type { AgentOsOptions, JsonRpcNotification, + NativeMountConfig, PermissionRequest, } from "@rivet-dev/agentos-core"; +import { + agentOsOptionFieldSchemas, + nativeMountConfigSchema, + sharedSidecarConfigSchema, +} from "@rivet-dev/agentos-core"; import type { ActorContext, BeforeConnectContext } from "rivetkit"; import { z } from "zod/v4"; import type { AgentOsActorState, AgentOsActorVars } from "./types.js"; @@ -11,13 +17,28 @@ const zFunction = < T extends (...args: any[]) => any = (...args: unknown[]) => unknown, >() => z.custom((val) => typeof val === "function"); -const AgentOsOptionsSchema = z.custom( - (val) => typeof val === "object" && val !== null, -); +export const nativeAgentOsOptionsSchema = z + // Native actor options are a JSON-serializable subset of AgentOsOptions. + // Keep this allow-list in sync with buildConfigJson() and + // crates/agentos-actor-plugin/src/config.rs::AgentOsConfigJson. + .object({ + software: agentOsOptionFieldSchemas.software, + defaultSoftware: agentOsOptionFieldSchemas.defaultSoftware, + loopbackExemptPorts: agentOsOptionFieldSchemas.loopbackExemptPorts, + allowedNodeBuiltins: agentOsOptionFieldSchemas.allowedNodeBuiltins, + rootFilesystem: agentOsOptionFieldSchemas.rootFilesystem, + mounts: z.array(nativeMountConfigSchema).optional(), + moduleAccessCwd: agentOsOptionFieldSchemas.moduleAccessCwd, + additionalInstructions: agentOsOptionFieldSchemas.additionalInstructions, + permissions: agentOsOptionFieldSchemas.permissions, + sidecar: sharedSidecarConfigSchema.optional(), + limits: agentOsOptionFieldSchemas.limits, + }) + .strict(); export const agentOsActorConfigSchema = z .object({ - options: AgentOsOptionsSchema.optional(), + options: nativeAgentOsOptionsSchema.optional(), preview: z .object({ defaultExpiresInSeconds: z.number().positive().default(3600), @@ -33,6 +54,28 @@ export const agentOsActorConfigSchema = z // --- Typed config types (generic callbacks overlaid on the Zod schema) --- +/** + * Type mirror of `nativeAgentOsOptionsSchema`. + * + * Keep this in sync with the schema above and the Rust serde mirror at + * `crates/agentos-actor-plugin/src/config.rs::AgentOsConfigJson`. + */ +export type NativeAgentOsOptions = Pick< + AgentOsOptions, + | "software" + | "defaultSoftware" + | "loopbackExemptPorts" + | "allowedNodeBuiltins" + | "rootFilesystem" + | "moduleAccessCwd" + | "additionalInstructions" + | "permissions" + | "limits" +> & { + mounts?: NativeMountConfig[]; + sidecar?: { kind: "shared"; pool?: string }; +}; + type AgentOsActorContext = ActorContext< AgentOsActorState, TConnParams, @@ -67,13 +110,15 @@ interface AgentOsActorConfigCallbacks { // Parsed config (after Zod defaults/transforms applied). export type AgentOsActorConfig = Omit< z.infer, - "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" + "options" | "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" > & + { options?: NativeAgentOsOptions } & AgentOsActorConfigCallbacks; // Input config (what users pass in before Zod transforms). export type AgentOsActorConfigInput = Omit< z.input, - "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" + "options" | "onBeforeConnect" | "onSessionEvent" | "onPermissionRequest" > & + { options?: NativeAgentOsOptions } & AgentOsActorConfigCallbacks; diff --git a/packages/agentos/src/index.ts b/packages/agentos/src/index.ts index 8b7b9c46e..aefd1bfcd 100644 --- a/packages/agentos/src/index.ts +++ b/packages/agentos/src/index.ts @@ -10,7 +10,10 @@ import { agentOs as createAgentOs, type AgentOsActorDefinition, } from "./actor.js"; -import type { AgentOsActorConfigInput } from "./config.js"; +import type { + AgentOsActorConfigInput, + NativeAgentOsOptions, +} from "./config.js"; export { setup } from "rivetkit"; @@ -24,11 +27,17 @@ export { createAgentOs as agentOs }; export { type AgentOsActorConfig, type AgentOsActorConfigInput, + type NativeAgentOsOptions, agentOsActorConfigSchema, + nativeAgentOsOptionsSchema, } from "./config.js"; export { getPluginPath } from "./plugin-binary.js"; +// Re-export the software-definition helper so custom agents/tools/commands can +// be defined without importing @rivet-dev/agentos-core directly. +export { defineSoftware } from "@rivet-dev/agentos-core"; + export type { AgentOsActionContext, AgentOsActorState, @@ -52,12 +61,24 @@ export type { } from "./types.js"; export type AgentOSActorConfigInput = - AgentOsActorConfigInput["options"]; + NativeAgentOsOptions & + Omit, "options">; export function agentOS( config: AgentOSActorConfigInput = {}, ): AgentOsActorDefinition { + const { + onBeforeConnect, + onSessionEvent, + onPermissionRequest, + preview, + ...options + } = config; return createAgentOs({ - options: config, + options, + preview, + onBeforeConnect, + onSessionEvent, + onPermissionRequest, } as AgentOsActorConfigInput); } diff --git a/packages/agentos/tests/actor.test.ts b/packages/agentos/tests/actor.test.ts index 3e5c4bfd5..17919f636 100644 --- a/packages/agentos/tests/actor.test.ts +++ b/packages/agentos/tests/actor.test.ts @@ -14,6 +14,7 @@ import type { import { createClient } from "rivetkit/client"; import common from "@agentos-software/common"; import { + agentOS, agentOs, buildConfigJson, getPluginPath, @@ -306,7 +307,7 @@ describe("@rivet-dev/agentos native plugin package bridge", () => { test("serializes config and hands plugin paths to the NAPI runtime", () => { const definition = agentOs({ options: { - additionalInstructions: ["stay deterministic"], + additionalInstructions: "stay deterministic", loopbackExemptPorts: [4020], mounts: [nodeModulesMount("/host/project/node_modules")], sidecar: { kind: "shared", pool: "agentos-smoke" }, @@ -329,7 +330,7 @@ describe("@rivet-dev/agentos native plugin package bridge", () => { expect(calls[0].pluginPath).toBe(getPluginPath()); expect(calls[0].sidecarPath).toBe(process.env.AGENTOS_SIDECAR_BIN); expect(JSON.parse(calls[0].configJson)).toMatchObject({ - additionalInstructions: ["stay deterministic"], + additionalInstructions: "stay deterministic", loopbackExemptPorts: [4020], sidecar: { pool: "agentos-smoke" }, mounts: [ @@ -348,6 +349,72 @@ describe("@rivet-dev/agentos native plugin package bridge", () => { }); }); + test("agentOS flat config keeps callbacks outside native VM options", () => { + const definition = agentOS({ + defaultSoftware: false, + software: [], + onSessionEvent: () => {}, + }); + const expectedHandle = Symbol("native-factory") as unknown as ActorFactoryHandle; + const calls: NapiNativePluginOptions[] = []; + const runtime = { + kind: "napi", + createNativePluginFactory(options: NapiNativePluginOptions) { + calls.push(options); + return expectedHandle; + }, + } as CoreRuntime; + + const handle = definition.nativeFactoryBuilder?.(runtime); + + expect(handle).toBe(expectedHandle); + expect(calls).toHaveLength(1); + expect(JSON.parse(calls[0].configJson)).toEqual({ software: [] }); + expect(calls[0].configJson).not.toContain("onSessionEvent"); + }); + + test("rejects native actor options that cannot cross the NAPI config boundary", () => { + expect(() => + agentOs({ + options: { + toolKits: [], + } as never, + }), + ).toThrow(/toolKits/); + + expect(() => + agentOS({ + toolKits: [], + } as never), + ).toThrow(/toolKits/); + + expect(() => + agentOs({ + options: { + mounts: [{ path: "/data", driver: {} }], + } as never, + }), + ).toThrow(/driver/); + + expect(() => + agentOs({ + options: { + sidecar: { kind: "explicit", handle: {} }, + } as never, + }), + ).toThrow(/sidecar/); + }); + + test("buildConfigJson rejects unknown options instead of dropping them", () => { + expect(() => + buildConfigJson({ + options: { + notARealOption: true, + }, + } as never), + ).toThrow(/notARealOption/); + }); + test("buildConfigJson keeps software descriptors pointed at package roots", () => { const configJson = buildConfigJson({ options: { diff --git a/packages/core/package.json b/packages/core/package.json index db0186e68..77238ae94 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,7 +65,8 @@ "googleapis": "^144.0.0", "isolated-vm": "^6.0.0", "long-timeout": "^0.1.1", - "minimatch": "^10.2.4" + "minimatch": "^10.2.4", + "zod": "^4.1.11" }, "devDependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.87", @@ -106,7 +107,6 @@ "sandbox-agent": "^0.4.2", "typescript": "^5.7.2", "vitest": "^2.1.8", - "ws": "^8.18.0", - "zod": "^4.1.11" + "ws": "^8.18.0" } } diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index f8f6e8348..c7f1d10e8 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -37,6 +37,7 @@ import type { } from "./agent-session-types.js"; import { type HostTool, type ToolKit, validateToolkits } from "./host-tools.js"; import { zodToJsonSchema } from "./host-tools-zod.js"; +import { parseAgentOsOptions } from "./options-schema.js"; import type { JsonRpcNotification, JsonRpcRequest, @@ -480,6 +481,14 @@ function defaultAgentStderrHandler(event: AgentStderrEvent): void { process.stderr.write(event.chunk); } +/** + * Public core VM options. + * + * Keep this interface in sync with + * `packages/core/src/options-schema.ts::agentOsOptionsSchema`. The Rivet + * native actor intentionally accepts only a subset via + * `packages/agentos/src/config.ts::nativeAgentOsOptionsSchema`. + */ export interface AgentOsOptions { /** * Software to install in the VM. Each entry provides agents, tools, @@ -2535,6 +2544,7 @@ export class AgentOs { } static async create(options?: AgentOsOptions): Promise { + options = parseAgentOsOptions(options); const software = options?.defaultSoftware === false ? (options.software ?? []) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fdf2c0099..9aa036476 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,6 +15,20 @@ export { toolKit, validateToolkits, } from "./host-tools.js"; +export { + agentOsLimitsSchema, + agentOsOptionFieldSchemas, + agentOsOptionsSchema, + hostToolSchema, + mountConfigSchema, + nativeMountConfigSchema, + parseAgentOsOptions, + permissionsSchema, + rootFilesystemConfigSchema, + sharedSidecarConfigSchema, + sidecarConfigSchema, + toolKitSchema, +} from "./options-schema.js"; export { createInMemoryLayerStore, createSnapshotExport, diff --git a/packages/core/src/options-schema.ts b/packages/core/src/options-schema.ts new file mode 100644 index 000000000..53b4af68e --- /dev/null +++ b/packages/core/src/options-schema.ts @@ -0,0 +1,296 @@ +import { z } from "zod/v4"; +import type { + AgentOsOptions, + AgentStderrHandler, + NativeMountConfig, +} from "./agent-os.js"; +import type { HostTool, ToolKit } from "./host-tools.js"; + +const stringArray = z.array(z.string()); +const nonNegativeInteger = z.number().int().nonnegative(); +const positiveInteger = z.number().int().positive(); +const functionSchema = z.custom<(...args: any[]) => any>( + (value) => typeof value === "function", + { message: "Expected function" }, +); + +const permissionModeSchema = z.enum(["allow", "deny"]); + +const fsPermissionRuleSchema = z + .object({ + mode: permissionModeSchema, + operations: stringArray.optional(), + paths: stringArray.optional(), + }) + .strict(); + +const patternPermissionRuleSchema = z + .object({ + mode: permissionModeSchema, + operations: stringArray.optional(), + patterns: stringArray.optional(), + }) + .strict(); + +const fsRulePermissionsSchema = z + .object({ + default: permissionModeSchema.optional(), + rules: z.array(fsPermissionRuleSchema), + }) + .strict(); + +const patternRulePermissionsSchema = z + .object({ + default: permissionModeSchema.optional(), + rules: z.array(patternPermissionRuleSchema), + }) + .strict(); + +const fsPermissionsSchema = z.union([permissionModeSchema, fsRulePermissionsSchema]); +const patternPermissionsSchema = z.union([ + permissionModeSchema, + patternRulePermissionsSchema, +]); + +export const permissionsSchema = z + .object({ + fs: fsPermissionsSchema.optional(), + network: patternPermissionsSchema.optional(), + childProcess: patternPermissionsSchema.optional(), + process: patternPermissionsSchema.optional(), + env: patternPermissionsSchema.optional(), + tool: patternPermissionsSchema.optional(), + }) + .strict(); + +export const agentOsLimitsSchema = z + .object({ + resources: z + .object({ + cpuCount: positiveInteger.optional(), + maxProcesses: nonNegativeInteger.optional(), + maxOpenFds: nonNegativeInteger.optional(), + maxPipes: nonNegativeInteger.optional(), + maxPtys: nonNegativeInteger.optional(), + maxSockets: nonNegativeInteger.optional(), + maxConnections: nonNegativeInteger.optional(), + maxSocketBufferedBytes: nonNegativeInteger.optional(), + maxSocketDatagramQueueLen: nonNegativeInteger.optional(), + maxFilesystemBytes: nonNegativeInteger.optional(), + maxInodeCount: nonNegativeInteger.optional(), + maxBlockingReadMs: nonNegativeInteger.optional(), + maxPreadBytes: nonNegativeInteger.optional(), + maxFdWriteBytes: nonNegativeInteger.optional(), + maxProcessArgvBytes: nonNegativeInteger.optional(), + maxProcessEnvBytes: nonNegativeInteger.optional(), + maxReaddirEntries: nonNegativeInteger.optional(), + maxWasmFuel: nonNegativeInteger.optional(), + maxWasmMemoryBytes: nonNegativeInteger.optional(), + maxWasmStackBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + http: z + .object({ maxFetchResponseBytes: nonNegativeInteger.optional() }) + .strict() + .optional(), + tools: z + .object({ + defaultToolTimeoutMs: nonNegativeInteger.optional(), + maxToolTimeoutMs: nonNegativeInteger.optional(), + maxRegisteredToolkits: nonNegativeInteger.optional(), + maxRegisteredToolsPerVm: nonNegativeInteger.optional(), + maxToolsPerToolkit: nonNegativeInteger.optional(), + maxToolSchemaBytes: nonNegativeInteger.optional(), + maxToolExamplesPerTool: nonNegativeInteger.optional(), + maxToolExampleInputBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + plugins: z + .object({ + maxPersistedManifestBytes: nonNegativeInteger.optional(), + maxPersistedManifestFileBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + acp: z + .object({ + maxReadLineBytes: nonNegativeInteger.optional(), + stdoutBufferByteLimit: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + jsRuntime: z + .object({ + v8HeapLimitMb: nonNegativeInteger.optional(), + capturedOutputLimitBytes: nonNegativeInteger.optional(), + stdinBufferLimitBytes: nonNegativeInteger.optional(), + eventPayloadLimitBytes: nonNegativeInteger.optional(), + v8IpcMaxFrameBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + python: z + .object({ + outputBufferMaxBytes: nonNegativeInteger.optional(), + executionTimeoutMs: nonNegativeInteger.optional(), + vfsRpcTimeoutMs: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + wasm: z + .object({ + maxModuleFileBytes: nonNegativeInteger.optional(), + capturedOutputLimitBytes: nonNegativeInteger.optional(), + syncReadLimitBytes: nonNegativeInteger.optional(), + }) + .strict() + .optional(), + }) + .strict(); + +const rootLowerInputSchema = z.union([ + z.object({ kind: z.literal("bundled-base-filesystem") }).strict(), + z.object({ kind: z.literal("snapshot-export"), source: z.unknown() }).strict(), +]); + +export const rootFilesystemConfigSchema = z + .object({ + type: z.literal("overlay").optional(), + mode: z.enum(["ephemeral", "read-only"]).optional(), + disableDefaultBaseLayer: z.boolean().optional(), + lowers: z.array(rootLowerInputSchema).optional(), + }) + .strict(); + +const nativeMountPluginSchema = z + .object({ + id: z.string(), + config: z.unknown().optional(), + }) + .strict(); + +const plainMountConfigSchema = z + .object({ + path: z.string(), + driver: z.custom((value) => typeof value === "object" && value !== null, { + message: "Expected filesystem driver object", + }), + readOnly: z.boolean().optional(), + }) + .strict(); + +export const nativeMountConfigSchema = z + .object({ + path: z.string(), + plugin: nativeMountPluginSchema, + readOnly: z.boolean().optional(), + }) + .strict() as z.ZodType; + +const overlayMountConfigSchema = z + .object({ + path: z.string(), + filesystem: z + .object({ + type: z.literal("overlay"), + store: z.unknown(), + mode: z.enum(["ephemeral", "read-only"]).optional(), + lowers: z.array(z.unknown()), + }) + .strict(), + }) + .strict(); + +export const mountConfigSchema = z.union([ + plainMountConfigSchema, + nativeMountConfigSchema, + overlayMountConfigSchema, +]); + +export const sharedSidecarConfigSchema = z + .object({ + kind: z.literal("shared"), + pool: z.string().optional(), + }) + .strict(); + +const explicitSidecarSchema = z + .object({ + kind: z.literal("explicit"), + handle: z.unknown(), + }) + .strict(); + +export const sidecarConfigSchema = z.union([ + sharedSidecarConfigSchema, + explicitSidecarSchema, +]); + +const toolExampleSchema = z + .object({ + description: z.string(), + input: z.unknown(), + }) + .strict(); + +export const hostToolSchema = z + .object({ + description: z.string(), + inputSchema: z.custom((value) => typeof value === "object" && value !== null, { + message: "Expected Zod schema object", + }), + execute: functionSchema, + examples: z.array(toolExampleSchema).optional(), + timeout: nonNegativeInteger.optional(), + }) + .strict() as z.ZodType; + +export const toolKitSchema = z + .object({ + name: z.string(), + description: z.string(), + tools: z.record(z.string(), hostToolSchema), + }) + .strict() as z.ZodType; + +/** + * Shared AgentOsOptions field schemas. + * + * Core uses the full object. The Rivet/native actor composes a narrower + * native-safe subset in `packages/agentos/src/config.ts`; keep that subset and + * `crates/agentos-actor-plugin/src/config.rs::AgentOsConfigJson` aligned when + * adding options that cross the native boundary. + */ +export const agentOsOptionFieldSchemas = { + software: z.array(z.unknown()).optional(), + defaultSoftware: z.boolean().optional(), + loopbackExemptPorts: z.array(z.number().int().min(0).max(65535)).optional(), + allowedNodeBuiltins: stringArray.optional(), + rootFilesystem: rootFilesystemConfigSchema.optional(), + mounts: z.array(mountConfigSchema).optional(), + moduleAccessCwd: z.string().optional(), + additionalInstructions: z.string().optional(), + scheduleDriver: z + .custom((value) => typeof value === "object" && value !== null, { + message: "Expected schedule driver object", + }) + .optional(), + toolKits: z.array(toolKitSchema).optional(), + permissions: permissionsSchema.optional(), + sidecar: sidecarConfigSchema.optional(), + limits: agentOsLimitsSchema.optional(), + onAgentStderr: z.custom( + (value) => typeof value === "function", + { message: "Expected function" }, + ).optional(), +} as const; + +export const agentOsOptionsSchema = z + .object(agentOsOptionFieldSchemas) + .strict() as z.ZodType; + +export function parseAgentOsOptions(options?: AgentOsOptions): AgentOsOptions { + return agentOsOptionsSchema.parse(options ?? {}); +} diff --git a/packages/core/tests/options-schema.test.ts b/packages/core/tests/options-schema.test.ts new file mode 100644 index 000000000..e2fbe8421 --- /dev/null +++ b/packages/core/tests/options-schema.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; +import { AgentOs, agentOsOptionsSchema } from "../src/index.js"; + +describe("AgentOsOptions validation", () => { + test("rejects unknown top-level options before booting a VM", async () => { + await expect( + AgentOs.create({ + onSessionEvent: () => {}, + } as never), + ).rejects.toThrow(/onSessionEvent/); + }); + + test("rejects unknown nested permission fields", () => { + expect(() => + agentOsOptionsSchema.parse({ + permissions: { + filesystem: "allow", + }, + }), + ).toThrow(/filesystem/); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e4757555..60a55588a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -357,6 +357,9 @@ importers: minimatch: specifier: ^10.2.4 version: 10.2.5 + zod: + specifier: ^4.1.11 + version: 4.3.6 devDependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -475,9 +478,6 @@ importers: ws: specifier: ^8.18.0 version: 8.20.0(bufferutil@4.1.0) - zod: - specifier: ^4.1.11 - version: 4.3.6 packages/dev-shell: dependencies: diff --git a/website/docs.config.mjs b/website/docs.config.mjs index 826717148..2bc5599ac 100644 --- a/website/docs.config.mjs +++ b/website/docs.config.mjs @@ -63,6 +63,7 @@ export const siteConfig = { { slug: "docs/agents/codex", label: "Codex", attrs: { "data-icon-src": "/images/registry/codex.svg" }, badge: { text: "Coming Soon", variant: "caution" } }, { slug: "docs/agents/amp", label: "Amp", attrs: { "data-icon-src": "/images/registry/amp.svg" }, badge: { text: "Coming Soon", variant: "caution" } }, { slug: "docs/agents/opencode", label: "OpenCode", attrs: { "data-icon-src": "/images/registry/opencode.svg" }, badge: { text: "Coming Soon", variant: "caution" } }, + { slug: "docs/agents/custom", label: "Custom Agents", attrs: { "data-icon": "wrench" } }, ], }, { slug: "docs/sessions", label: "Sessions & Transcripts", attrs: { "data-icon": "messages" } }, @@ -101,6 +102,14 @@ export const siteConfig = { items: [ { label: "API Reference", link: "/api", attrs: { target: "_blank" } }, { slug: "docs/deployment", label: "Deploy" }, + { + label: "Custom Software", + items: [ + { slug: "docs/custom-software/definition", label: "Definition" }, + { slug: "docs/custom-software/building-wasm", label: "Building Binaries" }, + { label: "Request Software", link: "https://github.com/rivet-dev/agent-os/issues/new/choose", attrs: { target: "_blank" } }, + ], + }, { label: "Architecture", items: [ diff --git a/website/src/content/docs/docs/agents/custom.mdx b/website/src/content/docs/docs/agents/custom.mdx new file mode 100644 index 000000000..69f1be151 --- /dev/null +++ b/website/src/content/docs/docs/agents/custom.mdx @@ -0,0 +1,152 @@ +--- +title: "Custom Agents" +description: "Bring your own coding agent to agentOS by speaking the Agent Client Protocol (ACP) inside the VM." +--- + +import { Aside } from '@astrojs/starlight/components'; + +A custom agent is a program that runs **inside the VM** to drive a coding agent. agentOS spawns it when you call `createSession()` and talks to it over the Agent Client Protocol. You ship it as a software package, exactly like the built-in agents. + +## Agent Client Protocol (ACP) + +agentOS speaks the [Agent Client Protocol (ACP)](https://agentclientprotocol.com) to every agent: JSON-RPC over stdio. The agent reads protocol messages on **stdin** and writes them on **stdout**, so stdout is reserved for ACP and **stderr is used for logs**. Your program only needs to speak ACP; how it runs the underlying model is up to you. See the [ACP documentation](https://agentclientprotocol.com) for the full protocol. + +## Two ways to build an agent + +There are two shapes, depending on whether the agent runs in the ACP process or in its own. + +### Single process (embedded) + +The ACP adapter **embeds the agent SDK** and runs it in the same process. One process inside the VM, lower memory footprint. + + + + + + + + + Host + + ACP + + VM + + ACP adapter + + agent (embedded) + + +For example, an adapter to run **OpenCode**, which speaks ACP natively. One package is both the ACP process and the agent, so there's no separate adapter and nothing else is spawned. + +```ts title="opencode.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +// Example: an adapter to run OpenCode. +export default defineSoftware({ + name: "opencode", + type: "agent", + packageDir, + // A single package provides everything that runs in the VM. + requires: ["@agentos-software/opencode"], + agent: { + id: "opencode", + // Same package for both: OpenCode *is* the ACP process. It speaks ACP + // on stdio itself, so there is no separate adapter to spawn the agent. + acpAdapter: "@agentos-software/opencode", + agentPackage: "@agentos-software/opencode", + staticEnv: { + OPENCODE_DISABLE_CONFIG_DEP_INSTALL: "1", + OPENCODE_DISABLE_EMBEDDED_WEB_UI: "1", + }, + }, +}); +``` + +### ACP adapter (separate agent) + +The ACP adapter is a thin **bridge** that spawns the real agent as its **own process** (a CLI or SDK) and translates between it and ACP. Full agent feature set, higher memory. + + + + + + + + + Host + + ACP + + VM + + ACP adapter + + spawns + + Agent process + (CLI / SDK) + + +For example, an adapter to run **Pi**: the `pi` CLI doesn't speak ACP, so `pi-acp` speaks ACP and spawns the CLI as a separate process. The descriptor names two packages, the adapter and the agent. + +```ts title="pi-cli.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +// Example: a pi-acp adapter that runs the Pi CLI. +export default defineSoftware({ + name: "pi-cli", + type: "agent", + packageDir, + // Two separate packages: the ACP adapter, and the agent it drives. + requires: ["pi-acp", "@mariozechner/pi-coding-agent"], + agent: { + id: "pi-cli", + // `pi-acp` is a thin ACP adapter. It does NOT run the agent itself: + // it speaks ACP on stdio and spawns the `pi` CLI as a separate child + // process, translating between ACP and the CLI. + acpAdapter: "pi-acp", + // The actual agent, launched by pi-acp as its own process. + agentPackage: "@mariozechner/pi-coding-agent", + // Tell the adapter where to find the `pi` CLI inside the VM + // (resolved at boot to a guest path under /root/node_modules). + env: (ctx) => ({ + PI_ACP_PI_COMMAND: ctx.resolveBin("@mariozechner/pi-coding-agent", "pi"), + }), + }, +}); +``` + +## Use your agent + +Register the package on the server with `software`. Sessions are then created from the client by `id`, exactly like any built-in agent. + +```ts title="server.ts" +import { agentOS, setup, defineSoftware } from "@rivet-dev/agentos"; + +const myAgent = defineSoftware({ + type: "agent", + /* ...name, packageDir, requires, agent (see above)... */ +}); + +const vm = agentOS({ software: [myAgent] }); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +See [Sessions](/docs/sessions) for creating and driving sessions. Ship your adapter as a package so its dependencies resolve from `node_modules/` (via `requires`) inside the VM, rather than as a loose file. + +All built-in agents are defined exactly this way. Browse them for reference on [GitHub](https://github.com/rivet-dev/agent-os/tree/main/registry/agent). + +## Read more + +- [Defining software packages](/docs/custom-software/definition): the full descriptor reference, including every `agent` field (`staticEnv`, `env`, `launchArgs`), the `SoftwareContext` helpers, and the tool and WASM-command software types. +- [Building binaries](/docs/custom-software/building-wasm): compile WASM command binaries and use the registry. diff --git a/website/src/content/docs/docs/agents/pi.mdx b/website/src/content/docs/docs/agents/pi.mdx index 95519648e..081b13dfe 100644 --- a/website/src/content/docs/docs/agents/pi.mdx +++ b/website/src/content/docs/docs/agents/pi.mdx @@ -79,3 +79,7 @@ const { sessionId } = await agent.createSession("pi", { See the [Pi extension documentation](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) for the full extension API. +## Customizing the agent + +Pi is a built-in agent, but it's just a software package under the hood. To ship your own ACP adapter, swap the underlying agent SDK, or register a tweaked Pi build as a new agent, see [Custom Agents](/docs/agents/custom). + diff --git a/website/src/content/docs/docs/architecture.mdx b/website/src/content/docs/docs/architecture.mdx index 6519d4e1b..a8c792c88 100644 --- a/website/src/content/docs/docs/architecture.mdx +++ b/website/src/content/docs/docs/architecture.mdx @@ -297,7 +297,7 @@ An agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) is just - **Long-lived.** Where a bare `exec()` runs once and exits, a session keeps an agent alive across many prompts. - **Streamed.** The agent's output flows back to your app in real time as `sessionEvent`s. - **Replayable.** Each session persists a transcript (with sequence numbers) that survives sleep/wake, so you can replay the conversation later. -- **Context injected.** agentOS adds a system prompt describing the VM environment and available tools, layered on top of the agent's own instructions. See [System Prompt](/docs/system-prompt). +- **Context injected.** agentOS adds a system prompt describing the VM environment and available commands and bindings, layered on top of the agent's own instructions. See [System Prompt](/docs/system-prompt). - See [Agent Sessions](/docs/architecture/agent-sessions) for the internals. ### Permissions & approvals diff --git a/website/src/content/docs/docs/bindings.mdx b/website/src/content/docs/docs/bindings.mdx index 7e91018c7..1d64f4a11 100644 --- a/website/src/content/docs/docs/bindings.mdx +++ b/website/src/content/docs/docs/bindings.mdx @@ -89,6 +89,8 @@ When bindings are registered, CLI shims are installed at `/usr/local/bin/agentos The agent interacts with bindings as shell commands: +The listing subcommand is still named `list-tools` for CLI compatibility. + ```bash # List all available bindings groups agentos list-tools diff --git a/website/src/content/docs/docs/custom-software/building-wasm.mdx b/website/src/content/docs/docs/custom-software/building-wasm.mdx new file mode 100644 index 000000000..dca12c905 --- /dev/null +++ b/website/src/content/docs/docs/custom-software/building-wasm.mdx @@ -0,0 +1,86 @@ +--- +title: "Building Binaries" +description: "Compile WASM command binaries for agentOS from source in the secure-exec registry." +--- + +import { Aside } from '@astrojs/starlight/components'; + +WASM command packages (`type: "wasm-commands"`) ship **compiled `.wasm` binaries** that run inside the VM as guest commands. The binaries are build artifacts and are not checked into git, so to add or change a command you build it from source in the **secure-exec registry**. + + + +## Where it lives + +Command source and packages live under `registry/` in [secure-exec](https://github.com/rivet-dev/secure-exec/tree/main/registry): + +- **`registry/native/crates/`**: the Rust source for the WASM commands. +- **`registry/native/c/`**: the C source for the WASM commands. +- **`registry/software//`**: the npm package for each command set (`@agentos-software/`). It contains `dist/` (the TypeScript descriptor) and `wasm/` (the compiled binaries it exposes via `commandDir`). + +## Build + +Build everything from `registry/`: + +```bash +make build # build all WASM binaries + the TypeScript packages +make copy-wasm # copy binaries into each package's wasm/ directory +make test +``` + +`copy-wasm` maps each compiled command into `registry/software//wasm/`, which is the `commandDir` the package exposes. The two toolchains build independently: + +### Rust + +Most commands are Rust. The source lives in `registry/native/crates/` and compiles for `wasm32-wasip1` with the pinned **nightly** toolchain from `rust-toolchain.toml` (the build vendors and patches `std` for WASI). Build just the Rust commands: + +```bash +make build-wasm-rust # runs: cd native && make wasm +``` + +### C + +C-based commands (e.g. `sqlite3`, `unzip`, `wget`, `zip`) live in `registry/native/c/` and compile with a **wasi-sdk** clang toolchain. Build just the C commands: + +```bash +make build-wasm-c # runs: cd native/c && make programs && make install +``` + +## Add a new command package + +1. Add the command source under `registry/native/crates/` (Rust) or `registry/native/c/` (C). +2. Create `registry/software//` as an `@agentos-software/` npm package that exports a descriptor with its `commandDir`. +3. Add a copy rule to the `copy-wasm` target mapping the built binary into `registry/software//wasm/`. +4. If it belongs in a meta-package (e.g. `common` or `build-essential`), add it there. +5. Verify with `make copy-wasm && make build && make test`. + +## Let an agent build it + +This is a mechanical, well-scoped task, so you can hand it to a coding agent. A prompt like: + +```text +Add a WASM command package for `` to the secure-exec registry: +- put the Rust source under registry/native/crates/ (or C under registry/native/c/), +- create registry/software// as an @agentos-software/ npm + package that exports a commandDir descriptor, +- add a copy-wasm rule mapping the built binary into its wasm/ directory, +then run `make copy-wasm && make build && make test` and fix any failures. +``` + +## Using the registry + +Install a published package and pass it to `software`. Registry WASM packages expose a `commandDir`, so you pass them directly (no `defineSoftware()` wrapper): + +```ts title="server.ts" +import { agentOS, setup } from "@rivet-dev/agentos"; +import coreutils from "@agentos-software/coreutils"; +import ripgrep from "@agentos-software/ripgrep"; + +const vm = agentOS({ software: [coreutils, ripgrep] }); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +Meta-packages bundle a full set, e.g. `@agentos-software/common` (coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip). Run the commands from the client; see [Processes & Shell](/docs/processes). Browse the full catalog on the [Registry](/registry), and see how packages map onto the `wasm-commands` descriptor in [Software Definition](/docs/custom-software/definition#wasm-command-software). diff --git a/website/src/content/docs/docs/custom-software/definition.mdx b/website/src/content/docs/docs/custom-software/definition.mdx new file mode 100644 index 000000000..fad8e1995 --- /dev/null +++ b/website/src/content/docs/docs/custom-software/definition.mdx @@ -0,0 +1,156 @@ +--- +title: "Software Definition" +description: "The full software-package definition for custom agents, command packages, and WASM commands in an agentOS VM." +--- + +import { Aside } from '@astrojs/starlight/components'; + +**Software** is anything you install into a VM: an agent, a command package, or a set of WASM commands. Every package, including the built-ins, is declared with `defineSoftware()` and passed to the `software` option. + +```ts +import { agentOS, setup } from "@rivet-dev/agentos"; +import pi from "@agentos-software/pi"; + +const vm = agentOS({ software: [pi /*, …more */] }); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + +`defineSoftware()` is a typed identity helper. It returns the descriptor unchanged but gives you full type-checking. The descriptor's `type` field discriminates between the three kinds of software. + +## Software types + +- **`"agent"`**: a coding agent runnable via `createSession(id)`. Key fields: `agent`, `requires`, `packageDir`. +- **`"tool"`**: command-package software exposed inside the VM. Key fields: `bins`, `requires`, `packageDir`. +- **`"wasm-commands"`**: compiled WASM command binaries. Key fields: `commandDir`, `aliases`, `permissions`. + +All descriptors share two base fields: + +- **`name`** (required). The software package name. +- **`type`** (required). One of `"agent"`, `"tool"`, or `"wasm-commands"`. + +### Agent software + +Registers a coding agent. See [Custom Agents](/docs/agents/custom) for the agent-focused guide; this is the full field reference. + +- **`packageDir`** (required). This package's directory on the host. Resolves `requires` from its own `node_modules/`, mounted into the VM at `/root/node_modules/`. +- **`requires`** (required). npm packages that must be available inside the VM (must include the adapter and agent packages). +- **`agent.id`** (required). The id passed to `createSession(id)`. Reuse a built-in id to override it, or pick a new one. +- **`agent.acpAdapter`** (required). npm package of the ACP adapter, spawned inside the VM. Must be in `requires`. +- **`agent.agentPackage`** (required). npm package of the underlying agent SDK/CLI. Must be in `requires`. +- **`agent.staticEnv`** (optional). Static env vars passed when spawning the adapter (merged **under** user `env`). +- **`agent.env`** (optional). `(ctx: SoftwareContext) => Record`; env computed at boot, e.g. to resolve a bin path. +- **`agent.launchArgs`** (optional). Extra CLI args prepended when launching the adapter. + +```ts title="pi.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +export default defineSoftware({ + name: "pi", + type: "agent", + packageDir, + requires: ["@agentos-software/pi", "@mariozechner/pi-coding-agent"], + agent: { + id: "pi", + acpAdapter: "@agentos-software/pi", + agentPackage: "@mariozechner/pi-coding-agent", + }, +}); +``` + +### Command package software + +Exposes one or more CLI commands inside the VM by mapping a command name to the npm package that provides its `bin`. The descriptor value is still `"tool"` because this is the software package type, separate from host [bindings](/docs/bindings). + +- **`packageDir`** (required). This package's directory on the host (resolves `requires`). +- **`requires`** (required). npm packages that must be available inside the VM. +- **`bins`** (required). `Record`, mapping the command name as invoked in the VM to the npm package providing it. + +```ts title="my-tool.ts" +import { defineSoftware } from "@rivet-dev/agentos"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +export default defineSoftware({ + name: "my-tool", + type: "tool", + packageDir, + requires: ["@my-org/my-cli"], + bins: { "my-cli": "@my-org/my-cli" }, +}); +``` + +### WASM command software + +Registers compiled WebAssembly command binaries (coreutils, ripgrep, jq, …). See [Building Binaries](/docs/custom-software/building-wasm) for how to produce the binaries. + +- **`commandDir`** (required). Absolute host path to the directory of `.wasm` command binaries. +- **`aliases`** (optional). `Record`; symlink-style aliases. +- **`permissions`** (optional). Permission-tier assignments: `full`, `readWrite`, `readOnly` (`string[]` or `"*"`), `isolated`. + +```ts title="my-commands.ts" +import { defineSoftware } from "@rivet-dev/agentos"; + +export default defineSoftware({ + name: "my-commands", + type: "wasm-commands", + commandDir: "/abs/path/to/wasm", + aliases: { ll: "ls" }, + permissions: { readOnly: "*" }, +}); +``` + + + +## `SoftwareContext` + +The `agent.env` callback (and other dynamic config) receives a `SoftwareContext` for resolving VM-side paths: + +```ts +interface SoftwareContext { + // Resolve a package's bin to its VM path, e.g. + // ctx.resolveBin("@mariozechner/pi-coding-agent", "pi") + // -> "/root/node_modules/@mariozechner/pi-coding-agent/dist/cli.js" + resolveBin(packageName: string, binName?: string): string; + + // Resolve a package root to its VM path, e.g. + // ctx.resolvePackage("pi-acp") -> "/root/node_modules/pi-acp" + resolvePackage(packageName: string): string; +} +``` + +```ts +agent: { + id: "pi-cli", + acpAdapter: "pi-acp", + agentPackage: "@mariozechner/pi-coding-agent", + env: (ctx) => ({ + PI_ACP_PI_COMMAND: ctx.resolveBin("@mariozechner/pi-coding-agent", "pi"), + }), +} +``` + +## Meta-packages + +A software entry may be an **array** of descriptors, letting one package bundle several (e.g. a "build-essential" set). Pass arrays directly to `software`: + +```ts +const vm = agentOS({ + software: [pi, buildEssential /* = [coreutils, make, git, curl] */], +}); +``` + +## Next steps + +- [Custom Agents](/docs/agents/custom): the agent-focused guide. +- [Building Binaries](/docs/custom-software/building-wasm): compile WASM commands and use the registry. +- [Request Software](https://github.com/rivet-dev/agent-os/issues/new/choose): ask for a package you need. diff --git a/website/src/content/docs/docs/sessions.mdx b/website/src/content/docs/docs/sessions.mdx index 1c70d4bd3..12dc80854 100644 --- a/website/src/content/docs/docs/sessions.mdx +++ b/website/src/content/docs/docs/sessions.mdx @@ -371,3 +371,28 @@ registry.start(); *[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)* +## Agent logs + +The agent (ACP adapter) runs as a process inside the VM. It uses **stdout** for ACP protocol traffic, so its **stderr** is the channel for logs, warnings, and crash diagnostics. Pass `onAgentStderr` to the VM to capture it, and route it to your own logger to see exactly what the agent is doing (or why it exited). + + + +```ts title="server.ts" +import { agentOS, setup } from "@rivet-dev/agentos"; +import pi from "@agentos-software/pi"; + +const vm = agentOS({ + software: [pi], + onAgentStderr(event) { + // event: { sessionId, agentType, processId, pid, chunk: Uint8Array } + const line = new TextDecoder().decode(event.chunk); + logger.info(`[agent:${event.agentType} session:${event.sessionId}] ${line}`); + }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); +``` + diff --git a/website/src/content/docs/docs/software.mdx b/website/src/content/docs/docs/software.mdx index 06072aae2..0cb1487de 100644 --- a/website/src/content/docs/docs/software.mdx +++ b/website/src/content/docs/docs/software.mdx @@ -62,6 +62,6 @@ console.log(result.stdout); Browse all available software packages on the [Registry](/registry). -## Publishing Custom Packages +## Custom Software -See the [agentos-registry contributing guide](https://github.com/rivet-dev/agent-os/blob/main/registry/CONTRIBUTING.md) for how to add new software packages to the registry. +Package your own agents, command packages, and WASM commands. See [Software Definition](/docs/custom-software/definition) to define a package, and [Building Binaries](/docs/custom-software/building-wasm) to compile WASM commands from source in the [secure-exec registry](https://github.com/rivet-dev/secure-exec/tree/main/registry). diff --git a/website/src/content/docs/docs/system-prompt.mdx b/website/src/content/docs/docs/system-prompt.mdx index 2ec54c1b4..b73f29bf4 100644 --- a/website/src/content/docs/docs/system-prompt.mdx +++ b/website/src/content/docs/docs/system-prompt.mdx @@ -4,21 +4,21 @@ description: "How agentOS injects context into agent sessions." skill: true --- -agentOS automatically injects a system prompt into every agent session that describes the VM environment and available tools. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.). +agentOS automatically injects a system prompt into every agent session that describes the VM environment and available commands and bindings. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.). -The base prompt is embedded in the sidecar (not written to a file inside the VM). At session start the sidecar assembles the base prompt with your additional instructions and generated tool docs, then injects the result into the agent adapter's launch arguments (for example, `--append-system-prompt` for Pi). +The base prompt is embedded in the sidecar (not written to a file inside the VM). At session start the sidecar assembles the base prompt with your additional instructions and generated binding docs, then injects the result into the agent adapter's launch arguments (for example, `--append-system-prompt` for Pi). ## Customization -- `additionalInstructions` appends extra instructions to the agent's system prompt. They are added after the base OS prompt and the generated tool docs, so they layer on top of (rather than replace) the agent's own instructions. -- `skipOsInstructions` suppresses the base OS prompt while still injecting the generated tool docs. +- `additionalInstructions` appends extra instructions to the agent's system prompt. They are added after the base OS prompt and the generated binding docs, so they layer on top of (rather than replace) the agent's own instructions. +- `skipOsInstructions` suppresses the base OS prompt while still injecting the generated binding docs. ```ts const session = await vm.createSession("pi", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, // Extra instructions appended to the agent system prompt additionalInstructions: "Always write tests before implementation.", - // Suppress the base OS prompt (tool docs are still injected) + // Suppress the base OS prompt (binding docs are still injected) skipOsInstructions: true, }); ``` From 1a00b488c450af257225198215203fe82477aec8 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 23 Jun 2026 20:45:02 -0700 Subject: [PATCH 2/2] feat(sidecar): configurable logfmt logging + ext stall-watchdog tracing Replace the hardcoded ERROR-level tracing subscriber in agentos-sidecar with a rivet-style init_tracing: EnvFilter-gated level (AGENTOS_LOG_LEVEL > LOG_LEVEL > RUST_LOG > info), logfmt format (RUST_LOG_FORMAT=text to opt out), and an optional file sink (AGENTOS_LOG_FILE). Output stays on stderr/file, never stdout (the frame channel). Add an ext.request span + entry/exit timing in acp_extension, plus a stall watchdog that warns every 10s while a request is in flight, so a hung tool turn surfaces as a breadcrumb long before the host's 120s frame timeout instead of silently. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 30 ++++- Cargo.lock | 47 ++++++++ crates/agentos-sidecar/Cargo.toml | 6 +- crates/agentos-sidecar/src/acp_extension.rs | 68 ++++++++++-- crates/agentos-sidecar/src/main.rs | 91 ++++++++++++++-- justfile | 26 ++--- packages/core/package.json | 6 +- packages/core/src/host-tools-zod.ts | 40 +++++-- packages/core/tests/host-tools-zod.test.ts | 55 ++++++++++ pnpm-lock.yaml | 115 +++++++++++--------- pnpm-workspace.yaml | 8 +- website/.astro/content-modules.mjs | 17 +-- website/.astro/data-store.json | 2 +- website/docs.config.mjs | 1 + website/src/content/docs/docs/debugging.mdx | 54 +++++++++ 15 files changed, 449 insertions(+), 117 deletions(-) create mode 100644 website/src/content/docs/docs/debugging.mdx diff --git a/CLAUDE.md b/CLAUDE.md index ca2579a3b..53a703064 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,13 +5,40 @@ Agent OS is the agent-facing wrapper around secure-exec. It provides ACP session ## Boundaries - secure-exec dependency workflow. Manage the secure-exec dependency ONLY through `scripts/secure-exec-dep.mjs` (the `just secure-exec-*` recipes); never hand-edit the `path` / `version` / `catalog:` pins. - - Testing against local secure-exec changes: run `just secure-exec-local` to repoint npm (`link:`) and crates (`path = "../secure-exec/..."`) at the sibling checkout, then `node scripts/secure-exec-dep.mjs set-crate-version ` so the Cargo version requirement matches the sibling crate version (otherwise cargo cannot resolve the path deps). Use `just secure-exec-status` to inspect. This mode is for local builds/tests ONLY. + - Testing against local secure-exec changes: run `just secure-exec-local` to repoint npm (`link:`) and crates (`path = "../secure-exec/..."`) at the sibling checkout, then `node scripts/secure-exec-dep.mjs set-crate-version ` so the Cargo version requirement matches the sibling crate version (otherwise cargo cannot resolve the path deps). Also run `pnpm install` in `../secure-exec` first, or cargo panics in `v8-runtime/build.rs` with "missing Node dependencies at .../packages/build-tools/node_modules" (the V8 bridge assets are built from there). Use `just secure-exec-status` to inspect. This mode is for local builds/tests ONLY. - Pushing changes that depend on secure-exec changes: NEVER push with local (`path:` / `link:`) dependencies. First preview-publish the secure-exec changes to their own secure-exec branch (the `preview-publish-secure-exec` flow), then point agent-os back at that exact published version with `just secure-exec-pinned` + `just secure-exec-set-version ` (and `set-crate-version ` for the crates). Only commit/push the pinned-to-remote state. - Keep generic runtime, kernel, VFS, language execution, and registry software behavior in secure-exec. - Agent OS owns ACP, sessions, agent adapters, toolkit semantics, quickstarts, and the AgentOs facade. - Call OS instances VMs, never sandboxes. - The protocol has no backwards compatibility. Clients and the sidecar ship in same-version lockstep, so never add protocol or config versioning, runtime negotiation, fallbacks, or converters. Configs such as `CreateVmConfig` carry no `version` field; the single same-version wire handshake is the only version check. Change the protocol freely and update both sides together. +## Development + +### secure-exec dependency versions (`just`) + +Two independent version tracks: +- **secure-exec** — the `@secure-exec/*` npm packages and the `secure-exec-*` Cargo crates always share **one** version (npm and crates are kept in sync; pin both to the same ``). +- **`@agentos-software/*`** software packages (registry agents / WASM commands) are on a **separate** track and version independently of secure-exec. + +Manage them ONLY via these recipes (never hand-edit `path`/`version`/`catalog:` pins): +- `just secure-exec-local` — point deps at the sibling `../secure-exec` checkout for local hacking. +- `just secure-exec-set-version ` — pin secure-exec to a published version: sets the `@secure-exec/*` npm packages **and** the `secure-exec-*` crates (same ``, they're in sync) and switches to pinned mode. +- `just agentos-pkgs-set-version ` — pin the `@agentos-software/*` software packages (separate version track). + +### Depending on unreleased secure-exec changes + +agent-os builds against secure-exec crates + npm packages, so a secure-exec change must reach agent-os before it can be pushed. NEVER push with local (`path:`/`link:`) deps. Flow: preview-publish the secure-exec branch (the `preview-publish-secure-exec` skill), then `just secure-exec-set-version ` (pins npm + crates + switches to pinned mode), and push only that pinned state. Caveat: a preview publishes npm but the crates.io job is dry-run/skipped — a secure-exec *crate* change only flows locally (`secure-exec-local`) or via a real crates.io release. + +### Preview-publishing agent-os + +`just preview-publish ` dispatches `.github/workflows/publish.yaml` to cut a **preview** (debug build, npm-only, dist-tag = sanitized branch name) — for handing a build to an external project. **Preview-publish is for previews ONLY; never cut a release with it.** Releases go through `just release` (the `scripts/publish` flow). + +### Testing a local build from an external project (same machine) + +To consume an unpublished agent-os build in another project on this machine: +- **npm:** `pnpm -r build`, then either `pnpm pack` the package(s) and `npm install ./rivet-dev-agentos-*.tgz` in the external project, or add a `link:`/`file:` override (e.g. `"@rivet-dev/agentos": "link:/abs/path/agent-os/packages/agentos"`). The sidecar binary ships as `@rivet-dev/agentos-sidecar`. +- **cargo:** point the external Cargo project at the local crate via a path dep or `[patch.crates-io]` override (e.g. `[patch.crates-io] agentos-sidecar = { path = "/abs/path/agent-os/crates/agentos-sidecar" }`). + ## Security Model Trust model (decide which side of the boundary something is on before judging whether it is a security bug). Three components: @@ -41,6 +68,7 @@ Trust model (decide which side of the boundary something is on before judging wh ## Website And Docs +- External/consumer usage (installing `@rivet-dev/agentos` and using it in your own project) is documented in the website quickstart + Agents/Custom Software pages under `website/`, not in this file. This `CLAUDE.md` is contributor/maintainer-only. - The Agent OS website and docs live in `website/` (Astro + Starlight) and deploy to `agentos-sdk.dev` (docs at `agentos-sdk.dev/docs`). The marketing pages and docs were migrated out of `rivet.dev/agent-os` and `rivet.dev/docs/agent-os`, which now 301-redirect to this domain. - Docs styling is owned by the shared **`@rivet-dev/docs-theme`** repo (`github.com/rivet-dev/docs-theme`), consumed via `github:rivet-dev/docs-theme#` and wired in via `...docsTheme(starlight, siteConfig)`. To change any docs styling (palette, header, sidebar, code blocks, fonts), edit that repo and follow its CLAUDE.md release workflow — never restyle docs in `website/src`. This site owns only content + `website/docs.config.mjs` (sidebar icons via each item's `attrs['data-icon']`). - Architecture reference docs live in `website/src/content/docs/docs/architecture/` and are surfaced in `website/docs.config.mjs` under Reference → Advanced → Architecture. Treat these pages as the canonical human-facing architecture reference. When architecture behavior changes or new architecture is added, recommend the corresponding docs update to the user; do not proactively edit the docs unless the user asks for docs work or the task explicitly includes it. diff --git a/Cargo.lock b/Cargo.lock index 536634684..3d728de6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,8 @@ dependencies = [ "serde_json", "tokio", "tracing", + "tracing-appender", + "tracing-logfmt", "tracing-subscriber", ] @@ -2177,6 +2179,15 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "maybe-owned" version = "0.3.4" @@ -3446,6 +3457,12 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.117" @@ -3683,6 +3700,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -3715,16 +3745,33 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-logfmt" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a250055a3518b5efba928a18ffac8d32d42ea607a9affff4532144cd5b2e378e" +dependencies = [ + "nu-ansi-term", + "time", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", ] diff --git a/crates/agentos-sidecar/Cargo.toml b/crates/agentos-sidecar/Cargo.toml index 24d2e9151..fc203e832 100644 --- a/crates/agentos-sidecar/Cargo.toml +++ b/crates/agentos-sidecar/Cargo.toml @@ -18,9 +18,11 @@ agentos-protocol = { workspace = true } serde_json = "1.0" serde_bare = "0.5" secure-exec-sidecar = { workspace = true } -tokio = { version = "1", features = ["sync", "time"] } +tokio = { version = "1", features = ["sync", "time", "macros"] } tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["fmt"] } +tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] } +tracing-logfmt = { version = "0.3", features = ["ansi_logs"] } +tracing-appender = "0.2" [dev-dependencies] agentos-bridge = { workspace = true } diff --git a/crates/agentos-sidecar/src/acp_extension.rs b/crates/agentos-sidecar/src/acp_extension.rs index 320cdef3e..77ad81c28 100644 --- a/crates/agentos-sidecar/src/acp_extension.rs +++ b/crates/agentos-sidecar/src/acp_extension.rs @@ -109,22 +109,74 @@ impl AcpExtension { ctx: ExtensionContext<'_>, payload: &[u8], ) -> Result { + use tracing::Instrument as _; let request = decode_request(payload)?; - let response = match request { - AcpRequest::AcpCreateSessionRequest(request) => self.create_session(ctx, request).await, - AcpRequest::AcpGetSessionStateRequest(request) => { - AcpHandlerOutput::response(self.get_session_state(ctx, request).await) + let kind = Self::acp_request_kind(&request); + let start = std::time::Instant::now(); + tracing::info!(target: "agentos_sidecar::acp_extension", kind, "ext request received"); + + let work = async move { + match request { + AcpRequest::AcpCreateSessionRequest(request) => { + self.create_session(ctx, request).await + } + AcpRequest::AcpGetSessionStateRequest(request) => { + AcpHandlerOutput::response(self.get_session_state(ctx, request).await) + } + AcpRequest::AcpCloseSessionRequest(request) => { + AcpHandlerOutput::response(self.close_session(ctx, request).await) + } + AcpRequest::AcpSessionRequest(request) => self.session_request(ctx, request).await, + AcpRequest::AcpResumeSessionRequest(request) => { + self.resume_session(ctx, request).await + } } - AcpRequest::AcpCloseSessionRequest(request) => { - AcpHandlerOutput::response(self.close_session(ctx, request).await) + } + .instrument(tracing::info_span!( + target: "agentos_sidecar::acp_extension", + "ext.request", + kind + )); + + // Stall watchdog: while the request is in flight, warn periodically so a + // hang surfaces as a breadcrumb long before the host's 120s frame + // timeout. This never interrupts the work itself. + tokio::pin!(work); + let response = loop { + tokio::select! { + result = &mut work => break result, + _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { + tracing::warn!( + target: "agentos_sidecar::acp_extension", + kind, + elapsed_ms = start.elapsed().as_millis() as u64, + "ext request still pending — possible stall before response frame", + ); + } } - AcpRequest::AcpSessionRequest(request) => self.session_request(ctx, request).await, - AcpRequest::AcpResumeSessionRequest(request) => self.resume_session(ctx, request).await, }; + + tracing::info!( + target: "agentos_sidecar::acp_extension", + kind, + elapsed_ms = start.elapsed().as_millis() as u64, + "ext request handled", + ); let payload = encode_response(response.response.unwrap_or_else(error_response))?; ExtensionResponse::with_wire_events(payload, response.events) } + /// Stable label for an ACP request kind, used as a tracing field. + fn acp_request_kind(request: &AcpRequest) -> &'static str { + match request { + AcpRequest::AcpCreateSessionRequest(_) => "create_session", + AcpRequest::AcpGetSessionStateRequest(_) => "get_session_state", + AcpRequest::AcpCloseSessionRequest(_) => "close_session", + AcpRequest::AcpSessionRequest(_) => "session_request", + AcpRequest::AcpResumeSessionRequest(_) => "resume_session", + } + } + async fn create_session( &self, mut ctx: ExtensionContext<'_>, diff --git a/crates/agentos-sidecar/src/main.rs b/crates/agentos-sidecar/src/main.rs index cdc17b202..8b5c9a153 100644 --- a/crates/agentos-sidecar/src/main.rs +++ b/crates/agentos-sidecar/src/main.rs @@ -1,13 +1,8 @@ +use tracing_subscriber::fmt::writer::BoxMakeWriter; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; + fn main() { - // The sidecar speaks its binary protocol over stdout (see - // `secure_exec_sidecar::stdio`), so tracing output MUST go to stderr — - // otherwise log lines are interleaved into the frame stream and the client - // misreads them as (garbage-length) frames. See crates/CLAUDE.md: - // "Control channels must be out-of-band." - tracing_subscriber::fmt() - .with_writer(std::io::stderr) - .with_max_level(tracing::Level::ERROR) - .init(); + init_tracing(); if let Err(error) = secure_exec_sidecar::stdio::run_with_extensions(agentos_sidecar_wrapper::extensions()) { @@ -15,3 +10,81 @@ fn main() { std::process::exit(1); } } + +/// `1` => true, anything else => false. Mirrors rivet's `env_flag`. +fn env_flag(name: &str) -> bool { + std::env::var(name).map_or(false, |v| v == "1") +} + +/// Initialize tracing for the sidecar. +/// +/// Mirrors the rivet logging setup (`rivetkit-napi::init_tracing`): an +/// `EnvFilter`-gated subscriber with a logfmt formatter, so log level and +/// verbosity are runtime-configurable instead of hardcoded. +/// +/// Configuration (all optional): +/// - level: `AGENTOS_LOG_LEVEL` > `LOG_LEVEL` > `RUST_LOG` > `"info"`. +/// - format: `RUST_LOG_FORMAT=logfmt` (default) or `text`. +/// - sink: `AGENTOS_LOG_FILE` (append to that file) else stderr. +/// - field toggles: `RUST_LOG_{SPAN_NAME,SPAN_PATH,TARGET,LOCATION,MODULE_PATH,ANSI_COLOR}`. +/// +/// The sink MUST be stderr or a file — NEVER stdout, which carries the +/// sidecar's binary frame protocol (see crates/CLAUDE.md: "Control channels +/// must be out-of-band"). +fn init_tracing() { + // Level priority: AGENTOS_LOG_LEVEL > LOG_LEVEL > RUST_LOG > "info". + let directive = std::env::var("AGENTOS_LOG_LEVEL") + .ok() + .or_else(|| std::env::var("LOG_LEVEL").ok()) + .or_else(|| std::env::var("RUST_LOG").ok()) + .unwrap_or_else(|| "info".to_string()); + let env_filter = EnvFilter::try_new(&directive).unwrap_or_else(|_| EnvFilter::new("info")); + + // Sink: a file if AGENTOS_LOG_FILE is set, else stderr. Never stdout. + let writer: BoxMakeWriter = match std::env::var("AGENTOS_LOG_FILE") { + Ok(path) if !path.is_empty() => { + let path = std::path::PathBuf::from(path); + let dir = path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .map(|p| p.to_path_buf()) + .unwrap_or_else(|| std::path::PathBuf::from(".")); + let name = path + .file_name() + .map(std::ffi::OsString::from) + .unwrap_or_else(|| std::ffi::OsString::from("agentos-sidecar.log")); + BoxMakeWriter::new(tracing_appender::rolling::never(dir, name)) + } + _ => BoxMakeWriter::new(std::io::stderr), + }; + + let registry = tracing_subscriber::registry().with(env_filter); + + let text_format = std::env::var("RUST_LOG_FORMAT") + .map(|v| v.eq_ignore_ascii_case("text")) + .unwrap_or(false); + + if text_format { + registry + .with( + tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_writer(writer), + ) + .init(); + } else { + registry + .with( + tracing_logfmt::builder() + .with_span_name(env_flag("RUST_LOG_SPAN_NAME")) + .with_span_path(env_flag("RUST_LOG_SPAN_PATH")) + .with_target(env_flag("RUST_LOG_TARGET")) + .with_location(env_flag("RUST_LOG_LOCATION")) + .with_module_path(env_flag("RUST_LOG_MODULE_PATH")) + .with_ansi_color(env_flag("RUST_LOG_ANSI_COLOR")) + .layer() + .with_writer(writer), + ) + .init(); + } +} diff --git a/justfile b/justfile index 47ffc0813..f4ef757ca 100644 --- a/justfile +++ b/justfile @@ -6,34 +6,22 @@ release *args: preview-publish REF: gh workflow run .github/workflows/publish.yaml --ref "{{ REF }}" -# Point the workspace at PUBLISHED secure-exec versions (CI/release default). -secure-exec-pinned: - node scripts/secure-exec-dep.mjs pinned - -# Point the workspace at the sibling ../secure-exec checkout for local hacking. +# Point deps at the sibling ../secure-exec checkout for local hacking. secure-exec-local: node scripts/secure-exec-dep.mjs local -# Bump the pinned @secure-exec/* npm version (core/s3/google-drive/sandbox). +# Pin secure-exec to a published version. The @secure-exec/* npm packages and the +# secure-exec-* crates are always the same version, so this sets both and switches +# to pinned mode. secure-exec-set-version VERSION: + node scripts/secure-exec-dep.mjs pinned node scripts/secure-exec-dep.mjs set-secure-exec-version "{{ VERSION }}" + node scripts/secure-exec-dep.mjs set-crate-version "{{ VERSION }}" -# Bump the pinned @agentos-software/* software-package npm version. +# Pin the @agentos-software/* software packages (separate version track). agentos-pkgs-set-version VERSION: node scripts/secure-exec-dep.mjs set-agentos-pkgs-version "{{ VERSION }}" -# Bump BOTH scopes at once (only when secure-exec + software publish in lockstep). -secure-exec-set-all-versions VERSION: - node scripts/secure-exec-dep.mjs set-version "{{ VERSION }}" - -# Bump the @secure-exec/* crate version requirement (must match the sibling crate version). -secure-exec-set-crate-version VERSION: - node scripts/secure-exec-dep.mjs set-crate-version "{{ VERSION }}" - -# Show the current secure-exec dependency mode + pinned versions. -secure-exec-status: - node scripts/secure-exec-dep.mjs status - dev-shell *args: pnpm --filter @rivet-dev/agentos-dev-shell dev-shell -- "$@" diff --git a/packages/core/package.json b/packages/core/package.json index db0186e68..ea73648b0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,7 +65,9 @@ "googleapis": "^144.0.0", "isolated-vm": "^6.0.0", "long-timeout": "^0.1.1", - "minimatch": "^10.2.4" + "minimatch": "^10.2.4", + "zod": "^4.1.11", + "zod-to-json-schema": "^3.25.2" }, "devDependencies": { "@anthropic-ai/claude-agent-sdk": "^0.2.87", @@ -107,6 +109,6 @@ "typescript": "^5.7.2", "vitest": "^2.1.8", "ws": "^8.18.0", - "zod": "^4.1.11" + "zod3": "npm:zod@^3.25.76" } } diff --git a/packages/core/src/host-tools-zod.ts b/packages/core/src/host-tools-zod.ts index fe29815e3..5e4078d4e 100644 --- a/packages/core/src/host-tools-zod.ts +++ b/packages/core/src/host-tools-zod.ts @@ -1,4 +1,5 @@ import type { ZodType } from "zod"; +import { zodToJsonSchema as zodV3ToJsonSchema } from "zod-to-json-schema"; const OPTIONAL_WRAPPER_TYPES = new Set(["default", "optional"]); const TRANSPARENT_WRAPPER_TYPES = new Set([ @@ -40,6 +41,9 @@ export class HostToolSchemaConversionError extends Error { } function getSchemaDef(schema: unknown): JsonObject { + if (!schema || typeof schema !== "object") { + return {}; + } return (( schema as { _def?: JsonObject; @@ -195,7 +199,8 @@ function validateSchema(schema: ZodType, path: string) { } if (typeName === "array") { - const itemSchema = getSchemaDef(schema).element as ZodType | undefined; + const def = getSchemaDef(schema); + const itemSchema = (def.element ?? def.type) as ZodType | undefined; if (!itemSchema) { throw new HostToolSchemaConversionError( path, @@ -246,8 +251,12 @@ function validateSchema(schema: ZodType, path: string) { } if (typeName === "literal") { - const values = getSchemaDef(schema).values; - const literalValues = Array.isArray(values) ? values : []; + const def = getSchemaDef(schema); + const literalValues = Array.isArray(def.values) + ? def.values + : "value" in def + ? [def.value] + : []; const [literalValue] = literalValues; if ( literalValues.length !== 1 || @@ -331,19 +340,32 @@ function findUnsupportedGeneratedKeyword( return null; } -export function zodToJsonSchema(schema: ZodType): unknown { - validateSchema(schema, "$"); - - const jsonSchema = ( +function generateJsonSchema(schema: ZodType): unknown { + const nativeJsonSchema = ( schema as ZodType & { toJSONSchema?: () => unknown } ).toJSONSchema?.(); - if (!jsonSchema) { + if (nativeJsonSchema) { + return nativeJsonSchema; + } + + const generated = zodV3ToJsonSchema(schema as never, { + $refStrategy: "none", + target: "jsonSchema7", + }); + if (!generated || (typeof generated === "object" && Object.keys(generated).length === 0)) { throw new HostToolSchemaConversionError( "$", displayTypeName(normalizeTypeName(schema)), - "schema does not expose toJSONSchema()", + "schema cannot be converted to JSON Schema", ); } + return generated; +} + +export function zodToJsonSchema(schema: ZodType): unknown { + validateSchema(schema, "$"); + + const jsonSchema = generateJsonSchema(schema); const unsupportedKeyword = findUnsupportedGeneratedKeyword(jsonSchema, "$"); if (unsupportedKeyword) { diff --git a/packages/core/tests/host-tools-zod.test.ts b/packages/core/tests/host-tools-zod.test.ts index 96023ca08..e9580dad5 100644 --- a/packages/core/tests/host-tools-zod.test.ts +++ b/packages/core/tests/host-tools-zod.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vitest"; import { z } from "zod"; +import { z as z3 } from "zod3"; import { HostToolSchemaConversionError, zodToJsonSchema, @@ -87,6 +88,60 @@ describe("zodToJsonSchema", () => { }); }); + test("converts equivalent Zod v3 schemas", () => { + const schema = z3.object({ + url: z3 + .string() + .min(1) + .max(128) + .regex(/^https?:\/\//) + .describe("Target URL"), + fullPage: z3.boolean().optional(), + format: z3.enum(["png", "jpg"]).describe("Image format"), + width: z3.number().min(320).max(1920).optional(), + tags: z3.array(z3.string()), + mode: z3.union([z3.literal("fast"), z3.literal("safe")]), + env: z3.record(z3.string(), z3.string()).optional(), + }); + + expect(zodToJsonSchema(schema)).toEqual({ + type: "object", + properties: { + url: { + type: "string", + minLength: 1, + maxLength: 128, + pattern: "^https?:\\/\\/", + description: "Target URL", + }, + fullPage: { type: "boolean" }, + format: { + type: "string", + enum: ["png", "jpg"], + description: "Image format", + }, + width: { + type: "number", + minimum: 320, + maximum: 1920, + }, + tags: { + type: "array", + items: { type: "string" }, + }, + mode: { + type: "string", + enum: ["fast", "safe"], + }, + env: { + type: "object", + additionalProperties: { type: "string" }, + }, + }, + required: ["url", "format", "tags", "mode"], + }); + }); + test("throws a typed error for unsupported discriminated unions with the offending path", () => { const schema = z.object({ payload: z.discriminatedUnion("kind", [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e4757555..74d4c2f99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,20 +82,20 @@ catalogs: specifier: 0.3.0-rc.2 version: 0.3.0-rc.2 '@secure-exec/core': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/google-drive': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/nodejs': specifier: 0.2.1 version: 0.2.1 '@secure-exec/s3': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/sandbox': - specifier: 0.0.0-nathan-workspace-binding-agentos.83db3e3 - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + specifier: 0.0.0-codex-native-sidecar-platform-ci.117d645 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 overrides: '@rivet-dev/agentos-core': workspace:* @@ -128,7 +128,7 @@ importers: version: link:packages/core '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@types/node': specifier: ^22.19.15 version: 22.19.15 @@ -208,7 +208,7 @@ importers: version: link:../../packages/agentos-sandbox '@secure-exec/s3': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 dockerode: specifier: ^4.0.9 version: 4.0.10 @@ -279,7 +279,7 @@ importers: version: link:../core '@secure-exec/sandbox': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) + version: 0.0.0-codex-native-sidecar-platform-ci.117d645(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) sandbox-agent: specifier: ^0.4.2 version: 0.4.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) @@ -335,7 +335,7 @@ importers: version: 0.6.2 '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@xterm/headless': specifier: ^6.0.0 version: 6.0.0 @@ -357,6 +357,12 @@ importers: minimatch: specifier: ^10.2.4 version: 10.2.5 + zod: + specifier: ^4.1.11 + version: 4.3.6 + zod-to-json-schema: + specifier: ^3.25.2 + version: 3.25.2(zod@4.3.6) devDependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -453,10 +459,10 @@ importers: version: link:../agentos-sandbox '@secure-exec/google-drive': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/s3': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -475,9 +481,9 @@ importers: ws: specifier: ^8.18.0 version: 8.20.0(bufferutil@4.1.0) - zod: - specifier: ^4.1.11 - version: 4.3.6 + zod3: + specifier: npm:zod@^3.25.76 + version: zod@3.25.76 packages/dev-shell: dependencies: @@ -536,7 +542,7 @@ importers: dependencies: '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 devDependencies: '@types/node': specifier: ^22.10.2 @@ -558,7 +564,7 @@ importers: dependencies: '@secure-exec/core': specifier: 'catalog:' - version: 0.0.0-nathan-workspace-binding-agentos.83db3e3 + version: 0.0.0-codex-native-sidecar-platform-ci.117d645 pyodide: specifier: ^0.28.3 version: 0.28.3(bufferutil@4.1.0) @@ -2917,50 +2923,50 @@ packages: resolution: {integrity: sha512-trO//ypJBSt5xkewuol9LOykvDgHwUXq8R+yQVS+0CmpN3lYUtewHkb+At9RVGRhDMmJZY2oasaXDnhfurQ33w==} hasBin: true - '@secure-exec/core@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-GKGY8U1wI/R/Rl8WTqQ+xLqu2FALZL0XzfIxolZVhfR/iy7mwkSN5phO6cimM5qjxwSTkTCEEvoVrBoakedRBQ==} + '@secure-exec/core@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-bomEzynHLrGIF+k1S6zIQHJwdQw8V/WfEGKL9nj+iMC8OlfDnaJ26SwV8Yk3M7KwjKpznQsMNmPeIK5iFZ0s8A==} '@secure-exec/core@0.2.1': resolution: {integrity: sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg==} - '@secure-exec/google-drive@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-zhd2O7jit5rghIQN8n+GZtpiiwYni8IhNHZOEgWcK0/UlYL5FecdEhGLeCo0rWTXV7WFPGfGPhX3aJOQKgnwOA==} + '@secure-exec/google-drive@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-3s7entAvaBHOYDHpg4g5dbaC91FsbZaHProA0B75kk6+EV2VLb7s0pAgRips9J1slh4RdDOolw6TRd8CrHXHsg==} '@secure-exec/nodejs@0.2.1': resolution: {integrity: sha512-UJMJqVFxexlHJV0Q9nWURvrz6GElj8673DDOOFln6FHR6JS+9SaSU3eISrN158DuNC3SFi4rgjb/scKnK4YOYQ==} - '@secure-exec/s3@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-/4w480tR0UZh0ohUTyjDipkEVchJsflEFPTeeYYBEkDphEoDWz1PlshpAQWBGTkC3sRd9QyhzEsEXVSNMvrmQA==} + '@secure-exec/s3@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-Q5ESnaQkPc1PxDXO//Yysk3MxJOE9xKA2EVvZjKmdqyKyiesgnV1/oik4LKb8JGBYO63mokOqprxk7MO9R7g5w==} - '@secure-exec/sandbox@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-yVninZkFt4BHtgNX2d3bGKqlORdlrIXvQLpOnqpaeMIHbP0IJSbGd7jRbk4HAFYRo8JKcEHo/dr8dOUBUF9lPQ==} + '@secure-exec/sandbox@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-zjwLoKGIJhSggKfEdcRrXv6AXJUiFvjCCPWy/v3R2uGr5EZTUp6KsECmzggzcT/6OxN54A46Eb/8ysZgEiyfbA==} - '@secure-exec/sidecar-darwin-arm64@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-3YRc7fe9tt7HRpeWXN6UXaiswLOfRmuUrLJkKPL03Bbxw+sOOiHNBzcmVRkoR4nMOfrv11sAwudIlydSylxW1Q==} + '@secure-exec/sidecar-darwin-arm64@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-cQe2UnN7lj4htBSablaPCv6kmzybUADXyWCfRRQKKzV5UD204+Hh63wtZ/wyzs4N7kQaoGqtVSNfskR0c4/Z+A==} engines: {node: '>=20'} cpu: [arm64] os: [darwin] - '@secure-exec/sidecar-darwin-x64@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-oGN8HWR6VnL22BjZeoJ/MVQTqGWzfLhpx4x4Pjw11cX6FU7/7sEP54zHfp8q+8f7zOHI0+plDOHXYEQ+mIi15g==} + '@secure-exec/sidecar-darwin-x64@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-KfDXmf65TDgD9fKA/fuIpqgsSmy6mGWfvKNGOELNii9aAj19aw5DKt0Sh9AjL4BVTkKxlkQRfhctTuYTF+LyzQ==} engines: {node: '>=20'} cpu: [x64] os: [darwin] - '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-+V75dmjpM87mjqW0oJZqbwCAqirKFeP6hRRIb+Ujrqy4rBuWCDC8QZdLZacF0S7gn8sWW7GcbBScTY2mgb2Q4A==} + '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-antjZRycgVuCcnQRoZV+By0isj6d1d0s/MbZs8t6Ssz4NNJKHeiRZUMEZ8H70Irywh1iFLcilP7hfau1nA4z5w==} engines: {node: '>=20'} cpu: [arm64] os: [linux] - '@secure-exec/sidecar-linux-x64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-OS+dS3WnX5HeBI0deeL/0weLeSUGGLofYM4pklemjH19V1TynGs2eZw4Wrkeh2IRmsK+DtNGwZ730yra4AVW8g==} + '@secure-exec/sidecar-linux-x64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-sXlgTa7r9x0Y8g35RJbUqwk3V7J/Rz/U6p4kMQ77tD+j7tj4L0sxDZn8n02z40iA7+OFD58BImoKOUNHOzEMkg==} engines: {node: '>=20'} cpu: [x64] os: [linux] - '@secure-exec/sidecar@0.0.0-nathan-workspace-binding-agentos.83db3e3': - resolution: {integrity: sha512-TsRIzKQfBH6CBRBdY17pHwZudMieuMar2xoLssbOGKnb3C5BvKMt/3yaANerPxVxH3LYs8KLjyLaCujSaRErSA==} + '@secure-exec/sidecar@0.0.0-codex-native-sidecar-platform-ci.117d645': + resolution: {integrity: sha512-kij9InI4RXl2ZLPZKhtcNMYiq/wQQjejq5yOuJnXULeH9iNG7IrGeQ6bMh346ef7mxUL1XnT7Ptv57gB8cWukg==} engines: {node: '>=20'} '@secure-exec/v8-darwin-arm64@0.2.1': @@ -9697,18 +9703,19 @@ snapshots: '@sandbox-agent/cli-win32-x64': 0.4.2 optional: true - '@secure-exec/core@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/core@0.0.0-codex-native-sidecar-platform-ci.117d645': dependencies: '@rivetkit/bare-ts': 0.6.2 - '@secure-exec/sidecar': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/sidecar': 0.0.0-codex-native-sidecar-platform-ci.117d645 + zod: 4.3.6 '@secure-exec/core@0.2.1': dependencies: better-sqlite3: 12.8.0 - '@secure-exec/google-drive@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/google-drive@0.0.0-codex-native-sidecar-platform-ci.117d645': dependencies: - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/nodejs@0.2.1': dependencies: @@ -9721,13 +9728,13 @@ snapshots: node-stdlib-browser: 1.3.1 web-streams-polyfill: 4.2.0 - '@secure-exec/s3@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/s3@0.0.0-codex-native-sidecar-platform-ci.117d645': dependencies: - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 - '@secure-exec/sandbox@0.0.0-nathan-workspace-binding-agentos.83db3e3(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6)': + '@secure-exec/sandbox@0.0.0-codex-native-sidecar-platform-ci.117d645(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6)': dependencies: - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 sandbox-agent: 0.4.2(dockerode@4.0.10)(get-port@7.2.0)(zod@4.3.6) transitivePeerDependencies: - '@cloudflare/sandbox' @@ -9741,24 +9748,24 @@ snapshots: - modal - zod - '@secure-exec/sidecar-darwin-arm64@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-darwin-arm64@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar-darwin-x64@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-darwin-x64@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-linux-arm64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar-linux-x64-gnu@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar-linux-x64-gnu@0.0.0-codex-native-sidecar-platform-ci.117d645': optional: true - '@secure-exec/sidecar@0.0.0-nathan-workspace-binding-agentos.83db3e3': + '@secure-exec/sidecar@0.0.0-codex-native-sidecar-platform-ci.117d645': optionalDependencies: - '@secure-exec/sidecar-darwin-arm64': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sidecar-darwin-x64': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sidecar-linux-arm64-gnu': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sidecar-linux-x64-gnu': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/sidecar-darwin-arm64': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sidecar-darwin-x64': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sidecar-linux-arm64-gnu': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sidecar-linux-x64-gnu': 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/v8-darwin-arm64@0.2.1': optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2d7787290..b097d6963 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,9 +59,9 @@ catalog: '@agentos-software/unzip': 0.3.0-rc.2 '@agentos-software/yq': 0.3.0-rc.2 '@agentos-software/zip': 0.3.0-rc.2 - '@secure-exec/core': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/google-drive': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/core': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/google-drive': 0.0.0-codex-native-sidecar-platform-ci.117d645 '@secure-exec/nodejs': 0.2.1 - '@secure-exec/s3': 0.0.0-nathan-workspace-binding-agentos.83db3e3 - '@secure-exec/sandbox': 0.0.0-nathan-workspace-binding-agentos.83db3e3 + '@secure-exec/s3': 0.0.0-codex-native-sidecar-platform-ci.117d645 + '@secure-exec/sandbox': 0.0.0-codex-native-sidecar-platform-ci.117d645 # <<< secure-exec catalog <<< diff --git a/website/.astro/content-modules.mjs b/website/.astro/content-modules.mjs index 57f6d061a..dfea5d849 100644 --- a/website/.astro/content-modules.mjs +++ b/website/.astro/content-modules.mjs @@ -1,18 +1,19 @@ export default new Map([ -["src/content/docs/docs/agent-to-agent.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fagent-to-agent.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/approvals.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fapprovals.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/benchmarks.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fbenchmarks.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/bindings.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fbindings.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/agent-to-agent.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fagent-to-agent.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/authentication.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fauthentication.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/architecture.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/benchmarks.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fbenchmarks.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/bindings.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fbindings.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/core.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fcore.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/cost-evaluation.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fcost-evaluation.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/crash-course.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fcrash-course.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/cron.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fcron.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/debugging.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fdebugging.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/deployment.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fdeployment.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/filesystem.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Ffilesystem.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/index.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Findex.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/deployment.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fdeployment.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/js-runtime.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fjs-runtime.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/limitations.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Flimitations.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/llm-credentials.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fllm-credentials.mdx&astroContentModuleFlag=true")], @@ -22,13 +23,13 @@ export default new Map([ ["src/content/docs/docs/permissions.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fpermissions.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/persistence.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fpersistence.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/processes.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fprocesses.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/resource-limits.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fresource-limits.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/quickstart.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fquickstart.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/sandbox.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fsandbox.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/resource-limits.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fresource-limits.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/security-model.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fsecurity-model.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/sessions.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fsessions.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/software.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fsoftware.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/system-prompt.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fsystem-prompt.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/sessions.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fsessions.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/versus-sandbox.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fversus-sandbox.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/webhooks.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fwebhooks.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/workflows.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Fworkflows.mdx&astroContentModuleFlag=true")], @@ -41,7 +42,7 @@ export default new Map([ ["src/content/docs/docs/architecture/compiler-toolchain.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fcompiler-toolchain.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/architecture/filesystem.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Ffilesystem.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/architecture/networking.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fnetworking.mdx&astroContentModuleFlag=true")], +["src/content/docs/docs/architecture/posix-syscalls.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fposix-syscalls.mdx&astroContentModuleFlag=true")], ["src/content/docs/docs/architecture/processes.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fprocesses.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/architecture/sessions-persistence.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fsessions-persistence.mdx&astroContentModuleFlag=true")], -["src/content/docs/docs/architecture/posix-syscalls.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fposix-syscalls.mdx&astroContentModuleFlag=true")]]); +["src/content/docs/docs/architecture/sessions-persistence.mdx", () => import("astro:content-layer-deferred-module?astro%3Acontent-layer-deferred-module=&fileName=src%2Fcontent%2Fdocs%2Fdocs%2Farchitecture%2Fsessions-persistence.mdx&astroContentModuleFlag=true")]]); \ No newline at end of file diff --git a/website/.astro/data-store.json b/website/.astro/data-store.json index a0da05b5a..49507b382 100644 --- a/website/.astro/data-store.json +++ b/website/.astro/data-store.json @@ -1 +1 @@ -[["Map",1,2,9,10],"meta::meta",["Map",3,4,5,6,7,8],"astro-version","5.18.2","content-config-digest","3edc204469c72622","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://agentos-sdk.dev\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"where\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":false,\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[null,null,null],\"rehypePlugins\":[null,[null,{\"experimentalHeadingIdCompat\":false}],null,[null,{\"themes\":[\"github-dark-default\"],\"defaultLocale\":\"en\",\"cascadeLayer\":\"starlight.components\",\"styleOverrides\":{\"borderRadius\":\"0.75rem\",\"borderWidth\":\"1px\",\"codePaddingBlock\":\"0.75rem\",\"codePaddingInline\":\"1rem\",\"codeFontFamily\":\"\\\"JetBrains Mono\\\", ui-monospace, SFMono-Regular, Menlo, monospace\",\"codeFontSize\":\"var(--sl-text-code)\",\"codeLineHeight\":\"var(--sl-line-height)\",\"uiFontFamily\":\"var(--__sl-font)\",\"textMarkers\":{\"lineDiffIndicatorMarginLeft\":\"0.25rem\",\"defaultChroma\":\"45\",\"backgroundOpacity\":\"60%\"},\"borderColor\":\"rgba(244, 241, 231, 0.1)\",\"codeBackground\":\"#0a0a0a\",\"frames\":{\"editorTabBarBackground\":\"#111110\",\"editorTabBarBorderBottomColor\":\"rgba(244, 241, 231, 0.1)\",\"editorActiveTabBackground\":\"#0a0a0a\",\"editorActiveTabIndicatorBottomColor\":\"#cb5a33\",\"terminalTitlebarBackground\":\"#111110\",\"terminalBackground\":\"#0a0a0a\",\"frameBoxShadowCssValue\":\"none\"}},\"plugins\":[{\"name\":\"Starlight Plugin\",\"hooks\":{}},{\"name\":\"astro-expressive-code\",\"hooks\":{}}],\"removeUnusedThemes\":false}]],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false},\"prefetch\":{\"prefetchAll\":true},\"i18n\":{\"defaultLocale\":\"en\",\"locales\":[\"en\"],\"routing\":{\"prefixDefaultLocale\":false,\"redirectToDefaultLocale\":false,\"fallbackType\":\"redirect\"}}}","docs",["Map",11,12,25,26,36,37,47,48,58,59,69,70,80,81,91,92,102,103,113,114,124,125,9,135,144,145,155,156,166,167,177,178,188,189,199,200,210,211,221,222,232,233,243,244,254,255,265,266,276,277,287,288,298,299,309,310,320,321,331,332,342,343,353,354,364,365,375,376,386,387,397,398,408,409,419,420,430,431,441,442,451,452,462,463,473,474],"docs/agent-to-agent",{"id":11,"data":13,"body":22,"filePath":23,"digest":24,"deferredRender":16},{"title":14,"description":15,"editUrl":16,"head":17,"template":18,"sidebar":19,"pagefind":16,"draft":20},"Agent-to-Agent Communication","Use bindings to let agents communicate with each other.",true,[],"doc",{"hidden":20,"attrs":21},false,{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nAgents communicate through [bindings](/docs/bindings). You define a bindings group that lets one agent send work to another, and the agent calls it like any other CLI command.\n\n## Example: code writer + reviewer\n\nThis example gives the writer agent a `review` binding. The writer sends the file's full contents (the VMs share no filesystem), and the binding writes them into a separate reviewer VM and sends a review prompt back through the reviewer.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport { z } from \"zod\";\n\n// The reviewer is its own isolated agent VM.\nconst reviewer = agentOS({});\n\n// Bridge the writer to the reviewer. The VMs share no filesystem, so the writer\n// sends the full file contents; the bridge writes them into the reviewer's VM\n// and asks the reviewer to review. Runs on the host.\nasync function reviewCode(code: string): Promise\u003Cstring> {\n const client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n const reviewerHandle = client.reviewer.getOrCreate(\"my-project\");\n\n // Write the submitted contents into the reviewer's VM.\n await reviewerHandle.writeFile(\"/home/user/review.ts\", code);\n\n // Ask the reviewer to review.\n const session = await reviewerHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n const result = await reviewerHandle.sendPrompt(\n session.sessionId,\n \"Review the code at /home/user/review.ts and list any issues.\",\n );\n await reviewerHandle.closeSession(session.sessionId);\n\n return result.text;\n}\n\n// The writer agent gets a `review` binding. When the writer runs\n// `agentos-review submit`, the bridge above executes on the host.\nconst writer = agentOS({\n bindings: [\n {\n name: \"review\",\n description: \"Send code to the reviewer agent and get back a review.\",\n bindings: {\n submit: {\n description:\n \"Submit the full contents of a file to the reviewer agent for review. Returns the reviewer's feedback as text.\",\n inputSchema: z.object({\n code: z.string().describe(\"The full source code to review.\"),\n }),\n execute: async (input: { code: string }) => ({\n review: await reviewCode(input.code),\n }),\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { writer, reviewer } });\n\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst writerAgent = client.writer.getOrCreate(\"my-project\");\n\nconst session = await writerAgent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// The writer calls the `review` binding, which bridges to the reviewer VM.\nawait writerAgent.sendPrompt(\n session.sessionId,\n \"Write a small REST API, then send it to the review agent for review.\",\n);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/agent-to-agent)*\n\nThe writer agent sees the review binding as a CLI command. Because the VMs share no filesystem, it sends the full file contents, not a path:\n\n```bash\nagentos-review submit --code \"$(cat api.ts)\"\n```\n\nThe binding writes the contents into the reviewer's VM, prompts the reviewer, and returns the review to the writer as JSON.\n\n## Why bindings?\n\nBindings are the natural communication layer between agents because:\n\n- **The agent doesn't need to know about other agents.** It just calls a binding. You can swap the implementation without changing the agent's behavior.\n- **No credentials in the VM.** The binding executes on the server, so it can access other agents directly without exposing connection details.\n- **Composable.** Chain any number of agents by adding more bindings. Each binding is a self-contained bridge to another agent.\n\n## Recommendations\n\n- Each agent has its own isolated VM and filesystem (they share no filesystem). Pass file contents through the binding input, then use `writeFile` in the binding to land them in the other VM.\n- Use [Workflows](/docs/workflows) to make multi-agent pipelines durable across restarts.","src/content/docs/docs/agent-to-agent.mdx","41a0f3d8af8944e6","docs/approvals",{"id":25,"data":27,"body":33,"filePath":34,"digest":35,"deferredRender":16},{"title":28,"description":29,"editUrl":16,"head":30,"template":18,"sidebar":31,"pagefind":16,"draft":20},"Approvals","Approve or deny agent tool use with human-in-the-loop or auto-approve patterns.",[],{"hidden":20,"attrs":32},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nWhen an agent wants to use a tool (write a file, run a command, etc.), it asks for permission. You approve or deny that request, either interactively or with a server-side hook.\n\n- **Human-in-the-loop**: subscribe to `permissionRequest` on the client and respond per-request.\n- **Auto-approve**: use the `onPermissionRequest` server hook to decide without a client round-trip.\n- **Selective approval**: inspect the request and approve some, forward others to the client.\n\n## Permission request flow\n\nWhen an agent wants to use a tool, it emits a `permissionRequest`. Every request is delivered to two places at once, and you respond from whichever fits your app:\n\n- **On the client**: subscribe to the `permissionRequest` event and call `respondPermission(sessionId, permissionId, reply)`.\n- **On the server**: the `onPermissionRequest` hook on the actor runs for every request, with no client round-trip.\n- If neither responds, the request blocks until a reply arrives, then rejects after 120 seconds.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Listen for permission requests over a live connection. The payload is\n// inferred from the actor's event schema, so no cast is needed.\nconst conn = agent.connect();\nconn.on(\"permissionRequest\", async (data) => {\n console.log(\"Permission requested:\", data.request);\n\n // Approve this single request.\n await agent.respondPermission(\n data.sessionId,\n data.request.permissionId,\n \"once\",\n );\n});\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Create a new file at /home/user/output.txt\");\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n // Runs server-side for every permission request, before any client round-trip.\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"permission requested:\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\nThe `permissionRequest` event payload:\n\n- **`data.sessionId`**: the session the request belongs to.\n- **`data.request.permissionId`**: the id to pass back to `respondPermission`.\n- **`data.request.description`**: human-readable summary of the requested action.\n- **`data.request.params`**: raw ACP permission details (requested tool, paths, etc.).\n\nReply options for `respondPermission`:\n\n| Reply | Behavior |\n|-------|----------|\n| `\"once\"` | Approve this single request |\n| `\"always\"` | Approve this and all future requests of the same type |\n| `\"reject\"` | Deny the request |\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/approvals)*\n\n## Patterns\n\n### Auto-approve\n\nThe `onPermissionRequest` hook runs server-side for every permission request before it reaches any client. Useful for fully automated pipelines.\n\n- **Signature**: `onPermissionRequest: async (sessionId, request) => { ... }`.\n- **Inspect**: `request.permissionId`, `request.description`, and `request.params`.\n- **Anything not handled** in the hook is forwarded to the client via the `permissionRequest` event.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n // The onPermissionRequest hook runs server-side for every request before it\n // is forwarded to clients. Use it to inspect requests in fully automated\n // pipelines without a client round-trip.\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"auto-approving\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// No need to handle permissions on the client. The server hook handles them.\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Write files as needed\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/approvals)*\n\n### Selective approval\n\nInspect the permission request to make approval decisions based on the tool or path. Approve some server-side, forward the rest to the client for human review.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n onPermissionRequest: async (sessionId, request) => {\n // `request.description` and `request.params` carry the raw ACP permission\n // details (the requested tool, paths, etc.). Inspect them to decide which\n // requests to handle server-side and which to forward to clients.\n const description = request.description ?? \"\";\n if (description.toLowerCase().includes(\"read\")) {\n console.log(\"read request handled server-side\", sessionId, request.permissionId);\n }\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Permission requests forwarded by the server reach the client here. The\n// payload is inferred from the actor's event schema, so no cast is needed.\nconst conn = agent.connect();\nconn.on(\"permissionRequest\", async (data) => {\n const approved = confirm(`Allow: ${JSON.stringify(data.request)}?`);\n await agent.respondPermission(\n data.sessionId,\n data.request.permissionId,\n approved ? \"once\" : \"reject\",\n );\n});\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Read config.json and update it\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/approvals)*\n\n- For interactive applications, subscribe to `permissionRequest` on the client and build an approval UI.\n- If neither the server hook nor the client responds, the agent blocks until a response is given or the action times out.","src/content/docs/docs/approvals.mdx","f010ed9546f3e16c","docs/architecture",{"id":36,"data":38,"body":44,"filePath":45,"digest":46,"deferredRender":16},{"title":39,"description":40,"editUrl":16,"head":41,"template":18,"sidebar":42,"pagefind":16,"draft":20},"Overview","A high-level tour of how agentOS works: the client / server / VM picture, the anatomy of a Linux VM (kernel + executor), agents and sessions, and the Rivet Actor orchestration underneath.",[],{"hidden":20,"attrs":43},{},"import { Aside } from '@astrojs/starlight/components';\n\nagentOS runs AI agents and untrusted code safely inside fully virtualized Linux VMs. Nothing the guest does touches your host directly: there is no real host filesystem, no real host network socket, and no real host process. Every guest operation is serviced by a kernel that agentOS owns.\n\nThis page is a high-level tour. It walks through the overall shape, the parts that make up a VM, how agent sessions work, and the orchestration layer underneath. Each section links out to a detailed page when you want to go deeper.\n\n## The big picture\n\nA running agentOS system has three roles: your **app** (the client), your **server** (which runs the sidecar that hosts the VMs), and the **VM** where guest code actually runs. Your app never runs guest code itself, it asks the server to.\n\n\u003Csvg viewBox=\"0 0 400 210\" role=\"img\" aria-label=\"A client (JavaScript, browser, or another backend) connects to an agentOS server. The server runs a sidecar that hosts many isolated VMs, each marked with the agentOS 'OS' logo; the sidecar brokers all guest syscalls and isolates each agent.\" style=\"width:100%;height:auto;max-width:420px;display:block;margin:2.5rem auto 0.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"bp-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003Csymbol id=\"bp-os\" viewBox=\"0 0 100 100\">\n \u003Crect x=\"8\" y=\"8\" width=\"84\" height=\"84\" rx=\"26\" fill=\"none\" stroke=\"#1b1916\" stroke-width=\"8\" />\n \u003Ctext x=\"50\" y=\"50\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"38\" fill=\"#1b1916\">OS\u003C/text>\n \u003C/symbol>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"67\" width=\"140\" height=\"60\" rx=\"12\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"82\" y=\"92\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"15\" font-weight=\"600\" fill=\"#1b1916\">Client\u003C/text>\n \u003Ctext x=\"82\" y=\"112\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">JS · Browser · Backend\u003C/text>\n \u003Cline x1=\"154\" y1=\"97\" x2=\"205\" y2=\"97\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#bp-arrow)\" />\n \u003Crect x=\"210\" y=\"40\" width=\"164\" height=\"114\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"224\" y=\"62\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Server\u003C/text>\n \u003Cg fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\">\n \u003Crect x=\"224\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"224\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003C/g>\n \u003Cg>\n \u003Cuse href=\"#bp-os\" x=\"229\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"265\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"301\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"337\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"229\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"265\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"301\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"337\" y=\"117\" width=\"18\" height=\"18\" />\n \u003C/g>\n \u003Cg>\n \u003Crect x=\"150\" y=\"170\" width=\"15\" height=\"15\" rx=\"4\" fill=\"none\" stroke=\"#56524a\" stroke-width=\"1.4\" />\n \u003Ctext x=\"157.5\" y=\"178\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"7\" fill=\"#56524a\">OS\u003C/text>\n \u003Ctext x=\"174\" y=\"178\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-size=\"12\" fill=\"#56524a\">= an isolated VM\u003C/text>\n \u003C/g>\n\u003C/svg>\n\nThe client speaks to the agentOS server over the wire. The server runs the **sidecar**, the trusted core that hosts every VM: it owns each VM's kernel and brokers every guest syscall the agent makes (filesystem, processes, network, permissions) before carrying it out. Each VM is a fully isolated world, so agents are isolated from one another and from your host.\n\n### Your app (the client)\n\n- **Trusted caller.** Your app drives agentOS. It creates VMs, opens sessions, sends prompts, and reads results back.\n- **Never runs guest code.** The agent and any code it generates run in the VM, not in your app's process.\n- **Available everywhere.** There is a TypeScript client and a Rust client, and the same VM is reachable from a Node script, a browser/React app, or a separate backend.\n- **Owns the configuration.** Everything you send (VM setup, permission policy, resource limits, mounts) is trusted input. See the [Security Model](/docs/security-model) for why your configuration is not an attack surface.\n\n### Your server (the sidecar)\n\n- **The trusted core.** The sidecar is the part of the system that owns everything: the kernel, the virtual filesystem, the process and socket tables, pipes, PTYs, the permission policy, and DNS.\n- **The enforcement point.** Every request the VM makes is serviced here. The sidecar decides what is allowed before carrying it out.\n- **Hosts every VM.** A single sidecar manages many VMs side by side, each with its own kernel, filesystem, and process table, so every agent runs in its own isolated world. A crash or runaway in one VM never affects another.\n\n### The VM\n\n- **A fully virtualized Linux environment.** Each VM has its own filesystem, process table, and network policy. Two VMs share nothing.\n- **The unit of isolation.** Put one tenant or one task per VM to control the blast radius. A crash or runaway in one VM never affects another.\n- **Where guest code lives.** The agent, the shell, npm packages, and any generated code all run inside the VM, behind the kernel's boundary.\n\n## Anatomy of a Linux VM\n\nInside every VM there are two halves. The **kernel** is the trusted core that owns all the resources and rules. The **executor** is where untrusted guest code actually runs. Guest code can only *ask* the kernel for things, it never holds a real capability of its own.\n\n\u003Csvg viewBox=\"0 0 700 360\" role=\"img\" aria-label=\"A VM split into a kernel and an executor. The kernel owns the virtual filesystem, process table, socket table, pipes, PTYs, DNS, and permission policy. The executor runs guest JavaScript, WASM, and native binaries, and reaches the kernel through syscalls.\" style=\"width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"vm-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"12\" width=\"676\" height=\"336\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"32\" y=\"40\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">The VM\u003C/text>\n\n \u003Crect x=\"32\" y=\"56\" width=\"636\" height=\"150\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"52\" y=\"82\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Kernel\u003C/text>\n \u003Ctext x=\"52\" y=\"100\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">trusted core, every operation goes through here\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"52\" y=\"116\" width=\"118\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"111\" y=\"135\" text-anchor=\"middle\">virtual filesystem\u003C/text>\n \u003Crect x=\"182\" y=\"116\" width=\"118\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"241\" y=\"135\" text-anchor=\"middle\">process table\u003C/text>\n \u003Crect x=\"312\" y=\"116\" width=\"118\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"371\" y=\"135\" text-anchor=\"middle\">socket table\u003C/text>\n \u003Crect x=\"442\" y=\"116\" width=\"92\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"488\" y=\"135\" text-anchor=\"middle\">pipes / PTYs\u003C/text>\n \u003Crect x=\"546\" y=\"116\" width=\"100\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"596\" y=\"135\" text-anchor=\"middle\">DNS\u003C/text>\n \u003Crect x=\"52\" y=\"156\" width=\"594\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"349\" y=\"175\" text-anchor=\"middle\">permission policy · network allowlist · resource limits\u003C/text>\n \u003C/g>\n\n \u003Cline x1=\"349\" y1=\"206\" x2=\"349\" y2=\"244\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#vm-arrow)\" />\n \u003Cline x1=\"319\" y1=\"244\" x2=\"319\" y2=\"206\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#vm-arrow)\" />\n \u003Ctext x=\"430\" y=\"228\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">syscalls / replies\u003C/text>\n\n \u003Crect x=\"32\" y=\"248\" width=\"636\" height=\"84\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"52\" y=\"274\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Executor\u003C/text>\n \u003Ctext x=\"52\" y=\"292\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">untrusted, runs guest code, holds no capabilities\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"382\" y=\"262\" width=\"170\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"467\" y=\"281\" text-anchor=\"middle\">guest JavaScript (native V8)\u003C/text>\n \u003Crect x=\"562\" y=\"262\" width=\"84\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"604\" y=\"281\" text-anchor=\"middle\">WASM\u003C/text>\n \u003Crect x=\"382\" y=\"298\" width=\"264\" height=\"22\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"514\" y=\"313\" text-anchor=\"middle\" font-size=\"10.5\">shell · coreutils · npm packages · native binaries\u003C/text>\n \u003C/g>\n\u003C/svg>\n\n### Kernel: the trusted core\n\n\u003Csvg viewBox=\"0 0 480 150\" role=\"img\" aria-label=\"Guest requests funnel into a single kernel chokepoint, which fans out to its owned subsystems: filesystem, processes, network, and policy.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"kn-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"14\" y=\"58\" width=\"92\" height=\"34\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"60\" y=\"79\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#56524a\">guest request\u003C/text>\n \u003Cline x1=\"106\" y1=\"75\" x2=\"150\" y2=\"75\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#kn-arrow)\" />\n \u003Crect x=\"154\" y=\"50\" width=\"92\" height=\"50\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"200\" y=\"79\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Kernel\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">\n \u003Cline x1=\"246\" y1=\"60\" x2=\"286\" y2=\"22\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Cline x1=\"246\" y1=\"70\" x2=\"286\" y2=\"58\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Cline x1=\"246\" y1=\"80\" x2=\"286\" y2=\"92\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Cline x1=\"246\" y1=\"90\" x2=\"286\" y2=\"128\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Crect x=\"290\" y=\"8\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"25\" text-anchor=\"middle\">filesystem\u003C/text>\n \u003Crect x=\"290\" y=\"46\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"63\" text-anchor=\"middle\">processes\u003C/text>\n \u003Crect x=\"290\" y=\"80\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"97\" text-anchor=\"middle\">network & DNS\u003C/text>\n \u003Crect x=\"290\" y=\"116\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"133\" text-anchor=\"middle\">policy & limits\u003C/text>\n \u003C/g>\n\u003C/svg>\n\nThe kernel is the single chokepoint. Each kind of guest operation is serviced by a kernel-owned subsystem, never by a real host capability.\n\n- **Virtual filesystem.** A per-VM filesystem. Guest reads and writes hit the VFS, not your host disk.\n- **Process table.** A virtual process table. Child processes are kernel-managed and visible only inside their VM. No real host process is ever spawned for guest work.\n- **Socket table and DNS.** A virtual network stack. Outbound traffic is gated by the network allowlist.\n- **Pipes and PTYs.** Kernel-owned IPC and terminal devices, so shells and pipelines behave like real Linux.\n- **Policy and limits.** The kernel checks the applied permission policy, network allowlist, and resource limits on every request.\n\n### Executor: where guest code runs\n\n\u003Csvg viewBox=\"0 0 480 130\" role=\"img\" aria-label=\"The untrusted executor runs guest JavaScript, WASM, and native binaries. It holds no capabilities and reaches the kernel through syscalls.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"ex-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"14\" y=\"14\" width=\"300\" height=\"102\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"30\" y=\"36\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Executor\u003C/text>\n \u003Ctext x=\"30\" y=\"52\" font-family=\"var(--sl-font)\" font-size=\"9.5\" fill=\"#56524a\">untrusted · no capabilities\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">\n \u003Crect x=\"30\" y=\"62\" width=\"80\" height=\"44\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"70\" y=\"88\" text-anchor=\"middle\">JS (V8)\u003C/text>\n \u003Crect x=\"120\" y=\"62\" width=\"80\" height=\"44\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"160\" y=\"88\" text-anchor=\"middle\">WASM\u003C/text>\n \u003Crect x=\"210\" y=\"62\" width=\"92\" height=\"44\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"256\" y=\"82\" text-anchor=\"middle\">native\u003C/text>\u003Ctext x=\"256\" y=\"96\" text-anchor=\"middle\">binaries\u003C/text>\n \u003C/g>\n \u003Cline x1=\"314\" y1=\"55\" x2=\"358\" y2=\"55\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#ex-arrow)\" />\n \u003Ctext x=\"336\" y=\"48\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">syscall\u003C/text>\n \u003Cline x1=\"358\" y1=\"75\" x2=\"314\" y2=\"75\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#ex-arrow)\" />\n \u003Ctext x=\"336\" y=\"92\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">reply\u003C/text>\n \u003Crect x=\"362\" y=\"38\" width=\"104\" height=\"54\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"414\" y=\"70\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Kernel\u003C/text>\n\u003C/svg>\n\nThe executor is the untrusted half of the VM. It runs the guest code and reaches the kernel for everything else.\n\n- **JavaScript Acceleration.** Guest JavaScript runs on a native V8 runtime (the same engine in Chrome and Node.js, with the full JIT compiler) inside an isolate. This is what we call **JavaScript Acceleration**: the guest's JavaScript executes at native speed, not through an interpreter or a translation shim. It is genuinely fast, and it presents normal Node.js semantics. See [JavaScript Runtime](/docs/js-runtime).\n- **WASM alongside it.** The shell (`sh`) and the coreutils behind process execution ship as WebAssembly modules, and you can run your own WASM too. See [POSIX Syscalls](/docs/architecture/posix-syscalls) and the [Compiler Toolchain](/docs/architecture/compiler-toolchain).\n- **Native binaries.** Tools mounted into the VM run inside the same boundary as everything else.\n- **No host fallthrough.** The executor holds no capability of its own. For every file read, process spawn, or socket open, it issues a syscall and blocks for the kernel's reply.\n\n### Processes & shell\n\n\u003Csvg viewBox=\"0 0 480 120\" role=\"img\" aria-label=\"exec, run, and spawn create entries in the kernel-owned virtual process table, with stdio bridged through pipes and PTYs.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"pr-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"14\" y=\"14\" width=\"92\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"31\" text-anchor=\"middle\">exec() / run()\u003C/text>\n \u003Crect x=\"14\" y=\"78\" width=\"92\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"95\" text-anchor=\"middle\">spawn / shell\u003C/text>\n \u003C/g>\n \u003Cline x1=\"106\" y1=\"27\" x2=\"172\" y2=\"52\" stroke=\"#1b1916\" stroke-width=\"1.3\" marker-end=\"url(#pr-arrow)\" />\n \u003Cline x1=\"106\" y1=\"91\" x2=\"172\" y2=\"66\" stroke=\"#1b1916\" stroke-width=\"1.3\" marker-end=\"url(#pr-arrow)\" />\n \u003Crect x=\"176\" y=\"36\" width=\"138\" height=\"46\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"245\" y=\"56\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"11.5\" font-weight=\"600\" fill=\"#1b1916\">process table\u003C/text>\n \u003Ctext x=\"245\" y=\"71\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">virtual · per-VM\u003C/text>\n \u003Cline x1=\"314\" y1=\"59\" x2=\"360\" y2=\"59\" stroke=\"#1b1916\" stroke-width=\"1.3\" marker-end=\"url(#pr-arrow)\" />\n \u003Crect x=\"364\" y=\"40\" width=\"102\" height=\"40\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\n \u003Ctext x=\"415\" y=\"64\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#1b1916\">pipes & PTYs\u003C/text>\n\u003C/svg>\n\n- **A real process model.** `exec()` and `run()` start fresh guest processes; you can also `spawn` long-running ones and open interactive shells.\n- **Kernel-managed.** Every process lives in the virtual process table, with stdio bridged through kernel-owned pipes and PTYs.\n- **Fresh each run.** Each `exec()` / `run()` starts a brand new guest process, so in-memory state never leaks from one run into the next.\n- See [Processes](/docs/architecture/processes) for the internals.\n\n### Virtual filesystem\n\n\u003Csvg viewBox=\"0 0 480 150\" role=\"img\" aria-label=\"The virtual filesystem layers a writable overlay over a snapshot root, plus mount points that graft host directories, S3, or cloud stores onto guest paths.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"60\" y=\"14\" width=\"360\" height=\"30\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\u003Ctext x=\"240\" y=\"33\" text-anchor=\"middle\">overlay (guest writes)\u003C/text>\n \u003Crect x=\"60\" y=\"52\" width=\"360\" height=\"30\" rx=\"8\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\u003Ctext x=\"240\" y=\"71\" text-anchor=\"middle\">root layer (snapshot)\u003C/text>\n \u003C/g>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"9.5\" fill=\"#1b1916\">\n \u003Crect x=\"60\" y=\"104\" width=\"108\" height=\"32\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"114\" y=\"124\" text-anchor=\"middle\">host dir mount\u003C/text>\n \u003Crect x=\"186\" y=\"104\" width=\"108\" height=\"32\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"240\" y=\"124\" text-anchor=\"middle\">S3 mount\u003C/text>\n \u003Crect x=\"312\" y=\"104\" width=\"108\" height=\"32\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"366\" y=\"124\" text-anchor=\"middle\">cloud store\u003C/text>\n \u003C/g>\n \u003Ctext x=\"240\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">mount points grafted onto guest paths\u003C/text>\n\u003C/svg>\n\n- **Layered engines.** The VFS is a tree of engines: a root layer bootstrapped from a snapshot, an overlay for writes, and mount points that graft other backends onto guest paths.\n- **Host-backed mounts.** A guest path can be backed by a host directory, S3, or a cloud store. The kernel confines all guest I/O to the mount root, even against symlink and `..` tricks.\n- **Persisted.** The `/home/user` filesystem survives sleep/wake.\n- See [Filesystem](/docs/architecture/filesystem) for the internals.\n\n### Networking\n\n\u003Csvg viewBox=\"0 0 480 150\" role=\"img\" aria-label=\"Guest fetch, node:http, node:net, and WASM sockets all converge on one kernel socket table, which gates outbound traffic through the network allowlist.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"nw-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">\n \u003Crect x=\"14\" y=\"10\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"26\" text-anchor=\"middle\">fetch()\u003C/text>\n \u003Crect x=\"14\" y=\"42\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"58\" text-anchor=\"middle\">node:http\u003C/text>\n \u003Crect x=\"14\" y=\"74\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"90\" text-anchor=\"middle\">node:net\u003C/text>\n \u003Crect x=\"14\" y=\"106\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"122\" text-anchor=\"middle\">WASM sockets\u003C/text>\n \u003C/g>\n \u003Cline x1=\"106\" y1=\"22\" x2=\"186\" y2=\"62\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Cline x1=\"106\" y1=\"54\" x2=\"186\" y2=\"66\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Cline x1=\"106\" y1=\"86\" x2=\"186\" y2=\"74\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Cline x1=\"106\" y1=\"118\" x2=\"186\" y2=\"78\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Crect x=\"190\" y=\"48\" width=\"116\" height=\"44\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"248\" y=\"68\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"11\" font-weight=\"600\" fill=\"#1b1916\">socket table\u003C/text>\n \u003Ctext x=\"248\" y=\"83\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">kernel-owned\u003C/text>\n \u003Cline x1=\"306\" y1=\"70\" x2=\"350\" y2=\"70\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#nw-arrow)\" />\n \u003Crect x=\"354\" y=\"50\" width=\"112\" height=\"40\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\n \u003Ctext x=\"410\" y=\"74\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">egress allowlist\u003C/text>\n\u003C/svg>\n\n- **One authoritative transport.** Guest `fetch()`, `node:http`, `node:net`, and WASM sockets all target the same kernel socket table. No part of guest networking opens a real host socket on its own.\n- **Egress policy.** Outbound traffic is gated by the network allowlist; loopback traffic stays confined to the VM.\n- **Preview URLs.** Servers a guest starts can be exposed through signed preview URLs.\n- See [Networking](/docs/architecture/networking) for the internals.\n\n\u003CAside type=\"note\">The security boundary that matters is between the trusted sidecar and the untrusted executor. Everything the guest tries to do crosses into the kernel, where the policy is checked before the operation runs. See the [Security Model](/docs/security-model) for the full threat model.\u003C/Aside>\n\n## Agents & sessions\n\nAn agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) is just another guest process running inside a VM, behind the same boundary as any other code. A **session** keeps that agent alive across many prompts and streams its output back to your app as events.\n\n\u003Csvg viewBox=\"0 0 700 210\" role=\"img\" aria-label=\"A client sends a prompt to an agent running inside a VM. The agent streams events back to the client and persists a transcript.\" style=\"width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"se-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"66\" width=\"150\" height=\"78\" rx=\"12\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"87\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"14\" font-weight=\"600\" fill=\"#1b1916\">Client\u003C/text>\n \u003Ctext x=\"87\" y=\"118\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">your app\u003C/text>\n\n \u003Cline x1=\"162\" y1=\"92\" x2=\"266\" y2=\"92\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#se-arrow)\" />\n \u003Ctext x=\"214\" y=\"84\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">prompt\u003C/text>\n \u003Cline x1=\"266\" y1=\"120\" x2=\"162\" y2=\"120\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#se-arrow)\" />\n \u003Ctext x=\"214\" y=\"136\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">events\u003C/text>\n\n \u003Crect x=\"270\" y=\"30\" width=\"280\" height=\"150\" rx=\"12\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"290\" y=\"56\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">The VM\u003C/text>\n \u003Crect x=\"300\" y=\"72\" width=\"220\" height=\"56\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\n \u003Ctext x=\"410\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Agent session\u003C/text>\n \u003Ctext x=\"410\" y=\"116\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">long-lived agent process\u003C/text>\n\n \u003Cline x1=\"550\" y1=\"105\" x2=\"630\" y2=\"105\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#se-arrow)\" />\n \u003Crect x=\"560\" y=\"72\" width=\"118\" height=\"66\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"619\" y=\"100\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Transcript\u003C/text>\n \u003Ctext x=\"619\" y=\"118\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">persisted, replayable\u003C/text>\n\u003C/svg>\n\n### Sessions & transcripts\n\n- **Long-lived.** Where a bare `exec()` runs once and exits, a session keeps an agent alive across many prompts.\n- **Streamed.** The agent's output flows back to your app in real time as `sessionEvent`s.\n- **Replayable.** Each session persists a transcript (with sequence numbers) that survives sleep/wake, so you can replay the conversation later.\n- **Context injected.** agentOS adds a system prompt describing the VM environment and available tools, layered on top of the agent's own instructions. See [System Prompt](/docs/system-prompt).\n- See [Agent Sessions](/docs/architecture/agent-sessions) for the internals.\n\n### Permissions & approvals\n\n- **Two layers, different jobs.** The lower-level [permission policy](/docs/permissions) is enforced by the kernel on every guest syscall (nothing is allowed until you opt in). On top of that, [approvals](/docs/approvals) are about an agent asking before it uses a tool.\n- **Human-in-the-loop or automatic.** Subscribe to `permissionRequest` and respond per request, or use a server-side hook to decide without a client round-trip.\n- **Blocks until answered.** If neither your hook nor your client responds, the agent waits rather than proceeding.\n\n## Orchestration (Rivet Actors)\n\nThe `agentOS()` actor (from `@rivet-dev/agentos`) wraps the raw VM in a [Rivet Actor](/docs/core), which adds durable state, scheduling, and orchestration. This is what gives you persistence, cron, and workflows out of the box.\n\n\u003Csvg viewBox=\"0 0 700 200\" role=\"img\" aria-label=\"A Rivet Actor wraps an agentOS VM and adds durable state, cron scheduling, workflows, and sleep/wake persistence.\" style=\"width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;\">\n \u003Crect x=\"40\" y=\"20\" width=\"620\" height=\"160\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"64\" y=\"46\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Rivet Actor\u003C/text>\n \u003Ctext x=\"64\" y=\"64\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">durable, addressable server object\u003C/text>\n\n \u003Crect x=\"64\" y=\"80\" width=\"180\" height=\"80\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"154\" y=\"116\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">agentOS VM\u003C/text>\n \u003Ctext x=\"154\" y=\"136\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">the virtual Linux VM\u003C/text>\n\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11.5\" fill=\"#1b1916\">\n \u003Crect x=\"272\" y=\"80\" width=\"120\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"332\" y=\"102\" text-anchor=\"middle\">Cron\u003C/text>\n \u003Crect x=\"412\" y=\"80\" width=\"120\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"472\" y=\"102\" text-anchor=\"middle\">Workflows\u003C/text>\n \u003Crect x=\"272\" y=\"126\" width=\"260\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"402\" y=\"148\" text-anchor=\"middle\">Persistence · sleep / wake\u003C/text>\n \u003Crect x=\"552\" y=\"80\" width=\"92\" height=\"80\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"598\" y=\"116\" text-anchor=\"middle\">Durable\u003C/text>\u003Ctext x=\"598\" y=\"132\" text-anchor=\"middle\">state\u003C/text>\n \u003C/g>\n\u003C/svg>\n\n### What are actors?\n\n- **Durable server objects.** A Rivet Actor is a long-lived, addressable object with its own state. You reach a specific VM by name (`vm.getOrCreate(\"my-agent\")`).\n- **Stateful by default.** Unlike the bare core package, the actor keeps its filesystem and sessions persistent and handles distributed state for you.\n- **The portable runtime.** Actors give you a consistent way to run `agentOS()` on any infrastructure, with persistence, networking, and orchestration built in.\n\n### Cron\n\n- **Recurring work.** Schedule a shell command or an agent session on a cron expression.\n- **Overlap control.** Choose what happens when a run is still going when the next is due (`allow`, `skip`, or `queue`).\n- **Observable.** Stream `cronEvent`s to watch executions. See [Cron Jobs](/docs/cron).\n\n### Workflows\n\n- **Durable multi-step tasks.** A workflow is the actor's `run` handler wrapped in `workflow()`, where each `ctx.step()` is recorded, retried, and resumed independently.\n- **Crash-proof.** If the process dies mid-run, replay skips completed steps and continues where it left off.\n- **Composable.** The output of one step feeds the next: clone a repo, let an agent fix a bug, run the tests. See [Workflow Automation](/docs/workflows).\n\n### Persistence & sleep/wake\n\n- **Sleeps when idle.** After a grace period (15 minutes by default) with no activity, the VM sleeps to free resources.\n- **Wakes on demand.** It wakes automatically when a client connects or a cron job fires.\n- **What survives.** The `/home/user` filesystem, session records, transcripts, preview tokens, and cron definitions all persist. In-memory kernel state (running processes, open shells) does not. See [Persistence & Sleep](/docs/persistence).\n\n## Going deeper\n\nThis page is the map. Each subsystem has its own detailed page in the Advanced architecture section:\n\n- **[Agent Sessions](/docs/architecture/agent-sessions)**: how a session is bound to a VM, and how prompts and events flow end to end.\n- **[Processes](/docs/architecture/processes)**: the virtual process table, `exec()` / `run()`, child processes, and PTYs.\n- **[Filesystem](/docs/architecture/filesystem)**: the per-VM virtual filesystem, overlays, and host-backed mounts.\n- **[Networking](/docs/architecture/networking)**: the virtual socket table, DNS, the allowlist, and guest `fetch()`.\n- **[POSIX Syscalls](/docs/architecture/posix-syscalls)**: how WebAssembly guests behave like normal POSIX programs on top of the kernel.\n- **[Compiler Toolchain](/docs/architecture/compiler-toolchain)**: how the shell and coreutils are compiled to WebAssembly and mounted into the VM.\n- **[System Prompt](/docs/system-prompt)**: the context agentOS injects into every agent session.\n- **[Persistence & Sleep](/docs/persistence)**: what survives sleep/wake, and how VMs sleep and wake.\n\nFor the trust model and what counts as a sandbox escape, see the [Security Model](/docs/security-model).","src/content/docs/docs/architecture.mdx","c46746a24d7972c4","docs/authentication",{"id":47,"data":49,"body":55,"filePath":56,"digest":57,"deferredRender":16},{"title":50,"description":51,"editUrl":16,"head":52,"template":18,"sidebar":53,"pagefind":16,"draft":20},"Authentication","Authenticate connections to agentOS actors using Rivet Actor connection params and hooks.",[],{"hidden":20,"attrs":54},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nagentOS uses the same authentication system as [Rivet Actors](/docs/actors/authentication): clients send credentials as connection params, and you validate them server-side.\n\n- Clients pass credentials in `params` when they connect.\n- Validate them on the server in `onBeforeConnect` (throw to reject the connection), or extract user data into connection state with `createConnState` (read it in actions via `c.conn.state`).\n- You can declare the credential shape with `agentOS\u003CConnParams>(...)` to document what you accept, but the client's `params` is `unknown` and is not checked against it. The real check is your hook, not the types.\n- The current `@rivet-dev/agentos` runtime is an interim stub, so wiring these hooks end to end depends on the native runtime landing.\n\n## Example\n\nThe server declares the credential shape and validates it in `onBeforeConnect` (throw to reject); the client passes credentials as `params`.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\ninterface ConnParams {\n authToken: string;\n}\n\n// Validate credentials server-side. onBeforeConnect receives the connection\n// params and rejects the connection by throwing. Wired via the underlying Rivet\n// Actor; see Actor Authentication for the full hook signatures.\nexport function onBeforeConnect(_c: unknown, params: ConnParams): void {\n if (typeof params?.authToken !== \"string\" || params.authToken.length === 0) {\n throw new Error(\"missing or invalid authToken\");\n }\n // verify the token (JWT signature, lookup, ...) here\n}\n\nconst vm = agentOS\u003CConnParams>({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Pass credentials when connecting; the server hook validates them.\nconst agent = client.vm.getOrCreate(\"my-agent\", {\n params: { authToken: \"my-jwt-token\" },\n});\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"List the files in the working directory.\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/authentication)*\n\nSee [Actor Authentication](/docs/actors/authentication) for JWT validation, role-based access control, external auth providers, and token caching.","src/content/docs/docs/authentication.mdx","5839914d3efbeeb9","docs/benchmarks",{"id":58,"data":60,"body":66,"filePath":67,"digest":68,"deferredRender":16},{"title":61,"description":62,"editUrl":16,"head":63,"template":18,"sidebar":64,"pagefind":16,"draft":20},"Benchmarks","Performance benchmarks comparing agentOS to traditional sandbox providers.",[],{"hidden":20,"attrs":65},{},"These are the benchmark figures shown on the agentOS marketing page. All numbers are computed from the same data source used by the marketing page. For independent sandbox comparison data, see the [ComputeSDK benchmarks](https://www.computesdk.com/benchmarks/).\n\n## Cold start\n\nTime from requesting an execution to first code running. Measured using the sleep workload (a minimal VM running an idle Node.js process). Sandbox baseline: **E2B**, the fastest mainstream sandbox provider as of March 30, 2026. See [ComputeSDK benchmarks](https://www.computesdk.com/benchmarks/) for independent sandbox comparison data.\n\n| Metric | agentOS | Fastest sandbox (E2B) |\n|---|--:|--:|\n| Cold start p50 | 4.8 ms | 440 ms |\n| Cold start p95 | 5.6 ms | 950 ms |\n| Cold start p99 | 6.1 ms | 3,150 ms |\n\n## Memory per instance\n\nMeasured via staircase benchmarking:\n\n1. **Warmup.** A throwaway VM is created, started, and destroyed before measurement begins. This pays one-time costs (module cache, JIT compilation) that are amortized away in any real deployment where the host process is long-lived.\n2. **Baseline.** GC is forced twice (`--expose-gc`), then RSS is sampled across the entire process tree by reading `/proc/[pid]/statm` for the host process and all descendants. This captures child processes (e.g. V8 isolates running as separate processes) that `process.memoryUsage().rss` would miss.\n3. **Staircase.** VMs are added one at a time. After each VM starts and settles, GC is forced and RSS is sampled again. The delta from the previous sample is the incremental cost of that VM.\n4. **Average.** The per-VM cost is the mean of all step deltas.\n5. **Teardown.** All VMs are disposed and the reclaimed RSS is recorded.\n\nRSS is a process-wide metric that includes thread stacks and OS-mapped pages beyond the VM itself, so the reported figure is an upper bound on the true per-VM cost.\n\nSandbox baseline: **Daytona**, the cheapest mainstream sandbox provider as of March 30, 2026. Default sandbox: 1 vCPU + 1 GiB RAM.\n\n### Full coding agent\n\nPi coding agent session with MCP servers and mounted file systems.\n\n| Metric | agentOS | Cheapest sandbox (Daytona) |\n|---|--:|--:|\n| Memory per instance | ~131 MB | ~1024 MB |\n\n### Simple shell command\n\nMinimal shell workload running simple commands.\n\n| Metric | agentOS | Cheapest sandbox (Daytona) |\n|---|--:|--:|\n| Memory per instance | ~22 MB | ~1024 MB |\n\n## Cost per execution-second\n\nAssumes one agent per sandbox (needed for isolation) and 70% host utilization for self-hosted hardware (the industry-standard HPA scaling threshold). Cost formula: `server cost per second / concurrent executions per server`, where concurrent executions = `floor(server RAM / agent memory) × 0.7`.\n\nSandbox baseline: **Daytona** at $0.0504/vCPU-h + $0.0162/GiB-h with a 1 vCPU + 1 GiB minimum. Source: [daytona.io/pricing](https://www.daytona.io/pricing).\n\n### Full coding agent\n\n| Host tier | agentOS | Cheapest sandbox | Difference |\n|---|--:|--:|--:|\n| AWS ARM | $0.00000058/s | $0.000018/s | 32x cheaper |\n| AWS x86 | $0.00000072/s | $0.000018/s | 26x cheaper |\n| Hetzner ARM | $0.000000066/s | $0.000018/s | 281x cheaper |\n| Hetzner x86 | $0.00000011/s | $0.000018/s | 171x cheaper |\n\n### Simple shell command\n\n| Host tier | agentOS | Cheapest sandbox | Difference |\n|---|--:|--:|--:|\n| AWS ARM | $0.000000073/s | $0.000018/s | 254x cheaper |\n| AWS x86 | $0.000000090/s | $0.000018/s | 205x cheaper |\n| Hetzner ARM | $0.000000011/s | $0.000018/s | 1738x cheaper |\n| Hetzner x86 | $0.000000017/s | $0.000018/s | 1061x cheaper |\n\n## Test environment\n\n| Component | Details |\n|---|---|\n| CPU | 12th Gen Intel i7-12700KF, 12 cores / 20 threads @ 3.7 GHz, 25 MB cache |\n| RAM | 2× 32 GB DDR4 @ 2400 MT/s |\n| Node.js | v24.13.0 |\n| OS | Linux 6.1.0 (Debian), x86_64 |\n\n## Sandbox baselines\n\n| Comparison | Provider | Why this provider |\n|---|---|---|\n| Cold start | E2B | Fastest mainstream sandbox provider on [ComputeSDK](https://www.computesdk.com/benchmarks/) as of March 30, 2026 |\n| Memory and cost | Daytona | Cheapest mainstream sandbox provider as of March 30, 2026 ($0.0504/vCPU-h + $0.0162/GiB-h) |\n\nSelf-hosted hardware tiers: AWS t4g.micro (ARM, $0.0084/h, 1 GiB), AWS t3.micro (x86, $0.0104/h, 1 GiB), Hetzner CAX11 (ARM, €3.29/mo, 4 GiB), Hetzner CX22 (x86, €5.39/mo, 4 GiB). All on-demand pricing.\n\n## Reproducing\n\nagentOS benchmarks live in the [agent-os repository](https://github.com/rivet-dev/agent-os) under `scripts/benchmarks/`.","src/content/docs/docs/benchmarks.mdx","b6a087cd9516c3ec","docs/bindings",{"id":69,"data":71,"body":77,"filePath":78,"digest":79,"deferredRender":16},{"title":72,"description":73,"editUrl":16,"head":74,"template":18,"sidebar":75,"pagefind":16,"draft":20},"Bindings","Expose custom host functions to agents as CLI commands inside the VM.",[],{"hidden":20,"attrs":76},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nExpose your host JavaScript functions (defined with Zod input schemas) to agents as auto-generated CLI commands installed at `/usr/local/bin/agentos-{name}` inside the VM, injected into the agent's [system prompt](/docs/system-prompt) and callable inside scripts for code-mode token savings.\n\n## Getting started\n\nDefine a bindings group with Zod input schemas and pass it to `agentOS()`. Each binding becomes a CLI command inside the VM.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { z } from \"zod\";\n\n// Define a group of host functions. Each binding has a Zod input schema and an\n// `execute` handler that runs on the host. Bindings are exposed to the agent as\n// CLI commands at /usr/local/bin/agentos-{name} inside the VM.\nconst weatherBindings = {\n name: \"weather\",\n description: \"Weather data bindings\",\n bindings: {\n forecast: {\n description: \"Get the weather forecast for a city\",\n inputSchema: z.object({\n city: z.string().describe(\"City name\"),\n days: z.number().optional().describe(\"Number of days\"),\n }),\n execute: async (input: { city: string; days?: number }) => {\n const res = await fetch(\n `https://api.weather.example/forecast?city=${input.city}&days=${input.days ?? 3}`,\n );\n return res.json();\n },\n examples: [\n { description: \"3-day forecast for Paris\", input: { city: \"Paris\", days: 3 } },\n ],\n },\n },\n};\n\nconst vm = agentOS({\n bindings: [weatherBindings],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"What's the weather in Paris?\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/bindings)*\n\nEach binding can set an explicit `timeout` (in milliseconds) for long-running work. Bindings run without a timeout unless one is set.\n\n### Zod to CLI mapping\n\nZod schema fields are converted to CLI flags automatically. Field names are converted from camelCase to kebab-case.\n\n| Zod type | CLI syntax | Example |\n|---|---|---|\n| `z.string()` | `--name value` | `--path /tmp/out.png` |\n| `z.number()` | `--name 42` | `--limit 5` |\n| `z.boolean()` | `--flag` / `--no-flag` | `--full-page` |\n| `z.enum([\"a\",\"b\"])` | `--name a` | `--format json` |\n| `z.array(z.string())` | `--name a --name b` | `--tags foo --tags bar` |\n\nOptional fields (via `.optional()`) become optional flags. Required fields are enforced at validation time. Use `.describe()` on Zod fields to generate useful `--help` output.\n\n### What the agent sees\n\nWhen bindings are registered, CLI shims are installed at `/usr/local/bin/agentos-{name}` inside the VM and the binding list is injected into the agent's [system prompt](/docs/system-prompt), so keep binding descriptions concise to save tokens.\n\nThe agent interacts with bindings as shell commands:\n\n```bash\n# List all available bindings groups\nagentos list-tools\n\n# List bindings in a specific group\nagentos list-tools weather\n\n# Get help for a binding\nagentos-weather forecast --help\n\n# Call a binding with flags\nagentos-weather forecast --city Paris --days 3\n\n# Call a binding with inline JSON\nagentos-weather forecast --json '{\"city\":\"Paris\",\"days\":3}'\n\n# Call a binding with JSON from a file\nagentos-weather forecast --json-file /tmp/input.json\n```\n\nOn success, the binding exits `0` and writes a JSON envelope to stdout:\n\n```json\n{\"ok\":true,\"result\":{\"temperature\":22,\"condition\":\"sunny\"}}\n```\n\nOn failure (validation or execution error), the binding exits non-zero and writes the error message to stderr:\n\n```text\nMissing required flag: --city\n```\n\n## Bindings vs MCP servers\n\nagentOS supports two ways to give agents access to external functionality: **bindings** and **MCP servers**. Both work, but they have different tradeoffs.\n\n| | Bindings | MCP Servers |\n|---|---|---|\n| **How it works** | Call JavaScript functions on the host directly | Connect to a standard MCP server |\n| **Authentication** | None required. Direct binding to the agent's OS. | Requires custom auth configuration per server |\n| **Code mode** | Built-in. Bindings are exposed as CLI commands, so agents can call them inside scripts for up to 80% token reduction. | Requires extra work to make code mode work out of the box |\n| **Latency** | Near-zero. Bound directly to the host process. | Extra network hop to reach the MCP server |\n| **Setup** | Define bindings in your actor code with Zod schemas | Configure any standard MCP server |\n\nUse bindings when you want to expose your own JavaScript functions to agents. Use MCP servers when you want to connect to existing third-party services. See [Sessions](/docs/sessions#mcpservers) for MCP server configuration.\n\n## Security\n\nBinding calls from the agent securely invoke your `execute()` functions on the host. Your functions run with full access to the host environment, so you can call databases, APIs, and services directly without proxying credentials into the VM. The agent never sees the credentials, it only sees the binding's input/output contract.\n\nBindings run on the host with full access to the host environment, so do not expose bindings that could compromise the host without appropriate safeguards.","src/content/docs/docs/bindings.mdx","0a104f8f8406948a","docs/core",{"id":80,"data":82,"body":88,"filePath":89,"digest":90,"deferredRender":16},{"title":83,"description":84,"editUrl":16,"head":85,"template":18,"sidebar":86,"pagefind":16,"draft":20},"Core Package","Use @rivet-dev/agentos-core standalone for direct VM control without the Rivet Actor runtime.",[],{"hidden":20,"attrs":87},{},"## agentOS vs agentOS Core\n\nThe `agentOS()` actor (from `@rivet-dev/agentos`) wraps the core package and adds:\n\n| | Core (`@rivet-dev/agentos-core`) | Actor (`@rivet-dev/agentos`) |\n|-|---|---|\n| Persistence | In-memory by default (pluggable via [mounts](#mounts)) | Persistent filesystem and sessions |\n| Distributed state | Manage yourself | Built-in distributed statefulness |\n| Stateful VMs | Complex to run yourself | Built into Rivet |\n| Sleep/wake | Manual `dispose()` / `create()` | Automatic |\n| Events | Direct callbacks | Broadcasted to all connected clients |\n| Preview URLs | None | Built-in signed URL server |\n| Multiplayer | N/A | Multiple clients on same actor |\n| Orchestration | N/A | Workflows, queues, cron |\n| Agent-to-agent communication | Custom | Built into [Rivet Actors](/docs/agent-to-agent) |\n| Authentication | Set up yourself | [Documentation](/docs/authentication) |\n\nWe recommend using [Rivet Actors](/docs/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.\n\n## Install\n\n```bash\nnpm install @rivet-dev/agentos-core\n```\n\n## Boot a VM\n\nDefine the actor on the server:\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\nThen drive it from a typed client:\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.vm.getOrCreate(\"my-agent\");\n\nconst result = await handle.exec(\"echo hello\");\nconsole.log(result.stdout); // \"hello\\n\"\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Filesystem\n\n```ts title=\"client.ts\"\nawait handle.writeFile(\"/home/user/hello.txt\", \"Hello, world!\");\nconst content = await handle.readFile(\"/home/user/hello.txt\");\nconsole.log(new TextDecoder().decode(content));\n\nawait handle.mkdir(\"/home/user/src\");\nawait handle.writeFiles([\n { path: \"/home/user/src/index.ts\", content: \"console.log('hi');\" },\n { path: \"/home/user/src/utils.ts\", content: \"export const add = (a: number, b: number) => a + b;\" },\n]);\n\nconst entries = await handle.readdirRecursive(\"/home/user\");\nfor (const entry of entries) {\n console.log(entry.type, entry.path);\n}\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Processes\n\nLong-running process output is delivered over the live `processOutput` / `processExit` events on a connection rather than per-pid callbacks:\n\n```ts title=\"client.ts\"\n// One-shot execution\nconst result = await handle.exec(\"ls -la /home/user\");\nconsole.log(result.stdout);\n\n// Long-running process with streaming output\nawait handle.writeFile(\n \"/tmp/server.mjs\",\n 'import http from \"http\"; http.createServer((req, res) => res.end(\"ok\")).listen(3000); console.log(\"listening\");',\n);\nconst { pid } = await handle.spawn(\"node\", [\"/tmp/server.mjs\"]);\n\nconst conn = handle.connect();\nconn.on(\"processOutput\", (data) => {\n if (data.pid === pid && data.stream === \"stdout\") {\n console.log(\"stdout:\", new TextDecoder().decode(data.data));\n }\n});\nconn.on(\"processExit\", (data) => {\n if (data.pid === pid) console.log(\"exited:\", data.exitCode);\n});\n\n// Write to stdin\nawait handle.writeProcessStdin(pid, \"some input\\n\");\n\n// Stop or kill\nawait handle.stopProcess(pid);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Agent sessions\n\n`createSession` returns a session record. All session operations take its `sessionId`. Session events and permission requests are delivered over the live connection (`sessionEvent` / `permissionRequest`):\n\n```ts title=\"client.ts\"\nconst conn = handle.connect();\n\n// Stream events (each event is a JSON-RPC notification)\nconn.on(\"sessionEvent\", (data) => {\n console.log(data.event.method, data.event.params);\n});\n\n// Handle permissions\nconn.on(\"permissionRequest\", (data) => {\n console.log(\"Permission:\", data.request.description);\n // Reply with \"once\", \"always\", or \"reject\"\n void handle.respondPermission(data.sessionId, data.request.permissionId, \"once\");\n});\n\nconst session = await handle.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Send a prompt. sendPrompt() resolves to { response, text }, where `text` is\n// the accumulated agent message text and `response` is the raw JSON-RPC response.\nconst { text } = await handle.sendPrompt(session.sessionId, \"Write a hello world script\");\nconsole.log(text);\n\n// Configure the session\nawait handle.setModel(session.sessionId, \"claude-sonnet-4-6\");\nawait handle.setMode(session.sessionId, \"plan\");\n\nawait handle.closeSession(session.sessionId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\nSubscribe to `sessionEvent` before sending a prompt so you do not miss the live stream. Persisted history can be read back later with `getSessionEvents()`.\n\n## Interactive shell\n\n```ts title=\"client.ts\"\nconst { shellId } = await handle.openShell();\n\nconst conn = handle.connect();\nconn.on(\"shellData\", (data) => {\n if (data.shellId === shellId) {\n process.stdout.write(new TextDecoder().decode(data.data));\n }\n});\n\nawait handle.writeShell(shellId, \"echo hello from shell\\n\");\n\n// Resize terminal\nawait handle.resizeShell(shellId, 120, 40);\n\nawait handle.closeShell(shellId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Networking\n\n```ts title=\"client.ts\"\n// Start a server inside the VM\nawait handle.writeFile(\n \"/tmp/app.mjs\",\n 'import http from \"http\"; http.createServer((req, res) => res.end(\"hello\")).listen(3000);',\n);\nawait handle.spawn(\"node\", [\"/tmp/app.mjs\"]);\n\n// Fetch from it\nconst response = await handle.vmFetch(3000, \"/\");\nconsole.log(new TextDecoder().decode(response.body));\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Cron jobs\n\nCron jobs run an `\"exec\"` command or a `\"session\"` prompt on a schedule. Fired jobs are surfaced over the live `cronEvent` connection:\n\n```ts title=\"client.ts\"\nconst { id } = await handle.scheduleCron({\n id: \"cleanup\",\n schedule: \"0 * * * *\",\n action: { type: \"exec\", command: \"rm\", args: [\"-rf\", \"/tmp/cache\"] },\n});\nconsole.log(\"Scheduled:\", id);\n\n// Run an agent session on a schedule\nawait handle.scheduleCron({\n schedule: \"0 9 * * *\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Review the logs and summarize any errors\",\n options: { cwd: \"/home/user\" },\n },\n});\n\nconst conn = handle.connect();\nconn.on(\"cronEvent\", (data) => {\n console.log(\"Cron event:\", data.event.id, data.event.schedule);\n});\n\nconsole.log(await handle.listCronJobs());\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Mounts\n\nConfigure filesystem backends at boot time.\n\nNative mount plugins (host directories, S3, etc.) are passed via `plugin`, each\nidentified by an `id` and a `config` object.\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n mounts: [\n // Host directory (read-only)\n {\n path: \"/mnt/code\",\n plugin: { id: \"host_dir\", config: { hostPath: \"/path/to/repo\" } },\n readOnly: true,\n },\n // S3 bucket\n {\n path: \"/mnt/data\",\n plugin: { id: \"s3\", config: { bucket: \"my-bucket\", prefix: \"agent/\" } },\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## `agentOS()` configuration reference\n\nWhen you use the [`agentOS()` actor](/docs/quickstart), all VM configuration is passed to the factory as a single flat object. This is the consolidated config block to copy and adapt:\n\n```ts title=\"server.ts\"\nimport { agentOS, nodeModulesMount, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n // Filesystems to mount at boot. Use nodeModulesMount() to expose a host\n // node_modules tree at /root/node_modules.\n mounts: [nodeModulesMount(\"/path/to/project/node_modules\")],\n // Software packages to install in the VM (see /docs/software)\n software: [pi],\n // Ports exempt from SSRF checks\n loopbackExemptPorts: [3000],\n // Extra instructions appended to agent system prompts\n additionalInstructions: \"Always write tests first.\",\n\n // Preview URL token lifetimes\n preview: {\n defaultExpiresInSeconds: 3600, // 1 hour (default)\n maxExpiresInSeconds: 86400, // 24 hours (default)\n },\n\n // Lifecycle hooks (see below)\n onSessionEvent: async (sessionId, event) => {\n console.log(\"Session event:\", sessionId, event.method);\n },\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"Permission request:\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\nThe top-level fields are documented inline above. See [Mounts](#mounts), [Software](/docs/software), and (for the hooks) [Approvals](/docs/approvals).\n\n### Lifecycle hooks\n\n`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.\n\n```ts title=\"server.ts\"\nimport { agentOS } from \"@rivet-dev/agentos\";\n\nexport const vm = agentOS({\n // Runs once per session event, server-side, for every session.\n onSessionEvent: async (sessionId, event) => {\n console.log(\"Session event:\", sessionId, event.method);\n },\n});\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n### Timeouts\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| Action timeout | 15 minutes | Maximum time for any single action |\n| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |\n\nThese are set internally by the `agentOS()` factory and cannot be overridden per-call. See [Persistence & Sleep](/docs/persistence) for details on the sleep lifecycle.","src/content/docs/docs/core.mdx","ee76c22189eea2e7","docs/cost-evaluation",{"id":91,"data":93,"body":99,"filePath":100,"digest":101,"deferredRender":16},{"title":94,"description":95,"editUrl":16,"head":96,"template":18,"sidebar":97,"pagefind":16,"draft":20},"Cost Evaluation","How agentOS compares on cost to per-second sandbox providers when you run coding-agent VMs on your own hardware.",[],{"hidden":20,"attrs":98},{},"import { Aside } from '@astrojs/starlight/components';\n\nagentOS is a library you run on hardware you already control, not a metered service. That changes the cost model for running coding-agent VMs from \"pay a provider per sandbox-second\" to \"pay for the compute you provision, then pack as much work onto it as it can hold.\" This page explains where the savings come from and how to reason about them honestly. It does not publish a single magic multiplier, because the real number depends on your workload, your hardware, and how you share VMs.\n\n\u003CAside type=\"note\">For measured latency (cold start, warm execution, and reuse fast paths), see [Benchmarks](/docs/benchmarks). This page is about cost structure, not raw performance.\u003C/Aside>\n\n## Where the savings come from\n\nTwo structural differences drive the cost gap versus per-second sandbox providers:\n\n- **You run on your own hardware**: you choose the cloud, instance type, architecture, and region. A small commodity instance (for example an ARM VM from a budget host) costs a flat hourly or monthly rate that is typically far below what per-sandbox-second billing adds up to once you have steady agent traffic. You also avoid egress fees and vendor lock-in.\n- **You decide the isolation granularity**: sandbox providers bill a full container or microVM per execution, usually with a minimum memory reservation that you pay for even when your code uses a fraction of it. With agentOS you own the VM lifecycle: you can dedicate a VM per tenant or per task for maximum isolation, or amortize setup by reusing one VM across many runs.\n\n## The isolation model matters for cost\n\nEach `AgentOs.create()` boots a fully virtualized VM, and each `exec()` / `run()` inside it is a fresh guest process. That gives you a dial between isolation and density:\n\n- **One VM per task or tenant (strongest isolation)**: create a VM, run the work, and dispose it, or give each tenant its own VM. Each VM is its own crash and resource domain, with the highest per-VM overhead. Best when load is untrusted or bursty.\n- **A shared VM for trusted work**: reuse one VM across many runs to amortize the VM boot cost. Each `exec()` / `run()` still executes in a fresh guest process, so in-memory state does not leak between runs, but the VM and filesystem are shared. Good for trusted, sequential work.\n\nThe denser you can safely pack agent work onto an instance, the lower your effective cost per execution. See [Resource Limits](/docs/resource-limits) for the per-VM caps that govern how densely you can pack, and [Processes & Shell](/docs/processes) for how guest processes run inside a VM.\n\n## How to estimate your own cost\n\nBecause agentOS runs on hardware you provision, the honest way to compare is to plug in your own numbers:\n\n1. **Pick your hardware and its rate**: take the hourly or monthly price of the instance you would run on, and divide down to a per-second instance cost.\n2. **Estimate how many concurrent VMs fit**: measure per-VM memory overhead on your target hardware under your isolation strategy, then divide your usable RAM by that figure. Leave headroom (the measurement and any orchestration layer will not bin-pack perfectly).\n3. **Divide instance cost by concurrent VMs**: that gives a cost-per-VM-second you can compare against a provider's per-sandbox-second rate.\n\n\u003CAside type=\"tip\">Measure on the hardware and isolation strategy you will actually deploy. Per-VM overhead depends on whether you create a fresh VM per task or reuse one across runs, and on the work the agent does, so a number measured on one machine will not transfer cleanly to another.\u003C/Aside>\n\n## Comparing against sandbox providers\n\nWhen you do compare against a per-second sandbox provider, hold the methodology honest:\n\n- **Sandbox cost** is the provider's minimum allocatable memory times their per-GiB-second rate (plus any egress and platform fees). The minimum reservation is the floor you pay even for tiny workloads.\n- **agentOS cost** is your instance cost per second divided by the number of VMs you can keep live on it, with realistic headroom for bin-packing inefficiency.\n\nThe advantage is largest for **many small, short executions**, where a per-sandbox minimum reservation dominates and your own hardware lets you pack densely. It narrows for **heavyweight, long-lived workloads** (for example dev servers that need hundreds of megabytes regardless), where the win shifts from density to hardware choice: you still avoid per-second metering, egress fees, and lock-in, but the raw memory-density advantage is smaller.\n\n| Workload | Primary cost advantage |\n| --- | --- |\n| Many small, short executions | Density: pack many VMs per instance, no per-sandbox minimum |\n| Heavyweight, long-lived workloads | Hardware choice, flat instance pricing, no egress or lock-in |\n| High concurrency | Reuse a VM across runs to amortize VM boot cost |\n\n\u003CAside type=\"caution\">Be careful generalizing cost ratios from a single benchmark. Provider pricing, instance pricing, and exchange rates change over time, and per-VM overhead varies by workload and isolation strategy. Re-measure on your own hardware before quoting a number.\u003C/Aside>\n\nWhen you do need a full Linux sandbox for heavier agent workloads, see [agentOS vs Sandbox](/docs/versus-sandbox) for how the two models combine.","src/content/docs/docs/cost-evaluation.mdx","896bc95a6238b39c","docs/crash-course",{"id":102,"data":104,"body":110,"filePath":111,"digest":112,"deferredRender":16},{"title":105,"description":106,"editUrl":16,"head":107,"template":18,"sidebar":108,"pagefind":16,"draft":20},"Crash Course","Run coding agents inside isolated VMs with full filesystem, process, and network control.",[],{"hidden":20,"attrs":109},{},"import { Aside } from '@astrojs/starlight/components';\nimport CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\n\u003CAside type=\"note\">\nagentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).\n\u003C/Aside>\n\n{/* SKILL_OVERVIEW_START */}\n\n## When to Use agentOS\n\n- **Coding agents**: Run any coding agent with full OS access, file editing, shell execution, and tool use.\n- **Automated pipelines**: CI-like workflows where agents clone repos, fix bugs, run tests, and open PRs.\n- **Multi-agent systems**: Coordinators dispatching to specialized agents, review pipelines, planning chains.\n- **Scheduled maintenance**: Cron-based agents that audit code, update dependencies, or generate reports.\n- **Collaborative workspaces**: Multiple users observing and interacting with the same agent session in realtime.\n\n## Minimal Project\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Subscribe to streaming events\nconst conn = agent.connect();\nconn.on(\"sessionEvent\", (data) => {\n console.log(data.event);\n});\n\n// Create a session and send a prompt\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconst response = await agent.sendPrompt(\n session.sessionId,\n \"Write a hello world script to /home/user/hello.js\",\n);\nconsole.log(response.text);\n\n// Read the file the agent created\nconst content = await agent.readFile(\"/home/user/hello.js\");\nconsole.log(new TextDecoder().decode(content));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course)*\n\nAfter the quickstart, customize your agent with the [Registry](/registry).\n\n## Agents\n\n### Sessions & Transcripts\n\nCreate agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Stream events as they arrive\nconst conn = agent.connect();\nconn.on(\"sessionEvent\", (data) => {\n console.log(data.event.method, data.event);\n});\n\n// Create a session\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Send a prompt and wait for the response\nconst response = await agent.sendPrompt(\n session.sessionId,\n \"List all files in the home directory\",\n);\nconsole.log(response.text);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/sessions)*\n\n### Approvals\n\nApprove or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\n// Auto-approve all permissions server-side\nconst vm = agentOS({\n software: [pi],\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"Auto-approving\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Or handle permissions client-side for human-in-the-loop\nconst conn = agent.connect();\nconn.on(\"permissionRequest\", async (data) => {\n console.log(\"Permission requested:\", data.request);\n // \"once\" | \"always\" | \"reject\"\n await agent.respondPermission(data.sessionId, data.request.permissionId, \"once\");\n});\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/approvals)*\n\n### Bindings\n\nExpose your JavaScript functions to agents as CLI commands inside the VM. Each binding group becomes a binary at `/usr/local/bin/agentos-{name}`, and each binding becomes a subcommand with flags auto-generated from its Zod input schema. The server below defines a `weather` binding group with a `forecast` binding; the client opens a session and prompts the agent, which calls the binding itself as a shell command.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { z } from \"zod\";\n\n// Define a group of bindings (host functions). Each binding has a Zod input\n// schema and an `execute` handler that runs on the host. Bindings are exposed to\n// the agent as CLI commands at /usr/local/bin/agentos-{name} inside the VM.\nconst weatherBindings = {\n name: \"weather\",\n description: \"Weather data bindings\",\n bindings: {\n forecast: {\n description: \"Get the weather forecast for a city\",\n inputSchema: z.object({\n city: z.string().describe(\"City name\"),\n days: z.number().optional().describe(\"Number of days\"),\n }),\n execute: async (input: { city: string; days?: number }) => {\n const res = await fetch(\n `https://api.weather.example/forecast?city=${input.city}&days=${input.days ?? 3}`,\n );\n return res.json();\n },\n examples: [\n { description: \"3-day forecast for Paris\", input: { city: \"Paris\", days: 3 } },\n ],\n },\n },\n};\n\nconst vm = agentOS({\n bindings: [weatherBindings],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// The agent invokes the binding itself as a shell command:\n// agentos-weather forecast --city Paris --days 3\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"What's the weather in Paris?\");\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/bindings) or [Documentation](/docs/bindings)*\n\n### Agent-to-Agent\n\nLet one agent call another through a [binding](/docs/bindings). The coder gets a `review` binding it invokes itself, which bridges into the reviewer's isolated VM.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport { z } from \"zod\";\nimport pi from \"@agentos-software/pi\";\n\n// The reviewer is its own isolated agent VM.\nconst reviewer = agentOS({ software: [pi] });\n\n// The coder gets a `review` binding it can call itself: it copies a file from the\n// coder's VM into the reviewer's VM and asks the reviewer to review it.\nconst coder = agentOS({\n software: [pi],\n bindings: [\n {\n name: \"review\",\n description: \"Send a file to the reviewer agent and get back a review.\",\n bindings: {\n submit: {\n description: \"Submit a file path for review by the reviewer agent.\",\n inputSchema: z.object({ path: z.string() }),\n execute: async ({ path }: { path: string }) => {\n const client = createClient\u003Ctypeof registry>({\n endpoint: \"http://localhost:6420\",\n });\n const content = await client.coder\n .getOrCreate(\"feature-auth\")\n .readFile(path);\n const reviewerHandle = client.reviewer.getOrCreate(\"feature-auth\");\n await reviewerHandle.writeFile(path, content);\n const session = await reviewerHandle.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n const result = await reviewerHandle.sendPrompt(\n session.sessionId,\n `Review ${path} for security issues`,\n );\n return { review: result.text };\n },\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { coder, reviewer } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./agent-to-agent-server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst coderAgent = client.coder.getOrCreate(\"feature-auth\");\nconst session = await coderAgent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// The coder implements the feature, then calls the `review` binding itself so the\n// reviewer agent reviews the code. This is true agent-to-agent: the coder drives it.\nawait coderAgent.sendPrompt(\n session.sessionId,\n \"Implement the login feature in /home/user/src/auth.ts, then run `agentos-review submit --path /home/user/src/auth.ts` to have it reviewed.\",\n);\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/agent-to-agent)*\n\n### Multiplayer & Realtime\n\nConnect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\n// Client A: creates the session and sends prompts\nconst clientA = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agentA = clientA.vm.getOrCreate(\"shared-agent\");\nconst connA = agentA.connect();\nconnA.on(\"sessionEvent\", (data) =>\n console.log(\"[A]\", data.event.method),\n);\n\nconst session = await agentA.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agentA.sendPrompt(session.sessionId, \"Build a REST API\");\n\n// Client B: observes the same session (separate process)\nconst clientB = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst connB = clientB.vm.getOrCreate(\"shared-agent\").connect();\nconnB.on(\"sessionEvent\", (data) =>\n console.log(\"[B]\", data.event.method),\n);\n// Client B sees the same events as Client A\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/multiplayer)*\n\n### Workflows\n\nOrchestrate multi-step agent tasks with durable workflows that survive crashes and restarts.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { actor, queue } from \"rivetkit\";\nimport { workflow } from \"rivetkit/workflow\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\n// A durable workflow actor. Its `run` is built with `workflow()`, so every\n// `step(...)` is recorded, retried, and resumed: if the process crashes\n// mid-run, replay skips completed steps and continues where it left off.\nconst bugFixer = actor({\n queues: {\n fixBug: queue\u003C{ repo: string; issue: string }>(),\n },\n run: workflow(async (ctx) => {\n await ctx.loop(\"fix-bug-loop\", async (loopCtx) => {\n // Wait durably for the next bug-fix request from the queue.\n const message = await loopCtx.queue.next(\"wait-fix-bug\");\n const { repo, issue } = message.body;\n const agent = loopCtx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n\n await loopCtx.step(\"clone-repo\", () =>\n agent.exec(`git clone ${repo} /home/user/repo`),\n );\n\n await loopCtx.step(\"fix-bug\", async () => {\n const session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agent.sendPrompt(session.sessionId, `Fix the bug in issue: ${issue}`);\n await agent.closeSession(session.sessionId);\n });\n\n await loopCtx.step(\"run-tests\", () =>\n agent.exec(\"cd /home/user/repo && npm test\"),\n );\n });\n }),\n});\n\nexport const registry = setup({ use: { vm, bugFixer } });\nregistry.start();\n```\n\n[Documentation](/docs/workflows)\n\n## Operating System\n\n### Filesystem\n\nRead, write, and manage files inside the VM. The `/home/user` directory is persisted automatically across sleep/wake cycles.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Write a file\nawait agent.writeFile(\"/home/user/config.json\", JSON.stringify({ key: \"value\" }));\n\n// Read a file\nconst content = await agent.readFile(\"/home/user/config.json\");\nconsole.log(new TextDecoder().decode(content));\n\n// List directory contents recursively\nconst files = await agent.readdirRecursive(\"/home/user\", { maxDepth: 2 });\nconsole.log(files);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/filesystem)*\n\n### Processes & Shell\n\nExecute commands, spawn long-running processes, and open interactive shells.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// One-shot execution\nconst result = await agent.exec(\"echo hello && ls /home/user\");\nconsole.log(\"stdout:\", result.stdout);\nconsole.log(\"exit code:\", result.exitCode);\n\n// Spawn a long-running process\nconst conn = agent.connect();\nconn.on(\"processOutput\", (data) => {\n console.log(`[pid ${data.pid}]`, new TextDecoder().decode(data.data));\n});\n\nconst { pid } = await agent.spawn(\"node\", [\"/home/user/server.js\"]);\nconsole.log(\"Process ID:\", pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/processes)*\n\n### Networking & Previews\n\nProxy HTTP requests into VMs with `vmFetch`. Create preview URLs for port forwarding VM services to shareable public URLs.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Fetch from a service running inside the VM\nconst response = await agent.vmFetch(3000, \"/api/health\");\nconsole.log(\"Status:\", response.status);\n\n// Create a preview URL (port forwarding to a public URL)\nconst preview = await agent.createSignedPreviewUrl(3000);\nconsole.log(\"Public URL:\", preview.path);\nconsole.log(\"Expires at:\", new Date(preview.expiresAt));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/networking)*\n\n### Cron Jobs\n\nSchedule recurring commands and agent sessions with cron expressions.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Schedule a command every hour\nawait agent.scheduleCron({\n schedule: \"0 * * * *\",\n action: { type: \"exec\", command: \"rm\", args: [\"-rf\", \"/tmp/cache/*\"] },\n});\n\n// Schedule an agent session daily at 9 AM\nawait agent.scheduleCron({\n schedule: \"0 9 * * *\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Review the codebase for security issues and write a report to /home/user/audit.md\",\n },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/cron)*\n\n### Sandbox Mounting\n\nagentOS uses a hybrid model: agents run in a lightweight VM by default and mount a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. Mount the sandbox as a filesystem and expose its process management as bindings.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createSandboxFs, createSandboxBindings } from \"@rivet-dev/agentos-sandbox\";\nimport { SandboxAgent } from \"sandbox-agent\";\nimport { docker } from \"sandbox-agent/docker\";\n\nconst sandbox = await SandboxAgent.start({ sandbox: docker() });\n\nconst vm = agentOS({\n // Bindings let the agent control the sandbox\n bindings: [createSandboxBindings({ client: sandbox })],\n // Mounts let the agent read the sandbox filesystem (optional)\n mounts: [\n { path: \"/home/user/sandbox\", plugin: createSandboxFs({ client: sandbox }) },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n[Documentation](/docs/sandbox)\n\n{/* SKILL_OVERVIEW_END */}","src/content/docs/docs/crash-course.mdx","7462b560edf60cf4","docs/cron",{"id":113,"data":115,"body":121,"filePath":122,"digest":123,"deferredRender":16},{"title":116,"description":117,"editUrl":16,"head":118,"template":18,"sidebar":119,"pagefind":16,"draft":20},"Cron Jobs","Schedule recurring commands and agent sessions in agentOS VMs.",[],{"hidden":20,"attrs":120},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nSchedule recurring work with cron expressions, running either a shell command (`exec`) or an agent session (`session`), with overlap modes (`allow`, `skip`, `queue`) and `cronEvent` streaming to monitor execution. Cron jobs keep the actor alive while a job runs; the actor can sleep between executions.\n\n## Schedule a command\n\nRun a shell command on a recurring schedule. Pass a custom `id` to make a job easier to manage and cancel later.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Schedule a cleanup script every hour\nconst { id } = await client.vm.getOrCreate(\"my-agent\").scheduleCron({\n schedule: \"0 * * * *\",\n action: {\n type: \"exec\",\n command: \"rm\",\n args: [\"-rf\", \"/tmp/cache/*\"],\n },\n});\nconsole.log(\"Cron job ID:\", id);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Schedule an agent session\n\nCreate a recurring agent session that runs a prompt on a schedule.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Run an agent every day at 9 AM to check for issues\nawait client.vm.getOrCreate(\"my-agent\").scheduleCron({\n schedule: \"0 9 * * *\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Review the logs in /home/user/logs/ and summarize any errors\",\n options: { cwd: \"/home/user\" },\n },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Overlap modes\n\nControl what happens when a cron job triggers while a previous execution is still running.\n\n| Mode | Behavior |\n|------|----------|\n| `\"skip\"` | Skip this trigger if the previous run is still active |\n| `\"allow\"` | Allow concurrent executions (default) |\n| `\"queue\"` | Queue this trigger and run it after the previous one finishes |\n\nPrefer `\"skip\"` for most jobs to avoid unbounded concurrency if a run takes longer than the interval. Use `\"queue\"` when every trigger must eventually execute.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Queue overlapping executions\nawait client.vm.getOrCreate(\"my-agent\").scheduleCron({\n schedule: \"*/5 * * * *\",\n overlap: \"queue\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Process the next batch of tasks\",\n },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Monitor cron events\n\nSubscribe to the `cronEvent` event to track job execution. It is emitted whenever a cron job runs, carrying a single payload field:\n\n- **`data.event`**: A `CronEvent` describing the run.\n\n```ts\nconst conn = handle.connect();\nconn.on(\"cronEvent\", (data) => {\n // data is inferred: { event: CronEvent }\n console.log(\"Cron event:\", data.event);\n});\n```\n\nSubscribe before scheduling so you do not miss early runs.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.vm.getOrCreate(\"my-agent\");\n\nconst conn = handle.connect();\nconn.on(\"cronEvent\", (data) => {\n // data is inferred: { event: CronEvent }\n console.log(\"Cron event:\", data.event);\n});\n\nawait handle.scheduleCron({\n schedule: \"*/1 * * * *\",\n action: { type: \"exec\", command: \"echo\", args: [\"heartbeat\"] },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## List and cancel cron jobs\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.vm.getOrCreate(\"my-agent\");\n\n// List all cron jobs\nconst jobs = await handle.listCronJobs();\nfor (const job of jobs) {\n console.log(job.id, job.schedule);\n}\n\n// Cancel a specific job\nawait handle.cancelCronJob(jobs[0].id);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Example: Heartbeat pattern\n\nSchedule a recurring agent session to periodically check on a task. This is the core pattern behind [OpenClaw](https://openclaw.org), where an agent wakes up on a schedule to review progress, take action, and go back to sleep.\n\n```ts\nawait handle.scheduleCron({\n schedule: \"*/30 * * * *\",\n overlap: \"skip\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Check the status of open issues and take any necessary action\",\n },\n});\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\nThe agent sleeps between executions and only consumes resources when the cron job fires.","src/content/docs/docs/cron.mdx","13054976f8a9c05d","docs/deployment",{"id":124,"data":126,"body":132,"filePath":133,"digest":134,"deferredRender":16},{"title":127,"description":128,"editUrl":16,"head":129,"template":18,"sidebar":130,"pagefind":16,"draft":20},"Deploy","Choose the right deployment option for agentOS.",[],{"hidden":20,"attrs":131},{},"agentOS is powered by [Rivet](https://rivet.dev), an open-source actor platform, and runs as Rivet Actors. Three ways to run it in production:\n\n- **[Rivet Cloud](https://rivet.dev/cloud)**: fully managed (Rivet Compute, or bring your own cloud). Zero-ops.\n- **Self-hosted**: run the open-source Rivet platform on your own infrastructure (Kubernetes, Hetzner, VMs, and more) for full control.\n- **[agentOS Core](/docs/core)**: embed `@rivet-dev/agent-os-core` directly in any Node.js backend, no platform required.\n\nPick a deploy target below, or see [Rivet's deployment guides](https://rivet.dev/docs/deploy/).\n\n## Deploy targets\n\nSee the [Rivet deploy docs](https://rivet.dev/docs/deploy/) for the full list. Available targets:\n\n\u003Cdiv style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin:1.5rem 0;\">\n \u003Ca href=\"https://rivet.dev/docs/deploy/rivet-compute\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 640 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M225.1 16C145 16 80 81 80 161.1l0 189.8C80 431 145 496 225.1 496l189.8 0C495 496 560 431 560 350.9l0-189.8C560 81 495 16 414.9 16L225.1 16zm-1.4 42.4l192.6 0c55.5 0 100.5 45 100.5 100.5l0 192.6c0 55.5-45 100.5-100.5 100.5l-192.6 0c-55.5 0-100.5-45-100.5-100.5l0-192.6c0-55.5 45-100.5 100.5-100.5zM283 161.2c0-2.9-2.5-5.2-5.5-5.2l-41.2 0c-13.8 0-24.9 10.6-24.9 23.6l0 156.2c0 13 11.2 23.6 24.9 23.6l41.2 0c3 0 5.5-2.3 5.5-5.2l0-193zm119 128.7c-6-10.3-19.7-18.6-30.6-18.6l-63 0c-3.3 0-4.5 2.6-2.7 5.7l37.6 63.8c6 10.3 19.7 18.6 30.6 18.6l63 0c3.3 0 4.5-2.6 2.7-5.7l-37.6-63.8zm2.6-86.4c0-26.2-22.4-47.5-50.1-47.5l-44.2 0c-3.3 0-5.9 2.5-5.9 5.6l0 83.8c0 3.1 2.6 5.6 5.9 5.6l44.2 0c27.6 0 50.1-21.3 50.1-47.5z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Rivet Compute\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy to Rivet's managed compute platform.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/vercel\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M256 32L512 480 0 480 256 32z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Vercel\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy Next.js + RivetKit apps to Vercel's edge network.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/supabase\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M253.9 22.1c-.3-21-26.9-30.1-40-13.6L16.3 257.1c-23.3 29.4-2.4 72.6 35.1 72.6l204.4 0 2.4 160.2c.3 21 26.9 30 40 13.6L495.7 254.9c23.3-29.3 2.4-72.6-35.1-72.6l-205.7 0z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Supabase Functions\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Run on Supabase Edge Functions with the WebAssembly runtime.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/railway\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M2.4 219.1c-1.2 8.5-2 17.2-2.4 25.8l388.9 0c-1.4-2.7-3.2-5-5-7.4-66.5-85.9-102.2-78.4-153.4-80.6-17.1-.7-28.6-1-96.5-1-36.3 0-75.8 .1-114.3 .2-5 13.4-9.8 26.5-12.1 37.1l199.3 0 0 26-204.4 0zM392 270.8L.2 270.8c.4 6.9 1.1 13.8 2 20.5l361.7 0c16.1 0 25.1-9.1 28.1-20.5zM22.5 362.2S82.5 509.4 255.7 512c103.6 0 192.5-61.5 233-149.8l-466.2 0zM255.7 0C160 0 76.7 52.6 32.7 130.3 67 130.2 134 130.2 134 130.2l0 0 0 0c79.2 0 82.1 .4 97.6 1l9.6 .4c33.4 1.1 74.3 4.7 106.6 29.1 17.5 13.2 42.8 42.4 57.9 63.3 13.9 19.2 17.9 41.4 8.5 62.6-8.7 19.5-27.5 31.1-50.2 31.1l-355.5 0s2.1 9 5.3 18.9l485.3 0c8.6-25.9 13-53 13-80.3 0-141.4-114.7-256.1-256.3-256.1z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Railway\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy containers to Railway's managed infrastructure.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/kubernetes\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M242.1 275l13.8 6.7 13.8-6.6 3.4-14.9-9.6-12-15.4 0-9.6 11.9 3.4 14.9zm-14.7-52.9l0 0c2.5 1.8 5.8 2.1 8.6 .8s4.6-4.1 4.7-7.2l0 0 .2-.1 2.9-50.3c-3.5 .4-6.9 1-10.1 1.8-17.9 4-34.4 13-47.6 25.9l41.2 29.2zm-22.1 38.1l0 0c3-.8 5.3-3.2 6-6.2s-.3-6.2-2.7-8.2l0 0 0-.2-37.6-33.7c-11.5 18.7-16.4 40.6-14 62.4l48.2-13.9zM221.8 296c-1-4.4-5.3-7.2-9.7-6.4l-.1 0-.1-.1-49.5 8.4c7.4 20.5 21.4 38 39.8 49.8l19.2-46.3-.1-.2 0 0c.7-1.6 .9-3.4 .5-5.2zm41.6 18.3l0 0c-1.5-2.8-4.5-4.5-7.7-4.4-3 .1-5.7 1.8-7.1 4.4l0 0 0 0-24.3 44c17.4 5.9 36.1 6.8 54.1 2.8 3.3-.7 6.6-1.7 9.7-2.7l-24.4-44.1zm36.4-24.8l-.1 0c-.6-.1-1.3-.2-1.9-.1-2.7 .2-5.2 1.6-6.7 4s-1.7 5.2-.6 7.7l0 0-.1 .1 19.4 46.8c18.5-11.8 32.5-29.5 39.9-50.1l-49.9-8.4zm40.8-77.5l-37.4 33.5 0 .1 0 0c-2.3 2-3.4 5.2-2.7 8.2s3 5.4 6 6.2l.1 0 0 .2 48.5 14c2.1-21.8-3-43.6-14.4-62.2zM271 215.5l0 0c.1 1.8 .7 3.5 1.8 4.9 2.8 3.5 7.9 4.2 11.5 1.5l0 0 .1 .1 40.9-29c-15.6-15.2-35.7-25-57.3-27.7l2.8 50.2zm215.1 89.1L446.4 132.3c-2.1-9.1-8.2-16.7-16.6-20.7L269.3 35c-8.4-4-18.2-4-26.6 0L82.1 111.7c-8.4 4-14.5 11.6-16.6 20.7L25.9 304.6c-1.8 8-.3 16.5 4.2 23.3 .5 .8 1.1 1.6 1.7 2.4L142.9 468.5c5.9 7.3 14.7 11.5 24 11.5l178.2 0c9.3 0 18.1-4.2 24-11.4L480.2 330.4c5.8-7.2 8-16.7 5.9-25.8zm-61-2.1c-1.6 5.4-7.1 8.5-12.5 7.1l-.1 0-.1 0-.1 0-.1 0-.9-.2c-.6-.1-1.1-.2-1.6-.3-2-.6-3.9-1.3-5.7-2.1-.9-.4-1.9-.8-2.9-1.2l-.3-.1c-5.3-2.1-10.8-3.8-16.5-4.8-1.7-.1-3.4 .4-4.7 1.6l-.6 .4 0 0 0 0c-.7-.1-2.8-.5-4-.7-9.3 29.2-29 54-55.3 69.6 .1 .4 .3 .8 .5 1.3 .3 .9 .6 1.7 1.1 2.5l0 0 0 0-.3 .7c-.8 1.5-1 3.3-.5 4.9 2.3 5.3 5.2 10.4 8.5 15.2 .6 .9 1.2 1.7 1.8 2.5 1.3 1.6 2.4 3.3 3.4 5.1 .3 .5 .6 1.2 .9 1.8l.4 .8c1.9 3.2 1.8 7.2-.2 10.3s-5.6 4.8-9.3 4.4-6.8-2.9-8.1-6.4l-.4-.7c-.3-.6-.6-1.2-.8-1.7-.8-1.9-1.4-3.9-1.9-5.9-.3-1-.5-1.9-.9-3l-.1-.3c-1.6-5.5-3.8-10.8-6.5-15.9-1-1.4-2.5-2.4-4.2-2.7l-.7-.2 0 0 0 0c-.2-.3-.5-1-1-1.7-.4-.7-.8-1.4-1.1-1.9-5.4 2-10.8 3.7-16.4 4.9-24 5.5-49.2 3.7-72.2-5.2l-2.2 3.9 0 0c-1.5 .2-3 .9-4.1 2-2.5 3.5-4.3 7.4-5.4 11.5-.6 1.9-1.3 3.8-2 5.7-.3 1-.6 2-.9 3-.5 2-1.1 3.9-1.9 5.8-.2 .5-.5 1.1-.8 1.6l-.4 .8 0 0 0 0c-1.7 3.7-5.4 6.2-9.5 6.3-1.3 0-2.6-.3-3.8-.9-4.9-2.7-6.7-8.8-4.1-13.8 .1-.3 .3-.6 .4-.9 .3-.6 .6-1.2 .8-1.7 1-1.8 2.1-3.5 3.4-5.2 .6-.8 1.2-1.6 1.8-2.5 3.4-4.9 6.3-10.1 8.7-15.6 .3-1.8 0-3.7-.9-5.4l0 0 0 0 1.7-4.1c-26.3-15.5-45.9-40.1-55.4-69.1l-4.1 .7 0 0-.4-.2c-1.5-1-3.2-1.6-5.1-1.7-5.6 1.1-11.1 2.7-16.5 4.8l-.3 .1c-1 .4-1.9 .8-2.9 1.2-1.9 .9-3.8 1.6-5.8 2.1-.5 .1-1.1 .3-1.8 .4l-.7 .2-.1 0-.1 0-.1 0-.1 0c-3.5 1.2-7.4 .2-10-2.4s-3.5-6.5-2.3-10 4.3-6 7.9-6.5l.1 0 .1 0 0 0 .1 0 .8-.2c.6-.2 1.3-.3 1.8-.4 2-.4 4-.5 6.1-.6 1 0 2.1-.1 3.1-.2l.2 0c5.7-.4 11.4-1.3 17-2.8 1.5-.9 2.8-2.2 3.7-3.6l.4-.5 0 0 0 0 4-1.1c-4.3-30.3 2.6-61.1 19.6-86.6l-3-2.7 0 0-.1-.4c-.1-1.8-.8-3.6-1.8-5.1-4.4-3.8-9.2-7.2-14.3-10-.9-.5-1.9-1-2.7-1.5-1.8-.9-3.6-2-5.2-3.2-.4-.3-.9-.7-1.4-1.1l-.6-.5-.1 0-.1-.1c-2.2-1.7-3.7-4.2-4.2-7-.5-2.6 .2-5.3 1.8-7.3 1.9-2.3 4.8-3.6 7.7-3.4 2.5 .1 4.8 1 6.7 2.5l.6 .5c.5 .4 1.1 .9 1.6 1.2 1.5 1.4 3 2.8 4.3 4.4 .6 .8 1.3 1.5 2.1 2.3l.2 .2c3.9 4.2 8.2 8.1 12.8 11.5 1.5 .9 3.3 1.1 5 .6l.7-.1 0 0 0 0c.6 .4 2.4 1.7 3.4 2.4 16.9-17.9 38.7-30.4 62.8-35.8 5.7-1.3 11.5-2.2 17.3-2.7l.2-4 0 0c1.5-1.3 2.6-3 3.1-4.9 .2-5.8-.1-11.6-1.1-17.3l0-.1c-.1-1.1-.3-2.1-.5-3.1-.4-2-.7-4-.8-6.1 0-.5 0-1.1 0-1.7l0-.8 0-.1 0-.1c-.4-3.7 1.4-7.3 4.5-9.3s7.2-2 10.3 0 4.9 5.6 4.5 9.3l0 1c0 .6 0 1.3 0 1.8-.1 2-.4 4.1-.8 6.1-.2 1-.4 2-.5 3.1l-.1 .5c-.9 5.6-1.2 11.3-1 16.9 .3 1.7 1.2 3.2 2.6 4.3l.5 .5 0 0 0 0c0 .7 .1 2.9 .2 4.2 15.1 1.4 29.8 5.5 43.5 12.1 13.5 6.6 25.8 15.5 36.2 26.4l3.6-2.6 0 0 .5 0c1.8 .3 3.6 .1 5.3-.7 4.6-3.4 8.8-7.2 12.6-11.4l.3-.4c.7-.8 1.4-1.5 2-2.3 1.3-1.6 2.8-3.1 4.3-4.5 .4-.4 1-.8 1.5-1.2l.6-.5c2.7-2.6 6.6-3.5 10.1-2.2s6 4.4 6.4 8.1-1.3 7.3-4.4 9.3l-.7 .6c-.5 .4-1 .8-1.4 1.1-1.7 1.2-3.4 2.2-5.2 3.1-.9 .5-1.8 1-2.8 1.5-5.1 2.9-9.8 6.2-14.3 10-1.2 1.3-1.8 3-1.7 4.7l-.1 .7 0 0 0 0c-.3 .2-.8 .7-1.5 1.3s-1.4 1.2-1.9 1.7c17 25.4 24.1 56.1 20 86.4l3.8 1.1 0 0 .3 .4c.9 1.6 2.2 2.9 3.8 3.7 5.6 1.5 11.3 2.4 17 2.8l.2 0c1.1 .1 2.1 .1 3.1 .2 2 0 4.1 .2 6.1 .6 .5 .1 1.1 .2 1.8 .4l1 .2c5.5 1.1 9.1 6.3 8.2 11.8z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Kubernetes\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy to any Kubernetes cluster with container images.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/aws-ecs\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 640 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M180.41 203.01c-.72 22.65 10.6 32.68 10.88 39.05a8.164 8.164 0 0 1-4.1 6.27l-12.8 8.96a10.66 10.66 0 0 1-5.63 1.92c-.43-.02-8.19 1.83-20.48-25.61a78.608 78.608 0 0 1-62.61 29.45c-16.28.89-60.4-9.24-58.13-56.21-1.59-38.28 34.06-62.06 70.93-60.05 7.1.02 21.6.37 46.99 6.27v-15.62c2.69-26.46-14.7-46.99-44.81-43.91-2.4.01-19.4-.5-45.84 10.11-7.36 3.38-8.3 2.82-10.75 2.82-7.41 0-4.36-21.48-2.94-24.2 5.21-6.4 35.86-18.35 65.94-18.18a76.857 76.857 0 0 1 55.69 17.28 70.285 70.285 0 0 1 17.67 52.36l-.01 69.29zM93.99 235.4c32.43-.47 46.16-19.97 49.29-30.47 2.46-10.05 2.05-16.41 2.05-27.4-9.67-2.32-23.59-4.85-39.56-4.87-15.15-1.14-42.82 5.63-41.74 32.26-1.24 16.79 11.12 31.4 29.96 30.48zm170.92 23.05c-7.86.72-11.52-4.86-12.68-10.37l-49.8-164.65c-.97-2.78-1.61-5.65-1.92-8.58a4.61 4.61 0 0 1 3.86-5.25c.24-.04-2.13 0 22.25 0 8.78-.88 11.64 6.03 12.55 10.37l35.72 140.83 33.16-140.83c.53-3.22 2.94-11.07 12.8-10.24h17.16c2.17-.18 11.11-.5 12.68 10.37l33.42 142.63L420.98 80.1c.48-2.18 2.72-11.37 12.68-10.37h19.72c.85-.13 6.15-.81 5.25 8.58-.43 1.85 3.41-10.66-52.75 169.9-1.15 5.51-4.82 11.09-12.68 10.37h-18.69c-10.94 1.15-12.51-9.66-12.68-10.75L328.67 110.7l-32.78 136.99c-.16 1.09-1.73 11.9-12.68 10.75h-18.3zm273.48 5.63c-5.88.01-33.92-.3-57.36-12.29a12.802 12.802 0 0 1-7.81-11.91v-10.75c0-8.45 6.2-6.9 8.83-5.89 10.04 4.06 16.48 7.14 28.81 9.6 36.65 7.53 52.77-2.3 56.72-4.48 13.15-7.81 14.19-25.68 5.25-34.95-10.48-8.79-15.48-9.12-53.13-21-4.64-1.29-43.7-13.61-43.79-52.36-.61-28.24 25.05-56.18 69.52-55.95 12.67-.01 46.43 4.13 55.57 15.62 1.35 2.09 2.02 4.55 1.92 7.04v10.11c0 4.44-1.62 6.66-4.87 6.66-7.71-.86-21.39-11.17-49.16-10.75-6.89-.36-39.89.91-38.41 24.97-.43 18.96 26.61 26.07 29.7 26.89 36.46 10.97 48.65 12.79 63.12 29.58 17.14 22.25 7.9 48.3 4.35 55.44-19.08 37.49-68.42 34.44-69.26 34.42zm40.2 104.86c-70.03 51.72-171.69 79.25-258.49 79.25A469.127 469.127 0 0 1 2.83 327.46c-6.53-5.89-.77-13.96 7.17-9.47a637.37 637.37 0 0 0 316.88 84.12 630.22 630.22 0 0 0 241.59-49.55c11.78-5 21.77 7.8 10.12 16.38zm29.19-33.29c-8.96-11.52-59.28-5.38-81.81-2.69-6.79.77-7.94-5.12-1.79-9.47 40.07-28.17 105.88-20.1 113.44-10.63 7.55 9.47-2.05 75.41-39.56 106.91-5.76 4.87-11.27 2.3-8.71-4.1 8.44-21.25 27.39-68.49 18.43-80.02z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">AWS ECS\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Run containerized workloads on Amazon Elastic Container Service.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/gcp-cloud-run\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M340.5 163.6l44.5-44.5 3-18.7c-81.1-73.7-210-65.4-283.1 17.4-20.3 23-35.4 51.7-43.4 81.3l15.9-2.2 89-14.7 6.9-7c39.6-43.5 106.5-49.3 152.3-12.3l15 .8zm107.9 34.2c-10.2-37.7-31.2-71.5-60.4-97.4l-62.5 62.5c26.4 21.6 41.4 54 40.8 88.1l0 11.1c30.7 0 55.6 24.9 55.6 55.6s-24.9 55-55.6 55l-111.3 0-10.9 11.9 0 66.7 10.9 10.5 111.3 0c79.9 .6 145.1-63 145.7-142.8 .4-48.4-23.5-93.8-63.6-120.9zM143.7 461.6l111.2 0 0-89-111.2 0c-7.9 0-15.6-1.7-22.8-5l-15.8 4.8-44.8 44.5-3.9 15.1c25.1 19 55.8 29.6 87.3 29.5zm0-288.7C63.9 173.3-.5 238.5 0 318.3 .3 362.9 21.1 404.9 56.4 432.1l64.5-64.5C93 354.9 80.5 322 93.2 294s45.6-40.4 73.5-27.8c12.3 5.6 22.2 15.4 27.8 27.8L259 229.5c-27.4-35.9-70.1-56.8-115.2-56.7z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Google Cloud Run\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy containers to Google Cloud Run for auto-scaling.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/hetzner\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M0 256a256 256 0 1 1 512 0 256 256 0 1 1 -512 0zm411.1 5.6c3-.6 5.9-1.8 8.4-3.7 2.3-2.4 3.4-5.6 3.2-8.9 0-2.6-.8-5.3-2.1-7.6-2.1-3.5-5.8-5.8-9.9-5.9l-2 0-4.8-.1-21.5 0c-1.3 0-1.9 .5-1.9 1.8l0 37.7c0 1.3 .5 1.9 1.9 1.9l5.6 0c1.3 0 1.9-.6 1.9-1.9l0-12.5 6.9 0c1.5 .1 3 .7 4 1.7L411.7 275c1 1 2.3 1.6 3.7 1.7l8.3 0c1.3 0 1.6-.8 .7-1.7l-13.3-13.4zm-1.4-8.1l-19.8 0 0-9.5 19.8 0c2 .3 3.5 1.9 3.5 4l0 1.4c0 2.1-1.5 3.8-3.5 4.1zm-37.8 14.3l-30.8 0 0-7.8 24.6 0c1.3 0 1.8-.5 1.8-1.9l0-4.5c0-1.3-.5-1.9-1.8-1.9l-24.6 0 0-7.8 30.8 0c1.3 0 1.8-.6 1.8-1.8l0-5.2c0-1.3-.5-1.8-1.8-1.8l-38.4 0c-1.3 0-1.8 .5-1.8 1.8l0 37.7c0 1.3 .5 1.9 1.8 1.9l38.4 0c1.3 0 1.8-.5 1.8-1.9l0-5.1c0-1.3-.6-1.9-1.9-1.9zm-47-19c.1-2.6-.7-5.2-2.1-7.4-2-3.6-5.8-5.9-10-6.1l-28.2 0c-1.3 0-1.9 .6-1.9 1.8l0 37.6c0 1.3 .5 1.9 1.9 1.9l6 0c1.3 0 1.9-.5 1.9-1.9l0-30.7 16.4 0c3.5 0 6.9 2.3 6.9 5.8l0 25c0 1.3 .5 1.9 1.8 1.9l5.5 0c1.3 0 1.9-.6 1.9-1.9l0-26zm-50.8 18.8l-26.5 0 26.6-23.3c1.1-.9 1.8-2.1 1.8-3.4l0-3.7c0-1.3-.5-1.8-1.8-1.8l-38.5 0c-1.3 0-1.8 .6-1.8 1.8l0 5.2c0 1.3 .5 1.8 1.8 1.8l24.7 0-24.8 23.2c-1.1 .9-1.8 2-1.9 3.4l0 4.1c0 1.3 .6 1.9 1.9 1.9l38.4 0c1.3 0 1.9-.6 1.9-1.9l0-5.4c.1-1.3-.4-1.9-1.8-1.9zm-48.9-32.3l-38.4 0c-1.3 0-1.9 .5-1.9 1.8l0 5.2c0 1.3 .5 1.8 1.9 1.8l14.5 0 0 30.7c0 1.3 .6 1.9 1.8 1.9l6.2 0c1.3 0 1.8-.6 1.8-1.9l0-30.7 14.1 0c1.3 0 1.9-.5 1.9-1.8l0-5.1c-.1-1.3-.6-1.9-1.9-1.9zm-48.9 32.6l-30.7 0 0-7.8 24.5 0c1.3 0 1.9-.5 1.9-1.9l0-4.5c0-1.3-.5-1.9-1.9-1.9l-24.5 0 0-7.8 30.7 0c1.3 0 1.8-.6 1.8-1.8l0-5.2c0-1.3-.5-1.8-1.8-1.8l-38.3 0c-1.3 0-1.9 .5-1.9 1.8l0 37.7c0 1.3 .6 1.9 1.9 1.9l38.3 0c1.3 0 1.8-.5 1.8-1.9l0-5.1c0-1.3-.5-1.9-1.8-1.9zm-48.9-32.6l-5.8 0c-1.3 0-1.8 .6-1.8 1.8l0 14.7-23.1 0 0-14.7c0-1.3-.5-1.8-1.8-1.8l-5.8 0c-1.3 0-1.8 .6-1.8 1.8l0 37.7c0 1.3 .5 1.9 1.8 1.9l5.8 0c1.3 0 1.8-.5 1.8-1.9l0-14.9 23.1 0 0 14.9c0 1.3 .5 1.9 1.8 1.9l5.8 0c1.3 0 1.9-.6 1.9-1.9l0-37.7c-.1-1.3-.6-1.8-1.9-1.8z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Hetzner\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy to Hetzner's cost-effective cloud infrastructure.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/vm-and-bare-metal\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M64 32C28.7 32 0 60.7 0 96l0 64c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 32zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64l0 64c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 288zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">VM & Bare Metal\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Run on virtual machines or bare metal servers with full control.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/custom\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" width=\"22\" height=\"22\" fill=\"currentColor\" style=\"color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M192 104.8c0-9.2-5.8-17.3-13.2-22.8C167.2 73.3 160 61.3 160 48c0-26.5 28.7-48 64-48s64 21.5 64 48c0 13.3-7.2 25.3-18.8 34c-7.4 5.5-13.2 13.6-13.2 22.8c0 12.8 10.4 23.2 23.2 23.2l56.8 0c26.5 0 48 21.5 48 48l0 56.8c0 12.8 10.4 23.2 23.2 23.2c9.2 0 17.3-5.8 22.8-13.2c8.7-11.6 20.7-18.8 34-18.8c26.5 0 48 28.7 48 64s-21.5 64-48 64c-13.3 0-25.3-7.2-34-18.8c-5.5-7.4-13.6-13.2-22.8-13.2c-12.8 0-23.2 10.4-23.2 23.2L384 464c0 26.5-21.5 48-48 48l-56.8 0c-12.8 0-23.2-10.4-23.2-23.2c0-9.2 5.8-17.3 13.2-22.8c11.6-8.7 18.8-20.7 18.8-34c0-26.5-28.7-48-64-48s-64 21.5-64 48c0 13.3 7.2 25.3 18.8 34c7.4 5.5 13.2 13.6 13.2 22.8c0 12.8-10.4 23.2-23.2 23.2L48 512c-26.5 0-48-21.5-48-48L0 343.2C0 330.4 10.4 320 23.2 320c9.2 0 17.3 5.8 22.8 13.2C54.7 344.8 66.7 352 80 352c26.5 0 48-28.7 48-64s-21.5-64-48-64c-13.3 0-25.3 7.2-34 18.8C40.5 250.2 32.4 256 23.2 256C10.4 256 0 245.6 0 232.8L0 176c0-26.5 21.5-48 48-48l120.8 0c12.8 0 23.2-10.4 23.2-23.2z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Custom Platform\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Integrate with any other hosting platform of your choice.\u003C/div>\n \u003C/a>\n\u003C/div>\n\n## Enterprise support\n\nEnterprise support and managed deployment are available, including dedicated support, custom SLAs, and compliance reviews. [Contact the Rivet team](https://rivet.dev/sales) to discuss your requirements.","src/content/docs/docs/deployment.mdx","3fe4a725cd89b727",{"id":9,"data":136,"body":141,"filePath":142,"digest":143,"deferredRender":16},{"title":137,"description":106,"editUrl":16,"head":138,"tableOfContents":20,"template":18,"sidebar":139,"pagefind":16,"draft":20},"Introduction",[],{"hidden":20,"attrs":140},{},"import DocsLanding from '@rivet-dev/docs-theme/components/DocsLanding.astro';\nimport AgentOSHeroLogo from '../../../components/AgentOSHeroLogo.astro';\n\n\u003CDocsLanding>\n\t\u003CAgentOSHeroLogo slot=\"hero\" />\n\u003C/DocsLanding>","src/content/docs/docs/index.mdx","352df9dbb91266b5","docs/filesystem",{"id":144,"data":146,"body":152,"filePath":153,"digest":154,"deferredRender":16},{"title":147,"description":148,"editUrl":16,"head":149,"template":18,"sidebar":150,"pagefind":16,"draft":20},"Filesystem","Read, write, mount, and manage files inside agentOS, all backed by a virtual filesystem isolated from the host disk.",[],{"hidden":20,"attrs":151},{},"import { Tabs, TabItem } from '@astrojs/starlight/components';\n\nEach 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](/docs/persistence) for the details.\n\n## Mounts\n\nBack 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.\n\n\u003CTabs>\n\n\u003CTabItem label=\"Host directory\">\n\nProject 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.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n {\n path: \"/home/user/repo\",\n plugin: { id: \"host_dir\", config: { hostPath: \"/path/to/repo\" } },\n readOnly: true,\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\u003C/TabItem>\n\n\u003CTabItem label=\"S3\">\n\nMount 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.\n\nThe 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.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n {\n path: \"/home/user/data\",\n plugin: {\n id: \"s3\",\n config: {\n bucket: \"my-bucket\",\n prefix: \"agent-data/\",\n region: \"us-east-1\",\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\nThe `s3` plugin config also accepts `credentials` (`{ accessKeyId, secretAccessKey }`) and a custom `endpoint` for S3-compatible providers.\n\n\u003C/TabItem>\n\n\u003CTabItem label=\"Google Drive\">\n\nMount a Google Drive folder with the built-in `google_drive` plugin.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n {\n path: \"/home/user/drive\",\n plugin: {\n id: \"google_drive\",\n config: {\n credentials: {\n clientEmail: process.env.GOOGLE_DRIVE_CLIENT_EMAIL!,\n privateKey: process.env.GOOGLE_DRIVE_PRIVATE_KEY!,\n },\n folderId: process.env.GOOGLE_DRIVE_FOLDER_ID!,\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\u003C/TabItem>\n\n\u003CTabItem label=\"In-memory\">\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createInMemoryFileSystem } from \"@rivet-dev/agent-os-core\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n { path: \"/home/user/scratch\", driver: createInMemoryFileSystem() },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\u003C/TabItem>\n\n\u003C/Tabs>\n\n## File operations\n\nThese 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](#mounts) is faster than copying files in. Programs that need stdin or live output use exec instead (see [Core](/docs/core)).\n\n### Read and write\n\n```ts\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Write a file (string or Uint8Array)\nawait agent.writeFile(\"/home/user/hello.txt\", \"Hello, world!\");\n\n// Read a file (returns Uint8Array)\nconst content = await agent.readFile(\"/home/user/hello.txt\");\nconsole.log(new TextDecoder().decode(content));\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n### Batch read and write\n\n```ts\n// Batch write (creates parent directories automatically)\nconst writeResults = await agent.writeFiles([\n { path: \"/home/user/src/index.ts\", content: \"console.log('hello');\" },\n { path: \"/home/user/src/utils.ts\", content: \"export function add(a: number, b: number) { return a + b; }\" },\n]);\n\n// Batch read\nconst readResults = await agent.readFiles([\n \"/home/user/src/index.ts\",\n \"/home/user/src/utils.ts\",\n]);\nfor (const result of readResults) {\n console.log(result.path, new TextDecoder().decode(result.content ?? new Uint8Array()));\n}\n```\n\n### Directories\n\n```ts\n// Create a directory\nawait agent.mkdir(\"/home/user/projects\");\n\n// List directory contents\nconst entries = await agent.readdir(\"/home/user/projects\");\n\n// Recursive listing with metadata\nconst tree = await agent.readdirRecursive(\"/home/user\", {\n maxDepth: 3,\n exclude: [\"node_modules\"],\n});\nfor (const entry of tree) {\n console.log(entry.type, entry.path, entry.size);\n}\n```\n\n### File metadata\n\n```ts\n// Check if a path exists\nconst fileExists = await agent.exists(\"/home/user/hello.txt\");\n\n// Get file metadata\nconst info = await agent.stat(\"/home/user/hello.txt\");\nconsole.log(info.size, info.isDirectory, info.mtimeMs);\n```\n\n### Move and delete\n\n```ts\n// Move/rename\nawait agent.move(\"/home/user/old.txt\", \"/home/user/new.txt\");\n\n// Delete a file\nawait agent.delete(\"/home/user/new.txt\");\n\n// Delete a directory recursively\nawait agent.delete(\"/home/user/temp\", { recursive: true });\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\n## Permissions\n\nFilesystem 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:\n\n```ts\nconst vm = agentOS({\n permissions: {\n fs: {\n default: \"allow\",\n rules: [{ mode: \"deny\", operations: [\"*\"], paths: [\"/home/user/secrets/**\"] }],\n },\n },\n});\n```\n\nSee [Permissions](/docs/permissions) for the full configuration.\n\n## Sandboxes\n\nFor heavier or untrusted workloads, run a full Linux [sandbox](/docs/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. See [Sandbox Mounting](/docs/sandbox) for setup.\n\n## Default layout\n\nWith no `mounts` configured, every VM boots an Alpine-based root filesystem with the standard POSIX directories:\n\n- `/home/user`: 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/user/data`).\n- `/bin`, `/sbin`, `/usr`: installed commands (common POSIX utilities by default, plus any [software](/docs/software) you add).\n- `/etc`, `/lib`, `/opt`, `/root`, `/run`, `/srv`, `/tmp`, `/var`, `/mnt`: standard system paths.\n\nIt is backed by the VM's own filesystem and persisted across sleep/wake. Nothing comes from or touches the host disk.","src/content/docs/docs/filesystem.mdx","90f14cdcae0332e3","docs/js-runtime",{"id":155,"data":157,"body":163,"filePath":164,"digest":165,"deferredRender":16},{"title":158,"description":159,"editUrl":16,"head":160,"template":18,"sidebar":161,"pagefind":16,"draft":20},"JavaScript Runtime","How agentOS runs guest JavaScript: native V8 acceleration, low memory overhead, and Node.js compatibility.",[],{"hidden":20,"attrs":162},{},"The JavaScript runtime is powered by the Rivet [Secure Exec](https://secureexec.dev) project, which provides the isolated V8 runtime that agentOS runs guest code in. Every guest VM executes its JavaScript inside this runtime, fully sandboxed from the host.\n\n## JavaScript Acceleration\n\n- **JavaScript is unusually slow as WebAssembly**: unlike most software, JavaScript pays a heavy penalty when compiled to WebAssembly, because so much engineering has gone into JIT-compiling JavaScript directly in V8.\n- **Native V8, full JIT**: agentOS therefore runs guest JavaScript on the native V8 engine with its full JIT compiler, not through a WASM translation layer. We call this **JavaScript Acceleration**.\n- **Native-speed execution**: guest JavaScript runs at native speed while staying inside the isolation boundary, with normal Node.js semantics.\n\n## Comparison to Node.js efficiency\n\n- **Isolate model, not processes**: agentOS runs each agent inside a V8 isolate rather than spawning a full Node.js process per agent.\n- **Low memory overhead**: an isolate carries far less per-agent memory overhead than a full Node.js process, so many agents fit in the footprint that a process-per-agent model would spend on a handful.\n- **Benchmarks**: see the Secure Exec [benchmarks](https://secureexec.dev/docs/benchmarks) for cold start, warm execution, and reuse measurements.\n\n## Node.js compatibility\n\nGuest code runs as Node.js (reporting `process.version` as `v22.0.0`), but it never touches the host runtime. Every `node:` builtin resolves to a kernel-backed bridge or an in-isolate polyfill, never the real host module. For the full builtin matrix (`fs`, `net`, `http`, `crypto`, undici-backed `fetch`, and more), see the Secure Exec [Node.js Compatibility](https://secureexec.dev/docs/nodejs-compatibility) reference.\n\n### npm packages\n\nBy default the VM has no npm packages installed. Mount a host `node_modules` directory to give guest code access to real packages: the `nodeModulesMount` helper projects it read-only at `/root/node_modules`, and the in-kernel resolver walks it exactly like Node.js does, with no bundling or patching.\n\n```ts\nimport { agentOS, setup, nodeModulesMount } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n // Project a host node_modules tree into the VM (read-only by default).\n mounts: [nodeModulesMount(\"/absolute/path/to/node_modules\")],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\nResolution matches naive Node.js over the mounted tree: the ancestor `node_modules` walk, `package.json` `exports`/`imports` and conditions, and `realpath`/symlink following (so pnpm and yarn layouts resolve too). Both ESM `import` and CommonJS `require` work. See the Secure Exec [module loading](https://secureexec.dev/docs/features/module-loading) guide for the full model.","src/content/docs/docs/js-runtime.mdx","9d5bf6a0894c1577","docs/limitations",{"id":166,"data":168,"body":174,"filePath":175,"digest":176,"deferredRender":16},{"title":169,"description":170,"editUrl":16,"head":171,"template":18,"sidebar":172,"pagefind":16,"draft":20},"Limitations","What the agentOS VM does not support, and how to work around it.",[],{"hidden":20,"attrs":173},{},"agentOS is a Linux environment with a POSIX-compliant virtual kernel. It handles most agent workloads (coding, scripting, file I/O, networking) with near-zero overhead.\n\n## Sandbox mounting\n\nWhen a workload needs a full Linux OS, agents can escalate to a full sandbox on demand without changing code. The [sandbox mounting](/docs/sandbox) extension mounts the sandbox as a filesystem and lets you execute commands on it, like mounting a hard drive on your own machine. Files written in the VM are available in the sandbox and vice versa.\n\nSee [agentOS vs Sandbox](/docs/versus-sandbox) for a detailed comparison.\n\n## Limitations\n\n### Software registry\n\nagentOS uses its own [software registry](/registry) of popular tools cross-compiled for the runtime. You cannot download and install arbitrary binaries (for example via `curl` or `apt`), and standard Linux package managers (`apt`, `yum`) are not available since agentOS runs a streamlined Linux environment rather than a full distribution. Native binaries that are not yet available in the registry (such as Go, Rust, or C++ toolchains) require a full [sandbox](/docs/sandbox).\n\nSee [Software](/docs/software) for how to install and configure available packages.\n\n### Lightweight Linux kernel\n\nagentOS provides a POSIX-compliant virtual Linux kernel with full filesystem operations, networking, and process management. It implements a focused subset of the kernel surface, so a few Linux-specific features are not available:\n\n- Kernel modules and eBPF\n- Container runtimes (e.g. Docker)\n- File watching (`inotify`, `fs.watch`)\n\n### No hardware access\n\nThe VM has no access to GPUs, USB devices, or other hardware.","src/content/docs/docs/limitations.mdx","9f65ad4a48fd07f0","docs/llm-credentials",{"id":177,"data":179,"body":185,"filePath":186,"digest":187,"deferredRender":16},{"title":180,"description":181,"editUrl":16,"head":182,"template":18,"sidebar":183,"pagefind":16,"draft":20},"LLM Credentials","Pass LLM API keys to agent sessions securely.",[],{"hidden":20,"attrs":184},{},"Pass LLM provider API keys to agent sessions so keys stay on the server and are injected at session creation, with per-tenant isolation for multi-tenant deployments.\n\n## Passing API keys\n\nPass LLM provider keys via the `env` option on `createSession`. The VM does not inherit from the host `process.env`, so keys must be passed explicitly.\n\n```ts\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({\n\tendpoint: \"http://localhost:6420\",\n});\n\n// Pass LLM provider keys via the `env` option on createSession. The VM does\n// not inherit from the host process.env, so keys must be passed explicitly.\nconst session = await client.vm.getOrCreate(\"my-agent\").createSession(\"pi\", {\n\tenv: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\nconsole.log(session.sessionId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/llm-credentials)*\n\n## Per-tenant credentials\n\nGive each tenant an isolated VM by keying `getOrCreate` on the tenant id, look up that tenant's API key on the server, and inject it via the session `env`. Credentials stay on the server and never reach the client.\n\nFirst, declare the agent software on the server:\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"./software/pi\";\n\n// The VM does not inherit the host process.env. LLM provider keys are passed\n// explicitly per session, so the server just declares the agent software here.\nconst vm = agentOS({\n\tsoftware: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/llm-credentials)*\n\nThen resolve each tenant's key and pass it at session creation:\n\n```ts\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({\n\tendpoint: \"http://localhost:6420\",\n});\n\n// Stand-in for your own per-tenant credential store.\ndeclare function lookupTenantApiKey(tenantId: string): Promise\u003Cstring>;\n\n// Give each tenant an isolated VM keyed by their tenant id, then inject that\n// tenant's API key from your database at session creation. Keys stay on the\n// server and never reach the client.\nasync function startTenantSession(tenantId: string) {\n\tconst anthropicApiKey = await lookupTenantApiKey(tenantId);\n\n\treturn client.vm.getOrCreate(tenantId).createSession(\"pi\", {\n\t\tenv: { ANTHROPIC_API_KEY: anthropicApiKey },\n\t});\n}\n\nconst session = await startTenantSession(\"tenant-123\");\nconsole.log(session.sessionId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/llm-credentials)*\n\nBecause keys are resolved per tenant from your own credential store (the `lookupTenantApiKey` stand-in above) and stay on the server, each session uses the tenant's own key and one tenant's key never reaches another tenant or the client.\n\n## Embedded LLM Gateway\n\nThe [Embedded LLM Gateway](/docs/llm-gateway) (coming soon) will remove the need to manage API keys manually. It routes all agent LLM requests through a managed proxy built into agentOS, providing per-tenant usage metering, rate limiting, and cost controls without deploying a separate gateway service.","src/content/docs/docs/llm-credentials.mdx","7f8cd46f40822971","docs/llm-gateway",{"id":188,"data":190,"body":196,"filePath":197,"digest":198,"deferredRender":16},{"title":191,"description":192,"editUrl":16,"head":193,"template":18,"sidebar":194,"pagefind":16,"draft":20},"Embedded LLM Gateway","Route, meter, and manage LLM API calls from agents.",[],{"hidden":20,"attrs":195},{},"{/* TODO: This page is coming soon. */}\n\nThe Embedded LLM Gateway runs as part of the agentOS library, not as an external service. It intercepts and manages all LLM API calls made by agents inside the VM.\n\n- **Unified routing** for all agent LLM requests\n- **API keys stay on the server** so they are never exposed to agent code inside the VM\n- **Usage metering** with per-session and per-agent breakdowns\n- **Rate limiting** and cost controls\n\nCheck back soon for full documentation.","src/content/docs/docs/llm-gateway.mdx","cbe27275943ef64e","docs/multiplayer",{"id":199,"data":201,"body":207,"filePath":208,"digest":209,"deferredRender":16},{"title":202,"description":203,"editUrl":16,"head":204,"template":18,"sidebar":205,"pagefind":16,"draft":20},"Multiplayer","Connect multiple clients to the same agentOS actor for collaborative agent workflows.",[],{"hidden":20,"attrs":206},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nConnect multiple clients to the same agentOS actor so all subscribers receive broadcasted session output, process logs, and shell data, enabling collaborative patterns where one user prompts and others observe.\n\n## Multiple clients observing a session\n\nAll clients connected to the same actor receive broadcasted events. This enables building collaborative UIs where multiple users watch an agent work.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\n// Client A: creates the session and sends prompts\nconst clientA = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agentA = clientA.vm.getOrCreate(\"shared-agent\");\n\nconst connA = agentA.connect();\nconnA.on(\"sessionEvent\", (data) => {\n console.log(\"[A]\", data.event.method);\n});\n\nconst session = await agentA.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agentA.sendPrompt(session.sessionId, \"Build a REST API\");\n\n// Client B: observes the same session (in a separate process)\nconst clientB = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\nconst connB = clientB.vm.getOrCreate(\"shared-agent\").connect();\nconnB.on(\"sessionEvent\", (data) => {\n console.log(\"[B]\", data.event.method);\n});\n\n// Client B sees the same events as Client A\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"./software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n onSessionEvent: async (sessionId, event) => {\n // Server-side hook runs once per event, even with multiple clients\n console.log(\"Session event:\", sessionId, event.method);\n },\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/multiplayer)*","src/content/docs/docs/multiplayer.mdx","bc8f21b30025df33","docs/networking",{"id":210,"data":212,"body":218,"filePath":219,"digest":220,"deferredRender":16},{"title":213,"description":214,"editUrl":16,"head":215,"template":18,"sidebar":216,"pagefind":16,"draft":20},"Networking & Previews","Proxy HTTP requests into agentOS VMs and create shareable preview URLs.",[],{"hidden":20,"attrs":217},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nProxy HTTP requests into VM services with `vmFetch` and create time-limited, token-based preview URLs (with configurable expiration, revocation, and CORS), all carried over one transport (the kernel socket table) that is loopback-only by default under a three-layer confinement model.\n\n## Run an HTTP server in the VM\n\nGuest code runs a normal Node HTTP server: it binds a loopback port inside the VM exactly like any Node process. Write the server file and spawn it.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Write a simple Node HTTP server and run it inside the VM. It binds a loopback\n// port (3000) exactly like any normal Node process.\nawait agent.writeFile(\n \"/home/user/server.js\",\n `const http = require(\"http\");\nhttp.createServer((req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello from inside the VM\");\n}).listen(3000, () => console.log(\"listening on http://127.0.0.1:3000\"));`,\n);\nconst { pid } = await agent.spawn(\"node\", [\"/home/user/server.js\"]);\nconsole.log(\"server pid:\", pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS();\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n## Fetch from a VM service\n\nWith the HTTP server running in the VM (above), send requests to it with `vmFetch`, including custom methods, headers, and body.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Simple GET from the VM service started above.\nconst response = await agent.vmFetch(3000, \"/\");\nconsole.log(\"Status:\", response.status);\nconsole.log(\"Body:\", new TextDecoder().decode(response.body));\n\n// With a custom method, headers, and body.\nconst posted = await agent.vmFetch(3000, \"/api/data\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ key: \"value\" }),\n});\nconsole.log(\"Status:\", posted.status, posted.statusText);\nconsole.log(\"Headers:\", posted.headers);\nconsole.log(\"Body:\", new TextDecoder().decode(posted.body));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({ software: [] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n## Preview URLs\n\nPreview URLs are port forwarding for VM services: a time-limited, public URL that proxies HTTP to a port inside the VM, for browser or external access (use `vmFetch` for server-to-server). Tokens survive sleep/wake and CORS is enabled; see [Security](/docs/security-model) for details.\n\n### Create a preview URL\n\nToken lifetimes are configured under the `preview` key:\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n software: [],\n preview: {\n defaultExpiresInSeconds: 3600, // 1 hour default\n maxExpiresInSeconds: 86400, // 24 hour maximum\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Start a web app in the VM\nawait agent.spawn(\"node\", [\"/home/user/app.js\"]);\n\n// Create a preview URL (default 1 hour expiration)\nconst preview = await agent.createSignedPreviewUrl(3000);\nconsole.log(\"Preview path:\", preview.path);\nconsole.log(\"Token:\", preview.token);\nconsole.log(\"Expires at:\", new Date(preview.expiresAt));\n\n// Create a preview URL with custom expiration\nconst shortPreview = await agent.createSignedPreviewUrl(3000, 300); // 5 minutes\nconsole.log(\"Short-lived preview:\", shortPreview.path);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n### Revoke a preview URL\n\nMint short-lived preview tokens so access expires automatically; the lifetime is capped by `preview.maxExpiresInSeconds`.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Mint a short-lived preview token so access expires automatically.\nconst preview = await client.vm.getOrCreate(\"my-agent\").createSignedPreviewUrl(3000, 300); // 5 minutes\nconsole.log(\"Preview path:\", preview.path);\nconsole.log(\"Expires at:\", new Date(preview.expiresAt));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({ software: [] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n## Permissions\n\nNetwork access is governed by the VM permission policy. By default the guest cannot reach the network; grant it, or allow only specific destinations:\n\n```ts\nconst vm = agentOS({\n permissions: {\n network: {\n default: \"deny\",\n rules: [{ mode: \"allow\", operations: [\"*\"], patterns: [\"api.example.com\"] }],\n },\n },\n});\n```\n\nSee [Permissions](/docs/permissions) for the full configuration.","src/content/docs/docs/networking.mdx","75e97eca8e5c1155","docs/permissions",{"id":221,"data":223,"body":229,"filePath":230,"digest":231,"deferredRender":16},{"title":224,"description":225,"editUrl":16,"head":226,"template":18,"sidebar":227,"pagefind":16,"draft":20},"Permissions","The per-scope kernel permission policy that gates every guest syscall in the sandbox.",[],{"hidden":20,"attrs":228},{},"The sandbox permission policy is the kernel-level enforcement layer. Every guest syscall the agent's sandboxed code makes is checked against a per-scope policy before any host resource is touched.\n\n- **Six scopes**, configured independently: `fs`, `network`, `childProcess`, `process`, `env`, `binding`.\n- **Each scope** is a mode (`\"allow\"` or `\"deny\"`), or a rule set.\n- **A denied operation** is rejected with `EACCES` before any host resource is touched.\n- **Merged over a secure default**, so partial policies work.\n\nFor the higher-level agent tool-approval layer (human-in-the-loop, auto-approve), see [Approvals](/docs/approvals).\n\n## Defaults and merge semantics\n\nThe sandbox is deny-by-default for outward-facing capabilities. When you pass no policy, this baseline applies:\n\n```ts\n{\n fs: \"allow\", // virtualized in-memory filesystem only\n childProcess: \"allow\",\n process: \"allow\",\n env: \"allow\",\n network: \"deny\", // no network egress until you opt in\n}\n```\n\n- `fs`/`childProcess`/`process`/`env` are allowed because they are fully virtualized (the guest sees only the VM, never the host) and are required to run a program at all.\n- `network` is denied: guest code cannot reach the network until you opt in.\n- Your policy is merged **over** this baseline. Omitted scopes keep their default; they are **not** denied. So `{ network: \"allow\" }` grants the network while keeping the execution essentials.\n\n```ts\n// Grant the network, leave everything else at the secure default.\nconst grantNetwork = { network: \"allow\" };\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/permissions)*\n\n## Permission scopes\n\n| Scope | Controls | Default |\n|---|---|---|\n| `fs` | Filesystem reads, writes, and metadata operations | `allow` |\n| `network` | Outbound connections: `fetch`, HTTP, DNS, and inbound `listen` | `deny` |\n| `childProcess` | Spawning child processes | `allow` |\n| `process` | Process-control operations | `allow` |\n| `env` | Environment variable access | `allow` |\n| `binding` | Invoking bindings registered with the runtime | `deny`* |\n\n\\* The `binding` scope is auto-granted to `allow` when you register bindings and set no `binding` policy of your own. Pass a `binding` policy to gate individual bindings.\n\n## Bind a policy to the VM\n\nA policy is a plain object keyed by scope. Pass it as `permissions` to `agentOS(...)` and it gates every guest syscall on that VM.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n\tpermissions: {\n\t\tnetwork: \"allow\",\n\t\tfs: \"deny\",\n\t},\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/permissions)*\n\n## Grant or deny a whole scope\n\nThe simplest value for a scope is a single mode string. `\"allow\"` permits every operation in the scope; `\"deny\"` rejects every one with `EACCES`. Omitted scopes keep their secure default, so you only list what you want to change.\n\n```ts\nconst permissions = {\n\tnetwork: \"allow\", // turn on network egress\n\tfs: \"deny\", // turn off all filesystem access\n};\n```\n\nThere is no typed `\"ask\"` mode. Interactive, human-in-the-loop approval lives in the higher-level [Approvals](/docs/approvals) layer, not the kernel policy. To block at the kernel level, use `\"deny\"`.\n\n## Allow only specific filesystem paths\n\nFor finer control, a scope can be a rule set instead of a bare mode: a `default` mode plus an ordered list of `rules`. The `fs` scope matches by `paths` (filesystem globs). Each rule names its `operations` (`read`, `write`, `stat`, `readdir`, `create_dir`, `rm`, `rename`, `symlink`, `readlink`, `chmod`, `truncate`, `mount_sensitive`, or `[\"*\"]` for all). Last matching rule wins; if no rule matches, `default` applies.\n\n```ts\n// Allow the filesystem everywhere, but deny anything under /home/user/vault.\nconst denyVault = {\n\tfs: {\n\t\tdefault: \"allow\",\n\t\trules: [{ mode: \"deny\", operations: [\"*\"], paths: [\"/home/user/vault/**\"] }],\n\t},\n};\n```\n\nTo invert it, flip `default` to `\"deny\"` and allow just one subtree:\n\n```ts\n// Deny the filesystem by default, allow only reads under /home/user/data.\nconst allowOnlyData = {\n\tfs: {\n\t\tdefault: \"deny\",\n\t\trules: [{ mode: \"allow\", operations: [\"read\", \"readdir\", \"stat\"], paths: [\"/home/user/data/**\"] }],\n\t},\n};\n```\n\n## Allow only specific network hosts\n\nEvery non-`fs` scope matches by `patterns` instead of `paths`. For `network`, a pattern is a host (or `host:port`), and the operations are `fetch`, `http`, `dns`, and `listen`.\n\n```ts\n// Deny the network by default, allow only api.example.com.\nconst allowOneHost = {\n\tnetwork: {\n\t\tdefault: \"deny\",\n\t\trules: [{ mode: \"allow\", operations: [\"*\"], patterns: [\"api.example.com\"] }],\n\t},\n};\n```\n\n## Allow only specific bindings\n\nBindings registered with the runtime are gated by the `binding` scope, matched by name via `patterns`. Bindings have no sub-operations, so pass `[\"*\"]` for `operations`.\n\n```ts\n// Deny all bindings by default, allow only the \"add\" binding by name.\nconst allowOneBinding = {\n\tbinding: {\n\t\tdefault: \"deny\",\n\t\trules: [{ mode: \"allow\", operations: [\"*\"], patterns: [\"add\"] }],\n\t},\n};\n```\n\nThe `childProcess`, `process`, and `env` scopes work the same way: `childProcess` patterns match the command (`operations: [\"spawn\"]`), `env` patterns match the variable name (`operations: [\"read\", \"write\"]`), and `process` is matched by pattern with `operations: [\"*\"]`.\n\n## Combine policies and see denials\n\nEach policy above sets one scope, so you can spread several into one `permissions` object and bind them together.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n\tpermissions: {\n\t\t...denyVault,\n\t\t...allowOneHost,\n\t\t...allowOneBinding,\n\t},\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/permissions)*\n\nWhen a scope or matching rule denies an operation, the kernel rejects it with `EACCES` before any host resource is touched. For example, with `network: \"deny\"`, an outbound `fetch()` inside the guest throws:\n\n```\nEACCES: permission denied, tcp://example.com:80: blocked by network.http policy\n```","src/content/docs/docs/permissions.mdx","c9c69bed92a9757c","docs/persistence",{"id":232,"data":234,"body":240,"filePath":241,"digest":242,"deferredRender":16},{"title":235,"description":236,"editUrl":16,"head":237,"template":18,"sidebar":238,"pagefind":16,"draft":20},"Persistence & Sleep","How agentOS persists data and manages sleep/wake cycles.",[],{"hidden":20,"attrs":239},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nagentOS automatically persists the `/home/user` filesystem and session transcripts (with sequence numbers for replay) across sleep/wake, sleeping after a configurable grace period (15 minutes by default) and waking automatically when a client connects or a cron job triggers.\n\n## What persists across sleep\n\n| Data | Storage | Persists? |\n|------|---------|-----------|\n| Files in `/home/user` | Persistent filesystem | Yes |\n| Session records | SQLite (`agent_os_sessions`) | Yes |\n| Session event history | SQLite (`agent_os_session_events`) | Yes |\n| Preview URL tokens | SQLite (`agent_os_preview_tokens`) | Yes |\n| Cron job definitions | Actor state | Yes |\n| Running processes | VM kernel | No |\n| Active shells | VM kernel | No |\n| In-memory mounts | VM memory | No |\n| VM kernel state | VM memory | No |\n\n## What prevents sleep\n\nThe actor stays awake as long as any of these are active:\n\n- **Active sessions** (created but not closed/destroyed)\n- **Running processes** (spawned but not exited)\n- **Active shells** (opened but not closed)\n- **Pending hooks** (server-side callbacks still executing)\n\nWhen all activity stops, the sleep grace period begins.\n\n## Sleep grace period\n\nAfter all activity stops, the actor waits 15 minutes before sleeping. This allows for brief pauses between interactions without restarting the VM.\n\n```\nActivity stops ──> 15 min grace period ──> Actor sleeps\n (VM shutdown, processes killed)\n\nNew client connects ──> Actor wakes ──> VM boots ──> Filesystem restored\n```\n\n## Timeouts\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| Action timeout | 15 minutes | Maximum time for any single action |\n| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |\n\nThese are set internally by the `agentOS()` factory and cannot be overridden per-call.\n\n## Sleep vs destroy\n\n| | Sleep | Destroy |\n|-|-------|---------|\n| Filesystem | Preserved | Deleted |\n| Session records | Preserved | Deleted |\n| Event history | Preserved | Deleted |\n| Preview tokens | Preserved | Deleted |\n| VM state | Lost | Lost |\n| Processes | Killed | Killed |\n\n## VM boot and shutdown events\n\nSubscribe to `vmBooted` and `vmShutdown` events to track VM lifecycle.\n\n\u003CCodeGroup>\n```ts title=\"lifecycle-client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\nconst conn = client.vm.getOrCreate(\"my-agent\").connect();\n\nconn.on(\"vmBooted\", () => {\n console.log(\"VM is ready\");\n});\n\nconn.on(\"vmShutdown\", (payload) => {\n console.log(\"VM shutdown reason:\", payload.reason);\n // reason: \"sleep\" | \"destroy\" | \"error\"\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n// import pi from \"@agentos-software/pi\";\nimport pi from \"./software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/persistence)*\n\n## Resuming after sleep\n\nWhen the actor wakes up, the VM boots and the filesystem is restored from SQLite, session records and event history are immediately available, and processes and shells from the previous session are gone. Clients can reconnect, list prior work with `listPersistedSessions` (which works without a running VM), and replay a session's persisted transcript with `getSessionEvents`.\n\n\u003CCodeGroup>\n```ts title=\"resume-client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst vm = client.vm.getOrCreate(\"my-agent\");\n\n// List sessions persisted before sleep (works without a running VM)\nconst sessions = await vm.listPersistedSessions();\nconsole.log(\"Previous sessions:\", sessions.length);\n\n// Replay the most recent session's transcript from durable storage\nconst last = sessions[0];\nif (last) {\n const events = await vm.getSessionEvents(last.sessionId);\n for (const e of events) {\n console.log(e.seq, e.event.method);\n }\n}\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/persistence)* (uses the same `server.ts` as above)\n\n## Persisted tables schema\n\n### `agent_os_fs_entries`\n\nStores the virtual filesystem.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `path` | TEXT PRIMARY KEY | File or directory path |\n| `is_directory` | INTEGER | 1 for directory, 0 for file |\n| `content` | BLOB | File content |\n| `mode` | INTEGER | POSIX mode bits |\n| `size` | INTEGER | File size in bytes |\n| `atime_ms` | INTEGER | Access time (ms) |\n| `mtime_ms` | INTEGER | Modification time (ms) |\n| `ctime_ms` | INTEGER | Change time (ms) |\n| `birthtime_ms` | INTEGER | Birth time (ms) |\n\n### `agent_os_sessions`\n\nStores session metadata.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `session_id` | TEXT PRIMARY KEY | Unique session identifier |\n| `agent_type` | TEXT | Agent type (e.g. \"pi\") |\n| `capabilities` | TEXT (JSON) | Agent capabilities |\n| `agent_info` | TEXT (JSON) | Agent metadata |\n| `created_at` | INTEGER | Creation timestamp (ms) |\n\n### `agent_os_session_events`\n\nStores session event history.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `id` | INTEGER PRIMARY KEY | Auto-incrementing ID |\n| `session_id` | TEXT | Session reference |\n| `seq` | INTEGER | Sequence number within session |\n| `event` | TEXT (JSON) | JSON-RPC notification |\n| `created_at` | INTEGER | Timestamp (ms) |","src/content/docs/docs/persistence.mdx","3afd6248c2d09a03","docs/processes",{"id":243,"data":245,"body":251,"filePath":252,"digest":253,"deferredRender":16},{"title":246,"description":247,"editUrl":16,"head":248,"template":18,"sidebar":249,"pagefind":16,"draft":20},"Processes & Shell","Execute commands, spawn long-running processes, and open interactive shells in agentOS VMs.",[],{"hidden":20,"attrs":250},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nRun commands with one-shot `exec`, spawn long-running processes with streaming stdout/stderr and stdin, manage their lifecycle (stop, kill, wait, inspect), open interactive PTY-backed shells, and inspect the process tree across all VM runtimes.\n\n## One-shot execution\n\nUse `exec` to run a command and wait for completion. Returns stdout, stderr, and exit code.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\nconst result = await client.vm.getOrCreate(\"my-agent\").exec(\"echo hello && ls /home/user\");\nconsole.log(\"stdout:\", result.stdout);\nconsole.log(\"stderr:\", result.stderr);\nconsole.log(\"exit code:\", result.exitCode);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Spawn a long-running process\n\nUse `spawn` for processes that run in the background. Output is streamed via `processOutput` and `processExit` events.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\nconst conn = agent.connect();\n\n// Subscribe to process output\nconn.on(\"processOutput\", (data) => {\n const text = new TextDecoder().decode(data.data);\n console.log(`[pid ${data.pid}] ${data.stream}: ${text}`);\n});\n\nconn.on(\"processExit\", (data) => {\n console.log(`[pid ${data.pid}] exited with code ${data.exitCode}`);\n});\n\n// Spawn a dev server\nconst { pid } = await agent.spawn(\"node\", [\"/home/user/server.js\"]);\nconsole.log(\"Started process:\", pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Write to stdin\n\nSend input to a running process.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst { pid } = await agent.spawn(\"cat\", []);\n\n// Write to stdin\nawait agent.writeProcessStdin(pid, \"hello from stdin\\n\");\n\n// Close stdin when done\nawait agent.closeProcessStdin(pid);\n\n// Wait for the process to exit\nconst exitCode = await agent.waitProcess(pid);\nconsole.log(\"exit code:\", exitCode);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Process lifecycle\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst { pid } = await agent.spawn(\"node\", [\"/home/user/server.js\"]);\n\n// List all processes tracked by the VM\nconst processes = await agent.listProcesses();\nfor (const p of processes) {\n console.log(p.pid, p.command, p.args.join(\" \"), p.running ? \"running\" : \"exited\");\n}\n\n// Inspect a specific process by pid\nconst info = await agent.getProcess(pid);\nconsole.log(info.running, info.exitCode);\n\n// Graceful stop (SIGTERM)\nawait agent.stopProcess(pid);\n\n// Force kill (SIGKILL)\nawait agent.killProcess(pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Interactive shells\n\nOpen an interactive shell with PTY support. Shell data is streamed via `shellData` events.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\nconst conn = agent.connect();\n\n// Subscribe to shell output\nconn.on(\"shellData\", (data) => {\n const text = new TextDecoder().decode(data.data);\n process.stdout.write(text);\n});\n\n// Open a shell\nconst { shellId } = await agent.openShell();\n\n// Write commands to the shell\nawait agent.writeShell(shellId, \"ls -la /home/user\\n\");\n\n// Resize the terminal\nawait agent.resizeShell(shellId, 120, 40);\n\n// Close the shell when done\nawait agent.closeShell(shellId);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*","src/content/docs/docs/processes.mdx","dfaf0eb9115e79aa","docs/quickstart",{"id":254,"data":256,"body":262,"filePath":263,"digest":264,"deferredRender":16},{"title":257,"description":258,"editUrl":16,"head":259,"template":18,"sidebar":260,"pagefind":16,"draft":20},"Quickstart","Set up an agentOS actor, create a session, and run your first coding agent.",[],{"hidden":20,"attrs":261},{},"import { Aside, Steps } from '@astrojs/starlight/components';\nimport CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\n\u003Cdiv style=\"border-radius:0.75rem;border:1px solid rgba(27,25,22,0.12);background:rgba(27,25,22,0.035);padding:0.875rem 1.125rem;margin:1.5rem 0;color:#56524a;display:flex;align-items:center;justify-content:space-between;gap:1.25rem;\">\n\u003Cspan>Use this pre-built prompt to get started faster.\u003C/span>\n\u003Cbutton type=\"button\" onclick=\"var b=this;navigator.clipboard.writeText(b.getAttribute('data-prompt')||'').then(function(){b.textContent='Copied!';setTimeout(function(){b.textContent='Copy prompt';},1500);});\" data-prompt=\"Set up a new agentOS project from scratch. agentOS runs each AI coding agent inside its own isolated VM. Do the following: 1. Initialize a Node + TypeScript project and install the dependencies: `npm install @rivet-dev/agentos @agentos-software/pi`. 2. Create `server.ts` that defines a VM actor with the Pi coding agent installed and starts the registry: import { agentOS, setup } from "@rivet-dev/agentos"; import pi from "@agentos-software/pi"; const vm = agentOS({ software: [pi] }); export const registry = setup({ use: { vm } }); registry.start(); 3. Create `client.ts` that connects to the server, gets or creates a VM, opens a Pi session with the ANTHROPIC_API_KEY from the environment, sends a prompt, and reads back the file the agent wrote: 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 session = await handle.createSession("pi", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! } }); await handle.sendPrompt(session.sessionId, "Write a hello world script to /home/user/hello.js"); const content = await handle.readFile("/home/user/hello.js"); console.log(new TextDecoder().decode(content)); 4. Explain how to run both: `npx tsx server.ts` in one terminal, then `npx tsx client.ts` in another, with ANTHROPIC_API_KEY set in the environment.\" style=\"appearance:none;border:1px solid rgba(27,25,22,0.18);background:#1b1916;color:#f4f1e7;font-family:var(--sl-font);font-size:0.8rem;font-weight:600;display:inline-flex;align-items:center;justify-content:center;height:2rem;padding:0 0.85rem;border-radius:6px;cursor:pointer;white-space:nowrap;margin-top:0;flex:none;box-sizing:border-box;\">Copy prompt\u003C/button>\n\u003C/div>\n\n\u003Cdiv style=\"border-radius:0.75rem;border:1px solid rgba(27,25,22,0.12);background:rgba(27,25,22,0.035);padding:0.875rem 1.125rem;margin:1.5rem 0;color:#56524a;display:flex;align-items:center;justify-content:space-between;gap:1.25rem;\">\n\u003Cspan>Prefer to read code? Clone the example repository.\u003C/span>\n\u003Ca href=\"https://github.com/rivet-dev/agent-os/tree/main/examples/docs/quickstart\" style=\"appearance:none;border:1px solid rgba(27,25,22,0.18);background:transparent;color:#1b1916;font-family:var(--sl-font);font-size:0.8rem;font-weight:600;display:inline-flex;align-items:center;justify-content:center;height:2rem;padding:0 0.85rem;border-radius:6px;cursor:pointer;white-space:nowrap;text-decoration:none;flex:none;gap:0.45rem;box-sizing:border-box;\">\u003Csvg viewBox=\"0 0 496 512\" width=\"14\" height=\"14\" fill=\"currentColor\" aria-hidden=\"true\">\u003Cpath d=\"M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z\"/>\u003C/svg>View on GitHub\u003C/a>\n\u003C/div>\n\n\u003Csvg viewBox=\"0 0 400 210\" role=\"img\" aria-label=\"A client (JavaScript, browser, or another backend) connects to a server that runs each agent in its own isolated VM, marked with the agentOS 'OS' logo.\" style=\"width:100%;height:auto;max-width:420px;display:block;margin:3rem auto 2.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"qs-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003Csymbol id=\"qs-os\" viewBox=\"0 0 100 100\">\n \u003Crect x=\"8\" y=\"8\" width=\"84\" height=\"84\" rx=\"26\" fill=\"none\" stroke=\"#1b1916\" stroke-width=\"8\" />\n \u003Ctext x=\"50\" y=\"50\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"38\" fill=\"#1b1916\">OS\u003C/text>\n \u003C/symbol>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"67\" width=\"140\" height=\"60\" rx=\"12\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"82\" y=\"92\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"15\" font-weight=\"600\" fill=\"#1b1916\">Client\u003C/text>\n \u003Ctext x=\"82\" y=\"112\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">JS · Browser · Backend\u003C/text>\n \u003Cline x1=\"154\" y1=\"97\" x2=\"205\" y2=\"97\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#qs-arrow)\" />\n \u003Crect x=\"210\" y=\"40\" width=\"164\" height=\"114\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"224\" y=\"62\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Server\u003C/text>\n \u003Cg fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\">\n \u003Crect x=\"224\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"224\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003C/g>\n \u003Cg>\n \u003Cuse href=\"#qs-os\" x=\"229\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"265\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"301\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"337\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"229\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"265\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"301\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"337\" y=\"117\" width=\"18\" height=\"18\" />\n \u003C/g>\n \u003Cg>\n \u003Crect x=\"146\" y=\"170\" width=\"15\" height=\"15\" rx=\"4\" fill=\"none\" stroke=\"#56524a\" stroke-width=\"1.4\" />\n \u003Ctext x=\"153.5\" y=\"178\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"7\" fill=\"#56524a\">OS\u003C/text>\n \u003Ctext x=\"170\" y=\"178\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-size=\"12\" fill=\"#56524a\">= agentOS VM\u003C/text>\n \u003C/g>\n\u003C/svg>\n\n\u003CSteps>\n\n1. **Install**\n\n - **@rivet-dev/agentos** — Actor framework with built-in persistence and orchestration\n - **@agentos-software/pi** — [Pi](https://github.com/mariozechner/pi-coding-agent) coding agent (Claude Code, Amp, and OpenCode coming soon)\n\n ```bash\n npm install @rivet-dev/agentos @agentos-software/pi\n ```\n\n2. **Create the server**\n\n ```ts title=\"server.ts\"\n import { agentOS, setup } from \"@rivet-dev/agentos\";\n import pi from \"@agentos-software/pi\";\n\n const vm = agentOS({\n software: [pi],\n });\n\n export const registry = setup({ use: { vm } });\n registry.start();\n ```\n\n3. **Create the client**\n\n The client can be any public frontend or another backend. The same `vm` actor is reachable from a plain Node script, a browser/React app, or a separate server.\n\n \u003CCodeGroup>\n ```ts title=\"TypeScript\"\n import { createClient } from \"@rivet-dev/agentos/client\";\n import type { registry } from \"./server\";\n\n const client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n const handle = client.vm.getOrCreate(\"my-agent\");\n\n // Subscribe to streaming events. The payload is inferred from the event schema.\n const conn = handle.connect();\n conn.on(\"sessionEvent\", (data) => {\n console.log(data.event);\n });\n\n // Create a session and send a prompt\n const session = await handle.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await handle.sendPrompt(\n session.sessionId,\n \"Write a hello world script to /home/user/hello.js\",\n );\n\n // Read the file the agent created\n const content = await handle.readFile(\"/home/user/hello.js\");\n console.log(new TextDecoder().decode(content));\n ```\n\n ```tsx title=\"React\"\n import { createRivetKit } from \"@rivet-dev/agentos/react\";\n import { useState } from \"react\";\n import type { registry } from \"./server\";\n\n const { useActor } = createRivetKit\u003Ctypeof registry>(\"http://localhost:6420\");\n\n export function Agent() {\n const [log, setLog] = useState(\"\");\n const agent = useActor({ name: \"vm\", key: \"my-agent\" });\n\n // Stream agent events into component state\n agent.useEvent(\"sessionEvent\", (data) => {\n setLog((prev) => prev + JSON.stringify(data.event) + \"\\n\");\n });\n\n async function run() {\n // In production, inject credentials on the server (see /docs/llm-credentials)\n const session = await agent.connection?.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.VITE_ANTHROPIC_API_KEY! },\n });\n if (!session) return;\n await agent.connection?.sendPrompt(\n session.sessionId,\n \"Write a hello world script to /home/user/hello.js\",\n );\n }\n\n return (\n \u003Cdiv>\n \u003Cbutton onClick={run}>Run agent\u003C/button>\n \u003Cpre>{log}\u003C/pre>\n \u003C/div>\n );\n }\n ```\n \u003C/CodeGroup>\n\n4. **Run it**\n\n Start the server, then run the client in a second terminal:\n\n ```bash\n # Terminal 1: start the server\n npx tsx server.ts\n\n # Terminal 2: run the client\n npx tsx client.ts\n ```\n\n5. **Customize**\n\n Now that you have a working agent, customize it to fit your needs:\n\n - **[Software](/docs/software)** — Install software packages inside the VM\n - **[Filesystem](/docs/filesystem)** — Read, write, and manage files inside the VM\n - **[Permissions & Resource Limits](/docs/permissions)** — Gate what the agent can do and cap its resource usage\n - **[Bindings](/docs/bindings)** — Expose your JavaScript functions to agents as CLI commands\n\n\u003C/Steps>\n\n\u003CAside type=\"note\">\nagentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).\n\u003C/Aside>\n\n\n## agentOS Core\n\nThe quickstart above uses `@rivet-dev/agentos`, which includes statefulness, multiplayer, and orchestration out of the box. If you only need direct VM control without those features, you can use the core package (`@rivet-dev/agentos-core`) standalone.\n\nSee [agentOS core documentation](/docs/core) for reference.","src/content/docs/docs/quickstart.mdx","b321471edd855301","docs/sandbox",{"id":265,"data":267,"body":273,"filePath":274,"digest":275,"deferredRender":16},{"title":268,"description":269,"editUrl":16,"head":270,"template":18,"sidebar":271,"pagefind":16,"draft":20},"Sandbox Mounting","Extend agentOS with full sandboxes for heavy workloads like browsers, desktop automation, and compilation.",[],{"hidden":20,"attrs":272},{},"For heavy workloads like browsers, desktop automation, and compilation, pair agentOS with a full sandbox on demand. Its filesystem mounts into the VM as a native directory, and its process management is exposed as [bindings](/docs/bindings), all provider-agnostic through [Sandbox Agent](https://sandboxagent.dev).\n\n## Why use agentOS with a sandbox?\n\nagentOS is an alternative to sandboxes that covers most use cases, but some workloads need a full sandbox for special kinds of software (browsers, desktop automation, heavy compilation). Sandbox mounting lets you lazily start a sandbox on demand, only when it is needed, and project it into the VM. The hybrid model means one agent session can handle both lightweight coding tasks and heavy system operations, using the right tool for each.\n\nSee [agentOS vs Sandbox](/docs/versus-sandbox) for a detailed comparison.\n\n## When to use a sandbox\n\n- **Native binaries** not yet supported in the agentOS runtime.\n- **Browsers and desktop automation**: Playwright, Puppeteer, Selenium, or anything that needs a display server.\n- **Heavy compilation**: Large builds or native toolchains that require a full Linux environment.\n- **GUI applications**: Desktop apps, VNC sessions, or any workload that needs a graphical environment.\n- **Node.js packages with native extensions** (e.g. `sharp`, `bcrypt`, `better-sqlite3`) that require a full build toolchain.\n\nStart with the default agentOS VM for all workloads, and only spin up a sandbox when a task genuinely requires one. Sandboxes are billed per second of uptime, so start them on demand and tear them down when the task is done.\n\n## Getting started\n\nThe sandbox integration ships as the `@rivet-dev/agentos-sandbox` package. It works through two mechanisms:\n\n- **Filesystem mount**: Projects the sandbox into the VM as a native directory, like mounting a hard drive on your own machine. Read and write files through the mount directly.\n- **Bindings**: Exposes sandbox process management as [bindings](/docs/bindings). Execute commands on the sandbox from within the VM.\n\nBoth are powered by [Sandbox Agent](https://sandboxagent.dev), and you can swap providers without changing agent code. Install both packages:\n\n```bash\nnpm install @rivet-dev/agentos-sandbox sandbox-agent\n```\n\n`createSandboxFs` and `createSandboxBindings` come from `@rivet-dev/agentos-sandbox`. `SandboxAgent` and the provider helpers (such as `docker`) come from the `sandbox-agent` package.\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createSandboxFs, createSandboxBindings } from \"@rivet-dev/agentos-sandbox\";\nimport { SandboxAgent } from \"sandbox-agent\";\nimport { docker } from \"sandbox-agent/docker\";\n\n// Start a sandbox through Sandbox Agent. Any provider works; Docker is used here.\nconst sandbox = await SandboxAgent.start({ sandbox: docker() });\n\n// `createSandboxFs` returns a mount plugin descriptor that projects the sandbox\n// filesystem into the VM, and `createSandboxBindings` exposes the sandbox's\n// process management as bindings.\nconst vm = agentOS({\n\t// Bindings let the agent control the sandbox\n\tbindings: [createSandboxBindings({ client: sandbox })],\n\t// Mounts let the agent read the sandbox filesystem (optional)\n\tmounts: [\n\t\t{ path: \"/home/user/sandbox\", plugin: createSandboxFs({ client: sandbox }) },\n\t],\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sandbox)*\n\n## Calling the mounted bindings\n\nOnce the sandbox is mounted, write code through the filesystem and run it inside the sandbox. The sandbox bindings are exposed inside the VM as a CLI command, so you call it through the same `exec`/`spawn` surface as any other command.\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst vm = client.vm.getOrCreate(\"my-agent\");\n\n// Write code via the filesystem. The /home/user/sandbox mount maps to the sandbox root.\nawait vm.writeFile(\"/home/user/sandbox/app/index.ts\", 'console.log(\"hello\")');\n\n// Run it inside the sandbox. Commands execute through the VM's process table,\n// reading the file from the mounted directory.\nconst result = await vm.exec(\"node /home/user/sandbox/app/index.ts\");\nconsole.log(result.stdout); // \"hello\\n\"\n\n// Call a mounted binding from the client. The sandbox bindings are exposed inside the\n// VM as a CLI command, so you invoke it through the same exec/spawn surface.\nconst install = await vm.exec(\"agentos-sandbox run-command --command \\\"npm install\\\" --cwd /home/user/sandbox/app\");\nconsole.log(install.exitCode, install.stdout);\n\n// Spawn a long-running process via the bindings and stream its output.\nconst { pid } = await vm.spawn(\"agentos-sandbox\", [\n\t\"create-process\",\n\t\"--command\",\n\t\"npm\",\n\t\"--args\",\n\t\"run\",\n\t\"--args\",\n\t\"dev\",\n]);\nconst conn = vm.connect();\nconn.on(\"processOutput\", (payload) => {\n\tif (payload.pid === pid) {\n\t\tconsole.log(payload.stream, new TextDecoder().decode(payload.data));\n\t}\n});\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sandbox)*\n\n## Bindings reference\n\nThe bindings expose these commands inside the VM:\n\n```bash\n# Run a command synchronously\nagentos-sandbox run-command --command \"npm install\" --cwd \"/app\"\n\n# Start a background process\nagentos-sandbox create-process --command \"npm\" --args \"run\" --args \"dev\"\n\n# List running processes\nagentos-sandbox list-processes\n\n# Get process output\nagentos-sandbox get-process-logs --id \"proc_abc123\"\n\n# Stop or kill a process\nagentos-sandbox stop-process --id \"proc_abc123\"\nagentos-sandbox kill-process --id \"proc_abc123\"\n\n# Send input to an interactive process\nagentos-sandbox send-input --id \"proc_abc123\" --data \"yes\"\n```\n\n## Sandbox providers\n\nThe extension works with any [Sandbox Agent](https://sandboxagent.dev) provider. See the [Sandbox Agent documentation](https://sandboxagent.dev) for available providers and setup instructions.","src/content/docs/docs/sandbox.mdx","7f580a2682a36f6c","docs/security-model",{"id":276,"data":278,"body":284,"filePath":285,"digest":286,"deferredRender":16},{"title":279,"description":280,"editUrl":16,"head":281,"template":18,"sidebar":282,"pagefind":16,"draft":20},"Security Model","Trust boundaries, isolation guarantees, and the agentOS threat model.",[],{"hidden":20,"attrs":283},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"caution\">\nagentOS is in beta and still undergoing security review. The security model described here is subject to change.\n\u003C/Aside>\n\nagentOS is a sandbox: it runs **untrusted code safely on behalf of a trusted caller**. Every actor boots its own fully virtualized VM with a virtual filesystem, process table, socket table, pipes, PTYs, a permission policy, and managed language runtimes. Guest JavaScript executes in a V8 isolate, and every guest syscall is serviced by the kernel rather than the host. There are no host escapes: guest code cannot spawn a real host process, touch the real host filesystem, or open a real host network socket.\n\n## Deny by default\n\nNo syscalls are bound to the system by default. Everything is denied until explicitly opted in.\n\n- **Network access** is denied until you opt in with a `network` permission.\n- **Filesystem mounts** expose nothing of the host until you configure them.\n- **Process spawning** runs only kernel-managed guest processes, never host processes.\n- **All other host capabilities** must be configured by the host before the VM can use them.\n\nOther in-VM scopes (the virtual filesystem, child processes, process info, env) are enabled so that normal programs run, but they are mediated entirely by the kernel and never touch the host.\n\n## Trust model: three components\n\nBefore judging whether something is a security bug, decide which side of the boundary it is on. agentOS has three components with very different trust levels.\n\n\u003Csvg width=\"700\" height=\"270\" viewBox=\"0 0 700 270\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"Three-component trust model: client, sidecar, executor\">\n \u003Crect x=\"20\" y=\"40\" width=\"180\" height=\"190\" rx=\"10\" fill=\"#f4f6f8\" stroke=\"#c8d0d8\" stroke-width=\"1.5\" />\n \u003Ctext x=\"110\" y=\"68\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#111827\">Client\u003C/text>\n \u003Ctext x=\"110\" y=\"90\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"12\" fill=\"#374151\">(trusted)\u003C/text>\n \u003Ctext x=\"110\" y=\"120\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Your host app\u003C/text>\n \u003Ctext x=\"110\" y=\"138\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Configures the VM\u003C/text>\n \u003Ctext x=\"110\" y=\"170\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">Untrusted: only the\u003C/text>\n \u003Ctext x=\"110\" y=\"186\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">code it submits\u003C/text>\n \u003Crect x=\"260\" y=\"40\" width=\"180\" height=\"190\" rx=\"10\" fill=\"#eef2f6\" stroke=\"#c8d0d8\" stroke-width=\"1.5\" />\n \u003Ctext x=\"350\" y=\"68\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#111827\">Sidecar / Kernel\u003C/text>\n \u003Ctext x=\"350\" y=\"90\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"12\" fill=\"#374151\">(trusted = TCB)\u003C/text>\n \u003Ctext x=\"350\" y=\"120\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Owns VFS, processes,\u003C/text>\n \u003Ctext x=\"350\" y=\"138\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">sockets, policy\u003C/text>\n \u003Ctext x=\"350\" y=\"170\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Enforces the\u003C/text>\n \u003Ctext x=\"350\" y=\"186\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">boundary\u003C/text>\n \u003Crect x=\"500\" y=\"40\" width=\"180\" height=\"190\" rx=\"10\" fill=\"#fdf2f2\" stroke=\"#e6b8b8\" stroke-width=\"1.5\" />\n \u003Ctext x=\"590\" y=\"68\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#111827\">Executor\u003C/text>\n \u003Ctext x=\"590\" y=\"90\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"12\" fill=\"#9a3412\">(untrusted = adversary)\u003C/text>\n \u003Ctext x=\"590\" y=\"120\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">V8 isolate / WASM\u003C/text>\n \u003Ctext x=\"590\" y=\"138\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Runs guest code\u003C/text>\n \u003Ctext x=\"590\" y=\"170\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">Assume actively\u003C/text>\n \u003Ctext x=\"590\" y=\"186\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">hostile\u003C/text>\n \u003Cline x1=\"200\" y1=\"135\" x2=\"258\" y2=\"135\" stroke=\"#6b7280\" stroke-width=\"1.5\" marker-end=\"url(#arrow)\" />\n \u003Cline x1=\"440\" y1=\"135\" x2=\"498\" y2=\"135\" stroke=\"#b91c1c\" stroke-width=\"1.5\" stroke-dasharray=\"4 3\" marker-end=\"url(#arrowred)\" />\n \u003Ctext x=\"229\" y=\"128\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"9\" fill=\"#6b7280\">wire\u003C/text>\n \u003Ctext x=\"469\" y=\"128\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"9\" fill=\"#b91c1c\">syscalls\u003C/text>\n \u003Ctext x=\"469\" y=\"252\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" font-weight=\"700\" fill=\"#b91c1c\">SECURITY BOUNDARY\u003C/text>\n \u003Cdefs>\n \u003Cmarker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"6\" refY=\"3\" orient=\"auto\">\u003Cpath d=\"M0,0 L6,3 L0,6 Z\" fill=\"#6b7280\" />\u003C/marker>\n \u003Cmarker id=\"arrowred\" markerWidth=\"8\" markerHeight=\"8\" refX=\"6\" refY=\"3\" orient=\"auto\">\u003Cpath d=\"M0,0 L6,3 L0,6 Z\" fill=\"#b91c1c\" />\u003C/marker>\n \u003C/defs>\n\u003C/svg>\n\n### Client (trusted)\n\nThe party that configures and manages the VM: your application code, container, or serverless function.\n\n- The client process and **every value it sends** are trusted: VM config, mount descriptors and their plugin configs (host directory paths, S3 endpoints and credentials, etc.), the permission policy, network allowlist, resource limits, env, and DNS overrides.\n- **Configuration is not an attack surface.** A defect that requires the client to supply a malicious config, endpoint, credential, or policy is not a sandbox vulnerability: the client is configuring its own VM and already controls the host.\n- The **one** thing from the client that is *not* trusted is the **code/payload** it asks to run, because that runs in the executor. How code reached the executor never makes it trusted.\n\nYou are responsible for hardening this side. See [What you are responsible for](#what-you-are-responsible-for).\n\n### Sidecar / kernel (trusted, the enforcement point)\n\nThe trusted computing base. It brokers client requests and owns the kernel, VFS, mount/plugin registry, socket table, and permission policy. It is responsible for enforcing the boundary against the executor.\n\n### Executor (untrusted, the adversary)\n\nV8 isolates or WASM running guest JS/Python/WASM plus any third-party, npm, or agent-generated code.\n\n- Assume everything here is **actively hostile**.\n- The executor reaches the outside world only through kernel-owned VFS, process, socket, pipe, PTY, permission, and DNS paths.\n\n## The security boundary\n\n**The security boundary is sidecar ↔ executor.** The runtime must stop guest code in the executor from:\n\n- Escaping the kernel boundary (the real host filesystem, network, process table, or memory).\n- Bypassing the **applied** permission policy, allowlist, or limits.\n- Exhausting host resources beyond configured bounds.\n- Reading another VM's state.\n\nTwo corollaries that are easy to get wrong:\n\n- **Trusted policy, untrusted subject.** The permission policy and limits are trusted input, but the guest executor is the subject they bind. \"Guest bypasses an applied permission, egress rule, or resource cap\" is in-scope and serious. Trusted = who sets the rule; untrusted = who is bound by it.\n- **Trusted mount, untrusted traffic.** A host-backed mount (host directory, S3, etc.) comes from trusted config, so its existence, target, and credentials are not attack surface. But the guest drives I/O through it, so confining those guest operations to the mount root (symlink, `..`, TOCTOU, and path-aliasing escapes) is in-scope.\n\n### In scope vs out of scope\n\n| In scope (sandbox escape) | Out of scope (not a sandbox bug) |\n| --- | --- |\n| Guest reaches the real host fs / net / process / memory | Client supplies a malicious config / endpoint / credential / policy |\n| Guest bypasses an applied permission, egress rule, or limit | Hardening that only guards trusted client-provided configuration |\n| Guest exhausts host resources past configured bounds | Wire-level authn/authz between mutually distrusting clients |\n| Guest reads another VM's state | VM-to-VM access via forged connection IDs (single-client transport) |\n| Guest escapes a host-backed mount root (symlink / `..` / TOCTOU) | The existence or target of a configured mount |\n\n**Transport scope.** The wire protocol is same-version lockstep and single-client over stdio (one trusted client per sidecar process). There is no second, mutually-distrusting client, so wire-level authn/authz between clients and VM-to-VM access via forged connection IDs are out of scope until a multi-client transport exists.\n\n## VM isolation\n\nEach agentOS actor runs in its own isolated VM.\n\n- **Sandboxed execution.** All agent code runs inside a V8 isolate with WebAssembly. No code escapes the isolate boundary.\n- **Virtual filesystem.** The VM has its own in-memory filesystem. Guest reads and writes never reach the real host filesystem. Agents cannot access host files unless explicitly mounted.\n- **Virtual network.** The VM has no direct access to the host network. Outbound requests are proxied through the host with configurable controls.\n- **Process isolation.** No host process is visible or accessible from inside the VM.\n- **Per-actor containment.** Each actor is its own VM. Two actors share no filesystem, globals, module state, memory, or crash fate. The sidecar process that hosts those VMs may be shared by default as a performance optimization, but isolation is enforced at the VM level, not the host-process level.\n\n### Kernel-owned syscall paths\n\nEvery guest syscall is mediated by the kernel and checked against the runtime's permission policy. Concretely, the kernel mediates:\n\n- **Filesystem.** A virtual, in-memory filesystem. Guest reads and writes never reach the real host filesystem. Host data enters the VM only through the `files`, `mounts`, or `nodeModules` you configure explicitly. See [Filesystem](/docs/filesystem).\n- **Processes.** `node:child_process` spawns kernel-managed guest processes, never real host processes. Children can only run the commands the VM mounts (WASM-backed `sh` and coreutils, V8-backed `node`). See [Processes](/docs/processes).\n- **Network.** Guest `fetch()`, `node:http`, and raw sockets all flow through the kernel socket table. Guest `fetch()` runs through undici inside the isolate and then through the kernel socket table; it never opens a real host socket. See [Networking](/docs/networking).\n- **DNS, pipes, and PTYs** are likewise kernel-owned: no guest path reaches the host directly.\n- **Bindings.** Registered [bindings](/docs/bindings) are the only sanctioned way to hand the guest a named host capability. The guest invokes a binding by name with JSON input, the call round-trips to the host handler, and only the handler's return value comes back. The guest never receives the underlying host access.\n\n## What enters the VM\n\nThe host filesystem is never exposed to the guest by default. Host data crosses the boundary only through options you configure:\n\n- **`files`** seed bytes into the virtual filesystem. The bytes are copied in; the host path is never exposed.\n- **`mounts`** project a host directory at a guest path, Docker-style. The guest sees only the mounted subtree, read through the VFS lazily, never the wider host filesystem. Mounts are read-only unless you opt out.\n- **`nodeModules`** project a host `node_modules` directory (read-only, lazily) at a guest path so guest `import`/`require` resolves real installed packages.\n\nIn every case the guest sees only the subtree you mount, and writes to read-only mounts are rejected.\n\n## Permissions\n\nPermissions are the capability gate at the boundary. They merge over a secure default that denies the network and enables the filesystem, child processes, process info, and env. Because the merge is partial, you name only the scope you change.\n\n```ts\n// Grant network egress; everything else keeps the secure defaults.\npermissions: { network: \"allow\" }\n```\n\nA scope can be `\"allow\"`, `\"deny\"`, or a `{ default, rules }` policy that matches request patterns. Guest servers are reachable only over loopback inside the VM unless you exempt a port explicitly. See [Permissions](/docs/permissions) and [Networking](/docs/networking) for the full policy shape.\n\n## Resource and timing limits\n\nThe VM bounds guest execution so runaway or hostile code cannot hang or exhaust the host:\n\n- **Timeouts and cancellation** kill or cancel a run from the outside.\n- **Memory, CPU-time, and payload limits** are enforced by the VM.\n- **Timing-side-channel mitigation.** In the default mode, high-resolution clocks (`Date.now()`, `performance.now()`, `process.hrtime()`) are frozen within a run and `SharedArrayBuffer` is removed, to blunt timing side channels of the kind used in Spectre-style attacks.\n\nSee [Security & Auth](/docs/security-model) for resource limits, network control, and authentication setup.\n\n## What agentOS guarantees\n\n- Agent code cannot read or write host files outside configured mounts.\n- Agent code cannot make network requests except through the host proxy.\n- Agent code cannot access host environment variables or secrets.\n- Each actor's filesystem, sessions, and state are isolated from other actors.\n- Resource limits (CPU, memory) are enforced at the VM level.\n- A crash, resource exhaustion, or escape attempt is contained to a single VM; other VMs keep running, even when they share a sidecar process.\n\n## What you are responsible for\n\nThe boundary protects the host from the guest. It does **not** harden your host process against everything else. The VM alone is not enough without a hardened host, and a hardened host alone does not protect against code that runs with full host access inside your own process.\n\n- Hardening the host process and deployment environment. For internet-facing workloads that take untrusted input, run your host inside an already-hardened environment (for example AWS Lambda, Google Cloud Run, or a similar sandboxed platform).\n- Validating authentication tokens in `onBeforeConnect`.\n- Scoping [permissions](/docs/permissions) appropriately for your use case.\n- Managing API keys and secrets on the host side (use the [LLM gateway](/docs/llm-gateway) to avoid passing keys into the VM).\n- Configuring [resource limits and network controls](/docs/security-model) to match your threat model.\n- Choosing your blast radius: prefer a fresh VM per untrusted or high-risk task so an escape attempt cannot outlive a single VM.\n\n\u003CAside type=\"caution\">The boundary contains guest code, but you still own the host. Treat the host process as trusted infrastructure and harden it.\u003C/Aside>\n\n## Further reading\n\n- [Security configuration](/docs/security-model) for resource limits, network control, and authentication setup\n- [Permissions](/docs/permissions) for agent tool-use approval patterns\n- [agentOS vs Sandbox](/docs/versus-sandbox) for when to escalate to a full sandbox","src/content/docs/docs/security-model.mdx","02132bd43ce0b857","docs/sessions",{"id":287,"data":289,"body":295,"filePath":296,"digest":297,"deferredRender":16},{"title":290,"description":291,"editUrl":16,"head":292,"template":18,"sidebar":293,"pagefind":16,"draft":20},"Sessions","Create agent sessions, send prompts, stream responses, and subscribe to events.",[],{"hidden":20,"attrs":294},{},"import { Aside } from '@astrojs/starlight/components';\nimport CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nSessions launch an agent inside the VM, stream its responses in real time over `sessionEvent`, and persist a replayable ACP transcript that survives sleep/wake.\n\n## Create a session\n\nUse `createSession` to launch an agent inside the VM. Returns session metadata including capabilities and agent info. The agent starts in `/home/user` by default; override it with the `cwd` option below.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconsole.log(session.sessionId);\nconsole.log(session.capabilities);\nconsole.log(session.agentInfo);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n### `createSession` options\n\nThe second argument to `createSession` accepts:\n\n- **`env`**: environment variables for the agent process (e.g. API keys). Not inherited from the host.\n- **`cwd`**: working directory inside the VM. Defaults to `/home/user`.\n- **`mcpServers`**: MCP servers (local child processes or remote URLs) exposing extra tools.\n- **`additionalInstructions`**: text appended to the agent's system prompt.\n- **`skipOsInstructions`**: skip the base OS instructions injection. Tool documentation is still included.\n\n\n## Send a prompt\n\nUse `sendPrompt` to send a message to an active session. The response contains the agent's reply.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconst response = await agent.sendPrompt(\n session.sessionId,\n \"Create a TypeScript function that checks if a number is prime\",\n);\nconsole.log(response.text);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Stream responses\n\nSubscribe to `sessionEvent` to receive real-time streaming output from the agent.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\nconst conn = agent.connect();\n\n// Subscribe to session events before sending the prompt\nconn.on(\"sessionEvent\", (data) => {\n console.log(`[${data.sessionId}]`, data.event.method, data.event.params);\n});\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Explain how async/await works\");\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n\n## Cancel a prompt\n\nUse `cancelPrompt` to stop an in-progress prompt.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Start a long-running prompt\nconst promptPromise = agent.sendPrompt(\n session.sessionId,\n \"Refactor the entire codebase to use TypeScript strict mode\",\n);\n\n// Cancel after 10 seconds\nsetTimeout(async () => {\n await agent.cancelPrompt(session.sessionId);\n}, 10_000);\n\nconst response = await promptPromise;\nconsole.log(response.text);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Close and destroy sessions\n\n- `closeSession` gracefully closes a session without removing persisted data\n- `destroySession` removes the session and all persisted data\n- To reconnect to a previously created session and replay its history, see [Replay events](#replay-events) and [Resuming a suspended session](/docs/architecture/agent-sessions#resuming-a-suspended-session)\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Close without destroying persisted data\nawait agent.closeSession(session.sessionId);\n\n// Destroy session and all persisted events\nawait agent.destroySession(session.sessionId);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Runtime configuration\n\nChange model, mode, and thought level on a live session.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Change model\nawait agent.setModel(session.sessionId, \"claude-sonnet-4-6\");\n\n// Change mode (e.g. \"plan\", \"auto\")\nawait agent.setMode(session.sessionId, \"plan\");\n\n// Change thought level\nawait agent.setThoughtLevel(session.sessionId, \"high\");\n\n// Query available options\nconst modes = await agent.getModes(session.sessionId);\nconsole.log(modes);\n\nconst options = await agent.getConfigOptions(session.sessionId);\nconsole.log(options);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Replay events\n\nUse `getSessionEvents` to replay a session's persisted events, including for VMs that are not currently running. Pair it with `listPersistedSessions` to find earlier sessions.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Hello\");\n\n// Replay persisted events\nconst events = await agent.getSessionEvents(session.sessionId);\nconsole.log(events);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Persisted session history\n\nQuery session history from SQLite. Works even when the VM is not running.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// List all persisted sessions\nconst sessions = await agent.listPersistedSessions();\nfor (const s of sessions) {\n console.log(s.sessionId, s.agentType, s.createdAt);\n}\n\n// Get full event history for a session\nconst events = await agent.getSessionEvents(sessions[0].sessionId);\nfor (const e of events) {\n console.log(e.seq, e.event.method, e.createdAt);\n}\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Multiple sessions\n\nA single VM can run multiple sessions simultaneously. Each session has its own agent process but shares the same filesystem. Use different session IDs to manage them independently.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Create two sessions in the same VM\nconst coder = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconst reviewer = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Coder writes code\nawait agent.sendPrompt(coder.sessionId, \"Write a REST API at /home/user/api.ts\");\n\n// Reviewer reads and reviews the same file\nawait agent.sendPrompt(reviewer.sessionId, \"Review /home/user/api.ts for issues\");\n\n// Close each session independently\nawait agent.closeSession(coder.sessionId);\nawait agent.closeSession(reviewer.sessionId);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*","src/content/docs/docs/sessions.mdx","b9f56d7e7930852a","docs/software",{"id":298,"data":300,"body":306,"filePath":307,"digest":308,"deferredRender":16},{"title":301,"description":302,"editUrl":16,"head":303,"template":18,"sidebar":304,"pagefind":16,"draft":20},"Software","Install software packages and configure the commands available inside agentOS.",[],{"hidden":20,"attrs":305},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nagentOS ships with a common set of POSIX utilities (coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip) out of the box. The `software` option installs additional packages, each providing one or more CLI commands.\n\n## Install\n\n```bash\nnpm install @rivet-dev/agentos @agentos-software/pi\n```\n\nAdd packages like `@agentos-software/ripgrep` or `@agentos-software/jq` for anything beyond the default utilities. Browse the full catalog on the [Registry](/registry).\n\n## Usage\n\nImport the software packages you want, list them in the `software` array on the actor, then run commands through the client handle.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\nimport ripgrep from \"@agentos-software/ripgrep\";\nimport jq from \"@agentos-software/jq\";\n\n// Each entry adds its CLI commands to the VM. Common POSIX utilities ship by\n// default; `pi` is the agent, and `ripgrep`/`jq` add the `rg` and `jq`\n// commands. Browse the registry for more packages.\nconst vm = agentOS({\n\tsoftware: [pi, ripgrep, jq],\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({\n\tendpoint: \"http://localhost:6420\",\n});\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// `rg` (ripgrep) and `jq` are now available inside the VM. Find files containing\n// \"TODO\" and pretty-print the matching paths as JSON.\nconst result = await agent.exec(\"rg --files-with-matches TODO /home/user | jq -R .\");\nconsole.log(result.stdout);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/software)*\n\n## Available Packages\n\nBrowse all available software packages on the [Registry](/registry).\n\n\n## Publishing Custom Packages\n\nSee the [agent-os-registry contributing guide](https://github.com/rivet-dev/agent-os/blob/main/registry/CONTRIBUTING.md) for how to add new software packages to the registry.","src/content/docs/docs/software.mdx","aca05fac12fb735b","docs/resource-limits",{"id":309,"data":311,"body":317,"filePath":318,"digest":319,"deferredRender":16},{"title":312,"description":313,"editUrl":16,"head":314,"template":18,"sidebar":315,"pagefind":16,"draft":20},"Resource Limits","Cap per-VM processes, file descriptors, sockets, and filesystem bytes so guest code can never exhaust the host.",[],{"hidden":20,"attrs":316},{},"Every agentOS VM runs with **per-VM resource caps**. Runaway or malicious guest code can exhaust its own VM, but it can never starve the host or any sibling VM.\n\n- **Bounded by default**: each VM ships with conservative caps. Unset fields fall back to built-in defaults that match the runtime's historical constants.\n- **Per-VM**: every VM gets its own budget. Limits are not shared across VMs.\n- **Enforced by the kernel**: a guest that exceeds a cap fails inside the VM (out-of-memory, `EMFILE`, `EAGAIN`, etc.). The host is never affected.\n- **Operator-raisable**: the operator (the trusted process that creates the VM) may raise any cap for trusted workloads. Guest code can never raise its own caps.\n\n## Setting limits\n\nSet caps on the `limits` object in the `agentOS` config. Limits are grouped by subsystem (`resources` and more). Omitted limits keep their secure default.\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n\tsoftware: [pi],\n\tlimits: {\n\t\tresources: {\n\t\t\tmaxProcesses: 64, // concurrent processes\n\t\t\tmaxOpenFds: 256, // open file descriptors\n\t\t\tmaxSockets: 128, // open sockets\n\t\t\tmaxFilesystemBytes: 256 * 1024 * 1024, // VFS storage budget\n\t\t\tmaxWasmStackBytes: 4 * 1024 * 1024, // WASM call-stack ceiling\n\t\t},\n\t},\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/resource-limits)*\n\n## Available caps\n\n| Limit | Controls | Notes |\n|---|---|---|\n| `resources.maxProcesses` | Concurrent processes in the VM process table | Caps fork bombs and runaway spawning. New spawns fail with `EAGAIN`. |\n| `resources.maxOpenFds` | Open file descriptors | Exhausting the table fails with `EMFILE` / `ENFILE`. |\n| `resources.maxSockets` | Open sockets in the socket table | Bounds concurrent connections; excess `connect`/`accept` fail. |\n| `resources.maxFilesystemBytes` | Total bytes stored in the virtual filesystem | Bounds VFS storage; writes past the budget fail with a no-space error. |\n| `resources.maxWasmStackBytes` | Maximum WASM call-stack size, in bytes | Deep recursion fails with a stack overflow instead of crashing the VM. |\n\n## Behavior at the limit\n\n- **WASM stack**: deep recursion throws a stack-overflow error in the guest, never a host crash.\n- **Filesystem bytes**: writing past the VFS budget fails with a no-space error to the guest.\n- **Counts (fds / processes / sockets)**: hitting a table cap returns the standard POSIX errno (`EMFILE`, `EAGAIN`, etc.), exactly as a real Linux kernel would under `ulimit`.","src/content/docs/docs/resource-limits.mdx","d88308a782482953","docs/system-prompt",{"id":320,"data":322,"body":328,"filePath":329,"digest":330,"deferredRender":16},{"title":323,"description":324,"editUrl":16,"head":325,"template":18,"sidebar":326,"pagefind":16,"draft":20},"System Prompt","How agentOS injects context into agent sessions.",[],{"hidden":20,"attrs":327},{},"agentOS automatically injects a system prompt into every agent session that describes the VM environment and available tools. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.).\n\nThe base prompt is embedded in the sidecar (not written to a file inside the VM). At session start the sidecar assembles the base prompt with your additional instructions and generated tool docs, then injects the result into the agent adapter's launch arguments (for example, `--append-system-prompt` for Pi).\n\n## Customization\n\n- `additionalInstructions` appends extra instructions to the agent's system prompt. They are added after the base OS prompt and the generated tool docs, so they layer on top of (rather than replace) the agent's own instructions.\n- `skipOsInstructions` suppresses the base OS prompt while still injecting the generated tool docs.\n\n```ts\nconst session = await vm.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n // Extra instructions appended to the agent system prompt\n additionalInstructions: \"Always write tests before implementation.\",\n // Suppress the base OS prompt (tool docs are still injected)\n skipOsInstructions: true,\n});\n```\n\n`additionalInstructions` can also be set globally in `agentOs({ options: { additionalInstructions } })` so it applies to every session.","src/content/docs/docs/system-prompt.mdx","e9ae34c14ca71ac2","docs/versus-sandbox",{"id":331,"data":333,"body":339,"filePath":340,"digest":341,"deferredRender":16},{"title":334,"description":335,"editUrl":16,"head":336,"template":18,"sidebar":337,"pagefind":16,"draft":20},"agentOS vs Sandbox","When to use the lightweight agentOS VM, a full sandbox, or both together.",[],{"hidden":20,"attrs":338},{},"- **agentOS** is a lightweight VM that runs inside your process. Near-zero cold start, low memory, direct backend integration via [bindings](/docs/bindings).\n- **Sandboxes** are full Linux environments with root access, system packages, and native binary support.\n- **You can use both.** agentOS works with sandboxes through [sandbox mounting](/docs/sandbox). Agents run in the lightweight VM by default and spin up a full sandbox on demand.\n\n## Comparison\n\n| | agentOS VM | Full Sandbox |\n|---|---|---|\n| **Cost** | Very low. Runs in your process. | Pay per second of uptime. |\n| **Startup** | Near-zero cold start (~6 ms). | Seconds to spin up. |\n| **Backend integration** | Direct. [Bindings](/docs/bindings) call your functions with zero latency. | Indirect. Requires network calls back to your backend. |\n| **API keys** | Stay on the server via the [LLM gateway](/docs/llm-gateway). | Must be injected into the sandbox environment. |\n| **Permissions** | Granular, deny-by-default. | Coarse-grained (container-level). |\n| **Infrastructure** | `npm install` | Vendor account + API keys. |\n| **Best for** | Coding, file manipulation, scripting, API calls, orchestration. | Browsers, desktop automation, native compilation, dev servers. |\n\n## When to use each\n\n### agentOS VM\n\nUse the lightweight VM for most agent workloads:\n\n- Coding and file editing\n- Running scripts and CLI tools\n- Calling APIs and services via bindings\n- Multi-agent orchestration and workflows\n- Tasks where backend integration matters (permissions, tool access, LLM routing)\n\n### Full sandbox\n\nSpin up a sandbox when the workload needs a real Linux kernel:\n\n- Browsers and desktop automation (Playwright, Puppeteer, Selenium)\n- Heavy compilation and native toolchains\n- Dev servers with hot reload, databases, and system ports\n- GUI applications and VNC sessions\n\n### Both together\n\nUse agentOS with [sandbox mounting](/docs/sandbox) for workflows that need both:\n\n- Agent runs in the agentOS VM with full access to bindings and permissions\n- Sandbox spins up on demand for heavy tasks\n- Sandbox filesystem is mounted into the VM as a native directory\n- Agent reads and writes sandbox files the same way it reads local files","src/content/docs/docs/versus-sandbox.mdx","0ffcdc2908107d12","docs/webhooks",{"id":342,"data":344,"body":350,"filePath":351,"digest":352,"deferredRender":16},{"title":345,"description":346,"editUrl":16,"head":347,"template":18,"sidebar":348,"pagefind":16,"draft":20},"Webhooks","Trigger agent workflows from external webhooks using Hono and queues.",[],{"hidden":20,"attrs":349},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nUse a lightweight HTTP server to receive webhooks and drive an agent. This example uses [Hono](https://hono.dev) to receive Slack webhooks and call an agent directly.\n\n## Example: Slack webhook to agent\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport { Hono } from \"hono\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n additionalInstructions: \"You answer Slack messages concisely.\",\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n\n// Hono server to receive Slack webhooks\nconst app = new Hono();\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\napp.post(\"/slack/events\", async (c) => {\n const body = await c.req.json();\n\n // Handle Slack URL verification\n if (body.type === \"url_verification\") {\n return c.json({ challenge: body.challenge });\n }\n\n // Call the agent directly for a user message\n if (body.event?.type === \"message\" && !body.event?.bot_id) {\n const { channel, text, user } = body.event;\n const agent = client.vm.getOrCreate(\"slack-agent\");\n\n const session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n const result = await agent.sendPrompt(\n session.sessionId,\n `Slack message from ${user} in #${channel}:\\n\\n${text}\\n\\nRespond helpfully.`,\n );\n await agent.closeSession(session.sessionId);\n\n // Post the response back to Slack\n await fetch(\"https://slack.com/api/chat.postMessage\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,\n },\n body: JSON.stringify({ channel, text: result.text }),\n });\n }\n\n return c.json({ ok: true });\n});\n\nexport default app;\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/webhooks)*\n\n## How it works\n\n1. Slack sends an HTTP POST to `/slack/events`\n2. The Hono handler validates the event and pushes it to the actor's queue\n3. The queue processes messages one at a time, creating agent sessions for each\n4. The agent responds and the worker posts the reply back to Slack\n\nThe queue provides backpressure and durability. If the agent is busy, messages wait in the queue. If the server restarts, queued messages are replayed.\n\n## Recommendations\n\n- Return `200` from the webhook handler immediately after queuing. External services like Slack have short timeout windows.\n- Store webhook secrets in environment variables, not in code.","src/content/docs/docs/webhooks.mdx","ac38e9dc5d31808e","docs/workflows",{"id":353,"data":355,"body":361,"filePath":362,"digest":363,"deferredRender":16},{"title":356,"description":357,"editUrl":16,"head":358,"template":18,"sidebar":359,"pagefind":16,"draft":20},"Workflow Automation","Orchestrate multi-step agent tasks with durable workflows.",[],{"hidden":20,"attrs":360},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nOrchestrate multi-step agent tasks with durable workflows that survive crashes and restarts. Build them with RivetKit's `workflow()` run handler, where each `ctx.step()` is recorded, retried, and resumed independently, and the output of one step can feed into the next.\n\n## Basic workflow\n\nA workflow is the durable `run` handler of an actor. Wrap it in `workflow()` and drive a multi-step agent task as an ordered series of steps: clone the repo, let an agent fix the bug, then run the tests. Trigger work by sending to a queue; the workflow loops and waits durably for the next message.\n\nSession creation and prompting happen within the step that uses them, so a session never has to outlive the work it backs (sessions are ephemeral and would not survive a replay). Steps reach the Agent OS VM, a separate actor, through `ctx.client()`.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { actor, queue } from \"rivetkit\";\nimport {\n type WorkflowLoopContextOf,\n workflow,\n} from \"rivetkit/workflow\";\nimport pi from \"./software/pi\";\n\n// The Agent OS VM that each workflow step drives. It is its own actor, kept\n// separate from the workflow orchestrator so steps can reach it over the client.\nconst vm = agentOS({ software: [pi] });\n\n// A durable workflow actor. Its `run` handler is built with `workflow()`, so\n// every `ctx.step(...)` is recorded, retried, and resumed independently: if the\n// process crashes mid-run, replay skips completed steps and continues where it\n// left off. Trigger work by sending to the `fixBug` queue; the workflow loops,\n// waiting durably for the next message.\nconst bugFixer = actor({\n state: {\n lastIssue: null as string | null,\n lastExitCode: null as number | null,\n },\n queues: {\n fixBug: queue\u003C{ repo: string; issue: string }>(),\n },\n run: workflow(async (ctx) => {\n await ctx.loop(\"fix-bug-loop\", async (loopCtx) => {\n // Wait durably for the next bug-fix request.\n const message = await loopCtx.queue.next(\"wait-fix-bug\");\n const { repo, issue } = message.body;\n\n // Step 1: Clone the repo. Each step is an isolated, retryable unit of\n // work; a crash here resumes from this step on replay.\n await loopCtx.step(\"clone-repo\", () => cloneRepo(loopCtx, repo));\n\n // Step 2: An agent fixes the bug. The session is created and closed\n // inside the step, so it never has to outlive the work it backs (sessions\n // are ephemeral and would not survive a replay).\n await loopCtx.step(\"fix-bug\", () => fixBugWithAgent(loopCtx, issue));\n\n // Step 3: Run the tests. The exit code feeds into the next step.\n const exitCode = await loopCtx.step(\"run-tests\", () => runTests(loopCtx));\n\n // State changes are only valid inside a step callback, so they are\n // recorded as part of replay.\n await loopCtx.step(\"record-result\", async () => {\n loopCtx.state.lastIssue = issue;\n loopCtx.state.lastExitCode = exitCode;\n });\n });\n }),\n actions: {\n getState: (c) => c.state,\n },\n});\n\nasync function cloneRepo(\n ctx: WorkflowLoopContextOf\u003Ctypeof bugFixer>,\n repo: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n await agentHandle.exec(`git clone ${repo} /home/user/repo`);\n}\n\nasync function fixBugWithAgent(\n ctx: WorkflowLoopContextOf\u003Ctypeof bugFixer>,\n issue: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n const session = await agentHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agentHandle.sendPrompt(\n session.sessionId,\n `Fix the bug described in issue: ${issue}`,\n );\n await agentHandle.closeSession(session.sessionId);\n}\n\nasync function runTests(\n ctx: WorkflowLoopContextOf\u003Ctypeof bugFixer>,\n): Promise\u003Cnumber> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n const tests = await agentHandle.exec(\"cd /home/user/repo && npm test\");\n return tests.exitCode;\n}\n\nexport const registry = setup({ use: { vm, bugFixer, codeReviewer } });\n\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.bugFixer.getOrCreate(\"main\");\n\n// Trigger the durable workflow by sending to its queue. The workflow runs each\n// step in order against the VM, surviving restarts: the output of one step (the\n// cloned repo, the agent's edits) feeds into the next.\nawait handle.send(\"fixBug\", {\n repo: \"https://github.com/example/repo.git\",\n issue: \"Fix the login redirect bug\",\n});\n\nconst state = await handle.getState();\nconsole.log(\"Last issue:\", state.lastIssue, \"exit code:\", state.lastExitCode);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/workflows)*\n\n## Agent chaining\n\nOutput of one agent session feeds into the next. Each session is created and completed within its own step, and data passes between steps through the VM filesystem (a review file) and step return values.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\n// Agent chaining: the output of one agent session feeds into the next. Each\n// session is created and completed within its own step, and data passes between\n// steps through the VM filesystem (a review file) and step return values.\nconst codeReviewer = actor({\n state: {\n reviewedFiles: 0,\n },\n queues: {\n codeReview: queue\u003C{ filePath: string }>(),\n },\n run: workflow(async (ctx) => {\n await ctx.loop(\"code-review-loop\", async (loopCtx) => {\n const message = await loopCtx.queue.next(\"wait-code-review\");\n const { filePath } = message.body;\n\n // Step 1: An agent reviews the code and writes findings to a file.\n await loopCtx.step(\"review\", () => reviewCode(loopCtx, filePath));\n\n // Step 2: Read the review back from the VM filesystem. Its text is the\n // step return value, so it flows into the next step.\n const review = await loopCtx.step(\"read-review\", () => readReview(loopCtx));\n\n // Step 3: A second session applies fixes based on the review.\n await loopCtx.step(\"fix\", () => applyReview(loopCtx, review));\n\n await loopCtx.step(\"record-review\", async () => {\n loopCtx.state.reviewedFiles += 1;\n });\n });\n }),\n actions: {\n getState: (c) => c.state,\n },\n});\n\nasync function reviewCode(\n ctx: WorkflowLoopContextOf\u003Ctypeof codeReviewer>,\n filePath: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"reviewer\");\n const session = await agentHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agentHandle.sendPrompt(\n session.sessionId,\n `Review the code at ${filePath} and write your findings to /home/user/review.md`,\n );\n await agentHandle.closeSession(session.sessionId);\n}\n\nasync function readReview(\n ctx: WorkflowLoopContextOf\u003Ctypeof codeReviewer>,\n): Promise\u003Cstring> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"reviewer\");\n const content = await agentHandle.readFile(\"/home/user/review.md\");\n return new TextDecoder().decode(content);\n}\n\nasync function applyReview(\n ctx: WorkflowLoopContextOf\u003Ctypeof codeReviewer>,\n review: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"reviewer\");\n const session = await agentHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agentHandle.sendPrompt(\n session.sessionId,\n `Apply the following review feedback:\\n\\n${review}`,\n );\n await agentHandle.closeSession(session.sessionId);\n}\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.codeReviewer.getOrCreate(\"main\");\n\n// Run the chained review + fix workflow against a file in the VM.\nawait handle.send(\"codeReview\", { filePath: \"/home/user/src/auth.ts\" });\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/workflows)*\n\n## Recommendations\n\n- Build the actor's `run` handler with `workflow()` so each `ctx.step()` is durable: recorded, retried, and resumed independently across crashes and restarts.\n- Keep step names stable across code changes. Renaming a step breaks replay for in-progress workflows.\n- Create and close sessions within the step that uses them. Sessions are ephemeral, so keep their lifetime scoped to one unit of work.\n- Pass data between steps via the filesystem or step return values, not session state.\n- Keep `state` changes and other actor-local side effects inside `ctx.step()` callbacks; use non-step workflow code (queue waits, loops, sleeps) only for orchestration.\n- Reach the Agent OS VM, a separate actor, from inside a step with `ctx.client()`.\n- See [Workflows](/docs/actors/workflows) for the full workflow API reference including timers, joins, and races.","src/content/docs/docs/workflows.mdx","d38fb314f7029e00","docs/agents/amp",{"id":364,"data":366,"body":372,"filePath":373,"digest":374,"deferredRender":16},{"title":367,"description":368,"editUrl":16,"head":369,"template":18,"sidebar":370,"pagefind":16,"draft":20},"Amp","Run the Amp coding agent inside a VM.",[],{"hidden":20,"attrs":371},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nAmp agent documentation is coming soon.\n\u003C/Aside>","src/content/docs/docs/agents/amp.mdx","e2bfc31430b94ed9","docs/agents/claude",{"id":375,"data":377,"body":383,"filePath":384,"digest":385,"deferredRender":16},{"title":378,"description":379,"editUrl":16,"head":380,"template":18,"sidebar":381,"pagefind":16,"draft":20},"Claude","Run the Claude coding agent inside a VM.",[],{"hidden":20,"attrs":382},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nClaude agent documentation is coming soon.\n\u003C/Aside>\n\n```ts\nimport claude from \"@agentos-software/claude\";\n\nconst vm = await AgentOs.create({ software: [claude] });\nconst { sessionId } = await vm.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },\n});\n```","src/content/docs/docs/agents/claude.mdx","86c453cf7c31ea0d","docs/agents/codex",{"id":386,"data":388,"body":394,"filePath":395,"digest":396,"deferredRender":16},{"title":389,"description":390,"editUrl":16,"head":391,"template":18,"sidebar":392,"pagefind":16,"draft":20},"Codex","Run the Codex coding agent inside a VM.",[],{"hidden":20,"attrs":393},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nCodex agent documentation is coming soon.\n\u003C/Aside>\n\n```ts\nimport codex from \"@agentos-software/codex\";\n\nconst vm = await AgentOs.create({ software: [codex] });\nconst { sessionId } = await vm.createSession(\"codex\", {\n env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY },\n});\n```","src/content/docs/docs/agents/codex.mdx","4526686fc19399c5","docs/agents/opencode",{"id":397,"data":399,"body":405,"filePath":406,"digest":407,"deferredRender":16},{"title":400,"description":401,"editUrl":16,"head":402,"template":18,"sidebar":403,"pagefind":16,"draft":20},"OpenCode","Run the OpenCode coding agent inside a VM.",[],{"hidden":20,"attrs":404},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nOpenCode agent documentation is coming soon.\n\u003C/Aside>\n\n```ts\nimport opencode from \"@agentos-software/opencode\";\n\nconst vm = await AgentOs.create({ software: [opencode] });\nconst { sessionId } = await vm.createSession(\"opencode\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },\n});\n```","src/content/docs/docs/agents/opencode.mdx","7c675c06ccdc87a5","docs/agents/pi",{"id":408,"data":410,"body":416,"filePath":417,"digest":418,"deferredRender":16},{"title":411,"description":412,"editUrl":16,"head":413,"template":18,"sidebar":414,"pagefind":16,"draft":20},"Pi","Run the Pi coding agent inside a VM with extensions and custom configuration.",[],{"hidden":20,"attrs":415},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\n## Quick start\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOs } from \"rivetkit/agent-os\";\nimport { setup } from \"rivetkit\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOs({\n options: { software: [pi] },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"rivetkit/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>(\"http://localhost:6420\");\nconst agent = client.vm.getOrCreate([\"my-agent\"]);\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\nconst { text } = await agent.sendPrompt(\n session.sessionId,\n \"What files are in the current directory?\",\n);\nconsole.log(text);\n```\n\u003C/CodeGroup>\n\nRead [Sessions](/docs/sessions) first for session options, streaming events, prompts, and lifecycle management.\n\n## Extensions\n\nPi supports [extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) that let you register custom tools, modify the system prompt, and hook into agent lifecycle events. Write a `.js` file into the VM's extensions directory before creating a session and Pi discovers it automatically.\n\nPi scans two directories for `.js` extension files:\n\n| Directory | Scope |\n|-----------|-------|\n| `~/.pi/agent/extensions/` | Global — applies to all Pi sessions |\n| `\u003Ccwd>/.pi/extensions/` | Project — applies only when cwd matches |\n\n```ts\nconst extensionCode = `\nexport default function(pi) {\n // Modify the system prompt before each agent turn\n pi.on(\"before_agent_start\", async (event) => {\n return {\n systemPrompt: event.systemPrompt +\n \"\\\\n\\\\nAlways respond in formal English.\"\n };\n });\n}\n`;\n\n// Write the extension before creating the session\nawait agent.mkdir(\"/home/user/.pi/agent/extensions\", { recursive: true });\nawait agent.writeFile(\"/home/user/.pi/agent/extensions/formal.js\", extensionCode);\n\n// Pi discovers the extension automatically\nconst { sessionId } = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },\n});\n```\n\nSee the [Pi extension documentation](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) for the full extension API.","src/content/docs/docs/agents/pi.mdx","99420e44a78f6d1e","docs/architecture/agent-sessions",{"id":419,"data":421,"body":427,"filePath":428,"digest":429,"deferredRender":16},{"title":422,"description":423,"editUrl":16,"head":424,"template":18,"sidebar":425,"pagefind":16,"draft":20},"Agent Sessions","Internals of agent sessions: how a session is created and bound to a VM, how prompts and events flow from client to sidecar to agent adapter and back, the session lifecycle, and where session state lives.",[],{"hidden":20,"attrs":426},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis page is an internals deep-dive on how agent sessions work under the hood. For the usage API (creating sessions, sending prompts, streaming responses, replaying events), see [Sessions](/docs/sessions).\n\nA session is a long-lived conversation with an agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) running inside a VM. Where a bare `exec()` / `run()` starts a fresh guest process and returns when it exits, a session keeps an agent process alive across many prompts, streams its output back as events, and persists a transcript that survives sleep/wake cycles. Everything below describes the machinery that makes that possible while keeping the agent inside the same isolation boundary as any other guest.\n\n## Where a session sits in the component model\n\nThe [three components](/docs/architecture#three-components) are unchanged for sessions: a trusted **client**, the trusted **sidecar** that owns the kernel, and the untrusted **executor** that runs guest code. An agent session adds one more layer on the guest side of the boundary:\n\n- **Client.** Calls `createSession`, `sendPrompt`, and the rest of the session API. It never runs the agent itself; it drives the session over the wire protocol.\n- **Sidecar / kernel.** Spawns the agent as a kernel-managed process inside the VM, owns the session's I/O, applies the permission policy on every syscall the agent makes, and persists the transcript.\n- **Agent adapter.** A per-agent-type shim, inside the VM, that translates between the session protocol and the specific agent's native interface. It normalizes the agent's output into the [Agent Communication Protocol (ACP)](/docs/sessions) so every agent type produces the same event shape.\n- **Executor.** The agent process itself plus any tools it spawns. It is untrusted guest code like any other: its file reads, child processes, and network calls all flow through the kernel.\n\nThe key consequence: an agent is not privileged. It is a guest process that happens to be long-lived and conversational. Its capabilities are exactly the VM's [permission policy](/docs/permissions), nothing more.\n\n## Creating a session and binding it to a VM\n\nA session is always created against an existing VM. The client resolves a VM handle (for example `client.vm.getOrCreate([...])`) and calls `createSession(agentType, options)` on it. Under the hood:\n\n1. **The request crosses the wire** (client to sidecar) carrying the agent type and the session options: `env`, `cwd`, `mcpServers`, `additionalInstructions`, and `skipOsInstructions`.\n2. **The kernel boots the VM** if it is not already running, with its bootstrapped virtual filesystem.\n3. **The sidecar selects the agent adapter** for the requested type and spawns the agent as a kernel-managed process inside that VM. Because the VM does not inherit the host `process.env`, the agent only sees the `env` passed in the options (this is why API keys must be supplied explicitly). The process starts in `cwd` (default `/home/user`).\n4. **The session is registered** in the VM with a `sessionId`, and the adapter performs its handshake with the agent to discover `capabilities` and `agentInfo`.\n5. **The handle returns** that metadata to the client.\n\nThe session is bound to that VM for its lifetime. The agent process, its working directory, and its persisted transcript all live inside the VM's isolation domain. A VM can host several sessions at once: each gets its own agent process, but they share the one VM's filesystem (see [Multiple sessions](/docs/sessions#multiple-sessions)). Two sessions in two different VMs share nothing, exactly as described in the [isolation model](/docs/architecture#isolation-model).\n\n\u003CAside type=\"note\">MCP servers configured on a session follow the same boundary. A `local` MCP server runs as a child process inside the VM (kernel-managed, gated by the permission policy); a `remote` MCP server is reached over the network, so its traffic flows through the kernel socket table and is subject to the network allowlist.\u003C/Aside>\n\n## How a prompt flows: client to agent and back\n\nSending a prompt is a request in one direction with a stream of events flowing back in the other. The lifecycle of a single prompt extends the general [lifecycle of a request](/docs/architecture#lifecycle-of-a-request):\n\n1. **The client calls `sendPrompt(sessionId, text)`.** The request crosses the wire to the sidecar (hop one).\n2. **The sidecar routes it to the session's agent adapter,** which translates the prompt into the agent's native input and writes it to the running agent process.\n3. **The agent works the turn.** As it thinks, calls tools, edits files, and produces output, every action it takes is a guest syscall back into the kernel (hop two): file reads/writes hit the VFS, tool subprocesses are kernel-managed, and network calls go through the socket table under the allowlist.\n4. **The adapter normalizes the agent's output into ACP events.** Each event is assigned a monotonically increasing sequence number, appended to the session's event log, and persisted.\n5. **Events stream back to the client** as `sessionEvent` notifications, carrying the `sessionId` and the ACP `event` (its `method` and `params`). This is why the docs recommend subscribing to `sessionEvent` before calling `sendPrompt`: events emitted early in the turn would otherwise be missed.\n6. **The turn resolves.** When the agent finishes the turn, `sendPrompt` resolves with the reply.\n\n`cancelPrompt(sessionId)` interrupts an in-progress turn: the request crosses to the sidecar, which signals the agent process to stop the current turn through the adapter, leaving the session itself alive for the next prompt.\n\n```\nclient sidecar / kernel agent adapter agent (executor)\n | sendPrompt | | |\n | --------------------> | route to session | |\n | | ---------------------------> | native input ---> |\n | | | | (thinks, calls\n | | \u003C--- syscalls (VFS, procs, sockets) ------------ | tools, edits)\n | | | \u003C-- native output |\n | sessionEvent (ACP) | persist + assign seq | |\n | \u003C------------------- | \u003C-------- ACP events ------- | |\n | ...stream... | | |\n | resolve(reply) | | |\n | \u003C------------------- | | |\n```\n\n## Session lifecycle\n\nA session moves through these states, all driven by client calls over the wire:\n\n- **Active.** Created and bound to a running VM, with a live agent process. Prompts can be sent and events stream back.\n- **Suspended.** When the VM sleeps, the agent process is torn down but the session's persisted transcript remains in storage. `resumeSession(sessionId)` reconnects: the kernel wakes the VM, re-spawns the agent, and rebinds the session so prompts can continue.\n- **Closed.** `closeSession(sessionId)` gracefully shuts down the agent process and releases its in-VM resources, but leaves the persisted transcript intact so history can still be queried.\n- **Destroyed.** `destroySession(sessionId)` removes the session and all of its persisted events. This is irreversible.\n\nBecause the transcript is persisted, closing or suspending a session is not the same as losing it: the event history can be read back later, even when the VM is not running.\n\nRuntime configuration (`setModel`, `setMode`, `setThoughtLevel`) mutates an active session in place by sending the change through the adapter to the live agent, without restarting the session.\n\n## Resuming a suspended session\n\nWhen a VM sleeps the agent process is destroyed, but the session registry and the transcript survive in SQLite. `resumeSession(sessionId)` (or simply prompting a suspended session) rebinds the stable, client-facing `sessionId` to a freshly spawned agent. Resume is **lazy** (it runs on the first prompt to a non-live session) and **capability-driven** (the orchestrator never special-cases an agent by name, only by what it advertises). There are two paths:\n\n- **Native ACP resume (optimization).** If the agent advertises ACP `loadSession`/`resume` and its own store survived on the durable root, the sidecar issues `session/load` and the agent restores its full context itself. The `sessionId` is unchanged.\n- **Universal transcript fallback.** If the agent has no native resume, or its store did not survive, the sidecar reconstructs a Markdown transcript from the recorded events, writes it into the VM (for example `/root/.agentos/threads/\u003CsessionId>.md`), starts a fresh agent, and prefixes the next prompt with a pointer to that file. Because this needs only file-read tools it works for any agent with no per-agent code, at the cost of pointing the agent at the transcript rather than pre-loading it into context.\n\nResume depends on a **durable root filesystem**. The RivetKit actor configures one automatically (its SQLite-backed root), so transcript capture and resume work out of the box. A direct `AgentOs` SDK user on the default in-memory root has no durable store: transcript capture is a no-op and context cannot be restored, so configure a durable root explicitly if you need resume outside the actor.\n\n## Where session state lives\n\nSession state spans two tiers:\n\n- **In-memory, while the VM runs.** The running agent process holds the live conversation, and the sidecar keeps the session's recent event log with sequence numbers, the basis for live reconnection: a client tracks the last sequence number it processed and asks for everything after it, so no events are dropped or duplicated across a reconnect.\n- **Persisted in SQLite, independent of the VM.** Every ACP event is written to a SQLite-backed transcript store inside the VM, keyed by `sessionId` and sequence number. This tier survives sleep/wake and VM shutdown. `listPersistedSessions()` and `getSessionEvents(sessionId)` read from it and work even when the VM is not running, which is what makes transcript-history UIs possible without keeping a VM warm.\n\nSee [Replay events](/docs/sessions#replay-events) for replaying a session's persisted events.\n\nThe transcript living inside the VM keeps session state on the same side of the boundary as the agent that produced it: it is part of the VM's isolation domain, not the client's. The client only ever sees it by asking the sidecar for it over the wire.\n\n## Where to go next\n\n- [Sessions](/docs/sessions): the usage API for creating sessions, sending prompts, and replaying events.\n- [Architecture](/docs/architecture): the component model, request lifecycle, and isolation model that sessions build on.\n- [Permissions](/docs/permissions): the policy the kernel enforces on every syscall an agent makes.\n- [Replay events](/docs/sessions#replay-events): in-memory versus persisted event replay.","src/content/docs/docs/architecture/agent-sessions.mdx","345b6bec33150332","docs/architecture/compiler-toolchain",{"id":430,"data":432,"body":438,"filePath":439,"digest":440,"deferredRender":16},{"title":433,"description":434,"editUrl":16,"head":435,"template":18,"sidebar":436,"pagefind":16,"draft":20},"Compiler Toolchain","How agentOS compiles its command suite to WebAssembly: Rust coreutils via cargo and C programs via wasi-sdk, linked against a patched wasi-libc plus the wasi-ext bindings, and how the resulting .wasm files become the guest's commands.",[],{"hidden":20,"attrs":437},{},"import { Aside } from '@astrojs/starlight/components';\n\nThe commands a guest runs through [process execution](/docs/processes), the shell\n(`sh`) and the coreutils behind it, are not native host binaries. They are\nWebAssembly modules compiled ahead of time and mounted into the VM. This page\ncovers how that command suite is produced: which toolchains compile it, what it\nlinks against, and how the resulting `.wasm` files become the guest's commands.\n\nFor *why* WASM is a first-class guest and *how* it presents a POSIX surface at\nruntime, see the [WASM VM](/docs/architecture/posix-syscalls) page. This page is the build-side\ncounterpart: it documents the toolchain that emits binaries carrying both\n[the host-import layer and the WASI shim](/docs/architecture/posix-syscalls).\n\n## Target: `wasm32-wasip1`\n\nEverything in the command suite is compiled to a single target,\n`wasm32-wasip1`: the WASI preview 1 ABI on the 32-bit WebAssembly architecture.\nPicking one target for the whole suite means a single libc, a single set of\nhost import declarations, and a single runtime shim can serve every command.\n\nA guest module built for this target expects standard WASI (preopened file\ndescriptors, clocks, randomness, file I/O) plus the extra agentOS import modules\ndescribed below. Both halves are satisfied at runtime by the kernel-backed\nruntime; nothing in a compiled command reaches a real host syscall.\n\n## Two source languages, two compilers\n\nThe suite is heterogeneous: most tools are Rust, some are C. Each language uses\nits own compiler driver, but both emit the same `wasm32-wasip1` ABI and link\nagainst the same sysroot, so the outputs are interchangeable at runtime.\n\n- **Rust coreutils** are built with **`cargo`** targeting `wasm32-wasip1`. Rust's\n standard library already has first-class support for this target, so the\n coreutils crates compile with an ordinary cross-compile invocation.\n- **C programs** are built with the **`wasi-sdk`** toolchain, a packaged\n `clang` plus sysroot tuned for WASI. C tools that have no Rust equivalent (or\n that are easier to carry as upstream C) go through this path.\n\n```bash\n# Rust coreutils\ncargo build --target wasm32-wasip1 --release\n\n# C programs via wasi-sdk, linked against the patched libc + wasi-ext\n$WASI_SDK/bin/clang --target=wasm32-wasip1 \\\n --sysroot=$WASI_SYSROOT \\\n -lwasi-ext \\\n tool.c -o tool.wasm\n```\n\n## What every binary links against\n\nRegardless of source language, each command links against the same two pieces.\nTogether they give a single binary both the standard WASI calls and the agentOS\nprocess / user / network extensions.\n\n- **A patched `wasi-libc`.** The libc is the WASI standard library, modified so\n that the calls a normal command-line program performs resolve against the\n agentOS surface instead of failing or hitting unimplemented stubs. This is the\n same patched libc the [Layer 2 shim](/docs/architecture/posix-syscalls) adapts at runtime; the\n build side and the runtime side are two ends of the same contract.\n- **The `wasi-ext` bindings.** These declare the extra WebAssembly import\n modules (`host_process`, `host_user`, `host_net`, and the small\n `host_sleep_ms` binding) that base WASI cannot express. Linking `wasi-ext`\n into a binary is what lets its libc emit `fork` / `exec`, `getuid` / `getgid`,\n and `connect` / `listen` as ordinary-looking syscalls that the host runtime\n then services through the kernel. See\n [Layer 1: custom host import modules](/docs/architecture/posix-syscalls) for the runtime half.\n\n\u003CAside type=\"note\">\nThe import declarations are compile-time only: linking `wasi-ext` tells the\nmodule *which* host imports to reference, but the calls are still routed through\nthe kernel and gated by the VM's [permission policy](/docs/permissions) at\nruntime. Building against `host_net` does not grant network access.\n\u003C/Aside>\n\n## From `.wasm` to a guest command\n\nThe compiler toolchain's product is a set of `.wasm` files, one per command.\nThose files are what the runtime mounts as the guest's executables: when a guest\ninvokes `ls`, `sh`, or any other bundled tool, the kernel resolves the name to\nthe corresponding module, instantiates it with the host imports and the WASI\nshim wired in, and runs it as a [child process](/docs/processes) with real\nprocess, user, and network semantics, all virtualized.\n\nThe same path is open to your own programs. A program you compile for\n`wasm32-wasip1` runs as a guest command exactly like the bundled ones; link the\n`wasi-ext` bindings if it needs processes, users, or sockets, and leave them out\nfor a pure-compute tool. Heavy native binaries that are not yet available as\nWASM belong in a [mounted sandbox](/docs/sandbox) instead.\n\n## Recommendations\n\n- Use the bundled WASM coreutils and `sh` for normal shell workloads; they\n already carry the patched libc and the `wasi-ext` extensions.\n- To ship your own command, compile it for `wasm32-wasip1` with `cargo` (Rust)\n or the `wasi-sdk` `clang` (C), and link `wasi-ext` only if it needs the\n process / user / network host imports.\n- Keep the build and runtime contracts aligned: the patched `wasi-libc` and the\n `wasi-ext` import declarations a binary is compiled against are the same ones\n the [WASM VM](/docs/architecture/posix-syscalls) runtime expects to satisfy.","src/content/docs/docs/architecture/compiler-toolchain.mdx","506cf678cbad8739","docs/architecture/filesystem",{"id":441,"data":443,"body":448,"filePath":449,"digest":450,"deferredRender":16},{"title":147,"description":444,"editUrl":16,"head":445,"template":18,"sidebar":446,"pagefind":16,"draft":20},"Internals of the kernel VFS: the overlay/mount/root engines, how guest fs syscalls are routed and confined, WASM preopens, and mount confinement against symlink and .. escapes.",[],{"hidden":20,"attrs":447},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis page is an internals deep-dive on the **kernel virtual filesystem (VFS)**: how it is layered, how a guest `fs` syscall is routed through it, and how guest I/O is confined to the VM. For the user-facing API (reading, writing, mounting, persistence), see [Filesystem](/docs/filesystem).\n\nThe invariant this whole subsystem exists to uphold: **every guest filesystem operation is serviced by the kernel-owned VFS, never by a real host capability.** There is no host disk reachable from the guest. The VFS presents normal Linux semantics to tools while keeping every byte inside the kernel.\n\n\u003CAside type=\"note\">The security boundary is sidecar to executor. The VFS lives inside the trusted sidecar; the guest in the executor only ever *asks* for a filesystem operation. Confinement is the kernel's job, not the guest's. See the [Security Model](/docs/security-model) for the full threat model.\u003C/Aside>\n\n## Where the VFS sits\n\nA guest `fs` call never touches the host. The path is always:\n\n```\nguest fs call (executor)\n -> kernel syscall (crosses sidecar \u003C-> executor boundary)\n -> VFS engine resolves the path\n -> backing store services the operation\n -> result returns to the executor\n```\n\n- The executor holds **no** filesystem capability of its own. It issues a syscall and blocks for the reply.\n- The kernel checks the applied permission policy for the filesystem scope before servicing the request.\n- The VFS resolves the path against the VM's layered engines, then services the operation against the engine that owns that path.\n\nBecause every byte is mediated here, two properties fall out for free: the guest can never reach the real host disk, and one VM's filesystem is never visible to another VM. Isolation is per-VM.\n\n## The VFS engines\n\nThe per-VM filesystem is not a single flat store. It is a tree of **engines**, each responsible for a subtree of the namespace. A path is resolved by walking from the root engine down to whichever engine owns the deepest matching prefix, then handing the remainder of the path to that engine.\n\n- **Root engine.** Owns `/` and the base namespace. Every VM boots with a root filesystem bootstrapped from a snapshot, so the guest starts against a populated POSIX tree (the default working directory is `/home/user`).\n- **Overlay engine.** Composes layers so writes land in a writable upper layer while reads fall through to a lower layer. This is how a read-mostly base can be presented as writable to the guest without mutating the shared lower layer.\n- **Mount engine.** Grafts a distinct backing store onto a guest path (a mount point). Below the mount point, operations are routed to that mount's backend instead of the parent engine. This is the mechanism behind in-memory, host-directory, S3, and Google Drive mounts.\n\nResolution is **longest-prefix wins**: if `/mnt/data` is a mount and the guest opens `/mnt/data/file`, the mount engine services it; anything outside `/mnt/data` stays with the parent (root/overlay) engine.\n\n```\n/ \u003C- root engine (bootstrapped from snapshot)\n|- home/user/... \u003C- root / overlay\n|- mnt/\n| |- scratch/... \u003C- mount engine -> in-memory backend\n| |- code/... \u003C- mount engine -> host-directory backend (read-only)\n| \\- data/... \u003C- mount engine -> S3 backend\n\\- ...\n```\n\nThe base layer is in-memory and per-VM; the runtime transparently persists it to backing storage so it survives sleep/wake. Mounts are pluggable: any guest path can be backed by the host, a remote, or a cloud store. See [Mounting filesystems](/docs/filesystem#mounting-filesystems) for the user-facing config.\n\n## Routing a guest syscall\n\nWhen the guest calls, say, `readFileSync(\"/mnt/data/report.csv\")`:\n\n1. **Permission check.** The kernel verifies the filesystem scope is granted for that operation. Nothing is bound by default; access is denied until opted in (see [Permissions](/docs/permissions)).\n2. **Engine resolution.** The VFS walks the namespace and selects the engine owning the longest matching prefix (`/mnt/data` -> the S3 mount engine).\n3. **Path normalization and confinement.** The remainder of the path is normalized within the owning engine's root. `.` and `..` segments are resolved *before* the operation reaches the backend, so the request cannot climb above the engine's root.\n4. **Backend operation.** The owning engine's backend services the read/write/stat/etc. against its store (in-memory pages, the persisted base, a host directory, S3, ...).\n5. **Reply.** The result crosses back to the executor, which unblocks.\n\nHost-side APIs (`agent.writeFile`, `agent.readFile`) enter the *same* VFS from the trusted side, which is why the host can seed and read files the guest sees, without ever exposing the real host disk to the guest.\n\n## Mount confinement\n\nA host-backed mount (host directory, S3, ...) comes from trusted config, so its existence, target, and credentials are not attack surface. What *is* in scope is the guest-driven traffic through it: the guest must not be able to use a mounted path to reach bytes outside the mount root. Confinement is enforced by the kernel, on every operation:\n\n- **`..` traversal.** Path segments are normalized relative to the mount root before the backend sees them. A guest path like `/mnt/code/../../etc/passwd` cannot resolve above the mount root; it is clamped to the mount's own subtree (and, above the mount point, handed back to the parent engine, which is itself the kernel VFS, not the host).\n- **Symlinks.** Symlink resolution (`realpath` following) is performed by the kernel against the *virtual* namespace, not the host's. A symlink inside a host-directory mount cannot be used to escape the mount root onto the wider host filesystem; the resolved target is re-confined to the mount root.\n- **Path aliasing / TOCTOU.** Because resolution and confinement happen inside the kernel on each operation, there is no window where the guest resolves a path and the backend later acts on a different one. The guest sees only the mounted subtree, never the wider host filesystem.\n\nMounts for host and remote backends are **read-only by default**; a writable mount must be opted into explicitly. The `readOnly` flag is enforced at the engine, so a write syscall to a read-only mount fails inside the kernel rather than reaching the backend.\n\n\u003CAside type=\"note\">\"Trusted mount, untrusted traffic\": the mount's target is trusted configuration, but the guest drives I/O through it, so confining guest operations to the mount root (`..`, symlink, TOCTOU, path-aliasing) is squarely in scope and enforced by the kernel.\u003C/Aside>\n\n## WASM preopens\n\nWASI does not grant a WASM guest an ambient filesystem. Instead, the host hands the module a set of **preopened directories**: capability handles to specific subtrees, and the guest can only reach paths reachable from a preopen.\n\nIn agentOS these preopens are wired to the **same kernel VFS** rather than to host directories:\n\n- A preopen maps a guest-visible path to a VFS subtree. File descriptors derived from it are serviced by the VFS engines above, with the same confinement rules.\n- The WASM guest therefore sees the virtualized filesystem (root snapshot, overlays, mounts) through standard WASI calls, with no host filesystem handle anywhere in the chain.\n- Confinement composes: a preopen rooted at a mount point inherits that mount's `..`/symlink confinement, because resolution still runs through the kernel VFS.\n\nThe result is that WASI filesystem access and the V8/Node `fs` path converge on one virtual filesystem, so both executor flavors get identical isolation and identical Linux semantics.\n\n## Where to go next\n\n- [Filesystem](/docs/filesystem): the user-facing API for reading, writing, mounting, and persistence.\n- [Architecture](/docs/architecture): the components, trust boundary, and kernel-owned syscall paths.\n- [Permissions](/docs/permissions): the filesystem scope the kernel checks on every operation.\n- [Security Model](/docs/security-model): the full trust model and threat boundary.","src/content/docs/docs/architecture/filesystem.mdx","57405a4cb6c16d65","docs/architecture/networking",{"id":451,"data":453,"body":459,"filePath":460,"digest":461,"deferredRender":16},{"title":454,"description":455,"editUrl":16,"head":456,"template":18,"sidebar":457,"pagefind":16,"draft":20},"Networking","How the kernel socket table works: a single VM-local transport that carries host, JavaScript, and WASM traffic, where fetch / net / dns route through it, how egress policy and loopback confinement are enforced, and how preview URLs are served.",[],{"hidden":20,"attrs":458},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis is the internals view of agentOS networking: the kernel socket table, the layers a request crosses, and where policy is enforced. For the user-facing API (`vmFetch`, preview URLs, the confinement model from a caller's perspective), see [Networking & Previews](/docs/networking). For the trust boundary this all sits inside, see [Architecture](/docs/architecture).\n\nThe governing rule is that there is exactly **one authoritative transport for everything VM-local**: the kernel socket table. No part of guest networking opens a real host socket on its own. Guest `fetch()`, `node:http`, `node:net`, WASM TCP clients and servers, and host-into-guest requests (`vmFetch` / `rt.fetch`) all target the same listener table.\n\n## The kernel socket table\n\nThe socket table is the floor of the stack and the only component that actually moves bytes between two in-VM endpoints. It is per VM, so two VMs never share a listener or a connection.\n\n- It exposes POSIX-style primitives: `socket_create`, `socket_bind_inet`, `socket_connect_inet_loopback`, `socket_read`, `socket_write`, `poll_targets`.\n- Every call is **owner-checked** (the calling process must own the descriptor) and **resource-accounted** against the VM's limits.\n- Failures return correct POSIX errnos (`ECONNREFUSED`, `EACCES`, …) so guest code branches the way it would on real Linux.\n- Connecting pairs two in-VM sockets and shuttles bytes between them. No host networking happens at this layer.\n\nBecause every server is a kernel TCP listener, a client never needs to know whether the server it is talking to is JS, WASM, raw TCP, or HTTP. HTTP is layered on top of kernel TCP bytes, so every listener lives in the one table and is reachable identically.\n\n\u003CAside type=\"note\">An earlier design carried two listener models at once: stream-mode listeners (`net.createServer`, WASM) on real kernel TCP sockets, and object-mode HTTP listeners (`http.createServer`) on a separate table that exchanged JSON request/response objects over stream events. A second guest process could not reach the object-mode table reliably, because the client expected byte-stream TCP semantics while the server only spoke object-mode dispatch. The current architecture removes the second model: everything is one socket table.\u003C/Aside>\n\n## The four layers\n\nA request passes through four layers. Only the top and bottom understand HTTP; the middle two move bytes and enforce policy.\n\n| Layer | Role | Trust | Lives in |\n| --- | --- | --- | --- |\n| 4 · Guest bridge | `node:http` / `node:net` / `fetch` / undici shim | untrusted (V8 isolate) | `crates/execution/assets/v8-bridge.source.js` |\n| 3 · Sync-RPC dispatch | routes `net.connect`, `net.http_request`, `net.listen`, … | trusted | `crates/sidecar/src/service.rs` |\n| 2 · Execution & enforcement | listener state, host fetch client, permission checks | trusted (TCB) | `crates/sidecar/src/execution.rs` |\n| 1 · Kernel socket table | `bind` / `listen` / `connect` / `read` / `write`, loopback routing | trusted (TCB floor) | `crates/kernel/src/socket_table.rs`, `kernel.rs` |\n\n### Layer 1: kernel socket table\n\n`crates/kernel/src/kernel.rs` exposes the primitives above. Loopback routing is the heart of VM-local networking: `socket_connect_inet_loopback` only succeeds against a socket that is actually bound and listening in the same VM's table; otherwise it returns `ECONNREFUSED`. Resource-limit checks run before the two sockets are paired.\n\n### Layer 2: sidecar execution (enforcement point / TCB)\n\n`crates/sidecar/src/execution.rs` is where policy is applied. Two roles matter for networking:\n\n- **Listener state.** `build_javascript_socket_path_context` walks every active process and records what is listening on which port, including a map of HTTP loopback targets keyed by `(family, port)`. This is the source of truth a connect consults to learn that, say, \"port 3000 is an HTTP server owned by process X, server Y.\"\n- **Host fetch client.** When the host calls `vmFetch` / `rt.fetch()`, the sidecar resolves the target to a VM-owned kernel listener, opens its own kernel socket, connects over loopback, and speaks HTTP/1.1 to the guest server. This is the only HTTP client that lives in the sidecar (the host has no guest isolate to do framing for it).\n\n### Layer 3: sync-RPC dispatch\n\n`crates/sidecar/src/service.rs` routes the bridge calls guest code makes. The guest-to-guest loopback HTTP path lands here as `net.http_request`. It is the most security-sensitive RPC, so it is guarded in order:\n\n1. The host must be a loopback address.\n2. The applied network policy must permit the operation.\n3. The requested `(process_id, server_id)` must match a listener that is currently live.\n\nThat last check stops a guest from forging a target to reach a process it should not.\n\n### Layer 4: guest bridge\n\n`crates/execution/assets/v8-bridge.source.js` is the Node-compatibility shim inside the untrusted V8 isolate. It presents `node:http`, `node:net`, `fetch`, and undici to guest code and translates them into Layer 3 bridge calls. `http.createServer()` is implemented on top of `net.Server`: each accepted byte socket is parsed as HTTP and dispatched to the guest's request handler.\n\n## How fetch, net, and dns route through it\n\n- **`node:net` (raw TCP).** `net.connect` / `net.createServer` map directly onto kernel `connect` / `bind` + `listen`. The bytes are the payload; no framing is added.\n- **`node:http` and `fetch`.** A guest HTTP server is a `net.Server` whose accepted sockets are HTTP-parsed in the bridge. A guest HTTP client runs undici over a kernel-backed dispatcher (or a raw serializer for the loopback fast path). Either way the bytes travel as kernel TCP.\n- **DNS.** Name resolution is serviced by the kernel resolver, not the host. Outbound connections that leave the VM resolve through it, and the resolved addresses are then filtered by the egress allowlist (see below). DNS pinning ties the connection to the address that was checked, closing the resolve-then-reconnect TOCTOU gap.\n\n### Where HTTP meets TCP\n\nThere is no shared HTTP/TCP translation module. Because the wire between every endpoint is raw TCP bytes through the kernel, HTTP is framed and deframed **at each edge that speaks HTTP**. The kernel (Layer 1) and the sidecar routing (Layer 2) never parse HTTP. There are three independent codecs, one per kind of endpoint:\n\n| Endpoint | Lives in | Encode / decode |\n| --- | --- | --- |\n| Guest HTTP server | guest bridge | `parseLoopbackRequestBuffer` (bytes to object), `serializeLoopbackResponse` (object to bytes), wired per accepted socket by `attachHttpServerSocket` |\n| Guest HTTP client | guest bridge | undici over a kernel-backed dispatcher, or `serializeRawHttpRequest` + `waitForRawHttpResponse` |\n| Host fetch client | sidecar execution | `serialize_kernel_http_fetch_request` (request to bytes), `parse_kernel_http_fetch_response` (bytes to JSON) |\n\nA WASM HTTP server or client does its own framing in guest code (reading the request line, writing a response with standard C socket calls). The kernel does not help it; it is just bytes, the same as for the JS endpoints.\n\n## Data flows\n\n- **Host to guest (`vmFetch` / `rt.fetch`).** The sidecar resolves the port to a VM-owned kernel listener, opens a sidecar-owned kernel socket, connects over loopback, serializes the request bytes, drives the target process forward so it can accept and respond, then parses the response bytes back into the host response object. It is **fail-closed**: no DNS, no external networking, no host-loopback fallback. If no VM-owned listener exists, it returns a missing-listener error.\n- **Guest to guest.** `net.connect` goes through the sidecar, which returns a loopback HTTP target handle. The guest sends the request through `net.http_request`, which dispatches into the target process's request handler. Cross-process loopback passes through the enforcement point rather than taking an in-isolate shortcut.\n- **Cross-runtime (JS and WASM, either direction).** Client and server connect through a kernel loopback socket pair and exchange raw bytes. JS to WASM, WASM to JS, and WASM to WASM all use the same path; only the side that runs the HTTP codec differs.\n- **Guest outbound to host or external.** Connections that do not target a VM-owned listener take the external network path: permission checks, DNS pinning, then a real host `TcpStream`. Reaching a host loopback port still requires an explicit loopback exemption entry.\n\n## Egress policy and loopback confinement\n\nGuest networking is confined by three distinct controls plus the loopback-only default. The permission policy and limits are **trusted configuration**; the guest executor is the **untrusted subject** they bind.\n\n### Loopback-only by default\n\nGuest listeners are reachable only over loopback (`127.0.0.1` / `::1`) inside the VM.\n\n- Binding to `0.0.0.0` or `::` does not widen this: the kernel normalizes the unspecified address down to loopback, so the listener still answers only on loopback.\n- A connection that originates outside the loopback interface and targets a port the VM does not own is refused with `EACCES`, noting the port is not exempt.\n- This confinement is independent of the permission policy. Even with the network allowed, a guest server stays loopback-only unless its port is explicitly exempted.\n\n### Three stacked controls\n\nThese are often conflated but are separate. They stack, and a request must pass every one that applies:\n\n1. **Permission policy** (`network.listen` / `network.connect`). Decides whether the guest may open a listener or initiate an outbound connection at all. A blocked operation fails with `blocked by network.listen policy` or `blocked by network.connect policy`.\n2. **Loopback confinement.** Decides who may reach an already-permitted guest listener. By default only loopback inside the VM; a per-port exemption loosens it.\n3. **DNS / egress allowlist.** Constrains where permitted outbound connections may go. The kernel filters resolved addresses, blocking outbound access to restricted ranges, so an allowed `connect` can still be refused by destination.\n\nThe per-port loopback exemption belongs to layer 2 only. It is a trusted, per-port whitelist that *loosens* the default loopback confinement (for example, exposing an in-VM dev server beyond loopback). It is not an egress control and grants no outbound reach; layers 1 and 3 still apply. It is configured with `loopbackExemptPorts`, a list of ports that are exempt from the SSRF checks at layer 2; each listed port is reachable from outside the loopback interface, while the permission policy and egress allowlist continue to apply.\n\n### Trust and ownership\n\nEvery guest connect, listen, read, and write passes through sidecar ownership and kernel owner checks. Guest-to-guest loopback is allowed only when the destination is a VM-owned listener and the applied network policy permits the connect. Host-loopback access from guest code is separate and still requires a loopback exemption plus the applied network policy. Long-lived waits must not block the sync-RPC path, so the stack uses stream events, bounded polling, and kernel socket waits with explicit timeouts.\n\n\u003CAside type=\"note\">Host-to-guest requests bypass egress, not the table. `vmFetch` / `rt.fetch` terminate at the guest's loopback listener and never leave the VM, so they work even when guest egress (layer 3) or outbound `connect` (layer 1) is denied. They are host control-plane traffic, not guest egress, and only ever reach VM-owned listeners, while still going through the same kernel socket table as everything else.\u003C/Aside>\n\n## Preview URLs\n\nA preview URL is port forwarding for a VM service: a time-limited, signed, publicly reachable URL that proxies HTTP to a port inside the VM. Mechanically it reuses the host-to-guest path:\n\n- A signed token is minted for a `(VM, port)` pair with an expiration, capped by `preview.maxExpiresInSeconds`. Tokens are stored in SQLite, survive sleep/wake cycles, and expired ones are cleaned up automatically.\n- An incoming request to the preview path is authenticated against the token, then proxied into the VM exactly like `vmFetch`: resolve the port to a VM-owned kernel listener, connect over loopback, frame HTTP/1.1, drive the target process, and stream the response back. The same fail-closed, VM-owned-listener-only rules apply.\n- CORS is enabled so browsers can reach preview URLs from any origin.\n- Revocation (`expireSignedPreviewUrl`) invalidates the token immediately, after which the proxy refuses the request before touching the socket table.\n\nBecause previews ride the host fetch path, they are subject to loopback confinement at the kernel but **not** to the guest egress allowlist: the request enters the listener from the host side and never becomes guest outbound traffic.\n\n## Where to go next\n\n- [Networking & Previews](/docs/networking): the `vmFetch` and preview URL API, with usage examples.\n- [Architecture](/docs/architecture): the client / sidecar / executor trust boundary this stack lives inside.\n- [Security Model](/docs/security-model): the full in-scope and out-of-scope threat model.","src/content/docs/docs/architecture/networking.mdx","cb88fbadc029bce5","docs/architecture/posix-syscalls",{"id":462,"data":464,"body":470,"filePath":471,"digest":472,"deferredRender":16},{"title":465,"description":466,"editUrl":16,"head":467,"template":18,"sidebar":468,"pagefind":16,"draft":20},"POSIX Syscalls","How agentOS extends WASI in two layers so WebAssembly guests behave like normal POSIX programs on top of the kernel.",[],{"hidden":20,"attrs":469},{},"import { Aside } from '@astrojs/starlight/components';\n\nNot everything inside an agentOS VM is JavaScript. The shell (`sh`) and the\ncoreutils behind [process execution](/docs/processes) ship as WebAssembly\nbinaries, and you can run your own WASM programs too. To make those programs\nbehave like normal Linux tools, agentOS presents a POSIX syscall surface on top\nof WebAssembly.\n\n- **WASM is a first-class guest.** WASM binaries run beside JavaScript inside the same VM.\n- **Same kernel, same boundary.** WASM syscalls route through the same kernel that backs JS guests, so there is no extra host access.\n- **POSIX shape, not host access.** The extensions below add process, user, and network *semantics*, all virtualized.\n\n## Why WASI alone is not enough\n\nThe base standard for WASM system access is **WASI** (specifically `wasip1`).\nWASI is intentionally minimal:\n\n- It gives a guest preopened file descriptors, clocks, randomness, and basic file I/O.\n- It has **no process model** (no `fork` / `exec` / `wait`).\n- It has **no users or groups** (no `getuid` / `getgid`).\n- It has **no general sockets** (no `connect` / `listen`).\n\nReal command-line programs expect all of those. agentOS closes the gap in two\nlayers, and both route through the kernel rather than the host.\n\n\u003CAside type=\"note\">\nEvery WASM syscall, like every JS syscall, goes through the kernel-owned virtual\nfilesystem, process table, and socket table. The extensions below add POSIX\n*shape*; they do not add host access. See the [Security Model](/docs/security-model)\nfor the isolation boundary.\n\u003C/Aside>\n\n## The two-layer model\n\nagentOS layers a POSIX surface over WASM. Layer 1 adds capabilities WASI does\nnot express at all; Layer 2 adapts the standard WASI calls so a normal libc\nbehaves correctly inside the VM. Both bottom out in the kernel.\n\n\u003Csvg viewBox=\"0 0 700 360\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"Two-layer WASM-on-kernel model\" style=\"max-width: 700px; width: 100%; height: auto; font-family: ui-sans-serif, system-ui, sans-serif;\">\n \u003Crect x=\"0\" y=\"0\" width=\"700\" height=\"360\" fill=\"#ffffff\" />\n\n {/* Guest */}\n \u003Crect x=\"40\" y=\"20\" width=\"620\" height=\"56\" rx=\"8\" fill=\"#f4f4f5\" stroke=\"#d4d4d8\" />\n \u003Ctext x=\"350\" y=\"44\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"600\" fill=\"#18181b\">WASM guest (sh, coreutils, your .wasm)\u003C/text>\n \u003Ctext x=\"350\" y=\"64\" text-anchor=\"middle\" font-size=\"12\" fill=\"#52525b\">compiled for wasm32-wasip1, linked against patched wasi-libc\u003C/text>\n\n {/* Layer 1 */}\n \u003Crect x=\"40\" y=\"100\" width=\"300\" height=\"120\" rx=\"8\" fill=\"#eef2ff\" stroke=\"#c7d2fe\" />\n \u003Ctext x=\"190\" y=\"124\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"600\" fill=\"#3730a3\">Layer 1: host import modules\u003C/text>\n \u003Ctext x=\"190\" y=\"148\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_process — spawn / wait\u003C/text>\n \u003Ctext x=\"190\" y=\"168\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_user — uid / gid\u003C/text>\n \u003Ctext x=\"190\" y=\"188\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_net — TCP sockets\u003C/text>\n \u003Ctext x=\"190\" y=\"208\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_sleep_ms — blocking sleep\u003C/text>\n\n {/* Layer 2 */}\n \u003Crect x=\"360\" y=\"100\" width=\"300\" height=\"120\" rx=\"8\" fill=\"#ecfdf5\" stroke=\"#a7f3d0\" />\n \u003Ctext x=\"510\" y=\"124\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"600\" fill=\"#065f46\">Layer 2: kernel-backed WASI shim\u003C/text>\n \u003Ctext x=\"510\" y=\"148\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">stdio through the kernel bridge\u003C/text>\n \u003Ctext x=\"510\" y=\"168\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">mounts mirrored as preopens\u003C/text>\n \u003Ctext x=\"510\" y=\"188\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">read-only tiers enforced\u003C/text>\n \u003Ctext x=\"510\" y=\"208\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">paths confined to their mount\u003C/text>\n\n {/* Arrows down to kernel */}\n \u003Cline x1=\"190\" y1=\"220\" x2=\"190\" y2=\"280\" stroke=\"#71717a\" stroke-width=\"2\" marker-end=\"url(#arrow)\" />\n \u003Cline x1=\"510\" y1=\"220\" x2=\"510\" y2=\"280\" stroke=\"#71717a\" stroke-width=\"2\" marker-end=\"url(#arrow)\" />\n\n {/* Kernel */}\n \u003Crect x=\"40\" y=\"284\" width=\"620\" height=\"56\" rx=\"8\" fill=\"#fafafa\" stroke=\"#d4d4d8\" />\n \u003Ctext x=\"350\" y=\"308\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"600\" fill=\"#18181b\">Kernel: virtual filesystem, process table, socket table\u003C/text>\n \u003Ctext x=\"350\" y=\"328\" text-anchor=\"middle\" font-size=\"12\" fill=\"#52525b\">same paths that back JavaScript guests — no host escape\u003C/text>\n\n \u003Cdefs>\n \u003Cmarker id=\"arrow\" markerWidth=\"10\" markerHeight=\"10\" refX=\"6\" refY=\"3\" orient=\"auto\" markerUnits=\"strokeWidth\">\n \u003Cpath d=\"M0,0 L6,3 L0,6 Z\" fill=\"#71717a\" />\n \u003C/marker>\n \u003C/defs>\n\u003C/svg>\n\n## Layer 1: custom host import modules\n\nStandard WASI cannot express `fork` / `exec`, `getuid`, or `connect`. agentOS\ndeclares extra WebAssembly import modules that the host runtime implements, so\nguest libc can call them as if they were ordinary syscalls. These bindings live\nin the `wasi-ext` crate and cover three areas:\n\n- **`host_process`**: process management. Spawn a child process (argv, env, inherited stdio fds, working directory), wait for a child to exit, and related file-descriptor operations. This is what gives a WASM `sh` real [child process](/docs/processes) semantics; spawns go through the kernel process table.\n- **`host_user`**: user and group identity (uid, gid, user info). Base WASI has no concept of a user; this lets tools that call `getuid` / `getgid` see the VM's virtualized identity.\n- **`host_net`**: TCP sockets (connect, listen, send, receive) through the kernel socket table, gated by the same [network permission policy](/docs/networking) as everything else. Base WASI has no general socket API.\n\nA small `host_sleep_ms` binding provides blocking sleep. Together these let a\nguest compiled for `wasip1` behave as if it had a process model, user identity,\nand a network, all virtualized.\n\n```c\n// Imported from the host runtime, declared by the wasi-ext bindings.\n// Guest libc calls these as if they were ordinary syscalls.\n__attribute__((import_module(\"host_process\"), import_name(\"proc_spawn\")))\nint host_proc_spawn(const char *argv, const char *envp, int cwd_fd);\n\n// getuid returns an errno; the uid is written through the out-pointer.\n__attribute__((import_module(\"host_user\"), import_name(\"getuid\")))\nint host_getuid(unsigned int *ret_uid);\n\n__attribute__((import_module(\"host_net\"), import_name(\"net_connect\")))\nint host_net_connect(int fd, const char *addr, int addr_len);\n```\n\n## Layer 2: the kernel-backed WASI shim\n\nThe second layer adapts the standard WASI calls themselves so that programs\nbuilt against a normal libc behave correctly inside the VM. The embedded shim:\n\n- **Routes stdio through the kernel.** `fd_read` / `fd_write` on the standard descriptors go through the kernel stdio bridge rather than host file descriptors, so output stays inside the VM and honors PTYs and redirection.\n- **Fills in libc expectations.** For example `fcntl(F_SETFL)` is serviced via `fd_fdstat_set_flags`, so flag changes that libc performs do not fail.\n- **Mirrors mounts as preopens.** The preopen table reflects the VM's guest path mappings, so mounted directories are visible to WASM path resolution exactly as they are to JS and to `node:fs`.\n- **Enforces read-only tiers.** `path_open` rejects create / truncate / write flags on read-only mounts while still allowing non-mutating opens (directory traversal, `O_DIRECTORY`), so read-only mounts stay read-only without breaking `find`, `ls`, and friends.\n- **Confines paths to their mount.** Targets are resolved beneath the specific preopen's root, so `..` segments cannot escape one mount into a sibling mount or a host path.\n\n```\nfd_read(0) -> kernel stdio bridge (not a host fd)\nfcntl(fd, F_SETFL) -> fd_fdstat_set_flags (libc flag changes succeed)\npath_open(\"/data/x\") -> resolved under the /data preopen root\npath_open(..O_CREAT) -> rejected on a read-only mount\npath_open(\"../../etc\")-> stays inside the mount; cannot escape\n```","src/content/docs/docs/architecture/posix-syscalls.mdx","59df167c76ec6de4","docs/architecture/processes",{"id":473,"data":475,"body":481,"filePath":482,"digest":483,"deferredRender":16},{"title":476,"description":477,"editUrl":16,"head":478,"template":18,"sidebar":479,"pagefind":16,"draft":20},"Processes","Internals of the kernel process model: the virtual process table, how spawns are serviced, stdio bridging, PTYs, and how WASM sh and coreutils map onto it.",[],{"hidden":20,"attrs":480},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis page is an internals deep-dive on the kernel's **process model**: the data\nstructures and syscall paths behind every guest process. For the client-facing\nAPI (`exec`, `spawn`, `openShell`, lifecycle, the process tree), see\n[Processes & Shell](/docs/processes). For the surrounding component and trust\nmodel, see [Architecture](/docs/architecture).\n\nTwo invariants frame everything below:\n\n- **No real host process is ever spawned for guest work.** Every guest process is an entry in a kernel-owned virtual process table, not an OS process. Guest JavaScript runs in V8 isolates; guest commands like `sh` and coreutils run as WebAssembly. Neither is `node` or a host binary.\n- **Every process operation is a syscall into the kernel.** Spawning, waiting, signaling, reading stdout, and resizing a PTY all cross from the untrusted executor into the sidecar-owned kernel, which services them against virtualized resources.\n\n## The virtual process table\n\nEach VM owns one process table. It is the authority for what is \"running\"\ninside that VM; nothing in it corresponds to a host PID.\n\n- **Per-VM and isolated.** Two VMs have two independent tables. A PID in one VM is meaningless in another, and processes are never visible across the VM boundary.\n- **Holds every guest process,** not only the ones a client started explicitly. A `spawn` from the client, a child spawned by guest `node:child_process`, and the processes behind a shell pipeline are all table entries. This is why the system-wide views (`allProcesses`, `processTree`) can show more than what the client launched.\n- **Tracks lifecycle and lineage.** Each entry carries its PID, the command and arguments, parent PID (so the tree can be reconstructed), running/exited status, exit code once collected, and its attached stdio endpoints.\n- **Records a driver.** An entry knows which execution backend services it (for example a V8 isolate versus a WASM runtime). This is the `driver` field surfaced on `allProcesses`. Drivers differ in *how* the code runs; they share the same table, the same kernel-owned stdio, and the same boundary.\n\n\u003CAside type=\"note\">The process table is part of the kernel the sidecar owns. The executor never mutates it directly; it can only ask the kernel to create, wait on, or signal an entry. That request-only relationship is the sidecar-to-executor boundary applied to processes.\u003C/Aside>\n\n## How a spawn is serviced\n\nA spawn, whether it originates from a client `spawn`/`exec` call or from guest\n`node:child_process`, follows one path through the kernel:\n\n1. **The request crosses into the kernel.** A client call arrives over the wire protocol; a guest call arrives as a syscall from the executor. Either way the kernel, not the caller, performs the work.\n2. **Permission check.** The kernel applies the VM's permission policy before doing anything. Process execution is denied by default and must be granted; the policy is trusted input, the guest making the request is not.\n3. **Resolve the program.** The command is resolved against the VM's virtual filesystem (PATH lookup over the VFS), not the host. The resolved program decides the driver: a JavaScript entrypoint runs in a V8 isolate; a `.wasm` program (including `sh` and coreutils) runs on the WASM runtime.\n4. **Allocate the table entry.** The kernel assigns a virtual PID, records the command, arguments, environment, working directory, and parent PID, and links stdio endpoints (see below).\n5. **Start execution.** The driver begins running the program. For a one-shot `exec` the kernel additionally collects stdout, stderr, and the exit code and returns them as the call's result; for `spawn` it leaves the process running and streams output via events.\n6. **Reap and record exit.** When the program finishes, the kernel records the exit code on the table entry and marks it exited, which is what a `wait`/`waitProcess` resolves against and what `processExit` reports.\n\nSignals (`stopProcess` / SIGTERM, `killProcess` / SIGKILL) are the same shape: a\nrequest into the kernel, which applies it to the virtualized process rather than\nto any host process.\n\n## Stdio bridging\n\nStandard streams are kernel-owned objects, not host file descriptors. Each\nprocess entry has stdin, stdout, and stderr endpoints that the kernel wires up\nwhen the entry is created.\n\n- **Capture vs. stream.** For `exec`, the kernel buffers stdout and stderr and hands them back when the process exits. For `spawn`, output is delivered incrementally as `processOutput` events tagged with the PID and the stream (`stdout`/`stderr`), and `processExit` signals completion.\n- **Writable stdin.** `writeProcessStdin` pushes bytes into the process's stdin endpoint; `closeProcessStdin` closes the write side so programs that read to EOF (like `cat`) can finish. None of this touches a real pipe on the host.\n- **Pipes between processes.** Shell pipelines (`a | b`) connect one process's stdout endpoint to the next process's stdin endpoint through kernel-owned pipes. The pipe is a virtual object in the kernel, so a pipeline behaves like Linux without any host IPC.\n\nBecause these endpoints are kernel objects, the same bridging works identically\nwhether the process is a V8 isolate or a WASM program; the driver writes to and\nreads from kernel stdio, not from anything host-provided.\n\n## PTYs and interactive shells\n\nAn interactive shell needs a terminal, not just piped stdio: line editing, job\ncontrol signals, and window size all depend on a PTY. The kernel provides\nvirtual PTY devices for this.\n\n- **A shell is a process plus a PTY.** `openShell` allocates a kernel PTY and starts a shell process attached to it, returning a `shellId`. The PTY is a virtualized terminal device, never a host `/dev/pts` entry.\n- **Bidirectional terminal I/O.** `writeShell` feeds keystrokes into the PTY master side; everything the shell and its children emit comes back as `shellData` events. This carries terminal control sequences, so full-screen TUIs behave correctly.\n- **Resize is a terminal operation.** `resizeShell` updates the PTY's window size (columns and rows), which the kernel propagates to the foreground process the way a real terminal resize would, so programs relying on `TIOCGWINSZ`-style sizing redraw correctly.\n- **Teardown.** `closeShell` tears down the PTY and the attached shell process. An open shell keeps the VM active, the same way an open PTY keeps a session alive on a real system.\n\n## WASM sh and coreutils on the process model\n\nThe shell and the standard commands behind process execution are not special\nhost helpers; they are ordinary guest processes that happen to be WebAssembly.\nFor the full WASM execution model see [WASM VM](/docs/architecture/posix-syscalls); here is how it\nmaps onto the process table specifically.\n\n- **They are normal table entries.** Running `sh`, `ls`, `cat`, etc. allocates virtual PIDs and table entries exactly like any other process, with the WASM driver recorded on each. A pipeline of coreutils is several entries linked by kernel pipes.\n- **POSIX process semantics are virtualized, not borrowed from the host.** Plain WASI has no process model (no `fork`/`exec`/`wait`). agentOS supplies those semantics through kernel-backed host imports, so a WASM program that spawns and waits on a child drives the *same* kernel process table that JS guests use. A coreutil spawning a subcommand is one table entry creating another.\n- **Same stdio, same PTY.** WASM processes read and write the kernel stdio endpoints described above, and a shell built from WASM `sh` attaches to a kernel PTY just like any interactive shell. The driver differs; the kernel-owned plumbing does not.\n\nThis is why the process model is uniform: whether an entry is a V8 isolate or a\nWASM binary, it lives in the same per-VM table, goes through the same\npermission-checked spawn path, and uses the same kernel-owned stdio and PTYs.\n\n## See also\n\n- [Processes & Shell](/docs/processes): the client API for running and managing processes.\n- [WASM VM](/docs/architecture/posix-syscalls): how WebAssembly guests get POSIX process, user, and network semantics.\n- [Architecture](/docs/architecture): components, the trust boundary, and the request lifecycle.\n- [Permissions](/docs/permissions): the policy the kernel checks on every spawn.","src/content/docs/docs/architecture/processes.mdx","da5a629c2d1b5a3c"] \ No newline at end of file +[["Map",1,2,9,10],"meta::meta",["Map",3,4,5,6,7,8],"astro-version","5.18.2","content-config-digest","3edc204469c72622","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"site\":\"https://agentos-sdk.dev\",\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"where\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":false,\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[null,null,null],\"rehypePlugins\":[null,[null,{\"experimentalHeadingIdCompat\":false}],null,[null,{\"themes\":[\"github-dark-default\"],\"defaultLocale\":\"en\",\"cascadeLayer\":\"starlight.components\",\"styleOverrides\":{\"borderRadius\":\"0.75rem\",\"borderWidth\":\"1px\",\"codePaddingBlock\":\"0.75rem\",\"codePaddingInline\":\"1rem\",\"codeFontFamily\":\"\\\"JetBrains Mono\\\", ui-monospace, SFMono-Regular, Menlo, monospace\",\"codeFontSize\":\"var(--sl-text-code)\",\"codeLineHeight\":\"var(--sl-line-height)\",\"uiFontFamily\":\"var(--__sl-font)\",\"textMarkers\":{\"lineDiffIndicatorMarginLeft\":\"0.25rem\",\"defaultChroma\":\"45\",\"backgroundOpacity\":\"60%\"},\"borderColor\":\"rgba(244, 241, 231, 0.1)\",\"codeBackground\":\"#0a0a0a\",\"frames\":{\"editorTabBarBackground\":\"#111110\",\"editorTabBarBorderBottomColor\":\"rgba(244, 241, 231, 0.1)\",\"editorActiveTabBackground\":\"#0a0a0a\",\"editorActiveTabIndicatorBottomColor\":\"#cb5a33\",\"terminalTitlebarBackground\":\"#111110\",\"terminalBackground\":\"#0a0a0a\",\"frameBoxShadowCssValue\":\"none\"}},\"plugins\":[{\"name\":\"Starlight Plugin\",\"hooks\":{}},{\"name\":\"astro-expressive-code\",\"hooks\":{}}],\"removeUnusedThemes\":false}]],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[],\"actionBodySizeLimit\":1048576},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false,\"svgo\":false},\"legacy\":{\"collections\":false},\"prefetch\":{\"prefetchAll\":true},\"i18n\":{\"defaultLocale\":\"en\",\"locales\":[\"en\"],\"routing\":{\"prefixDefaultLocale\":false,\"redirectToDefaultLocale\":false,\"fallbackType\":\"redirect\"}}}","docs",["Map",11,12,25,26,36,37,47,48,58,59,69,70,80,81,91,92,102,103,113,114,124,125,9,135,144,145,155,156,166,167,177,178,188,189,199,200,210,211,221,222,232,233,243,244,254,255,265,266,276,277,287,288,298,299,309,310,320,321,331,332,342,343,353,354,364,365,375,376,386,387,397,398,408,409,419,420,430,431,441,442,451,452,462,463,473,474,484,485,495,496],"docs/agent-to-agent",{"id":11,"data":13,"body":22,"filePath":23,"digest":24,"deferredRender":16},{"title":14,"description":15,"editUrl":16,"head":17,"template":18,"sidebar":19,"pagefind":16,"draft":20},"Agent-to-Agent Communication","Use bindings to let agents communicate with each other.",true,[],"doc",{"hidden":20,"attrs":21},false,{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nAgents communicate through [bindings](/docs/bindings). You define a bindings group that lets one agent send work to another, and the agent calls it like any other CLI command.\n\n## Example: code writer + reviewer\n\nThis example gives the writer agent a `review` binding. The writer sends the file's full contents (the VMs share no filesystem), and the binding writes them into a separate reviewer VM and sends a review prompt back through the reviewer.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport { z } from \"zod\";\n\n// The reviewer is its own isolated agent VM.\nconst reviewer = agentOS({});\n\n// Bridge the writer to the reviewer. The VMs share no filesystem, so the writer\n// sends the full file contents; the bridge writes them into the reviewer's VM\n// and asks the reviewer to review. Runs on the host.\nasync function reviewCode(code: string): Promise\u003Cstring> {\n const client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n const reviewerHandle = client.reviewer.getOrCreate(\"my-project\");\n\n // Write the submitted contents into the reviewer's VM.\n await reviewerHandle.writeFile(\"/home/agentos/review.ts\", code);\n\n // Ask the reviewer to review.\n const session = await reviewerHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n const result = await reviewerHandle.sendPrompt(\n session.sessionId,\n \"Review the code at /home/agentos/review.ts and list any issues.\",\n );\n await reviewerHandle.closeSession(session.sessionId);\n\n return result.text;\n}\n\n// The writer agent gets a `review` binding. When the writer runs\n// `agentos-review submit`, the bridge above executes on the host.\nconst writer = agentOS({\n bindings: [\n {\n name: \"review\",\n description: \"Send code to the reviewer agent and get back a review.\",\n bindings: {\n submit: {\n description:\n \"Submit the full contents of a file to the reviewer agent for review. Returns the reviewer's feedback as text.\",\n inputSchema: z.object({\n code: z.string().describe(\"The full source code to review.\"),\n }),\n execute: async (input: { code: string }) => ({\n review: await reviewCode(input.code),\n }),\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { writer, reviewer } });\n\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst writerAgent = client.writer.getOrCreate(\"my-project\");\n\nconst session = await writerAgent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// The writer calls the `review` binding, which bridges to the reviewer VM.\nawait writerAgent.sendPrompt(\n session.sessionId,\n \"Write a small REST API, then send it to the review agent for review.\",\n);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/agent-to-agent)*\n\nThe writer agent sees the review binding as a CLI command. Because the VMs share no filesystem, it sends the full file contents, not a path:\n\n```bash\nagentos-review submit --code \"$(cat api.ts)\"\n```\n\nThe binding writes the contents into the reviewer's VM, prompts the reviewer, and returns the review to the writer as JSON.\n\n## Why bindings?\n\nBindings are the natural communication layer between agents because:\n\n- **The agent doesn't need to know about other agents.** It just calls a binding. You can swap the implementation without changing the agent's behavior.\n- **No credentials in the VM.** The binding executes on the server, so it can access other agents directly without exposing connection details.\n- **Composable.** Chain any number of agents by adding more bindings. Each binding is a self-contained bridge to another agent.\n\n## Recommendations\n\n- Each agent has its own isolated VM and filesystem (they share no filesystem). Pass file contents through the binding input, then use `writeFile` in the binding to land them in the other VM.\n- Use [Workflows](/docs/workflows) to make multi-agent pipelines durable across restarts.","src/content/docs/docs/agent-to-agent.mdx","8d4b44e6ae7cf0d0","docs/approvals",{"id":25,"data":27,"body":33,"filePath":34,"digest":35,"deferredRender":16},{"title":28,"description":29,"editUrl":16,"head":30,"template":18,"sidebar":31,"pagefind":16,"draft":20},"Approvals","Approve or deny agent tool use with human-in-the-loop or auto-approve patterns.",[],{"hidden":20,"attrs":32},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nWhen an agent wants to use a tool (write a file, run a command, etc.), it asks for permission. You approve or deny that request, either interactively or with a server-side hook.\n\n- **Human-in-the-loop**: subscribe to `permissionRequest` on the client and respond per-request.\n- **Auto-approve**: use the `onPermissionRequest` server hook to decide without a client round-trip.\n- **Selective approval**: inspect the request and approve some, forward others to the client.\n\n## Permission request flow\n\nWhen an agent wants to use a tool, it emits a `permissionRequest`. Every request is delivered to two places at once, and you respond from whichever fits your app:\n\n- **On the client**: subscribe to the `permissionRequest` event and call `respondPermission(sessionId, permissionId, reply)`.\n- **On the server**: the `onPermissionRequest` hook on the actor runs for every request, with no client round-trip.\n- If neither responds, the request blocks until a reply arrives, then rejects after 120 seconds.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Listen for permission requests over a live connection. The payload is\n// inferred from the actor's event schema, so no cast is needed.\nconst conn = agent.connect();\nconn.on(\"permissionRequest\", async (data) => {\n console.log(\"Permission requested:\", data.request);\n\n // Approve this single request.\n await agent.respondPermission(\n data.sessionId,\n data.request.permissionId,\n \"once\",\n );\n});\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Create a new file at /home/agentos/output.txt\");\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n // Runs server-side for every permission request, before any client round-trip.\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"permission requested:\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\nThe `permissionRequest` event payload:\n\n- **`data.sessionId`**: the session the request belongs to.\n- **`data.request.permissionId`**: the id to pass back to `respondPermission`.\n- **`data.request.description`**: human-readable summary of the requested action.\n- **`data.request.params`**: raw ACP permission details (requested tool, paths, etc.).\n\nReply options for `respondPermission`:\n\n| Reply | Behavior |\n|-------|----------|\n| `\"once\"` | Approve this single request |\n| `\"always\"` | Approve this and all future requests of the same type |\n| `\"reject\"` | Deny the request |\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/approvals)*\n\n## Patterns\n\n### Auto-approve\n\nThe `onPermissionRequest` hook runs server-side for every permission request before it reaches any client. Useful for fully automated pipelines.\n\n- **Signature**: `onPermissionRequest: async (sessionId, request) => { ... }`.\n- **Inspect**: `request.permissionId`, `request.description`, and `request.params`.\n- **Anything not handled** in the hook is forwarded to the client via the `permissionRequest` event.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n // The onPermissionRequest hook runs server-side for every request before it\n // is forwarded to clients. Use it to inspect requests in fully automated\n // pipelines without a client round-trip.\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"auto-approving\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// No need to handle permissions on the client. The server hook handles them.\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Write files as needed\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/approvals)*\n\n### Selective approval\n\nInspect the permission request to make approval decisions based on the tool or path. Approve some server-side, forward the rest to the client for human review.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n onPermissionRequest: async (sessionId, request) => {\n // `request.description` and `request.params` carry the raw ACP permission\n // details (the requested tool, paths, etc.). Inspect them to decide which\n // requests to handle server-side and which to forward to clients.\n const description = request.description ?? \"\";\n if (description.toLowerCase().includes(\"read\")) {\n console.log(\"read request handled server-side\", sessionId, request.permissionId);\n }\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Permission requests forwarded by the server reach the client here. The\n// payload is inferred from the actor's event schema, so no cast is needed.\nconst conn = agent.connect();\nconn.on(\"permissionRequest\", async (data) => {\n const approved = confirm(`Allow: ${JSON.stringify(data.request)}?`);\n await agent.respondPermission(\n data.sessionId,\n data.request.permissionId,\n approved ? \"once\" : \"reject\",\n );\n});\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Read config.json and update it\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/approvals)*\n\n- For interactive applications, subscribe to `permissionRequest` on the client and build an approval UI.\n- If neither the server hook nor the client responds, the agent blocks until a response is given or the action times out.","src/content/docs/docs/approvals.mdx","d19f1d65b985e474","docs/architecture",{"id":36,"data":38,"body":44,"filePath":45,"digest":46,"deferredRender":16},{"title":39,"description":40,"editUrl":16,"head":41,"template":18,"sidebar":42,"pagefind":16,"draft":20},"Overview","A high-level tour of how agentOS works: the client / server / VM picture, the anatomy of a Linux VM (kernel + executor), agents and sessions, and the Rivet Actor orchestration underneath.",[],{"hidden":20,"attrs":43},{},"import { Aside } from '@astrojs/starlight/components';\n\nagentOS runs AI agents and untrusted code safely inside fully virtualized Linux VMs. Nothing the guest does touches your host directly: there is no real host filesystem, no real host network socket, and no real host process. Every guest operation is serviced by a kernel that agentOS owns.\n\nThis page is a high-level tour. It walks through the overall shape, the parts that make up a VM, how agent sessions work, and the orchestration layer underneath. Each section links out to a detailed page when you want to go deeper.\n\n## The big picture\n\nA running agentOS system has three roles: your **app** (the client), your **server** (which runs the sidecar that hosts the VMs), and the **VM** where guest code actually runs. Your app never runs guest code itself, it asks the server to.\n\n\u003Csvg viewBox=\"0 0 400 210\" role=\"img\" aria-label=\"A client (JavaScript, browser, or another backend) connects to an agentOS server. The server runs a sidecar that hosts many isolated VMs, each marked with the agentOS 'OS' logo; the sidecar brokers all guest syscalls and isolates each agent.\" style=\"width:100%;height:auto;max-width:420px;display:block;margin:2.5rem auto 0.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"bp-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003Csymbol id=\"bp-os\" viewBox=\"0 0 100 100\">\n \u003Crect x=\"8\" y=\"8\" width=\"84\" height=\"84\" rx=\"26\" fill=\"none\" stroke=\"#1b1916\" stroke-width=\"8\" />\n \u003Ctext x=\"50\" y=\"50\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"38\" fill=\"#1b1916\">OS\u003C/text>\n \u003C/symbol>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"67\" width=\"140\" height=\"60\" rx=\"12\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"82\" y=\"92\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"15\" font-weight=\"600\" fill=\"#1b1916\">Client\u003C/text>\n \u003Ctext x=\"82\" y=\"112\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">JS · Browser · Backend\u003C/text>\n \u003Cline x1=\"154\" y1=\"97\" x2=\"205\" y2=\"97\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#bp-arrow)\" />\n \u003Crect x=\"210\" y=\"40\" width=\"164\" height=\"114\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"224\" y=\"62\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Server\u003C/text>\n \u003Cg fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\">\n \u003Crect x=\"224\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"224\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003C/g>\n \u003Cg>\n \u003Cuse href=\"#bp-os\" x=\"229\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"265\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"301\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"337\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"229\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"265\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"301\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#bp-os\" x=\"337\" y=\"117\" width=\"18\" height=\"18\" />\n \u003C/g>\n \u003Cg>\n \u003Crect x=\"150\" y=\"170\" width=\"15\" height=\"15\" rx=\"4\" fill=\"none\" stroke=\"#56524a\" stroke-width=\"1.4\" />\n \u003Ctext x=\"157.5\" y=\"178\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"7\" fill=\"#56524a\">OS\u003C/text>\n \u003Ctext x=\"174\" y=\"178\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-size=\"12\" fill=\"#56524a\">= an isolated VM\u003C/text>\n \u003C/g>\n\u003C/svg>\n\nThe client speaks to the agentOS server over the wire. The server runs the **sidecar**, the trusted core that hosts every VM: it owns each VM's kernel and brokers every guest syscall the agent makes (filesystem, processes, network, permissions) before carrying it out. Each VM is a fully isolated world, so agents are isolated from one another and from your host.\n\n### Your app (the client)\n\n- **Trusted caller.** Your app drives agentOS. It creates VMs, opens sessions, sends prompts, and reads results back.\n- **Never runs guest code.** The agent and any code it generates run in the VM, not in your app's process.\n- **Available everywhere.** There is a TypeScript client and a Rust client, and the same VM is reachable from a Node script, a browser/React app, or a separate backend.\n- **Owns the configuration.** Everything you send (VM setup, permission policy, resource limits, mounts) is trusted input. See the [Security Model](/docs/security-model) for why your configuration is not an attack surface.\n\n### Your server (the sidecar)\n\n- **The trusted core.** The sidecar is the part of the system that owns everything: the kernel, the virtual filesystem, the process and socket tables, pipes, PTYs, the permission policy, and DNS.\n- **The enforcement point.** Every request the VM makes is serviced here. The sidecar decides what is allowed before carrying it out.\n- **Hosts every VM.** A single sidecar manages many VMs side by side, each with its own kernel, filesystem, and process table, so every agent runs in its own isolated world. A crash or runaway in one VM never affects another.\n\n### The VM\n\n- **A fully virtualized Linux environment.** Each VM has its own filesystem, process table, and network policy. Two VMs share nothing.\n- **The unit of isolation.** Put one tenant or one task per VM to control the blast radius. A crash or runaway in one VM never affects another.\n- **Where guest code lives.** The agent, the shell, npm packages, and any generated code all run inside the VM, behind the kernel's boundary.\n\n## Anatomy of a Linux VM\n\nInside every VM there are two halves. The **kernel** is the trusted core that owns all the resources and rules. The **executor** is where untrusted guest code actually runs. Guest code can only *ask* the kernel for things, it never holds a real capability of its own.\n\n\u003Csvg viewBox=\"0 0 700 360\" role=\"img\" aria-label=\"A VM split into a kernel and an executor. The kernel owns the virtual filesystem, process table, socket table, pipes, PTYs, DNS, and permission policy. The executor runs guest JavaScript, WASM, and native binaries, and reaches the kernel through syscalls.\" style=\"width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"vm-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"12\" width=\"676\" height=\"336\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"32\" y=\"40\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">The VM\u003C/text>\n\n \u003Crect x=\"32\" y=\"56\" width=\"636\" height=\"150\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"52\" y=\"82\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Kernel\u003C/text>\n \u003Ctext x=\"52\" y=\"100\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">trusted core, every operation goes through here\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"52\" y=\"116\" width=\"118\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"111\" y=\"135\" text-anchor=\"middle\">virtual filesystem\u003C/text>\n \u003Crect x=\"182\" y=\"116\" width=\"118\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"241\" y=\"135\" text-anchor=\"middle\">process table\u003C/text>\n \u003Crect x=\"312\" y=\"116\" width=\"118\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"371\" y=\"135\" text-anchor=\"middle\">socket table\u003C/text>\n \u003Crect x=\"442\" y=\"116\" width=\"92\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"488\" y=\"135\" text-anchor=\"middle\">pipes / PTYs\u003C/text>\n \u003Crect x=\"546\" y=\"116\" width=\"100\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"596\" y=\"135\" text-anchor=\"middle\">DNS\u003C/text>\n \u003Crect x=\"52\" y=\"156\" width=\"594\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"349\" y=\"175\" text-anchor=\"middle\">permission policy · network allowlist · resource limits\u003C/text>\n \u003C/g>\n\n \u003Cline x1=\"349\" y1=\"206\" x2=\"349\" y2=\"244\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#vm-arrow)\" />\n \u003Cline x1=\"319\" y1=\"244\" x2=\"319\" y2=\"206\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#vm-arrow)\" />\n \u003Ctext x=\"430\" y=\"228\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">syscalls / replies\u003C/text>\n\n \u003Crect x=\"32\" y=\"248\" width=\"636\" height=\"84\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"52\" y=\"274\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Executor\u003C/text>\n \u003Ctext x=\"52\" y=\"292\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">untrusted, runs guest code, holds no capabilities\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"382\" y=\"262\" width=\"170\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"467\" y=\"281\" text-anchor=\"middle\">guest JavaScript (native V8)\u003C/text>\n \u003Crect x=\"562\" y=\"262\" width=\"84\" height=\"30\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"604\" y=\"281\" text-anchor=\"middle\">WASM\u003C/text>\n \u003Crect x=\"382\" y=\"298\" width=\"264\" height=\"22\" rx=\"6\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"514\" y=\"313\" text-anchor=\"middle\" font-size=\"10.5\">shell · coreutils · npm packages · native binaries\u003C/text>\n \u003C/g>\n\u003C/svg>\n\n### Kernel: the trusted core\n\n\u003Csvg viewBox=\"0 0 480 150\" role=\"img\" aria-label=\"Guest requests funnel into a single kernel chokepoint, which fans out to its owned subsystems: filesystem, processes, network, and policy.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"kn-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"14\" y=\"58\" width=\"92\" height=\"34\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"60\" y=\"79\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#56524a\">guest request\u003C/text>\n \u003Cline x1=\"106\" y1=\"75\" x2=\"150\" y2=\"75\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#kn-arrow)\" />\n \u003Crect x=\"154\" y=\"50\" width=\"92\" height=\"50\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"200\" y=\"79\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Kernel\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">\n \u003Cline x1=\"246\" y1=\"60\" x2=\"286\" y2=\"22\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Cline x1=\"246\" y1=\"70\" x2=\"286\" y2=\"58\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Cline x1=\"246\" y1=\"80\" x2=\"286\" y2=\"92\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Cline x1=\"246\" y1=\"90\" x2=\"286\" y2=\"128\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#kn-arrow)\" />\n \u003Crect x=\"290\" y=\"8\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"25\" text-anchor=\"middle\">filesystem\u003C/text>\n \u003Crect x=\"290\" y=\"46\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"63\" text-anchor=\"middle\">processes\u003C/text>\n \u003Crect x=\"290\" y=\"80\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"97\" text-anchor=\"middle\">network & DNS\u003C/text>\n \u003Crect x=\"290\" y=\"116\" width=\"176\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"378\" y=\"133\" text-anchor=\"middle\">policy & limits\u003C/text>\n \u003C/g>\n\u003C/svg>\n\nThe kernel is the single chokepoint. Each kind of guest operation is serviced by a kernel-owned subsystem, never by a real host capability.\n\n- **Virtual filesystem.** A per-VM filesystem. Guest reads and writes hit the VFS, not your host disk.\n- **Process table.** A virtual process table. Child processes are kernel-managed and visible only inside their VM. No real host process is ever spawned for guest work.\n- **Socket table and DNS.** A virtual network stack. Outbound traffic is gated by the network allowlist.\n- **Pipes and PTYs.** Kernel-owned IPC and terminal devices, so shells and pipelines behave like real Linux.\n- **Policy and limits.** The kernel checks the applied permission policy, network allowlist, and resource limits on every request.\n\n### Executor: where guest code runs\n\n\u003Csvg viewBox=\"0 0 480 130\" role=\"img\" aria-label=\"The untrusted executor runs guest JavaScript, WASM, and native binaries. It holds no capabilities and reaches the kernel through syscalls.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"ex-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"14\" y=\"14\" width=\"300\" height=\"102\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"30\" y=\"36\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Executor\u003C/text>\n \u003Ctext x=\"30\" y=\"52\" font-family=\"var(--sl-font)\" font-size=\"9.5\" fill=\"#56524a\">untrusted · no capabilities\u003C/text>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">\n \u003Crect x=\"30\" y=\"62\" width=\"80\" height=\"44\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"70\" y=\"88\" text-anchor=\"middle\">JS (V8)\u003C/text>\n \u003Crect x=\"120\" y=\"62\" width=\"80\" height=\"44\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"160\" y=\"88\" text-anchor=\"middle\">WASM\u003C/text>\n \u003Crect x=\"210\" y=\"62\" width=\"92\" height=\"44\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"256\" y=\"82\" text-anchor=\"middle\">native\u003C/text>\u003Ctext x=\"256\" y=\"96\" text-anchor=\"middle\">binaries\u003C/text>\n \u003C/g>\n \u003Cline x1=\"314\" y1=\"55\" x2=\"358\" y2=\"55\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#ex-arrow)\" />\n \u003Ctext x=\"336\" y=\"48\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">syscall\u003C/text>\n \u003Cline x1=\"358\" y1=\"75\" x2=\"314\" y2=\"75\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#ex-arrow)\" />\n \u003Ctext x=\"336\" y=\"92\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">reply\u003C/text>\n \u003Crect x=\"362\" y=\"38\" width=\"104\" height=\"54\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"414\" y=\"70\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Kernel\u003C/text>\n\u003C/svg>\n\nThe executor is the untrusted half of the VM. It runs the guest code and reaches the kernel for everything else.\n\n- **JavaScript Acceleration.** Guest JavaScript runs on a native V8 runtime (the same engine in Chrome and Node.js, with the full JIT compiler) inside an isolate. This is what we call **JavaScript Acceleration**: the guest's JavaScript executes at native speed, not through an interpreter or a translation shim. It is genuinely fast, and it presents normal Node.js semantics. See [JavaScript Runtime](/docs/js-runtime).\n- **WASM alongside it.** The shell (`sh`) and the coreutils behind process execution ship as WebAssembly modules, and you can run your own WASM too. See [POSIX Syscalls](/docs/architecture/posix-syscalls) and the [Compiler Toolchain](/docs/architecture/compiler-toolchain).\n- **Native binaries.** Tools mounted into the VM run inside the same boundary as everything else.\n- **No host fallthrough.** The executor holds no capability of its own. For every file read, process spawn, or socket open, it issues a syscall and blocks for the kernel's reply.\n\n### Processes & shell\n\n\u003Csvg viewBox=\"0 0 480 120\" role=\"img\" aria-label=\"exec, run, and spawn create entries in the kernel-owned virtual process table, with stdio bridged through pipes and PTYs.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"pr-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"14\" y=\"14\" width=\"92\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"31\" text-anchor=\"middle\">exec() / run()\u003C/text>\n \u003Crect x=\"14\" y=\"78\" width=\"92\" height=\"26\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"95\" text-anchor=\"middle\">spawn / shell\u003C/text>\n \u003C/g>\n \u003Cline x1=\"106\" y1=\"27\" x2=\"172\" y2=\"52\" stroke=\"#1b1916\" stroke-width=\"1.3\" marker-end=\"url(#pr-arrow)\" />\n \u003Cline x1=\"106\" y1=\"91\" x2=\"172\" y2=\"66\" stroke=\"#1b1916\" stroke-width=\"1.3\" marker-end=\"url(#pr-arrow)\" />\n \u003Crect x=\"176\" y=\"36\" width=\"138\" height=\"46\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"245\" y=\"56\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"11.5\" font-weight=\"600\" fill=\"#1b1916\">process table\u003C/text>\n \u003Ctext x=\"245\" y=\"71\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">virtual · per-VM\u003C/text>\n \u003Cline x1=\"314\" y1=\"59\" x2=\"360\" y2=\"59\" stroke=\"#1b1916\" stroke-width=\"1.3\" marker-end=\"url(#pr-arrow)\" />\n \u003Crect x=\"364\" y=\"40\" width=\"102\" height=\"40\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\n \u003Ctext x=\"415\" y=\"64\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#1b1916\">pipes & PTYs\u003C/text>\n\u003C/svg>\n\n- **A real process model.** `exec()` and `run()` start fresh guest processes; you can also `spawn` long-running ones and open interactive shells.\n- **Kernel-managed.** Every process lives in the virtual process table, with stdio bridged through kernel-owned pipes and PTYs.\n- **Fresh each run.** Each `exec()` / `run()` starts a brand new guest process, so in-memory state never leaks from one run into the next.\n- See [Processes](/docs/architecture/processes) for the internals.\n\n### Virtual filesystem\n\n\u003Csvg viewBox=\"0 0 480 150\" role=\"img\" aria-label=\"The virtual filesystem layers a writable overlay over a snapshot root, plus mount points that graft host directories, S3, or cloud stores onto guest paths.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11\" fill=\"#1b1916\">\n \u003Crect x=\"60\" y=\"14\" width=\"360\" height=\"30\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\u003Ctext x=\"240\" y=\"33\" text-anchor=\"middle\">overlay (guest writes)\u003C/text>\n \u003Crect x=\"60\" y=\"52\" width=\"360\" height=\"30\" rx=\"8\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\u003Ctext x=\"240\" y=\"71\" text-anchor=\"middle\">root layer (snapshot)\u003C/text>\n \u003C/g>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"9.5\" fill=\"#1b1916\">\n \u003Crect x=\"60\" y=\"104\" width=\"108\" height=\"32\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"114\" y=\"124\" text-anchor=\"middle\">host dir mount\u003C/text>\n \u003Crect x=\"186\" y=\"104\" width=\"108\" height=\"32\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"240\" y=\"124\" text-anchor=\"middle\">S3 mount\u003C/text>\n \u003Crect x=\"312\" y=\"104\" width=\"108\" height=\"32\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"366\" y=\"124\" text-anchor=\"middle\">cloud store\u003C/text>\n \u003C/g>\n \u003Ctext x=\"240\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">mount points grafted onto guest paths\u003C/text>\n\u003C/svg>\n\n- **Layered engines.** The VFS is a tree of engines: a root layer bootstrapped from a snapshot, an overlay for writes, and mount points that graft other backends onto guest paths.\n- **Host-backed mounts.** A guest path can be backed by a host directory, S3, or a cloud store. The kernel confines all guest I/O to the mount root, even against symlink and `..` tricks.\n- **Persisted.** The `/home/agentos` filesystem survives sleep/wake.\n- See [Filesystem](/docs/architecture/filesystem) for the internals.\n\n### Networking\n\n\u003Csvg viewBox=\"0 0 480 150\" role=\"img\" aria-label=\"Guest fetch, node:http, node:net, and WASM sockets all converge on one kernel socket table, which gates outbound traffic through the network allowlist.\" style=\"width:100%;height:auto;max-width:440px;display:block;margin:2.5rem auto;\">\n \u003Cdefs>\n \u003Cmarker id=\"nw-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">\n \u003Crect x=\"14\" y=\"10\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"26\" text-anchor=\"middle\">fetch()\u003C/text>\n \u003Crect x=\"14\" y=\"42\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"58\" text-anchor=\"middle\">node:http\u003C/text>\n \u003Crect x=\"14\" y=\"74\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"90\" text-anchor=\"middle\">node:net\u003C/text>\n \u003Crect x=\"14\" y=\"106\" width=\"92\" height=\"24\" rx=\"6\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1\" />\u003Ctext x=\"60\" y=\"122\" text-anchor=\"middle\">WASM sockets\u003C/text>\n \u003C/g>\n \u003Cline x1=\"106\" y1=\"22\" x2=\"186\" y2=\"62\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Cline x1=\"106\" y1=\"54\" x2=\"186\" y2=\"66\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Cline x1=\"106\" y1=\"86\" x2=\"186\" y2=\"74\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Cline x1=\"106\" y1=\"118\" x2=\"186\" y2=\"78\" stroke=\"#1b1916\" stroke-width=\"1.2\" marker-end=\"url(#nw-arrow)\" />\n \u003Crect x=\"190\" y=\"48\" width=\"116\" height=\"44\" rx=\"10\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.4\" />\n \u003Ctext x=\"248\" y=\"68\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"11\" font-weight=\"600\" fill=\"#1b1916\">socket table\u003C/text>\n \u003Ctext x=\"248\" y=\"83\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"9\" fill=\"#56524a\">kernel-owned\u003C/text>\n \u003Cline x1=\"306\" y1=\"70\" x2=\"350\" y2=\"70\" stroke=\"#1b1916\" stroke-width=\"1.4\" marker-end=\"url(#nw-arrow)\" />\n \u003Crect x=\"354\" y=\"50\" width=\"112\" height=\"40\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\n \u003Ctext x=\"410\" y=\"74\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#1b1916\">egress allowlist\u003C/text>\n\u003C/svg>\n\n- **One authoritative transport.** Guest `fetch()`, `node:http`, `node:net`, and WASM sockets all target the same kernel socket table. No part of guest networking opens a real host socket on its own.\n- **Egress policy.** Outbound traffic is gated by the network allowlist; loopback traffic stays confined to the VM.\n- **Preview URLs.** Servers a guest starts can be exposed through signed preview URLs.\n- See [Networking](/docs/architecture/networking) for the internals.\n\n\u003CAside type=\"note\">The security boundary that matters is between the trusted sidecar and the untrusted executor. Everything the guest tries to do crosses into the kernel, where the policy is checked before the operation runs. See the [Security Model](/docs/security-model) for the full threat model.\u003C/Aside>\n\n## Agents & sessions\n\nAn agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) is just another guest process running inside a VM, behind the same boundary as any other code. A **session** keeps that agent alive across many prompts and streams its output back to your app as events.\n\n\u003Csvg viewBox=\"0 0 700 210\" role=\"img\" aria-label=\"A client sends a prompt to an agent running inside a VM. The agent streams events back to the client and persists a transcript.\" style=\"width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"se-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"66\" width=\"150\" height=\"78\" rx=\"12\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"87\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"14\" font-weight=\"600\" fill=\"#1b1916\">Client\u003C/text>\n \u003Ctext x=\"87\" y=\"118\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">your app\u003C/text>\n\n \u003Cline x1=\"162\" y1=\"92\" x2=\"266\" y2=\"92\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#se-arrow)\" />\n \u003Ctext x=\"214\" y=\"84\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">prompt\u003C/text>\n \u003Cline x1=\"266\" y1=\"120\" x2=\"162\" y2=\"120\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#se-arrow)\" />\n \u003Ctext x=\"214\" y=\"136\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">events\u003C/text>\n\n \u003Crect x=\"270\" y=\"30\" width=\"280\" height=\"150\" rx=\"12\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"290\" y=\"56\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">The VM\u003C/text>\n \u003Crect x=\"300\" y=\"72\" width=\"220\" height=\"56\" rx=\"8\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\" />\n \u003Ctext x=\"410\" y=\"98\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Agent session\u003C/text>\n \u003Ctext x=\"410\" y=\"116\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">long-lived agent process\u003C/text>\n\n \u003Cline x1=\"550\" y1=\"105\" x2=\"630\" y2=\"105\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#se-arrow)\" />\n \u003Crect x=\"560\" y=\"72\" width=\"118\" height=\"66\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"619\" y=\"100\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"12\" font-weight=\"600\" fill=\"#1b1916\">Transcript\u003C/text>\n \u003Ctext x=\"619\" y=\"118\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">persisted, replayable\u003C/text>\n\u003C/svg>\n\n### Sessions & transcripts\n\n- **Long-lived.** Where a bare `exec()` runs once and exits, a session keeps an agent alive across many prompts.\n- **Streamed.** The agent's output flows back to your app in real time as `sessionEvent`s.\n- **Replayable.** Each session persists a transcript (with sequence numbers) that survives sleep/wake, so you can replay the conversation later.\n- **Context injected.** agentOS adds a system prompt describing the VM environment and available tools, layered on top of the agent's own instructions. See [System Prompt](/docs/system-prompt).\n- See [Agent Sessions](/docs/architecture/agent-sessions) for the internals.\n\n### Permissions & approvals\n\n- **Two layers, different jobs.** The lower-level [permission policy](/docs/permissions) is enforced by the kernel on every guest syscall (nothing is allowed until you opt in). On top of that, [approvals](/docs/approvals) are about an agent asking before it uses a tool.\n- **Human-in-the-loop or automatic.** Subscribe to `permissionRequest` and respond per request, or use a server-side hook to decide without a client round-trip.\n- **Blocks until answered.** If neither your hook nor your client responds, the agent waits rather than proceeding.\n\n## Orchestration (Rivet Actors)\n\nThe `agentOS()` actor (from `@rivet-dev/agentos`) wraps the raw VM in a [Rivet Actor](/docs/core), which adds durable state, scheduling, and orchestration. This is what gives you persistence, cron, and workflows out of the box.\n\n\u003Csvg viewBox=\"0 0 700 200\" role=\"img\" aria-label=\"A Rivet Actor wraps an agentOS VM and adds durable state, cron scheduling, workflows, and sleep/wake persistence.\" style=\"width:100%;height:auto;max-width:680px;display:block;margin:1.5rem auto 0.5rem;\">\n \u003Crect x=\"40\" y=\"20\" width=\"620\" height=\"160\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"64\" y=\"46\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Rivet Actor\u003C/text>\n \u003Ctext x=\"64\" y=\"64\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">durable, addressable server object\u003C/text>\n\n \u003Crect x=\"64\" y=\"80\" width=\"180\" height=\"80\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.3\" />\n \u003Ctext x=\"154\" y=\"116\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">agentOS VM\u003C/text>\n \u003Ctext x=\"154\" y=\"136\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10\" fill=\"#56524a\">the virtual Linux VM\u003C/text>\n\n \u003Cg font-family=\"var(--sl-font)\" font-size=\"11.5\" fill=\"#1b1916\">\n \u003Crect x=\"272\" y=\"80\" width=\"120\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"332\" y=\"102\" text-anchor=\"middle\">Cron\u003C/text>\n \u003Crect x=\"412\" y=\"80\" width=\"120\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"472\" y=\"102\" text-anchor=\"middle\">Workflows\u003C/text>\n \u003Crect x=\"272\" y=\"126\" width=\"260\" height=\"34\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"402\" y=\"148\" text-anchor=\"middle\">Persistence · sleep / wake\u003C/text>\n \u003Crect x=\"552\" y=\"80\" width=\"92\" height=\"80\" rx=\"7\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.1\" />\u003Ctext x=\"598\" y=\"116\" text-anchor=\"middle\">Durable\u003C/text>\u003Ctext x=\"598\" y=\"132\" text-anchor=\"middle\">state\u003C/text>\n \u003C/g>\n\u003C/svg>\n\n### What are actors?\n\n- **Durable server objects.** A Rivet Actor is a long-lived, addressable object with its own state. You reach a specific VM by name (`vm.getOrCreate(\"my-agent\")`).\n- **Stateful by default.** Unlike the bare core package, the actor keeps its filesystem and sessions persistent and handles distributed state for you.\n- **The portable runtime.** Actors give you a consistent way to run `agentOS()` on any infrastructure, with persistence, networking, and orchestration built in.\n\n### Cron\n\n- **Recurring work.** Schedule a shell command or an agent session on a cron expression.\n- **Overlap control.** Choose what happens when a run is still going when the next is due (`allow`, `skip`, or `queue`).\n- **Observable.** Stream `cronEvent`s to watch executions. See [Cron Jobs](/docs/cron).\n\n### Workflows\n\n- **Durable multi-step tasks.** A workflow is the actor's `run` handler wrapped in `workflow()`, where each `ctx.step()` is recorded, retried, and resumed independently.\n- **Crash-proof.** If the process dies mid-run, replay skips completed steps and continues where it left off.\n- **Composable.** The output of one step feeds the next: clone a repo, let an agent fix a bug, run the tests. See [Workflow Automation](/docs/workflows).\n\n### Persistence & sleep/wake\n\n- **Sleeps when idle.** After a grace period (15 minutes by default) with no activity, the VM sleeps to free resources.\n- **Wakes on demand.** It wakes automatically when a client connects or a cron job fires.\n- **What survives.** The `/home/agentos` filesystem, session records, transcripts, preview tokens, and cron definitions all persist. In-memory kernel state (running processes, open shells) does not. See [Persistence & Sleep](/docs/persistence).\n\n## Going deeper\n\nThis page is the map. Each subsystem has its own detailed page in the Advanced architecture section:\n\n- **[Agent Sessions](/docs/architecture/agent-sessions)**: how a session is bound to a VM, and how prompts and events flow end to end.\n- **[Processes](/docs/architecture/processes)**: the virtual process table, `exec()` / `run()`, child processes, and PTYs.\n- **[Filesystem](/docs/architecture/filesystem)**: the per-VM virtual filesystem, overlays, and host-backed mounts.\n- **[Networking](/docs/architecture/networking)**: the virtual socket table, DNS, the allowlist, and guest `fetch()`.\n- **[POSIX Syscalls](/docs/architecture/posix-syscalls)**: how WebAssembly guests behave like normal POSIX programs on top of the kernel.\n- **[Compiler Toolchain](/docs/architecture/compiler-toolchain)**: how the shell and coreutils are compiled to WebAssembly and mounted into the VM.\n- **[System Prompt](/docs/system-prompt)**: the context agentOS injects into every agent session.\n- **[Persistence & Sleep](/docs/persistence)**: what survives sleep/wake, and how VMs sleep and wake.\n\nFor the trust model and what counts as a sandbox escape, see the [Security Model](/docs/security-model).","src/content/docs/docs/architecture.mdx","f6bb02d332a92b03","docs/authentication",{"id":47,"data":49,"body":55,"filePath":56,"digest":57,"deferredRender":16},{"title":50,"description":51,"editUrl":16,"head":52,"template":18,"sidebar":53,"pagefind":16,"draft":20},"Authentication","Authenticate connections to agentOS actors using Rivet Actor connection params and hooks.",[],{"hidden":20,"attrs":54},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nagentOS uses the same authentication system as [Rivet Actors](/docs/actors/authentication): clients send credentials as connection params, and you validate them server-side.\n\n- Clients pass credentials in `params` when they connect.\n- Validate them on the server in `onBeforeConnect` (throw to reject the connection), or extract user data into connection state with `createConnState` (read it in actions via `c.conn.state`).\n- You can declare the credential shape with `agentOS\u003CConnParams>(...)` to document what you accept, but the client's `params` is `unknown` and is not checked against it. The real check is your hook, not the types.\n- The current `@rivet-dev/agentos` runtime is an interim stub, so wiring these hooks end to end depends on the native runtime landing.\n\n## Example\n\nThe server declares the credential shape and validates it in `onBeforeConnect` (throw to reject); the client passes credentials as `params`.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\ninterface ConnParams {\n authToken: string;\n}\n\n// Validate credentials server-side. onBeforeConnect receives the connection\n// params and rejects the connection by throwing. Wired via the underlying Rivet\n// Actor; see Actor Authentication for the full hook signatures.\nexport function onBeforeConnect(_c: unknown, params: ConnParams): void {\n if (typeof params?.authToken !== \"string\" || params.authToken.length === 0) {\n throw new Error(\"missing or invalid authToken\");\n }\n // verify the token (JWT signature, lookup, ...) here\n}\n\nconst vm = agentOS\u003CConnParams>({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Pass credentials when connecting; the server hook validates them.\nconst agent = client.vm.getOrCreate(\"my-agent\", {\n params: { authToken: \"my-jwt-token\" },\n});\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"List the files in the working directory.\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/authentication)*\n\nSee [Actor Authentication](/docs/actors/authentication) for JWT validation, role-based access control, external auth providers, and token caching.","src/content/docs/docs/authentication.mdx","5839914d3efbeeb9","docs/benchmarks",{"id":58,"data":60,"body":66,"filePath":67,"digest":68,"deferredRender":16},{"title":61,"description":62,"editUrl":16,"head":63,"template":18,"sidebar":64,"pagefind":16,"draft":20},"Benchmarks","Performance benchmarks comparing agentOS to traditional sandbox providers.",[],{"hidden":20,"attrs":65},{},"These are the benchmark figures shown on the agentOS marketing page. All numbers are computed from the same data source used by the marketing page. For independent sandbox comparison data, see the [ComputeSDK benchmarks](https://www.computesdk.com/benchmarks/).\n\n## Cold start\n\nTime from requesting an execution to first code running. Measured using the sleep workload (a minimal VM running an idle Node.js process). Sandbox baseline: **E2B**, the fastest mainstream sandbox provider as of March 30, 2026. See [ComputeSDK benchmarks](https://www.computesdk.com/benchmarks/) for independent sandbox comparison data.\n\n| Metric | agentOS | Fastest sandbox (E2B) |\n|---|--:|--:|\n| Cold start p50 | 4.8 ms | 440 ms |\n| Cold start p95 | 5.6 ms | 950 ms |\n| Cold start p99 | 6.1 ms | 3,150 ms |\n\n## Memory per instance\n\nMeasured via staircase benchmarking:\n\n1. **Warmup.** A throwaway VM is created, started, and destroyed before measurement begins. This pays one-time costs (module cache, JIT compilation) that are amortized away in any real deployment where the host process is long-lived.\n2. **Baseline.** GC is forced twice (`--expose-gc`), then RSS is sampled across the entire process tree by reading `/proc/[pid]/statm` for the host process and all descendants. This captures child processes (e.g. V8 isolates running as separate processes) that `process.memoryUsage().rss` would miss.\n3. **Staircase.** VMs are added one at a time. After each VM starts and settles, GC is forced and RSS is sampled again. The delta from the previous sample is the incremental cost of that VM.\n4. **Average.** The per-VM cost is the mean of all step deltas.\n5. **Teardown.** All VMs are disposed and the reclaimed RSS is recorded.\n\nRSS is a process-wide metric that includes thread stacks and OS-mapped pages beyond the VM itself, so the reported figure is an upper bound on the true per-VM cost.\n\nSandbox baseline: **Daytona**, the cheapest mainstream sandbox provider as of March 30, 2026. Default sandbox: 1 vCPU + 1 GiB RAM.\n\n### Full coding agent\n\nPi coding agent session with MCP servers and mounted file systems.\n\n| Metric | agentOS | Cheapest sandbox (Daytona) |\n|---|--:|--:|\n| Memory per instance | ~131 MB | ~1024 MB |\n\n### Simple shell command\n\nMinimal shell workload running simple commands.\n\n| Metric | agentOS | Cheapest sandbox (Daytona) |\n|---|--:|--:|\n| Memory per instance | ~22 MB | ~1024 MB |\n\n## Cost per execution-second\n\nAssumes one agent per sandbox (needed for isolation) and 70% host utilization for self-hosted hardware (the industry-standard HPA scaling threshold). Cost formula: `server cost per second / concurrent executions per server`, where concurrent executions = `floor(server RAM / agent memory) × 0.7`.\n\nSandbox baseline: **Daytona** at $0.0504/vCPU-h + $0.0162/GiB-h with a 1 vCPU + 1 GiB minimum. Source: [daytona.io/pricing](https://www.daytona.io/pricing).\n\n### Full coding agent\n\n| Host tier | agentOS | Cheapest sandbox | Difference |\n|---|--:|--:|--:|\n| AWS ARM | $0.00000058/s | $0.000018/s | 32x cheaper |\n| AWS x86 | $0.00000072/s | $0.000018/s | 26x cheaper |\n| Hetzner ARM | $0.000000066/s | $0.000018/s | 281x cheaper |\n| Hetzner x86 | $0.00000011/s | $0.000018/s | 171x cheaper |\n\n### Simple shell command\n\n| Host tier | agentOS | Cheapest sandbox | Difference |\n|---|--:|--:|--:|\n| AWS ARM | $0.000000073/s | $0.000018/s | 254x cheaper |\n| AWS x86 | $0.000000090/s | $0.000018/s | 205x cheaper |\n| Hetzner ARM | $0.000000011/s | $0.000018/s | 1738x cheaper |\n| Hetzner x86 | $0.000000017/s | $0.000018/s | 1061x cheaper |\n\n## Test environment\n\n| Component | Details |\n|---|---|\n| CPU | 12th Gen Intel i7-12700KF, 12 cores / 20 threads @ 3.7 GHz, 25 MB cache |\n| RAM | 2× 32 GB DDR4 @ 2400 MT/s |\n| Node.js | v24.13.0 |\n| OS | Linux 6.1.0 (Debian), x86_64 |\n\n## Sandbox baselines\n\n| Comparison | Provider | Why this provider |\n|---|---|---|\n| Cold start | E2B | Fastest mainstream sandbox provider on [ComputeSDK](https://www.computesdk.com/benchmarks/) as of March 30, 2026 |\n| Memory and cost | Daytona | Cheapest mainstream sandbox provider as of March 30, 2026 ($0.0504/vCPU-h + $0.0162/GiB-h) |\n\nSelf-hosted hardware tiers: AWS t4g.micro (ARM, $0.0084/h, 1 GiB), AWS t3.micro (x86, $0.0104/h, 1 GiB), Hetzner CAX11 (ARM, €3.29/mo, 4 GiB), Hetzner CX22 (x86, €5.39/mo, 4 GiB). All on-demand pricing.\n\n## Reproducing\n\nagentOS benchmarks live in the [agent-os repository](https://github.com/rivet-dev/agent-os) under `scripts/benchmarks/`.","src/content/docs/docs/benchmarks.mdx","b6a087cd9516c3ec","docs/bindings",{"id":69,"data":71,"body":77,"filePath":78,"digest":79,"deferredRender":16},{"title":72,"description":73,"editUrl":16,"head":74,"template":18,"sidebar":75,"pagefind":16,"draft":20},"Bindings","Expose custom host functions to agents as CLI commands inside the VM.",[],{"hidden":20,"attrs":76},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nExpose your host JavaScript functions (defined with Zod input schemas) to agents as auto-generated CLI commands installed at `/usr/local/bin/agentos-{name}` inside the VM, injected into the agent's [system prompt](/docs/system-prompt) and callable inside scripts for code-mode token savings.\n\n## Getting started\n\nDefine a bindings group with Zod input schemas and pass it to `agentOS()`. Each binding becomes a CLI command inside the VM.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { z } from \"zod\";\n\n// Define a group of host functions. Each binding has a Zod input schema and an\n// `execute` handler that runs on the host. Bindings are exposed to the agent as\n// CLI commands at /usr/local/bin/agentos-{name} inside the VM.\nconst weatherBindings = {\n name: \"weather\",\n description: \"Weather data bindings\",\n bindings: {\n forecast: {\n description: \"Get the weather forecast for a city\",\n inputSchema: z.object({\n city: z.string().describe(\"City name\"),\n days: z.number().optional().describe(\"Number of days\"),\n }),\n execute: async (input: { city: string; days?: number }) => {\n const res = await fetch(\n `https://api.weather.example/forecast?city=${input.city}&days=${input.days ?? 3}`,\n );\n return res.json();\n },\n examples: [\n { description: \"3-day forecast for Paris\", input: { city: \"Paris\", days: 3 } },\n ],\n },\n },\n};\n\nconst vm = agentOS({\n bindings: [weatherBindings],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"What's the weather in Paris?\");\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/bindings)*\n\nEach binding can set an explicit `timeout` (in milliseconds) for long-running work. Bindings run without a timeout unless one is set.\n\n### Zod to CLI mapping\n\nZod schema fields are converted to CLI flags automatically. Field names are converted from camelCase to kebab-case.\n\n| Zod type | CLI syntax | Example |\n|---|---|---|\n| `z.string()` | `--name value` | `--path /tmp/out.png` |\n| `z.number()` | `--name 42` | `--limit 5` |\n| `z.boolean()` | `--flag` / `--no-flag` | `--full-page` |\n| `z.enum([\"a\",\"b\"])` | `--name a` | `--format json` |\n| `z.array(z.string())` | `--name a --name b` | `--tags foo --tags bar` |\n\nOptional fields (via `.optional()`) become optional flags. Required fields are enforced at validation time. Use `.describe()` on Zod fields to generate useful `--help` output.\n\n### What the agent sees\n\nWhen bindings are registered, CLI shims are installed at `/usr/local/bin/agentos-{name}` inside the VM and the binding list is injected into the agent's [system prompt](/docs/system-prompt), so keep binding descriptions concise to save tokens.\n\nThe agent interacts with bindings as shell commands:\n\n```bash\n# List all available bindings groups\nagentos list-tools\n\n# List bindings in a specific group\nagentos list-tools weather\n\n# Get help for a binding\nagentos-weather forecast --help\n\n# Call a binding with flags\nagentos-weather forecast --city Paris --days 3\n\n# Call a binding with inline JSON\nagentos-weather forecast --json '{\"city\":\"Paris\",\"days\":3}'\n\n# Call a binding with JSON from a file\nagentos-weather forecast --json-file /tmp/input.json\n```\n\nOn success, the binding exits `0` and writes a JSON envelope to stdout:\n\n```json\n{\"ok\":true,\"result\":{\"temperature\":22,\"condition\":\"sunny\"}}\n```\n\nOn failure (validation or execution error), the binding exits non-zero and writes the error message to stderr:\n\n```text\nMissing required flag: --city\n```\n\n## Bindings vs MCP servers\n\nagentOS supports two ways to give agents access to external functionality: **bindings** and **MCP servers**. Both work, but they have different tradeoffs.\n\n| | Bindings | MCP Servers |\n|---|---|---|\n| **How it works** | Call JavaScript functions on the host directly | Connect to a standard MCP server |\n| **Authentication** | None required. Direct binding to the agent's OS. | Requires custom auth configuration per server |\n| **Code mode** | Built-in. Bindings are exposed as CLI commands, so agents can call them inside scripts for up to 80% token reduction. | Requires extra work to make code mode work out of the box |\n| **Latency** | Near-zero. Bound directly to the host process. | Extra network hop to reach the MCP server |\n| **Setup** | Define bindings in your actor code with Zod schemas | Configure any standard MCP server |\n\nUse bindings when you want to expose your own JavaScript functions to agents. Use MCP servers when you want to connect to existing third-party services. See [Sessions](/docs/sessions#mcpservers) for MCP server configuration.\n\n## Security\n\nBinding calls from the agent securely invoke your `execute()` functions on the host. Your functions run with full access to the host environment, so you can call databases, APIs, and services directly without proxying credentials into the VM. The agent never sees the credentials, it only sees the binding's input/output contract.\n\nBindings run on the host with full access to the host environment, so do not expose bindings that could compromise the host without appropriate safeguards.","src/content/docs/docs/bindings.mdx","0a104f8f8406948a","docs/core",{"id":80,"data":82,"body":88,"filePath":89,"digest":90,"deferredRender":16},{"title":83,"description":84,"editUrl":16,"head":85,"template":18,"sidebar":86,"pagefind":16,"draft":20},"Core Package","Use @rivet-dev/agentos-core standalone for direct VM control without the Rivet Actor runtime.",[],{"hidden":20,"attrs":87},{},"## agentOS vs agentOS Core\n\nThe `agentOS()` actor (from `@rivet-dev/agentos`) wraps the core package and adds:\n\n| | Core (`@rivet-dev/agentos-core`) | Actor (`@rivet-dev/agentos`) |\n|-|---|---|\n| Persistence | In-memory by default (pluggable via [mounts](#mounts)) | Persistent filesystem and sessions |\n| Distributed state | Manage yourself | Built-in distributed statefulness |\n| Stateful VMs | Complex to run yourself | Built into Rivet |\n| Sleep/wake | Manual `dispose()` / `create()` | Automatic |\n| Events | Direct callbacks | Broadcasted to all connected clients |\n| Preview URLs | None | Built-in signed URL server |\n| Multiplayer | N/A | Multiple clients on same actor |\n| Orchestration | N/A | Workflows, queues, cron |\n| Agent-to-agent communication | Custom | Built into [Rivet Actors](/docs/agent-to-agent) |\n| Authentication | Set up yourself | [Documentation](/docs/authentication) |\n\nWe recommend using [Rivet Actors](/docs/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.\n\n## Install\n\n```bash\nnpm install @rivet-dev/agentos-core\n```\n\n## Boot a VM\n\nDefine the actor on the server:\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\nThen drive it from a typed client:\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.vm.getOrCreate(\"my-agent\");\n\nconst result = await handle.exec(\"echo hello\");\nconsole.log(result.stdout); // \"hello\\n\"\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Filesystem\n\n```ts title=\"client.ts\"\nawait handle.writeFile(\"/home/agentos/hello.txt\", \"Hello, world!\");\nconst content = await handle.readFile(\"/home/agentos/hello.txt\");\nconsole.log(new TextDecoder().decode(content));\n\nawait handle.mkdir(\"/home/agentos/src\");\nawait handle.writeFiles([\n { path: \"/home/agentos/src/index.ts\", content: \"console.log('hi');\" },\n { path: \"/home/agentos/src/utils.ts\", content: \"export const add = (a: number, b: number) => a + b;\" },\n]);\n\nconst entries = await handle.readdirRecursive(\"/home/agentos\");\nfor (const entry of entries) {\n console.log(entry.type, entry.path);\n}\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Processes\n\nLong-running process output is delivered over the live `processOutput` / `processExit` events on a connection rather than per-pid callbacks:\n\n```ts title=\"client.ts\"\n// One-shot execution\nconst result = await handle.exec(\"ls -la /home/agentos\");\nconsole.log(result.stdout);\n\n// Long-running process with streaming output\nawait handle.writeFile(\n \"/tmp/server.mjs\",\n 'import http from \"http\"; http.createServer((req, res) => res.end(\"ok\")).listen(3000); console.log(\"listening\");',\n);\nconst { pid } = await handle.spawn(\"node\", [\"/tmp/server.mjs\"]);\n\nconst conn = handle.connect();\nconn.on(\"processOutput\", (data) => {\n if (data.pid === pid && data.stream === \"stdout\") {\n console.log(\"stdout:\", new TextDecoder().decode(data.data));\n }\n});\nconn.on(\"processExit\", (data) => {\n if (data.pid === pid) console.log(\"exited:\", data.exitCode);\n});\n\n// Write to stdin\nawait handle.writeProcessStdin(pid, \"some input\\n\");\n\n// Stop or kill\nawait handle.stopProcess(pid);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Agent sessions\n\n`createSession` returns a session record. All session operations take its `sessionId`. Session events and permission requests are delivered over the live connection (`sessionEvent` / `permissionRequest`):\n\n```ts title=\"client.ts\"\nconst conn = handle.connect();\n\n// Stream events (each event is a JSON-RPC notification)\nconn.on(\"sessionEvent\", (data) => {\n console.log(data.event.method, data.event.params);\n});\n\n// Handle permissions\nconn.on(\"permissionRequest\", (data) => {\n console.log(\"Permission:\", data.request.description);\n // Reply with \"once\", \"always\", or \"reject\"\n void handle.respondPermission(data.sessionId, data.request.permissionId, \"once\");\n});\n\nconst session = await handle.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Send a prompt. sendPrompt() resolves to { response, text }, where `text` is\n// the accumulated agent message text and `response` is the raw JSON-RPC response.\nconst { text } = await handle.sendPrompt(session.sessionId, \"Write a hello world script\");\nconsole.log(text);\n\n// Configure the session\nawait handle.setModel(session.sessionId, \"claude-sonnet-4-6\");\nawait handle.setMode(session.sessionId, \"plan\");\n\nawait handle.closeSession(session.sessionId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\nSubscribe to `sessionEvent` before sending a prompt so you do not miss the live stream. Persisted history can be read back later with `getSessionEvents()`.\n\n## Interactive shell\n\n```ts title=\"client.ts\"\nconst { shellId } = await handle.openShell();\n\nconst conn = handle.connect();\nconn.on(\"shellData\", (data) => {\n if (data.shellId === shellId) {\n process.stdout.write(new TextDecoder().decode(data.data));\n }\n});\n\nawait handle.writeShell(shellId, \"echo hello from shell\\n\");\n\n// Resize terminal\nawait handle.resizeShell(shellId, 120, 40);\n\nawait handle.closeShell(shellId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Networking\n\n```ts title=\"client.ts\"\n// Start a server inside the VM\nawait handle.writeFile(\n \"/tmp/app.mjs\",\n 'import http from \"http\"; http.createServer((req, res) => res.end(\"hello\")).listen(3000);',\n);\nawait handle.spawn(\"node\", [\"/tmp/app.mjs\"]);\n\n// Fetch from it\nconst response = await handle.vmFetch(3000, \"/\");\nconsole.log(new TextDecoder().decode(response.body));\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Cron jobs\n\nCron jobs run an `\"exec\"` command or a `\"session\"` prompt on a schedule. Fired jobs are surfaced over the live `cronEvent` connection:\n\n```ts title=\"client.ts\"\nconst { id } = await handle.scheduleCron({\n id: \"cleanup\",\n schedule: \"0 * * * *\",\n action: { type: \"exec\", command: \"rm\", args: [\"-rf\", \"/tmp/cache\"] },\n});\nconsole.log(\"Scheduled:\", id);\n\n// Run an agent session on a schedule\nawait handle.scheduleCron({\n schedule: \"0 9 * * *\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Review the logs and summarize any errors\",\n options: { cwd: \"/workspace\" },\n },\n});\n\nconst conn = handle.connect();\nconn.on(\"cronEvent\", (data) => {\n console.log(\"Cron event:\", data.event.id, data.event.schedule);\n});\n\nconsole.log(await handle.listCronJobs());\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## Mounts\n\nConfigure filesystem backends at boot time.\n\nNative mount plugins (host directories, S3, etc.) are passed via `plugin`, each\nidentified by an `id` and a `config` object.\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n mounts: [\n // Host directory (read-only)\n {\n path: \"/mnt/code\",\n plugin: { id: \"host_dir\", config: { hostPath: \"/path/to/repo\" } },\n readOnly: true,\n },\n // S3 bucket\n {\n path: \"/mnt/data\",\n plugin: { id: \"s3\", config: { bucket: \"my-bucket\", prefix: \"agent/\" } },\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n## `agentOS()` configuration reference\n\nWhen you use the [`agentOS()` actor](/docs/quickstart), all VM configuration is passed to the factory as a single flat object. This is the consolidated config block to copy and adapt:\n\n```ts title=\"server.ts\"\nimport { agentOS, nodeModulesMount, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n // Filesystems to mount at boot. Use nodeModulesMount() to expose a host\n // node_modules tree at /root/node_modules.\n mounts: [nodeModulesMount(\"/path/to/project/node_modules\")],\n // Software packages to install in the VM (see /docs/software)\n software: [pi],\n // Ports exempt from SSRF checks\n loopbackExemptPorts: [3000],\n // Extra instructions appended to agent system prompts\n additionalInstructions: \"Always write tests first.\",\n\n // Preview URL token lifetimes\n preview: {\n defaultExpiresInSeconds: 3600, // 1 hour (default)\n maxExpiresInSeconds: 86400, // 24 hours (default)\n },\n\n // Lifecycle hooks (see below)\n onSessionEvent: async (sessionId, event) => {\n console.log(\"Session event:\", sessionId, event.method);\n },\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"Permission request:\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\nThe top-level fields are documented inline above. See [Mounts](#mounts), [Software](/docs/software), and (for the hooks) [Approvals](/docs/approvals).\n\n### Lifecycle hooks\n\n`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.\n\n```ts title=\"server.ts\"\nimport { agentOS } from \"@rivet-dev/agentos\";\n\nexport const vm = agentOS({\n // Runs once per session event, server-side, for every session.\n onSessionEvent: async (sessionId, event) => {\n console.log(\"Session event:\", sessionId, event.method);\n },\n});\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/core)*\n\n### Timeouts\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| Action timeout | 15 minutes | Maximum time for any single action |\n| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |\n\nThese are set internally by the `agentOS()` factory and cannot be overridden per-call. See [Persistence & Sleep](/docs/persistence) for details on the sleep lifecycle.","src/content/docs/docs/core.mdx","4819d30fb220121c","docs/cost-evaluation",{"id":91,"data":93,"body":99,"filePath":100,"digest":101,"deferredRender":16},{"title":94,"description":95,"editUrl":16,"head":96,"template":18,"sidebar":97,"pagefind":16,"draft":20},"Cost Evaluation","How agentOS compares on cost to per-second sandbox providers when you run coding-agent VMs on your own hardware.",[],{"hidden":20,"attrs":98},{},"import { Aside } from '@astrojs/starlight/components';\n\nagentOS is a library you run on hardware you already control, not a metered service. That changes the cost model for running coding-agent VMs from \"pay a provider per sandbox-second\" to \"pay for the compute you provision, then pack as much work onto it as it can hold.\" This page explains where the savings come from and how to reason about them honestly. It does not publish a single magic multiplier, because the real number depends on your workload, your hardware, and how you share VMs.\n\n\u003CAside type=\"note\">For measured latency (cold start, warm execution, and reuse fast paths), see [Benchmarks](/docs/benchmarks). This page is about cost structure, not raw performance.\u003C/Aside>\n\n## Where the savings come from\n\nTwo structural differences drive the cost gap versus per-second sandbox providers:\n\n- **You run on your own hardware**: you choose the cloud, instance type, architecture, and region. A small commodity instance (for example an ARM VM from a budget host) costs a flat hourly or monthly rate that is typically far below what per-sandbox-second billing adds up to once you have steady agent traffic. You also avoid egress fees and vendor lock-in.\n- **You decide the isolation granularity**: sandbox providers bill a full container or microVM per execution, usually with a minimum memory reservation that you pay for even when your code uses a fraction of it. With agentOS you own the VM lifecycle: you can dedicate a VM per tenant or per task for maximum isolation, or amortize setup by reusing one VM across many runs.\n\n## The isolation model matters for cost\n\nEach `AgentOs.create()` boots a fully virtualized VM, and each `exec()` / `run()` inside it is a fresh guest process. That gives you a dial between isolation and density:\n\n- **One VM per task or tenant (strongest isolation)**: create a VM, run the work, and dispose it, or give each tenant its own VM. Each VM is its own crash and resource domain, with the highest per-VM overhead. Best when load is untrusted or bursty.\n- **A shared VM for trusted work**: reuse one VM across many runs to amortize the VM boot cost. Each `exec()` / `run()` still executes in a fresh guest process, so in-memory state does not leak between runs, but the VM and filesystem are shared. Good for trusted, sequential work.\n\nThe denser you can safely pack agent work onto an instance, the lower your effective cost per execution. See [Resource Limits](/docs/resource-limits) for the per-VM caps that govern how densely you can pack, and [Processes & Shell](/docs/processes) for how guest processes run inside a VM.\n\n## How to estimate your own cost\n\nBecause agentOS runs on hardware you provision, the honest way to compare is to plug in your own numbers:\n\n1. **Pick your hardware and its rate**: take the hourly or monthly price of the instance you would run on, and divide down to a per-second instance cost.\n2. **Estimate how many concurrent VMs fit**: measure per-VM memory overhead on your target hardware under your isolation strategy, then divide your usable RAM by that figure. Leave headroom (the measurement and any orchestration layer will not bin-pack perfectly).\n3. **Divide instance cost by concurrent VMs**: that gives a cost-per-VM-second you can compare against a provider's per-sandbox-second rate.\n\n\u003CAside type=\"tip\">Measure on the hardware and isolation strategy you will actually deploy. Per-VM overhead depends on whether you create a fresh VM per task or reuse one across runs, and on the work the agent does, so a number measured on one machine will not transfer cleanly to another.\u003C/Aside>\n\n## Comparing against sandbox providers\n\nWhen you do compare against a per-second sandbox provider, hold the methodology honest:\n\n- **Sandbox cost** is the provider's minimum allocatable memory times their per-GiB-second rate (plus any egress and platform fees). The minimum reservation is the floor you pay even for tiny workloads.\n- **agentOS cost** is your instance cost per second divided by the number of VMs you can keep live on it, with realistic headroom for bin-packing inefficiency.\n\nThe advantage is largest for **many small, short executions**, where a per-sandbox minimum reservation dominates and your own hardware lets you pack densely. It narrows for **heavyweight, long-lived workloads** (for example dev servers that need hundreds of megabytes regardless), where the win shifts from density to hardware choice: you still avoid per-second metering, egress fees, and lock-in, but the raw memory-density advantage is smaller.\n\n| Workload | Primary cost advantage |\n| --- | --- |\n| Many small, short executions | Density: pack many VMs per instance, no per-sandbox minimum |\n| Heavyweight, long-lived workloads | Hardware choice, flat instance pricing, no egress or lock-in |\n| High concurrency | Reuse a VM across runs to amortize VM boot cost |\n\n\u003CAside type=\"caution\">Be careful generalizing cost ratios from a single benchmark. Provider pricing, instance pricing, and exchange rates change over time, and per-VM overhead varies by workload and isolation strategy. Re-measure on your own hardware before quoting a number.\u003C/Aside>\n\nWhen you do need a full Linux sandbox for heavier agent workloads, see [agentOS vs Sandbox](/docs/versus-sandbox) for how the two models combine.","src/content/docs/docs/cost-evaluation.mdx","896bc95a6238b39c","docs/crash-course",{"id":102,"data":104,"body":110,"filePath":111,"digest":112,"deferredRender":16},{"title":105,"description":106,"editUrl":16,"head":107,"template":18,"sidebar":108,"pagefind":16,"draft":20},"Crash Course","Run coding agents inside isolated VMs with full filesystem, process, and network control.",[],{"hidden":20,"attrs":109},{},"import { Aside } from '@astrojs/starlight/components';\nimport CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\n\u003CAside type=\"note\">\nagentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).\n\u003C/Aside>\n\n{/* SKILL_OVERVIEW_START */}\n\n## When to Use agentOS\n\n- **Coding agents**: Run any coding agent with full OS access, file editing, shell execution, and tool use.\n- **Automated pipelines**: CI-like workflows where agents clone repos, fix bugs, run tests, and open PRs.\n- **Multi-agent systems**: Coordinators dispatching to specialized agents, review pipelines, planning chains.\n- **Scheduled maintenance**: Cron-based agents that audit code, update dependencies, or generate reports.\n- **Collaborative workspaces**: Multiple users observing and interacting with the same agent session in realtime.\n\n## Minimal Project\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Subscribe to streaming events\nconst conn = agent.connect();\nconn.on(\"sessionEvent\", (data) => {\n console.log(data.event);\n});\n\n// Create a session and send a prompt\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconst response = await agent.sendPrompt(\n session.sessionId,\n \"Write a hello world script to /workspace/hello.js\",\n);\nconsole.log(response.text);\n\n// Read the file the agent created\nconst content = await agent.readFile(\"/workspace/hello.js\");\nconsole.log(new TextDecoder().decode(content));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course)*\n\nAfter the quickstart, customize your agent with the [Registry](/registry).\n\n## Agents\n\n### Sessions & Transcripts\n\nCreate agent sessions, send prompts, and stream responses in realtime. Transcripts are persisted automatically across sleep/wake cycles.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Stream events as they arrive\nconst conn = agent.connect();\nconn.on(\"sessionEvent\", (data) => {\n console.log(data.event.method, data.event);\n});\n\n// Create a session\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Send a prompt and wait for the response\nconst response = await agent.sendPrompt(\n session.sessionId,\n \"List all files in the home directory\",\n);\nconsole.log(response.text);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/sessions)*\n\n### Approvals\n\nApprove or deny agent tool use with human-in-the-loop patterns or auto-approve for trusted workloads.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\n// Auto-approve all permissions server-side\nconst vm = agentOS({\n software: [pi],\n onPermissionRequest: async (sessionId, request) => {\n console.log(\"Auto-approving\", sessionId, request.permissionId);\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Or handle permissions client-side for human-in-the-loop\nconst conn = agent.connect();\nconn.on(\"permissionRequest\", async (data) => {\n console.log(\"Permission requested:\", data.request);\n // \"once\" | \"always\" | \"reject\"\n await agent.respondPermission(data.sessionId, data.request.permissionId, \"once\");\n});\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/approvals)*\n\n### Bindings\n\nExpose your JavaScript functions to agents as CLI commands inside the VM. Each binding group becomes a binary at `/usr/local/bin/agentos-{name}`, and each binding becomes a subcommand with flags auto-generated from its Zod input schema. The server below defines a `weather` binding group with a `forecast` binding; the client opens a session and prompts the agent, which calls the binding itself as a shell command.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { z } from \"zod\";\n\n// Define a group of bindings (host functions). Each binding has a Zod input\n// schema and an `execute` handler that runs on the host. Bindings are exposed to\n// the agent as CLI commands at /usr/local/bin/agentos-{name} inside the VM.\nconst weatherBindings = {\n name: \"weather\",\n description: \"Weather data bindings\",\n bindings: {\n forecast: {\n description: \"Get the weather forecast for a city\",\n inputSchema: z.object({\n city: z.string().describe(\"City name\"),\n days: z.number().optional().describe(\"Number of days\"),\n }),\n execute: async (input: { city: string; days?: number }) => {\n const res = await fetch(\n `https://api.weather.example/forecast?city=${input.city}&days=${input.days ?? 3}`,\n );\n return res.json();\n },\n examples: [\n { description: \"3-day forecast for Paris\", input: { city: \"Paris\", days: 3 } },\n ],\n },\n },\n};\n\nconst vm = agentOS({\n bindings: [weatherBindings],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// The agent invokes the binding itself as a shell command:\n// agentos-weather forecast --city Paris --days 3\nconst session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"What's the weather in Paris?\");\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/bindings) or [Documentation](/docs/bindings)*\n\n### Agent-to-Agent\n\nLet one agent call another through a [binding](/docs/bindings). The coder gets a `review` binding it invokes itself, which bridges into the reviewer's isolated VM.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport { z } from \"zod\";\nimport pi from \"@agentos-software/pi\";\n\n// The reviewer is its own isolated agent VM.\nconst reviewer = agentOS({ software: [pi] });\n\n// The coder gets a `review` binding it can call itself: it copies a file from the\n// coder's VM into the reviewer's VM and asks the reviewer to review it.\nconst coder = agentOS({\n software: [pi],\n bindings: [\n {\n name: \"review\",\n description: \"Send a file to the reviewer agent and get back a review.\",\n bindings: {\n submit: {\n description: \"Submit a file path for review by the reviewer agent.\",\n inputSchema: z.object({ path: z.string() }),\n execute: async ({ path }: { path: string }) => {\n const client = createClient\u003Ctypeof registry>({\n endpoint: \"http://localhost:6420\",\n });\n const content = await client.coder\n .getOrCreate(\"feature-auth\")\n .readFile(path);\n const reviewerHandle = client.reviewer.getOrCreate(\"feature-auth\");\n await reviewerHandle.writeFile(path, content);\n const session = await reviewerHandle.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n const result = await reviewerHandle.sendPrompt(\n session.sessionId,\n `Review ${path} for security issues`,\n );\n return { review: result.text };\n },\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { coder, reviewer } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./agent-to-agent-server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst coderAgent = client.coder.getOrCreate(\"feature-auth\");\nconst session = await coderAgent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// The coder implements the feature, then calls the `review` binding itself so the\n// reviewer agent reviews the code. This is true agent-to-agent: the coder drives it.\nawait coderAgent.sendPrompt(\n session.sessionId,\n \"Implement the login feature in /home/agentos/src/auth.ts, then run `agentos-review submit --path /home/agentos/src/auth.ts` to have it reviewed.\",\n);\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/agent-to-agent)*\n\n### Multiplayer & Realtime\n\nConnect multiple clients to the same agent VM. All subscribers see session output, process logs, and shell data in realtime.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\n// Client A: creates the session and sends prompts\nconst clientA = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agentA = clientA.vm.getOrCreate(\"shared-agent\");\nconst connA = agentA.connect();\nconnA.on(\"sessionEvent\", (data) =>\n console.log(\"[A]\", data.event.method),\n);\n\nconst session = await agentA.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agentA.sendPrompt(session.sessionId, \"Build a REST API\");\n\n// Client B: observes the same session (separate process)\nconst clientB = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst connB = clientB.vm.getOrCreate(\"shared-agent\").connect();\nconnB.on(\"sessionEvent\", (data) =>\n console.log(\"[B]\", data.event.method),\n);\n// Client B sees the same events as Client A\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/multiplayer)*\n\n### Workflows\n\nOrchestrate multi-step agent tasks with durable workflows that survive crashes and restarts.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { actor, queue } from \"rivetkit\";\nimport { workflow } from \"rivetkit/workflow\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\n// A durable workflow actor. Its `run` is built with `workflow()`, so every\n// `step(...)` is recorded, retried, and resumed: if the process crashes\n// mid-run, replay skips completed steps and continues where it left off.\nconst bugFixer = actor({\n queues: {\n fixBug: queue\u003C{ repo: string; issue: string }>(),\n },\n run: workflow(async (ctx) => {\n await ctx.loop(\"fix-bug-loop\", async (loopCtx) => {\n // Wait durably for the next bug-fix request from the queue.\n const message = await loopCtx.queue.next(\"wait-fix-bug\");\n const { repo, issue } = message.body;\n const agent = loopCtx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n\n await loopCtx.step(\"clone-repo\", () =>\n agent.exec(`git clone ${repo} /home/agentos/repo`),\n );\n\n await loopCtx.step(\"fix-bug\", async () => {\n const session = await agent.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agent.sendPrompt(session.sessionId, `Fix the bug in issue: ${issue}`);\n await agent.closeSession(session.sessionId);\n });\n\n await loopCtx.step(\"run-tests\", () =>\n agent.exec(\"cd /home/agentos/repo && npm test\"),\n );\n });\n }),\n});\n\nexport const registry = setup({ use: { vm, bugFixer } });\nregistry.start();\n```\n\n[Documentation](/docs/workflows)\n\n## Operating System\n\n### Filesystem\n\nRead, write, and manage files inside the VM. The `/home/agentos` directory is persisted automatically across sleep/wake cycles.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Write a file\nawait agent.writeFile(\"/home/agentos/config.json\", JSON.stringify({ key: \"value\" }));\n\n// Read a file\nconst content = await agent.readFile(\"/home/agentos/config.json\");\nconsole.log(new TextDecoder().decode(content));\n\n// List directory contents recursively\nconst files = await agent.readdirRecursive(\"/home/agentos\", { maxDepth: 2 });\nconsole.log(files);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/filesystem)*\n\n### Processes & Shell\n\nExecute commands, spawn long-running processes, and open interactive shells.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// One-shot execution\nconst result = await agent.exec(\"echo hello && ls /home/agentos\");\nconsole.log(\"stdout:\", result.stdout);\nconsole.log(\"exit code:\", result.exitCode);\n\n// Spawn a long-running process\nconst conn = agent.connect();\nconn.on(\"processOutput\", (data) => {\n console.log(`[pid ${data.pid}]`, new TextDecoder().decode(data.data));\n});\n\nconst { pid } = await agent.spawn(\"node\", [\"/home/agentos/server.js\"]);\nconsole.log(\"Process ID:\", pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/processes)*\n\n### Networking & Previews\n\nProxy HTTP requests into VMs with `vmFetch`. Create preview URLs for port forwarding VM services to shareable public URLs.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Fetch from a service running inside the VM\nconst response = await agent.vmFetch(3000, \"/api/health\");\nconsole.log(\"Status:\", response.status);\n\n// Create a preview URL (port forwarding to a public URL)\nconst preview = await agent.createSignedPreviewUrl(3000);\nconsole.log(\"Public URL:\", preview.path);\nconsole.log(\"Expires at:\", new Date(preview.expiresAt));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/networking)*\n\n### Cron Jobs\n\nSchedule recurring commands and agent sessions with cron expressions.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Schedule a command every hour\nawait agent.scheduleCron({\n schedule: \"0 * * * *\",\n action: { type: \"exec\", command: \"rm\", args: [\"-rf\", \"/tmp/cache/*\"] },\n});\n\n// Schedule an agent session daily at 9 AM\nawait agent.scheduleCron({\n schedule: \"0 9 * * *\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Review the codebase for security issues and write a report to /home/agentos/audit.md\",\n },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*See [Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/crash-course) or [Documentation](/docs/cron)*\n\n### Sandbox Mounting\n\nagentOS uses a hybrid model: agents run in a lightweight VM by default and mount a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. Mount the sandbox as a filesystem and expose its process management as bindings.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createSandboxFs, createSandboxBindings } from \"@rivet-dev/agentos-sandbox\";\nimport { SandboxAgent } from \"sandbox-agent\";\nimport { docker } from \"sandbox-agent/docker\";\n\nconst sandbox = await SandboxAgent.start({ sandbox: docker() });\n\nconst vm = agentOS({\n // Bindings let the agent control the sandbox\n bindings: [createSandboxBindings({ client: sandbox })],\n // Mounts let the agent read the sandbox filesystem (optional)\n mounts: [\n { path: \"/home/agentos/sandbox\", plugin: createSandboxFs({ client: sandbox }) },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n[Documentation](/docs/sandbox)\n\n{/* SKILL_OVERVIEW_END */}","src/content/docs/docs/crash-course.mdx","df70f0a88a28da3f","docs/cron",{"id":113,"data":115,"body":121,"filePath":122,"digest":123,"deferredRender":16},{"title":116,"description":117,"editUrl":16,"head":118,"template":18,"sidebar":119,"pagefind":16,"draft":20},"Cron Jobs","Schedule recurring commands and agent sessions in agentOS VMs.",[],{"hidden":20,"attrs":120},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nSchedule recurring work with cron expressions, running either a shell command (`exec`) or an agent session (`session`), with overlap modes (`allow`, `skip`, `queue`) and `cronEvent` streaming to monitor execution. Cron jobs keep the actor alive while a job runs; the actor can sleep between executions.\n\n## Schedule a command\n\nRun a shell command on a recurring schedule. Pass a custom `id` to make a job easier to manage and cancel later.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Schedule a cleanup script every hour\nconst { id } = await client.vm.getOrCreate(\"my-agent\").scheduleCron({\n schedule: \"0 * * * *\",\n action: {\n type: \"exec\",\n command: \"rm\",\n args: [\"-rf\", \"/tmp/cache/*\"],\n },\n});\nconsole.log(\"Cron job ID:\", id);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Schedule an agent session\n\nCreate a recurring agent session that runs a prompt on a schedule.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Run an agent every day at 9 AM to check for issues\nawait client.vm.getOrCreate(\"my-agent\").scheduleCron({\n schedule: \"0 9 * * *\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Review the logs in /workspace/logs/ and summarize any errors\",\n options: { cwd: \"/workspace\" },\n },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Overlap modes\n\nControl what happens when a cron job triggers while a previous execution is still running.\n\n| Mode | Behavior |\n|------|----------|\n| `\"skip\"` | Skip this trigger if the previous run is still active |\n| `\"allow\"` | Allow concurrent executions (default) |\n| `\"queue\"` | Queue this trigger and run it after the previous one finishes |\n\nPrefer `\"skip\"` for most jobs to avoid unbounded concurrency if a run takes longer than the interval. Use `\"queue\"` when every trigger must eventually execute.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Queue overlapping executions\nawait client.vm.getOrCreate(\"my-agent\").scheduleCron({\n schedule: \"*/5 * * * *\",\n overlap: \"queue\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Process the next batch of tasks\",\n },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Monitor cron events\n\nSubscribe to the `cronEvent` event to track job execution. It is emitted whenever a cron job runs, carrying a single payload field:\n\n- **`data.event`**: A `CronEvent` describing the run.\n\n```ts\nconst conn = handle.connect();\nconn.on(\"cronEvent\", (data) => {\n // data is inferred: { event: CronEvent }\n console.log(\"Cron event:\", data.event);\n});\n```\n\nSubscribe before scheduling so you do not miss early runs.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.vm.getOrCreate(\"my-agent\");\n\nconst conn = handle.connect();\nconn.on(\"cronEvent\", (data) => {\n // data is inferred: { event: CronEvent }\n console.log(\"Cron event:\", data.event);\n});\n\nawait handle.scheduleCron({\n schedule: \"*/1 * * * *\",\n action: { type: \"exec\", command: \"echo\", args: [\"heartbeat\"] },\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## List and cancel cron jobs\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.vm.getOrCreate(\"my-agent\");\n\n// List all cron jobs\nconst jobs = await handle.listCronJobs();\nfor (const job of jobs) {\n console.log(job.id, job.schedule);\n}\n\n// Cancel a specific job\nawait handle.cancelCronJob(jobs[0].id);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\n## Example: Heartbeat pattern\n\nSchedule a recurring agent session to periodically check on a task. This is the core pattern behind [OpenClaw](https://openclaw.org), where an agent wakes up on a schedule to review progress, take action, and go back to sleep.\n\n```ts\nawait handle.scheduleCron({\n schedule: \"*/30 * * * *\",\n overlap: \"skip\",\n action: {\n type: \"session\",\n agentType: \"pi\",\n prompt: \"Check the status of open issues and take any necessary action\",\n },\n});\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/cron)*\n\nThe agent sleeps between executions and only consumes resources when the cron job fires.","src/content/docs/docs/cron.mdx","0503c0250522732d","docs/deployment",{"id":124,"data":126,"body":132,"filePath":133,"digest":134,"deferredRender":16},{"title":127,"description":128,"editUrl":16,"head":129,"template":18,"sidebar":130,"pagefind":16,"draft":20},"Deploy","Choose the right deployment option for agentOS.",[],{"hidden":20,"attrs":131},{},"agentOS is powered by [Rivet](https://rivet.dev), an open-source actor platform, and runs as Rivet Actors. Three ways to run it in production:\n\n- **[Rivet Cloud](https://rivet.dev/cloud)**: fully managed (Rivet Compute, or bring your own cloud). Zero-ops.\n- **Self-hosted**: run the open-source Rivet platform on your own infrastructure (Kubernetes, Hetzner, VMs, and more) for full control.\n- **[agentOS Core](/docs/core)**: embed `@rivet-dev/agent-os-core` directly in any Node.js backend, no platform required.\n\nPick a deploy target below, or see [Rivet's deployment guides](https://rivet.dev/docs/deploy/).\n\n## Deploy targets\n\nSee the [Rivet deploy docs](https://rivet.dev/docs/deploy/) for the full list. Available targets:\n\n\u003Cdiv style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem;margin:1.5rem 0;\">\n \u003Ca href=\"https://rivet.dev/docs/deploy/rivet-compute\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 640 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M225.1 16C145 16 80 81 80 161.1l0 189.8C80 431 145 496 225.1 496l189.8 0C495 496 560 431 560 350.9l0-189.8C560 81 495 16 414.9 16L225.1 16zm-1.4 42.4l192.6 0c55.5 0 100.5 45 100.5 100.5l0 192.6c0 55.5-45 100.5-100.5 100.5l-192.6 0c-55.5 0-100.5-45-100.5-100.5l0-192.6c0-55.5 45-100.5 100.5-100.5zM283 161.2c0-2.9-2.5-5.2-5.5-5.2l-41.2 0c-13.8 0-24.9 10.6-24.9 23.6l0 156.2c0 13 11.2 23.6 24.9 23.6l41.2 0c3 0 5.5-2.3 5.5-5.2l0-193zm119 128.7c-6-10.3-19.7-18.6-30.6-18.6l-63 0c-3.3 0-4.5 2.6-2.7 5.7l37.6 63.8c6 10.3 19.7 18.6 30.6 18.6l63 0c3.3 0 4.5-2.6 2.7-5.7l-37.6-63.8zm2.6-86.4c0-26.2-22.4-47.5-50.1-47.5l-44.2 0c-3.3 0-5.9 2.5-5.9 5.6l0 83.8c0 3.1 2.6 5.6 5.9 5.6l44.2 0c27.6 0 50.1-21.3 50.1-47.5z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Rivet Compute\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy to Rivet's managed compute platform.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/vercel\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M256 32L512 480 0 480 256 32z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Vercel\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy Next.js + RivetKit apps to Vercel's edge network.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/supabase\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M253.9 22.1c-.3-21-26.9-30.1-40-13.6L16.3 257.1c-23.3 29.4-2.4 72.6 35.1 72.6l204.4 0 2.4 160.2c.3 21 26.9 30 40 13.6L495.7 254.9c23.3-29.3 2.4-72.6-35.1-72.6l-205.7 0z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Supabase Functions\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Run on Supabase Edge Functions with the WebAssembly runtime.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/railway\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M2.4 219.1c-1.2 8.5-2 17.2-2.4 25.8l388.9 0c-1.4-2.7-3.2-5-5-7.4-66.5-85.9-102.2-78.4-153.4-80.6-17.1-.7-28.6-1-96.5-1-36.3 0-75.8 .1-114.3 .2-5 13.4-9.8 26.5-12.1 37.1l199.3 0 0 26-204.4 0zM392 270.8L.2 270.8c.4 6.9 1.1 13.8 2 20.5l361.7 0c16.1 0 25.1-9.1 28.1-20.5zM22.5 362.2S82.5 509.4 255.7 512c103.6 0 192.5-61.5 233-149.8l-466.2 0zM255.7 0C160 0 76.7 52.6 32.7 130.3 67 130.2 134 130.2 134 130.2l0 0 0 0c79.2 0 82.1 .4 97.6 1l9.6 .4c33.4 1.1 74.3 4.7 106.6 29.1 17.5 13.2 42.8 42.4 57.9 63.3 13.9 19.2 17.9 41.4 8.5 62.6-8.7 19.5-27.5 31.1-50.2 31.1l-355.5 0s2.1 9 5.3 18.9l485.3 0c8.6-25.9 13-53 13-80.3 0-141.4-114.7-256.1-256.3-256.1z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Railway\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy containers to Railway's managed infrastructure.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/kubernetes\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M242.1 275l13.8 6.7 13.8-6.6 3.4-14.9-9.6-12-15.4 0-9.6 11.9 3.4 14.9zm-14.7-52.9l0 0c2.5 1.8 5.8 2.1 8.6 .8s4.6-4.1 4.7-7.2l0 0 .2-.1 2.9-50.3c-3.5 .4-6.9 1-10.1 1.8-17.9 4-34.4 13-47.6 25.9l41.2 29.2zm-22.1 38.1l0 0c3-.8 5.3-3.2 6-6.2s-.3-6.2-2.7-8.2l0 0 0-.2-37.6-33.7c-11.5 18.7-16.4 40.6-14 62.4l48.2-13.9zM221.8 296c-1-4.4-5.3-7.2-9.7-6.4l-.1 0-.1-.1-49.5 8.4c7.4 20.5 21.4 38 39.8 49.8l19.2-46.3-.1-.2 0 0c.7-1.6 .9-3.4 .5-5.2zm41.6 18.3l0 0c-1.5-2.8-4.5-4.5-7.7-4.4-3 .1-5.7 1.8-7.1 4.4l0 0 0 0-24.3 44c17.4 5.9 36.1 6.8 54.1 2.8 3.3-.7 6.6-1.7 9.7-2.7l-24.4-44.1zm36.4-24.8l-.1 0c-.6-.1-1.3-.2-1.9-.1-2.7 .2-5.2 1.6-6.7 4s-1.7 5.2-.6 7.7l0 0-.1 .1 19.4 46.8c18.5-11.8 32.5-29.5 39.9-50.1l-49.9-8.4zm40.8-77.5l-37.4 33.5 0 .1 0 0c-2.3 2-3.4 5.2-2.7 8.2s3 5.4 6 6.2l.1 0 0 .2 48.5 14c2.1-21.8-3-43.6-14.4-62.2zM271 215.5l0 0c.1 1.8 .7 3.5 1.8 4.9 2.8 3.5 7.9 4.2 11.5 1.5l0 0 .1 .1 40.9-29c-15.6-15.2-35.7-25-57.3-27.7l2.8 50.2zm215.1 89.1L446.4 132.3c-2.1-9.1-8.2-16.7-16.6-20.7L269.3 35c-8.4-4-18.2-4-26.6 0L82.1 111.7c-8.4 4-14.5 11.6-16.6 20.7L25.9 304.6c-1.8 8-.3 16.5 4.2 23.3 .5 .8 1.1 1.6 1.7 2.4L142.9 468.5c5.9 7.3 14.7 11.5 24 11.5l178.2 0c9.3 0 18.1-4.2 24-11.4L480.2 330.4c5.8-7.2 8-16.7 5.9-25.8zm-61-2.1c-1.6 5.4-7.1 8.5-12.5 7.1l-.1 0-.1 0-.1 0-.1 0-.9-.2c-.6-.1-1.1-.2-1.6-.3-2-.6-3.9-1.3-5.7-2.1-.9-.4-1.9-.8-2.9-1.2l-.3-.1c-5.3-2.1-10.8-3.8-16.5-4.8-1.7-.1-3.4 .4-4.7 1.6l-.6 .4 0 0 0 0c-.7-.1-2.8-.5-4-.7-9.3 29.2-29 54-55.3 69.6 .1 .4 .3 .8 .5 1.3 .3 .9 .6 1.7 1.1 2.5l0 0 0 0-.3 .7c-.8 1.5-1 3.3-.5 4.9 2.3 5.3 5.2 10.4 8.5 15.2 .6 .9 1.2 1.7 1.8 2.5 1.3 1.6 2.4 3.3 3.4 5.1 .3 .5 .6 1.2 .9 1.8l.4 .8c1.9 3.2 1.8 7.2-.2 10.3s-5.6 4.8-9.3 4.4-6.8-2.9-8.1-6.4l-.4-.7c-.3-.6-.6-1.2-.8-1.7-.8-1.9-1.4-3.9-1.9-5.9-.3-1-.5-1.9-.9-3l-.1-.3c-1.6-5.5-3.8-10.8-6.5-15.9-1-1.4-2.5-2.4-4.2-2.7l-.7-.2 0 0 0 0c-.2-.3-.5-1-1-1.7-.4-.7-.8-1.4-1.1-1.9-5.4 2-10.8 3.7-16.4 4.9-24 5.5-49.2 3.7-72.2-5.2l-2.2 3.9 0 0c-1.5 .2-3 .9-4.1 2-2.5 3.5-4.3 7.4-5.4 11.5-.6 1.9-1.3 3.8-2 5.7-.3 1-.6 2-.9 3-.5 2-1.1 3.9-1.9 5.8-.2 .5-.5 1.1-.8 1.6l-.4 .8 0 0 0 0c-1.7 3.7-5.4 6.2-9.5 6.3-1.3 0-2.6-.3-3.8-.9-4.9-2.7-6.7-8.8-4.1-13.8 .1-.3 .3-.6 .4-.9 .3-.6 .6-1.2 .8-1.7 1-1.8 2.1-3.5 3.4-5.2 .6-.8 1.2-1.6 1.8-2.5 3.4-4.9 6.3-10.1 8.7-15.6 .3-1.8 0-3.7-.9-5.4l0 0 0 0 1.7-4.1c-26.3-15.5-45.9-40.1-55.4-69.1l-4.1 .7 0 0-.4-.2c-1.5-1-3.2-1.6-5.1-1.7-5.6 1.1-11.1 2.7-16.5 4.8l-.3 .1c-1 .4-1.9 .8-2.9 1.2-1.9 .9-3.8 1.6-5.8 2.1-.5 .1-1.1 .3-1.8 .4l-.7 .2-.1 0-.1 0-.1 0-.1 0c-3.5 1.2-7.4 .2-10-2.4s-3.5-6.5-2.3-10 4.3-6 7.9-6.5l.1 0 .1 0 0 0 .1 0 .8-.2c.6-.2 1.3-.3 1.8-.4 2-.4 4-.5 6.1-.6 1 0 2.1-.1 3.1-.2l.2 0c5.7-.4 11.4-1.3 17-2.8 1.5-.9 2.8-2.2 3.7-3.6l.4-.5 0 0 0 0 4-1.1c-4.3-30.3 2.6-61.1 19.6-86.6l-3-2.7 0 0-.1-.4c-.1-1.8-.8-3.6-1.8-5.1-4.4-3.8-9.2-7.2-14.3-10-.9-.5-1.9-1-2.7-1.5-1.8-.9-3.6-2-5.2-3.2-.4-.3-.9-.7-1.4-1.1l-.6-.5-.1 0-.1-.1c-2.2-1.7-3.7-4.2-4.2-7-.5-2.6 .2-5.3 1.8-7.3 1.9-2.3 4.8-3.6 7.7-3.4 2.5 .1 4.8 1 6.7 2.5l.6 .5c.5 .4 1.1 .9 1.6 1.2 1.5 1.4 3 2.8 4.3 4.4 .6 .8 1.3 1.5 2.1 2.3l.2 .2c3.9 4.2 8.2 8.1 12.8 11.5 1.5 .9 3.3 1.1 5 .6l.7-.1 0 0 0 0c.6 .4 2.4 1.7 3.4 2.4 16.9-17.9 38.7-30.4 62.8-35.8 5.7-1.3 11.5-2.2 17.3-2.7l.2-4 0 0c1.5-1.3 2.6-3 3.1-4.9 .2-5.8-.1-11.6-1.1-17.3l0-.1c-.1-1.1-.3-2.1-.5-3.1-.4-2-.7-4-.8-6.1 0-.5 0-1.1 0-1.7l0-.8 0-.1 0-.1c-.4-3.7 1.4-7.3 4.5-9.3s7.2-2 10.3 0 4.9 5.6 4.5 9.3l0 1c0 .6 0 1.3 0 1.8-.1 2-.4 4.1-.8 6.1-.2 1-.4 2-.5 3.1l-.1 .5c-.9 5.6-1.2 11.3-1 16.9 .3 1.7 1.2 3.2 2.6 4.3l.5 .5 0 0 0 0c0 .7 .1 2.9 .2 4.2 15.1 1.4 29.8 5.5 43.5 12.1 13.5 6.6 25.8 15.5 36.2 26.4l3.6-2.6 0 0 .5 0c1.8 .3 3.6 .1 5.3-.7 4.6-3.4 8.8-7.2 12.6-11.4l.3-.4c.7-.8 1.4-1.5 2-2.3 1.3-1.6 2.8-3.1 4.3-4.5 .4-.4 1-.8 1.5-1.2l.6-.5c2.7-2.6 6.6-3.5 10.1-2.2s6 4.4 6.4 8.1-1.3 7.3-4.4 9.3l-.7 .6c-.5 .4-1 .8-1.4 1.1-1.7 1.2-3.4 2.2-5.2 3.1-.9 .5-1.8 1-2.8 1.5-5.1 2.9-9.8 6.2-14.3 10-1.2 1.3-1.8 3-1.7 4.7l-.1 .7 0 0 0 0c-.3 .2-.8 .7-1.5 1.3s-1.4 1.2-1.9 1.7c17 25.4 24.1 56.1 20 86.4l3.8 1.1 0 0 .3 .4c.9 1.6 2.2 2.9 3.8 3.7 5.6 1.5 11.3 2.4 17 2.8l.2 0c1.1 .1 2.1 .1 3.1 .2 2 0 4.1 .2 6.1 .6 .5 .1 1.1 .2 1.8 .4l1 .2c5.5 1.1 9.1 6.3 8.2 11.8z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Kubernetes\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy to any Kubernetes cluster with container images.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/aws-ecs\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 640 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M180.41 203.01c-.72 22.65 10.6 32.68 10.88 39.05a8.164 8.164 0 0 1-4.1 6.27l-12.8 8.96a10.66 10.66 0 0 1-5.63 1.92c-.43-.02-8.19 1.83-20.48-25.61a78.608 78.608 0 0 1-62.61 29.45c-16.28.89-60.4-9.24-58.13-56.21-1.59-38.28 34.06-62.06 70.93-60.05 7.1.02 21.6.37 46.99 6.27v-15.62c2.69-26.46-14.7-46.99-44.81-43.91-2.4.01-19.4-.5-45.84 10.11-7.36 3.38-8.3 2.82-10.75 2.82-7.41 0-4.36-21.48-2.94-24.2 5.21-6.4 35.86-18.35 65.94-18.18a76.857 76.857 0 0 1 55.69 17.28 70.285 70.285 0 0 1 17.67 52.36l-.01 69.29zM93.99 235.4c32.43-.47 46.16-19.97 49.29-30.47 2.46-10.05 2.05-16.41 2.05-27.4-9.67-2.32-23.59-4.85-39.56-4.87-15.15-1.14-42.82 5.63-41.74 32.26-1.24 16.79 11.12 31.4 29.96 30.48zm170.92 23.05c-7.86.72-11.52-4.86-12.68-10.37l-49.8-164.65c-.97-2.78-1.61-5.65-1.92-8.58a4.61 4.61 0 0 1 3.86-5.25c.24-.04-2.13 0 22.25 0 8.78-.88 11.64 6.03 12.55 10.37l35.72 140.83 33.16-140.83c.53-3.22 2.94-11.07 12.8-10.24h17.16c2.17-.18 11.11-.5 12.68 10.37l33.42 142.63L420.98 80.1c.48-2.18 2.72-11.37 12.68-10.37h19.72c.85-.13 6.15-.81 5.25 8.58-.43 1.85 3.41-10.66-52.75 169.9-1.15 5.51-4.82 11.09-12.68 10.37h-18.69c-10.94 1.15-12.51-9.66-12.68-10.75L328.67 110.7l-32.78 136.99c-.16 1.09-1.73 11.9-12.68 10.75h-18.3zm273.48 5.63c-5.88.01-33.92-.3-57.36-12.29a12.802 12.802 0 0 1-7.81-11.91v-10.75c0-8.45 6.2-6.9 8.83-5.89 10.04 4.06 16.48 7.14 28.81 9.6 36.65 7.53 52.77-2.3 56.72-4.48 13.15-7.81 14.19-25.68 5.25-34.95-10.48-8.79-15.48-9.12-53.13-21-4.64-1.29-43.7-13.61-43.79-52.36-.61-28.24 25.05-56.18 69.52-55.95 12.67-.01 46.43 4.13 55.57 15.62 1.35 2.09 2.02 4.55 1.92 7.04v10.11c0 4.44-1.62 6.66-4.87 6.66-7.71-.86-21.39-11.17-49.16-10.75-6.89-.36-39.89.91-38.41 24.97-.43 18.96 26.61 26.07 29.7 26.89 36.46 10.97 48.65 12.79 63.12 29.58 17.14 22.25 7.9 48.3 4.35 55.44-19.08 37.49-68.42 34.44-69.26 34.42zm40.2 104.86c-70.03 51.72-171.69 79.25-258.49 79.25A469.127 469.127 0 0 1 2.83 327.46c-6.53-5.89-.77-13.96 7.17-9.47a637.37 637.37 0 0 0 316.88 84.12 630.22 630.22 0 0 0 241.59-49.55c11.78-5 21.77 7.8 10.12 16.38zm29.19-33.29c-8.96-11.52-59.28-5.38-81.81-2.69-6.79.77-7.94-5.12-1.79-9.47 40.07-28.17 105.88-20.1 113.44-10.63 7.55 9.47-2.05 75.41-39.56 106.91-5.76 4.87-11.27 2.3-8.71-4.1 8.44-21.25 27.39-68.49 18.43-80.02z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">AWS ECS\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Run containerized workloads on Amazon Elastic Container Service.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/gcp-cloud-run\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M340.5 163.6l44.5-44.5 3-18.7c-81.1-73.7-210-65.4-283.1 17.4-20.3 23-35.4 51.7-43.4 81.3l15.9-2.2 89-14.7 6.9-7c39.6-43.5 106.5-49.3 152.3-12.3l15 .8zm107.9 34.2c-10.2-37.7-31.2-71.5-60.4-97.4l-62.5 62.5c26.4 21.6 41.4 54 40.8 88.1l0 11.1c30.7 0 55.6 24.9 55.6 55.6s-24.9 55-55.6 55l-111.3 0-10.9 11.9 0 66.7 10.9 10.5 111.3 0c79.9 .6 145.1-63 145.7-142.8 .4-48.4-23.5-93.8-63.6-120.9zM143.7 461.6l111.2 0 0-89-111.2 0c-7.9 0-15.6-1.7-22.8-5l-15.8 4.8-44.8 44.5-3.9 15.1c25.1 19 55.8 29.6 87.3 29.5zm0-288.7C63.9 173.3-.5 238.5 0 318.3 .3 362.9 21.1 404.9 56.4 432.1l64.5-64.5C93 354.9 80.5 322 93.2 294s45.6-40.4 73.5-27.8c12.3 5.6 22.2 15.4 27.8 27.8L259 229.5c-27.4-35.9-70.1-56.8-115.2-56.7z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Google Cloud Run\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy containers to Google Cloud Run for auto-scaling.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/hetzner\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M0 256a256 256 0 1 1 512 0 256 256 0 1 1 -512 0zm411.1 5.6c3-.6 5.9-1.8 8.4-3.7 2.3-2.4 3.4-5.6 3.2-8.9 0-2.6-.8-5.3-2.1-7.6-2.1-3.5-5.8-5.8-9.9-5.9l-2 0-4.8-.1-21.5 0c-1.3 0-1.9 .5-1.9 1.8l0 37.7c0 1.3 .5 1.9 1.9 1.9l5.6 0c1.3 0 1.9-.6 1.9-1.9l0-12.5 6.9 0c1.5 .1 3 .7 4 1.7L411.7 275c1 1 2.3 1.6 3.7 1.7l8.3 0c1.3 0 1.6-.8 .7-1.7l-13.3-13.4zm-1.4-8.1l-19.8 0 0-9.5 19.8 0c2 .3 3.5 1.9 3.5 4l0 1.4c0 2.1-1.5 3.8-3.5 4.1zm-37.8 14.3l-30.8 0 0-7.8 24.6 0c1.3 0 1.8-.5 1.8-1.9l0-4.5c0-1.3-.5-1.9-1.8-1.9l-24.6 0 0-7.8 30.8 0c1.3 0 1.8-.6 1.8-1.8l0-5.2c0-1.3-.5-1.8-1.8-1.8l-38.4 0c-1.3 0-1.8 .5-1.8 1.8l0 37.7c0 1.3 .5 1.9 1.8 1.9l38.4 0c1.3 0 1.8-.5 1.8-1.9l0-5.1c0-1.3-.6-1.9-1.9-1.9zm-47-19c.1-2.6-.7-5.2-2.1-7.4-2-3.6-5.8-5.9-10-6.1l-28.2 0c-1.3 0-1.9 .6-1.9 1.8l0 37.6c0 1.3 .5 1.9 1.9 1.9l6 0c1.3 0 1.9-.5 1.9-1.9l0-30.7 16.4 0c3.5 0 6.9 2.3 6.9 5.8l0 25c0 1.3 .5 1.9 1.8 1.9l5.5 0c1.3 0 1.9-.6 1.9-1.9l0-26zm-50.8 18.8l-26.5 0 26.6-23.3c1.1-.9 1.8-2.1 1.8-3.4l0-3.7c0-1.3-.5-1.8-1.8-1.8l-38.5 0c-1.3 0-1.8 .6-1.8 1.8l0 5.2c0 1.3 .5 1.8 1.8 1.8l24.7 0-24.8 23.2c-1.1 .9-1.8 2-1.9 3.4l0 4.1c0 1.3 .6 1.9 1.9 1.9l38.4 0c1.3 0 1.9-.6 1.9-1.9l0-5.4c.1-1.3-.4-1.9-1.8-1.9zm-48.9-32.3l-38.4 0c-1.3 0-1.9 .5-1.9 1.8l0 5.2c0 1.3 .5 1.8 1.9 1.8l14.5 0 0 30.7c0 1.3 .6 1.9 1.8 1.9l6.2 0c1.3 0 1.8-.6 1.8-1.9l0-30.7 14.1 0c1.3 0 1.9-.5 1.9-1.8l0-5.1c-.1-1.3-.6-1.9-1.9-1.9zm-48.9 32.6l-30.7 0 0-7.8 24.5 0c1.3 0 1.9-.5 1.9-1.9l0-4.5c0-1.3-.5-1.9-1.9-1.9l-24.5 0 0-7.8 30.7 0c1.3 0 1.8-.6 1.8-1.8l0-5.2c0-1.3-.5-1.8-1.8-1.8l-38.3 0c-1.3 0-1.9 .5-1.9 1.8l0 37.7c0 1.3 .6 1.9 1.9 1.9l38.3 0c1.3 0 1.8-.5 1.8-1.9l0-5.1c0-1.3-.5-1.9-1.8-1.9zm-48.9-32.6l-5.8 0c-1.3 0-1.8 .6-1.8 1.8l0 14.7-23.1 0 0-14.7c0-1.3-.5-1.8-1.8-1.8l-5.8 0c-1.3 0-1.8 .6-1.8 1.8l0 37.7c0 1.3 .5 1.9 1.8 1.9l5.8 0c1.3 0 1.8-.5 1.8-1.9l0-14.9 23.1 0 0 14.9c0 1.3 .5 1.9 1.8 1.9l5.8 0c1.3 0 1.9-.6 1.9-1.9l0-37.7c-.1-1.3-.6-1.8-1.9-1.8z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Hetzner\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Deploy to Hetzner's cost-effective cloud infrastructure.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/vm-and-bare-metal\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" fill=\"currentColor\" style=\"height:22px;width:auto;max-width:40px;color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M64 32C28.7 32 0 60.7 0 96l0 64c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 32zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm48 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM64 288c-35.3 0-64 28.7-64 64l0 64c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-64c0-35.3-28.7-64-64-64L64 288zm280 72a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm56 24a24 24 0 1 1 48 0 24 24 0 1 1 -48 0z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">VM & Bare Metal\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Run on virtual machines or bare metal servers with full control.\u003C/div>\n \u003C/a>\n \u003Ca href=\"https://rivet.dev/docs/deploy/custom\" style=\"display:block;text-decoration:none;color:inherit;border:1px solid rgba(27,25,22,0.1);border-radius:12px;padding:1.1rem;background:#faf8f3;\">\n \u003Csvg viewBox=\"0 0 512 512\" width=\"22\" height=\"22\" fill=\"currentColor\" style=\"color:#1b1916;display:block;margin-bottom:0.6rem;\" aria-hidden=\"true\">\u003Cpath d=\"M192 104.8c0-9.2-5.8-17.3-13.2-22.8C167.2 73.3 160 61.3 160 48c0-26.5 28.7-48 64-48s64 21.5 64 48c0 13.3-7.2 25.3-18.8 34c-7.4 5.5-13.2 13.6-13.2 22.8c0 12.8 10.4 23.2 23.2 23.2l56.8 0c26.5 0 48 21.5 48 48l0 56.8c0 12.8 10.4 23.2 23.2 23.2c9.2 0 17.3-5.8 22.8-13.2c8.7-11.6 20.7-18.8 34-18.8c26.5 0 48 28.7 48 64s-21.5 64-48 64c-13.3 0-25.3-7.2-34-18.8c-5.5-7.4-13.6-13.2-22.8-13.2c-12.8 0-23.2 10.4-23.2 23.2L384 464c0 26.5-21.5 48-48 48l-56.8 0c-12.8 0-23.2-10.4-23.2-23.2c0-9.2 5.8-17.3 13.2-22.8c11.6-8.7 18.8-20.7 18.8-34c0-26.5-28.7-48-64-48s-64 21.5-64 48c0 13.3 7.2 25.3 18.8 34c7.4 5.5 13.2 13.6 13.2 22.8c0 12.8-10.4 23.2-23.2 23.2L48 512c-26.5 0-48-21.5-48-48L0 343.2C0 330.4 10.4 320 23.2 320c9.2 0 17.3 5.8 22.8 13.2C54.7 344.8 66.7 352 80 352c26.5 0 48-28.7 48-64s-21.5-64-48-64c-13.3 0-25.3 7.2-34 18.8C40.5 250.2 32.4 256 23.2 256C10.4 256 0 245.6 0 232.8L0 176c0-26.5 21.5-48 48-48l120.8 0c12.8 0 23.2-10.4 23.2-23.2z\"/>\u003C/svg>\n \u003Cdiv style=\"font-weight:600;color:#1b1916;margin-bottom:0.25rem;\">Custom Platform\u003C/div>\n \u003Cdiv style=\"font-size:0.875rem;color:#56524a;line-height:1.4;\">Integrate with any other hosting platform of your choice.\u003C/div>\n \u003C/a>\n\u003C/div>\n\n## Enterprise support\n\nEnterprise support and managed deployment are available, including dedicated support, custom SLAs, and compliance reviews. [Contact the Rivet team](https://rivet.dev/sales) to discuss your requirements.","src/content/docs/docs/deployment.mdx","3fe4a725cd89b727",{"id":9,"data":136,"body":141,"filePath":142,"digest":143,"deferredRender":16},{"title":137,"description":106,"editUrl":16,"head":138,"tableOfContents":20,"template":18,"sidebar":139,"pagefind":16,"draft":20},"Introduction",[],{"hidden":20,"attrs":140},{},"import DocsLanding from '@rivet-dev/docs-theme/components/DocsLanding.astro';\nimport AgentOSHeroLogo from '../../../components/AgentOSHeroLogo.astro';\n\n\u003CDocsLanding>\n\t\u003CAgentOSHeroLogo slot=\"hero\" />\n\u003C/DocsLanding>","src/content/docs/docs/index.mdx","352df9dbb91266b5","docs/filesystem",{"id":144,"data":146,"body":152,"filePath":153,"digest":154,"deferredRender":16},{"title":147,"description":148,"editUrl":16,"head":149,"template":18,"sidebar":150,"pagefind":16,"draft":20},"Filesystem","Read, write, mount, and manage files inside agentOS, all backed by a virtual filesystem isolated from the host disk.",[],{"hidden":20,"attrs":151},{},"import { Tabs, TabItem } from '@astrojs/starlight/components';\n\nEach 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](/docs/persistence) for the details.\n\n## Mounts\n\nBack 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.\n\n\u003CTabs>\n\n\u003CTabItem label=\"Host directory\">\n\nProject 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.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n {\n path: \"/home/agentos/repo\",\n plugin: { id: \"host_dir\", config: { hostPath: \"/path/to/repo\" } },\n readOnly: true,\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\u003C/TabItem>\n\n\u003CTabItem label=\"S3\">\n\nMount 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.\n\nThe 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.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n {\n path: \"/home/agentos/data\",\n plugin: {\n id: \"s3\",\n config: {\n bucket: \"my-bucket\",\n prefix: \"agent-data/\",\n region: \"us-east-1\",\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\nThe `s3` plugin config also accepts `credentials` (`{ accessKeyId, secretAccessKey }`) and a custom `endpoint` for S3-compatible providers.\n\n\u003C/TabItem>\n\n\u003CTabItem label=\"Google Drive\">\n\nMount a Google Drive folder with the built-in `google_drive` plugin.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n {\n path: \"/home/agentos/drive\",\n plugin: {\n id: \"google_drive\",\n config: {\n credentials: {\n clientEmail: process.env.GOOGLE_DRIVE_CLIENT_EMAIL!,\n privateKey: process.env.GOOGLE_DRIVE_PRIVATE_KEY!,\n },\n folderId: process.env.GOOGLE_DRIVE_FOLDER_ID!,\n },\n },\n },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\u003C/TabItem>\n\n\u003CTabItem label=\"In-memory\">\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createInMemoryFileSystem } from \"@rivet-dev/agent-os-core\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n mounts: [\n { path: \"/home/agentos/scratch\", driver: createInMemoryFileSystem() },\n ],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\u003C/TabItem>\n\n\u003C/Tabs>\n\n## File operations\n\nThese 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](#mounts) is faster than copying files in. Programs that need stdin or live output use exec instead (see [Core](/docs/core)).\n\n### Read and write\n\n```ts\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Write a file (string or Uint8Array)\nawait agent.writeFile(\"/home/agentos/hello.txt\", \"Hello, world!\");\n\n// Read a file (returns Uint8Array)\nconst content = await agent.readFile(\"/home/agentos/hello.txt\");\nconsole.log(new TextDecoder().decode(content));\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n### Batch read and write\n\n```ts\n// Batch write (creates parent directories automatically)\nconst writeResults = await agent.writeFiles([\n { path: \"/home/agentos/src/index.ts\", content: \"console.log('hello');\" },\n { path: \"/home/agentos/src/utils.ts\", content: \"export function add(a: number, b: number) { return a + b; }\" },\n]);\n\n// Batch read\nconst readResults = await agent.readFiles([\n \"/home/agentos/src/index.ts\",\n \"/home/agentos/src/utils.ts\",\n]);\nfor (const result of readResults) {\n console.log(result.path, new TextDecoder().decode(result.content ?? new Uint8Array()));\n}\n```\n\n### Directories\n\n```ts\n// Create a directory\nawait agent.mkdir(\"/home/agentos/projects\");\n\n// List directory contents\nconst entries = await agent.readdir(\"/home/agentos/projects\");\n\n// Recursive listing with metadata\nconst tree = await agent.readdirRecursive(\"/home/agentos\", {\n maxDepth: 3,\n exclude: [\"node_modules\"],\n});\nfor (const entry of tree) {\n console.log(entry.type, entry.path, entry.size);\n}\n```\n\n### File metadata\n\n```ts\n// Check if a path exists\nconst fileExists = await agent.exists(\"/home/agentos/hello.txt\");\n\n// Get file metadata\nconst info = await agent.stat(\"/home/agentos/hello.txt\");\nconsole.log(info.size, info.isDirectory, info.mtimeMs);\n```\n\n### Move and delete\n\n```ts\n// Move/rename\nawait agent.move(\"/home/agentos/old.txt\", \"/home/agentos/new.txt\");\n\n// Delete a file\nawait agent.delete(\"/home/agentos/new.txt\");\n\n// Delete a directory recursively\nawait agent.delete(\"/home/agentos/temp\", { recursive: true });\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/filesystem)*\n\n\n## Permissions\n\nFilesystem 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:\n\n```ts\nconst vm = agentOS({\n permissions: {\n fs: {\n default: \"allow\",\n rules: [{ mode: \"deny\", operations: [\"*\"], paths: [\"/home/agentos/secrets/**\"] }],\n },\n },\n});\n```\n\nSee [Permissions](/docs/permissions) for the full configuration.\n\n## Sandboxes\n\nFor heavier or untrusted workloads, run a full Linux [sandbox](/docs/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. See [Sandbox Mounting](/docs/sandbox) for setup.\n\n## Default layout\n\nWith no `mounts` configured, every VM boots an Alpine-based root filesystem with the standard POSIX directories:\n\n- `/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`).\n- `/bin`, `/sbin`, `/usr`: installed commands (common POSIX utilities by default, plus any [software](/docs/software) you add).\n- `/etc`, `/lib`, `/opt`, `/root`, `/run`, `/srv`, `/tmp`, `/var`, `/mnt`: standard system paths.\n\nIt is backed by the VM's own filesystem and persisted across sleep/wake. Nothing comes from or touches the host disk.","src/content/docs/docs/filesystem.mdx","06e3197dcbf22096","docs/js-runtime",{"id":155,"data":157,"body":163,"filePath":164,"digest":165,"deferredRender":16},{"title":158,"description":159,"editUrl":16,"head":160,"template":18,"sidebar":161,"pagefind":16,"draft":20},"JavaScript Runtime","How agentOS runs guest JavaScript: native V8 acceleration, low memory overhead, and Node.js compatibility.",[],{"hidden":20,"attrs":162},{},"The JavaScript runtime is powered by the Rivet [Secure Exec](https://secureexec.dev) project, which provides the isolated V8 runtime that agentOS runs guest code in. Every guest VM executes its JavaScript inside this runtime, fully sandboxed from the host.\n\n## JavaScript Acceleration\n\n- **JavaScript is unusually slow as WebAssembly**: unlike most software, JavaScript pays a heavy penalty when compiled to WebAssembly, because so much engineering has gone into JIT-compiling JavaScript directly in V8.\n- **Native V8, full JIT**: agentOS therefore runs guest JavaScript on the native V8 engine with its full JIT compiler, not through a WASM translation layer. We call this **JavaScript Acceleration**.\n- **Native-speed execution**: guest JavaScript runs at native speed while staying inside the isolation boundary, with normal Node.js semantics.\n\n## Comparison to Node.js efficiency\n\n- **Isolate model, not processes**: agentOS runs each agent inside a V8 isolate rather than spawning a full Node.js process per agent.\n- **Low memory overhead**: an isolate carries far less per-agent memory overhead than a full Node.js process, so many agents fit in the footprint that a process-per-agent model would spend on a handful.\n- **Benchmarks**: see the Secure Exec [benchmarks](https://secureexec.dev/docs/benchmarks) for cold start, warm execution, and reuse measurements.\n\n## Node.js compatibility\n\nGuest code runs as Node.js (reporting `process.version` as `v22.0.0`), but it never touches the host runtime. Every `node:` builtin resolves to a kernel-backed bridge or an in-isolate polyfill, never the real host module. For the full builtin matrix (`fs`, `net`, `http`, `crypto`, undici-backed `fetch`, and more), see the Secure Exec [Node.js Compatibility](https://secureexec.dev/docs/nodejs-compatibility) reference.\n\n### npm packages\n\nBy default the VM has no npm packages installed. Mount a host `node_modules` directory to give guest code access to real packages: the `nodeModulesMount` helper projects it read-only at `/root/node_modules`, and the in-kernel resolver walks it exactly like Node.js does, with no bundling or patching.\n\n```ts\nimport { agentOS, setup, nodeModulesMount } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n // Project a host node_modules tree into the VM (read-only by default).\n mounts: [nodeModulesMount(\"/absolute/path/to/node_modules\")],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\nResolution matches naive Node.js over the mounted tree: the ancestor `node_modules` walk, `package.json` `exports`/`imports` and conditions, and `realpath`/symlink following (so pnpm and yarn layouts resolve too). Both ESM `import` and CommonJS `require` work. See the Secure Exec [module loading](https://secureexec.dev/docs/features/module-loading) guide for the full model.","src/content/docs/docs/js-runtime.mdx","9d5bf6a0894c1577","docs/limitations",{"id":166,"data":168,"body":174,"filePath":175,"digest":176,"deferredRender":16},{"title":169,"description":170,"editUrl":16,"head":171,"template":18,"sidebar":172,"pagefind":16,"draft":20},"Limitations","What the agentOS VM does not support, and how to work around it.",[],{"hidden":20,"attrs":173},{},"agentOS is a Linux environment with a POSIX-compliant virtual kernel. It handles most agent workloads (coding, scripting, file I/O, networking) with near-zero overhead.\n\n## Sandbox mounting\n\nWhen a workload needs a full Linux OS, agents can escalate to a full sandbox on demand without changing code. The [sandbox mounting](/docs/sandbox) extension mounts the sandbox as a filesystem and lets you execute commands on it, like mounting a hard drive on your own machine. Files written in the VM are available in the sandbox and vice versa.\n\nSee [agentOS vs Sandbox](/docs/versus-sandbox) for a detailed comparison.\n\n## Limitations\n\n### Software registry\n\nagentOS uses its own [software registry](/registry) of popular tools cross-compiled for the runtime. You cannot download and install arbitrary binaries (for example via `curl` or `apt`), and standard Linux package managers (`apt`, `yum`) are not available since agentOS runs a streamlined Linux environment rather than a full distribution. Native binaries that are not yet available in the registry (such as Go, Rust, or C++ toolchains) require a full [sandbox](/docs/sandbox).\n\nSee [Software](/docs/software) for how to install and configure available packages.\n\n### Lightweight Linux kernel\n\nagentOS provides a POSIX-compliant virtual Linux kernel with full filesystem operations, networking, and process management. It implements a focused subset of the kernel surface, so a few Linux-specific features are not available:\n\n- Kernel modules and eBPF\n- Container runtimes (e.g. Docker)\n- File watching (`inotify`, `fs.watch`)\n\n### No hardware access\n\nThe VM has no access to GPUs, USB devices, or other hardware.","src/content/docs/docs/limitations.mdx","9f65ad4a48fd07f0","docs/llm-credentials",{"id":177,"data":179,"body":185,"filePath":186,"digest":187,"deferredRender":16},{"title":180,"description":181,"editUrl":16,"head":182,"template":18,"sidebar":183,"pagefind":16,"draft":20},"LLM Credentials","Pass LLM API keys to agent sessions securely.",[],{"hidden":20,"attrs":184},{},"Pass LLM provider API keys to agent sessions so keys stay on the server and are injected at session creation, with per-tenant isolation for multi-tenant deployments.\n\n## Passing API keys\n\nPass LLM provider keys via the `env` option on `createSession`. The VM does not inherit from the host `process.env`, so keys must be passed explicitly.\n\n```ts\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({\n\tendpoint: \"http://localhost:6420\",\n});\n\n// Pass LLM provider keys via the `env` option on createSession. The VM does\n// not inherit from the host process.env, so keys must be passed explicitly.\nconst session = await client.vm.getOrCreate(\"my-agent\").createSession(\"pi\", {\n\tenv: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\nconsole.log(session.sessionId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/llm-credentials)*\n\n## Per-tenant credentials\n\nGive each tenant an isolated VM by keying `getOrCreate` on the tenant id, look up that tenant's API key on the server, and inject it via the session `env`. Credentials stay on the server and never reach the client.\n\nFirst, declare the agent software on the server:\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"./software/pi\";\n\n// The VM does not inherit the host process.env. LLM provider keys are passed\n// explicitly per session, so the server just declares the agent software here.\nconst vm = agentOS({\n\tsoftware: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/llm-credentials)*\n\nThen resolve each tenant's key and pass it at session creation:\n\n```ts\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({\n\tendpoint: \"http://localhost:6420\",\n});\n\n// Stand-in for your own per-tenant credential store.\ndeclare function lookupTenantApiKey(tenantId: string): Promise\u003Cstring>;\n\n// Give each tenant an isolated VM keyed by their tenant id, then inject that\n// tenant's API key from your database at session creation. Keys stay on the\n// server and never reach the client.\nasync function startTenantSession(tenantId: string) {\n\tconst anthropicApiKey = await lookupTenantApiKey(tenantId);\n\n\treturn client.vm.getOrCreate(tenantId).createSession(\"pi\", {\n\t\tenv: { ANTHROPIC_API_KEY: anthropicApiKey },\n\t});\n}\n\nconst session = await startTenantSession(\"tenant-123\");\nconsole.log(session.sessionId);\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/llm-credentials)*\n\nBecause keys are resolved per tenant from your own credential store (the `lookupTenantApiKey` stand-in above) and stay on the server, each session uses the tenant's own key and one tenant's key never reaches another tenant or the client.\n\n## Embedded LLM Gateway\n\nThe [Embedded LLM Gateway](/docs/llm-gateway) (coming soon) will remove the need to manage API keys manually. It routes all agent LLM requests through a managed proxy built into agentOS, providing per-tenant usage metering, rate limiting, and cost controls without deploying a separate gateway service.","src/content/docs/docs/llm-credentials.mdx","7f8cd46f40822971","docs/llm-gateway",{"id":188,"data":190,"body":196,"filePath":197,"digest":198,"deferredRender":16},{"title":191,"description":192,"editUrl":16,"head":193,"template":18,"sidebar":194,"pagefind":16,"draft":20},"Embedded LLM Gateway","Route, meter, and manage LLM API calls from agents.",[],{"hidden":20,"attrs":195},{},"{/* TODO: This page is coming soon. */}\n\nThe Embedded LLM Gateway runs as part of the agentOS library, not as an external service. It intercepts and manages all LLM API calls made by agents inside the VM.\n\n- **Unified routing** for all agent LLM requests\n- **API keys stay on the server** so they are never exposed to agent code inside the VM\n- **Usage metering** with per-session and per-agent breakdowns\n- **Rate limiting** and cost controls\n\nCheck back soon for full documentation.","src/content/docs/docs/llm-gateway.mdx","cbe27275943ef64e","docs/multiplayer",{"id":199,"data":201,"body":207,"filePath":208,"digest":209,"deferredRender":16},{"title":202,"description":203,"editUrl":16,"head":204,"template":18,"sidebar":205,"pagefind":16,"draft":20},"Multiplayer","Connect multiple clients to the same agentOS actor for collaborative agent workflows.",[],{"hidden":20,"attrs":206},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nConnect multiple clients to the same agentOS actor so all subscribers receive broadcasted session output, process logs, and shell data, enabling collaborative patterns where one user prompts and others observe.\n\n## Multiple clients observing a session\n\nAll clients connected to the same actor receive broadcasted events. This enables building collaborative UIs where multiple users watch an agent work.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\n// Client A: creates the session and sends prompts\nconst clientA = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agentA = clientA.vm.getOrCreate(\"shared-agent\");\n\nconst connA = agentA.connect();\nconnA.on(\"sessionEvent\", (data) => {\n console.log(\"[A]\", data.event.method);\n});\n\nconst session = await agentA.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agentA.sendPrompt(session.sessionId, \"Build a REST API\");\n\n// Client B: observes the same session (in a separate process)\nconst clientB = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\nconst connB = clientB.vm.getOrCreate(\"shared-agent\").connect();\nconnB.on(\"sessionEvent\", (data) => {\n console.log(\"[B]\", data.event.method);\n});\n\n// Client B sees the same events as Client A\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"./software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n onSessionEvent: async (sessionId, event) => {\n // Server-side hook runs once per event, even with multiple clients\n console.log(\"Session event:\", sessionId, event.method);\n },\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/multiplayer)*","src/content/docs/docs/multiplayer.mdx","bc8f21b30025df33","docs/networking",{"id":210,"data":212,"body":218,"filePath":219,"digest":220,"deferredRender":16},{"title":213,"description":214,"editUrl":16,"head":215,"template":18,"sidebar":216,"pagefind":16,"draft":20},"Networking & Previews","Proxy HTTP requests into agentOS VMs and create shareable preview URLs.",[],{"hidden":20,"attrs":217},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nProxy HTTP requests into VM services with `vmFetch` and create time-limited, token-based preview URLs (with configurable expiration, revocation, and CORS), all carried over one transport (the kernel socket table) that is loopback-only by default under a three-layer confinement model.\n\n## Run an HTTP server in the VM\n\nGuest code runs a normal Node HTTP server: it binds a loopback port inside the VM exactly like any Node process. Write the server file and spawn it.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Write a simple Node HTTP server and run it inside the VM. It binds a loopback\n// port (3000) exactly like any normal Node process.\nawait agent.writeFile(\n \"/home/agentos/server.js\",\n `const http = require(\"http\");\nhttp.createServer((req, res) => {\n res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n res.end(\"Hello from inside the VM\");\n}).listen(3000, () => console.log(\"listening on http://127.0.0.1:3000\"));`,\n);\nconst { pid } = await agent.spawn(\"node\", [\"/home/agentos/server.js\"]);\nconsole.log(\"server pid:\", pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS();\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n## Fetch from a VM service\n\nWith the HTTP server running in the VM (above), send requests to it with `vmFetch`, including custom methods, headers, and body.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Simple GET from the VM service started above.\nconst response = await agent.vmFetch(3000, \"/\");\nconsole.log(\"Status:\", response.status);\nconsole.log(\"Body:\", new TextDecoder().decode(response.body));\n\n// With a custom method, headers, and body.\nconst posted = await agent.vmFetch(3000, \"/api/data\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ key: \"value\" }),\n});\nconsole.log(\"Status:\", posted.status, posted.statusText);\nconsole.log(\"Headers:\", posted.headers);\nconsole.log(\"Body:\", new TextDecoder().decode(posted.body));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({ software: [] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n## Preview URLs\n\nPreview URLs are port forwarding for VM services: a time-limited, public URL that proxies HTTP to a port inside the VM, for browser or external access (use `vmFetch` for server-to-server). Tokens survive sleep/wake and CORS is enabled; see [Security](/docs/security-model) for details.\n\n### Create a preview URL\n\nToken lifetimes are configured under the `preview` key:\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n software: [],\n preview: {\n defaultExpiresInSeconds: 3600, // 1 hour default\n maxExpiresInSeconds: 86400, // 24 hour maximum\n },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Start a web app in the VM\nawait agent.spawn(\"node\", [\"/home/agentos/app.js\"]);\n\n// Create a preview URL (default 1 hour expiration)\nconst preview = await agent.createSignedPreviewUrl(3000);\nconsole.log(\"Preview path:\", preview.path);\nconsole.log(\"Token:\", preview.token);\nconsole.log(\"Expires at:\", new Date(preview.expiresAt));\n\n// Create a preview URL with custom expiration\nconst shortPreview = await agent.createSignedPreviewUrl(3000, 300); // 5 minutes\nconsole.log(\"Short-lived preview:\", shortPreview.path);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n### Revoke a preview URL\n\nMint short-lived preview tokens so access expires automatically; the lifetime is capped by `preview.maxExpiresInSeconds`.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\n// Mint a short-lived preview token so access expires automatically.\nconst preview = await client.vm.getOrCreate(\"my-agent\").createSignedPreviewUrl(3000, 300); // 5 minutes\nconsole.log(\"Preview path:\", preview.path);\nconsole.log(\"Expires at:\", new Date(preview.expiresAt));\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({ software: [] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/networking)*\n\n## Permissions\n\nNetwork access is governed by the VM permission policy. By default the guest cannot reach the network; grant it, or allow only specific destinations:\n\n```ts\nconst vm = agentOS({\n permissions: {\n network: {\n default: \"deny\",\n rules: [{ mode: \"allow\", operations: [\"*\"], patterns: [\"api.example.com\"] }],\n },\n },\n});\n```\n\nSee [Permissions](/docs/permissions) for the full configuration.","src/content/docs/docs/networking.mdx","1fca376033af442c","docs/permissions",{"id":221,"data":223,"body":229,"filePath":230,"digest":231,"deferredRender":16},{"title":224,"description":225,"editUrl":16,"head":226,"template":18,"sidebar":227,"pagefind":16,"draft":20},"Permissions","The per-scope kernel permission policy that gates every guest syscall in the sandbox.",[],{"hidden":20,"attrs":228},{},"The sandbox permission policy is the kernel-level enforcement layer. Every guest syscall the agent's sandboxed code makes is checked against a per-scope policy before any host resource is touched.\n\n- **Six scopes**, configured independently: `fs`, `network`, `childProcess`, `process`, `env`, `binding`.\n- **Each scope** is a mode (`\"allow\"` or `\"deny\"`), or a rule set.\n- **A denied operation** is rejected with `EACCES` before any host resource is touched.\n- **Merged over a secure default**, so partial policies work.\n\nFor the higher-level agent tool-approval layer (human-in-the-loop, auto-approve), see [Approvals](/docs/approvals).\n\n## Defaults and merge semantics\n\nThe sandbox is deny-by-default for outward-facing capabilities. When you pass no policy, this baseline applies:\n\n```ts\n{\n fs: \"allow\", // virtualized in-memory filesystem only\n childProcess: \"allow\",\n process: \"allow\",\n env: \"allow\",\n network: \"deny\", // no network egress until you opt in\n}\n```\n\n- `fs`/`childProcess`/`process`/`env` are allowed because they are fully virtualized (the guest sees only the VM, never the host) and are required to run a program at all.\n- `network` is denied: guest code cannot reach the network until you opt in.\n- Your policy is merged **over** this baseline. Omitted scopes keep their default; they are **not** denied. So `{ network: \"allow\" }` grants the network while keeping the execution essentials.\n\n```ts\n// Grant the network, leave everything else at the secure default.\nconst grantNetwork = { network: \"allow\" };\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/permissions)*\n\n## Permission scopes\n\n| Scope | Controls | Default |\n|---|---|---|\n| `fs` | Filesystem reads, writes, and metadata operations | `allow` |\n| `network` | Outbound connections: `fetch`, HTTP, DNS, and inbound `listen` | `deny` |\n| `childProcess` | Spawning child processes | `allow` |\n| `process` | Process-control operations | `allow` |\n| `env` | Environment variable access | `allow` |\n| `binding` | Invoking bindings registered with the runtime | `deny`* |\n\n\\* The `binding` scope is auto-granted to `allow` when you register bindings and set no `binding` policy of your own. Pass a `binding` policy to gate individual bindings.\n\n## Bind a policy to the VM\n\nA policy is a plain object keyed by scope. Pass it as `permissions` to `agentOS(...)` and it gates every guest syscall on that VM.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n\tpermissions: {\n\t\tnetwork: \"allow\",\n\t\tfs: \"deny\",\n\t},\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/permissions)*\n\n## Grant or deny a whole scope\n\nThe simplest value for a scope is a single mode string. `\"allow\"` permits every operation in the scope; `\"deny\"` rejects every one with `EACCES`. Omitted scopes keep their secure default, so you only list what you want to change.\n\n```ts\nconst permissions = {\n\tnetwork: \"allow\", // turn on network egress\n\tfs: \"deny\", // turn off all filesystem access\n};\n```\n\nThere is no typed `\"ask\"` mode. Interactive, human-in-the-loop approval lives in the higher-level [Approvals](/docs/approvals) layer, not the kernel policy. To block at the kernel level, use `\"deny\"`.\n\n## Allow only specific filesystem paths\n\nFor finer control, a scope can be a rule set instead of a bare mode: a `default` mode plus an ordered list of `rules`. The `fs` scope matches by `paths` (filesystem globs). Each rule names its `operations` (`read`, `write`, `stat`, `readdir`, `create_dir`, `rm`, `rename`, `symlink`, `readlink`, `chmod`, `truncate`, `mount_sensitive`, or `[\"*\"]` for all). Last matching rule wins; if no rule matches, `default` applies.\n\n```ts\n// Allow the filesystem everywhere, but deny anything under /home/agentos/vault.\nconst denyVault = {\n\tfs: {\n\t\tdefault: \"allow\",\n\t\trules: [{ mode: \"deny\", operations: [\"*\"], paths: [\"/home/agentos/vault/**\"] }],\n\t},\n};\n```\n\nTo invert it, flip `default` to `\"deny\"` and allow just one subtree:\n\n```ts\n// Deny the filesystem by default, allow only reads under /home/agentos/data.\nconst allowOnlyData = {\n\tfs: {\n\t\tdefault: \"deny\",\n\t\trules: [{ mode: \"allow\", operations: [\"read\", \"readdir\", \"stat\"], paths: [\"/home/agentos/data/**\"] }],\n\t},\n};\n```\n\n## Allow only specific network hosts\n\nEvery non-`fs` scope matches by `patterns` instead of `paths`. For `network`, a pattern is a host (or `host:port`), and the operations are `fetch`, `http`, `dns`, and `listen`.\n\n```ts\n// Deny the network by default, allow only api.example.com.\nconst allowOneHost = {\n\tnetwork: {\n\t\tdefault: \"deny\",\n\t\trules: [{ mode: \"allow\", operations: [\"*\"], patterns: [\"api.example.com\"] }],\n\t},\n};\n```\n\n## Allow only specific bindings\n\nBindings registered with the runtime are gated by the `binding` scope, matched by name via `patterns`. Bindings have no sub-operations, so pass `[\"*\"]` for `operations`.\n\n```ts\n// Deny all bindings by default, allow only the \"add\" binding by name.\nconst allowOneBinding = {\n\tbinding: {\n\t\tdefault: \"deny\",\n\t\trules: [{ mode: \"allow\", operations: [\"*\"], patterns: [\"add\"] }],\n\t},\n};\n```\n\nThe `childProcess`, `process`, and `env` scopes work the same way: `childProcess` patterns match the command (`operations: [\"spawn\"]`), `env` patterns match the variable name (`operations: [\"read\", \"write\"]`), and `process` is matched by pattern with `operations: [\"*\"]`.\n\n## Combine policies and see denials\n\nEach policy above sets one scope, so you can spread several into one `permissions` object and bind them together.\n\n```ts\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n\nconst vm = agentOS({\n\tpermissions: {\n\t\t...denyVault,\n\t\t...allowOneHost,\n\t\t...allowOneBinding,\n\t},\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/permissions)*\n\nWhen a scope or matching rule denies an operation, the kernel rejects it with `EACCES` before any host resource is touched. For example, with `network: \"deny\"`, an outbound `fetch()` inside the guest throws:\n\n```\nEACCES: permission denied, tcp://example.com:80: blocked by network.http policy\n```","src/content/docs/docs/permissions.mdx","d002ea4218bc974b","docs/persistence",{"id":232,"data":234,"body":240,"filePath":241,"digest":242,"deferredRender":16},{"title":235,"description":236,"editUrl":16,"head":237,"template":18,"sidebar":238,"pagefind":16,"draft":20},"Persistence & Sleep","How agentOS persists data and manages sleep/wake cycles.",[],{"hidden":20,"attrs":239},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nagentOS automatically persists the `/home/agentos` filesystem and session transcripts (with sequence numbers for replay) across sleep/wake, sleeping after a configurable grace period (15 minutes by default) and waking automatically when a client connects or a cron job triggers.\n\n## What persists across sleep\n\n| Data | Storage | Persists? |\n|------|---------|-----------|\n| Files in `/home/agentos` | Persistent filesystem | Yes |\n| Session records | SQLite (`agent_os_sessions`) | Yes |\n| Session event history | SQLite (`agent_os_session_events`) | Yes |\n| Preview URL tokens | SQLite (`agent_os_preview_tokens`) | Yes |\n| Cron job definitions | Actor state | Yes |\n| Running processes | VM kernel | No |\n| Active shells | VM kernel | No |\n| In-memory mounts | VM memory | No |\n| VM kernel state | VM memory | No |\n\n## What prevents sleep\n\nThe actor stays awake as long as any of these are active:\n\n- **Active sessions** (created but not closed/destroyed)\n- **Running processes** (spawned but not exited)\n- **Active shells** (opened but not closed)\n- **Pending hooks** (server-side callbacks still executing)\n\nWhen all activity stops, the sleep grace period begins.\n\n## Sleep grace period\n\nAfter all activity stops, the actor waits 15 minutes before sleeping. This allows for brief pauses between interactions without restarting the VM.\n\n```\nActivity stops ──> 15 min grace period ──> Actor sleeps\n (VM shutdown, processes killed)\n\nNew client connects ──> Actor wakes ──> VM boots ──> Filesystem restored\n```\n\n## Timeouts\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| Action timeout | 15 minutes | Maximum time for any single action |\n| Sleep grace period | 15 minutes | Time before sleeping after all activity stops |\n\nThese are set internally by the `agentOS()` factory and cannot be overridden per-call.\n\n## Sleep vs destroy\n\n| | Sleep | Destroy |\n|-|-------|---------|\n| Filesystem | Preserved | Deleted |\n| Session records | Preserved | Deleted |\n| Event history | Preserved | Deleted |\n| Preview tokens | Preserved | Deleted |\n| VM state | Lost | Lost |\n| Processes | Killed | Killed |\n\n## VM boot and shutdown events\n\nSubscribe to `vmBooted` and `vmShutdown` events to track VM lifecycle.\n\n\u003CCodeGroup>\n```ts title=\"lifecycle-client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\nconst conn = client.vm.getOrCreate(\"my-agent\").connect();\n\nconn.on(\"vmBooted\", () => {\n console.log(\"VM is ready\");\n});\n\nconn.on(\"vmShutdown\", (payload) => {\n console.log(\"VM shutdown reason:\", payload.reason);\n // reason: \"sleep\" | \"destroy\" | \"error\"\n});\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\n// import pi from \"@agentos-software/pi\";\nimport pi from \"./software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/persistence)*\n\n## Resuming after sleep\n\nWhen the actor wakes up, the VM boots and the filesystem is restored from SQLite, session records and event history are immediately available, and processes and shells from the previous session are gone. Clients can reconnect, list prior work with `listPersistedSessions` (which works without a running VM), and replay a session's persisted transcript with `getSessionEvents`.\n\n\u003CCodeGroup>\n```ts title=\"resume-client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst vm = client.vm.getOrCreate(\"my-agent\");\n\n// List sessions persisted before sleep (works without a running VM)\nconst sessions = await vm.listPersistedSessions();\nconsole.log(\"Previous sessions:\", sessions.length);\n\n// Replay the most recent session's transcript from durable storage\nconst last = sessions[0];\nif (last) {\n const events = await vm.getSessionEvents(last.sessionId);\n for (const e of events) {\n console.log(e.seq, e.event.method);\n }\n}\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/persistence)* (uses the same `server.ts` as above)\n\n## Persisted tables schema\n\n### `agent_os_fs_entries`\n\nStores the virtual filesystem.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `path` | TEXT PRIMARY KEY | File or directory path |\n| `is_directory` | INTEGER | 1 for directory, 0 for file |\n| `content` | BLOB | File content |\n| `mode` | INTEGER | POSIX mode bits |\n| `size` | INTEGER | File size in bytes |\n| `atime_ms` | INTEGER | Access time (ms) |\n| `mtime_ms` | INTEGER | Modification time (ms) |\n| `ctime_ms` | INTEGER | Change time (ms) |\n| `birthtime_ms` | INTEGER | Birth time (ms) |\n\n### `agent_os_sessions`\n\nStores session metadata.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `session_id` | TEXT PRIMARY KEY | Unique session identifier |\n| `agent_type` | TEXT | Agent type (e.g. \"pi\") |\n| `capabilities` | TEXT (JSON) | Agent capabilities |\n| `agent_info` | TEXT (JSON) | Agent metadata |\n| `created_at` | INTEGER | Creation timestamp (ms) |\n\n### `agent_os_session_events`\n\nStores session event history.\n\n| Column | Type | Description |\n|--------|------|-------------|\n| `id` | INTEGER PRIMARY KEY | Auto-incrementing ID |\n| `session_id` | TEXT | Session reference |\n| `seq` | INTEGER | Sequence number within session |\n| `event` | TEXT (JSON) | JSON-RPC notification |\n| `created_at` | INTEGER | Timestamp (ms) |","src/content/docs/docs/persistence.mdx","db2d72a66b6888b7","docs/processes",{"id":243,"data":245,"body":251,"filePath":252,"digest":253,"deferredRender":16},{"title":246,"description":247,"editUrl":16,"head":248,"template":18,"sidebar":249,"pagefind":16,"draft":20},"Processes & Shell","Execute commands, spawn long-running processes, and open interactive shells in agentOS VMs.",[],{"hidden":20,"attrs":250},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nRun commands with one-shot `exec`, spawn long-running processes with streaming stdout/stderr and stdin, manage their lifecycle (stop, kill, wait, inspect), open interactive PTY-backed shells, and inspect the process tree across all VM runtimes.\n\n## One-shot execution\n\nUse `exec` to run a command and wait for completion. Returns stdout, stderr, and exit code.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\nconst result = await client.vm.getOrCreate(\"my-agent\").exec(\"echo hello && ls /home/agentos\");\nconsole.log(\"stdout:\", result.stdout);\nconsole.log(\"stderr:\", result.stderr);\nconsole.log(\"exit code:\", result.exitCode);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Spawn a long-running process\n\nUse `spawn` for processes that run in the background. Output is streamed via `processOutput` and `processExit` events.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\nconst conn = agent.connect();\n\n// Subscribe to process output\nconn.on(\"processOutput\", (data) => {\n const text = new TextDecoder().decode(data.data);\n console.log(`[pid ${data.pid}] ${data.stream}: ${text}`);\n});\n\nconn.on(\"processExit\", (data) => {\n console.log(`[pid ${data.pid}] exited with code ${data.exitCode}`);\n});\n\n// Spawn a dev server\nconst { pid } = await agent.spawn(\"node\", [\"/home/agentos/server.js\"]);\nconsole.log(\"Started process:\", pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Write to stdin\n\nSend input to a running process.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst { pid } = await agent.spawn(\"cat\", []);\n\n// Write to stdin\nawait agent.writeProcessStdin(pid, \"hello from stdin\\n\");\n\n// Close stdin when done\nawait agent.closeProcessStdin(pid);\n\n// Wait for the process to exit\nconst exitCode = await agent.waitProcess(pid);\nconsole.log(\"exit code:\", exitCode);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Process lifecycle\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst { pid } = await agent.spawn(\"node\", [\"/home/agentos/server.js\"]);\n\n// List all processes tracked by the VM\nconst processes = await agent.listProcesses();\nfor (const p of processes) {\n console.log(p.pid, p.command, p.args.join(\" \"), p.running ? \"running\" : \"exited\");\n}\n\n// Inspect a specific process by pid\nconst info = await agent.getProcess(pid);\nconsole.log(info.running, info.exitCode);\n\n// Graceful stop (SIGTERM)\nawait agent.stopProcess(pid);\n\n// Force kill (SIGKILL)\nawait agent.killProcess(pid);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*\n\n## Interactive shells\n\nOpen an interactive shell with PTY support. Shell data is streamed via `shellData` events.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\nconst conn = agent.connect();\n\n// Subscribe to shell output\nconn.on(\"shellData\", (data) => {\n const text = new TextDecoder().decode(data.data);\n process.stdout.write(text);\n});\n\n// Open a shell\nconst { shellId } = await agent.openShell();\n\n// Write commands to the shell\nawait agent.writeShell(shellId, \"ls -la /home/agentos\\n\");\n\n// Resize the terminal\nawait agent.resizeShell(shellId, 120, 40);\n\n// Close the shell when done\nawait agent.closeShell(shellId);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/processes)*","src/content/docs/docs/processes.mdx","9f67b62499e23168","docs/quickstart",{"id":254,"data":256,"body":262,"filePath":263,"digest":264,"deferredRender":16},{"title":257,"description":258,"editUrl":16,"head":259,"template":18,"sidebar":260,"pagefind":16,"draft":20},"Quickstart","Set up an agentOS actor, create a session, and run your first coding agent.",[],{"hidden":20,"attrs":261},{},"import { Aside, Steps } from '@astrojs/starlight/components';\nimport CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\n\u003Cdiv style=\"border-radius:0.75rem;border:1px solid rgba(27,25,22,0.12);background:rgba(27,25,22,0.035);padding:0.875rem 1.125rem;margin:1.5rem 0;color:#56524a;display:flex;align-items:center;justify-content:space-between;gap:1.25rem;\">\n\u003Cspan>Use this pre-built prompt to get started faster.\u003C/span>\n\u003Cbutton type=\"button\" onclick=\"var b=this;navigator.clipboard.writeText(b.getAttribute('data-prompt')||'').then(function(){b.textContent='Copied!';setTimeout(function(){b.textContent='Copy prompt';},1500);});\" data-prompt=\"Set up a new agentOS project from scratch. agentOS runs each AI coding agent inside its own isolated VM. Do the following: 1. Initialize a Node + TypeScript project and install the dependencies: `npm install @rivet-dev/agentos @agentos-software/pi`. 2. Create `server.ts` that defines a VM actor with the Pi coding agent installed and starts the registry: import { agentOS, setup } from "@rivet-dev/agentos"; import pi from "@agentos-software/pi"; const vm = agentOS({ software: [pi] }); export const registry = setup({ use: { vm } }); registry.start(); 3. Create `client.ts` that connects to the server, gets or creates a VM, opens a Pi session with the ANTHROPIC_API_KEY from the environment, sends a prompt, and reads back the file the agent wrote: 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 session = await handle.createSession("pi", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! } }); await handle.sendPrompt(session.sessionId, "Write a hello world script to /workspace/hello.js"); const content = await handle.readFile("/workspace/hello.js"); console.log(new TextDecoder().decode(content)); 4. Explain how to run both: `npx tsx server.ts` in one terminal, then `npx tsx client.ts` in another, with ANTHROPIC_API_KEY set in the environment.\" style=\"appearance:none;border:1px solid rgba(27,25,22,0.18);background:#1b1916;color:#f4f1e7;font-family:var(--sl-font);font-size:0.8rem;font-weight:600;display:inline-flex;align-items:center;justify-content:center;height:2rem;padding:0 0.85rem;border-radius:6px;cursor:pointer;white-space:nowrap;margin-top:0;flex:none;box-sizing:border-box;\">Copy prompt\u003C/button>\n\u003C/div>\n\n\u003Cdiv style=\"border-radius:0.75rem;border:1px solid rgba(27,25,22,0.12);background:rgba(27,25,22,0.035);padding:0.875rem 1.125rem;margin:1.5rem 0;color:#56524a;display:flex;align-items:center;justify-content:space-between;gap:1.25rem;\">\n\u003Cspan>Prefer to read code? Clone the example repository.\u003C/span>\n\u003Ca href=\"https://github.com/rivet-dev/agent-os/tree/main/examples/docs/quickstart\" style=\"appearance:none;border:1px solid rgba(27,25,22,0.18);background:transparent;color:#1b1916;font-family:var(--sl-font);font-size:0.8rem;font-weight:600;display:inline-flex;align-items:center;justify-content:center;height:2rem;padding:0 0.85rem;border-radius:6px;cursor:pointer;white-space:nowrap;text-decoration:none;flex:none;gap:0.45rem;box-sizing:border-box;\">\u003Csvg viewBox=\"0 0 496 512\" width=\"14\" height=\"14\" fill=\"currentColor\" aria-hidden=\"true\">\u003Cpath d=\"M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z\"/>\u003C/svg>View on GitHub\u003C/a>\n\u003C/div>\n\n\u003Csvg viewBox=\"0 0 400 210\" role=\"img\" aria-label=\"A client (JavaScript, browser, or another backend) connects to a server that runs each agent in its own isolated VM, marked with the agentOS 'OS' logo.\" style=\"width:100%;height:auto;max-width:420px;display:block;margin:3rem auto 2.5rem;\">\n \u003Cdefs>\n \u003Cmarker id=\"qs-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\">\n \u003Cpath d=\"M0,0 L10,5 L0,10 z\" fill=\"#1b1916\" />\n \u003C/marker>\n \u003Csymbol id=\"qs-os\" viewBox=\"0 0 100 100\">\n \u003Crect x=\"8\" y=\"8\" width=\"84\" height=\"84\" rx=\"26\" fill=\"none\" stroke=\"#1b1916\" stroke-width=\"8\" />\n \u003Ctext x=\"50\" y=\"50\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"38\" fill=\"#1b1916\">OS\u003C/text>\n \u003C/symbol>\n \u003C/defs>\n \u003Crect x=\"12\" y=\"67\" width=\"140\" height=\"60\" rx=\"12\" fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"82\" y=\"92\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"15\" font-weight=\"600\" fill=\"#1b1916\">Client\u003C/text>\n \u003Ctext x=\"82\" y=\"112\" text-anchor=\"middle\" font-family=\"var(--sl-font)\" font-size=\"10.5\" fill=\"#56524a\">JS · Browser · Backend\u003C/text>\n \u003Cline x1=\"154\" y1=\"97\" x2=\"205\" y2=\"97\" stroke=\"#1b1916\" stroke-width=\"1.5\" marker-end=\"url(#qs-arrow)\" />\n \u003Crect x=\"210\" y=\"40\" width=\"164\" height=\"114\" rx=\"14\" fill=\"#faf8f3\" stroke=\"#1b1916\" stroke-width=\"1.5\" />\n \u003Ctext x=\"224\" y=\"62\" font-family=\"var(--sl-font)\" font-size=\"13\" font-weight=\"600\" fill=\"#1b1916\">Server\u003C/text>\n \u003Cg fill=\"#ffffff\" stroke=\"#1b1916\" stroke-width=\"1.2\">\n \u003Crect x=\"224\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"76\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"224\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"260\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"296\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003Crect x=\"332\" y=\"112\" width=\"28\" height=\"28\" rx=\"5\" />\n \u003C/g>\n \u003Cg>\n \u003Cuse href=\"#qs-os\" x=\"229\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"265\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"301\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"337\" y=\"81\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"229\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"265\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"301\" y=\"117\" width=\"18\" height=\"18\" />\n \u003Cuse href=\"#qs-os\" x=\"337\" y=\"117\" width=\"18\" height=\"18\" />\n \u003C/g>\n \u003Cg>\n \u003Crect x=\"146\" y=\"170\" width=\"15\" height=\"15\" rx=\"4\" fill=\"none\" stroke=\"#56524a\" stroke-width=\"1.4\" />\n \u003Ctext x=\"153.5\" y=\"178\" text-anchor=\"middle\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-weight=\"700\" font-size=\"7\" fill=\"#56524a\">OS\u003C/text>\n \u003Ctext x=\"170\" y=\"178\" dominant-baseline=\"central\" font-family=\"var(--sl-font)\" font-size=\"12\" fill=\"#56524a\">= agentOS VM\u003C/text>\n \u003C/g>\n\u003C/svg>\n\n\u003CSteps>\n\n1. **Install**\n\n - **@rivet-dev/agentos** — Actor framework with built-in persistence and orchestration\n - **@agentos-software/pi** — [Pi](https://github.com/mariozechner/pi-coding-agent) coding agent (Claude Code, Amp, and OpenCode coming soon)\n\n ```bash\n npm install @rivet-dev/agentos @agentos-software/pi\n ```\n\n2. **Create the server**\n\n ```ts title=\"server.ts\"\n import { agentOS, setup } from \"@rivet-dev/agentos\";\n import pi from \"@agentos-software/pi\";\n\n const vm = agentOS({\n software: [pi],\n });\n\n export const registry = setup({ use: { vm } });\n registry.start();\n ```\n\n3. **Create the client**\n\n The client can be any public frontend or another backend. The same `vm` actor is reachable from a plain Node script, a browser/React app, or a separate server.\n\n \u003CCodeGroup>\n ```ts title=\"TypeScript\"\n import { createClient } from \"@rivet-dev/agentos/client\";\n import type { registry } from \"./server\";\n\n const client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n const handle = client.vm.getOrCreate(\"my-agent\");\n\n // Subscribe to streaming events. The payload is inferred from the event schema.\n const conn = handle.connect();\n conn.on(\"sessionEvent\", (data) => {\n console.log(data.event);\n });\n\n // Create a session and send a prompt\n const session = await handle.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await handle.sendPrompt(\n session.sessionId,\n \"Write a hello world script to /workspace/hello.js\",\n );\n\n // Read the file the agent created\n const content = await handle.readFile(\"/workspace/hello.js\");\n console.log(new TextDecoder().decode(content));\n ```\n\n ```tsx title=\"React\"\n import { createRivetKit } from \"@rivet-dev/agentos/react\";\n import { useState } from \"react\";\n import type { registry } from \"./server\";\n\n const { useActor } = createRivetKit\u003Ctypeof registry>(\"http://localhost:6420\");\n\n export function Agent() {\n const [log, setLog] = useState(\"\");\n const agent = useActor({ name: \"vm\", key: \"my-agent\" });\n\n // Stream agent events into component state\n agent.useEvent(\"sessionEvent\", (data) => {\n setLog((prev) => prev + JSON.stringify(data.event) + \"\\n\");\n });\n\n async function run() {\n // In production, inject credentials on the server (see /docs/llm-credentials)\n const session = await agent.connection?.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.VITE_ANTHROPIC_API_KEY! },\n });\n if (!session) return;\n await agent.connection?.sendPrompt(\n session.sessionId,\n \"Write a hello world script to /workspace/hello.js\",\n );\n }\n\n return (\n \u003Cdiv>\n \u003Cbutton onClick={run}>Run agent\u003C/button>\n \u003Cpre>{log}\u003C/pre>\n \u003C/div>\n );\n }\n ```\n \u003C/CodeGroup>\n\n4. **Run it**\n\n Start the server, then run the client in a second terminal:\n\n ```bash\n # Terminal 1: start the server\n npx tsx server.ts\n\n # Terminal 2: run the client\n npx tsx client.ts\n ```\n\n5. **Customize**\n\n Now that you have a working agent, customize it to fit your needs:\n\n - **[Software](/docs/software)** — Install software packages inside the VM\n - **[Filesystem](/docs/filesystem)** — Read, write, and manage files inside the VM\n - **[Permissions & Resource Limits](/docs/permissions)** — Gate what the agent can do and cap its resource usage\n - **[Bindings](/docs/bindings)** — Expose your JavaScript functions to agents as CLI commands\n\n\u003C/Steps>\n\n\u003CAside type=\"note\">\nagentOS is in preview and the API is subject to change. If you run into issues, please [report them on GitHub](https://github.com/rivet-dev/rivet/issues) or [join our Discord](https://rivet.dev/discord).\n\u003C/Aside>\n\n\n## agentOS Core\n\nThe quickstart above uses `@rivet-dev/agentos`, which includes statefulness, multiplayer, and orchestration out of the box. If you only need direct VM control without those features, you can use the core package (`@rivet-dev/agentos-core`) standalone.\n\nSee [agentOS core documentation](/docs/core) for reference.","src/content/docs/docs/quickstart.mdx","9a85e064b655bb19","docs/sandbox",{"id":265,"data":267,"body":273,"filePath":274,"digest":275,"deferredRender":16},{"title":268,"description":269,"editUrl":16,"head":270,"template":18,"sidebar":271,"pagefind":16,"draft":20},"Sandbox Mounting","Extend agentOS with full sandboxes for heavy workloads like browsers, desktop automation, and compilation.",[],{"hidden":20,"attrs":272},{},"For heavy workloads like browsers, desktop automation, and compilation, pair agentOS with a full sandbox on demand. Its filesystem mounts into the VM as a native directory, and its process management is exposed as [bindings](/docs/bindings), all provider-agnostic through [Sandbox Agent](https://sandboxagent.dev).\n\n## Why use agentOS with a sandbox?\n\nagentOS is an alternative to sandboxes that covers most use cases, but some workloads need a full sandbox for special kinds of software (browsers, desktop automation, heavy compilation). Sandbox mounting lets you lazily start a sandbox on demand, only when it is needed, and project it into the VM. The hybrid model means one agent session can handle both lightweight coding tasks and heavy system operations, using the right tool for each.\n\nSee [agentOS vs Sandbox](/docs/versus-sandbox) for a detailed comparison.\n\n## When to use a sandbox\n\n- **Native binaries** not yet supported in the agentOS runtime.\n- **Browsers and desktop automation**: Playwright, Puppeteer, Selenium, or anything that needs a display server.\n- **Heavy compilation**: Large builds or native toolchains that require a full Linux environment.\n- **GUI applications**: Desktop apps, VNC sessions, or any workload that needs a graphical environment.\n- **Node.js packages with native extensions** (e.g. `sharp`, `bcrypt`, `better-sqlite3`) that require a full build toolchain.\n\nStart with the default agentOS VM for all workloads, and only spin up a sandbox when a task genuinely requires one. Sandboxes are billed per second of uptime, so start them on demand and tear them down when the task is done.\n\n## Getting started\n\nThe sandbox integration ships as the `@rivet-dev/agentos-sandbox` package. It works through two mechanisms:\n\n- **Filesystem mount**: Projects the sandbox into the VM as a native directory, like mounting a hard drive on your own machine. Read and write files through the mount directly.\n- **Bindings**: Exposes sandbox process management as [bindings](/docs/bindings). Execute commands on the sandbox from within the VM.\n\nBoth are powered by [Sandbox Agent](https://sandboxagent.dev), and you can swap providers without changing agent code. Install both packages:\n\n```bash\nnpm install @rivet-dev/agentos-sandbox sandbox-agent\n```\n\n`createSandboxFs` and `createSandboxBindings` come from `@rivet-dev/agentos-sandbox`. `SandboxAgent` and the provider helpers (such as `docker`) come from the `sandbox-agent` package.\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createSandboxFs, createSandboxBindings } from \"@rivet-dev/agentos-sandbox\";\nimport { SandboxAgent } from \"sandbox-agent\";\nimport { docker } from \"sandbox-agent/docker\";\n\n// Start a sandbox through Sandbox Agent. Any provider works; Docker is used here.\nconst sandbox = await SandboxAgent.start({ sandbox: docker() });\n\n// `createSandboxFs` returns a mount plugin descriptor that projects the sandbox\n// filesystem into the VM, and `createSandboxBindings` exposes the sandbox's\n// process management as bindings.\nconst vm = agentOS({\n\t// Bindings let the agent control the sandbox\n\tbindings: [createSandboxBindings({ client: sandbox })],\n\t// Mounts let the agent read the sandbox filesystem (optional)\n\tmounts: [\n\t\t{ path: \"/home/agentos/sandbox\", plugin: createSandboxFs({ client: sandbox }) },\n\t],\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sandbox)*\n\n## Calling the mounted bindings\n\nOnce the sandbox is mounted, write code through the filesystem and run it inside the sandbox. The sandbox bindings are exposed inside the VM as a CLI command, so you call it through the same `exec`/`spawn` surface as any other command.\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst vm = client.vm.getOrCreate(\"my-agent\");\n\n// Write code via the filesystem. The /home/agentos/sandbox mount maps to the sandbox root.\nawait vm.writeFile(\"/home/agentos/sandbox/app/index.ts\", 'console.log(\"hello\")');\n\n// Run it inside the sandbox. Commands execute through the VM's process table,\n// reading the file from the mounted directory.\nconst result = await vm.exec(\"node /home/agentos/sandbox/app/index.ts\");\nconsole.log(result.stdout); // \"hello\\n\"\n\n// Call a mounted binding from the client. The sandbox bindings are exposed inside the\n// VM as a CLI command, so you invoke it through the same exec/spawn surface.\nconst install = await vm.exec(\"agentos-sandbox run-command --command \\\"npm install\\\" --cwd /home/agentos/sandbox/app\");\nconsole.log(install.exitCode, install.stdout);\n\n// Spawn a long-running process via the bindings and stream its output.\nconst { pid } = await vm.spawn(\"agentos-sandbox\", [\n\t\"create-process\",\n\t\"--command\",\n\t\"npm\",\n\t\"--args\",\n\t\"run\",\n\t\"--args\",\n\t\"dev\",\n]);\nconst conn = vm.connect();\nconn.on(\"processOutput\", (payload) => {\n\tif (payload.pid === pid) {\n\t\tconsole.log(payload.stream, new TextDecoder().decode(payload.data));\n\t}\n});\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sandbox)*\n\n## Bindings reference\n\nThe bindings expose these commands inside the VM:\n\n```bash\n# Run a command synchronously\nagentos-sandbox run-command --command \"npm install\" --cwd \"/app\"\n\n# Start a background process\nagentos-sandbox create-process --command \"npm\" --args \"run\" --args \"dev\"\n\n# List running processes\nagentos-sandbox list-processes\n\n# Get process output\nagentos-sandbox get-process-logs --id \"proc_abc123\"\n\n# Stop or kill a process\nagentos-sandbox stop-process --id \"proc_abc123\"\nagentos-sandbox kill-process --id \"proc_abc123\"\n\n# Send input to an interactive process\nagentos-sandbox send-input --id \"proc_abc123\" --data \"yes\"\n```\n\n## Sandbox providers\n\nThe extension works with any [Sandbox Agent](https://sandboxagent.dev) provider. See the [Sandbox Agent documentation](https://sandboxagent.dev) for available providers and setup instructions.","src/content/docs/docs/sandbox.mdx","63a0b8f188c17422","docs/security-model",{"id":276,"data":278,"body":284,"filePath":285,"digest":286,"deferredRender":16},{"title":279,"description":280,"editUrl":16,"head":281,"template":18,"sidebar":282,"pagefind":16,"draft":20},"Security Model","Trust boundaries, isolation guarantees, and the agentOS threat model.",[],{"hidden":20,"attrs":283},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"caution\">\nagentOS is in beta and still undergoing security review. The security model described here is subject to change.\n\u003C/Aside>\n\nagentOS is a sandbox: it runs **untrusted code safely on behalf of a trusted caller**. Every actor boots its own fully virtualized VM with a virtual filesystem, process table, socket table, pipes, PTYs, a permission policy, and managed language runtimes. Guest JavaScript executes in a V8 isolate, and every guest syscall is serviced by the kernel rather than the host. There are no host escapes: guest code cannot spawn a real host process, touch the real host filesystem, or open a real host network socket.\n\n## Deny by default\n\nNo syscalls are bound to the system by default. Everything is denied until explicitly opted in.\n\n- **Network access** is denied until you opt in with a `network` permission.\n- **Filesystem mounts** expose nothing of the host until you configure them.\n- **Process spawning** runs only kernel-managed guest processes, never host processes.\n- **All other host capabilities** must be configured by the host before the VM can use them.\n\nOther in-VM scopes (the virtual filesystem, child processes, process info, env) are enabled so that normal programs run, but they are mediated entirely by the kernel and never touch the host.\n\n## Trust model: three components\n\nBefore judging whether something is a security bug, decide which side of the boundary it is on. agentOS has three components with very different trust levels.\n\n\u003Csvg width=\"700\" height=\"270\" viewBox=\"0 0 700 270\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"Three-component trust model: client, sidecar, executor\">\n \u003Crect x=\"20\" y=\"40\" width=\"180\" height=\"190\" rx=\"10\" fill=\"#f4f6f8\" stroke=\"#c8d0d8\" stroke-width=\"1.5\" />\n \u003Ctext x=\"110\" y=\"68\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#111827\">Client\u003C/text>\n \u003Ctext x=\"110\" y=\"90\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"12\" fill=\"#374151\">(trusted)\u003C/text>\n \u003Ctext x=\"110\" y=\"120\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Your host app\u003C/text>\n \u003Ctext x=\"110\" y=\"138\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Configures the VM\u003C/text>\n \u003Ctext x=\"110\" y=\"170\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">Untrusted: only the\u003C/text>\n \u003Ctext x=\"110\" y=\"186\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">code it submits\u003C/text>\n \u003Crect x=\"260\" y=\"40\" width=\"180\" height=\"190\" rx=\"10\" fill=\"#eef2f6\" stroke=\"#c8d0d8\" stroke-width=\"1.5\" />\n \u003Ctext x=\"350\" y=\"68\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#111827\">Sidecar / Kernel\u003C/text>\n \u003Ctext x=\"350\" y=\"90\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"12\" fill=\"#374151\">(trusted = TCB)\u003C/text>\n \u003Ctext x=\"350\" y=\"120\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Owns VFS, processes,\u003C/text>\n \u003Ctext x=\"350\" y=\"138\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">sockets, policy\u003C/text>\n \u003Ctext x=\"350\" y=\"170\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Enforces the\u003C/text>\n \u003Ctext x=\"350\" y=\"186\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">boundary\u003C/text>\n \u003Crect x=\"500\" y=\"40\" width=\"180\" height=\"190\" rx=\"10\" fill=\"#fdf2f2\" stroke=\"#e6b8b8\" stroke-width=\"1.5\" />\n \u003Ctext x=\"590\" y=\"68\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"16\" font-weight=\"700\" fill=\"#111827\">Executor\u003C/text>\n \u003Ctext x=\"590\" y=\"90\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"12\" fill=\"#9a3412\">(untrusted = adversary)\u003C/text>\n \u003Ctext x=\"590\" y=\"120\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">V8 isolate / WASM\u003C/text>\n \u003Ctext x=\"590\" y=\"138\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#374151\">Runs guest code\u003C/text>\n \u003Ctext x=\"590\" y=\"170\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">Assume actively\u003C/text>\n \u003Ctext x=\"590\" y=\"186\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" fill=\"#9a3412\">hostile\u003C/text>\n \u003Cline x1=\"200\" y1=\"135\" x2=\"258\" y2=\"135\" stroke=\"#6b7280\" stroke-width=\"1.5\" marker-end=\"url(#arrow)\" />\n \u003Cline x1=\"440\" y1=\"135\" x2=\"498\" y2=\"135\" stroke=\"#b91c1c\" stroke-width=\"1.5\" stroke-dasharray=\"4 3\" marker-end=\"url(#arrowred)\" />\n \u003Ctext x=\"229\" y=\"128\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"9\" fill=\"#6b7280\">wire\u003C/text>\n \u003Ctext x=\"469\" y=\"128\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"9\" fill=\"#b91c1c\">syscalls\u003C/text>\n \u003Ctext x=\"469\" y=\"252\" text-anchor=\"middle\" font-family=\"Manrope, sans-serif\" font-size=\"11\" font-weight=\"700\" fill=\"#b91c1c\">SECURITY BOUNDARY\u003C/text>\n \u003Cdefs>\n \u003Cmarker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"6\" refY=\"3\" orient=\"auto\">\u003Cpath d=\"M0,0 L6,3 L0,6 Z\" fill=\"#6b7280\" />\u003C/marker>\n \u003Cmarker id=\"arrowred\" markerWidth=\"8\" markerHeight=\"8\" refX=\"6\" refY=\"3\" orient=\"auto\">\u003Cpath d=\"M0,0 L6,3 L0,6 Z\" fill=\"#b91c1c\" />\u003C/marker>\n \u003C/defs>\n\u003C/svg>\n\n### Client (trusted)\n\nThe party that configures and manages the VM: your application code, container, or serverless function.\n\n- The client process and **every value it sends** are trusted: VM config, mount descriptors and their plugin configs (host directory paths, S3 endpoints and credentials, etc.), the permission policy, network allowlist, resource limits, env, and DNS overrides.\n- **Configuration is not an attack surface.** A defect that requires the client to supply a malicious config, endpoint, credential, or policy is not a sandbox vulnerability: the client is configuring its own VM and already controls the host.\n- The **one** thing from the client that is *not* trusted is the **code/payload** it asks to run, because that runs in the executor. How code reached the executor never makes it trusted.\n\nYou are responsible for hardening this side. See [What you are responsible for](#what-you-are-responsible-for).\n\n### Sidecar / kernel (trusted, the enforcement point)\n\nThe trusted computing base. It brokers client requests and owns the kernel, VFS, mount/plugin registry, socket table, and permission policy. It is responsible for enforcing the boundary against the executor.\n\n### Executor (untrusted, the adversary)\n\nV8 isolates or WASM running guest JS/Python/WASM plus any third-party, npm, or agent-generated code.\n\n- Assume everything here is **actively hostile**.\n- The executor reaches the outside world only through kernel-owned VFS, process, socket, pipe, PTY, permission, and DNS paths.\n\n## The security boundary\n\n**The security boundary is sidecar ↔ executor.** The runtime must stop guest code in the executor from:\n\n- Escaping the kernel boundary (the real host filesystem, network, process table, or memory).\n- Bypassing the **applied** permission policy, allowlist, or limits.\n- Exhausting host resources beyond configured bounds.\n- Reading another VM's state.\n\nTwo corollaries that are easy to get wrong:\n\n- **Trusted policy, untrusted subject.** The permission policy and limits are trusted input, but the guest executor is the subject they bind. \"Guest bypasses an applied permission, egress rule, or resource cap\" is in-scope and serious. Trusted = who sets the rule; untrusted = who is bound by it.\n- **Trusted mount, untrusted traffic.** A host-backed mount (host directory, S3, etc.) comes from trusted config, so its existence, target, and credentials are not attack surface. But the guest drives I/O through it, so confining those guest operations to the mount root (symlink, `..`, TOCTOU, and path-aliasing escapes) is in-scope.\n\n### In scope vs out of scope\n\n| In scope (sandbox escape) | Out of scope (not a sandbox bug) |\n| --- | --- |\n| Guest reaches the real host fs / net / process / memory | Client supplies a malicious config / endpoint / credential / policy |\n| Guest bypasses an applied permission, egress rule, or limit | Hardening that only guards trusted client-provided configuration |\n| Guest exhausts host resources past configured bounds | Wire-level authn/authz between mutually distrusting clients |\n| Guest reads another VM's state | VM-to-VM access via forged connection IDs (single-client transport) |\n| Guest escapes a host-backed mount root (symlink / `..` / TOCTOU) | The existence or target of a configured mount |\n\n**Transport scope.** The wire protocol is same-version lockstep and single-client over stdio (one trusted client per sidecar process). There is no second, mutually-distrusting client, so wire-level authn/authz between clients and VM-to-VM access via forged connection IDs are out of scope until a multi-client transport exists.\n\n## VM isolation\n\nEach agentOS actor runs in its own isolated VM.\n\n- **Sandboxed execution.** All agent code runs inside a V8 isolate with WebAssembly. No code escapes the isolate boundary.\n- **Virtual filesystem.** The VM has its own in-memory filesystem. Guest reads and writes never reach the real host filesystem. Agents cannot access host files unless explicitly mounted.\n- **Virtual network.** The VM has no direct access to the host network. Outbound requests are proxied through the host with configurable controls.\n- **Process isolation.** No host process is visible or accessible from inside the VM.\n- **Per-actor containment.** Each actor is its own VM. Two actors share no filesystem, globals, module state, memory, or crash fate. The sidecar process that hosts those VMs may be shared by default as a performance optimization, but isolation is enforced at the VM level, not the host-process level.\n\n### Kernel-owned syscall paths\n\nEvery guest syscall is mediated by the kernel and checked against the runtime's permission policy. Concretely, the kernel mediates:\n\n- **Filesystem.** A virtual, in-memory filesystem. Guest reads and writes never reach the real host filesystem. Host data enters the VM only through the `files`, `mounts`, or `nodeModules` you configure explicitly. See [Filesystem](/docs/filesystem).\n- **Processes.** `node:child_process` spawns kernel-managed guest processes, never real host processes. Children can only run the commands the VM mounts (WASM-backed `sh` and coreutils, V8-backed `node`). See [Processes](/docs/processes).\n- **Network.** Guest `fetch()`, `node:http`, and raw sockets all flow through the kernel socket table. Guest `fetch()` runs through undici inside the isolate and then through the kernel socket table; it never opens a real host socket. See [Networking](/docs/networking).\n- **DNS, pipes, and PTYs** are likewise kernel-owned: no guest path reaches the host directly.\n- **Bindings.** Registered [bindings](/docs/bindings) are the only sanctioned way to hand the guest a named host capability. The guest invokes a binding by name with JSON input, the call round-trips to the host handler, and only the handler's return value comes back. The guest never receives the underlying host access.\n\n## What enters the VM\n\nThe host filesystem is never exposed to the guest by default. Host data crosses the boundary only through options you configure:\n\n- **`files`** seed bytes into the virtual filesystem. The bytes are copied in; the host path is never exposed.\n- **`mounts`** project a host directory at a guest path, Docker-style. The guest sees only the mounted subtree, read through the VFS lazily, never the wider host filesystem. Mounts are read-only unless you opt out.\n- **`nodeModules`** project a host `node_modules` directory (read-only, lazily) at a guest path so guest `import`/`require` resolves real installed packages.\n\nIn every case the guest sees only the subtree you mount, and writes to read-only mounts are rejected.\n\n## Permissions\n\nPermissions are the capability gate at the boundary. They merge over a secure default that denies the network and enables the filesystem, child processes, process info, and env. Because the merge is partial, you name only the scope you change.\n\n```ts\n// Grant network egress; everything else keeps the secure defaults.\npermissions: { network: \"allow\" }\n```\n\nA scope can be `\"allow\"`, `\"deny\"`, or a `{ default, rules }` policy that matches request patterns. Guest servers are reachable only over loopback inside the VM unless you exempt a port explicitly. See [Permissions](/docs/permissions) and [Networking](/docs/networking) for the full policy shape.\n\n## Resource and timing limits\n\nThe VM bounds guest execution so runaway or hostile code cannot hang or exhaust the host:\n\n- **Timeouts and cancellation** kill or cancel a run from the outside.\n- **Memory, CPU-time, and payload limits** are enforced by the VM.\n- **Timing-side-channel mitigation.** In the default mode, high-resolution clocks (`Date.now()`, `performance.now()`, `process.hrtime()`) are frozen within a run and `SharedArrayBuffer` is removed, to blunt timing side channels of the kind used in Spectre-style attacks.\n\nSee [Security & Auth](/docs/security-model) for resource limits, network control, and authentication setup.\n\n## What agentOS guarantees\n\n- Agent code cannot read or write host files outside configured mounts.\n- Agent code cannot make network requests except through the host proxy.\n- Agent code cannot access host environment variables or secrets.\n- Each actor's filesystem, sessions, and state are isolated from other actors.\n- Resource limits (CPU, memory) are enforced at the VM level.\n- A crash, resource exhaustion, or escape attempt is contained to a single VM; other VMs keep running, even when they share a sidecar process.\n\n## What you are responsible for\n\nThe boundary protects the host from the guest. It does **not** harden your host process against everything else. The VM alone is not enough without a hardened host, and a hardened host alone does not protect against code that runs with full host access inside your own process.\n\n- Hardening the host process and deployment environment. For internet-facing workloads that take untrusted input, run your host inside an already-hardened environment (for example AWS Lambda, Google Cloud Run, or a similar sandboxed platform).\n- Validating authentication tokens in `onBeforeConnect`.\n- Scoping [permissions](/docs/permissions) appropriately for your use case.\n- Managing API keys and secrets on the host side (use the [LLM gateway](/docs/llm-gateway) to avoid passing keys into the VM).\n- Configuring [resource limits and network controls](/docs/security-model) to match your threat model.\n- Choosing your blast radius: prefer a fresh VM per untrusted or high-risk task so an escape attempt cannot outlive a single VM.\n\n\u003CAside type=\"caution\">The boundary contains guest code, but you still own the host. Treat the host process as trusted infrastructure and harden it.\u003C/Aside>\n\n## Further reading\n\n- [Security configuration](/docs/security-model) for resource limits, network control, and authentication setup\n- [Permissions](/docs/permissions) for agent tool-use approval patterns\n- [agentOS vs Sandbox](/docs/versus-sandbox) for when to escalate to a full sandbox","src/content/docs/docs/security-model.mdx","02132bd43ce0b857","docs/sessions",{"id":287,"data":289,"body":295,"filePath":296,"digest":297,"deferredRender":16},{"title":290,"description":291,"editUrl":16,"head":292,"template":18,"sidebar":293,"pagefind":16,"draft":20},"Sessions","Create agent sessions, send prompts, stream responses, and subscribe to events.",[],{"hidden":20,"attrs":294},{},"import { Aside } from '@astrojs/starlight/components';\nimport CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nSessions launch an agent inside the VM, stream its responses in real time over `sessionEvent`, and persist a replayable ACP transcript that survives sleep/wake.\n\n## Create a session\n\nUse `createSession` to launch an agent inside the VM. Returns session metadata including capabilities and agent info. The agent starts in `/home/agentos` by default; override it with the `cwd` option below.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconsole.log(session.sessionId);\nconsole.log(session.capabilities);\nconsole.log(session.agentInfo);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n### `createSession` options\n\nThe second argument to `createSession` accepts:\n\n- **`env`**: environment variables for the agent process (e.g. API keys). Not inherited from the host.\n- **`cwd`**: working directory inside the VM. Defaults to `/home/agentos`.\n- **`mcpServers`**: MCP servers (local child processes or remote URLs) exposing extra tools.\n- **`additionalInstructions`**: text appended to the agent's system prompt.\n- **`skipOsInstructions`**: skip the base OS instructions injection. Tool documentation is still included.\n\n\n## Send a prompt\n\nUse `sendPrompt` to send a message to an active session. The response contains the agent's reply.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconst response = await agent.sendPrompt(\n session.sessionId,\n \"Create a TypeScript function that checks if a number is prime\",\n);\nconsole.log(response.text);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Stream responses\n\nSubscribe to `sessionEvent` to receive real-time streaming output from the agent.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\nconst conn = agent.connect();\n\n// Subscribe to session events before sending the prompt\nconn.on(\"sessionEvent\", (data) => {\n console.log(`[${data.sessionId}]`, data.event.method, data.event.params);\n});\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Explain how async/await works\");\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n\n## Cancel a prompt\n\nUse `cancelPrompt` to stop an in-progress prompt.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Start a long-running prompt\nconst promptPromise = agent.sendPrompt(\n session.sessionId,\n \"Refactor the entire codebase to use TypeScript strict mode\",\n);\n\n// Cancel after 10 seconds\nsetTimeout(async () => {\n await agent.cancelPrompt(session.sessionId);\n}, 10_000);\n\nconst response = await promptPromise;\nconsole.log(response.text);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Close and destroy sessions\n\n- `closeSession` gracefully closes a session without removing persisted data\n- `destroySession` removes the session and all persisted data\n- To reconnect to a previously created session and replay its history, see [Replay events](#replay-events) and [Resuming a suspended session](/docs/architecture/agent-sessions#resuming-a-suspended-session)\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Close without destroying persisted data\nawait agent.closeSession(session.sessionId);\n\n// Destroy session and all persisted events\nawait agent.destroySession(session.sessionId);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Runtime configuration\n\nChange model, mode, and thought level on a live session.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Change model\nawait agent.setModel(session.sessionId, \"claude-sonnet-4-6\");\n\n// Change mode (e.g. \"plan\", \"auto\")\nawait agent.setMode(session.sessionId, \"plan\");\n\n// Change thought level\nawait agent.setThoughtLevel(session.sessionId, \"high\");\n\n// Query available options\nconst modes = await agent.getModes(session.sessionId);\nconsole.log(modes);\n\nconst options = await agent.getConfigOptions(session.sessionId);\nconsole.log(options);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Replay events\n\nUse `getSessionEvents` to replay a session's persisted events, including for VMs that are not currently running. Pair it with `listPersistedSessions` to find earlier sessions.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nawait agent.sendPrompt(session.sessionId, \"Hello\");\n\n// Replay persisted events\nconst events = await agent.getSessionEvents(session.sessionId);\nconsole.log(events);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Persisted session history\n\nQuery session history from SQLite. Works even when the VM is not running.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// List all persisted sessions\nconst sessions = await agent.listPersistedSessions();\nfor (const s of sessions) {\n console.log(s.sessionId, s.agentType, s.createdAt);\n}\n\n// Get full event history for a session\nconst events = await agent.getSessionEvents(sessions[0].sessionId);\nfor (const e of events) {\n console.log(e.seq, e.event.method, e.createdAt);\n}\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*\n\n## Multiple sessions\n\nA single VM can run multiple sessions simultaneously. Each session has its own agent process but shares the same filesystem. Use different session IDs to manage them independently.\n\n\u003CCodeGroup>\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// Create two sessions in the same VM\nconst coder = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\nconst reviewer = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\n// Coder writes code\nawait agent.sendPrompt(coder.sessionId, \"Write a REST API at /home/agentos/api.ts\");\n\n// Reviewer reads and reviews the same file\nawait agent.sendPrompt(reviewer.sessionId, \"Review /home/agentos/api.ts for issues\");\n\n// Close each session independently\nawait agent.closeSession(coder.sessionId);\nawait agent.closeSession(reviewer.sessionId);\n```\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({ software: [pi] });\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/sessions)*","src/content/docs/docs/sessions.mdx","fb57047ce9d609e5","docs/software",{"id":298,"data":300,"body":306,"filePath":307,"digest":308,"deferredRender":16},{"title":301,"description":302,"editUrl":16,"head":303,"template":18,"sidebar":304,"pagefind":16,"draft":20},"Software","Install software packages and configure the commands available inside agentOS.",[],{"hidden":20,"attrs":305},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nagentOS ships with a common set of POSIX utilities (coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip) out of the box. The `software` option installs additional packages, each providing one or more CLI commands.\n\n## Install\n\n```bash\nnpm install @rivet-dev/agentos @agentos-software/pi\n```\n\nAdd packages like `@agentos-software/ripgrep` or `@agentos-software/jq` for anything beyond the default utilities. Browse the full catalog on the [Registry](/registry).\n\n## Usage\n\nImport the software packages you want, list them in the `software` array on the actor, then run commands through the client handle.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\nimport ripgrep from \"@agentos-software/ripgrep\";\nimport jq from \"@agentos-software/jq\";\n\n// Each entry adds its CLI commands to the VM. Common POSIX utilities ship by\n// default; `pi` is the agent, and `ripgrep`/`jq` add the `rg` and `jq`\n// commands. Browse the registry for more packages.\nconst vm = agentOS({\n\tsoftware: [pi, ripgrep, jq],\n});\n\nexport const registry = setup({ use: { vm } });\n\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({\n\tendpoint: \"http://localhost:6420\",\n});\nconst agent = client.vm.getOrCreate(\"my-agent\");\n\n// `rg` (ripgrep) and `jq` are now available inside the VM. Find files containing\n// \"TODO\" and pretty-print the matching paths as JSON.\nconst result = await agent.exec(\"rg --files-with-matches TODO /home/agentos | jq -R .\");\nconsole.log(result.stdout);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/software)*\n\n## Available Packages\n\nBrowse all available software packages on the [Registry](/registry).\n\n\n## Publishing Custom Packages\n\nSee the [agentos-registry contributing guide](https://github.com/rivet-dev/agent-os/blob/main/registry/CONTRIBUTING.md) for how to add new software packages to the registry.","src/content/docs/docs/software.mdx","958f56c5b8ae9cb3","docs/resource-limits",{"id":309,"data":311,"body":317,"filePath":318,"digest":319,"deferredRender":16},{"title":312,"description":313,"editUrl":16,"head":314,"template":18,"sidebar":315,"pagefind":16,"draft":20},"Resource Limits","Cap per-VM processes, file descriptors, sockets, and filesystem bytes so guest code can never exhaust the host.",[],{"hidden":20,"attrs":316},{},"Every agentOS VM runs with **per-VM resource caps**. Runaway or malicious guest code can exhaust its own VM, but it can never starve the host or any sibling VM.\n\n- **Bounded by default**: each VM ships with conservative caps. Unset fields fall back to built-in defaults that match the runtime's historical constants.\n- **Per-VM**: every VM gets its own budget. Limits are not shared across VMs.\n- **Enforced by the kernel**: a guest that exceeds a cap fails inside the VM (out-of-memory, `EMFILE`, `EAGAIN`, etc.). The host is never affected.\n- **Operator-raisable**: the operator (the trusted process that creates the VM) may raise any cap for trusted workloads. Guest code can never raise its own caps.\n\n## Setting limits\n\nSet caps on the `limits` object in the `agentOS` config. Limits are grouped by subsystem (`resources` and more). Omitted limits keep their secure default.\n\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n\tsoftware: [pi],\n\tlimits: {\n\t\tresources: {\n\t\t\tmaxProcesses: 64, // concurrent processes\n\t\t\tmaxOpenFds: 256, // open file descriptors\n\t\t\tmaxSockets: 128, // open sockets\n\t\t\tmaxFilesystemBytes: 256 * 1024 * 1024, // VFS storage budget\n\t\t\tmaxWasmStackBytes: 4 * 1024 * 1024, // WASM call-stack ceiling\n\t\t},\n\t},\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/resource-limits)*\n\n## Available caps\n\n| Limit | Controls | Notes |\n|---|---|---|\n| `resources.maxProcesses` | Concurrent processes in the VM process table | Caps fork bombs and runaway spawning. New spawns fail with `EAGAIN`. |\n| `resources.maxOpenFds` | Open file descriptors | Exhausting the table fails with `EMFILE` / `ENFILE`. |\n| `resources.maxSockets` | Open sockets in the socket table | Bounds concurrent connections; excess `connect`/`accept` fail. |\n| `resources.maxFilesystemBytes` | Total bytes stored in the virtual filesystem | Bounds VFS storage; writes past the budget fail with a no-space error. |\n| `resources.maxWasmStackBytes` | Maximum WASM call-stack size, in bytes | Deep recursion fails with a stack overflow instead of crashing the VM. |\n\n## Behavior at the limit\n\n- **WASM stack**: deep recursion throws a stack-overflow error in the guest, never a host crash.\n- **Filesystem bytes**: writing past the VFS budget fails with a no-space error to the guest.\n- **Counts (fds / processes / sockets)**: hitting a table cap returns the standard POSIX errno (`EMFILE`, `EAGAIN`, etc.), exactly as a real Linux kernel would under `ulimit`.","src/content/docs/docs/resource-limits.mdx","d88308a782482953","docs/system-prompt",{"id":320,"data":322,"body":328,"filePath":329,"digest":330,"deferredRender":16},{"title":323,"description":324,"editUrl":16,"head":325,"template":18,"sidebar":326,"pagefind":16,"draft":20},"System Prompt","How agentOS injects context into agent sessions.",[],{"hidden":20,"attrs":327},{},"agentOS automatically injects a system prompt into every agent session that describes the VM environment and available tools. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.).\n\nThe base prompt is embedded in the sidecar (not written to a file inside the VM). At session start the sidecar assembles the base prompt with your additional instructions and generated tool docs, then injects the result into the agent adapter's launch arguments (for example, `--append-system-prompt` for Pi).\n\n## Customization\n\n- `additionalInstructions` appends extra instructions to the agent's system prompt. They are added after the base OS prompt and the generated tool docs, so they layer on top of (rather than replace) the agent's own instructions.\n- `skipOsInstructions` suppresses the base OS prompt while still injecting the generated tool docs.\n\n```ts\nconst session = await vm.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n // Extra instructions appended to the agent system prompt\n additionalInstructions: \"Always write tests before implementation.\",\n // Suppress the base OS prompt (tool docs are still injected)\n skipOsInstructions: true,\n});\n```\n\n`additionalInstructions` can also be set globally in `agentOs({ options: { additionalInstructions } })` so it applies to every session.","src/content/docs/docs/system-prompt.mdx","e9ae34c14ca71ac2","docs/versus-sandbox",{"id":331,"data":333,"body":339,"filePath":340,"digest":341,"deferredRender":16},{"title":334,"description":335,"editUrl":16,"head":336,"template":18,"sidebar":337,"pagefind":16,"draft":20},"agentOS vs Sandbox","When to use the lightweight agentOS VM, a full sandbox, or both together.",[],{"hidden":20,"attrs":338},{},"- **agentOS** is a lightweight VM that runs inside your process. Near-zero cold start, low memory, direct backend integration via [bindings](/docs/bindings).\n- **Sandboxes** are full Linux environments with root access, system packages, and native binary support.\n- **You can use both.** agentOS works with sandboxes through [sandbox mounting](/docs/sandbox). Agents run in the lightweight VM by default and spin up a full sandbox on demand.\n\n## Comparison\n\n| | agentOS VM | Full Sandbox |\n|---|---|---|\n| **Cost** | Very low. Runs in your process. | Pay per second of uptime. |\n| **Startup** | Near-zero cold start (~6 ms). | Seconds to spin up. |\n| **Backend integration** | Direct. [Bindings](/docs/bindings) call your functions with zero latency. | Indirect. Requires network calls back to your backend. |\n| **API keys** | Stay on the server via the [LLM gateway](/docs/llm-gateway). | Must be injected into the sandbox environment. |\n| **Permissions** | Granular, deny-by-default. | Coarse-grained (container-level). |\n| **Infrastructure** | `npm install` | Vendor account + API keys. |\n| **Best for** | Coding, file manipulation, scripting, API calls, orchestration. | Browsers, desktop automation, native compilation, dev servers. |\n\n## When to use each\n\n### agentOS VM\n\nUse the lightweight VM for most agent workloads:\n\n- Coding and file editing\n- Running scripts and CLI tools\n- Calling APIs and services via bindings\n- Multi-agent orchestration and workflows\n- Tasks where backend integration matters (permissions, tool access, LLM routing)\n\n### Full sandbox\n\nSpin up a sandbox when the workload needs a real Linux kernel:\n\n- Browsers and desktop automation (Playwright, Puppeteer, Selenium)\n- Heavy compilation and native toolchains\n- Dev servers with hot reload, databases, and system ports\n- GUI applications and VNC sessions\n\n### Both together\n\nUse agentOS with [sandbox mounting](/docs/sandbox) for workflows that need both:\n\n- Agent runs in the agentOS VM with full access to bindings and permissions\n- Sandbox spins up on demand for heavy tasks\n- Sandbox filesystem is mounted into the VM as a native directory\n- Agent reads and writes sandbox files the same way it reads local files","src/content/docs/docs/versus-sandbox.mdx","0ffcdc2908107d12","docs/webhooks",{"id":342,"data":344,"body":350,"filePath":351,"digest":352,"deferredRender":16},{"title":345,"description":346,"editUrl":16,"head":347,"template":18,"sidebar":348,"pagefind":16,"draft":20},"Webhooks","Trigger agent workflows from external webhooks using Hono and queues.",[],{"hidden":20,"attrs":349},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nUse a lightweight HTTP server to receive webhooks and drive an agent. This example uses [Hono](https://hono.dev) to receive Slack webhooks and call an agent directly.\n\n## Example: Slack webhook to agent\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport { Hono } from \"hono\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOS({\n software: [pi],\n additionalInstructions: \"You answer Slack messages concisely.\",\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n\n// Hono server to receive Slack webhooks\nconst app = new Hono();\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\n\napp.post(\"/slack/events\", async (c) => {\n const body = await c.req.json();\n\n // Handle Slack URL verification\n if (body.type === \"url_verification\") {\n return c.json({ challenge: body.challenge });\n }\n\n // Call the agent directly for a user message\n if (body.event?.type === \"message\" && !body.event?.bot_id) {\n const { channel, text, user } = body.event;\n const agent = client.vm.getOrCreate(\"slack-agent\");\n\n const session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n const result = await agent.sendPrompt(\n session.sessionId,\n `Slack message from ${user} in #${channel}:\\n\\n${text}\\n\\nRespond helpfully.`,\n );\n await agent.closeSession(session.sessionId);\n\n // Post the response back to Slack\n await fetch(\"https://slack.com/api/chat.postMessage\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,\n },\n body: JSON.stringify({ channel, text: result.text }),\n });\n }\n\n return c.json({ ok: true });\n});\n\nexport default app;\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/webhooks)*\n\n## How it works\n\n1. Slack sends an HTTP POST to `/slack/events`\n2. The Hono handler validates the event and pushes it to the actor's queue\n3. The queue processes messages one at a time, creating agent sessions for each\n4. The agent responds and the worker posts the reply back to Slack\n\nThe queue provides backpressure and durability. If the agent is busy, messages wait in the queue. If the server restarts, queued messages are replayed.\n\n## Recommendations\n\n- Return `200` from the webhook handler immediately after queuing. External services like Slack have short timeout windows.\n- Store webhook secrets in environment variables, not in code.","src/content/docs/docs/webhooks.mdx","ac38e9dc5d31808e","docs/workflows",{"id":353,"data":355,"body":361,"filePath":362,"digest":363,"deferredRender":16},{"title":356,"description":357,"editUrl":16,"head":358,"template":18,"sidebar":359,"pagefind":16,"draft":20},"Workflow Automation","Orchestrate multi-step agent tasks with durable workflows.",[],{"hidden":20,"attrs":360},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\nOrchestrate multi-step agent tasks with durable workflows that survive crashes and restarts. Build them with RivetKit's `workflow()` run handler, where each `ctx.step()` is recorded, retried, and resumed independently, and the output of one step can feed into the next.\n\n## Basic workflow\n\nA workflow is the durable `run` handler of an actor. Wrap it in `workflow()` and drive a multi-step agent task as an ordered series of steps: clone the repo, let an agent fix the bug, then run the tests. Trigger work by sending to a queue; the workflow loops and waits durably for the next message.\n\nSession creation and prompting happen within the step that uses them, so a session never has to outlive the work it backs (sessions are ephemeral and would not survive a replay). Steps reach the Agent OS VM, a separate actor, through `ctx.client()`.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOS, setup } from \"@rivet-dev/agentos\";\nimport { actor, queue } from \"rivetkit\";\nimport {\n type WorkflowLoopContextOf,\n workflow,\n} from \"rivetkit/workflow\";\nimport pi from \"./software/pi\";\n\n// The Agent OS VM that each workflow step drives. It is its own actor, kept\n// separate from the workflow orchestrator so steps can reach it over the client.\nconst vm = agentOS({ software: [pi] });\n\n// A durable workflow actor. Its `run` handler is built with `workflow()`, so\n// every `ctx.step(...)` is recorded, retried, and resumed independently: if the\n// process crashes mid-run, replay skips completed steps and continues where it\n// left off. Trigger work by sending to the `fixBug` queue; the workflow loops,\n// waiting durably for the next message.\nconst bugFixer = actor({\n state: {\n lastIssue: null as string | null,\n lastExitCode: null as number | null,\n },\n queues: {\n fixBug: queue\u003C{ repo: string; issue: string }>(),\n },\n run: workflow(async (ctx) => {\n await ctx.loop(\"fix-bug-loop\", async (loopCtx) => {\n // Wait durably for the next bug-fix request.\n const message = await loopCtx.queue.next(\"wait-fix-bug\");\n const { repo, issue } = message.body;\n\n // Step 1: Clone the repo. Each step is an isolated, retryable unit of\n // work; a crash here resumes from this step on replay.\n await loopCtx.step(\"clone-repo\", () => cloneRepo(loopCtx, repo));\n\n // Step 2: An agent fixes the bug. The session is created and closed\n // inside the step, so it never has to outlive the work it backs (sessions\n // are ephemeral and would not survive a replay).\n await loopCtx.step(\"fix-bug\", () => fixBugWithAgent(loopCtx, issue));\n\n // Step 3: Run the tests. The exit code feeds into the next step.\n const exitCode = await loopCtx.step(\"run-tests\", () => runTests(loopCtx));\n\n // State changes are only valid inside a step callback, so they are\n // recorded as part of replay.\n await loopCtx.step(\"record-result\", async () => {\n loopCtx.state.lastIssue = issue;\n loopCtx.state.lastExitCode = exitCode;\n });\n });\n }),\n actions: {\n getState: (c) => c.state,\n },\n});\n\nasync function cloneRepo(\n ctx: WorkflowLoopContextOf\u003Ctypeof bugFixer>,\n repo: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n await agentHandle.exec(`git clone ${repo} /home/agentos/repo`);\n}\n\nasync function fixBugWithAgent(\n ctx: WorkflowLoopContextOf\u003Ctypeof bugFixer>,\n issue: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n const session = await agentHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agentHandle.sendPrompt(\n session.sessionId,\n `Fix the bug described in issue: ${issue}`,\n );\n await agentHandle.closeSession(session.sessionId);\n}\n\nasync function runTests(\n ctx: WorkflowLoopContextOf\u003Ctypeof bugFixer>,\n): Promise\u003Cnumber> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"bug-fixer\");\n const tests = await agentHandle.exec(\"cd /home/agentos/repo && npm test\");\n return tests.exitCode;\n}\n\nexport const registry = setup({ use: { vm, bugFixer, codeReviewer } });\n\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.bugFixer.getOrCreate(\"main\");\n\n// Trigger the durable workflow by sending to its queue. The workflow runs each\n// step in order against the VM, surviving restarts: the output of one step (the\n// cloned repo, the agent's edits) feeds into the next.\nawait handle.send(\"fixBug\", {\n repo: \"https://github.com/example/repo.git\",\n issue: \"Fix the login redirect bug\",\n});\n\nconst state = await handle.getState();\nconsole.log(\"Last issue:\", state.lastIssue, \"exit code:\", state.lastExitCode);\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/workflows)*\n\n## Agent chaining\n\nOutput of one agent session feeds into the next. Each session is created and completed within its own step, and data passes between steps through the VM filesystem (a review file) and step return values.\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\n// Agent chaining: the output of one agent session feeds into the next. Each\n// session is created and completed within its own step, and data passes between\n// steps through the VM filesystem (a review file) and step return values.\nconst codeReviewer = actor({\n state: {\n reviewedFiles: 0,\n },\n queues: {\n codeReview: queue\u003C{ filePath: string }>(),\n },\n run: workflow(async (ctx) => {\n await ctx.loop(\"code-review-loop\", async (loopCtx) => {\n const message = await loopCtx.queue.next(\"wait-code-review\");\n const { filePath } = message.body;\n\n // Step 1: An agent reviews the code and writes findings to a file.\n await loopCtx.step(\"review\", () => reviewCode(loopCtx, filePath));\n\n // Step 2: Read the review back from the VM filesystem. Its text is the\n // step return value, so it flows into the next step.\n const review = await loopCtx.step(\"read-review\", () => readReview(loopCtx));\n\n // Step 3: A second session applies fixes based on the review.\n await loopCtx.step(\"fix\", () => applyReview(loopCtx, review));\n\n await loopCtx.step(\"record-review\", async () => {\n loopCtx.state.reviewedFiles += 1;\n });\n });\n }),\n actions: {\n getState: (c) => c.state,\n },\n});\n\nasync function reviewCode(\n ctx: WorkflowLoopContextOf\u003Ctypeof codeReviewer>,\n filePath: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"reviewer\");\n const session = await agentHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agentHandle.sendPrompt(\n session.sessionId,\n `Review the code at ${filePath} and write your findings to /home/agentos/review.md`,\n );\n await agentHandle.closeSession(session.sessionId);\n}\n\nasync function readReview(\n ctx: WorkflowLoopContextOf\u003Ctypeof codeReviewer>,\n): Promise\u003Cstring> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"reviewer\");\n const content = await agentHandle.readFile(\"/home/agentos/review.md\");\n return new TextDecoder().decode(content);\n}\n\nasync function applyReview(\n ctx: WorkflowLoopContextOf\u003Ctypeof codeReviewer>,\n review: string,\n): Promise\u003Cvoid> {\n const agentHandle = ctx.client\u003Ctypeof registry>().vm.getOrCreate(\"reviewer\");\n const session = await agentHandle.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n });\n await agentHandle.sendPrompt(\n session.sessionId,\n `Apply the following review feedback:\\n\\n${review}`,\n );\n await agentHandle.closeSession(session.sessionId);\n}\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"@rivet-dev/agentos/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>({ endpoint: \"http://localhost:6420\" });\nconst handle = client.codeReviewer.getOrCreate(\"main\");\n\n// Run the chained review + fix workflow against a file in the VM.\nawait handle.send(\"codeReview\", { filePath: \"/home/agentos/src/auth.ts\" });\n```\n\u003C/CodeGroup>\n\n*[See Full Example](https://github.com/rivet-dev/agent-os/tree/main/examples/docs/workflows)*\n\n## Recommendations\n\n- Build the actor's `run` handler with `workflow()` so each `ctx.step()` is durable: recorded, retried, and resumed independently across crashes and restarts.\n- Keep step names stable across code changes. Renaming a step breaks replay for in-progress workflows.\n- Create and close sessions within the step that uses them. Sessions are ephemeral, so keep their lifetime scoped to one unit of work.\n- Pass data between steps via the filesystem or step return values, not session state.\n- Keep `state` changes and other actor-local side effects inside `ctx.step()` callbacks; use non-step workflow code (queue waits, loops, sleeps) only for orchestration.\n- Reach the Agent OS VM, a separate actor, from inside a step with `ctx.client()`.\n- See [Workflows](/docs/actors/workflows) for the full workflow API reference including timers, joins, and races.","src/content/docs/docs/workflows.mdx","5061f90b5c158a2a","docs/agents/amp",{"id":364,"data":366,"body":372,"filePath":373,"digest":374,"deferredRender":16},{"title":367,"description":368,"editUrl":16,"head":369,"template":18,"sidebar":370,"pagefind":16,"draft":20},"Amp","Run the Amp coding agent inside a VM.",[],{"hidden":20,"attrs":371},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nAmp agent documentation is coming soon.\n\u003C/Aside>","src/content/docs/docs/agents/amp.mdx","e2bfc31430b94ed9","docs/agents/claude",{"id":375,"data":377,"body":383,"filePath":384,"digest":385,"deferredRender":16},{"title":378,"description":379,"editUrl":16,"head":380,"template":18,"sidebar":381,"pagefind":16,"draft":20},"Claude","Run the Claude coding agent inside a VM.",[],{"hidden":20,"attrs":382},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nClaude agent documentation is coming soon.\n\u003C/Aside>\n\n```ts\nimport claude from \"@agentos-software/claude\";\n\nconst vm = await AgentOs.create({ software: [claude] });\nconst { sessionId } = await vm.createSession(\"claude\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },\n});\n```","src/content/docs/docs/agents/claude.mdx","86c453cf7c31ea0d","docs/agents/codex",{"id":386,"data":388,"body":394,"filePath":395,"digest":396,"deferredRender":16},{"title":389,"description":390,"editUrl":16,"head":391,"template":18,"sidebar":392,"pagefind":16,"draft":20},"Codex","Run the Codex coding agent inside a VM.",[],{"hidden":20,"attrs":393},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nCodex agent documentation is coming soon.\n\u003C/Aside>\n\n```ts\nimport codex from \"@agentos-software/codex\";\n\nconst vm = await AgentOs.create({ software: [codex] });\nconst { sessionId } = await vm.createSession(\"codex\", {\n env: { OPENAI_API_KEY: process.env.OPENAI_API_KEY },\n});\n```","src/content/docs/docs/agents/codex.mdx","4526686fc19399c5","docs/agents/opencode",{"id":397,"data":399,"body":405,"filePath":406,"digest":407,"deferredRender":16},{"title":400,"description":401,"editUrl":16,"head":402,"template":18,"sidebar":403,"pagefind":16,"draft":20},"OpenCode","Run the OpenCode coding agent inside a VM.",[],{"hidden":20,"attrs":404},{},"import { Aside } from '@astrojs/starlight/components';\n\n\u003CAside type=\"note\">\nOpenCode agent documentation is coming soon.\n\u003C/Aside>\n\n```ts\nimport opencode from \"@agentos-software/opencode\";\n\nconst vm = await AgentOs.create({ software: [opencode] });\nconst { sessionId } = await vm.createSession(\"opencode\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },\n});\n```","src/content/docs/docs/agents/opencode.mdx","7c675c06ccdc87a5","docs/agents/pi",{"id":408,"data":410,"body":416,"filePath":417,"digest":418,"deferredRender":16},{"title":411,"description":412,"editUrl":16,"head":413,"template":18,"sidebar":414,"pagefind":16,"draft":20},"Pi","Run the Pi coding agent inside a VM with extensions and custom configuration.",[],{"hidden":20,"attrs":415},{},"import CodeGroup from '@rivet-dev/docs-theme/components/CodeGroup.astro';\n\n## Quick start\n\n\u003CCodeGroup>\n```ts title=\"server.ts\"\nimport { agentOs } from \"rivetkit/agent-os\";\nimport { setup } from \"rivetkit\";\nimport pi from \"@agentos-software/pi\";\n\nconst vm = agentOs({\n options: { software: [pi] },\n});\n\nexport const registry = setup({ use: { vm } });\nregistry.start();\n```\n\n```ts title=\"client.ts\"\nimport { createClient } from \"rivetkit/client\";\nimport type { registry } from \"./server\";\n\nconst client = createClient\u003Ctypeof registry>(\"http://localhost:6420\");\nconst agent = client.vm.getOrCreate([\"my-agent\"]);\n\nconst session = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },\n});\n\nconst { text } = await agent.sendPrompt(\n session.sessionId,\n \"What files are in the current directory?\",\n);\nconsole.log(text);\n```\n\u003C/CodeGroup>\n\nRead [Sessions](/docs/sessions) first for session options, streaming events, prompts, and lifecycle management.\n\n## Extensions\n\nPi supports [extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) that let you register custom tools, modify the system prompt, and hook into agent lifecycle events. Write a `.js` file into the VM's extensions directory before creating a session and Pi discovers it automatically.\n\nPi scans two directories for `.js` extension files:\n\n| Directory | Scope |\n|-----------|-------|\n| `~/.pi/agent/extensions/` | Global — applies to all Pi sessions |\n| `\u003Ccwd>/.pi/extensions/` | Project — applies only when cwd matches |\n\n```ts\nconst extensionCode = `\nexport default function(pi) {\n // Modify the system prompt before each agent turn\n pi.on(\"before_agent_start\", async (event) => {\n return {\n systemPrompt: event.systemPrompt +\n \"\\\\n\\\\nAlways respond in formal English.\"\n };\n });\n}\n`;\n\n// Write the extension before creating the session\nawait agent.mkdir(\"/home/agentos/.pi/agent/extensions\", { recursive: true });\nawait agent.writeFile(\"/home/agentos/.pi/agent/extensions/formal.js\", extensionCode);\n\n// Pi discovers the extension automatically\nconst { sessionId } = await agent.createSession(\"pi\", {\n env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },\n});\n```\n\nSee the [Pi extension documentation](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions) for the full extension API.","src/content/docs/docs/agents/pi.mdx","25154b7a014f2dd5","docs/architecture/agent-sessions",{"id":419,"data":421,"body":427,"filePath":428,"digest":429,"deferredRender":16},{"title":422,"description":423,"editUrl":16,"head":424,"template":18,"sidebar":425,"pagefind":16,"draft":20},"Agent Sessions","Internals of agent sessions: how a session is created and bound to a VM, how prompts and events flow from client to sidecar to agent adapter and back, the session lifecycle, and where session state lives.",[],{"hidden":20,"attrs":426},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis page is an internals deep-dive on how agent sessions work under the hood. For the usage API (creating sessions, sending prompts, streaming responses, replaying events), see [Sessions](/docs/sessions).\n\nA session is a long-lived conversation with an agent (such as [Pi](https://github.com/mariozechner/pi-coding-agent)) running inside a VM. Where a bare `exec()` / `run()` starts a fresh guest process and returns when it exits, a session keeps an agent process alive across many prompts, streams its output back as events, and persists a transcript that survives sleep/wake cycles. Everything below describes the machinery that makes that possible while keeping the agent inside the same isolation boundary as any other guest.\n\n## Where a session sits in the component model\n\nThe [three components](/docs/architecture#three-components) are unchanged for sessions: a trusted **client**, the trusted **sidecar** that owns the kernel, and the untrusted **executor** that runs guest code. An agent session adds one more layer on the guest side of the boundary:\n\n- **Client.** Calls `createSession`, `sendPrompt`, and the rest of the session API. It never runs the agent itself; it drives the session over the wire protocol.\n- **Sidecar / kernel.** Spawns the agent as a kernel-managed process inside the VM, owns the session's I/O, applies the permission policy on every syscall the agent makes, and persists the transcript.\n- **Agent adapter.** A per-agent-type shim, inside the VM, that translates between the session protocol and the specific agent's native interface. It normalizes the agent's output into the [Agent Communication Protocol (ACP)](/docs/sessions) so every agent type produces the same event shape.\n- **Executor.** The agent process itself plus any tools it spawns. It is untrusted guest code like any other: its file reads, child processes, and network calls all flow through the kernel.\n\nThe key consequence: an agent is not privileged. It is a guest process that happens to be long-lived and conversational. Its capabilities are exactly the VM's [permission policy](/docs/permissions), nothing more.\n\n## Creating a session and binding it to a VM\n\nA session is always created against an existing VM. The client resolves a VM handle (for example `client.vm.getOrCreate([...])`) and calls `createSession(agentType, options)` on it. Under the hood:\n\n1. **The request crosses the wire** (client to sidecar) carrying the agent type and the session options: `env`, `cwd`, `mcpServers`, `additionalInstructions`, and `skipOsInstructions`.\n2. **The kernel boots the VM** if it is not already running, with its bootstrapped virtual filesystem.\n3. **The sidecar selects the agent adapter** for the requested type and spawns the agent as a kernel-managed process inside that VM. Because the VM does not inherit the host `process.env`, the agent only sees the `env` passed in the options (this is why API keys must be supplied explicitly). The process starts in `cwd` (default `/home/agentos`).\n4. **The session is registered** in the VM with a `sessionId`, and the adapter performs its handshake with the agent to discover `capabilities` and `agentInfo`.\n5. **The handle returns** that metadata to the client.\n\nThe session is bound to that VM for its lifetime. The agent process, its working directory, and its persisted transcript all live inside the VM's isolation domain. A VM can host several sessions at once: each gets its own agent process, but they share the one VM's filesystem (see [Multiple sessions](/docs/sessions#multiple-sessions)). Two sessions in two different VMs share nothing, exactly as described in the [isolation model](/docs/architecture#isolation-model).\n\n\u003CAside type=\"note\">MCP servers configured on a session follow the same boundary. A `local` MCP server runs as a child process inside the VM (kernel-managed, gated by the permission policy); a `remote` MCP server is reached over the network, so its traffic flows through the kernel socket table and is subject to the network allowlist.\u003C/Aside>\n\n## How a prompt flows: client to agent and back\n\nSending a prompt is a request in one direction with a stream of events flowing back in the other. The lifecycle of a single prompt extends the general [lifecycle of a request](/docs/architecture#lifecycle-of-a-request):\n\n1. **The client calls `sendPrompt(sessionId, text)`.** The request crosses the wire to the sidecar (hop one).\n2. **The sidecar routes it to the session's agent adapter,** which translates the prompt into the agent's native input and writes it to the running agent process.\n3. **The agent works the turn.** As it thinks, calls tools, edits files, and produces output, every action it takes is a guest syscall back into the kernel (hop two): file reads/writes hit the VFS, tool subprocesses are kernel-managed, and network calls go through the socket table under the allowlist.\n4. **The adapter normalizes the agent's output into ACP events.** Each event is assigned a monotonically increasing sequence number, appended to the session's event log, and persisted.\n5. **Events stream back to the client** as `sessionEvent` notifications, carrying the `sessionId` and the ACP `event` (its `method` and `params`). This is why the docs recommend subscribing to `sessionEvent` before calling `sendPrompt`: events emitted early in the turn would otherwise be missed.\n6. **The turn resolves.** When the agent finishes the turn, `sendPrompt` resolves with the reply.\n\n`cancelPrompt(sessionId)` interrupts an in-progress turn: the request crosses to the sidecar, which signals the agent process to stop the current turn through the adapter, leaving the session itself alive for the next prompt.\n\n```\nclient sidecar / kernel agent adapter agent (executor)\n | sendPrompt | | |\n | --------------------> | route to session | |\n | | ---------------------------> | native input ---> |\n | | | | (thinks, calls\n | | \u003C--- syscalls (VFS, procs, sockets) ------------ | tools, edits)\n | | | \u003C-- native output |\n | sessionEvent (ACP) | persist + assign seq | |\n | \u003C------------------- | \u003C-------- ACP events ------- | |\n | ...stream... | | |\n | resolve(reply) | | |\n | \u003C------------------- | | |\n```\n\n## Session lifecycle\n\nA session moves through these states, all driven by client calls over the wire:\n\n- **Active.** Created and bound to a running VM, with a live agent process. Prompts can be sent and events stream back.\n- **Suspended.** When the VM sleeps, the agent process is torn down but the session's persisted transcript remains in storage. `resumeSession(sessionId)` reconnects: the kernel wakes the VM, re-spawns the agent, and rebinds the session so prompts can continue.\n- **Closed.** `closeSession(sessionId)` gracefully shuts down the agent process and releases its in-VM resources, but leaves the persisted transcript intact so history can still be queried.\n- **Destroyed.** `destroySession(sessionId)` removes the session and all of its persisted events. This is irreversible.\n\nBecause the transcript is persisted, closing or suspending a session is not the same as losing it: the event history can be read back later, even when the VM is not running.\n\nRuntime configuration (`setModel`, `setMode`, `setThoughtLevel`) mutates an active session in place by sending the change through the adapter to the live agent, without restarting the session.\n\n## Resuming a suspended session\n\nWhen a VM sleeps the agent process is destroyed, but the session registry and the transcript survive in SQLite. `resumeSession(sessionId)` (or simply prompting a suspended session) rebinds the stable, client-facing `sessionId` to a freshly spawned agent. Resume is **lazy** (it runs on the first prompt to a non-live session) and **capability-driven** (the orchestrator never special-cases an agent by name, only by what it advertises). There are two paths:\n\n- **Native ACP resume (optimization).** If the agent advertises ACP `loadSession`/`resume` and its own store survived on the durable root, the sidecar issues `session/load` and the agent restores its full context itself. The `sessionId` is unchanged.\n- **Universal transcript fallback.** If the agent has no native resume, or its store did not survive, the sidecar reconstructs a Markdown transcript from the recorded events, writes it into the VM (for example `/root/.agentos/threads/\u003CsessionId>.md`), starts a fresh agent, and prefixes the next prompt with a pointer to that file. Because this needs only file-read tools it works for any agent with no per-agent code, at the cost of pointing the agent at the transcript rather than pre-loading it into context.\n\nResume depends on a **durable root filesystem**. The RivetKit actor configures one automatically (its SQLite-backed root), so transcript capture and resume work out of the box. A direct `AgentOs` SDK user on the default in-memory root has no durable store: transcript capture is a no-op and context cannot be restored, so configure a durable root explicitly if you need resume outside the actor.\n\n## Where session state lives\n\nSession state spans two tiers:\n\n- **In-memory, while the VM runs.** The running agent process holds the live conversation, and the sidecar keeps the session's recent event log with sequence numbers, the basis for live reconnection: a client tracks the last sequence number it processed and asks for everything after it, so no events are dropped or duplicated across a reconnect.\n- **Persisted in SQLite, independent of the VM.** Every ACP event is written to a SQLite-backed transcript store inside the VM, keyed by `sessionId` and sequence number. This tier survives sleep/wake and VM shutdown. `listPersistedSessions()` and `getSessionEvents(sessionId)` read from it and work even when the VM is not running, which is what makes transcript-history UIs possible without keeping a VM warm.\n\nSee [Replay events](/docs/sessions#replay-events) for replaying a session's persisted events.\n\nThe transcript living inside the VM keeps session state on the same side of the boundary as the agent that produced it: it is part of the VM's isolation domain, not the client's. The client only ever sees it by asking the sidecar for it over the wire.\n\n## Where to go next\n\n- [Sessions](/docs/sessions): the usage API for creating sessions, sending prompts, and replaying events.\n- [Architecture](/docs/architecture): the component model, request lifecycle, and isolation model that sessions build on.\n- [Permissions](/docs/permissions): the policy the kernel enforces on every syscall an agent makes.\n- [Replay events](/docs/sessions#replay-events): in-memory versus persisted event replay.","src/content/docs/docs/architecture/agent-sessions.mdx","ef7d4437f03b4f4d","docs/architecture/compiler-toolchain",{"id":430,"data":432,"body":438,"filePath":439,"digest":440,"deferredRender":16},{"title":433,"description":434,"editUrl":16,"head":435,"template":18,"sidebar":436,"pagefind":16,"draft":20},"Compiler Toolchain","How agentOS compiles its command suite to WebAssembly: Rust coreutils via cargo and C programs via wasi-sdk, linked against a patched wasi-libc plus the wasi-ext bindings, and how the resulting .wasm files become the guest's commands.",[],{"hidden":20,"attrs":437},{},"import { Aside } from '@astrojs/starlight/components';\n\nThe commands a guest runs through [process execution](/docs/processes), the shell\n(`sh`) and the coreutils behind it, are not native host binaries. They are\nWebAssembly modules compiled ahead of time and mounted into the VM. This page\ncovers how that command suite is produced: which toolchains compile it, what it\nlinks against, and how the resulting `.wasm` files become the guest's commands.\n\nFor *why* WASM is a first-class guest and *how* it presents a POSIX surface at\nruntime, see the [WASM VM](/docs/architecture/posix-syscalls) page. This page is the build-side\ncounterpart: it documents the toolchain that emits binaries carrying both\n[the host-import layer and the WASI shim](/docs/architecture/posix-syscalls).\n\n## Target: `wasm32-wasip1`\n\nEverything in the command suite is compiled to a single target,\n`wasm32-wasip1`: the WASI preview 1 ABI on the 32-bit WebAssembly architecture.\nPicking one target for the whole suite means a single libc, a single set of\nhost import declarations, and a single runtime shim can serve every command.\n\nA guest module built for this target expects standard WASI (preopened file\ndescriptors, clocks, randomness, file I/O) plus the extra agentOS import modules\ndescribed below. Both halves are satisfied at runtime by the kernel-backed\nruntime; nothing in a compiled command reaches a real host syscall.\n\n## Two source languages, two compilers\n\nThe suite is heterogeneous: most tools are Rust, some are C. Each language uses\nits own compiler driver, but both emit the same `wasm32-wasip1` ABI and link\nagainst the same sysroot, so the outputs are interchangeable at runtime.\n\n- **Rust coreutils** are built with **`cargo`** targeting `wasm32-wasip1`. Rust's\n standard library already has first-class support for this target, so the\n coreutils crates compile with an ordinary cross-compile invocation.\n- **C programs** are built with the **`wasi-sdk`** toolchain, a packaged\n `clang` plus sysroot tuned for WASI. C tools that have no Rust equivalent (or\n that are easier to carry as upstream C) go through this path.\n\n```bash\n# Rust coreutils\ncargo build --target wasm32-wasip1 --release\n\n# C programs via wasi-sdk, linked against the patched libc + wasi-ext\n$WASI_SDK/bin/clang --target=wasm32-wasip1 \\\n --sysroot=$WASI_SYSROOT \\\n -lwasi-ext \\\n tool.c -o tool.wasm\n```\n\n## What every binary links against\n\nRegardless of source language, each command links against the same two pieces.\nTogether they give a single binary both the standard WASI calls and the agentOS\nprocess / user / network extensions.\n\n- **A patched `wasi-libc`.** The libc is the WASI standard library, modified so\n that the calls a normal command-line program performs resolve against the\n agentOS surface instead of failing or hitting unimplemented stubs. This is the\n same patched libc the [Layer 2 shim](/docs/architecture/posix-syscalls) adapts at runtime; the\n build side and the runtime side are two ends of the same contract.\n- **The `wasi-ext` bindings.** These declare the extra WebAssembly import\n modules (`host_process`, `host_user`, `host_net`, and the small\n `host_sleep_ms` binding) that base WASI cannot express. Linking `wasi-ext`\n into a binary is what lets its libc emit `fork` / `exec`, `getuid` / `getgid`,\n and `connect` / `listen` as ordinary-looking syscalls that the host runtime\n then services through the kernel. See\n [Layer 1: custom host import modules](/docs/architecture/posix-syscalls) for the runtime half.\n\n\u003CAside type=\"note\">\nThe import declarations are compile-time only: linking `wasi-ext` tells the\nmodule *which* host imports to reference, but the calls are still routed through\nthe kernel and gated by the VM's [permission policy](/docs/permissions) at\nruntime. Building against `host_net` does not grant network access.\n\u003C/Aside>\n\n## From `.wasm` to a guest command\n\nThe compiler toolchain's product is a set of `.wasm` files, one per command.\nThose files are what the runtime mounts as the guest's executables: when a guest\ninvokes `ls`, `sh`, or any other bundled tool, the kernel resolves the name to\nthe corresponding module, instantiates it with the host imports and the WASI\nshim wired in, and runs it as a [child process](/docs/processes) with real\nprocess, user, and network semantics, all virtualized.\n\nThe same path is open to your own programs. A program you compile for\n`wasm32-wasip1` runs as a guest command exactly like the bundled ones; link the\n`wasi-ext` bindings if it needs processes, users, or sockets, and leave them out\nfor a pure-compute tool. Heavy native binaries that are not yet available as\nWASM belong in a [mounted sandbox](/docs/sandbox) instead.\n\n## Recommendations\n\n- Use the bundled WASM coreutils and `sh` for normal shell workloads; they\n already carry the patched libc and the `wasi-ext` extensions.\n- To ship your own command, compile it for `wasm32-wasip1` with `cargo` (Rust)\n or the `wasi-sdk` `clang` (C), and link `wasi-ext` only if it needs the\n process / user / network host imports.\n- Keep the build and runtime contracts aligned: the patched `wasi-libc` and the\n `wasi-ext` import declarations a binary is compiled against are the same ones\n the [WASM VM](/docs/architecture/posix-syscalls) runtime expects to satisfy.","src/content/docs/docs/architecture/compiler-toolchain.mdx","506cf678cbad8739","docs/architecture/filesystem",{"id":441,"data":443,"body":448,"filePath":449,"digest":450,"deferredRender":16},{"title":147,"description":444,"editUrl":16,"head":445,"template":18,"sidebar":446,"pagefind":16,"draft":20},"Internals of the kernel VFS: the overlay/mount/root engines, how guest fs syscalls are routed and confined, WASM preopens, and mount confinement against symlink and .. escapes.",[],{"hidden":20,"attrs":447},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis page is an internals deep-dive on the **kernel virtual filesystem (VFS)**: how it is layered, how a guest `fs` syscall is routed through it, and how guest I/O is confined to the VM. For the user-facing API (reading, writing, mounting, persistence), see [Filesystem](/docs/filesystem).\n\nThe invariant this whole subsystem exists to uphold: **every guest filesystem operation is serviced by the kernel-owned VFS, never by a real host capability.** There is no host disk reachable from the guest. The VFS presents normal Linux semantics to tools while keeping every byte inside the kernel.\n\n\u003CAside type=\"note\">The security boundary is sidecar to executor. The VFS lives inside the trusted sidecar; the guest in the executor only ever *asks* for a filesystem operation. Confinement is the kernel's job, not the guest's. See the [Security Model](/docs/security-model) for the full threat model.\u003C/Aside>\n\n## Where the VFS sits\n\nA guest `fs` call never touches the host. The path is always:\n\n```\nguest fs call (executor)\n -> kernel syscall (crosses sidecar \u003C-> executor boundary)\n -> VFS engine resolves the path\n -> backing store services the operation\n -> result returns to the executor\n```\n\n- The executor holds **no** filesystem capability of its own. It issues a syscall and blocks for the reply.\n- The kernel checks the applied permission policy for the filesystem scope before servicing the request.\n- The VFS resolves the path against the VM's layered engines, then services the operation against the engine that owns that path.\n\nBecause every byte is mediated here, two properties fall out for free: the guest can never reach the real host disk, and one VM's filesystem is never visible to another VM. Isolation is per-VM.\n\n## The VFS engines\n\nThe per-VM filesystem is not a single flat store. It is a tree of **engines**, each responsible for a subtree of the namespace. A path is resolved by walking from the root engine down to whichever engine owns the deepest matching prefix, then handing the remainder of the path to that engine.\n\n- **Root engine.** Owns `/` and the base namespace. Every VM boots with a root filesystem bootstrapped from a snapshot, so the guest starts against a populated POSIX tree (the default working directory is `/home/agentos`).\n- **Overlay engine.** Composes layers so writes land in a writable upper layer while reads fall through to a lower layer. This is how a read-mostly base can be presented as writable to the guest without mutating the shared lower layer.\n- **Mount engine.** Grafts a distinct backing store onto a guest path (a mount point). Below the mount point, operations are routed to that mount's backend instead of the parent engine. This is the mechanism behind in-memory, host-directory, S3, and Google Drive mounts.\n\nResolution is **longest-prefix wins**: if `/mnt/data` is a mount and the guest opens `/mnt/data/file`, the mount engine services it; anything outside `/mnt/data` stays with the parent (root/overlay) engine.\n\n```\n/ \u003C- root engine (bootstrapped from snapshot)\n|- home/user/... \u003C- root / overlay\n|- mnt/\n| |- scratch/... \u003C- mount engine -> in-memory backend\n| |- code/... \u003C- mount engine -> host-directory backend (read-only)\n| \\- data/... \u003C- mount engine -> S3 backend\n\\- ...\n```\n\nThe base layer is in-memory and per-VM; the runtime transparently persists it to backing storage so it survives sleep/wake. Mounts are pluggable: any guest path can be backed by the host, a remote, or a cloud store. See [Mounting filesystems](/docs/filesystem#mounting-filesystems) for the user-facing config.\n\n## Routing a guest syscall\n\nWhen the guest calls, say, `readFileSync(\"/mnt/data/report.csv\")`:\n\n1. **Permission check.** The kernel verifies the filesystem scope is granted for that operation. Nothing is bound by default; access is denied until opted in (see [Permissions](/docs/permissions)).\n2. **Engine resolution.** The VFS walks the namespace and selects the engine owning the longest matching prefix (`/mnt/data` -> the S3 mount engine).\n3. **Path normalization and confinement.** The remainder of the path is normalized within the owning engine's root. `.` and `..` segments are resolved *before* the operation reaches the backend, so the request cannot climb above the engine's root.\n4. **Backend operation.** The owning engine's backend services the read/write/stat/etc. against its store (in-memory pages, the persisted base, a host directory, S3, ...).\n5. **Reply.** The result crosses back to the executor, which unblocks.\n\nHost-side APIs (`agent.writeFile`, `agent.readFile`) enter the *same* VFS from the trusted side, which is why the host can seed and read files the guest sees, without ever exposing the real host disk to the guest.\n\n## Mount confinement\n\nA host-backed mount (host directory, S3, ...) comes from trusted config, so its existence, target, and credentials are not attack surface. What *is* in scope is the guest-driven traffic through it: the guest must not be able to use a mounted path to reach bytes outside the mount root. Confinement is enforced by the kernel, on every operation:\n\n- **`..` traversal.** Path segments are normalized relative to the mount root before the backend sees them. A guest path like `/mnt/code/../../etc/passwd` cannot resolve above the mount root; it is clamped to the mount's own subtree (and, above the mount point, handed back to the parent engine, which is itself the kernel VFS, not the host).\n- **Symlinks.** Symlink resolution (`realpath` following) is performed by the kernel against the *virtual* namespace, not the host's. A symlink inside a host-directory mount cannot be used to escape the mount root onto the wider host filesystem; the resolved target is re-confined to the mount root.\n- **Path aliasing / TOCTOU.** Because resolution and confinement happen inside the kernel on each operation, there is no window where the guest resolves a path and the backend later acts on a different one. The guest sees only the mounted subtree, never the wider host filesystem.\n\nMounts for host and remote backends are **read-only by default**; a writable mount must be opted into explicitly. The `readOnly` flag is enforced at the engine, so a write syscall to a read-only mount fails inside the kernel rather than reaching the backend.\n\n\u003CAside type=\"note\">\"Trusted mount, untrusted traffic\": the mount's target is trusted configuration, but the guest drives I/O through it, so confining guest operations to the mount root (`..`, symlink, TOCTOU, path-aliasing) is squarely in scope and enforced by the kernel.\u003C/Aside>\n\n## WASM preopens\n\nWASI does not grant a WASM guest an ambient filesystem. Instead, the host hands the module a set of **preopened directories**: capability handles to specific subtrees, and the guest can only reach paths reachable from a preopen.\n\nIn agentOS these preopens are wired to the **same kernel VFS** rather than to host directories:\n\n- A preopen maps a guest-visible path to a VFS subtree. File descriptors derived from it are serviced by the VFS engines above, with the same confinement rules.\n- The WASM guest therefore sees the virtualized filesystem (root snapshot, overlays, mounts) through standard WASI calls, with no host filesystem handle anywhere in the chain.\n- Confinement composes: a preopen rooted at a mount point inherits that mount's `..`/symlink confinement, because resolution still runs through the kernel VFS.\n\nThe result is that WASI filesystem access and the V8/Node `fs` path converge on one virtual filesystem, so both executor flavors get identical isolation and identical Linux semantics.\n\n## Where to go next\n\n- [Filesystem](/docs/filesystem): the user-facing API for reading, writing, mounting, and persistence.\n- [Architecture](/docs/architecture): the components, trust boundary, and kernel-owned syscall paths.\n- [Permissions](/docs/permissions): the filesystem scope the kernel checks on every operation.\n- [Security Model](/docs/security-model): the full trust model and threat boundary.","src/content/docs/docs/architecture/filesystem.mdx","75d7e3f62abf5032","docs/architecture/networking",{"id":451,"data":453,"body":459,"filePath":460,"digest":461,"deferredRender":16},{"title":454,"description":455,"editUrl":16,"head":456,"template":18,"sidebar":457,"pagefind":16,"draft":20},"Networking","How the kernel socket table works: a single VM-local transport that carries host, JavaScript, and WASM traffic, where fetch / net / dns route through it, how egress policy and loopback confinement are enforced, and how preview URLs are served.",[],{"hidden":20,"attrs":458},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis is the internals view of agentOS networking: the kernel socket table, the layers a request crosses, and where policy is enforced. For the user-facing API (`vmFetch`, preview URLs, the confinement model from a caller's perspective), see [Networking & Previews](/docs/networking). For the trust boundary this all sits inside, see [Architecture](/docs/architecture).\n\nThe governing rule is that there is exactly **one authoritative transport for everything VM-local**: the kernel socket table. No part of guest networking opens a real host socket on its own. Guest `fetch()`, `node:http`, `node:net`, WASM TCP clients and servers, and host-into-guest requests (`vmFetch` / `rt.fetch`) all target the same listener table.\n\n## The kernel socket table\n\nThe socket table is the floor of the stack and the only component that actually moves bytes between two in-VM endpoints. It is per VM, so two VMs never share a listener or a connection.\n\n- It exposes POSIX-style primitives: `socket_create`, `socket_bind_inet`, `socket_connect_inet_loopback`, `socket_read`, `socket_write`, `poll_targets`.\n- Every call is **owner-checked** (the calling process must own the descriptor) and **resource-accounted** against the VM's limits.\n- Failures return correct POSIX errnos (`ECONNREFUSED`, `EACCES`, …) so guest code branches the way it would on real Linux.\n- Connecting pairs two in-VM sockets and shuttles bytes between them. No host networking happens at this layer.\n\nBecause every server is a kernel TCP listener, a client never needs to know whether the server it is talking to is JS, WASM, raw TCP, or HTTP. HTTP is layered on top of kernel TCP bytes, so every listener lives in the one table and is reachable identically.\n\n\u003CAside type=\"note\">An earlier design carried two listener models at once: stream-mode listeners (`net.createServer`, WASM) on real kernel TCP sockets, and object-mode HTTP listeners (`http.createServer`) on a separate table that exchanged JSON request/response objects over stream events. A second guest process could not reach the object-mode table reliably, because the client expected byte-stream TCP semantics while the server only spoke object-mode dispatch. The current architecture removes the second model: everything is one socket table.\u003C/Aside>\n\n## The four layers\n\nA request passes through four layers. Only the top and bottom understand HTTP; the middle two move bytes and enforce policy.\n\n| Layer | Role | Trust | Lives in |\n| --- | --- | --- | --- |\n| 4 · Guest bridge | `node:http` / `node:net` / `fetch` / undici shim | untrusted (V8 isolate) | `crates/execution/assets/v8-bridge.source.js` |\n| 3 · Sync-RPC dispatch | routes `net.connect`, `net.http_request`, `net.listen`, … | trusted | `crates/sidecar/src/service.rs` |\n| 2 · Execution & enforcement | listener state, host fetch client, permission checks | trusted (TCB) | `crates/sidecar/src/execution.rs` |\n| 1 · Kernel socket table | `bind` / `listen` / `connect` / `read` / `write`, loopback routing | trusted (TCB floor) | `crates/kernel/src/socket_table.rs`, `kernel.rs` |\n\n### Layer 1: kernel socket table\n\n`crates/kernel/src/kernel.rs` exposes the primitives above. Loopback routing is the heart of VM-local networking: `socket_connect_inet_loopback` only succeeds against a socket that is actually bound and listening in the same VM's table; otherwise it returns `ECONNREFUSED`. Resource-limit checks run before the two sockets are paired.\n\n### Layer 2: sidecar execution (enforcement point / TCB)\n\n`crates/sidecar/src/execution.rs` is where policy is applied. Two roles matter for networking:\n\n- **Listener state.** `build_javascript_socket_path_context` walks every active process and records what is listening on which port, including a map of HTTP loopback targets keyed by `(family, port)`. This is the source of truth a connect consults to learn that, say, \"port 3000 is an HTTP server owned by process X, server Y.\"\n- **Host fetch client.** When the host calls `vmFetch` / `rt.fetch()`, the sidecar resolves the target to a VM-owned kernel listener, opens its own kernel socket, connects over loopback, and speaks HTTP/1.1 to the guest server. This is the only HTTP client that lives in the sidecar (the host has no guest isolate to do framing for it).\n\n### Layer 3: sync-RPC dispatch\n\n`crates/sidecar/src/service.rs` routes the bridge calls guest code makes. The guest-to-guest loopback HTTP path lands here as `net.http_request`. It is the most security-sensitive RPC, so it is guarded in order:\n\n1. The host must be a loopback address.\n2. The applied network policy must permit the operation.\n3. The requested `(process_id, server_id)` must match a listener that is currently live.\n\nThat last check stops a guest from forging a target to reach a process it should not.\n\n### Layer 4: guest bridge\n\n`crates/execution/assets/v8-bridge.source.js` is the Node-compatibility shim inside the untrusted V8 isolate. It presents `node:http`, `node:net`, `fetch`, and undici to guest code and translates them into Layer 3 bridge calls. `http.createServer()` is implemented on top of `net.Server`: each accepted byte socket is parsed as HTTP and dispatched to the guest's request handler.\n\n## How fetch, net, and dns route through it\n\n- **`node:net` (raw TCP).** `net.connect` / `net.createServer` map directly onto kernel `connect` / `bind` + `listen`. The bytes are the payload; no framing is added.\n- **`node:http` and `fetch`.** A guest HTTP server is a `net.Server` whose accepted sockets are HTTP-parsed in the bridge. A guest HTTP client runs undici over a kernel-backed dispatcher (or a raw serializer for the loopback fast path). Either way the bytes travel as kernel TCP.\n- **DNS.** Name resolution is serviced by the kernel resolver, not the host. Outbound connections that leave the VM resolve through it, and the resolved addresses are then filtered by the egress allowlist (see below). DNS pinning ties the connection to the address that was checked, closing the resolve-then-reconnect TOCTOU gap.\n\n### Where HTTP meets TCP\n\nThere is no shared HTTP/TCP translation module. Because the wire between every endpoint is raw TCP bytes through the kernel, HTTP is framed and deframed **at each edge that speaks HTTP**. The kernel (Layer 1) and the sidecar routing (Layer 2) never parse HTTP. There are three independent codecs, one per kind of endpoint:\n\n| Endpoint | Lives in | Encode / decode |\n| --- | --- | --- |\n| Guest HTTP server | guest bridge | `parseLoopbackRequestBuffer` (bytes to object), `serializeLoopbackResponse` (object to bytes), wired per accepted socket by `attachHttpServerSocket` |\n| Guest HTTP client | guest bridge | undici over a kernel-backed dispatcher, or `serializeRawHttpRequest` + `waitForRawHttpResponse` |\n| Host fetch client | sidecar execution | `serialize_kernel_http_fetch_request` (request to bytes), `parse_kernel_http_fetch_response` (bytes to JSON) |\n\nA WASM HTTP server or client does its own framing in guest code (reading the request line, writing a response with standard C socket calls). The kernel does not help it; it is just bytes, the same as for the JS endpoints.\n\n## Data flows\n\n- **Host to guest (`vmFetch` / `rt.fetch`).** The sidecar resolves the port to a VM-owned kernel listener, opens a sidecar-owned kernel socket, connects over loopback, serializes the request bytes, drives the target process forward so it can accept and respond, then parses the response bytes back into the host response object. It is **fail-closed**: no DNS, no external networking, no host-loopback fallback. If no VM-owned listener exists, it returns a missing-listener error.\n- **Guest to guest.** `net.connect` goes through the sidecar, which returns a loopback HTTP target handle. The guest sends the request through `net.http_request`, which dispatches into the target process's request handler. Cross-process loopback passes through the enforcement point rather than taking an in-isolate shortcut.\n- **Cross-runtime (JS and WASM, either direction).** Client and server connect through a kernel loopback socket pair and exchange raw bytes. JS to WASM, WASM to JS, and WASM to WASM all use the same path; only the side that runs the HTTP codec differs.\n- **Guest outbound to host or external.** Connections that do not target a VM-owned listener take the external network path: permission checks, DNS pinning, then a real host `TcpStream`. Reaching a host loopback port still requires an explicit loopback exemption entry.\n\n## Egress policy and loopback confinement\n\nGuest networking is confined by three distinct controls plus the loopback-only default. The permission policy and limits are **trusted configuration**; the guest executor is the **untrusted subject** they bind.\n\n### Loopback-only by default\n\nGuest listeners are reachable only over loopback (`127.0.0.1` / `::1`) inside the VM.\n\n- Binding to `0.0.0.0` or `::` does not widen this: the kernel normalizes the unspecified address down to loopback, so the listener still answers only on loopback.\n- A connection that originates outside the loopback interface and targets a port the VM does not own is refused with `EACCES`, noting the port is not exempt.\n- This confinement is independent of the permission policy. Even with the network allowed, a guest server stays loopback-only unless its port is explicitly exempted.\n\n### Three stacked controls\n\nThese are often conflated but are separate. They stack, and a request must pass every one that applies:\n\n1. **Permission policy** (`network.listen` / `network.connect`). Decides whether the guest may open a listener or initiate an outbound connection at all. A blocked operation fails with `blocked by network.listen policy` or `blocked by network.connect policy`.\n2. **Loopback confinement.** Decides who may reach an already-permitted guest listener. By default only loopback inside the VM; a per-port exemption loosens it.\n3. **DNS / egress allowlist.** Constrains where permitted outbound connections may go. The kernel filters resolved addresses, blocking outbound access to restricted ranges, so an allowed `connect` can still be refused by destination.\n\nThe per-port loopback exemption belongs to layer 2 only. It is a trusted, per-port whitelist that *loosens* the default loopback confinement (for example, exposing an in-VM dev server beyond loopback). It is not an egress control and grants no outbound reach; layers 1 and 3 still apply. It is configured with `loopbackExemptPorts`, a list of ports that are exempt from the SSRF checks at layer 2; each listed port is reachable from outside the loopback interface, while the permission policy and egress allowlist continue to apply.\n\n### Trust and ownership\n\nEvery guest connect, listen, read, and write passes through sidecar ownership and kernel owner checks. Guest-to-guest loopback is allowed only when the destination is a VM-owned listener and the applied network policy permits the connect. Host-loopback access from guest code is separate and still requires a loopback exemption plus the applied network policy. Long-lived waits must not block the sync-RPC path, so the stack uses stream events, bounded polling, and kernel socket waits with explicit timeouts.\n\n\u003CAside type=\"note\">Host-to-guest requests bypass egress, not the table. `vmFetch` / `rt.fetch` terminate at the guest's loopback listener and never leave the VM, so they work even when guest egress (layer 3) or outbound `connect` (layer 1) is denied. They are host control-plane traffic, not guest egress, and only ever reach VM-owned listeners, while still going through the same kernel socket table as everything else.\u003C/Aside>\n\n## Preview URLs\n\nA preview URL is port forwarding for a VM service: a time-limited, signed, publicly reachable URL that proxies HTTP to a port inside the VM. Mechanically it reuses the host-to-guest path:\n\n- A signed token is minted for a `(VM, port)` pair with an expiration, capped by `preview.maxExpiresInSeconds`. Tokens are stored in SQLite, survive sleep/wake cycles, and expired ones are cleaned up automatically.\n- An incoming request to the preview path is authenticated against the token, then proxied into the VM exactly like `vmFetch`: resolve the port to a VM-owned kernel listener, connect over loopback, frame HTTP/1.1, drive the target process, and stream the response back. The same fail-closed, VM-owned-listener-only rules apply.\n- CORS is enabled so browsers can reach preview URLs from any origin.\n- Revocation (`expireSignedPreviewUrl`) invalidates the token immediately, after which the proxy refuses the request before touching the socket table.\n\nBecause previews ride the host fetch path, they are subject to loopback confinement at the kernel but **not** to the guest egress allowlist: the request enters the listener from the host side and never becomes guest outbound traffic.\n\n## Where to go next\n\n- [Networking & Previews](/docs/networking): the `vmFetch` and preview URL API, with usage examples.\n- [Architecture](/docs/architecture): the client / sidecar / executor trust boundary this stack lives inside.\n- [Security Model](/docs/security-model): the full in-scope and out-of-scope threat model.","src/content/docs/docs/architecture/networking.mdx","cb88fbadc029bce5","docs/architecture/posix-syscalls",{"id":462,"data":464,"body":470,"filePath":471,"digest":472,"deferredRender":16},{"title":465,"description":466,"editUrl":16,"head":467,"template":18,"sidebar":468,"pagefind":16,"draft":20},"POSIX Syscalls","How agentOS extends WASI in two layers so WebAssembly guests behave like normal POSIX programs on top of the kernel.",[],{"hidden":20,"attrs":469},{},"import { Aside } from '@astrojs/starlight/components';\n\nNot everything inside an agentOS VM is JavaScript. The shell (`sh`) and the\ncoreutils behind [process execution](/docs/processes) ship as WebAssembly\nbinaries, and you can run your own WASM programs too. To make those programs\nbehave like normal Linux tools, agentOS presents a POSIX syscall surface on top\nof WebAssembly.\n\n- **WASM is a first-class guest.** WASM binaries run beside JavaScript inside the same VM.\n- **Same kernel, same boundary.** WASM syscalls route through the same kernel that backs JS guests, so there is no extra host access.\n- **POSIX shape, not host access.** The extensions below add process, user, and network *semantics*, all virtualized.\n\n## Why WASI alone is not enough\n\nThe base standard for WASM system access is **WASI** (specifically `wasip1`).\nWASI is intentionally minimal:\n\n- It gives a guest preopened file descriptors, clocks, randomness, and basic file I/O.\n- It has **no process model** (no `fork` / `exec` / `wait`).\n- It has **no users or groups** (no `getuid` / `getgid`).\n- It has **no general sockets** (no `connect` / `listen`).\n\nReal command-line programs expect all of those. agentOS closes the gap in two\nlayers, and both route through the kernel rather than the host.\n\n\u003CAside type=\"note\">\nEvery WASM syscall, like every JS syscall, goes through the kernel-owned virtual\nfilesystem, process table, and socket table. The extensions below add POSIX\n*shape*; they do not add host access. See the [Security Model](/docs/security-model)\nfor the isolation boundary.\n\u003C/Aside>\n\n## The two-layer model\n\nagentOS layers a POSIX surface over WASM. Layer 1 adds capabilities WASI does\nnot express at all; Layer 2 adapts the standard WASI calls so a normal libc\nbehaves correctly inside the VM. Both bottom out in the kernel.\n\n\u003Csvg viewBox=\"0 0 700 360\" xmlns=\"http://www.w3.org/2000/svg\" role=\"img\" aria-label=\"Two-layer WASM-on-kernel model\" style=\"max-width: 700px; width: 100%; height: auto; font-family: ui-sans-serif, system-ui, sans-serif;\">\n \u003Crect x=\"0\" y=\"0\" width=\"700\" height=\"360\" fill=\"#ffffff\" />\n\n {/* Guest */}\n \u003Crect x=\"40\" y=\"20\" width=\"620\" height=\"56\" rx=\"8\" fill=\"#f4f4f5\" stroke=\"#d4d4d8\" />\n \u003Ctext x=\"350\" y=\"44\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"600\" fill=\"#18181b\">WASM guest (sh, coreutils, your .wasm)\u003C/text>\n \u003Ctext x=\"350\" y=\"64\" text-anchor=\"middle\" font-size=\"12\" fill=\"#52525b\">compiled for wasm32-wasip1, linked against patched wasi-libc\u003C/text>\n\n {/* Layer 1 */}\n \u003Crect x=\"40\" y=\"100\" width=\"300\" height=\"120\" rx=\"8\" fill=\"#eef2ff\" stroke=\"#c7d2fe\" />\n \u003Ctext x=\"190\" y=\"124\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"600\" fill=\"#3730a3\">Layer 1: host import modules\u003C/text>\n \u003Ctext x=\"190\" y=\"148\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_process — spawn / wait\u003C/text>\n \u003Ctext x=\"190\" y=\"168\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_user — uid / gid\u003C/text>\n \u003Ctext x=\"190\" y=\"188\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_net — TCP sockets\u003C/text>\n \u003Ctext x=\"190\" y=\"208\" text-anchor=\"middle\" font-size=\"12\" fill=\"#3730a3\">host_sleep_ms — blocking sleep\u003C/text>\n\n {/* Layer 2 */}\n \u003Crect x=\"360\" y=\"100\" width=\"300\" height=\"120\" rx=\"8\" fill=\"#ecfdf5\" stroke=\"#a7f3d0\" />\n \u003Ctext x=\"510\" y=\"124\" text-anchor=\"middle\" font-size=\"14\" font-weight=\"600\" fill=\"#065f46\">Layer 2: kernel-backed WASI shim\u003C/text>\n \u003Ctext x=\"510\" y=\"148\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">stdio through the kernel bridge\u003C/text>\n \u003Ctext x=\"510\" y=\"168\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">mounts mirrored as preopens\u003C/text>\n \u003Ctext x=\"510\" y=\"188\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">read-only tiers enforced\u003C/text>\n \u003Ctext x=\"510\" y=\"208\" text-anchor=\"middle\" font-size=\"12\" fill=\"#065f46\">paths confined to their mount\u003C/text>\n\n {/* Arrows down to kernel */}\n \u003Cline x1=\"190\" y1=\"220\" x2=\"190\" y2=\"280\" stroke=\"#71717a\" stroke-width=\"2\" marker-end=\"url(#arrow)\" />\n \u003Cline x1=\"510\" y1=\"220\" x2=\"510\" y2=\"280\" stroke=\"#71717a\" stroke-width=\"2\" marker-end=\"url(#arrow)\" />\n\n {/* Kernel */}\n \u003Crect x=\"40\" y=\"284\" width=\"620\" height=\"56\" rx=\"8\" fill=\"#fafafa\" stroke=\"#d4d4d8\" />\n \u003Ctext x=\"350\" y=\"308\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"600\" fill=\"#18181b\">Kernel: virtual filesystem, process table, socket table\u003C/text>\n \u003Ctext x=\"350\" y=\"328\" text-anchor=\"middle\" font-size=\"12\" fill=\"#52525b\">same paths that back JavaScript guests — no host escape\u003C/text>\n\n \u003Cdefs>\n \u003Cmarker id=\"arrow\" markerWidth=\"10\" markerHeight=\"10\" refX=\"6\" refY=\"3\" orient=\"auto\" markerUnits=\"strokeWidth\">\n \u003Cpath d=\"M0,0 L6,3 L0,6 Z\" fill=\"#71717a\" />\n \u003C/marker>\n \u003C/defs>\n\u003C/svg>\n\n## Layer 1: custom host import modules\n\nStandard WASI cannot express `fork` / `exec`, `getuid`, or `connect`. agentOS\ndeclares extra WebAssembly import modules that the host runtime implements, so\nguest libc can call them as if they were ordinary syscalls. These bindings live\nin the `wasi-ext` crate and cover three areas:\n\n- **`host_process`**: process management. Spawn a child process (argv, env, inherited stdio fds, working directory), wait for a child to exit, and related file-descriptor operations. This is what gives a WASM `sh` real [child process](/docs/processes) semantics; spawns go through the kernel process table.\n- **`host_user`**: user and group identity (uid, gid, user info). Base WASI has no concept of a user; this lets tools that call `getuid` / `getgid` see the VM's virtualized identity.\n- **`host_net`**: TCP sockets (connect, listen, send, receive) through the kernel socket table, gated by the same [network permission policy](/docs/networking) as everything else. Base WASI has no general socket API.\n\nA small `host_sleep_ms` binding provides blocking sleep. Together these let a\nguest compiled for `wasip1` behave as if it had a process model, user identity,\nand a network, all virtualized.\n\n```c\n// Imported from the host runtime, declared by the wasi-ext bindings.\n// Guest libc calls these as if they were ordinary syscalls.\n__attribute__((import_module(\"host_process\"), import_name(\"proc_spawn\")))\nint host_proc_spawn(const char *argv, const char *envp, int cwd_fd);\n\n// getuid returns an errno; the uid is written through the out-pointer.\n__attribute__((import_module(\"host_user\"), import_name(\"getuid\")))\nint host_getuid(unsigned int *ret_uid);\n\n__attribute__((import_module(\"host_net\"), import_name(\"net_connect\")))\nint host_net_connect(int fd, const char *addr, int addr_len);\n```\n\n## Layer 2: the kernel-backed WASI shim\n\nThe second layer adapts the standard WASI calls themselves so that programs\nbuilt against a normal libc behave correctly inside the VM. The embedded shim:\n\n- **Routes stdio through the kernel.** `fd_read` / `fd_write` on the standard descriptors go through the kernel stdio bridge rather than host file descriptors, so output stays inside the VM and honors PTYs and redirection.\n- **Fills in libc expectations.** For example `fcntl(F_SETFL)` is serviced via `fd_fdstat_set_flags`, so flag changes that libc performs do not fail.\n- **Mirrors mounts as preopens.** The preopen table reflects the VM's guest path mappings, so mounted directories are visible to WASM path resolution exactly as they are to JS and to `node:fs`.\n- **Enforces read-only tiers.** `path_open` rejects create / truncate / write flags on read-only mounts while still allowing non-mutating opens (directory traversal, `O_DIRECTORY`), so read-only mounts stay read-only without breaking `find`, `ls`, and friends.\n- **Confines paths to their mount.** Targets are resolved beneath the specific preopen's root, so `..` segments cannot escape one mount into a sibling mount or a host path.\n\n```\nfd_read(0) -> kernel stdio bridge (not a host fd)\nfcntl(fd, F_SETFL) -> fd_fdstat_set_flags (libc flag changes succeed)\npath_open(\"/data/x\") -> resolved under the /data preopen root\npath_open(..O_CREAT) -> rejected on a read-only mount\npath_open(\"../../etc\")-> stays inside the mount; cannot escape\n```","src/content/docs/docs/architecture/posix-syscalls.mdx","59df167c76ec6de4","docs/architecture/processes",{"id":473,"data":475,"body":481,"filePath":482,"digest":483,"deferredRender":16},{"title":476,"description":477,"editUrl":16,"head":478,"template":18,"sidebar":479,"pagefind":16,"draft":20},"Processes","Internals of the kernel process model: the virtual process table, how spawns are serviced, stdio bridging, PTYs, and how WASM sh and coreutils map onto it.",[],{"hidden":20,"attrs":480},{},"import { Aside } from '@astrojs/starlight/components';\n\nThis page is an internals deep-dive on the kernel's **process model**: the data\nstructures and syscall paths behind every guest process. For the client-facing\nAPI (`exec`, `spawn`, `openShell`, lifecycle, the process tree), see\n[Processes & Shell](/docs/processes). For the surrounding component and trust\nmodel, see [Architecture](/docs/architecture).\n\nTwo invariants frame everything below:\n\n- **No real host process is ever spawned for guest work.** Every guest process is an entry in a kernel-owned virtual process table, not an OS process. Guest JavaScript runs in V8 isolates; guest commands like `sh` and coreutils run as WebAssembly. Neither is `node` or a host binary.\n- **Every process operation is a syscall into the kernel.** Spawning, waiting, signaling, reading stdout, and resizing a PTY all cross from the untrusted executor into the sidecar-owned kernel, which services them against virtualized resources.\n\n## The virtual process table\n\nEach VM owns one process table. It is the authority for what is \"running\"\ninside that VM; nothing in it corresponds to a host PID.\n\n- **Per-VM and isolated.** Two VMs have two independent tables. A PID in one VM is meaningless in another, and processes are never visible across the VM boundary.\n- **Holds every guest process,** not only the ones a client started explicitly. A `spawn` from the client, a child spawned by guest `node:child_process`, and the processes behind a shell pipeline are all table entries. This is why the system-wide views (`allProcesses`, `processTree`) can show more than what the client launched.\n- **Tracks lifecycle and lineage.** Each entry carries its PID, the command and arguments, parent PID (so the tree can be reconstructed), running/exited status, exit code once collected, and its attached stdio endpoints.\n- **Records a driver.** An entry knows which execution backend services it (for example a V8 isolate versus a WASM runtime). This is the `driver` field surfaced on `allProcesses`. Drivers differ in *how* the code runs; they share the same table, the same kernel-owned stdio, and the same boundary.\n\n\u003CAside type=\"note\">The process table is part of the kernel the sidecar owns. The executor never mutates it directly; it can only ask the kernel to create, wait on, or signal an entry. That request-only relationship is the sidecar-to-executor boundary applied to processes.\u003C/Aside>\n\n## How a spawn is serviced\n\nA spawn, whether it originates from a client `spawn`/`exec` call or from guest\n`node:child_process`, follows one path through the kernel:\n\n1. **The request crosses into the kernel.** A client call arrives over the wire protocol; a guest call arrives as a syscall from the executor. Either way the kernel, not the caller, performs the work.\n2. **Permission check.** The kernel applies the VM's permission policy before doing anything. Process execution is denied by default and must be granted; the policy is trusted input, the guest making the request is not.\n3. **Resolve the program.** The command is resolved against the VM's virtual filesystem (PATH lookup over the VFS), not the host. The resolved program decides the driver: a JavaScript entrypoint runs in a V8 isolate; a `.wasm` program (including `sh` and coreutils) runs on the WASM runtime.\n4. **Allocate the table entry.** The kernel assigns a virtual PID, records the command, arguments, environment, working directory, and parent PID, and links stdio endpoints (see below).\n5. **Start execution.** The driver begins running the program. For a one-shot `exec` the kernel additionally collects stdout, stderr, and the exit code and returns them as the call's result; for `spawn` it leaves the process running and streams output via events.\n6. **Reap and record exit.** When the program finishes, the kernel records the exit code on the table entry and marks it exited, which is what a `wait`/`waitProcess` resolves against and what `processExit` reports.\n\nSignals (`stopProcess` / SIGTERM, `killProcess` / SIGKILL) are the same shape: a\nrequest into the kernel, which applies it to the virtualized process rather than\nto any host process.\n\n## Stdio bridging\n\nStandard streams are kernel-owned objects, not host file descriptors. Each\nprocess entry has stdin, stdout, and stderr endpoints that the kernel wires up\nwhen the entry is created.\n\n- **Capture vs. stream.** For `exec`, the kernel buffers stdout and stderr and hands them back when the process exits. For `spawn`, output is delivered incrementally as `processOutput` events tagged with the PID and the stream (`stdout`/`stderr`), and `processExit` signals completion.\n- **Writable stdin.** `writeProcessStdin` pushes bytes into the process's stdin endpoint; `closeProcessStdin` closes the write side so programs that read to EOF (like `cat`) can finish. None of this touches a real pipe on the host.\n- **Pipes between processes.** Shell pipelines (`a | b`) connect one process's stdout endpoint to the next process's stdin endpoint through kernel-owned pipes. The pipe is a virtual object in the kernel, so a pipeline behaves like Linux without any host IPC.\n\nBecause these endpoints are kernel objects, the same bridging works identically\nwhether the process is a V8 isolate or a WASM program; the driver writes to and\nreads from kernel stdio, not from anything host-provided.\n\n## PTYs and interactive shells\n\nAn interactive shell needs a terminal, not just piped stdio: line editing, job\ncontrol signals, and window size all depend on a PTY. The kernel provides\nvirtual PTY devices for this.\n\n- **A shell is a process plus a PTY.** `openShell` allocates a kernel PTY and starts a shell process attached to it, returning a `shellId`. The PTY is a virtualized terminal device, never a host `/dev/pts` entry.\n- **Bidirectional terminal I/O.** `writeShell` feeds keystrokes into the PTY master side; everything the shell and its children emit comes back as `shellData` events. This carries terminal control sequences, so full-screen TUIs behave correctly.\n- **Resize is a terminal operation.** `resizeShell` updates the PTY's window size (columns and rows), which the kernel propagates to the foreground process the way a real terminal resize would, so programs relying on `TIOCGWINSZ`-style sizing redraw correctly.\n- **Teardown.** `closeShell` tears down the PTY and the attached shell process. An open shell keeps the VM active, the same way an open PTY keeps a session alive on a real system.\n\n## WASM sh and coreutils on the process model\n\nThe shell and the standard commands behind process execution are not special\nhost helpers; they are ordinary guest processes that happen to be WebAssembly.\nFor the full WASM execution model see [WASM VM](/docs/architecture/posix-syscalls); here is how it\nmaps onto the process table specifically.\n\n- **They are normal table entries.** Running `sh`, `ls`, `cat`, etc. allocates virtual PIDs and table entries exactly like any other process, with the WASM driver recorded on each. A pipeline of coreutils is several entries linked by kernel pipes.\n- **POSIX process semantics are virtualized, not borrowed from the host.** Plain WASI has no process model (no `fork`/`exec`/`wait`). agentOS supplies those semantics through kernel-backed host imports, so a WASM program that spawns and waits on a child drives the *same* kernel process table that JS guests use. A coreutil spawning a subcommand is one table entry creating another.\n- **Same stdio, same PTY.** WASM processes read and write the kernel stdio endpoints described above, and a shell built from WASM `sh` attaches to a kernel PTY just like any interactive shell. The driver differs; the kernel-owned plumbing does not.\n\nThis is why the process model is uniform: whether an entry is a V8 isolate or a\nWASM binary, it lives in the same per-VM table, goes through the same\npermission-checked spawn path, and uses the same kernel-owned stdio and PTYs.\n\n## See also\n\n- [Processes & Shell](/docs/processes): the client API for running and managing processes.\n- [WASM VM](/docs/architecture/posix-syscalls): how WebAssembly guests get POSIX process, user, and network semantics.\n- [Architecture](/docs/architecture): components, the trust boundary, and the request lifecycle.\n- [Permissions](/docs/permissions): the policy the kernel checks on every spawn.","src/content/docs/docs/architecture/processes.mdx","da5a629c2d1b5a3c","docs/architecture/sessions-persistence",{"id":484,"data":486,"body":492,"filePath":493,"digest":494,"deferredRender":16},{"title":487,"description":488,"editUrl":16,"head":489,"template":18,"sidebar":490,"pagefind":16,"draft":20},"Sessions & Persistence","How Agent OS, ACP, RivetKit actors, and durable session persistence fit together.",[],{"hidden":20,"attrs":491},{},"Agent OS runs coding agents inside VMs and talks to them through the Agent\nCommunication Protocol (ACP). RivetKit wraps those VMs in durable actors, so a\nsession can survive actor sleep/wake even though the live VM and agent process\ndo not.\n\n## Layers\n\nAgent OS session architecture has four layers:\n\n| Layer | Responsibility |\n| --- | --- |\n| RivetKit actor | Owns the public API and durable actor-local SQLite state. |\n| Agent OS client | Thin facade used by the actor to create sessions, prompt agents, and call the sidecar. |\n| Agent OS sidecar ACP extension | Launches ACP adapters inside the VM, speaks JSON-RPC, handles permissions, and owns resume orchestration. |\n| ACP adapter / agent | Runs inside the VM and speaks ACP over stdio. |\n\nThe actor is durable. The VM is disposable. The ACP agent process is live state\ninside the VM.\n\n## API Shape\n\nThe actor-facing session API is:\n\n- `createSession(agentType, options)`\n- `sendPrompt(sessionId, text)`\n- `closeSession(sessionId)`\n- `listPersistedSessions()`\n- `getSessionEvents(sessionId)`\n\n`sessionId` is the stable, client-facing id. If fallback resume creates a new\nlive ACP session id after wake, the actor keeps an internal\n`externalSessionId -> liveSessionId` remap. Clients keep using the original\n`sessionId`.\n\n## Create Flow\n\n1. The actor calls Agent OS `createSession`.\n2. The sidecar starts the ACP adapter process inside the VM.\n3. The sidecar sends ACP `initialize`.\n4. The sidecar sends ACP `session/new`.\n5. The actor persists session metadata in `agent_os_sessions`.\n6. The actor starts capturing ACP `session/update` events for the session.\n\nPersisted session metadata includes:\n\n- `session_id`\n- `agent_type`\n- agent capabilities and agent info\n- create-time `cwd`\n- create-time `env`\n\nThe create-time `cwd` and `env` are used later so resumed sessions start with\nthe same working directory and environment they were created with.\n\n## Prompt Flow\n\n1. The actor receives `sendPrompt(sessionId, text)`.\n2. If the session is persisted but not live in the current VM, the actor lazily\n resumes it first.\n3. The actor writes a synthetic `user_prompt` event before forwarding the\n prompt.\n4. The actor forwards the prompt to the live ACP session id.\n5. The sidecar sends ACP `session/prompt`.\n6. Inbound ACP `session/update` events are captured into\n `agent_os_session_events`.\n\n`agent_os_session_events` is ordered per session. Sequence numbers are allocated\ninside the SQLite insert so concurrent prompt and stream captures cannot reuse\nthe same sequence number.\n\n## Sleep And Wake\n\nWhen a RivetKit actor sleeps:\n\n- the VM is destroyed\n- ACP adapter processes exit\n- the actor's in-memory `live_sessions` remap is lost\n- actor SQLite survives\n\nWhen the actor wakes:\n\n- a fresh VM boots\n- stable session ids still exist in `agent_os_sessions`\n- no ACP session is live yet\n- resume happens lazily on the next prompt\n\n## Resume Flow\n\nOn the first post-wake prompt for a persisted session:\n\n1. The actor reads `agent_os_sessions`.\n2. The actor reconstructs a Markdown transcript from\n `agent_os_session_events`.\n3. The actor writes the transcript to\n `/root/.agentos/threads/\u003CsessionId>.md`.\n4. The actor calls sidecar `resumeSession` with:\n - stable external `sessionId`\n - agent type\n - transcript path\n - persisted create-time `cwd`\n - persisted create-time `env`\n\nThe sidecar then chooses one of two resume paths.\n\n### Native Resume\n\nIf the ACP agent advertises `loadSession` or `resume`, the sidecar sends\n`session/load` or `session/resume`.\n\nWhen native resume succeeds:\n\n- the live ACP id is the stable external `sessionId`\n- the agent restores its own context\n- no transcript preamble is injected\n\nOpenCode uses this path when its own session store is still available in the\ndurable VM filesystem.\n\n### Transcript Fallback\n\nIf native resume is unsupported, or if native resume reports a normalized\n`unknown_session`, the sidecar falls back to a fresh session:\n\n1. The sidecar sends ACP `session/new`.\n2. The sidecar returns the new live ACP id to the actor.\n3. The actor stores `externalSessionId -> liveSessionId`.\n4. The sidecar prepends a one-shot preamble to the next prompt pointing at the\n transcript path.\n\nThe fallback is universal because it only requires the agent to read a file with\nits normal tools. It is lower fidelity than native resume because the transcript\nis pointed to, not automatically loaded into the agent's context window.\n\n## Unknown Session Normalization\n\nAdapters report missing sessions differently. The sidecar normalizes known\nmissing-session shapes into:\n\n```json\n{ \"error\": { \"data\": { \"kind\": \"unknown_session\" } } }\n```\n\nFor example, OpenCode currently reports a missing native session as:\n\n```json\n{ \"code\": -32603, \"data\": { \"details\": \"NotFoundError\" } }\n```\n\nThat shape is captured before normalization in tests, then normalized so the\nresume state machine can safely choose transcript fallback. Other internal\nerrors still propagate as failures.\n\n## Persistence\n\nDurable session state lives in actor SQLite:\n\n| Table | Purpose |\n| --- | --- |\n| `agent_os_sessions` | Stable session registry, agent type, capabilities, agent info, create-time `cwd`, and create-time `env`. |\n| `agent_os_session_events` | Append-only prompt and ACP event log keyed by the stable external `sessionId`. |\n\nThe transcript file is not canonical state. It is a disposable render of\n`agent_os_session_events`, rebuilt on demand during fallback resume.\n\n## What Is Durable\n\n| Data | Survives sleep/wake? | Notes |\n| --- | --- | --- |\n| Actor SQLite | Yes | Stores session registry, events, preview tokens, and other actor data. |\n| VM filesystem | Yes, when backed by the actor sqlite_vfs root | Used by agents and resume transcripts. |\n| Live ACP process | No | Recreated on wake. |\n| Actor in-memory vars | No | Includes the live ACP id remap. |\n| Client-facing `sessionId` | Yes | Stored in `agent_os_sessions`. |\n\n## Where To Look In Code\n\n- Sidecar ACP orchestration:\n `crates/agentos-sidecar/src/acp_extension.rs`\n- Agent OS TypeScript client surface:\n `packages/core/src/agent-os.ts`\n- RivetKit actor session actions:\n `rivetkit-rust/packages/rivetkit-agent-os/src/actions/session.rs`\n- RivetKit persistence helpers:\n `rivetkit-rust/packages/rivetkit-agent-os/src/persistence.rs`","src/content/docs/docs/architecture/sessions-persistence.mdx","c7a4d6fe57f3fcbb","docs/debugging",{"id":495,"data":497,"body":503,"filePath":504,"digest":505,"deferredRender":16},{"title":498,"description":499,"editUrl":16,"head":500,"template":18,"sidebar":501,"pagefind":16,"draft":20},"Debugging","Capture agent logs and runtime (sidecar) logs to diagnose sessions, tool calls, and crashes.",[],{"hidden":20,"attrs":502},{},"import { Aside } from '@astrojs/starlight/components';\n\nTwo log streams help diagnose what's happening inside a VM: the **agent's** own output and the **runtime (sidecar)** logs.\n\n## Agent logs (`onAgentStderr`)\n\nThe coding agent (ACP adapter) runs as a process inside the VM and uses **stdout for the ACP protocol**, so its **stderr** carries the agent's logs, warnings, and crash output — the first place to look when a tool call or session fails mid-turn. Capture it with `onAgentStderr` on the VM:\n\n```ts\nconst agentOs = await AgentOs.create({\n software: [pi],\n onAgentStderr(event) {\n // event: { sessionId, agentType, processId, pid, chunk: Uint8Array }\n process.stderr.write(`[agent:${event.agentType}] `);\n process.stderr.write(event.chunk);\n },\n});\n```\n\nIt's a VM-level option covering every session's agent process; if omitted, chunks are written to the host `process.stderr` by default. See [Sessions → Agent logs](/docs/sessions#agent-logs).\n\n## Runtime logs (sidecar)\n\nThe agentOS sidecar emits structured **logfmt** logs for request handling, networking, and lifecycle. Configure them with environment variables on the **host process** (the sidecar inherits the host environment):\n\n| env var | effect |\n|---------|--------|\n| `AGENTOS_LOG_LEVEL` / `LOG_LEVEL` / `RUST_LOG` | log filter, in that priority. Uses [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) syntax, e.g. `debug`, `info`, `agentos_sidecar=debug,info`. Default `info`. |\n| `RUST_LOG_FORMAT` | `logfmt` (default) or `text` |\n| `AGENTOS_LOG_FILE` | append logs to this file instead of stderr (never stdout, which carries the wire protocol) |\n| `RUST_LOG_{SPAN_NAME,SPAN_PATH,TARGET,LOCATION,MODULE_PATH,ANSI_COLOR}` | per-field toggles (`=1` to enable) |\n\n```bash\nAGENTOS_LOG_LEVEL=debug AGENTOS_LOG_FILE=./sidecar.log RUST_LOG_FORMAT=logfmt node app.mjs\n```\n\nProduces logfmt lines such as:\n\n```text\nts=2026-… level=info message=\"ext request received\" kind=create_session\nts=2026-… level=info message=\"ext request handled\" kind=create_session elapsed_ms=1798\nts=2026-… level=debug message=\"querying: api.anthropic.com. A\"\n```\n\n\u003CAside type=\"note\">\nMost sidecar log activity is on the session/ACP path. A bare `AgentOs.create()` or a single `exec()` emits almost nothing — create a session (and send a prompt) to see request-handling logs.\n\u003C/Aside>\n\nUse **agent logs** to see what the agent did (tool calls, model errors), and **runtime logs** to see what the sidecar did around it (request timing, DNS, lifecycle).","src/content/docs/docs/debugging.mdx","37486db894865f03"] \ No newline at end of file diff --git a/website/docs.config.mjs b/website/docs.config.mjs index 826717148..655e4d951 100644 --- a/website/docs.config.mjs +++ b/website/docs.config.mjs @@ -127,6 +127,7 @@ export const siteConfig = { label: "More", items: [ { slug: "docs/core", label: "Core SDK" }, + { slug: "docs/debugging", label: "Debugging", attrs: { "data-icon": "bug" } }, { slug: "docs/benchmarks" }, { slug: "docs/cost-evaluation", label: "Cost Evaluation" }, ], diff --git a/website/src/content/docs/docs/debugging.mdx b/website/src/content/docs/docs/debugging.mdx new file mode 100644 index 000000000..cc3a4f467 --- /dev/null +++ b/website/src/content/docs/docs/debugging.mdx @@ -0,0 +1,54 @@ +--- +title: "Debugging" +description: "Capture agent logs and runtime (sidecar) logs to diagnose sessions, tool calls, and crashes." +--- + +import { Aside } from '@astrojs/starlight/components'; + +Two log streams help diagnose what's happening inside a VM: the **agent's** own output and the **runtime (sidecar)** logs. + +## Agent logs (`onAgentStderr`) + +The coding agent (ACP adapter) runs as a process inside the VM and uses **stdout for the ACP protocol**, so its **stderr** carries the agent's logs, warnings, and crash output — the first place to look when a tool call or session fails mid-turn. Capture it with `onAgentStderr` on the VM: + +```ts +const agentOs = await AgentOs.create({ + software: [pi], + onAgentStderr(event) { + // event: { sessionId, agentType, processId, pid, chunk: Uint8Array } + process.stderr.write(`[agent:${event.agentType}] `); + process.stderr.write(event.chunk); + }, +}); +``` + +It's a VM-level option covering every session's agent process; if omitted, chunks are written to the host `process.stderr` by default. See [Sessions → Agent logs](/docs/sessions#agent-logs). + +## Runtime logs (sidecar) + +The agentOS sidecar emits structured **logfmt** logs for request handling, networking, and lifecycle. Configure them with environment variables on the **host process** (the sidecar inherits the host environment): + +| env var | effect | +|---------|--------| +| `AGENTOS_LOG_LEVEL` / `LOG_LEVEL` / `RUST_LOG` | log filter, in that priority. Uses [EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) syntax, e.g. `debug`, `info`, `agentos_sidecar=debug,info`. Default `info`. | +| `RUST_LOG_FORMAT` | `logfmt` (default) or `text` | +| `AGENTOS_LOG_FILE` | append logs to this file instead of stderr (never stdout, which carries the wire protocol) | +| `RUST_LOG_{SPAN_NAME,SPAN_PATH,TARGET,LOCATION,MODULE_PATH,ANSI_COLOR}` | per-field toggles (`=1` to enable) | + +```bash +AGENTOS_LOG_LEVEL=debug AGENTOS_LOG_FILE=./sidecar.log RUST_LOG_FORMAT=logfmt node app.mjs +``` + +Produces logfmt lines such as: + +```text +ts=2026-… level=info message="ext request received" kind=create_session +ts=2026-… level=info message="ext request handled" kind=create_session elapsed_ms=1798 +ts=2026-… level=debug message="querying: api.anthropic.com. A" +``` + + + +Use **agent logs** to see what the agent did (tool calls, model errors), and **runtime logs** to see what the sidecar did around it (request timing, DNS, lifecycle).