diff --git a/docs/content/docs/architecture.mdx b/docs/content/docs/architecture.mdx index 0cff94938b..b0a436962b 100644 --- a/docs/content/docs/architecture.mdx +++ b/docs/content/docs/architecture.mdx @@ -3,9 +3,9 @@ title: "Architecture" description: "How Core, the sidecar, kernel, VFS, and executors fit together." --- -`@rivet-dev/agentos-core` controls the same runtime from a trusted Node.js -process. It may retain host-only state such as callbacks, binding handlers, -mount plugins, local package paths, and custom scheduling. +`@rivet-dev/agentos-core` controls the runtime from a trusted Node.js process. +It may retain host-only state such as host function handlers, mount plugins, +local package paths, and custom scheduling. ## Runtime boundary @@ -31,5 +31,5 @@ Each owner advances only its own migration ladder. There is no legacy storage adoption path. The default VM root is an ephemeral in-memory overlay over the bundled base -image. Configure durable filesystem storage for files that must survive VM -replacement. +image. Store the root filesystem in the VM's SQLite database for files that +must survive VM recreation; see [Persistence](/agentos/docs/persistence). diff --git a/docs/content/docs/architecture/filesystem.mdx b/docs/content/docs/architecture/filesystem.mdx index c144870f32..7e45c8a2ba 100644 --- a/docs/content/docs/architecture/filesystem.mdx +++ b/docs/content/docs/architecture/filesystem.mdx @@ -51,7 +51,7 @@ Resolution is **longest-prefix wins**: if `/mnt/data` is a mount and the guest o \- ... ``` -The 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](/agentos/docs/filesystem#mounts) for the user-facing config. +The base layer is in-memory and per-VM, and is discarded when the VM is disposed. A root or mount backed by the VM's SQLite database survives VM recreation; see [Persistence](/agentos/docs/persistence). Mounts are pluggable: any guest path can be backed by the host, a remote, or a cloud store. See [Mounting filesystems](/agentos/docs/filesystem#mounts) for the user-facing config. ## Routing a guest syscall diff --git a/docs/content/docs/architecture/networking.mdx b/docs/content/docs/architecture/networking.mdx index 09d30dca33..cada2bd4b5 100644 --- a/docs/content/docs/architecture/networking.mdx +++ b/docs/content/docs/architecture/networking.mdx @@ -1,12 +1,12 @@ --- title: "Networking" -description: "Explore the agentOS kernel socket table, JavaScript and WASM traffic, DNS, egress policy, loopback isolation, and preview routing." +description: "Explore the agentOS kernel socket table, JavaScript and WASM traffic, DNS, egress policy, and loopback isolation." skill: true --- These internal architecture docs are mostly generated and maintained by LLMs, then reviewed by humans. They are intentionally verbose; use your preferred LLM to ask focused questions about the architecture as needed. -This 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 (`httpRequest`, preview URLs, the confinement model from a caller's perspective), see [Networking & Previews](/agentos/docs/networking). For the trust boundary this all sits inside, see [Architecture](/agentos/docs/architecture). +This 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 (`httpRequest`, `fetch`, the confinement model from a caller's perspective), see [Networking](/agentos/docs/networking). For the trust boundary this all sits inside, see [Architecture](/agentos/docs/architecture). The governing rule for TCP is that there is exactly **one authoritative transport for everything VM-local**: the kernel socket table. No untrusted guest code opens a host socket directly. Guest `fetch()`, `node:http`, TCP `node:net`, WASM TCP clients and servers, and host-into-guest requests (`httpRequest` / `rt.fetch`) all target the same listener table. The trusted sidecar implements `AF_UNIX` streams with a separate, per-VM private host transport while keeping pathname inodes and permissions authoritative in the guest VFS. @@ -118,20 +118,9 @@ Every guest connect, listen, read, and write passes through sidecar ownership an Host-to-guest requests bypass egress, not the table. `httpRequest` / `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. -## Preview URLs - -A 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: - -- 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. Active tokens are bounded by `preview.maxActiveTokens` (1,024 by default); creation fails with a limit error that names the option when the bound is reached. -- An incoming request to the preview path is authenticated against the token, then proxied into the VM exactly like `httpRequest`: resolve the port to a VM-owned kernel listener, connect over loopback, frame HTTP/1.1, drive the target process, and return the bounded buffered response. The same fail-closed, VM-owned-listener-only rules apply. -- CORS is enabled so browsers can reach preview URLs from any origin. -- Revocation (`network.preview.expire`) invalidates the token immediately, after which the proxy refuses the request before touching the socket table. - -Because 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. - ## Where to go next -- [Networking & Previews](/agentos/docs/networking): the `httpRequest` and preview URL API, with usage examples. +- [Networking](/agentos/docs/networking): the `httpRequest` and `fetch` API, with usage examples. - [JavaScript Executor & Socket Reactor](/agentos/docs/architecture/javascript-executor): how Tokio readiness wakes V8, how the channel lanes are separated, and how Node stream backpressure reaches the transport. - [Architecture](/agentos/docs/architecture): the client / sidecar / executor trust boundary this stack lives inside. - [Security Model](/agentos/docs/security-model): the full in-scope and out-of-scope threat model. diff --git a/docs/content/docs/architecture/packages-and-command-resolution.mdx b/docs/content/docs/architecture/packages-and-command-resolution.mdx index eb39fe51ae..f390936d0e 100644 --- a/docs/content/docs/architecture/packages-and-command-resolution.mdx +++ b/docs/content/docs/architecture/packages-and-command-resolution.mdx @@ -52,15 +52,15 @@ The shared Core package-source model is closed: - `Url { url, expected_digest? }` downloads a remote artifact under strict size, time, and redirect limits; -- `Path { path, expected_digest? }` reads a trusted local artifact and is - available only to embedded Core callers. +- `Path { path, expected_digest? }` reads a trusted local artifact from the + host. -The hosted actor accepts only `Url`. It cannot construct a host path, mount a -package from the host filesystem, or receive a custom resolver callback. +There is no custom resolver callback. A host that installs packages on behalf +of untrusted users should accept only `Url` sources from them. Both variants verify the bytes before publishing them into the same process-local content-addressed cache. Cache identity is the content digest, -not the URL. Installed packages are pinned while a VM generation uses them and +not the URL. Installed packages are pinned while a VM uses them and otherwise participate in bounded LRU eviction. ## Command execution diff --git a/docs/content/docs/architecture/processes.mdx b/docs/content/docs/architecture/processes.mdx index ee281ea1d3..996ccdafa0 100644 --- a/docs/content/docs/architecture/processes.mdx +++ b/docs/content/docs/architecture/processes.mdx @@ -7,9 +7,9 @@ skill: true These internal architecture docs are mostly generated and maintained by LLMs, then reviewed by humans. They are intentionally verbose; use your preferred LLM to ask focused questions about the architecture as needed. This page is an internals deep-dive on the kernel's **process model**: the data -structures and syscall paths behind every guest process. For the client-facing -hosted API (`process.run`, `process.spawn`, `terminal.open`, lifecycle, the process tree), see -[Processes & Shell](/agentos/docs/processes). For the surrounding component and trust +structures and syscall paths behind every guest process. For the host-facing +API (`process.exec`, `process.spawn`, `terminal.open`, lifecycle, the process tree), see +[Processes & Shells](/agentos/docs/processes). For the surrounding component and trust model, see [Architecture](/agentos/docs/architecture). Two invariants frame everything below: @@ -31,22 +31,22 @@ inside that VM; nothing in it corresponds to a host PID. ## How a spawn is serviced -A spawn, whether it originates from a client `spawn`/`run` call or from guest +A spawn, whether it originates from a host `spawn`/`exec` call or from guest `node:child_process`, follows one path through the kernel: -1. **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. +1. **The request crosses into the kernel.** A host call arrives over the sidecar protocol; a guest call arrives as a syscall from the executor. Either way the kernel, not the caller, performs the work. 2. **Permission check.** The kernel applies the VM's configured permission policy before doing anything. The policy is trusted input; the guest making the request is not. 3. **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. 4. **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). -5. **Start execution.** The driver begins running the program. For a one-shot `run`, output and exit status are collected for the call's result; for `spawn`, the process remains available for explicit waiting and output replay. -6. **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 `process.wait` resolves against and what the `process.exit` event reports. +5. **Start execution.** The driver begins running the program. For a one-shot `exec`, output and exit status are collected for the call's result; for `spawn`, the process remains available for explicit waiting and output replay. +6. **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 `process.wait` resolves against and what `onProcessExit` reports. Signals (`process.signal`) are the same shape: a request into the kernel, which applies it to the virtualized process rather than to any host process. ## Process image replacement -Guest `execve` is distinct from the hosted one-shot `process.run` API. It +Guest `execve` is distinct from the host's one-shot `process.exec` API. It replaces the current process image in place, following Linux process semantics: - the virtual PID, parent, process group, session, working directory, pending signals, blocked-signal mask, stdio, and non-`FD_CLOEXEC` descriptors survive; @@ -67,8 +67,8 @@ Standard streams are kernel-owned objects, not host file descriptors. Each process entry has stdin, stdout, and stderr endpoints that the kernel wires up when the entry is created. -- **Capture vs. stream.** `process.run` returns bounded stdout and stderr after the process exits. For `process.spawn`, the hosted actor exposes `process.output` events with generation-scoped process handles and bounded `process.output.read` replay; `process.exit` signals completion. Process and terminal replay use the same sidecar-owned retention and cursor implementation, so `maxEvents`, `maxBytes`, truncation, and progress behavior are identical across Rust Core and TypeScript Core. Events are low-latency hints; replay is the authoritative recovery path. -- **Writable stdin.** `process.stdin.write` pushes bytes into the process's stdin endpoint; `process.stdin.close` closes the write side so programs that read to EOF (like `cat`) can finish. None of this touches a real pipe on the host. +- **Capture vs. stream.** `process.exec` returns bounded stdout and stderr after the process exits. For `process.spawn`, `onProcessOutput` delivers live output and, when output retention is enabled, `process.readOutput` pages through bounded replay; `onProcessExit` signals completion. Replay uses a sidecar-owned retention and cursor implementation, so `maxEvents`, `maxBytes`, truncation, and progress behavior are identical across Rust Core and TypeScript Core. Events are low-latency hints; replay is the authoritative recovery path. +- **Writable stdin.** `process.writeStdin` pushes bytes into the process's stdin endpoint; `process.closeStdin` closes the write side so programs that read to EOF (like `cat`) can finish. None of this touches a real pipe on the host. - **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. Because these endpoints are kernel objects, the same bridging works identically @@ -81,9 +81,9 @@ An interactive shell needs a terminal, not just piped stdio: line editing, job control signals, and window size all depend on a PTY. The kernel provides virtual PTY devices for this. -- **A shell is a process plus a PTY.** `terminal.open` allocates a kernel PTY and starts a shell process attached to it, returning a generation-scoped terminal handle. The PTY is a virtualized terminal device, never a host `/dev/pts` entry. -- **Bidirectional terminal I/O.** `terminal.stdin.write` feeds keystrokes into the PTY master side; output is available through `terminal.output` events and `terminal.output.read` replay. This carries terminal control sequences, so full-screen TUIs behave correctly. -- **Resize is a terminal operation.** `terminal.pty.resize` 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. +- **A shell is a process plus a PTY.** `terminal.open` 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. +- **Bidirectional terminal I/O.** `terminal.write` feeds keystrokes into the PTY master side; output arrives through `onShellData`. This carries terminal control sequences, so full-screen TUIs behave correctly. +- **Resize is a terminal operation.** `terminal.resize` 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. - **Teardown.** `terminal.close` 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. ## WASM sh and coreutils on the process model diff --git a/docs/content/docs/architecture/software-cache.mdx b/docs/content/docs/architecture/software-cache.mdx new file mode 100644 index 0000000000..67f8caaf99 --- /dev/null +++ b/docs/content/docs/architecture/software-cache.mdx @@ -0,0 +1,63 @@ +--- +title: "Software Installation and Preloading" +description: "Understand URL package installation, process caching, readiness, and preload coordination for hosted agentOS VMs." +--- + +Each VM projects installed packages as read-only filesystem leaves. The +projection is bounded by `config.limits.agentosPackages.maxMounts` (4,096 by +default), including command aliases and `provides.files` directory mounts. +Installing another package can fail with a resource-limit error even when the +download succeeded; the existing VM projection remains in place. + +Hosted VMs install immutable `.aospkg` files from HTTPS URLs. Embedded Core +also accepts a trusted local file path. A hosted VM never receives or +materializes a host path. + +## Resolution and projection + +The resolver validates the URL, blocks ambient credentials and private network +destinations, bounds redirects and response headers, downloads into a temporary +file, verifies the optional SHA-256 digest, and validates the package format. +The verified content digest becomes the package identity. + +The sidecar projects installed packages under +`/opt/agentos/pkgs//` and command links under +`/opt/agentos/bin`. `v1.software.list` reads the current installed set. +`v1.software.install` and `v1.software.uninstall` are durable config mutations, +so they use the same revision checks and reconciliation as `v1.config.patch`. + +## Process cache + +All VMs in one worker process share the child sidecar's content-addressed +cache. Concurrent requests for the same digest are coalesced into one +acquisition. The cache is bounded by bytes, entries, active downloads, pending +downloads, and acquisition time. Live VM generations pin their package files. +Unpinned entries are evicted in least-recently-used order. + +By default, each worker gets an isolated process-lifetime temporary cache. For +persistent recovery, set a distinct `AGENTOS_PACKAGE_CACHE_DIR` for each +concurrently running worker. An OS lock prevents startup recovery or eviction +from removing another worker's live package files. Restarting a worker with the +same directory recovers verified packages. + +The services worker removes its temporary cache after normal shutdown or +startup failure. Forced termination or a host crash can leave temporary +directories behind; configure host temporary-directory cleanup for those +leftovers. Persistent directories are never removed at worker shutdown. + +Partial downloads and failed publications use temporary files that are removed +when their operation is cancelled or fails. Package source URLs are kept only +as a short-lived bounded hint. Bytes are still verified before projection. + +## Startup preloading + +The `agentOS-preload-coordinator` is a central Rivet Actor that stores a +low-frequency popularity hint. It is not an authoritative registry or cache. +Each worker process reads one bounded plan at startup, coalesces that read for +the process, and warms likely package digests with bounded concurrency. The +default process warm deadline is 10 seconds. + +Required packages still determine VM readiness. An advisory preload timeout or +coordinator failure only reduces the cache hit rate. + +See [Software](/agentos/docs/software#install-while-running) for the installation API. diff --git a/docs/content/docs/bash.mdx b/docs/content/docs/bash.mdx index a7fc5ba08e..61bbeda8e9 100644 --- a/docs/content/docs/bash.mdx +++ b/docs/content/docs/bash.mdx @@ -9,54 +9,125 @@ Run bash inside the VM. Shell commands are the simplest agentOS execution surface, and share the same execution lifecycle as JavaScript, TypeScript, Python, and package workflows. -This page uses embedded Core. For hosted actors, use `process.run` with an -explicit executable and argument array, or run `sh` with `-c` for shell parsing; -see [Processes & Terminals](/agentos/docs/processes). - -- **`process.exec()`**: Runs a bash command line. Use for pipes, redirects, globs. -- **`process.execFile()`**: Injection-safe; args are never parsed by a shell. +- **Hosted `v1.process.run`** takes an executable and an argument array. Run + `sh` with `-c` for pipes, redirects, and globs. +- **Library `process.exec()`** runs a bash command line, and + **`process.execFile()`** runs an executable whose arguments are never parsed + by a shell. - **In-VM only**: Commands run inside the VM, never in the host shell. For multi-step work, consider [JavaScript](/agentos/docs/javascript) or -[Python](/agentos/docs/python) instead — one round trip and real data structures -rather than a chain of shell calls. +[Python](/agentos/docs/python) instead. One call returns real data structures +instead of a chain of shell calls. ## Run commands - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +// Pipes, redirects, and globs need a shell, so run `sh -c`. +const piped = await vm.v1.process.run({ + command: "sh", + args: ["-c", "echo 'hello world' | tr a-z A-Z"], + options: { captureStdio: true }, +}); +console.log(piped.stdout.trim()); // "HELLO WORLD" + +// Arguments are passed verbatim, so nothing is parsed by a shell. +const raw = await vm.v1.process.run({ + command: "echo", + args: ["$(whoami)"], + options: { captureStdio: true }, +}); +console.log(raw.stdout.trim(), raw.status.exitCode); // "$(whoami)" 0 +``` + + + + ## Background and interactive work -`spawn` starts a long-lived process and returns a `pid`. From there you get +`spawn` starts a long-lived process and returns a handle. From there you get stdin, a PTY, output replay, signals, and waiting. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const shell = await vm.v1.process.spawn({ command: "sh", args: [] }); + +await vm.v1.process.stdin.write({ process: shell, data: "echo from the background\n" }); +await vm.v1.process.stdin.close({ process: shell }); + +await vm.v1.process.wait({ process: shell }); +const replay = await vm.v1.process.output.read({ process: shell }); +console.log(replay.events.length); +``` + + + + ## Files and software -Shell commands see the persistent [filesystem](/agentos/docs/filesystem) shared by +Shell commands see the same [filesystem](/agentos/docs/filesystem) as JavaScript and Python. Common POSIX commands ship by default; more is projected through the [software registry](/agentos/docs/software). - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +await vm.v1.process.run({ + command: "sh", + args: ["-c", "printf 'a\\nb\\nc\\n' > /workspace/out.txt"], +}); + +const lines = await vm.v1.process.run({ + command: "sh", + args: ["-c", "cat /workspace/out.txt | wc -l"], + options: { captureStdio: true }, +}); +console.log(lines.stdout.trim()); // "3" +``` + + + + ## Host functions -[Host functions](/agentos/docs/host-functions) appear as commands, so pipelines use trusted host -capabilities without putting credentials inside the VM. +[Host functions](/agentos/docs/host-functions) appear as commands, so pipelines +use trusted host capabilities without putting credentials inside the VM. Host +functions are library-only. - + ## Permissions, limits, and timeouts Every command inherits the VM [permission policy](/agentos/docs/permissions) and [resource limits](/agentos/docs/resource-limits). - + ## Embedded API -The examples above already use `AgentOs.create()`. The host owns `dispose()`, -and filesystem state only outlives the VM when you configure `database`. +The library examples use `AgentOs.create()`. The host owns `dispose()`, and +files only outlive the VM when its root filesystem is stored in SQLite; see +[Persistence](/agentos/docs/persistence). Read more in the [embedded API quickstart](/agentos/docs/quickstart-embedded). diff --git a/docs/content/docs/browser.mdx b/docs/content/docs/browser.mdx index f9e847447c..7bda693bfd 100644 --- a/docs/content/docs/browser.mdx +++ b/docs/content/docs/browser.mdx @@ -3,10 +3,13 @@ title: "Browser Automation" description: "Run a packaged browser CLI as ordinary agentOS software." --- -Browser automation is ordinary VM software. Build the CLI into an immutable -`.aospkg`, host it at an HTTPS URL, and include that source in actor creation -config. - +Browser automation is ordinary VM software. Install a browser CLI package, such +as Browserbase from the [registry](/agentos/docs/registry), at boot or with +`software.install`, then run it like any other command. See +[Software](/agentos/docs/software). The guest receives only the credentials explicitly passed to the process -environment. The actor does not provide a browser-specific API. +environment. agentOS does not provide a browser-specific API. + +For workloads that need a full local browser, mount an +[external sandbox](/agentos/docs/sandboxes) and run the browser there. diff --git a/docs/content/docs/cron.mdx b/docs/content/docs/cron.mdx index 3533cd77c7..2337e04261 100644 --- a/docs/content/docs/cron.mdx +++ b/docs/content/docs/cron.mdx @@ -1,12 +1,68 @@ --- -title: "Crons & Loops" -description: "Schedule recurring VM commands while an embedded VM is active." +title: "Crons & Schedules" +description: "Schedule recurring and one-shot VM commands." skill: true --- -Core provides an in-process cron helper. Jobs run while the embedding process -and VM are alive; a hosting service is responsible for durable scheduling. +Schedule commands to run inside the VM on a cron expression. Hosted VMs store +their jobs with the VM. Library jobs run while the embedding process and the VM +are alive, so durable scheduling across process restarts is up to the +application that owns the VM. ## Schedule a command - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const job = await vm.v1.cron.schedule({ + name: "cleanup", + expression: "0 * * * *", + command: "rm", + args: ["-rf", "/tmp/cache"], + options: { env: {} }, +}); +console.log("Scheduled:", job.name); + +const conn = vm.connect(); +await conn.ready; +conn.on("cron.fired", (event) => { + console.log("Cron event:", event); +}); + +for (const scheduled of await vm.v1.cron.list({})) { + console.log(scheduled.name, scheduled.expression); +} +``` + + + + + +Hosted jobs are identified by `name` and run a command with arguments. A hosted +VM holds up to 1,024 jobs, with names and expressions up to 256 bytes, and +retains 32 runs per job by default (256 maximum). + +In the library, `schedule` takes a standard cron expression or an ISO 8601 +timestamp for a one-shot job. `action` is either `{ type: "exec", command, args }`, +which runs a command in the VM, or `{ type: "callback", fn }`, which calls a +function in the host process. `overlap` controls what happens when a job fires +while its previous run is still going: `"allow"` (the default) starts another +run, `"skip"` drops the new run, and `"queue"` waits for the previous run to +finish. + +## Inspect and cancel jobs + +| Operation | Hosted | Library | +|---|---|---| +| List jobs | `v1.cron.list` | `cron.list()`, with `lastRun`, `nextRun`, `runCount`, and `running` | +| Cancel a job | `v1.cron.cancel({ name })` | `cron.cancel(id)`, or `cancel()` on the returned job | +| Run events | `cron.fired` | `vm.onCronEvent` with `cron:fire`, `cron:complete`, and `cron:error` | + +In the library, pass `scheduleDriver` to `AgentOs.create()` to replace the +default timer-based driver, for example with one backed by your own durable +scheduler. diff --git a/docs/content/docs/custom-software/definition.mdx b/docs/content/docs/custom-software/definition.mdx index 4e67aa9abd..230ce4acb5 100644 --- a/docs/content/docs/custom-software/definition.mdx +++ b/docs/content/docs/custom-software/definition.mdx @@ -20,6 +20,6 @@ payload. Commands are projected under `/opt/agentos/pkgs//` and linked under `/opt/agentos/bin`. The sidecar reads the package projection live. -The package format contains no hosted action, callback, or host-path metadata. -The hosted actor accepts an HTTPS URL and optional exact digest; embedded Core -also accepts a trusted local path. +The package format contains no callback or host-path metadata. Install a package +from an HTTPS URL or a trusted local path, each with an optional exact digest; +see [Software](/agentos/docs/software). diff --git a/docs/content/docs/custom-software/publishing.mdx b/docs/content/docs/custom-software/publishing.mdx index 47111750e8..a4888d6010 100644 --- a/docs/content/docs/custom-software/publishing.mdx +++ b/docs/content/docs/custom-software/publishing.mdx @@ -22,6 +22,15 @@ Run the credential-free release staging path locally: just software-artifacts-dry-run ``` -The release workflow uploads the same directory through the configured -S3-compatible endpoint under immutable commit and version prefixes. Consumers -pass the resulting artifact URL and digest directly to `software.install`. +The release workflow uploads the same directory under immutable commit and +version prefixes, and copies the newest release to `latest`: + +```text +https://releases.rivet.dev/agent-os//software/manifest.json +https://releases.rivet.dev/agent-os/latest/software/manifest.json +``` + +The same directory also holds `default-software.json`, the manifest of the +packages every VM installs by default. Consumers pass an artifact URL and +digest from the manifest directly to `software.install`; see +[Software](/agentos/docs/software#install-while-running). diff --git a/docs/content/docs/embedded.mdx b/docs/content/docs/embedded.mdx index 50e5f87045..4f36ebaee4 100644 --- a/docs/content/docs/embedded.mdx +++ b/docs/content/docs/embedded.mdx @@ -4,26 +4,31 @@ description: "Embed agentOS Core in a trusted Node.js application." skill: true --- -`@rivet-dev/agentos-core` runs the same VM runtime without Rivet actor -lifecycle. Your application owns VM identity, persistence, and disposal. -Its native sidecar and platform package must match the installed agentOS -version. If resolution reports a missing or mismatched binary, reinstall the -packages; `AGENTOS_SIDECAR_BIN` can select a custom local build. +`@rivet-dev/agentos-core` runs agentOS VMs inside your Node.js process. Your +application owns VM identity, persistence, and disposal. Its native sidecar and +platform package must match the installed agentOS version. If resolution +reports a missing or mismatched binary, reinstall the packages; +`AGENTOS_SIDECAR_BIN` can select a custom local build. -## Embedded-only capabilities +## Trusted host capabilities -Because the embedding process is trusted, Core can accept values that cannot -cross the hosted actor boundary: +Because the embedding process is trusted, Core accepts values that run in or +read from the host: -- host bindings; -- host-directory and callback-backed mount plugins; -- external VM providers; -- trusted local `.aospkg` paths; +- [host functions](/agentos/docs/host-functions); +- host-directory and other [mount plugins](/agentos/docs/filesystem#mounts); +- [external sandboxes](/agentos/docs/sandboxes); +- local `.aospkg` paths; - custom schedule drivers and sidecar placement. -The hosted actor uses a smaller generated configuration type and a fixed -filesystem registry. It never serializes callbacks or exposes a host path. +## Sidecar placement + +VMs share a pooled sidecar process by default. Create a dedicated sidecar with +`AgentOs.createSidecar()` and pass it as `sidecar: { kind: "explicit", handle }` +to isolate a group of VMs or configure process-wide limits. + + diff --git a/docs/content/docs/filesystem.mdx b/docs/content/docs/filesystem.mdx index dd55ebf459..f72f7b23dd 100644 --- a/docs/content/docs/filesystem.mdx +++ b/docs/content/docs/filesystem.mdx @@ -1,25 +1,109 @@ --- title: "Filesystem" -description: "Read and write VM files and configure safe durable filesystems." +description: "Read and write VM files, and mount durable storage, host directories, S3, and other filesystem backends." --- -The generated actor client exposes file operations under `filesystem`. -`filesystem.stat` and `filesystem.readdirRecursive` report file lengths as -`sizeBytes`. +Every language and shell in the VM sees the same filesystem, so files written +here are immediately visible to Bash, JavaScript, Python, and installed +software. - + -The actor's filesystem configuration is a closed registry. By default, each VM -uses an ephemeral in-memory overlay over the bundled base image. Its writes -survive for that VM's lifetime but are discarded when the VM is replaced. Use -a `durable` root or mount when files must survive VM replacement. Unknown -backends, JavaScript VFS objects, callbacks, and host paths are rejected. +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); -Host-directory, S3, Google Drive, and custom plugins remain available only in -trusted embedded Core: +await vm.v1.filesystem.writeFile({ + path: "/home/agentos/hello.txt", + content: "Hello, world!", +}); +const content = await vm.v1.filesystem.readFile({ + path: "/home/agentos/hello.txt", +}); +console.log(new TextDecoder().decode(content)); - +await vm.v1.filesystem.mkdir({ path: "/home/agentos/src", recursive: false }); +await vm.v1.filesystem.writeFiles({ + entries: [ + { path: "/home/agentos/src/index.ts", content: "console.log('hi');" }, + { + path: "/home/agentos/src/utils.ts", + content: "export const add = (a: number, b: number) => a + b;", + }, + ], +}); -Large-file streaming is a follow-up. Current actions enforce bounded payloads -and fail with a typed limit error. +const entries = await vm.v1.filesystem.readdirRecursive({ + path: "/home/agentos", + exclude: [], +}); +for (const entry of entries) { + console.log(entry.type, entry.path); +} +``` + + + + + +Both APIs also provide batch `readFiles`, `stat`, `readdir`, `readdirEntries`, +`exists`, `move`, `remove`, `export` for a snapshot of the root filesystem, and +`listMounts`. Hosted VMs report byte counts in `stat` and `readdirRecursive` as +`sizeBytes`; the library reports them as `size`. The library can also `mount` +and `unmount` filesystems on a running VM. + +## Root filesystem + +By default, each VM uses an in-memory overlay over the bundled base image. +Writes survive for the VM's lifetime and are discarded when the VM is replaced +or disposed. See [Persistence](/agentos/docs/persistence) to keep files across +VM recreation. + +## Mounts + +Mount additional filesystem backends at any guest path. + +Hosted VMs resolve filesystem configuration through a closed registry, such as +`durable` storage. Unknown backends, JavaScript VFS objects, callbacks, and host +paths are rejected. The library accepts native mount plugins, each identified +by an `id` and a `config` object: + +| Plugin | Backend | +|---|---| +| `host_dir` | A directory on the host, confined to the configured path | +| `chunked_s3` | An S3 or S3-compatible bucket | +| `chunked_sqlite` | The VM's SQLite `database` | +| `google_drive` | A Google Drive folder | + + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); + +// Mount durable storage at /data when the VM is first created. +const vm = client.agentOS.getOrCreate(["my-vm"], { + createWithInput: { + config: { + filesystem: { + mounts: [{ path: "/data", backend: { type: "durable" } }], + }, + }, + }, +}); +``` + + + + + +Host-backed mounts are confined to their configured root on every operation, +including symlink and `..` resolution. `nodeModulesMount()` is a library +shortcut that exposes a host `node_modules` tree read-only at +`/root/node_modules`. + +File transfers are bounded. An operation that exceeds a payload limit fails +with a typed limit error that names the limit. diff --git a/docs/content/docs/host-functions.mdx b/docs/content/docs/host-functions.mdx index 697e979405..819abe25e7 100644 --- a/docs/content/docs/host-functions.mdx +++ b/docs/content/docs/host-functions.mdx @@ -1,20 +1,16 @@ --- title: "Custom Host Functions" -description: "Expose trusted host functions to agentOS coding agents as typed CLI commands, with scoped input, output, and execution behavior." +description: "Expose trusted host functions to guest programs in agentOS VMs as typed CLI commands and JavaScript globals." skill: true --- -Expose your host JavaScript functions (defined with Zod input schemas) to agents as auto-generated CLI commands installed at `/bin/agentos-{name}` inside the VM, injected into the agent's [system prompt](/agentos/docs/system-prompt) and callable inside scripts for code-mode token savings. +Host functions expose JavaScript functions from your process to code running inside the VM. They run as in-process callbacks, so they are available in the library only, not on hosted VMs. Each function is defined with a Zod input schema and becomes an auto-generated CLI command at `/bin/agentos-{name}`, plus an async function that inline JavaScript can call directly. ## Getting started -Pass `agentOS({ hostFunctions })` a record of collections. The keys name everything: the collection key becomes the CLI binary `agentos-{name}` and a guest global, and each function key becomes a subcommand. A function needs only an `inputSchema` and an `execute` handler. `.describe()` on the schema is what the agent reads. +Pass `AgentOs.create({ hostFunctions })` a record of collections. The keys name everything: the collection key becomes the CLI binary `agentos-{name}` and a guest global, and each function key becomes a subcommand. A function needs only an `inputSchema` and an `execute` handler. `.describe()` on the schema becomes the command's help text. - - - - - + Each host function can override its `timeout` in milliseconds. The default is 30 seconds and can be changed with `limits.hostFunctions.defaultTimeoutMs`. @@ -32,11 +28,9 @@ Zod schema fields are converted to CLI flags automatically. Field names are conv Optional fields (via `.optional()`) become optional flags. Required fields are enforced at validation time. Use `.describe()` on Zod fields to generate useful `--help` output, and `.describe()` on the whole schema to describe the function itself. -### What the agent sees - -When host functions are registered, CLI shims are installed at `/bin/agentos-{name}` inside the VM and the function list is injected into the agent's [system prompt](/agentos/docs/system-prompt), so keep each schema's `.describe()` concise to save tokens. +### Calling host functions from the shell -The agent interacts with host functions as shell commands: +Guest programs call host functions as shell commands: ```bash # List all available host-function collections @@ -85,30 +79,21 @@ violation, a thrown error, or a timeout. A collection whose name is already a global, such as `process`, is not defined, and a warning is written to stderr. The `hostFunction` permission scope applies exactly as it does to the commands. -## Host functions and MCP servers - -agentOS supports two ways to give agents access to external functionality: **host functions** and session-scoped **MCP servers**. Both work, but they have different tradeoffs. - -| | Host functions | MCP servers | -|---|---|---| -| **How it works** | Call JavaScript functions on the host directly | Connect to a standard MCP server | -| **Authentication** | None required. Calls go directly to the host process. | Requires custom auth configuration per server | -| **Code mode** | Built in. Host functions 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 | -| **Latency** | Near-zero. Bound directly to the host process. | Extra network hop to reach the MCP server | -| **Setup** | Define host functions in your actor code with Zod schemas | Configure any standard MCP server | - -Use host functions 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](/agentos/docs/sessions#open-a-session) for MCP server configuration. +See [Bash](/agentos/docs/bash#host-functions), +[Node.js](/agentos/docs/javascript#host-functions), and +[Python](/agentos/docs/python#host-functions) for calling host functions from +each language. ## Security -Host-function calls from the agent 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, only the function's input/output contract. +Host function calls from guest code 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. Guest code never sees the credentials, only the function's input and output contract. -Host functions run on the host with full access to the host environment, so do not expose functions that could compromise the host without appropriate safeguards. +Because handlers run with full host access, do not expose functions that could compromise the host without appropriate safeguards. Use the `hostFunction` [permission](/agentos/docs/permissions) scope to restrict which collections guest code may call. ## Embedded API -Pass the same host-function collections to `AgentOs.create()`. The function contract and -generated VM commands are unchanged. +Host functions are passed to `AgentOs.create()` and their handlers run in the +embedding process. diff --git a/docs/content/docs/hosted-api.mdx b/docs/content/docs/hosted-api.mdx new file mode 100644 index 0000000000..792d1320ce --- /dev/null +++ b/docs/content/docs/hosted-api.mdx @@ -0,0 +1,257 @@ +--- +title: "Hosted HTTP API" +description: "Reference for the hosted agentOS VM API: creating VMs, authentication, actions, configuration, lifecycle, events, and errors." +--- + +Each hosted VM is one instance of the prebuilt `agentOS` Rivet Actor. You +create it by key, then call its actions over the Rivet gateway. The +`@rivet-dev/agentos` TypeScript client is generated from the same contract and +calls the same actions. See the [hosted quickstart](/agentos/docs/quickstart-hosted) +for a walkthrough. + +## Overview + +| Field | Value | +|---|---| +| **Base URL** | `https://api.rivet.dev`, or your control plane when [self-hosting](/agentos/docs/self-hosting) | +| **Actor name** | `agentOS` | +| **Create a VM** | `PUT /actors?namespace={namespace}` | +| **Call an action** | `POST /gateway/{vm_id}/action/v1.{group}.{action}` | +| **Request body** | `{ "args": [input] }` | +| **Response body** | `{ "output": result }` | +| **Authentication** | `Authorization: Bearer` to create VMs and tokens; `x-rivet-token` for actions | + +## Create a VM + +```bash HTTP +curl -X PUT "https://api.rivet.dev/actors?namespace=$RIVET_NAMESPACE" \ + -H "Authorization: Bearer $RIVET_TOKEN" \ + --json '{ + "name": "agentOS", + "key": "my-vm", + "runner_name_selector": "services", + "crash_policy": "sleep" + }' +# → { "actor": { "actor_id": "…" }, "created": true } +``` + +The returned `actor_id` is the VM ID used in every action path. A second request +with the same key returns the existing VM. + +The TypeScript client does the same with `getOrCreate`, and can pass creation +input on first creation: + +```ts TypeScript @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); + +const vm = client.agentOS.getOrCreate(["my-vm"], { + createWithInput: { config: { environment: { CI: "true" } } }, +}); +const vmId = await vm.resolve(); +``` + +## Authentication + +Keep your admin token on the backend. To let a browser or other untrusted +client call one VM directly, issue a JWT scoped to that VM. See +[JWTs](/docs/jwt). + +```bash HTTP +curl -X POST "https://api.rivet.dev/auth/tokens" \ + -H "Authorization: Bearer $RIVET_TOKEN" \ + --json @- < { + console.log(data.process.pid, data.stream, new TextDecoder().decode(data.data)); +}); +conn.on("process.exit", (data) => { + console.log(data.process.pid, "exited with code", data.status.exitCode); +}); +``` + +## Errors + +| Code | Cause | +| --- | --- | +| `stale_generation` | A process, terminal, context, or stream handle from before a `v1.vm.restart` was used. | +| `expired` | A fetch-stream handle expired. | +| `timeout` | A required package-resolution deadline passed. | +| `not_found` | A closed terminal's replay was evicted from the bounded retention window. | + +Final terminal output and its confirmed exit remain readable after exit within +the bounded closed-terminal retention window. + +## Contract compatibility + +The `@rivet-dev/agentos` package exports `AGENTOS_API_VERSION`, +`AGENTOS_CONTRACT_MAJOR`, `AGENTOS_CONTRACT_HASH`, and `AGENTOS_CAPABILITIES`. +Rust DTOs and the action registry generate the TypeScript surface. + +The contract is currently `agentos-sdk.dev/v1alpha1`, major `1`. The hash covers +the canonical public actions, events, DTOs, and capabilities. Private cron and +maintenance actions are excluded from both the client and the hash. + +## Inspector + +The Rivet Inspector shows a hosted VM's filesystem, processes, terminals, +software, previews, configuration, status, and limit warnings through the same +public contract. It can restart the VM but cannot edit host mounts. + +Published services binaries and the services container include the Inspector +tabs. A source or custom deployment can point the service at another built +copy: + +```sh +AGENTOS_INSPECTOR_TABS_DIR=node_modules/@rivet-dev/agentos/assets/inspector-tabs-app \ + rivet-services start +``` diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx index 4392d21a09..2589e907b1 100644 --- a/docs/content/docs/index.mdx +++ b/docs/content/docs/index.mdx @@ -7,7 +7,10 @@ agentOS provides isolated filesystem, process, terminal, language, and networkin APIs in a lightweight VM. - + + Create durable VMs by ID and control them over RivetKit or HTTP. + + Run a VM inside a trusted Node.js backend. @@ -15,5 +18,14 @@ APIs in a lightweight VM. -Core owns the VM process, lifecycle, and host bindings. Managed orchestration -adapters are separate consumers of the same runtime APIs. +Run agentOS two ways: + +- **Hosted:** the prebuilt `agentOS` Rivet Actor gives each VM a durable ID, + versioned configuration, replayable output, and expiring previews, controlled + from `@rivet-dev/agentos` or plain HTTP. +- **Library:** `@rivet-dev/agentos-core` runs VMs inside your Node.js backend. + Your application owns the VM's lifecycle, persistence, and access to host + resources such as [host functions](/agentos/docs/host-functions) and + [mounts](/agentos/docs/filesystem#mounts). + +See [which one to use](/agentos/docs/quickstart-hosted#which-one-should-i-use). diff --git a/docs/content/docs/javascript.mdx b/docs/content/docs/javascript.mdx index e4e6e1beae..4444772178 100644 --- a/docs/content/docs/javascript.mdx +++ b/docs/content/docs/javascript.mdx @@ -20,7 +20,21 @@ driving [Bash](/agentos/docs/bash): `evaluate()` returns a JSON-serializable value. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const sum = await vm.v1.javascript.evaluate({ expression: "1 + 2" }); +console.log(sum.value); // 3 +``` + + + + Returning `undefined`, a function, a symbol, or a circular value fails with `evaluation_serialization_failed` rather than silently losing the value. @@ -29,27 +43,43 @@ Returning `undefined`, a function, a symbol, or a circular value fails with `execute()` runs source and captures its output instead of returning a value. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); -Executions are ephemeral, so capture stdio only when you want it — `"stderr"` for +const result = await vm.v1.javascript.execute({ + source: `console.log("hello from agentOS")`, +}); +console.log(result); +``` + + + + + +Executions are ephemeral, so capture stdio only when you want it: `"stderr"` for diagnostics, `"all"` for both streams. `onStdout`/`onStderr` stream live and work independently of capture. ## Pass data into code -`inputs` hands host values to the guest as real objects, so data never gets +In the library, `inputs` hands host values to the guest as real objects, so data never gets interpolated into source. - + ## Keep state between calls Pass a `contextId` to keep one V8 isolate and its global state alive across calls. - + -A context runs one operation at a time — reusing a busy `contextId` fails +A context runs one operation at a time, so reusing a busy `contextId` fails immediately. Files, npm, Bash, and type checks may pass the same id, but they run in fresh processes and never touch retained memory. @@ -63,19 +93,50 @@ Create the context explicitly before use; an unknown id fails instead of silentl starting fresh state. A context pins to the first inline language that used it, though JavaScript and TypeScript intentionally share one isolate. -Contexts live for the VM lifetime. They do not survive actor sleep/wake, because -the VM is disposed; create them lazily on the first stateful use after wake. +Contexts live until the VM is disposed. They are in-memory state, so they do +not survive a library VM recreated from the same `database`, or a hosted VM's +sleep and wake. Create them lazily on first use. ## Type check before running Executing TypeScript transpiles it without a semantic check, so validate generated code explicitly. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +// Type checking validates generated code before it executes. +const checked = await vm.v1.typescript.check({ + source: `const total: number = "not a number";`, +}); +console.log(checked); +``` + + + + ## Install npm packages - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +await vm.v1.javascript.npm.install({}); +``` + + + + Installs modify the VM-wide filesystem, so a package installed once is importable by every later execution in that VM, in any language. Only one npm/Python @@ -83,10 +144,37 @@ mutation runs at a time per VM; a concurrent install fails with `execution_busy` ## Background processes and web servers -`spawn` starts a long-lived process and returns a `pid`. From there you get +`spawn` starts a long-lived process and returns a handle. From there you get stdin, output, signals, and waiting. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const server = await vm.v1.javascript.spawn({ + source: ` + import http from "node:http"; + http.createServer((req, res) => res.end("ok")).listen(3000, "127.0.0.1"); + await new Promise(() => {}); + `, +}); + +// The listener stays inside the VM. No host port is exposed. +const response = await vm.v1.network.fetch({ + request: { port: 3000, path: "/health" }, +}); +console.log(response.status); // 200 + +await vm.v1.process.signal({ process: server, signal: "SIGTERM" }); +``` + + + + Spawned processes always start with fresh state, so they take no `contextId`. @@ -96,29 +184,28 @@ There is a real Linux environment behind all of this, shared by every language. Files and installed packages are immediately visible to Bash, Python, and other executions. - + See [Filesystem](/agentos/docs/filesystem), [Processes & Shells](/agentos/docs/processes), and -[Networking & Previews](/agentos/docs/networking). +[Networking](/agentos/docs/networking). ## Host functions -Guest code invokes [host functions](/agentos/docs/host-functions) as ordinary typed commands, so host -credentials stay outside the VM. +Guest JavaScript calls [host functions](/agentos/docs/host-functions) as async globals, so host +credentials stay outside the VM. Host functions are library-only. - + ## Permissions, limits, and timeouts Every operation inherits the VM [permission policy](/agentos/docs/permissions) and [resource limits](/agentos/docs/resource-limits). - + ## Embedded API -The examples above already use `AgentOs.create()`. Contexts remain available -until the host calls `dispose()`; there is no actor sleep cycle to recreate them -after. +The library examples use `AgentOs.create()`. Contexts remain available until +the host calls `dispose()`. Read more in the [embedded API quickstart](/agentos/docs/quickstart-embedded). diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index 42b33d6913..656b339593 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -10,10 +10,8 @@ overhead. ## Sandbox mounting -When a workload needs a full Linux OS, an embedded application can compose Core -with the [external sandbox](/agentos/docs/sandboxes) extension. That extension -uses trusted host bindings and mounts, so it is unavailable from the static -hosted actor. +When a workload needs a full Linux OS, mount an +[external sandbox](/agentos/docs/sandboxes) into the VM. See [agentOS vs Sandbox](/agentos/docs/versus-sandbox) for a detailed comparison. @@ -21,7 +19,7 @@ See [agentOS vs Sandbox](/agentos/docs/versus-sandbox) for a detailed comparison ### Software registry -agentOS uses its own [software registry](/agentos/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 [external sandbox](/agentos/docs/sandboxes/). +agentOS uses its own [software registry](/agentos/docs/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 [external sandbox](/agentos/docs/sandboxes). See [Software](/agentos/docs/software) for how to install and configure available packages. diff --git a/docs/content/docs/networking.mdx b/docs/content/docs/networking.mdx index 2f74cbad0c..5dfa1aaadc 100644 --- a/docs/content/docs/networking.mdx +++ b/docs/content/docs/networking.mdx @@ -1,12 +1,77 @@ --- title: "Networking & Previews" -description: "Proxy HTTP requests into VM ports and create expiring preview routes." +description: "Send HTTP requests to servers running inside an agentOS VM, expose them through expiring previews, and control guest network access." --- -`network.httpRequest` proxies one bounded HTTP request to a VM loopback port. -Streaming helpers provide bounded chunked response delivery. +Servers inside the VM listen on VM-local ports. They are not exposed on the +host network. Send a bounded HTTP request to a VM port and get the buffered +response back. - + -Core provides direct HTTP request methods. Public preview routing is owned by -the hosting service, not the VM runtime. +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +// Start a server inside the VM +await vm.v1.filesystem.writeFile({ + path: "/tmp/app.mjs", + content: 'import http from "http"; http.createServer((req, res) => res.end("hello")).listen(3000);', +}); +await vm.v1.process.spawn({ command: "node", args: ["/tmp/app.mjs"] }); + +// fetch reaches services running in the VM. +const response = await vm.v1.network.fetch({ + request: { port: 3000, path: "/", method: "GET", headers: {} }, +}); +console.log(response.status); +``` + + + + + +For large responses, hosted VMs stream the body with the pull-based +`v1.network.fetchStream.start`, `read`, and `cancel` actions. The library +provides `vm.fetch(port, request)` with standard `Request` and `Response` +objects, and `fetchStreamStart`, `fetchStreamRead`, and `fetchStreamCancel` for +bounded chunks. Either way, the caller owns backpressure. + +## Previews + +Hosted VMs can expose any port through an expiring preview. A preview is a +signed URL path that proxies HTTP to a port inside the VM until it expires or is +revoked. `config.preview.defaultTtlMs` sets the default lifetime. + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +await vm.v1.process.spawn({ command: "node", args: ["/home/agentos/app.js"] }); + +// Create a preview URL for port 3000, valid for 1 hour +const preview = await vm.v1.network.preview.create({ + port: 3000, + ttlMs: 60 * 60 * 1_000, +}); +console.log(preview.path, new Date(Number(preview.expiresAtMs))); + +// Revoke it early +await vm.v1.network.preview.expire({ token: preview.token }); +``` + +The library does not create public URLs. To expose a library VM's server to +browsers, route requests through your application and forward them with +`httpRequest` or `fetch`. + +## Guest network access + +Guest listeners and loopback traffic stay inside the VM. Outbound access is +limited to the sidecar's default allowlist unless the `network` +[permission](/agentos/docs/permissions) scope allows more. Requests from the +host and previews enter the VM from the host side, so they are subject to +loopback confinement but not to the guest egress policy. diff --git a/docs/content/docs/performance.mdx b/docs/content/docs/performance.mdx index a9ec6efe30..2fbb98cd44 100644 --- a/docs/content/docs/performance.mdx +++ b/docs/content/docs/performance.mdx @@ -5,23 +5,21 @@ skill: true --- Measure the exact workload and hardware used in production. VM startup includes -Core configuration, filesystem bootstrap, required software reconciliation, -and the first execution. Hosted startup additionally includes actor resolution. +Core configuration, filesystem bootstrap, software projection, and the first +execution. ## Cold-start phases Record these phases separately: -1. actor resolution and process startup; -2. required software acquisition; -3. optional preload warming; -4. Core and filesystem boot; -5. first guest instruction. +1. sidecar process startup, or reuse of a shared sidecar; +2. software acquisition; +3. Core and filesystem boot; +4. first guest instruction. -Required software must finish before readiness. Optional preload work has a -strict startup deadline and may time out without making the actor unhealthy. -The process-local content-addressed cache should make a digest hit visible as a -separate metric from a network download. +Software passed at boot must be available before the VM is ready. The sidecar's +content-addressed package cache should make a digest hit visible as a separate +metric from a network download. ## Memory per VM @@ -45,7 +43,7 @@ From the repository root: ``` The benchmark script builds an optimized native binary, points the Core client -at it with `AGENT_OS_SIDECAR_BIN`, and writes JSON results under +at it with `AGENTOS_SIDECAR_BIN`, and writes JSON results under `scripts/benchmarks/results/`. For a focused run, build first and select the minimal sleep/command workload: @@ -53,7 +51,7 @@ For a focused run, build first and select the minimal sleep/command workload: ```sh pnpm build cargo build --release -p agentos-native-sidecar -export AGENT_OS_SIDECAR_BIN="$PWD/target/release/agentos-native-sidecar" +export AGENTOS_SIDECAR_BIN="$PWD/target/release/agentos-native-sidecar" pnpm exec tsx scripts/benchmarks/coldstart.bench.ts \ --workload=sleep --iterations=2000 diff --git a/docs/content/docs/permissions.mdx b/docs/content/docs/permissions.mdx index ddb86324d9..23d22ec30e 100644 --- a/docs/content/docs/permissions.mdx +++ b/docs/content/docs/permissions.mdx @@ -1,13 +1,25 @@ --- title: "Permissions" -description: "Constrain untrusted guest filesystem, process, and network access." +description: "Constrain untrusted guest filesystem, process, network, environment, and host function access." --- -Pass a serializable permission tree in creation config or a later complete -`config.set` replacement. The trusted sidecar enforces it for every guest -operation. +Pass a permission tree when creating the VM. The trusted sidecar enforces it +for every guest operation. Hosted VMs take the same serializable tree in their +creation config or a later complete `v1.config.set` replacement. - + + +The tree has one scope per capability: `fs`, `network`, `childProcess`, +`process`, `env`, and `hostFunction`. A scope can be `"allow"`, `"deny"`, or a +rule set with a `default` mode and `rules` that match operations and target +patterns. + +Your policy is merged over the default, so an omitted scope keeps its default. +By default the guest behaves like a sandboxed machine: its virtual filesystem, +processes, environment, listeners, and loopback networking work, and outbound +network access is limited to the sidecar's default allowlist. Library host +functions are governed by the `hostFunction` scope. + +See the [Security Model](/agentos/docs/security-model) for the trust boundary +these policies enforce. -Host bindings have a separate policy surface in embedded Core. The hosted actor -does not expose host bindings. diff --git a/docs/content/docs/persistence.mdx b/docs/content/docs/persistence.mdx index a874a8cb31..765fe63ade 100644 --- a/docs/content/docs/persistence.mdx +++ b/docs/content/docs/persistence.mdx @@ -1,21 +1,57 @@ --- title: "Persistence & Sleep" -description: "Persist VM filesystem and Core state with SQLite." +description: "Persist an agentOS VM's filesystem in SQLite so files survive VM recreation." --- -Core durable state uses `agentos_core_*` tables and filesystem storage uses -`agentos_fs_*`. Each owner has an independent schema version and migration -transaction. +By default, a VM's root filesystem is an in-memory overlay over the bundled +base image. Its files are discarded when the VM is replaced or disposed. + +To keep files, give a hosted VM a `durable` root in its config. In the library, +give the VM a SQLite `database` and store its root filesystem there with the +`chunked_sqlite` plugin; a new VM created with the same options sees the same +files. + + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); + +const vm = client.agentOS.getOrCreate(["my-vm"], { + createWithInput: { + config: { filesystem: { root: { type: "durable" } } }, + }, +}); + +await vm.v1.filesystem.writeFile({ + path: "/home/agentos/result.txt", + content: "persistent", +}); +``` -This durable metadata does not make the default VM filesystem persistent. The -default root is an ephemeral in-memory overlay and is discarded when the VM is -replaced. Configure a `durable` root or mount only for paths that need to -survive VM replacement. + -Configured durable filesystems survive VM replacement. Running processes, -terminals, open streams, and live subscriptions do not. + + +In the library, `database` on its own stores Core and filesystem metadata, but +does not make the default in-memory root durable. A SQLite-backed root starts +without the bundled base image's files. + +Persisted filesystems survive VM recreation. Running processes, terminals, +language contexts, open streams, and live subscriptions do not. + +## Sleep + +A hosted VM sleeps when idle unless a process is running, and reports +`vm.shutdown` with reason `"sleep"`. The VM starts lazily on the first action +after wake. Durable files, configuration, and cron jobs survive sleep. See +[actor lifecycle](/actors/docs/lifecycle). + +Library durable state uses `agentos_core_*` tables and filesystem storage uses +`agentos_fs_*`. Each owner has an independent schema version and migration +transaction. - +In the library, the embedding application owns VM identity and decides when to +recreate or dispose the VM. -The embedding application owns VM identity and decides when to recreate or -dispose the VM. diff --git a/docs/content/docs/processes.mdx b/docs/content/docs/processes.mdx index 723f75598f..838d091fec 100644 --- a/docs/content/docs/processes.mdx +++ b/docs/content/docs/processes.mdx @@ -1,49 +1,140 @@ --- -title: "Processes & Terminals" -description: "Execute commands, manage long-running processes, and attach terminals." +title: "Processes & Shells" +description: "Execute commands, manage long-running processes, replay their output, and open interactive shells." --- -`process.exec` runs a command to completion. `process.spawn` starts a -long-running process and returns a process ID for wait, signal, stdin, PTY -resize, and bounded output replay. -`process.get` and `process.list` report `startedAtMs`; nodes returned by -`process.tree` report `startTimeMs` and `exitTimeMs`. These are Unix timestamps -in milliseconds. Completed tree nodes use the same structured `exit` status as -`process.get`. Tree timestamps record when agentOS first observed the process -or its exit, not exact kernel lifecycle times. `exitTimeMs` can be absent when -agentOS did not observe the exit. -`process.tree` includes guest child processes as well as processes launched -through Core. Guest and tracked display PIDs can overlap; use the returned -process ID rather than reconstructing one from a node's `pid`. -For untracked guest nodes, `args` is the kernel's runtime argument list. For -example, Node's `-e` source is passed to its runtime separately, so the tree -shows `-e` but not the source text. - - +Every guest process is an entry in the VM's kernel process table, never a host +process. + +| | Hosted | Library | +|---|---|---| +| Run to completion with an argument array | `v1.process.run({ command, args })` | `process.execFile(file, args)` | +| Run a shell command line | `v1.process.run({ command: "sh", args: ["-c", line] })` | `process.exec(line)` | +| Start a long-running process | `v1.process.spawn({ command, args })` returns `{ generation, pid }` | `process.spawn(command, args)` returns `{ pid }` | + + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +// One-shot execution +const result = await vm.v1.process.run({ + command: "ls", + args: ["-la", "/home/agentos"], + options: { captureStdio: true }, +}); +console.log(result.stdout); + +// Long-running process with live output and exit events. +const conn = vm.connect(); +await conn.ready; +const server = await conn.v1.process.spawn({ + command: "node", + args: ["/tmp/server.mjs"], +}); + +conn.on("process.output", (event) => { + if (event.process.pid !== server.pid) return; + console.log(event.stream, new TextDecoder().decode(event.data)); +}); +conn.on("process.exit", (event) => { + if (event.process.pid !== server.pid) return; + console.log("exited:", event.status.exitCode); +}); + +// Write to stdin +await conn.v1.process.stdin.write({ process: server, data: "some input\n" }); + +// Stop or kill +await conn.v1.process.signal({ process: server, signal: "SIGTERM" }); +``` + + + + Process arguments are passed as individual argv entries. An empty string is a valid argument; it is not dropped or treated as an omitted argument. Empty -commands or working directories are invalid input, including when opening a -terminal. - -Use `onProcessOutput` and `onProcessExit` for live events. Replay retained -output after reconnecting to the same durable VM. - -Interactive terminals are separate resources under `terminal.open`, -`terminal.stdin.write`, `terminal.pty.resize`, `terminal.output.read`, -`terminal.wait`, and `terminal.close`. Their live events are -`terminal.output` and `terminal.exit`. - -A process wait returns an exit code only after agentOS observes a -guest exit. A rejected launch or lost exit observation rejects the wait instead -of inventing an exit code. An observation failure does not prove the guest has -stopped; inspect the VM or restart it before assuming it is safe to retry work. -With output retention enabled, Core output callbacks include the sidecar's -optional `sequence` and `timestampMs` metadata for correlating live output with -replay. Local diagnostics without a guest output event omit this metadata. -Ordinary-process replay returns `exitCode` when the sidecar has confirmed exit -and reconciles Core's `wait`, `get`, and `list` state, even if the live exit -notification was missed. -Real-terminal replay also reconciles `wait_shell` after a missed exit event or -an earlier observation failure. TypeScript Core's local prompt terminal does -not provide replay. +commands or working directories are invalid input. + +## Manage running processes + +| Operation | Hosted | Library | +|---|---|---| +| Write and close stdin | `v1.process.stdin.write`, `v1.process.stdin.close` | `process.writeStdin`, `process.closeStdin` | +| Send a signal | `v1.process.signal` | `process.signal`, `process.kill` | +| Resize a PTY | `v1.process.pty.resize` | `process.resizePty` | +| Wait for exit | `v1.process.wait` | `process.wait` | +| Inspect | `v1.process.get`, `v1.process.list`, `v1.process.tree` | `process.get`, `process.list`, `process.tree` | + +Hosted operations take the process handle `{ generation, pid }`; library +operations take the `pid`. A hosted handle from before a `v1.vm.restart` fails +with `stale_generation`. + +`get` and `list` report `startedAtMs` as a Unix timestamp in milliseconds. +`tree` includes guest child processes as well as processes launched through the +API, so it can show more than you started. Use the returned handle or `pid` +rather than reconstructing one from a node; guest children that were not +launched through the API have no hosted process handle. + +A process wait returns an exit only after agentOS observes a guest exit. A +rejected launch or lost exit observation rejects the wait instead of inventing +an exit code. An observation failure does not prove the guest has stopped; +inspect the VM or recreate it before assuming it is safe to retry work. + +## Output events and replay + +Process output can be replayed after the fact, even after the process has +exited or a live event was missed. Hosted VMs retain output for every spawned +process; in the library, pass `output: { retainEvents: true }` to `spawn`. +Then page through retained output with a sequence cursor. + + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const shell = await vm.v1.process.spawn({ command: "sh", args: [] }); + +await vm.v1.process.stdin.write({ process: shell, data: "echo from the background\n" }); +await vm.v1.process.stdin.close({ process: shell }); +await vm.v1.process.wait({ process: shell }); + +// Later, from any client +const replay = await vm.v1.process.output.read({ + process: shell, + maxEvents: 32, + maxBytes: 64 * 1024, +}); +console.log(replay.events.length, replay.nextCursor); +``` + + + + + +Pass the last cursor as `after` to read the next page. A replay page reports +`nextCursor`, `hasMore`, and `truncated`. Live events are low-latency hints: +`process.output` and `process.exit` on a hosted connection, or +`vm.onProcessOutput` and `vm.onProcessExit` in the library. Retention is +bounded; see [Resource Limits](/agentos/docs/resource-limits) for the defaults. + +## Interactive shells + +Interactive terminals allocate a kernel PTY and start a shell attached to it. + +| Operation | Hosted | Library | +|---|---|---| +| Open | `v1.terminal.open` | `terminal.open`, returns a `shellId` | +| Write input | `v1.terminal.stdin.write` | `terminal.write` | +| Resize | `v1.terminal.pty.resize` | `terminal.resize` | +| Wait and close | `v1.terminal.wait`, `v1.terminal.close` | `terminal.wait`, `terminal.close` | +| Output | `terminal.output` and `terminal.exit` events, `v1.terminal.output.read` replay | `vm.onShellData`, `vm.onShellStderr`, `vm.onShellExit` | + +Hosted VMs also list open terminals with `v1.terminal.list`. diff --git a/docs/content/docs/python.mdx b/docs/content/docs/python.mdx index b2a5f56715..362c56a1aa 100644 --- a/docs/content/docs/python.mdx +++ b/docs/content/docs/python.mdx @@ -22,7 +22,21 @@ driving [Bash](/agentos/docs/bash): `evaluate()` returns a JSON-serializable value. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const result = await vm.v1.python.evaluate({ expression: "21 * 2" }); +console.log(result.value); // 42 +``` + + + + A value the JSON encoder can't represent fails with `evaluation_serialization_failed` rather than silently losing the value. @@ -31,40 +45,57 @@ A value the JSON encoder can't represent fails with `execute()` runs source and captures its output instead of returning a value. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +const result = await vm.v1.python.execute({ + source: `print("hello from agentOS")`, +}); +console.log(result); +``` + + + + -Executions are ephemeral, so capture stdio only when you want it — `"stderr"` for +Executions are ephemeral, so capture stdio only when you want it: `"stderr"` for diagnostics, `"all"` for both streams. `onStdout`/`onStderr` stream live and work independently of capture. ## Pass data into code -`inputs` hands host values to the guest as real objects, so data never gets +In the library, `inputs` hands host values to the guest as real objects, so data never gets interpolated into source. - + ## Keep state between calls Pass a `contextId` to keep globals, functions, imports, and modules alive in one interpreter. - + Only inline `execute()`/`evaluate()` retain memory. Files, modules, installs, and Bash may pass the same id, but they run fresh. A context runs one operation at a -time — reusing a busy `contextId` fails immediately. +time, so reusing a busy `contextId` fails immediately. Contexts do not survive +a hosted VM's sleep and wake; create them lazily on first use. ## Files, modules, and async - + Awaited work counts against the deadline; unawaited `asyncio` tasks are not promised to survive between operations. ## Install packages - + Installs modify the VM-wide filesystem, so a package installed once is importable by every later execution in that VM, in any language. Only one npm/Python @@ -78,10 +109,10 @@ host: Pyodide builds of compiled packages such as Pillow come from ## Background processes and web servers -`spawn*` starts a long-lived process and returns a `pid`. From there you get +`spawn*` starts a long-lived process and returns a handle. From there you get stdin, output, signals, and waiting. - + ## A full Linux environment underneath @@ -89,29 +120,28 @@ There is a real Linux environment behind all of this, shared by every language. Files and installed packages are immediately visible to Bash, JavaScript, and other executions. - + See [Filesystem](/agentos/docs/filesystem), [Processes & Shells](/agentos/docs/processes), and -[Networking & Previews](/agentos/docs/networking). +[Networking](/agentos/docs/networking). ## Host functions Python calls [host functions](/agentos/docs/host-functions) as normal commands via `subprocess`, so -host credentials stay outside the VM. +host credentials stay outside the VM. Host functions are library-only. - + ## Permissions, limits, and timeouts Every operation inherits the VM [permission policy](/agentos/docs/permissions) and [resource limits](/agentos/docs/resource-limits). - + ## Embedded API -The examples above already use `AgentOs.create()`. Python contexts remain -available until the host calls `dispose()`; there is no actor sleep cycle to -recreate them after. +The library examples use `AgentOs.create()`. Python contexts remain available +until the host calls `dispose()`. Read more in the [embedded API quickstart](/agentos/docs/quickstart-embedded). diff --git a/docs/content/docs/quickstart-embedded.mdx b/docs/content/docs/quickstart-embedded.mdx index 65e9bb123b..1e24bc8fe0 100644 --- a/docs/content/docs/quickstart-embedded.mdx +++ b/docs/content/docs/quickstart-embedded.mdx @@ -4,9 +4,9 @@ description: "Run an agentOS VM directly inside a Node.js application." skill: true --- -Use `@rivet-dev/agentos-core` when your application owns VM identity, -persistence, and lifecycle. The embedded API returns a VM handle directly; -there is no actor server or generated transport client. +`@rivet-dev/agentos-core` runs VMs inside your Node.js process. Your +application owns VM identity, persistence, and lifecycle, and +`AgentOs.create()` returns a VM handle directly. ## Install @@ -18,21 +18,18 @@ npm install @rivet-dev/agentos-core -The VM remains active until `dispose()` is called. Configure SQLite when its -filesystem must survive process replacement; omit `database` for an in-memory -VM. +The VM remains active until `dispose()` is called. Its filesystem is in memory +by default; see [Persistence](/agentos/docs/persistence) to keep files across VM +recreation. -## Embedded-only capabilities +## Configuration -Embedded Core may use trusted resources that cannot cross the hosted actor -boundary: +The embedding process is trusted, so it can also give the VM access to host +resources: -- host bindings implemented as in-process callbacks; -- host-directory and callback-backed mounts; +- [host functions](/agentos/docs/host-functions) implemented as in-process callbacks; +- host-directory and other [mounts](/agentos/docs/filesystem#mounts); - local `.aospkg` paths; - explicit ownership of a shared sidecar process. - -Managed orchestration layers can wrap this same Core surface without changing -the VM runtime or its security boundary. diff --git a/docs/content/docs/quickstart-hosted.mdx b/docs/content/docs/quickstart-hosted.mdx new file mode 100644 index 0000000000..7b0e19fb18 --- /dev/null +++ b/docs/content/docs/quickstart-hosted.mdx @@ -0,0 +1,330 @@ +--- +title: "Quickstart (Hosted)" +description: "Create durable agentOS VMs by ID and control them from the RivetKit client or plain HTTP." +skill: true +--- + +Hosted agentOS VMs run as the prebuilt `agentOS` Rivet Actor, on Rivet Cloud or +on your own Rivet control plane. Create a VM by key, then control it from the +`@rivet-dev/agentos` client or plain HTTP. To run VMs inside your own Node.js +process instead, see the [library quickstart](/agentos/docs/quickstart). + +## Which one should I use? + +| | [Hosted](/agentos/docs/quickstart-hosted) | [Library](/agentos/docs/quickstart) | +|---|---|---| +| **Runs** | As a durable instance on Rivet Cloud or a [self-hosted](/agentos/docs/self-hosting) Docker image | Embedded inside your process as a library | +| **Lifecycle** | Durable, sleeps when idle unless a process is running; see [actor lifecycle](/actors/docs/lifecycle) | Ephemeral, runs until `dispose()` or process exit | +| **Package** | `@rivet-dev/agentos`, or plain HTTP | `@rivet-dev/agentos-core` | +| **VM identity** | A durable VM you create by key and reach by ID | A handle your process owns until `dispose()` | +| **Configuration** | Stored with the VM and versioned | Passed to `AgentOs.create()` | +| **Clients** | Your backend, or browsers with a scoped JWT | Your backend | +| **Host access** | None | Host functions, host mounts, external sandboxes | +| **Previews** | Expiring preview URLs | None | +| **Best for** | Long-lived VMs shared across clients | Embedding VMs in an existing backend | + +## Create a VM + +### Rivet Cloud + + + + +Sign up at the [Rivet dashboard](https://dashboard.rivet.dev) and create a new project. + + + + +When creating the project, choose agentOS as the product. + + + + +Set `RIVET_NAMESPACE` and `RIVET_TOKEN` from your project, then create a VM by +ID from TypeScript or plain HTTP: + + + +```ts TypeScript @nocheck +// npm install @rivet-dev/agentos +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); + +const vm = client.agentOS.getOrCreate(["my-vm"]); +``` + +```bash HTTP +curl -X PUT "https://api.rivet.dev/actors?namespace=$RIVET_NAMESPACE" \ + -H "Authorization: Bearer $RIVET_TOKEN" \ + --json '{ + "name": "agentOS", + "key": "my-vm", + "runner_name_selector": "services", + "crash_policy": "sleep" + }' +# → { "actor": { "actor_id": "…" }, "created": true } +``` + + + + + + +### Self-hosting + +See [Self-Hosting (Hosted)](/agentos/docs/self-hosting) to run hosted VMs on your own +infrastructure. + +## Show me the code + +### Run Bash Commands + + + +```ts TypeScript @nocheck +const result = await vm.v1.process.run({ + command: "sh", + args: ["-c", "echo hello && ls /home/agentos"], + options: { captureStdio: true }, +}); + +console.log(result.stdout); +``` + +```bash HTTP +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.process.run" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{ + "command": "sh", + "args": ["-c", "echo hello && ls /home/agentos"], + "options": { "captureStdio": true } + }]}' +``` + + + +### Work With the Filesystem + + + +```ts TypeScript @nocheck +await vm.v1.filesystem.writeFile({ + path: "/home/agentos/hello.txt", + content: "Hello, world!", +}); + +const file = await vm.v1.filesystem.readFile({ + path: "/home/agentos/hello.txt", +}); +``` + +```bash HTTP +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.filesystem.writeFile" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{ + "path": "/home/agentos/hello.txt", + "content": "Hello, world!" + }]}' + +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.filesystem.readFile" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{"path": "/home/agentos/hello.txt"}]}' +``` + + + +### Run a Dev Server & Create Preview URLs + +Start a Node.js server in the VM, then expose its port through a signed URL +that expires on its own, or revoke it early. A running server keeps the VM +awake. + + + +```ts TypeScript @nocheck +await vm.v1.filesystem.writeFile({ + path: "/home/agentos/server.js", + content: `require("node:http") + .createServer((req, res) => res.end("hello")) + .listen(3000);`, +}); + +await vm.v1.process.spawn({ + command: "node", + args: ["/home/agentos/server.js"], +}); + +const preview = await vm.v1.network.preview.create({ + port: 3000, + ttlMs: 60 * 60 * 1_000, +}); + +await vm.v1.network.preview.expire({ token: preview.token }); +``` + +```bash HTTP +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.filesystem.writeFile" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{ + "path": "/home/agentos/server.js", + "content": "require(\"node:http\").createServer((req, res) => res.end(\"hello\")).listen(3000);" + }]}' + +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.process.spawn" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{"command": "node", "args": ["/home/agentos/server.js"]}]}' + +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.network.preview.create" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{"port": 3000, "ttlMs": 3600000}]}' +# → { "output": { "path": "/preview/…/", "token": "…", … } } + +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.network.preview.expire" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{"token": "…"}]}' +``` + + + +### Use the Language Runtimes + +Run JavaScript, TypeScript, and Python directly, with npm and pip installs +built in. + + + +```ts TypeScript @nocheck +await vm.v1.javascript.execute({ source: "console.log(1 + 2)" }); +await vm.v1.typescript.execute({ + source: "const n: number = 3; console.log(n)", +}); + +const { value } = await vm.v1.python.evaluate({ + expression: "21 * 2", +}); +``` + +```bash HTTP +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.javascript.execute" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{"source": "console.log(1 + 2)"}]}' + +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.python.evaluate" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{"expression": "21 * 2"}]}' +``` + + + +### Schedule Cron Jobs + + + +```ts TypeScript @nocheck +await vm.v1.cron.schedule({ + name: "cleanup", + expression: "0 * * * *", + command: "rm", + args: ["-rf", "/tmp/cache"], +}); +``` + +```bash HTTP +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.cron.schedule" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{ + "name": "cleanup", + "expression": "0 * * * *", + "command": "rm", + "args": ["-rf", "/tmp/cache"] + }]}' +``` + + + +### Install Software + +Install tools from the [software registry](/agentos/docs/registry/software) +while the VM is running. Packages are verified against their digest before +their commands appear on `/opt/agentos/bin`. + + + +```ts TypeScript @nocheck +// Registry packages are published next to a manifest with each release. +const base = "https://releases.rivet.dev/agent-os/latest/software/"; +const { artifacts } = await (await fetch(`${base}manifest.json`)).json(); +const ripgrep = artifacts.find((artifact) => artifact.name === "ripgrep"); + +const installed = await vm.v1.software.install({ + source: { url: new URL(ripgrep.path, base).href, digest: ripgrep.digest }, +}); +if (installed.config.state === "restart_required") { + await vm.v1.vm.restart({}); +} +``` + +```bash HTTP +curl "https://api.rivet.dev/gateway/$VM_ID/action/v1.software.install" \ + -H "x-rivet-token: $VM_TOKEN" \ + --json '{"args": [{ + "source": { + "url": "https://releases.rivet.dev/agent-os/latest/software/packages/ripgrep/.aospkg", + "digest": "sha256:" + } + }]}' +``` + + + +### Give Clients Direct Access With JWTs + +Keep your admin token on the backend and issue a JWT scoped to one VM. +Browsers and other untrusted clients can then call that VM directly, and +nothing else. + + + +```ts TypeScript @nocheck +// Backend: issue a token for this VM only +const vmId = await vm.resolve(); +const { token } = await vm.issueToken({ + subject: "user-123", + expiresIn: 3600, +}); + +// Client: call the VM directly +const userClient = createAgentOsClient({ + endpoint: "https://api.rivet.dev", + namespace: "my-namespace", + token, +}); +const userVm = userClient.getForId("agentOS", vmId); +``` + +```bash HTTP +curl -X POST "https://api.rivet.dev/auth/tokens" \ + -H "Authorization: Bearer $RIVET_TOKEN" \ + --json @- < + +## Next steps + +- See the [Hosted HTTP API](/agentos/docs/hosted-api) for configuration, + events, errors, and every action. +- Keep files across restarts with [Persistence & Sleep](/agentos/docs/persistence). diff --git a/docs/content/docs/quickstart.mdx b/docs/content/docs/quickstart.mdx index eea0a98505..86c529fc57 100644 --- a/docs/content/docs/quickstart.mdx +++ b/docs/content/docs/quickstart.mdx @@ -1,24 +1,92 @@ --- -title: "Quickstart" -description: "Run an agentOS VM directly inside a Node.js application." +title: "Quickstart (Library)" +description: "Run agentOS VMs directly inside a Node.js application with @rivet-dev/agentos-core." skill: true --- -## Install +Use agentOS as a library to run VMs inside your own Node.js process. To create +durable VMs by ID and control them over HTTP instead, see the +[hosted quickstart](/agentos/docs/quickstart-hosted). + +## Which one should I use? + +| | [Hosted](/agentos/docs/quickstart-hosted) | [Library](/agentos/docs/quickstart) | +|---|---|---| +| **Runs** | As a durable instance on Rivet Cloud or a [self-hosted](/agentos/docs/self-hosting) Docker image | Embedded inside your process as a library | +| **Lifecycle** | Durable, sleeps when idle unless a process is running; see [actor lifecycle](/actors/docs/lifecycle) | Ephemeral, runs until `dispose()` or process exit | +| **Package** | `@rivet-dev/agentos`, or plain HTTP | `@rivet-dev/agentos-core` | +| **VM identity** | A durable VM you create by key and reach by ID | A handle your process owns until `dispose()` | +| **Configuration** | Stored with the VM and versioned | Passed to `AgentOs.create()` | +| **Clients** | Your backend, or browsers with a scoped JWT | Your backend | +| **Host access** | None | Host functions, host mounts, external sandboxes | +| **Previews** | Expiring preview URLs | None | +| **Best for** | Long-lived VMs shared across clients | Embedding VMs in an existing backend | + +## Create a VM + + + ```bash npm install @rivet-dev/agentos-core ``` -## Create a VM + + + + + +The VM runs until `dispose()` is called or your process exits. + + + + +## Show me the code + +### Run Bash Commands + + + +### Work With the Filesystem + + + +### Run a Dev Server + +Start a Node.js server in the VM, then send it a request from your process. +The listener is not exposed on the host network. + + + +### Use the Language Runtimes + +Run JavaScript, TypeScript, and Python directly, with npm and pip installs +built in. + + + +### Schedule Cron Jobs + + + +### Install Software + +Install tools from the [software registry](/agentos/docs/registry/software) +while the VM is running. Packages are verified against their digest before +their commands appear on `/opt/agentos/bin`. + + + +### Persist Files + +The filesystem is in memory by default. Store the root filesystem in SQLite to +keep files across VM recreation. - + -The VM remains active until `dispose()` is called. Omit `database` for an -ephemeral VM, or configure SQLite-backed storage when state must survive -process replacement. +## Next steps - -Core runs in a trusted host process and can use host bindings, host-backed -mounts, local packages, and remote verified package URLs. - +- Expose your own functions to guest code with + [host functions](/agentos/docs/host-functions). +- Lock the VM down with [Permissions](/agentos/docs/permissions) and + [Resource Limits](/agentos/docs/resource-limits). diff --git a/docs/content/docs/resource-limits.mdx b/docs/content/docs/resource-limits.mdx index 9cf2ac0432..b7eb6b4a7a 100644 --- a/docs/content/docs/resource-limits.mdx +++ b/docs/content/docs/resource-limits.mdx @@ -3,53 +3,76 @@ title: "Resource Limits" description: "Configure bounded VM, process, JavaScript, Python, and WASM resources." --- -Limits are part of the durable actor config and are applied by the sidecar -during VM creation. +Pass a `limits` tree in the VM's configuration. The sidecar applies it when the +VM starts. Near-limit pressure is reported as the `vm.limitWarning` event on a +hosted VM, or through `onLimitWarning` in the library. - + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +await vm.v1.config.set({ + config: { + limits: { + tls: { maxBufferedBytes: 2048 }, + execution: { completedTtlMs: 60_000, liveExecutionWarningThreshold: 32 }, + agentosPackages: { maxMounts: 8192 }, + }, + }, +}); + +// Changed limits apply to the next VM generation. +await vm.v1.vm.restart({}); +``` + + + + Use `limits.tls.maxBufferedBytes` to bound TLS plaintext buffering. `limits.execution.completedTtlMs` and `limits.execution.maxCompletedExecutions` bound retained completed language executions. `limits.execution.liveExecutionWarningThreshold` sets the live execution count that triggers a warning, not a rejection. -These fields are optional in both Rust and TypeScript Core; omission keeps the -sidecar default. +These fields are optional; omission keeps the sidecar default. -Process and terminal output replay is also sidecar-owned. The defaults retain +Process output replay is also sidecar-owned. The defaults retain up to 1,024 events or 1 MiB for each opted-in process, return at most 256 events or 768 KiB per read, and retain at most 1,024 replay records per VM. Override them with `limits.process.outputReplayEvents`, `outputReplayBytes`, `outputReplayPageEvents`, `outputReplayPageBytes`, and `maxOutputReplays`. -Page limits cannot exceed their corresponding retention limits. Omitting an -ordinary-process or terminal page bound uses that VM's configured limit; -explicit zero or a value above the limit is rejected. Raising a VM page limit -does not require a matching client constant change. +Page limits cannot exceed their corresponding retention limits. Omitting a page +bound in `process.readOutput` uses the VM's configured limit; explicit zero or a +value above the limit is rejected. Every queue, buffer, timeout, and resource collection has a bounded default. -Near-capacity conditions emit `vm.limitWarning`; exhausted limits fail -with a typed error naming the limit and the field used to raise it. +Near-capacity conditions call `onLimitWarning`; exhausted limits fail with a +typed error naming the limit and the field used to raise it. `limits.reactor.shutdownDeadlineMs` bounds the SQLite close wait and subsequent resource drain together. Process termination and filesystem flushing happen before this budget. If close exceeds the deadline, VM shutdown reports a typed `timeout` with the limit name instead of reporting -a clean stop. Close completion can no longer be confirmed, so the generation -stays quarantined—even if local resource counts reach zero—until the sidecar -worker restarts. Quarantined generations count toward VM admission capacity. -The hosted actor also has a fixed ten-second shutdown wait; raising the sidecar -deadline does not extend it. The runtime test API has a separate one-second -disposal wait. Exceeding either client wait reports a timeout, not confirmed -shutdown; the remote operation may still be running. +a clean stop. Close completion can no longer be confirmed, so the VM stays +quarantined until the sidecar process restarts, even if local resource counts +reach zero. Quarantined VMs count toward VM admission capacity. + +Limits are fixed for the life of a VM. To change them on a hosted VM, update +the config and call `v1.vm.restart`. In the library, dispose the VM and create +a new one with the new limits. The hosted actor also has a fixed ten-second +shutdown wait; raising the sidecar deadline does not extend it. -The hosted action envelope, filesystem transfer, process replay, network, and -software-cache ceilings are listed in -[Hosted Actor Limits](/agentos/docs/actor-limits). +## Sidecar limits -Replacing limits normally requires a runtime generation replacement. Graceful -process draining during replacement is a separately tracked follow-up. +Library VMs share a sidecar process by default. Limits that apply to the whole +process, such as `runtime.executor.maxActiveVms`, are set when creating a +dedicated sidecar with `AgentOs.createSidecar()`, as in the example above. ## Embedded API -Embedded Core uses the same sidecar and VM resource policies without the actor -message envelope. Read more in the +Limits apply to VMs created with `AgentOs.create()` and sidecars created with +`AgentOs.createSidecar()`. Read more in the [embedded API quickstart](/agentos/docs/quickstart-embedded). diff --git a/docs/content/docs/sandboxes.mdx b/docs/content/docs/sandboxes.mdx index ecb0251da0..7336344934 100644 --- a/docs/content/docs/sandboxes.mdx +++ b/docs/content/docs/sandboxes.mdx @@ -1,14 +1,22 @@ --- -title: "External VMs" -description: "Attach an external VM provider through trusted embedded Core." +title: "External Sandboxes" +description: "Mount a full external sandbox into an agentOS VM for workloads that need a real Linux kernel." --- -External VM providers expose a trusted host-backed filesystem and process -bridge. They are supported by `@rivet-dev/agentos-core`, not by the hosted -actor. +When a workload needs a full Linux environment, attach an external sandbox to +a library VM with the `sandbox` option. External sandboxes rely on trusted host +callbacks and mounts, so they are not available on hosted VMs. agentOS mounts the sandbox filesystem into +the VM at `/mnt/sandbox` and registers `agentos-sandbox` host functions for +running commands and listing processes in the sandbox. - + + +Guest code reads and writes sandbox files the same way it reads local files. +Providers live in `@rivet-dev/agentos-sandbox`. + +The sandbox bridge runs through trusted host callbacks in the embedding +process, so credentials for the sandbox provider stay on the host. + +See [agentOS vs Sandbox](/agentos/docs/versus-sandbox) to decide when a +workload needs one. -The fixed actor rejects host mounts and provider clients because they would -cross its security boundary. Use its whitelisted filesystem registry for hosted -storage. diff --git a/docs/content/docs/security-model.mdx b/docs/content/docs/security-model.mdx index 9dfb699605..c61b62053f 100644 --- a/docs/content/docs/security-model.mdx +++ b/docs/content/docs/security-model.mdx @@ -8,7 +8,7 @@ skill: true agentOS is in beta and is still undergoing security review. -agentOS runs untrusted guest code on behalf of a trusted client. Each VM has a +agentOS runs untrusted guest code on behalf of a trusted host application. Each VM has a kernel-owned filesystem, process table, socket table, pipes, PTYs, permission policy, and language runtimes. Guest JavaScript executes in V8; WASM and Python use their managed executors. Guest operations reach the host only through the @@ -18,29 +18,40 @@ sidecar's policy-enforcing interfaces. | Component | Trust | Responsibility | | --- | --- | --- | -| Client | Trusted, except for submitted guest code and data | Chooses configuration, permissions, software URLs, and credentials | +| Host application | Trusted, except for submitted guest code and data | Chooses configuration, permissions, software sources, host functions, mounts, and credentials | | Sidecar, kernel, and VFS | Trusted enforcement point | Applies limits and permissions; owns processes, filesystems, sockets, and mounts | | Executor | Untrusted | Runs guest JavaScript, Python, WASM, and third-party code | The security boundary is **sidecar/runtime to executor**. A guest bypassing an applied permission, mount boundary, egress rule, or resource limit is in scope. -A trusted client deliberately choosing a permissive configuration or unsafe +A trusted host deliberately choosing a permissive configuration or unsafe credential is not a runtime escape. -## Hosted actor restrictions +## Host capabilities -The static hosted actor accepts only serializable configuration. Its filesystem -descriptors are resolved through a fixed whitelist, and software installation -accepts only remote HTTPS `.aospkg` sources. It cannot receive: +Core runs inside the trusted application process, so the host may explicitly +give a VM access to host resources: host functions, host-backed mounts, external +sandboxes, and local software paths. Each of these runs with the host's +privileges. Expose them deliberately, and never pass guest-controlled values +into their configuration. + +If your application lets untrusted users configure VMs, accept only +serializable configuration from them and resolve it against your own allowlist +of mounts and software sources. + +## Hosted VM restrictions + +The hosted `agentOS` actor accepts only serializable configuration. Its +filesystem descriptors are resolved through a fixed registry, and software +installation accepts only remote HTTPS `.aospkg` sources. It cannot receive: - a host filesystem path or host-directory mount; -- a binding callback, closure, or arbitrary host object; +- a host function callback, closure, or arbitrary host object; - a custom filesystem or software resolver; - an unregistered actor implementation or lifecycle hook. -Embedded Core runs inside a trusted application process and may explicitly -configure host bindings, host-backed mounts, and local software paths. Those are -embedded capabilities, not actor capabilities. +Use [scoped JWTs](/docs/jwt) to give an untrusted client access to exactly one +hosted VM. ## Isolation guarantees @@ -48,7 +59,7 @@ embedded capabilities, not actor capabilities. - Guest sockets pass through the kernel socket table and applied network policy. - Trusted VM setup can update mounts and command stubs without relaxing guest filesystem permissions, including while a VM is running. -- Each actor's filesystem and runtime state are isolated from other actors. +- Each VM's filesystem, process table, and runtime state are isolated from other VMs. - Host-backed embedded mounts are confined to the configured root on every operation, including symlink and `..` resolution. - Resource limits bound process count, descriptors, sockets, filesystem bytes, @@ -66,9 +77,9 @@ can be `"allow"`, `"deny"`, or a rule set that matches the operation and target. ## Operator responsibilities -- Authenticate and authorize clients before they receive an actor connection. -- Keep object-store credentials and operator package-download settings outside - actor-readable durable configuration. +- Authenticate and authorize users before they can reach a VM. +- Keep object-store credentials and package-download settings out of + configuration that guest code or untrusted users can read. - Choose permissions and resource limits for the workload. - Treat software URLs and config as trusted control-plane input, while treating the downloaded bytes and executed code as hostile. diff --git a/docs/content/docs/self-hosting.mdx b/docs/content/docs/self-hosting.mdx new file mode 100644 index 0000000000..05e3e573b8 --- /dev/null +++ b/docs/content/docs/self-hosting.mdx @@ -0,0 +1,84 @@ +--- +title: "Self-Hosting (Hosted)" +description: "Run hosted agentOS VMs on your own infrastructure with the Rivet control plane and the agentOS worker image." +--- + +Self-hosting runs the hosted agentOS API on your own infrastructure instead of +Rivet Cloud. It needs two processes: the Rivet control plane, and the agentOS +worker that runs the `agentOS` actor and its VMs. If you only need VMs inside +your own Node.js process, use the [library](/agentos/docs/quickstart) instead; +it needs neither. + +## Which one should I use? + +| | [Hosted](/agentos/docs/quickstart-hosted) | [Library](/agentos/docs/quickstart) | +|---|---|---| +| **Runs** | As a durable instance on Rivet Cloud or a [self-hosted](/agentos/docs/self-hosting) Docker image | Embedded inside your process as a library | +| **Lifecycle** | Durable, sleeps when idle unless a process is running; see [actor lifecycle](/actors/docs/lifecycle) | Ephemeral, runs until `dispose()` or process exit | +| **Package** | `@rivet-dev/agentos`, or plain HTTP | `@rivet-dev/agentos-core` | +| **VM identity** | A durable VM you create by key and reach by ID | A handle your process owns until `dispose()` | +| **Configuration** | Stored with the VM and versioned | Passed to `AgentOs.create()` | +| **Clients** | Your backend, or browsers with a scoped JWT | Your backend | +| **Host access** | None | Host functions, host mounts, external sandboxes | +| **Previews** | Expiring preview URLs | None | +| **Best for** | Long-lived VMs shared across clients | Embedding VMs in an existing backend | + +## Run it + + + + +```sh +docker run -p 6420:6420 rivetdev/engine +``` + +See the [self-hosting guides](/docs/deploy/) for other ways to run it. + + + + +The worker is what actually runs your VMs. It connects to the control plane: + +```sh +docker run \ + --add-host=host.docker.internal:host-gateway \ + -e RIVET_ENDPOINT=http://host.docker.internal:6420 \ + rivetdev/services +``` + + + + +Create a VM by ID on your control plane from TypeScript or plain HTTP: + + + +```ts TypeScript @nocheck +// npm install @rivet-dev/agentos +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "http://localhost:6420" }); + +const vm = client.agentOS.getOrCreate(["my-vm"]); +``` + +```bash HTTP +curl -X PUT "http://localhost:6420/actors?namespace=default" \ + --json '{ + "name": "agentOS", + "key": "my-vm", + "runner_name_selector": "services", + "crash_policy": "sleep" + }' +# → { "actor": { "actor_id": "…" }, "created": true } +``` + + + + + + +Everything in the [hosted quickstart](/agentos/docs/quickstart-hosted) and the +[Hosted HTTP API](/agentos/docs/hosted-api) works the same way. Use +`http://localhost:6420`, or wherever your control plane is reachable, as the +base URL instead of `https://api.rivet.dev`. diff --git a/docs/content/docs/software.mdx b/docs/content/docs/software.mdx index a82ffd6362..c9cb8e7a79 100644 --- a/docs/content/docs/software.mdx +++ b/docs/content/docs/software.mdx @@ -1,49 +1,121 @@ --- title: "Software" -description: "Install immutable .aospkg command packages." +description: "Install immutable .aospkg command packages when a VM is created or while it is running." --- -The hosted actor accepts a remote HTTPS package source and optional exact -`sha256:<64 lowercase hex>` digest. +agentOS software ships as immutable `.aospkg` packages containing real compiled +tools. Installed packages are projected read-only under +`/opt/agentos/pkgs//`, and their commands are linked under +`/opt/agentos/bin`. Every VM includes a default bundle with `sh`, coreutils, and +the standard CLI tools programs rely on. - +Browse the available packages in the [registry](/agentos/docs/registry). -`software.list()` reads the live installed set. `software.uninstall` removes -one package by its digest-derived `packageId`. Desired package sources live in -`config.get`, not `software.list`. If the VM is unavailable, `software.list` -returns `not_ready`; the desired sources remain readable from config. +## Registry packages -Software changes apply live when the VM is ready and its desired configuration -is fully applied. Otherwise they update desired configuration and report -`restart_required`; call `vm.restart({})` to apply it. Uninstalling a desired -package does not require a working VM or a fresh download. +Registry packages are published with each release under +`https://releases.rivet.dev/agent-os//software/`, or `latest` for the +newest release. Its `manifest.json` lists each package's name, digest, size, +and path relative to the manifest. Registry software is not installed from npm; +see [Publishing Software](/agentos/docs/custom-software/publishing). -Both mutations return `{ source, software, config }`. `source` identifies the -configured package, and `config` reports the new revision and state. `software` -contains resolved or installed metadata, or is `null` when uninstall changes -only desired configuration. Installed metadata reports package bytes as -`sizeBytes`; a resolved source in `config.get` uses the same field. +## Install while running -Packages are cached per process by content digest. URL-to-digest observations -may avoid a repeat download, but the exact bytes are always verified before -projection. +Install a package into a running VM from an HTTPS URL, with an optional exact +`sha256:<64 lowercase hex>` digest. The library also accepts a trusted local +path. -The central preload coordinator supplies a low-frequency popularity hint so a -worker can warm likely packages before its first VM needs them. Required -packages still determine readiness. Advisory preload timeout or coordinator -failure only reduces cache hit rate. See -[Software Installation and Preloading](/agentos/docs/architecture/software-cache). + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); +const vm = client.agentOS.getOrCreate(["my-vm"]); + +// Registry software is published as digest-addressed .aospkg files next to a +// manifest. Use a release version, or `latest`. +const base = "https://releases.rivet.dev/agent-os/latest/software/"; +const { artifacts } = await (await fetch(`${base}manifest.json`)).json(); +const ripgrep = artifacts.find((artifact) => artifact.name === "ripgrep"); + +const installed = await vm.v1.software.install({ + source: { url: new URL(ripgrep.path, base).href, digest: ripgrep.digest }, +}); +if (installed.config.state === "restart_required") { + await vm.v1.vm.restart({}); +} + +for (const software of await vm.v1.software.list({})) { + console.log(software.packageId, software.commands); +} + +await vm.v1.software.uninstall({ packageId: installed.source.packageId }); +``` + + + + + +On hosted VMs, software is part of the VM's durable configuration, so install +and uninstall use the same revision checks as `v1.config.patch`. A change +applies live when the VM is ready and its desired configuration is fully +applied; otherwise it reports `restart_required`, and `v1.vm.restart` applies +it. Both mutations return `{ source, software, config }`. `v1.software.list` +reads the live installed set, and the desired package sources live in +`v1.config.get`. + +In the library, `software.install(source)` returns the installed package's +`packageId`, `digest`, `sizeBytes`, `packageName`, `version`, and `commands`. +`software.installed()` lists packages installed through `software.install`, and +`software.list()` reports every package that provides commands in the VM. Only +one software or mount mutation runs at a time per VM; a concurrent call fails +with `invalid_state` instead of queueing. -Registry software is not installed from npm. Releases upload digest-addressed -`.aospkg` files and a relative-URL manifest to S3-compatible object storage. -Embedded Core can additionally install a trusted local artifact path. In -TypeScript, `software.install(source)` and `software.uninstall(packageId)` use -the same sidecar verification and projection path as Rust's `install_software` -and `uninstall_software`. `software.installed()` in TypeScript (or -`installed_software()` in Rust) returns packages installed through that API. -Legacy `software.link` remains a separate trusted-directory operation. +## Install when the VM is created + + + +```ts Hosted @nocheck +import { createAgentOsClient } from "@rivet-dev/agentos"; + +const client = createAgentOsClient({ endpoint: "https://api.rivet.dev" }); + +const vm = client.agentOS.getOrCreate(["my-vm"], { + createWithInput: { + config: { + software: [ + { + url: "https://releases.rivet.dev/agent-os/latest/software/packages/git/.aospkg", + }, + ], + }, + }, +}); +``` + + + + + +Hosted VMs take package URLs in their `software` config. The library takes +`{ packagePath }` references to packed `.aospkg` files, which is what the +`@agentos-software/*` packages in the agentOS repository export, and +`defaultSoftware: false` for a bare VM with only the software you list. + +## Verification and caching + +Each URL source is downloaded under strict size, time, and redirect limits, +verified against the expected digest when one is given, and validated before +it is projected. The verified content digest is the package identity. + +Packages are cached by content digest, so VMs that share a worker or sidecar +reuse one verified copy, and repeat installs of the same bytes skip the +download. Hosted workers also warm popular packages at startup; see +[Software Installation and Preloading](/agentos/docs/architecture/software-cache). ## Embedded API -Embedded Core accepts trusted local package paths as well as URLs. Read more in -the [embedded API quickstart](/agentos/docs/quickstart-embedded). +The embedding process is trusted, so `software.install` accepts local package +paths as well as URLs. Read more in the +[embedded API quickstart](/agentos/docs/quickstart-embedded). diff --git a/docs/content/docs/versus-sandbox.mdx b/docs/content/docs/versus-sandbox.mdx index eac8566bbc..ba2cffab47 100644 --- a/docs/content/docs/versus-sandbox.mdx +++ b/docs/content/docs/versus-sandbox.mdx @@ -6,9 +6,8 @@ skill: true - **agentOS** is a lightweight VM that runs inside your process. Near-zero cold start, low memory, and direct backend integration via [host functions](/agentos/docs/host-functions). - **Sandboxes** are full Linux environments with root access, system packages, and native binary support. -- **Embedded applications can compose both.** Embedded Core can register a full - sandbox through trusted host functions and mounts. The static hosted actor does - not expose those host capabilities. +- **You can compose both.** agentOS can mount a full sandbox into the VM through + trusted host functions and mounts. ## Comparison @@ -17,9 +16,9 @@ skill: true | **Cost** | Very low. Runs in your process. | Pay per second of uptime. | | **Startup** | Near-zero cold start (~6 ms). | Seconds to spin up. | | **Backend integration** | Direct. [Host functions](/agentos/docs/host-functions) call your functions with zero latency. | Indirect. Requires network calls back to your backend. | -| **Credentials** | Embedded [host functions](/agentos/docs/host-functions) keep credentials on the host. Hosted VMs use serializable config. | Must be injected into the sandbox environment. | +| **Credentials** | [Host functions](/agentos/docs/host-functions) keep credentials on the host. | Must be injected into the sandbox environment. | | **Permissions** | Granular, deny-by-default. | Coarse-grained (container-level). | -| **Infrastructure** | Static actor binary or embedded Core | Vendor account + API keys. | +| **Infrastructure** | An npm package in your backend, or [hosted VMs](/agentos/docs/quickstart-hosted). | Vendor account + API keys. | | **Best for** | Coding, file manipulation, scripting, API calls, orchestration. | Browsers, desktop automation, native compilation, dev servers. | ## When to use each @@ -45,13 +44,10 @@ Spin up a sandbox when the workload needs a real Linux kernel: ### Both together -Embedded applications can use agentOS with -[external sandboxes](/agentos/docs/sandboxes) when a workflow needs both: +Use agentOS with [external sandboxes](/agentos/docs/sandboxes) when a workflow +needs both: - Guest code runs in the agentOS VM with access to configured bindings and permissions - Sandbox spins up on demand for heavy tasks - Sandbox filesystem is mounted into the VM as a native directory - Guest code reads and writes sandbox files the same way it reads local files - -This composition requires trusted in-process callbacks and is therefore not a -hosted actor feature. diff --git a/docs/content/use-cases/index.mdx b/docs/content/use-cases/index.mdx index 4b516df075..71e714bcce 100644 --- a/docs/content/use-cases/index.mdx +++ b/docs/content/use-cases/index.mdx @@ -22,7 +22,7 @@ Purpose-built for agents that write, test, and deploy code autonomously. Run long-lived commands asynchronously and observe their bounded output. -- Durable files survive actor sleep and restart +- Durable files survive VM recreation - Spawn and signal independent guest processes - Recover bounded output by sequence cursor - Monitor progress in real-time @@ -58,7 +58,7 @@ generates reports on a schedule. Use agentOS as the execution layer beneath an external workflow system. - Isolated command execution -- Durable files and configuration +- Durable files - Scheduled command runs - Typed failures and resource limits diff --git a/docs/sidebar.json b/docs/sidebar.json index a32fd8181c..863a1bd7cf 100644 --- a/docs/sidebar.json +++ b/docs/sidebar.json @@ -9,7 +9,12 @@ "icon": "faSquareInfo" }, { - "title": "Quickstart", + "title": "Quickstart (Hosted)", + "href": "/agentos/docs/quickstart-hosted", + "icon": "faForwardFast" + }, + { + "title": "Quickstart (Library)", "href": "/agentos/docs/quickstart", "icon": "faForwardFast" }, @@ -40,16 +45,6 @@ } ] }, - { - "title": "Scheduling", - "pages": [ - { - "title": "Crons & Loops", - "href": "/agentos/docs/cron", - "icon": "faClock" - } - ] - }, { "title": "Operating System", "pages": [ @@ -68,6 +63,11 @@ "href": "/agentos/docs/processes", "icon": "faTerminal" }, + { + "title": "Crons & Schedules", + "href": "/agentos/docs/cron", + "icon": "faClock" + }, { "title": "Networking & Previews", "href": "/agentos/docs/networking", @@ -110,6 +110,14 @@ { "title": "Reference", "pages": [ + { + "title": "Hosted HTTP API", + "href": "/agentos/docs/hosted-api" + }, + { + "title": "Self-Hosting (Hosted)", + "href": "/agentos/docs/self-hosting" + }, { "title": "Custom Software", "collapsible": true, @@ -185,7 +193,11 @@ { "title": "Packages & Command Resolution", "href": "/agentos/docs/architecture/packages-and-command-resolution" - }, + }, + { + "title": "Software Installation & Preloading", + "href": "/agentos/docs/architecture/software-cache" + }, { "title": "Compiler Toolchain", "href": "/agentos/docs/architecture/compiler-toolchain" diff --git a/examples/embedded/config-reference.ts b/examples/embedded/config-reference.ts index 6e1978942f..0ee1bbc6dc 100644 --- a/examples/embedded/config-reference.ts +++ b/examples/embedded/config-reference.ts @@ -1,20 +1,25 @@ import { AgentOs, nodeModulesMount } from "@rivet-dev/agentos-core"; -// Common embedded AgentOs.create() configuration. Embedded Core additionally -// permits trusted host resources that the hosted actor intentionally forbids. +// Common AgentOs.create() configuration. The embedding process is trusted, so +// it may also configure host resources such as host directories and local +// package paths. const vm = await AgentOs.create({ - // Durable SQLite storage for the root filesystem and Core state. - // Omit it for an in-memory VM that keeps nothing after dispose(). + // SQLite storage for VM-owned state. On its own it does not make the root + // filesystem durable; see /agentos/docs/persistence. database: { type: "sqlite_file", path: ".agentos/agentos.sqlite" }, + // Store the root filesystem in `database` so files survive VM recreation. + // Omit it for the default in-memory root, discarded on dispose(). + rootFilesystem: { + type: "native", + plugin: { id: "chunked_sqlite", config: {} }, + }, // Filesystems to mount at boot. Use nodeModulesMount() to expose a host // node_modules tree at /root/node_modules. mounts: [nodeModulesMount("/path/to/project/node_modules")], // Kernel permission policy (see /agentos/docs/permissions) and runtime caps. - // Embedded Core may also configure host bindings. permissions: { network: "allow" }, limits: { jsRuntime: { v8HeapLimitMb: 128 } }, - // Trusted embedded callers may install a local .aospkg path. The hosted actor - // accepts only remote HTTPS sources. + // Install a packed .aospkg from a trusted local path at boot. software: [{ packagePath: "/path/to/tool.aospkg" }], // Also install the default software bundle (sh + coreutils). Defaults to true; // set false for a bare VM with only the software you list. diff --git a/examples/embedded/host-functions-bash.ts b/examples/embedded/host-functions-bash.ts new file mode 100644 index 0000000000..25ca01e171 --- /dev/null +++ b/examples/embedded/host-functions-bash.ts @@ -0,0 +1,33 @@ +import { AgentOs } from "@rivet-dev/agentos-core"; +import { z } from "zod"; + +// The handler runs on the host, so the API key never enters the VM. +const vm = await AgentOs.create({ + hostFunctions: { + weather: { + forecast: { + inputSchema: z + .object({ city: z.string() }) + .describe("Get the weather forecast for a city"), + execute: async ({ city }) => { + const res = await fetch( + `https://api.weather.example/forecast?city=${city}&key=${process.env.WEATHER_API_KEY}`, + ); + return res.json(); + }, + }, + }, + }, +}); + +try { + // The collection is projected into the VM as an `agentos-weather` command, + // so it composes with pipes and redirects like any other program. + const result = await vm.process.exec( + "agentos-weather forecast --city Paris > /tmp/forecast.json && wc -c < /tmp/forecast.json", + { output: { capture: "all" } }, + ); + console.log(result.outcome === "succeeded" ? result.stdout : result.error); +} finally { + await vm.dispose(); +} diff --git a/examples/embedded/host-functions-javascript.ts b/examples/embedded/host-functions-javascript.ts new file mode 100644 index 0000000000..71cbe9e42a --- /dev/null +++ b/examples/embedded/host-functions-javascript.ts @@ -0,0 +1,36 @@ +import { AgentOs } from "@rivet-dev/agentos-core"; +import { z } from "zod"; + +// The handler runs on the host, so the API key never enters the VM. +const vm = await AgentOs.create({ + hostFunctions: { + weather: { + forecast: { + inputSchema: z + .object({ city: z.string() }) + .describe("Get the weather forecast for a city"), + execute: async ({ city }) => { + const res = await fetch( + `https://api.weather.example/forecast?city=${city}&key=${process.env.WEATHER_API_KEY}`, + ); + return res.json(); + }, + }, + }, + }, +}); + +try { + // Guest JavaScript gets each collection as a global, and each host function + // as an async function. + const result = await vm.javascript.execute( + ` + const forecast = await weather.forecast({ city: "Paris" }); + console.log(forecast); + `, + { output: { capture: "all" } }, + ); + console.log(result.outcome === "succeeded" ? result.stdout : result.error); +} finally { + await vm.dispose(); +} diff --git a/examples/embedded/host-functions-python.ts b/examples/embedded/host-functions-python.ts new file mode 100644 index 0000000000..025f050f45 --- /dev/null +++ b/examples/embedded/host-functions-python.ts @@ -0,0 +1,43 @@ +import { AgentOs } from "@rivet-dev/agentos-core"; +import { z } from "zod"; + +// The handler runs on the host, so the API key never enters the VM. +const vm = await AgentOs.create({ + hostFunctions: { + weather: { + forecast: { + inputSchema: z + .object({ city: z.string() }) + .describe("Get the weather forecast for a city"), + execute: async ({ city }) => { + const res = await fetch( + `https://api.weather.example/forecast?city=${city}&key=${process.env.WEATHER_API_KEY}`, + ); + return res.json(); + }, + }, + }, + }, +}); + +try { + // Python calls the `agentos-weather` command like any other program. + const result = await vm.python.execute( + ` +import json +import subprocess + +completed = subprocess.run( + ["agentos-weather", "forecast", "--city", "Paris"], + check=True, + capture_output=True, + text=True, +) +print(json.loads(completed.stdout)["result"]) +`, + { output: { capture: "all" } }, + ); + console.log(result.outcome === "succeeded" ? result.stdout : result.error); +} finally { + await vm.dispose(); +} diff --git a/examples/embedded/host-functions.ts b/examples/embedded/host-functions.ts index 858994d7d2..97c00ea954 100644 --- a/examples/embedded/host-functions.ts +++ b/examples/embedded/host-functions.ts @@ -1,9 +1,8 @@ import { AgentOs } from "@rivet-dev/agentos-core"; import { z } from "zod"; -// Host functions are defined exactly as they are for the actor. Pass them to -// AgentOs.create() and `execute` runs in this host process, with its input typed -// by its own schema. +// Pass host functions to AgentOs.create(). `execute` runs in this host process, +// with its input typed by its own schema. const vm = await AgentOs.create({ hostFunctions: { weather: { diff --git a/examples/embedded/mounts.ts b/examples/embedded/mounts.ts index 69fd96a252..5cbeb50ecf 100644 --- a/examples/embedded/mounts.ts +++ b/examples/embedded/mounts.ts @@ -1,8 +1,8 @@ import { AgentOs } from "@rivet-dev/agentos-core"; -// Embedded applications configure filesystem backends at boot. Native mount plugins (host -// directories, S3, etc.) are passed via `plugin`, each identified by an `id` -// and a `config` object. +// Configure filesystem backends at boot. Native mount plugins (host +// directories, S3, Google Drive, etc.) are passed via `plugin`, each identified +// by an `id` and a `config` object. const vm = await AgentOs.create({ mounts: [ // Host directory (read-only) @@ -14,7 +14,12 @@ const vm = await AgentOs.create({ // S3 bucket { path: "/mnt/data", - plugin: { id: "s3", config: { bucket: "my-bucket", prefix: "agent/" } }, + plugin: { + id: "chunked_s3", + config: { bucket: "my-bucket", prefix: "agent/", region: "us-east-1" }, + }, }, ], }); + +await vm.dispose(); diff --git a/examples/embedded/persistence.ts b/examples/embedded/persistence.ts index 0ae2980625..934e14278f 100644 --- a/examples/embedded/persistence.ts +++ b/examples/embedded/persistence.ts @@ -4,10 +4,16 @@ import { AgentOs, type AgentOsOptions } from "@rivet-dev/agentos-core"; mkdirSync(".agentos", { recursive: true }); const options = { + // The SQLite file that stores the VM's durable state. database: { type: "sqlite_file", path: resolve(".agentos/agentos.sqlite"), }, + // Store the root filesystem in that database instead of in memory. + rootFilesystem: { + type: "native", + plugin: { id: "chunked_sqlite", config: {} }, + }, } satisfies AgentOsOptions; let vm = await AgentOs.create(options); diff --git a/examples/embedded/quickstart.ts b/examples/embedded/quickstart.ts new file mode 100644 index 0000000000..bf1eaa5639 --- /dev/null +++ b/examples/embedded/quickstart.ts @@ -0,0 +1,72 @@ +// docs:start create +import { AgentOs } from "@rivet-dev/agentos-core"; + +// `AgentOs.create()` boots a VM inside this process and returns a handle to it. +const vm = await AgentOs.create(); +// docs:end create + +try { + // docs:start bash + const result = await vm.process.exec("echo hello && ls /home/agentos", { + output: { capture: "all" }, + }); + + console.log(result.stdout); + // docs:end bash + + // docs:start filesystem + await vm.filesystem.writeFile("/home/agentos/hello.txt", "Hello, world!"); + + const file = await vm.filesystem.readFile("/home/agentos/hello.txt"); + // docs:end filesystem + console.log(new TextDecoder().decode(file)); + + // docs:start dev-server + await vm.filesystem.writeFile( + "/home/agentos/server.js", + `require("node:http") + .createServer((req, res) => res.end("hello")) + .listen(3000);`, + ); + + await vm.process.spawn("node", ["/home/agentos/server.js"]); + + // The listener stays inside the VM. Send it a request from the host. + const response = await vm.network.httpRequest({ port: 3000, path: "/" }); + console.log(new TextDecoder().decode(response.body)); + // docs:end dev-server + + // docs:start languages + await vm.javascript.execute("console.log(1 + 2)"); + await vm.typescript.execute("const n: number = 3; console.log(n)"); + + const answer = await vm.python.evaluate("21 * 2"); + // docs:end languages + console.log(answer.outcome === "succeeded" ? answer.value : answer.error); + + // docs:start cron + vm.cron.schedule({ + id: "cleanup", + schedule: "0 * * * *", + action: { type: "exec", command: "rm", args: ["-rf", "/tmp/cache"] }, + }); + // docs:end cron + + // docs:start software + // Registry packages are published next to a manifest with each release. + const base = "https://releases.rivet.dev/agent-os/latest/software/"; + const manifest = (await (await fetch(`${base}manifest.json`)).json()) as { + artifacts: { name: string; digest: string; path: string }[]; + }; + const ripgrep = manifest.artifacts.find((artifact) => artifact.name === "ripgrep"); + if (!ripgrep) throw new Error("ripgrep is not in the manifest"); + + await vm.software.install({ + type: "url", + url: new URL(ripgrep.path, base).href, + expectedDigest: ripgrep.digest, + }); + // docs:end software +} finally { + await vm.dispose(); +} diff --git a/examples/embedded/software-install.ts b/examples/embedded/software-install.ts new file mode 100644 index 0000000000..d17349b25f --- /dev/null +++ b/examples/embedded/software-install.ts @@ -0,0 +1,36 @@ +import { AgentOs } from "@rivet-dev/agentos-core"; + +// Registry software is published as digest-addressed .aospkg files next to a +// manifest. Use a release version, or `latest`. +const base = "https://releases.rivet.dev/agent-os/latest/software/"; +const manifest = (await (await fetch(`${base}manifest.json`)).json()) as { + artifacts: { name: string; digest: string; path: string }[]; +}; +const ripgrep = manifest.artifacts.find((artifact) => artifact.name === "ripgrep"); +if (!ripgrep) throw new Error("ripgrep is not in the manifest"); + +const vm = await AgentOs.create(); + +try { + // Install a package while the VM is running. The bytes are verified against + // the digest before the package is projected into /opt/agentos. + const installed = await vm.software.install({ + type: "url", + url: new URL(ripgrep.path, base).href, + expectedDigest: ripgrep.digest, + }); + console.log(installed.packageName, installed.version, installed.commands); + + const result = await vm.process.execFile("rg", ["--version"], { + output: { capture: "all" }, + }); + console.log(result.stdout); + + // Packages installed through this API, and every command the VM provides. + console.log(vm.software.installed()); + console.log(await vm.software.list()); + + await vm.software.uninstall(installed.packageId); +} finally { + await vm.dispose(); +} diff --git a/examples/embedded/vm.ts b/examples/embedded/vm.ts index 56a8b82d2c..c9c71e897e 100644 --- a/examples/embedded/vm.ts +++ b/examples/embedded/vm.ts @@ -1,17 +1,8 @@ // docs:start boot -import { mkdirSync } from "node:fs"; -import { resolve } from "node:path"; import { AgentOs } from "@rivet-dev/agentos-core"; -// Embed a VM with the agentOS package. There is no actor runtime or -// client/server split. `AgentOs.create()` returns a handle to the VM. -mkdirSync(".agentos", { recursive: true }); -const vm = await AgentOs.create({ - database: { - type: "sqlite_file", - path: resolve(".agentos/agentos.sqlite"), - }, -}); +// `AgentOs.create()` boots a VM inside this process and returns a handle to it. +const vm = await AgentOs.create(); const result = await vm.process.exec("echo hello"); console.log(result.stdout); // "hello\n"