From 2c52eeb5b36b896ed4573f1cf871877dad03146c Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 3 Jul 2026 16:23:05 -0700 Subject: [PATCH] fix(agentos): use bindings terminology --- examples/agent-to-agent/README.md | 2 +- examples/agent-to-agent/client.ts | 2 +- examples/agent-to-agent/server.ts | 6 +- examples/bindings/README.md | 4 +- examples/bindings/server.ts | 6 +- examples/browser-terminal/README.md | 5 +- examples/browser-terminal/package.json | 5 - examples/browser-terminal/server.ts | 2 - examples/browser-terminal/src/ActorView.tsx | 10 +- examples/browser-terminal/src/rivet.ts | 3 +- examples/browser-terminal/tsconfig.json | 8 +- .../crash-course/agent-to-agent-server.ts | 6 +- examples/crash-course/sandbox-stubs.d.ts | 56 -------- examples/crash-course/sandbox.ts | 24 ++-- examples/filesystem/isolation.ts | 2 +- examples/quickstart/bindings/README.md | 25 ++++ examples/quickstart/bindings/index.ts | 61 +++++++++ .../{tools => bindings}/package.json | 2 +- .../{tools => bindings}/tsconfig.json | 0 examples/quickstart/package.json | 2 +- examples/quickstart/sandbox/README.md | 6 +- examples/quickstart/sandbox/index.ts | 104 +++------------ examples/quickstart/tools/README.md | 25 ---- examples/quickstart/tools/index.ts | 126 ------------------ examples/sandbox/README.md | 13 +- examples/sandbox/client.ts | 42 ++---- examples/sandbox/server.ts | 46 ++++--- .../src/{toolkit.ts => bindings.ts} | 37 ++--- packages/agentos-sandbox/src/index.ts | 4 +- .../tests/vm-integration.test.ts | 32 ++--- packages/agentos/tests/actor.test.ts | 10 +- packages/agentos/tests/config.test.ts | 12 ++ packages/core/src/agent-os.ts | 24 +++- packages/core/src/host-tools.ts | 76 +++++++++-- packages/core/src/index.ts | 6 + packages/core/src/options-schema.ts | 21 ++- packages/core/src/types.ts | 9 +- packages/core/tests/host-tools.test.ts | 74 +++++----- packages/core/tests/options-schema.test.ts | 22 +++ .../core/tests/public-api-exports.test.ts | 18 ++- .../core/tests/sandbox-integration.test.ts | 22 +-- pnpm-lock.yaml | 37 ++--- website/public/docs/docs/bindings.md | 4 +- website/public/docs/docs/core.md | 8 +- website/public/docs/docs/crash-course.md | 4 +- website/public/docs/docs/sandbox.md | 12 +- website/src/content/docs/docs/bindings.mdx | 2 +- website/src/content/docs/docs/core.mdx | 6 + .../src/content/docs/docs/crash-course.mdx | 2 +- website/src/content/docs/docs/sandbox.mdx | 15 ++- website/src/pages/index.astro | 45 +++---- website/src/pages/registry/[slug].astro | 15 ++- website/src/pages/registry/index.astro | 4 +- 53 files changed, 538 insertions(+), 576 deletions(-) delete mode 100644 examples/crash-course/sandbox-stubs.d.ts create mode 100644 examples/quickstart/bindings/README.md create mode 100644 examples/quickstart/bindings/index.ts rename examples/quickstart/{tools => bindings}/package.json (92%) rename examples/quickstart/{tools => bindings}/tsconfig.json (100%) delete mode 100644 examples/quickstart/tools/README.md delete mode 100644 examples/quickstart/tools/index.ts rename packages/agentos-sandbox/src/{toolkit.ts => bindings.ts} (87%) create mode 100644 packages/agentos/tests/config.test.ts diff --git a/examples/agent-to-agent/README.md b/examples/agent-to-agent/README.md index d3a2dd96f..a9c9b4c99 100644 --- a/examples/agent-to-agent/README.md +++ b/examples/agent-to-agent/README.md @@ -9,7 +9,7 @@ Run two agents in separate isolated VMs and let one delegate to the other. The w ## How it works -Both agents are independent `agentOS` VMs registered under one `setup`. The writer is given a `review` binding: a host-side tool the agent can invoke by name. When the writer runs `agentos-review submit --path ...`, the binding's `execute` runs on the host, where it reads the file out of the writer's VM, copies it into the reviewer's VM, opens a reviewer session, and prompts the reviewer to review the code. The review text is returned to the writer as the binding's result. The two VMs never touch directly — the host bridge is the only path between them. +Both agents are independent `agentOS` VMs registered under one `setup`. The writer is given a `review` binding: a host-side function the agent can invoke by name. When the writer runs `agentos-review submit --path ...`, the binding's `execute` runs on the host, where it reads the file out of the writer's VM, copies it into the reviewer's VM, opens a reviewer session, and prompts the reviewer to review the code. The review text is returned to the writer as the binding's result. The two VMs never touch directly — the host bridge is the only path between them. ## Run it diff --git a/examples/agent-to-agent/client.ts b/examples/agent-to-agent/client.ts index 8b165cf65..aea8ba11b 100644 --- a/examples/agent-to-agent/client.ts +++ b/examples/agent-to-agent/client.ts @@ -8,7 +8,7 @@ const sessionId = await writerAgent.createSession("claude", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! }, }); -// The writer calls the `review` toolkit, which bridges to the reviewer VM. +// The writer calls the `review` binding, which bridges to the reviewer VM. await writerAgent.sendPrompt( sessionId, "Write a small REST API, then send it to the review agent for review.", diff --git a/examples/agent-to-agent/server.ts b/examples/agent-to-agent/server.ts index 6f095c97c..4bb272753 100644 --- a/examples/agent-to-agent/server.ts +++ b/examples/agent-to-agent/server.ts @@ -28,14 +28,14 @@ async function reviewCode(code: string): Promise { return result.text; } -// The writer agent gets a `review` toolkit. When the writer runs +// The writer agent gets a `review` binding group. When the writer runs // `agentos-review submit`, the bridge above executes on the host. const writer = agentOS({ - toolKits: [ + bindings: [ { name: "review", description: "Send code to the reviewer agent and get back a review.", - tools: { + bindings: { submit: { description: "Submit the full contents of a file to the reviewer agent for review. Returns the reviewer's feedback as text.", diff --git a/examples/bindings/README.md b/examples/bindings/README.md index fec6e8671..710f092dc 100644 --- a/examples/bindings/README.md +++ b/examples/bindings/README.md @@ -5,11 +5,11 @@ category: "Reference" order: 3 --- -Give an agent access to your own host code—API calls, database lookups, internal services—without writing a tool from scratch. Reach for bindings when the agent needs to call back into your application and you want type-safe inputs plus an auto-generated CLI surface inside the VM. +Give an agent access to your own host code—API calls, database lookups, internal services—without writing a binding from scratch. Reach for bindings when the agent needs to call back into your application and you want type-safe inputs plus an auto-generated CLI surface inside the VM. ## How it works -A binding group bundles a `name`, a `description`, and a map of named tools. Each tool declares a Zod `inputSchema`, an `execute` handler that runs on the host, and optional `examples`. You pass the groups to `agentOS({ toolKits: [...] })`, and Agent OS exposes every group to the agent as a CLI command at `/usr/local/bin/agentos-{name}` inside the VM. When the agent invokes the command, the Zod schema validates the arguments and the handler executes host-side, returning the result back to the guest. The client side stays thin: create a session and send a prompt, and the agent decides when to call the binding. +A binding group bundles a `name`, a `description`, and a map of named bindings. Each binding declares a Zod `inputSchema`, an `execute` handler that runs on the host, and optional `examples`. You pass the groups to `agentOS({ bindings: [...] })`, and Agent OS exposes every group to the agent as a CLI command at `/usr/local/bin/agentos-{name}` inside the VM. When the agent invokes the command, the Zod schema validates the arguments and the handler executes host-side, returning the result back to the guest. The client side stays thin: create a session and send a prompt, and the agent decides when to call the binding. ## Run it diff --git a/examples/bindings/server.ts b/examples/bindings/server.ts index 2b7ce4c91..961c81aa5 100644 --- a/examples/bindings/server.ts +++ b/examples/bindings/server.ts @@ -1,13 +1,13 @@ import { agentOS, setup } from "@rivet-dev/agentos"; import { z } from "zod"; -// Define a group of bindings (host functions). Each tool has a Zod input +// Define a group of bindings (host functions). Each binding has a Zod input // schema and an `execute` handler that runs on the host. The group is exposed to // the agent as a CLI command at /usr/local/bin/agentos-{name} inside the VM. const weatherBindings = { name: "weather", description: "Weather data bindings", - tools: { + bindings: { forecast: { description: "Get the weather forecast for a city", inputSchema: z.object({ @@ -28,7 +28,7 @@ const weatherBindings = { }; const vm = agentOS({ - toolKits: [weatherBindings], + bindings: [weatherBindings], }); export const registry = setup({ use: { vm } }); diff --git a/examples/browser-terminal/README.md b/examples/browser-terminal/README.md index eef3e036e..a65d8ff33 100644 --- a/examples/browser-terminal/README.md +++ b/examples/browser-terminal/README.md @@ -66,9 +66,8 @@ Override the web→server endpoint with `VITE_AGENTOS_ENDPOINT` (default ## Notes -- Software: `@agentos-software/common` (provides `sh` + coreutils) plus `git`, - `curl`, `ripgrep`, `jq`, and `sqlite3`. Agent OS has no vim/editor package, so - there is no in-VM editor. +- Software: `@agentos-software/common` provides `sh` plus the standard shell + tools. Agent OS has no vim/editor package, so there is no in-VM editor. - The shipped actor has no `listShells` action and keeps no server-side scrollback, so reconnect re-adopts saved shell ids and resumes **live** output only (history from before the reload is not replayed). Stale ids (VM recreated) diff --git a/examples/browser-terminal/package.json b/examples/browser-terminal/package.json index 538b61e6d..222744478 100644 --- a/examples/browser-terminal/package.json +++ b/examples/browser-terminal/package.json @@ -14,11 +14,6 @@ }, "dependencies": { "@agentos-software/common": "link:../../../secure-exec/registry/software/common", - "@agentos-software/curl": "link:../../../secure-exec/registry/software/curl", - "@agentos-software/git": "link:../../../secure-exec/registry/software/git", - "@agentos-software/jq": "link:../../../secure-exec/registry/software/jq", - "@agentos-software/ripgrep": "link:../../../secure-exec/registry/software/ripgrep", - "@agentos-software/sqlite3": "link:../../../secure-exec/registry/software/sqlite3", "@rivet-dev/agentos": "workspace:*", "@rivetkit/react": "catalog:rivetkit", "@xterm/addon-fit": "^0.10.0", diff --git a/examples/browser-terminal/server.ts b/examples/browser-terminal/server.ts index 6aac03e59..a2a0e7f15 100644 --- a/examples/browser-terminal/server.ts +++ b/examples/browser-terminal/server.ts @@ -1,8 +1,6 @@ import common from "@agentos-software/common"; import { agentOS, setup } from "@rivet-dev/agentos"; -// TEMP (local debug): only `common` (sha 42d8146) has valid projected packs; -// `sqlite3` (efc374f) is missing its `dist/package`, so it's dropped for now. const shellVm = agentOS({ software: [common], }); diff --git a/examples/browser-terminal/src/ActorView.tsx b/examples/browser-terminal/src/ActorView.tsx index 155a39c63..4444e4668 100644 --- a/examples/browser-terminal/src/ActorView.tsx +++ b/examples/browser-terminal/src/ActorView.tsx @@ -16,6 +16,12 @@ interface ShellExitPayload { shellId: string; } +interface ShellEventConnection { + on(name: "shellData", cb: (payload: ShellDataPayload) => void): () => void; + on(name: "shellStderr", cb: (payload: ShellDataPayload) => void): () => void; + on(name: "shellExit", cb: (payload: ShellExitPayload) => void): () => void; +} + function toBytes(data: unknown): Uint8Array { if (data instanceof Uint8Array) return data; if (data instanceof ArrayBuffer) return new Uint8Array(data); @@ -89,9 +95,7 @@ export function ActorView({ actorId }: { actorId: string }) { useEffect(() => { if (!conn) return; - const events = conn as unknown as { - on(name: string, cb: (p: never) => void): () => void; - }; + const events = conn as ShellEventConnection; const offData = events.on("shellData", (p: ShellDataPayload) => dispatchData(p.shellId, toBytes(p.data)), ); diff --git a/examples/browser-terminal/src/rivet.ts b/examples/browser-terminal/src/rivet.ts index a2c4c1241..65f706155 100644 --- a/examples/browser-terminal/src/rivet.ts +++ b/examples/browser-terminal/src/rivet.ts @@ -1,9 +1,10 @@ import { createRivetKit } from "@rivetkit/react"; +import type { registry } from "../server"; const ENDPOINT = (import.meta.env.VITE_AGENTOS_ENDPOINT as string | undefined) ?? "http://localhost:6420"; -export const { useActor } = createRivetKit(ENDPOINT); +export const { useActor } = createRivetKit(ENDPOINT); export const ACTOR_NAME = "shellVm"; diff --git a/examples/browser-terminal/tsconfig.json b/examples/browser-terminal/tsconfig.json index c97ccc68f..d93161ff6 100644 --- a/examples/browser-terminal/tsconfig.json +++ b/examples/browser-terminal/tsconfig.json @@ -12,7 +12,13 @@ "esModuleInterop": true, "resolveJsonModule": true, "allowImportingTsExtensions": true, - "verbatimModuleSyntax": false + "verbatimModuleSyntax": false, + "baseUrl": ".", + "paths": { + "@rivet-dev/agentos": ["../../packages/agentos/src/index.ts"], + "@rivet-dev/agentos/client": ["../../packages/agentos/src/client.ts"], + "@rivet-dev/agentos/react": ["../../packages/agentos/src/react.ts"] + } }, "include": ["server.ts", "vite.config.ts", "src"] } diff --git a/examples/crash-course/agent-to-agent-server.ts b/examples/crash-course/agent-to-agent-server.ts index 5365fa342..b85a796e4 100644 --- a/examples/crash-course/agent-to-agent-server.ts +++ b/examples/crash-course/agent-to-agent-server.ts @@ -6,15 +6,15 @@ import pi from "@agentos-software/pi"; // The reviewer is its own isolated agent VM. const reviewer = agentOS({ software: [pi] }); -// The coder gets a `review` toolkit it can call itself: it copies a file from the +// The coder gets a `review` binding it can call itself: it copies a file from the // coder's VM into the reviewer's VM and asks the reviewer to review it. const coder = agentOS({ software: [pi], - toolKits: [ + bindings: [ { name: "review", description: "Send a file to the reviewer agent and get back a review.", - tools: { + bindings: { submit: { description: "Submit a file path for review by the reviewer agent.", inputSchema: z.object({ path: z.string() }), diff --git a/examples/crash-course/sandbox-stubs.d.ts b/examples/crash-course/sandbox-stubs.d.ts deleted file mode 100644 index 034f202f5..000000000 --- a/examples/crash-course/sandbox-stubs.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Local ambient stubs for this example only. - * - * The sandbox integration ships as `@rivet-dev/agentos-sandbox`, which depends on - * the third-party `sandbox-agent` package. Their runtimes pull native/Docker - * deps, so these declarations model just enough of the public surface for the - * sandbox example to type-check. Remove once the packages are installed here. - * - * Real packages and exports: - * - `@rivet-dev/agentos-sandbox` -> `createSandboxFs`, `createSandboxBindings` - * - `sandbox-agent` -> `SandboxAgent` - * - `sandbox-agent/docker` -> `docker` - */ - -declare module "sandbox-agent" { - export class SandboxAgent { - static start(options: { sandbox: unknown }): Promise; - dispose(): Promise; - } -} - -declare module "sandbox-agent/docker" { - export function docker(options?: unknown): unknown; -} - -declare module "@rivet-dev/agentos-sandbox" { - import type { - NativeMountPluginDescriptor, - ToolKit, - } from "@rivet-dev/agentos-core"; - import type { SandboxAgent } from "sandbox-agent"; - - export interface SandboxFsOptions { - /** A connected SandboxAgent client instance. */ - client: SandboxAgent; - /** Base path to scope all operations under. Defaults to "/". */ - basePath?: string; - /** Per-request timeout for sandbox-agent HTTP calls. */ - timeoutMs?: number; - /** Maximum file size allowed for buffered pread/truncate fallbacks. */ - maxFullReadBytes?: number; - } - export interface SandboxToolkitOptions { - /** A connected SandboxAgent client instance. */ - client: SandboxAgent; - } - /** - * Build the mount plugin descriptor that projects the sandbox filesystem into - * the VM. Use it as the `plugin` of a `{ path, plugin }` mount entry. - */ - export function createSandboxFs( - options: SandboxFsOptions, - ): NativeMountPluginDescriptor; - /** Build a toolkit that exposes the sandbox's process management. */ - export function createSandboxBindings(options: SandboxToolkitOptions): ToolKit; -} diff --git a/examples/crash-course/sandbox.ts b/examples/crash-course/sandbox.ts index d7767cd72..2e52ebb76 100644 --- a/examples/crash-course/sandbox.ts +++ b/examples/crash-course/sandbox.ts @@ -1,18 +1,24 @@ -import { agentOS, setup } from "@rivet-dev/agentos"; -import { createSandboxFs, createSandboxBindings } from "@rivet-dev/agentos-sandbox"; +import { AgentOs } from "@rivet-dev/agentos-core"; +import { + createSandboxBindings, + createSandboxFs, +} from "@rivet-dev/agentos-sandbox"; import { SandboxAgent } from "sandbox-agent"; import { docker } from "sandbox-agent/docker"; const sandbox = await SandboxAgent.start({ sandbox: docker() }); -const vm = agentOS({ - // Toolkits let the agent control the sandbox - toolKits: [createSandboxBindings({ client: sandbox })], - // Mounts let the agent read the sandbox filesystem (optional) +const vm = await AgentOs.create({ + // Bindings let the agent control the sandbox. + bindings: [createSandboxBindings({ client: sandbox })], + // Mounts let the agent read the sandbox filesystem. mounts: [ - { path: "/home/agentos/sandbox", plugin: createSandboxFs({ client: sandbox }) }, + { + path: "/home/agentos/sandbox", + plugin: createSandboxFs({ client: sandbox }), + }, ], }); -export const registry = setup({ use: { vm } }); -registry.start(); +await vm.dispose(); +await sandbox.dispose(); diff --git a/examples/filesystem/isolation.ts b/examples/filesystem/isolation.ts index 9b908054f..c6a75ac01 100644 --- a/examples/filesystem/isolation.ts +++ b/examples/filesystem/isolation.ts @@ -1,3 +1,4 @@ +import { existsSync } from "node:fs"; import { createClient } from "@rivet-dev/agentos/client"; import type { registry } from "./server"; @@ -26,7 +27,6 @@ const bytes = await agent.readFile("/home/agentos/seed.json"); console.log("host readFile:", new TextDecoder().decode(bytes)); // The same path on the real host disk does not exist. The VFS is isolated. -const { existsSync } = await import("node:fs"); console.log( "host disk sees /home/agentos/seed.json?", existsSync("/home/agentos/seed.json") ? "YES (unexpected!)" : "NO - isolated from host", diff --git a/examples/quickstart/bindings/README.md b/examples/quickstart/bindings/README.md new file mode 100644 index 000000000..f1451bdcf --- /dev/null +++ b/examples/quickstart/bindings/README.md @@ -0,0 +1,25 @@ +--- +title: "Bindings" +description: "Define host-side bindings callable from inside the VM as CLI commands." +category: "Quickstart" +order: 9 +--- + +Expose host-side functions to code running inside the VM. Reach for this when guest code needs to call back out to capabilities you implement on the host — weather lookups, calculators, database access — without granting it direct host access. + +## How it works + +You declare binding groups with `bindingGroup`, where each `binding` pairs a Zod `inputSchema` with an `execute` function that runs on the host. Pass the groups to `AgentOs.create({ bindings })` and Agent OS installs CLI commands inside the VM. This example wires up `weather` and `calc`, then invokes each through `agentos-weather` and `agentos-calc`. + +## Run it + +```bash +npm install +npx tsx index.ts +``` + +Prints the weather and calculator results returned from the host. + +## Source + +View the source on GitHub: https://github.com/rivet-dev/agent-os/tree/main/examples/quickstart/bindings diff --git a/examples/quickstart/bindings/index.ts b/examples/quickstart/bindings/index.ts new file mode 100644 index 000000000..2bf0c8e89 --- /dev/null +++ b/examples/quickstart/bindings/index.ts @@ -0,0 +1,61 @@ +// Bindings: define functions that execute on the host and are callable +// from inside the VM as CLI commands. + +import { AgentOs, binding, bindingGroup } from "@rivet-dev/agentos-core"; +import { z } from "zod"; + +const weatherBindings = bindingGroup({ + name: "weather", + description: "Look up weather information for cities.", + bindings: { + get: binding({ + description: "Get the current weather for a city.", + inputSchema: z.object({ + city: z.string().describe("City name (e.g. 'London')."), + }), + execute: async (input) => { + const { city } = input; + return { + city, + temperature: 18, + conditions: "partly cloudy", + humidity: 65, + }; + }, + examples: [ + { description: "Get London weather", input: { city: "London" } }, + ], + }), + }, +}); + +const calcBindings = bindingGroup({ + name: "calc", + description: "Simple calculator operations.", + bindings: { + add: binding({ + description: "Add two numbers.", + inputSchema: z.object({ a: z.number(), b: z.number() }), + execute: (input) => ({ result: input.a + input.b }), + }), + }, +}); + +const vm = await AgentOs.create({ + bindings: [weatherBindings, calcBindings], + permissions: { + fs: "allow", + network: "allow", + childProcess: "allow", + env: "allow", + binding: "allow", + }, +}); + +const weather = await vm.exec("agentos-weather get --city London"); +console.log("Weather:", JSON.stringify(weather)); + +const sum = await vm.exec("agentos-calc add --a 10 --b 32"); +console.log("Sum:", JSON.stringify(sum)); + +await vm.dispose(); diff --git a/examples/quickstart/tools/package.json b/examples/quickstart/bindings/package.json similarity index 92% rename from examples/quickstart/tools/package.json rename to examples/quickstart/bindings/package.json index 5982f0c93..d329d4b4a 100644 --- a/examples/quickstart/tools/package.json +++ b/examples/quickstart/bindings/package.json @@ -1,5 +1,5 @@ { - "name": "@rivet-dev/agentos-example-quickstart-tools", + "name": "@rivet-dev/agentos-example-quickstart-bindings", "version": "0.1.0", "private": true, "type": "module", diff --git a/examples/quickstart/tools/tsconfig.json b/examples/quickstart/bindings/tsconfig.json similarity index 100% rename from examples/quickstart/tools/tsconfig.json rename to examples/quickstart/bindings/tsconfig.json diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 9e1ee8b1a..f6b45a621 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -13,7 +13,7 @@ "processes": "node --import tsx src/processes.ts", "network": "node --import tsx src/network.ts", "cron": "node --import tsx src/cron.ts", - "tools": "node --import tsx src/tools.ts", + "bindings": "node --import tsx bindings/index.ts", "agent-session": "node --import tsx src/agent-session.ts", "sandbox": "node --import tsx src/sandbox.ts", "nodejs": "node --import tsx src/nodejs.ts", diff --git a/examples/quickstart/sandbox/README.md b/examples/quickstart/sandbox/README.md index 205f241c2..7bd13e11a 100644 --- a/examples/quickstart/sandbox/README.md +++ b/examples/quickstart/sandbox/README.md @@ -1,6 +1,6 @@ --- title: "Sandbox" -description: "Mount a Docker sandbox filesystem and run commands through the sandbox toolkit." +description: "Mount a Docker sandbox filesystem and run commands through sandbox bindings." category: "Quickstart" order: 11 --- @@ -9,7 +9,7 @@ Back a VM with a Docker-backed sandbox so guest reads, writes, and commands run ## How it works -A `SandboxAgent` starts a Docker container via `sandbox-agent`. Two pieces wire it into the VM: `createSandboxFs` mounts the container's filesystem at `/sandbox`, and `createSandboxToolkit` registers a `sandbox` toolkit for running commands. Files written under `/sandbox` land in the container, and tools like `run-command` and `list-processes` execute against it over the VM's tools RPC port. Set `SKIP_DOCKER=1` to no-op the example where Docker is unavailable. +The quickstart starts one Docker container for one VM. Two pieces wire it into the VM: `createSandboxFs` mounts the container's filesystem at `/sandbox`, and `createSandboxBindings` registers sandbox bindings for running commands. Files written under `/sandbox` land in the container, and commands like `run-command` and `list-processes` execute through the `agentos-sandbox` CLI. Set `SKIP_DOCKER=1` to no-op the example where Docker is unavailable. ## Run it @@ -18,7 +18,7 @@ npm install npx tsx index.ts ``` -You should see a file read back from the sandbox mount, the tools RPC port, and the output of an `echo` command plus a process listing from inside the Docker sandbox. +You should see a file read back from the sandbox mount, the output of an `echo` command, and a process listing from inside the Docker sandbox. ## Source diff --git a/examples/quickstart/sandbox/index.ts b/examples/quickstart/sandbox/index.ts index 3978b42b3..788a9a837 100644 --- a/examples/quickstart/sandbox/index.ts +++ b/examples/quickstart/sandbox/index.ts @@ -1,13 +1,15 @@ // Sandbox extension: mount a Docker sandbox filesystem and run commands. // // Requires Docker. Starts a sandbox-agent container, mounts its filesystem -// at /sandbox, and registers the sandbox toolkit for running commands. +// at /sandbox, and registers sandbox bindings for running commands. import { AgentOs } from "@rivet-dev/agentos-core"; import { + createSandboxBindings, createSandboxFs, - createSandboxToolkit, } from "@rivet-dev/agentos-sandbox"; +import { SandboxAgent } from "sandbox-agent"; +import { docker } from "sandbox-agent/docker"; const SANDBOX_QUICKSTART_PERMISSIONS = { fs: "allow", @@ -18,79 +20,15 @@ const SANDBOX_QUICKSTART_PERMISSIONS = { } as const; const skipDocker = process.env.SKIP_DOCKER === "1"; -async function readToolsPort(vm: AgentOs): Promise { - let stdout = ""; - let stderr = ""; - await vm.writeFile( - "/tmp/read-tools-port.cjs", - 'process.stdout.write(process.env.AGENTOS_TOOLS_PORT||"")', - ); - const proc = vm.spawn("node", ["/tmp/read-tools-port.cjs"], { - onStdout: (data) => { - stdout += new TextDecoder().decode(data); - }, - onStderr: (data) => { - stderr += new TextDecoder().decode(data); - }, - }); - const exitCode = await vm.waitProcess(proc.pid); - if (exitCode !== 0) { - throw new Error(`Failed to read AGENTOS_TOOLS_PORT: ${stderr.trim()}`); - } - const port = stdout.trim(); - if (!port) { - throw new Error("AGENTOS_TOOLS_PORT is not set inside the VM"); - } - return port; -} - -async function callTool( - vm: AgentOs, - port: string, - toolkit: string, - tool: string, - input: Record, -): Promise { - const outFile = `/tmp/${toolkit}-${tool}-out.json`; - let stderr = ""; - const source = [ - 'import{writeFileSync as w}from"node:fs";', - `const r=await fetch("http://127.0.0.1:${port}/call",{method:"POST",headers:{"Content-Type":"application/json"},body:${JSON.stringify( - JSON.stringify({ toolkit, tool, input }), - )}});`, - `w(${JSON.stringify(outFile)},await r.text());`, - ].join(""); - await vm.writeFile("/tmp/tool-call.mjs", source); - const proc = vm.spawn("node", ["/tmp/tool-call.mjs"], { - onStderr: (data) => { - stderr += new TextDecoder().decode(data); - }, - }); - const exitCode = await vm.waitProcess(proc.pid); - if (exitCode !== 0) { - throw new Error( - `Tool call process exited with code ${exitCode}: ${stderr.trim()}`, - ); - } - return JSON.parse(new TextDecoder().decode(await vm.readFile(outFile))); -} - if (skipDocker) { console.log("Skipping sandbox quickstart because SKIP_DOCKER=1."); process.exit(0); } -const [{ SandboxAgent }, { docker }] = await Promise.all([ - import("sandbox-agent"), - import("sandbox-agent/docker"), -]); - -// Start a Docker-backed sandbox. const sandbox = await SandboxAgent.start({ sandbox: docker(), }); -// Mount the sandbox filesystem at /sandbox and register the toolkit. const vm = await AgentOs.create({ permissions: SANDBOX_QUICKSTART_PERMISSIONS, mounts: [ @@ -99,25 +37,23 @@ const vm = await AgentOs.create({ plugin: createSandboxFs({ client: sandbox }), }, ], - toolKits: [createSandboxToolkit({ client: sandbox })], + bindings: [createSandboxBindings({ client: sandbox })], }); -// Write and read a file through the mounted sandbox filesystem. -await vm.writeFile("/sandbox/hello.txt", "Hello from agentOS!"); -const content = await vm.readFile("/sandbox/hello.txt"); -console.log("Read from sandbox mount:", new TextDecoder().decode(content)); +try { + // Write and read a file through the mounted sandbox filesystem. + await vm.writeFile("/sandbox/hello.txt", "Hello from agentOS!"); + const content = await vm.readFile("/sandbox/hello.txt"); + console.log("Read from sandbox mount:", new TextDecoder().decode(content)); -const port = await readToolsPort(vm); -console.log("Tools RPC port:", port); - -const runCommandResult = await callTool(vm, port, "sandbox", "run-command", { - command: "echo", - args: ["hello from Docker sandbox"], -}); -console.log("Sandbox command:", JSON.stringify(runCommandResult)); - -const processList = await callTool(vm, port, "sandbox", "list-processes", {}); -console.log("Sandbox processes:", JSON.stringify(processList)); + const runCommandResult = await vm.exec( + "agentos-sandbox run-command --command echo --args 'hello from Docker sandbox'", + ); + console.log("Sandbox command:", JSON.stringify(runCommandResult)); -await vm.dispose(); -await sandbox.dispose(); + const processList = await vm.exec("agentos-sandbox list-processes"); + console.log("Sandbox processes:", JSON.stringify(processList)); +} finally { + await vm.dispose(); + await sandbox.dispose(); +} diff --git a/examples/quickstart/tools/README.md b/examples/quickstart/tools/README.md deleted file mode 100644 index 49ef9c3bb..000000000 --- a/examples/quickstart/tools/README.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: "Tools" -description: "Define host-side tool kits callable from inside the VM over the tools RPC server." -category: "Quickstart" -order: 9 ---- - -Expose host-side functions to code running inside the VM. Reach for this when guest code needs to call back out to capabilities you implement on the host — weather lookups, calculators, database access — without granting it direct host access. - -## How it works - -You declare tool kits with `toolKit`, where each `hostTool` pairs a Zod `inputSchema` with an `execute` function that runs on the host. Pass the kits to `AgentOs.create` and the runtime stands up a tools RPC server inside the VM, advertised through the `AGENTOS_TOOLS_PORT` environment variable. Guest Node scripts read that port and `POST` to `/call` with a `{ toolkit, tool, input }` body; the host validates the input, runs `execute`, and returns the result as JSON. This example wires up `weather` and `calc` kits, then invokes each from inside the VM. - -## Run it - -```bash -npm install -npx tsx index.ts -``` - -Prints the tools RPC port, then the weather and calculator results returned from the host. - -## Source - -View the source on GitHub: https://github.com/rivet-dev/agent-os/tree/main/examples/quickstart/tools diff --git a/examples/quickstart/tools/index.ts b/examples/quickstart/tools/index.ts deleted file mode 100644 index 410cb1f7d..000000000 --- a/examples/quickstart/tools/index.ts +++ /dev/null @@ -1,126 +0,0 @@ -// Tools: define functions that execute on the host and are callable -// from inside the VM via the tools RPC server. -// -// Each toolkit becomes a set of tools accessible at AGENTOS_TOOLS_PORT. -// Node scripts inside the VM can call the server directly with fetch. - -import { AgentOs, hostTool, toolKit } from "@rivet-dev/agentos-core"; -import { z } from "zod"; - -const weatherToolKit = toolKit({ - name: "weather", - description: "Look up weather information for cities.", - tools: { - get: hostTool({ - description: "Get the current weather for a city.", - inputSchema: z.object({ - city: z.string().describe("City name (e.g. 'London')."), - }), - execute: async (input) => { - const { city } = input; - return { - city, - temperature: 18, - conditions: "partly cloudy", - humidity: 65, - }; - }, - examples: [ - { description: "Get London weather", input: { city: "London" } }, - ], - }), - }, -}); - -const calcToolKit = toolKit({ - name: "calc", - description: "Simple calculator operations.", - tools: { - add: hostTool({ - description: "Add two numbers.", - inputSchema: z.object({ a: z.number(), b: z.number() }), - execute: (input) => ({ result: input.a + input.b }), - }), - }, -}); - -const vm = await AgentOs.create({ - toolKits: [weatherToolKit, calcToolKit], - permissions: { - fs: "allow", - network: "allow", - childProcess: "allow", - env: "allow", - binding: "allow", - }, -}); - -async function readToolsPort(): Promise { - let stdout = ""; - let stderr = ""; - await vm.writeFile( - "/tmp/read-tools-port.cjs", - 'process.stdout.write(process.env.AGENTOS_TOOLS_PORT||"")', - ); - const proc = vm.spawn("node", ["/tmp/read-tools-port.cjs"], { - onStdout: (data) => { - stdout += new TextDecoder().decode(data); - }, - onStderr: (data) => { - stderr += new TextDecoder().decode(data); - }, - }); - const exitCode = await vm.waitProcess(proc.pid); - if (exitCode !== 0) { - throw new Error(`Failed to read AGENTOS_TOOLS_PORT: ${stderr.trim()}`); - } - const port = stdout.trim(); - if (!port) { - throw new Error("AGENTOS_TOOLS_PORT is not set inside the VM"); - } - return port; -} - -const port = await readToolsPort(); -console.log("Tools RPC port:", port); - -// Helper: call a tool via the RPC server using a Node script inside the VM -async function callTool( - toolkit: string, - tool: string, - input: Record, -): Promise { - const outFile = `/tmp/${toolkit}-${tool}-out.json`; - let stderr = ""; - const source = [ - 'import{writeFileSync as w}from"node:fs";', - `const r=await fetch("http://127.0.0.1:${port}/call",{method:"POST",headers:{"Content-Type":"application/json"},body:${JSON.stringify( - JSON.stringify({ toolkit, tool, input }), - )}});`, - `w(${JSON.stringify(outFile)},await r.text());`, - ].join(""); - await vm.writeFile("/tmp/tool-call.mjs", source); - const proc = vm.spawn("node", ["/tmp/tool-call.mjs"], { - onStderr: (data) => { - stderr += new TextDecoder().decode(data); - }, - }); - const exitCode = await vm.waitProcess(proc.pid); - if (exitCode !== 0) { - throw new Error( - `Tool call process exited with code ${exitCode}: ${stderr.trim()}`, - ); - } - const data = await vm.readFile(outFile); - return JSON.parse(new TextDecoder().decode(data)); -} - -// Call the weather tool -const weather = await callTool("weather", "get", { city: "London" }); -console.log("Weather:", JSON.stringify(weather)); - -// Call the calculator tool -const sum = await callTool("calc", "add", { a: 10, b: 32 }); -console.log("Sum:", JSON.stringify(sum)); - -await vm.dispose(); diff --git a/examples/sandbox/README.md b/examples/sandbox/README.md index dbbb308c8..79e3c4570 100644 --- a/examples/sandbox/README.md +++ b/examples/sandbox/README.md @@ -1,25 +1,26 @@ --- title: "Sandbox" -description: "Mount a Sandbox Agent (Docker) filesystem into the VM and expose its process management as bindings." +description: "Mount a fresh Sandbox Agent (Docker) filesystem into each VM and expose its process management as bindings." category: "Reference" order: 5 --- -Back a VM with a real Sandbox Agent container: the sandbox's filesystem appears as a mount inside the VM, and its process management is callable as bindings. Reach for this when you want guest code to read, write, and run against a live Docker sandbox instead of the in-memory VFS. +Back each VM with its own real Sandbox Agent container: the sandbox's filesystem appears as a mount inside the VM, and its process management is callable through sandbox bindings. Reach for this when you want guest code to read, write, and run against a live Docker sandbox instead of the in-memory VFS. ## How it works -The server starts a sandbox through `SandboxAgent.start({ sandbox: docker() })`, then wires it into `agentOS` two ways. `createSandboxFs({ client })` returns a mount-plugin descriptor that projects the sandbox filesystem under `/home/agentos/sandbox`, so `vm.writeFile` and `vm.exec` operate on real container files. `createSandboxBindings({ client })` exposes the sandbox's process management as bindings, surfaced inside the VM as the `agentos-sandbox` CLI command. From the client you write a file to the mount, `exec` it, invoke a binding like `run-command`, and `spawn` a long-running process whose stdout/stderr stream back over `vm.connect()`. +The helper starts one sandbox for one VM, then wires it in two ways. `createSandboxFs({ client })` returns a mount-plugin descriptor that projects the sandbox filesystem under `/home/agentos/sandbox`, so `vm.writeFile` and `vm.exec` operate on real container files. `createSandboxBindings({ client })` exposes the sandbox's process management as the `agentos-sandbox` CLI command. The client disposes both the VM and sandbox together. + +Do not create one `SandboxAgent` at module scope and reuse it for multiple actor instances. Dynamic per-actor sandbox creation for `agentOS(...)` needs a future actor-scoped options hook; no `createOptions` callback is supported today. ## Run it ```sh npm install -npm run server # starts the VM with the sandbox mount + bindings -npm run client # writes a file, runs it, and streams process output +tsx client.ts ``` -You should see `hello` printed from a file executed inside the Docker sandbox, followed by streamed output from the spawned dev process. +You should see text read back from a file written through the sandbox mount. ## Source diff --git a/examples/sandbox/client.ts b/examples/sandbox/client.ts index 0832fb8af..8d0c0b601 100644 --- a/examples/sandbox/client.ts +++ b/examples/sandbox/client.ts @@ -1,28 +1,14 @@ -import { createClient } from "@rivet-dev/agentos/client"; -import type { registry } from "./server"; - -const client = createClient({ endpoint: "http://localhost:6420" }); -const vm = client.vm.getOrCreate("my-agent"); - -// Write code via the filesystem. The /home/agentos/sandbox mount maps to the sandbox root. -await vm.writeFile("/home/agentos/sandbox/app/index.ts", 'console.log("hello")'); - -// Run it inside the sandbox. Commands execute through the VM's process table, -// reading the file from the mounted directory. -const result = await vm.exec("node /home/agentos/sandbox/app/index.ts"); -console.log(result.stdout); // "hello\n" - -// Run a command against the mounted app directory. Because the sandbox -// filesystem is mounted into the VM, commands operate on the same files. -const install = await vm.exec("npm install --prefix /home/agentos/sandbox/app"); -console.log(install.exitCode, install.stdout); - -// Spawn a long-running process and stream its output. Connect to the VM, -// then subscribe to `processOutput` events for the spawned pid. -const { pid } = await vm.spawn("npm", ["run", "dev", "--prefix", "/home/agentos/sandbox/app"]); -const conn = vm.connect(); -conn.on("processOutput", (payload) => { - if (payload.pid === pid) { - console.log(payload.stream, new TextDecoder().decode(payload.data)); - } -}); +import { createSandboxVm, disposeSandboxVm } from "./server"; + +const handle = await createSandboxVm(); + +try { + await handle.vm.writeFile( + "/home/agentos/sandbox/hello.txt", + "Hello from a fresh sandbox", + ); + const content = await handle.vm.readFile("/home/agentos/sandbox/hello.txt"); + console.log(new TextDecoder().decode(content)); +} finally { + await disposeSandboxVm(handle); +} diff --git a/examples/sandbox/server.ts b/examples/sandbox/server.ts index ab0f5eacf..9a9bdbdf1 100644 --- a/examples/sandbox/server.ts +++ b/examples/sandbox/server.ts @@ -1,22 +1,32 @@ -import { agentOS, setup } from "@rivet-dev/agentos"; -import { createSandboxFs, createSandboxToolkit } from "@rivet-dev/agentos-sandbox"; +import { AgentOs } from "@rivet-dev/agentos-core"; +import { + createSandboxBindings, + createSandboxFs, +} from "@rivet-dev/agentos-sandbox"; import { SandboxAgent } from "sandbox-agent"; import { docker } from "sandbox-agent/docker"; -// Start a sandbox through Sandbox Agent. Any provider works; Docker is used here. -// `SandboxAgent` and the provider helpers come from the `sandbox-agent` package. -const sandbox = await SandboxAgent.start({ sandbox: docker() }); +export async function createSandboxVm() { + const sandbox = await SandboxAgent.start({ sandbox: docker() }); + try { + const vm = await AgentOs.create({ + mounts: [ + { + path: "/home/agentos/sandbox", + plugin: createSandboxFs({ client: sandbox }), + }, + ], + bindings: [createSandboxBindings({ client: sandbox })], + }); + return { vm, sandbox }; + } catch (error) { + await sandbox.dispose(); + throw error; + } +} -// `createSandboxFs` returns a mount plugin descriptor that projects the sandbox -// filesystem into the VM, and `createSandboxToolkit` exposes the sandbox's -// process management to agents as a host toolkit. -const vm = agentOS({ - mounts: [ - { path: "/home/agentos/sandbox", plugin: createSandboxFs({ client: sandbox }) }, - ], - toolKits: [createSandboxToolkit({ client: sandbox })], -}); - -export const registry = setup({ use: { vm } }); - -registry.start(); +export async function disposeSandboxVm( + handle: Awaited>, +) { + await Promise.allSettled([handle.vm.dispose(), handle.sandbox.dispose()]); +} diff --git a/packages/agentos-sandbox/src/toolkit.ts b/packages/agentos-sandbox/src/bindings.ts similarity index 87% rename from packages/agentos-sandbox/src/toolkit.ts rename to packages/agentos-sandbox/src/bindings.ts index 3d8d89ee6..71f8df227 100644 --- a/packages/agentos-sandbox/src/toolkit.ts +++ b/packages/agentos-sandbox/src/bindings.ts @@ -1,36 +1,37 @@ /** - * Sandbox toolkit exposing process management and command execution - * as host tools for agents running inside an agentOS VM. + * Sandbox bindings exposing process management and command execution + * for agents running inside an agentOS VM. */ -import type { HostTool, ToolKit } from "@rivet-dev/agentos-core"; +import type { Binding, BindingGroup } from "@rivet-dev/agentos-core"; import type { SandboxAgent } from "sandbox-agent"; import { z } from "zod"; -export interface SandboxToolkitOptions { +export interface SandboxBindingsOptions { /** A connected SandboxAgent client instance. */ client: SandboxAgent; } -/** Host tool type alias for convenience. */ -function hostTool( - def: HostTool, -): HostTool { +function binding( + def: Binding, +): Binding { return def; } /** - * Create a ToolKit that exposes sandbox process management operations. + * Create sandbox bindings that expose sandbox process management operations. */ -export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { +export function createSandboxBindings( + options: SandboxBindingsOptions, +): BindingGroup { const { client } = options; return { name: "sandbox", description: "Execute commands and manage processes in a remote sandbox environment.", - tools: { - "run-command": hostTool({ + bindings: { + "run-command": binding({ description: "Run a command synchronously in the sandbox and return its stdout, stderr, and exit code.", inputSchema: z.object({ @@ -73,7 +74,7 @@ export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { }, }), - "create-process": hostTool({ + "create-process": binding({ description: "Start a long-running background process in the sandbox. Returns a process ID for later management.", inputSchema: z.object({ @@ -108,7 +109,7 @@ export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { }, }), - "list-processes": hostTool({ + "list-processes": binding({ description: "List all processes running in the sandbox.", inputSchema: z.object({}), execute: async () => { @@ -126,7 +127,7 @@ export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { }, }), - "stop-process": hostTool({ + "stop-process": binding({ description: "Gracefully stop a running process in the sandbox.", inputSchema: z.object({ id: z.string().describe("The process ID to stop."), @@ -141,7 +142,7 @@ export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { }, }), - "kill-process": hostTool({ + "kill-process": binding({ description: "Forcefully kill a running process in the sandbox.", inputSchema: z.object({ id: z.string().describe("The process ID to kill."), @@ -156,7 +157,7 @@ export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { }, }), - "get-process-logs": hostTool({ + "get-process-logs": binding({ description: "Get stdout/stderr logs from a sandbox process.", inputSchema: z.object({ id: z.string().describe("The process ID."), @@ -196,7 +197,7 @@ export function createSandboxToolkit(options: SandboxToolkitOptions): ToolKit { }, }), - "send-input": hostTool({ + "send-input": binding({ description: "Send text input to an interactive sandbox process via stdin.", inputSchema: z.object({ diff --git a/packages/agentos-sandbox/src/index.ts b/packages/agentos-sandbox/src/index.ts index 0d214a228..daf7aa07b 100644 --- a/packages/agentos-sandbox/src/index.ts +++ b/packages/agentos-sandbox/src/index.ts @@ -4,5 +4,5 @@ export type { } from "@secure-exec/sandbox"; export { createSandboxFs } from "@secure-exec/sandbox"; -export type { SandboxToolkitOptions } from "./toolkit.js"; -export { createSandboxToolkit } from "./toolkit.js"; +export type { SandboxBindingsOptions } from "./bindings.js"; +export { createSandboxBindings } from "./bindings.js"; diff --git a/packages/agentos-sandbox/tests/vm-integration.test.ts b/packages/agentos-sandbox/tests/vm-integration.test.ts index b602c35b5..b794b9c5d 100644 --- a/packages/agentos-sandbox/tests/vm-integration.test.ts +++ b/packages/agentos-sandbox/tests/vm-integration.test.ts @@ -11,7 +11,7 @@ import { expect, it, } from "vitest"; -import { createSandboxFs, createSandboxToolkit } from "../src/index.js"; +import { createSandboxBindings, createSandboxFs } from "../src/index.js"; let sandbox: MockSandboxAgentHandle; @@ -44,7 +44,7 @@ describe("VM integration", () => { plugin: createSandboxFs({ client: sandbox.client }), }, ], - toolKits: [createSandboxToolkit({ client: sandbox.client })], + bindings: [createSandboxBindings({ client: sandbox.client })], }); }); @@ -87,11 +87,11 @@ describe("VM integration", () => { expect(new TextDecoder().decode(content)).toBe("deep file"); }); - // -- Toolkit direct execution (host RPC, not via CLI shim) -- + // -- Binding direct execution (host RPC, not via CLI shim) -- - it("should execute run-command tool directly via the toolkit", async () => { - const tk = createSandboxToolkit({ client: sandbox.client }); - const result = await tk.tools["run-command"].execute({ + it("should execute run-command binding directly", async () => { + const sandboxBindings = createSandboxBindings({ client: sandbox.client }); + const result = await sandboxBindings.bindings["run-command"].execute({ command: "echo", args: ["hello", "from", "sandbox"], }); @@ -99,31 +99,31 @@ describe("VM integration", () => { expect(result.stdout).toContain("hello from sandbox"); }); - it("should exercise the toolkit tool directly from a VM context", async () => { - // Write a file into the sandbox via the toolkit, then read it via the mount. - const tk = createSandboxToolkit({ client: sandbox.client }); + it("should exercise the binding directly from a VM context", async () => { + // Write a file into the sandbox via the binding, then read it via the mount. + const sandboxBindings = createSandboxBindings({ client: sandbox.client }); - // Confirm the sandbox toolkit runs commands successfully. - const result = await tk.tools["run-command"].execute({ + // Confirm the sandbox binding runs commands successfully. + const result = await sandboxBindings.bindings["run-command"].execute({ command: "echo", - args: ["hello from sandbox toolkit"], + args: ["hello from sandbox binding"], }); expect(result.exitCode).toBe(0); - expect(result.stdout).toContain("hello from sandbox toolkit"); + expect(result.stdout).toContain("hello from sandbox binding"); // Create a process and list it. - const proc = await tk.tools["create-process"].execute({ + const proc = await sandboxBindings.bindings["create-process"].execute({ command: "sleep", args: ["60"], }); expect(proc.status).toBe("running"); - const listed = await tk.tools["list-processes"].execute({}); + const listed = await sandboxBindings.bindings["list-processes"].execute({}); const found = listed.processes.find( (p: { id: string }) => p.id === proc.id, ); expect(found).toBeDefined(); - await tk.tools["kill-process"].execute({ id: proc.id }); + await sandboxBindings.bindings["kill-process"].execute({ id: proc.id }); }); }); diff --git a/packages/agentos/tests/actor.test.ts b/packages/agentos/tests/actor.test.ts index b5fb197e7..00b464759 100644 --- a/packages/agentos/tests/actor.test.ts +++ b/packages/agentos/tests/actor.test.ts @@ -376,9 +376,15 @@ describe("@rivet-dev/agentos native plugin package bridge", () => { test("rejects native actor options that cannot cross the NAPI config boundary", () => { expect(() => agentOS({ - toolKits: [], + bindings: [], } as never), - ).toThrow(/toolKits/); + ).toThrow(/bindings/); + + expect(() => + agentOS({ + createOptions: () => ({}), + } as never), + ).toThrow(/createOptions/); expect(() => agentOS({ diff --git a/packages/agentos/tests/config.test.ts b/packages/agentos/tests/config.test.ts new file mode 100644 index 000000000..5a56ebefc --- /dev/null +++ b/packages/agentos/tests/config.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, test } from "vitest"; +import { agentOS } from "../src/index.js"; + +describe("@rivet-dev/agentos native actor config", () => { + test("rejects create options that cannot cross the NAPI config boundary", () => { + expect(() => + agentOS({ + createOptions: () => ({}), + } as never), + ).toThrow(/createOptions/); + }); +}); diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index 62fda2414..5b105d05f 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -35,7 +35,13 @@ import type { SessionInitData, SessionModeState, } from "./agent-session-types.js"; -import { type HostTool, type ToolKit, validateToolkits } from "./host-tools.js"; +import { + type BindingGroupInput, + type HostTool, + type ToolKit, + normalizeBindingGroups, + validateToolkits, +} from "./host-tools.js"; import { zodToJsonSchema } from "./host-tools-zod.js"; import type { JsonRpcNotification, @@ -618,7 +624,9 @@ export interface AgentOsOptions { additionalInstructions?: string; /** Custom schedule driver for cron jobs. Defaults to TimerScheduleDriver. */ scheduleDriver?: ScheduleDriver; - /** Host-side toolkits available to agents inside the VM. */ + /** Host-side bindings available to agents inside the VM. */ + bindings?: BindingGroupInput[]; + /** @deprecated Use `bindings` instead. */ toolKits?: ToolKit[]; /** * Custom permission policy for the kernel. Controls access to filesystem, @@ -2798,9 +2806,15 @@ export class AgentOs { packageRefs.map((ref) => ({ packageDir: ref.dir })), ); const localMounts = await resolveCompatLocalMounts(options?.mounts); - const toolKits = options?.toolKits; - if (toolKits && toolKits.length > 0) { - validateToolkits(toolKits); + if (options?.bindings && options.toolKits) { + throw new Error("Use either bindings or toolKits, not both."); + } + const bindingInputs = options?.bindings ?? options?.toolKits; + const toolKits = bindingInputs + ? normalizeBindingGroups(bindingInputs) + : undefined; + if (bindingInputs && bindingInputs.length > 0) { + validateToolkits(bindingInputs); } // Resolve the sidecar handle up front so every VM created here leases the diff --git a/packages/core/src/host-tools.ts b/packages/core/src/host-tools.ts index 3222c4797..d9f96b823 100644 --- a/packages/core/src/host-tools.ts +++ b/packages/core/src/host-tools.ts @@ -1,10 +1,10 @@ import type { ZodType } from "zod"; -/** Maximum length for tool and toolkit descriptions (characters). */ +/** Maximum length for binding descriptions (characters). */ export const MAX_TOOL_DESCRIPTION_LENGTH = 200; /** - * A single tool that executes on the host. + * A single binding that executes on the host. * Mirrors the shape of AI SDK's tool() but with host-execution semantics. */ export interface HostTool { @@ -12,7 +12,7 @@ export interface HostTool { description: string; /** Zod schema for the input. Drives CLI flag generation and validation. */ inputSchema: ZodType; - /** Runs on the host when the agent invokes the tool. */ + /** Runs on the host when the agent invokes the binding. */ execute: (input: INPUT) => Promise | OUTPUT; /** Examples included in auto-generated prompt docs. */ examples?: ToolExample[]; @@ -27,8 +27,10 @@ export interface ToolExample { input: INPUT; } +export type Binding = HostTool; + /** - * A named group of tools. Becomes a CLI binary: agentos-{name}. + * @deprecated Use BindingGroup. */ export interface ToolKit { /** Toolkit name. Must be lowercase alphanumeric + hyphens. Becomes the CLI suffix: agentos-{name}. */ @@ -39,6 +41,20 @@ export interface ToolKit { tools: Record; } +/** + * A named group of bindings. Becomes a CLI binary: agentos-{name}. + */ +export interface BindingGroup { + /** Binding group name. Must be lowercase alphanumeric + hyphens. */ + name: string; + /** Description shown in binding listings and prompt docs. */ + description: string; + /** The bindings in this group. Keys become subcommands. */ + bindings: Record; +} + +export type BindingGroupInput = BindingGroup | ToolKit; + /** Helper to create a HostTool with type inference. */ export function hostTool( def: HostTool, @@ -46,14 +62,44 @@ export function hostTool( return def; } +/** Helper to create a binding with type inference. */ +export function binding( + def: Binding, +): Binding { + return def; +} + /** Helper to create a ToolKit. */ export function toolKit(def: ToolKit): ToolKit { return def; } +/** Helper to create a binding group. */ +export function bindingGroup(def: BindingGroup): BindingGroup { + return def; +} + +export function normalizeBindingGroup(group: BindingGroupInput): ToolKit { + if ("bindings" in group) { + return { + name: group.name, + description: group.description, + tools: group.bindings, + }; + } + return group; +} + +export function normalizeBindingGroups(groups: BindingGroupInput[]): ToolKit[] { + return groups.map(normalizeBindingGroup); +} + const TOOLKIT_COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; -function validateToolCommandName(kind: "Toolkit" | "Tool", name: string): void { +function validateToolCommandName( + kind: "Binding group" | "Binding", + name: string, +): void { if (TOOLKIT_COMMAND_NAME_RE.test(name)) { return; } @@ -63,24 +109,28 @@ function validateToolCommandName(kind: "Toolkit" | "Tool", name: string): void { } /** - * Validate all description lengths in the given toolkits. - * Throws if any toolkit or tool description exceeds MAX_TOOL_DESCRIPTION_LENGTH. + * Validate all description lengths in the given binding groups. + * Throws if any group or binding description exceeds MAX_TOOL_DESCRIPTION_LENGTH. */ -export function validateToolkits(toolKits: ToolKit[]): void { - for (const tk of toolKits) { - validateToolCommandName("Toolkit", tk.name); +export function validateBindings(bindingGroups: BindingGroupInput[]): void { + for (const input of bindingGroups) { + const tk = normalizeBindingGroup(input); + validateToolCommandName("Binding group", tk.name); if (tk.description.length > MAX_TOOL_DESCRIPTION_LENGTH) { throw new Error( - `Toolkit "${tk.name}" description is ${tk.description.length} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, + `Binding group "${tk.name}" description is ${tk.description.length} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, ); } for (const [toolName, tool] of Object.entries(tk.tools)) { - validateToolCommandName("Tool", toolName); + validateToolCommandName("Binding", toolName); if (tool.description.length > MAX_TOOL_DESCRIPTION_LENGTH) { throw new Error( - `Tool "${tk.name}/${toolName}" description is ${tool.description.length} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, + `Binding "${tk.name}/${toolName}" description is ${tool.description.length} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, ); } } } } + +/** @deprecated Use validateBindings. */ +export const validateToolkits = validateBindings; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8ad9e963c..81e06ccd2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,15 +10,21 @@ export { } from "./cron/index.js"; export { createHostDirBackend, nodeModulesMount } from "./host-dir-mount.js"; export { + binding, + bindingGroup, hostTool, MAX_TOOL_DESCRIPTION_LENGTH, + normalizeBindingGroup, + normalizeBindingGroups, toolKit, + validateBindings, validateToolkits, } from "./host-tools.js"; export { agentOsLimitsSchema, agentOsOptionFieldSchemas, agentOsOptionsSchema, + bindingGroupSchema, hostToolSchema, mountConfigSchema, nativeMountConfigSchema, diff --git a/packages/core/src/options-schema.ts b/packages/core/src/options-schema.ts index 0bdd4a3f6..84c498805 100644 --- a/packages/core/src/options-schema.ts +++ b/packages/core/src/options-schema.ts @@ -6,7 +6,12 @@ import type { LimitWarningHandler, NativeMountConfig, } from "./agent-os.js"; -import type { HostTool, ToolKit } from "./host-tools.js"; +import type { + BindingGroup, + BindingGroupInput, + HostTool, + ToolKit, +} from "./host-tools.js"; const stringArray = z.array(z.string()); const nonNegativeInteger = z.number().int().nonnegative(); @@ -257,6 +262,19 @@ export const toolKitSchema = z }) .strict() as z.ZodType; +export const bindingGroupSchema = z + .object({ + name: z.string(), + description: z.string(), + bindings: z.record(z.string(), hostToolSchema), + }) + .strict() as z.ZodType; + +const bindingGroupInputSchema = z.union([ + bindingGroupSchema, + toolKitSchema, +]) as z.ZodType; + /** * Shared AgentOsOptions field schemas. * @@ -280,6 +298,7 @@ export const agentOsOptionFieldSchemas = { message: "Expected schedule driver object", }) .optional(), + bindings: z.array(bindingGroupInputSchema).optional(), toolKits: z.array(toolKitSchema).optional(), permissions: permissionsSchema.optional(), sidecar: sidecarConfigSchema.optional(), diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2c29184ce..aef45baa1 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -66,7 +66,14 @@ export type { HostDirMountPluginConfig, NodeModulesMountConfig, } from "./host-dir-mount.js"; -export type { HostTool, ToolExample, ToolKit } from "./host-tools.js"; +export type { + Binding, + BindingGroup, + BindingGroupInput, + HostTool, + ToolExample, + ToolKit, +} from "./host-tools.js"; export type { AcpTimeoutErrorData, JsonRpcError, diff --git a/packages/core/tests/host-tools.test.ts b/packages/core/tests/host-tools.test.ts index 624234908..b5d268906 100644 --- a/packages/core/tests/host-tools.test.ts +++ b/packages/core/tests/host-tools.test.ts @@ -6,22 +6,22 @@ import { } from "../src/host-tools-zod.js"; import { MAX_TOOL_DESCRIPTION_LENGTH, - hostTool, - toolKit, - validateToolkits, + binding, + bindingGroup, + validateBindings, } from "../src/index.js"; -describe("host tool description limits", () => { - test("accepts toolkit and tool descriptions at the exported limit", () => { +describe("host binding description limits", () => { + test("accepts binding group and binding descriptions at the exported limit", () => { const description = "a".repeat(MAX_TOOL_DESCRIPTION_LENGTH); expect(() => - validateToolkits([ - toolKit({ + validateBindings([ + bindingGroup({ name: "browser", description, - tools: { - screenshot: hostTool({ + bindings: { + screenshot: binding({ description, inputSchema: z.object({ url: z.string() }), execute: () => ({ ok: true }), @@ -32,14 +32,14 @@ describe("host tool description limits", () => { ).not.toThrow(); }); - test("rejects toolkit descriptions longer than the exported limit", () => { + test("rejects binding group descriptions longer than the exported limit", () => { expect(() => - validateToolkits([ - toolKit({ + validateBindings([ + bindingGroup({ name: "browser", description: "a".repeat(MAX_TOOL_DESCRIPTION_LENGTH + 1), - tools: { - screenshot: hostTool({ + bindings: { + screenshot: binding({ description: "Take a screenshot", inputSchema: z.object({ url: z.string() }), execute: () => ({ ok: true }), @@ -48,18 +48,18 @@ describe("host tool description limits", () => { }), ]), ).toThrow( - `Toolkit "browser" description is ${MAX_TOOL_DESCRIPTION_LENGTH + 1} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, + `Binding group "browser" description is ${MAX_TOOL_DESCRIPTION_LENGTH + 1} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, ); }); - test("rejects tool descriptions longer than the exported limit", () => { + test("rejects binding descriptions longer than the exported limit", () => { expect(() => - validateToolkits([ - toolKit({ + validateBindings([ + bindingGroup({ name: "browser", description: "Browser automation", - tools: { - screenshot: hostTool({ + bindings: { + screenshot: binding({ description: "a".repeat(MAX_TOOL_DESCRIPTION_LENGTH + 1), inputSchema: z.object({ url: z.string() }), execute: () => ({ ok: true }), @@ -68,18 +68,18 @@ describe("host tool description limits", () => { }), ]), ).toThrow( - `Tool "browser/screenshot" description is ${MAX_TOOL_DESCRIPTION_LENGTH + 1} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, + `Binding "browser/screenshot" description is ${MAX_TOOL_DESCRIPTION_LENGTH + 1} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`, ); }); - test("rejects toolkit names that cannot become stable command names", () => { + test("rejects binding group names that cannot become stable command names", () => { expect(() => - validateToolkits([ - toolKit({ + validateBindings([ + bindingGroup({ name: "Browser_Tools", description: "Browser automation", - tools: { - screenshot: hostTool({ + bindings: { + screenshot: binding({ description: "Take a screenshot", inputSchema: z.object({ url: z.string() }), execute: () => ({ ok: true }), @@ -88,18 +88,18 @@ describe("host tool description limits", () => { }), ]), ).toThrow( - 'Toolkit name "Browser_Tools" must be lowercase alphanumeric with optional single hyphen separators', + 'Binding group name "Browser_Tools" must be lowercase alphanumeric with optional single hyphen separators', ); }); - test("rejects tool names that cannot become stable subcommands", () => { + test("rejects binding names that cannot become stable subcommands", () => { expect(() => - validateToolkits([ - toolKit({ + validateBindings([ + bindingGroup({ name: "browser-tools", description: "Browser automation", - tools: { - "screenshot_now": hostTool({ + bindings: { + "screenshot_now": binding({ description: "Take a screenshot", inputSchema: z.object({ url: z.string() }), execute: () => ({ ok: true }), @@ -108,12 +108,12 @@ describe("host tool description limits", () => { }), ]), ).toThrow( - 'Tool name "screenshot_now" must be lowercase alphanumeric with optional single hyphen separators', + 'Binding name "screenshot_now" must be lowercase alphanumeric with optional single hyphen separators', ); }); - test("fails loudly when a host tool input schema uses an unsupported discriminated union", () => { - const tool = hostTool({ + test("fails loudly when a binding input schema uses an unsupported discriminated union", () => { + const hostBinding = binding({ description: "Inspect a variant payload", inputSchema: z.object({ payload: z.discriminatedUnion("kind", [ @@ -125,8 +125,8 @@ describe("host tool description limits", () => { }); try { - zodToJsonSchema(tool.inputSchema); - throw new Error("Expected unsupported host tool schema to fail"); + zodToJsonSchema(hostBinding.inputSchema); + throw new Error("Expected unsupported binding schema to fail"); } catch (error) { expect(error).toBeInstanceOf(HostToolSchemaConversionError); expect(error).toMatchObject({ diff --git a/packages/core/tests/options-schema.test.ts b/packages/core/tests/options-schema.test.ts index e2fbe8421..ce3bd563f 100644 --- a/packages/core/tests/options-schema.test.ts +++ b/packages/core/tests/options-schema.test.ts @@ -19,4 +19,26 @@ describe("AgentOsOptions validation", () => { }), ).toThrow(/filesystem/); }); + + test("rejects create option factories on the one-shot core constructor", () => { + expect(() => + agentOsOptionsSchema.parse({ + createOptions: () => ({}), + }), + ).toThrow(/createOptions/); + }); + + test("accepts bindings as the public name for host binding groups", () => { + expect( + agentOsOptionsSchema.safeParse({ + bindings: [ + { + name: "weather", + description: "Weather bindings", + bindings: {}, + }, + ], + }).success, + ).toBe(true); + }); }); diff --git a/packages/core/tests/public-api-exports.test.ts b/packages/core/tests/public-api-exports.test.ts index 754ecdabb..9308ac7c3 100644 --- a/packages/core/tests/public-api-exports.test.ts +++ b/packages/core/tests/public-api-exports.test.ts @@ -11,6 +11,9 @@ import { TimerScheduleDriver, agentOsLimitsSchema, agentOsOptionsSchema, + binding, + bindingGroup, + bindingGroupSchema, createHostDirBackend, createInMemoryFileSystem, createInMemoryLayerStore, @@ -19,17 +22,13 @@ import { isPackageDescriptor, OPT_AGENTOS_BIN, OPT_AGENTOS_ROOT, - hostTool, - hostToolSchema, isAcpTimeoutErrorData, isUnknownSessionErrorData, mountConfigSchema, nodeModulesMount, parseAgentOsOptions, rootFilesystemConfigSchema, - toolKit, - toolKitSchema, - validateToolkits, + validateBindings, type AcpTimeoutErrorData, type AgentOsLimits, type ExecOptions, @@ -58,16 +57,15 @@ describe("root public API exports", () => { expect(CronManager).toBeTypeOf("function"); expect(TimerScheduleDriver).toBeTypeOf("function"); expect(createHostDirBackend).toBeTypeOf("function"); - expect(hostTool).toBeTypeOf("function"); - expect(toolKit).toBeTypeOf("function"); - expect(validateToolkits).toBeTypeOf("function"); + expect(binding).toBeTypeOf("function"); + expect(bindingGroup).toBeTypeOf("function"); + expect(validateBindings).toBeTypeOf("function"); expect(MAX_TOOL_DESCRIPTION_LENGTH).toBeGreaterThan(0); expect(agentOsLimitsSchema.safeParse({}).success).toBe(true); expect(agentOsOptionsSchema.safeParse({ defaultSoftware: false }).success).toBe( true, ); - expect(hostToolSchema).toBeTypeOf("object"); - expect(toolKitSchema).toBeTypeOf("object"); + expect(bindingGroupSchema).toBeTypeOf("object"); expect(mountConfigSchema).toBeTypeOf("object"); expect(rootFilesystemConfigSchema).toBeTypeOf("object"); expect(parseAgentOsOptions({ defaultSoftware: false })).toEqual({ diff --git a/packages/core/tests/sandbox-integration.test.ts b/packages/core/tests/sandbox-integration.test.ts index 58f9d6455..e1ec1f5cb 100644 --- a/packages/core/tests/sandbox-integration.test.ts +++ b/packages/core/tests/sandbox-integration.test.ts @@ -1,7 +1,7 @@ import common from "@agentos-software/common"; import { + createSandboxBindings, createSandboxFs, - createSandboxToolkit, } from "@rivet-dev/agentos-sandbox"; import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest"; import { AgentOs } from "../src/index.js"; @@ -46,7 +46,7 @@ describe("sandbox quickstart truth test", () => { } }); - test("mounts createSandboxFs and exercises run-command plus list-processes from createSandboxToolkit", async () => { + test("mounts createSandboxFs and exercises sandbox bindings", async () => { if (!sandbox) { throw new Error("Sandbox test harness did not start."); } @@ -60,7 +60,7 @@ describe("sandbox quickstart truth test", () => { plugin: createSandboxFs({ client: sandbox.client }), }, ], - toolKits: [createSandboxToolkit({ client: sandbox.client })], + bindings: [createSandboxBindings({ client: sandbox.client })], }); await sandbox.client.writeFsFile( @@ -70,8 +70,10 @@ describe("sandbox quickstart truth test", () => { const content = await vm.readFile(SANDBOX_FILE_PATH); expect(new TextDecoder().decode(content)).toBe(SANDBOX_FILE_CONTENT); - const toolkit = createSandboxToolkit({ client: sandbox.client }); - const runCommandResponse = (await toolkit.tools["run-command"].execute({ + const sandboxBindings = createSandboxBindings({ client: sandbox.client }); + const runCommandResponse = (await sandboxBindings.bindings[ + "run-command" + ].execute({ command: "echo", args: ["hello from sandbox"], })) as { @@ -83,7 +85,9 @@ describe("sandbox quickstart truth test", () => { expect(runCommandResponse.stderr).toBe(""); expect(runCommandResponse.stdout.trim()).toBe("hello from sandbox"); - const createdProcess = (await toolkit.tools["create-process"].execute({ + const createdProcess = (await sandboxBindings.bindings[ + "create-process" + ].execute({ command: "sleep", args: ["60"], })) as { @@ -92,7 +96,7 @@ describe("sandbox quickstart truth test", () => { }; expect(createdProcess.status).toBe("running"); - const listProcessesResponse = (await toolkit.tools[ + const listProcessesResponse = (await sandboxBindings.bindings[ "list-processes" ].execute({})) as { processes: Array<{ @@ -110,6 +114,8 @@ describe("sandbox quickstart truth test", () => { ), ).toBe(true); - await toolkit.tools["kill-process"].execute({ id: createdProcess.id }); + await sandboxBindings.bindings["kill-process"].execute({ + id: createdProcess.id, + }); }, 150_000); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 858619bb0..1f32946ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,21 +296,6 @@ importers: '@agentos-software/common': specifier: link:../../../secure-exec/registry/software/common version: link:../../../secure-exec/registry/software/common - '@agentos-software/curl': - specifier: link:../../../secure-exec/registry/software/curl - version: link:../../../secure-exec/registry/software/curl - '@agentos-software/git': - specifier: link:../../../secure-exec/registry/software/git - version: link:../../../secure-exec/registry/software/git - '@agentos-software/jq': - specifier: link:../../../secure-exec/registry/software/jq - version: link:../../../secure-exec/registry/software/jq - '@agentos-software/ripgrep': - specifier: link:../../../secure-exec/registry/software/ripgrep - version: link:../../../secure-exec/registry/software/ripgrep - '@agentos-software/sqlite3': - specifier: link:../../../secure-exec/registry/software/sqlite3 - version: link:../../../secure-exec/registry/software/sqlite3 '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1390,7 +1375,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/cron: + examples/quickstart/bindings: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1439,7 +1424,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/filesystem: + examples/quickstart/cron: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1488,7 +1473,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/git: + examples/quickstart/filesystem: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1537,7 +1522,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/hello-world: + examples/quickstart/git: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1586,7 +1571,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/network: + examples/quickstart/hello-world: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1635,7 +1620,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/nodejs: + examples/quickstart/network: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1684,7 +1669,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/pi-extensions: + examples/quickstart/nodejs: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1733,7 +1718,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/processes: + examples/quickstart/pi-extensions: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1782,7 +1767,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/s3-filesystem: + examples/quickstart/processes: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1831,7 +1816,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/sandbox: + examples/quickstart/s3-filesystem: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' @@ -1880,7 +1865,7 @@ importers: specifier: ^5.7.2 version: 5.9.3 - examples/quickstart/tools: + examples/quickstart/sandbox: dependencies: '@agentos-software/claude-code': specifier: 'catalog:' diff --git a/website/public/docs/docs/bindings.md b/website/public/docs/docs/bindings.md index 765d62693..d2b0f6ca3 100644 --- a/website/public/docs/docs/bindings.md +++ b/website/public/docs/docs/bindings.md @@ -6,7 +6,7 @@ Expose your host JavaScript functions (defined with Zod input schemas) to agents ## Getting started -Define a bindings group with Zod input schemas and pass it to `agentOS()`. Each binding becomes a CLI command inside the VM. +Define a bindings group with Zod input schemas and pass it to `agentOS({ bindings: [...] })`. Each binding becomes a CLI command inside the VM. Each binding can set an explicit `timeout` (in milliseconds) for long-running work. Bindings run without a timeout unless one is set. @@ -82,4 +82,4 @@ Use bindings when you want to expose your own JavaScript functions to agents. Us Binding calls from the agent securely invoke your `execute()` functions on the host. Your functions run with full access to the host environment, so you can call databases, APIs, and services directly without proxying credentials into the VM. The agent never sees the credentials, it only sees the binding's input/output contract. -Bindings run on the host with full access to the host environment, so do not expose bindings that could compromise the host without appropriate safeguards. \ No newline at end of file +Bindings run on the host with full access to the host environment, so do not expose bindings that could compromise the host without appropriate safeguards. diff --git a/website/public/docs/docs/core.md b/website/public/docs/docs/core.md index 13ca65558..087effd5b 100644 --- a/website/public/docs/docs/core.md +++ b/website/public/docs/docs/core.md @@ -72,6 +72,12 @@ When you use the [`agentOS()` actor](/docs/quickstart), all VM configuration is The top-level fields are documented inline above. See [Mounts](#mounts), [Software](/docs/software), and (for the hooks) [Approvals](/docs/approvals). +### Dynamic per-actor resources + +Dynamic per-actor VM options are not exposed yet. The native RivetKit actor config crosses a JSON boundary today, so every value in `agentOS(...)` must be serializable at actor-definition time. There is no supported `createOptions` callback on `AgentOs.create(...)` or `agentOS(...)` today. + +Do not create one host client, such as a Sandbox Agent client, and pass it into static actor config for multiple actors; that would share the same backing resource across actor instances. + ### Lifecycle hooks `onPermissionRequest(sessionId, request)` fires when an agent requests permission. `onSessionEvent(sessionId, event)` is a server-side hook called once for every session event: unlike the client-side `sessionEvent` connection subscription, it runs in the actor for every event regardless of connected clients, making it the place for server-side logging, persistence, or side effects. @@ -83,4 +89,4 @@ The top-level fields are documented inline above. See [Mounts](#mounts), [Softwa | Action timeout | 15 minutes | Maximum time for any single action | | Sleep grace period | 15 minutes | Time before sleeping after all activity stops | -These are set internally by the `agentOS()` factory and cannot be overridden per-call. See [Persistence & Sleep](/docs/persistence) for details on the sleep lifecycle. \ No newline at end of file +These are set internally by the `agentOS()` factory and cannot be overridden per-call. See [Persistence & Sleep](/docs/persistence) for details on the sleep lifecycle. diff --git a/website/public/docs/docs/crash-course.md b/website/public/docs/docs/crash-course.md index 6f6fb9f8d..c5b23d92b 100644 --- a/website/public/docs/docs/crash-course.md +++ b/website/public/docs/docs/crash-course.md @@ -82,6 +82,6 @@ Schedule recurring commands and agent sessions with cron expressions. ### Sandbox Mounting -agentOS uses a hybrid model: agents run in a lightweight VM by default and mount a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. Mount the sandbox as a filesystem and expose its process management as bindings. +agentOS uses a hybrid model: agents run in a lightweight VM by default and can mount a full sandbox for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. In direct `AgentOs.create(...)` code, start one sandbox for that VM, mount its filesystem, expose its process management as bindings, and dispose both resources together. -[Documentation](/docs/sandbox) \ No newline at end of file +[Documentation](/docs/sandbox) diff --git a/website/public/docs/docs/sandbox.md b/website/public/docs/docs/sandbox.md index fa842ddd7..4491de7be 100644 --- a/website/public/docs/docs/sandbox.md +++ b/website/public/docs/docs/sandbox.md @@ -2,11 +2,11 @@ Extend agentOS with full sandboxes for heavy workloads like browsers, desktop automation, and compilation. -For heavy workloads like browsers, desktop automation, and compilation, pair agentOS with a full sandbox on demand. Its filesystem mounts into the VM as a native directory, and its process management is exposed as [bindings](/docs/bindings), all provider-agnostic through [Sandbox Agent](https://sandboxagent.dev). +For heavy workloads like browsers, desktop automation, and compilation, pair agentOS with a full sandbox on demand. Its filesystem mounts into the VM as a native directory, and its process management is exposed through host bindings, all provider-agnostic through [Sandbox Agent](https://sandboxagent.dev). ## Why use agentOS with a sandbox? -agentOS is an alternative to sandboxes that covers most use cases, but some workloads need a full sandbox for special kinds of software (browsers, desktop automation, heavy compilation). Sandbox mounting lets you lazily start a sandbox on demand, only when it is needed, and project it into the VM. The hybrid model means one agent session can handle both lightweight coding tasks and heavy system operations, using the right tool for each. +agentOS is an alternative to sandboxes that covers most use cases, but some workloads need a full sandbox for special kinds of software (browsers, desktop automation, heavy compilation). Sandbox mounting lets you lazily start a sandbox on demand, only when it is needed, and project it into the VM. The hybrid model means one agent session can handle both lightweight coding tasks and heavy system operations, using the right binding for each. See [agentOS vs Sandbox](/docs/versus-sandbox) for a detailed comparison. @@ -25,7 +25,7 @@ Start with the default agentOS VM for all workloads, and only spin up a sandbox The sandbox integration ships as the `@rivet-dev/agentos-sandbox` package. It works through two mechanisms: - **Filesystem mount**: Projects the sandbox into the VM as a native directory, like mounting a hard drive on your own machine. Read and write files through the mount directly. -- **Bindings**: Exposes sandbox process management as [bindings](/docs/bindings). Execute commands on the sandbox from within the VM. +- **Bindings**: Exposes sandbox process management as the `agentos-sandbox` command inside the VM. Both are powered by [Sandbox Agent](https://sandboxagent.dev), and you can swap providers without changing agent code. Install both packages: @@ -35,9 +35,11 @@ npm install @rivet-dev/agentos-sandbox sandbox-agent `createSandboxFs` and `createSandboxBindings` come from `@rivet-dev/agentos-sandbox`. `SandboxAgent` and the provider helpers (such as `docker`) come from the `sandbox-agent` package. +**Warning:** do not create one `SandboxAgent` at module scope and reuse it for multiple actor instances. Direct `AgentOs.create(...)` examples should start one sandbox for the one VM they create and dispose both together. Dynamic per-actor sandbox creation for `agentOS(...)` needs a future actor-scoped options hook; no `createOptions` callback is supported today. + ## Calling the mounted bindings -Once the sandbox is mounted, write code through the filesystem and run it inside the sandbox. The sandbox bindings are exposed inside the VM as a CLI command, so you call it through the same `exec`/`spawn` surface as any other command. +Once the sandbox is mounted, write code through the filesystem and run it inside the sandbox. The sandbox bindings are exposed inside the VM as a CLI command, so you call them through the same `exec`/`spawn` surface as any other command. ## Bindings reference @@ -66,4 +68,4 @@ agentos-sandbox send-input --id "proc_abc123" --data "yes" ## Sandbox providers -The extension works with any [Sandbox Agent](https://sandboxagent.dev) provider. See the [Sandbox Agent documentation](https://sandboxagent.dev) for available providers and setup instructions. \ No newline at end of file +The extension works with any [Sandbox Agent](https://sandboxagent.dev) provider. See the [Sandbox Agent documentation](https://sandboxagent.dev) for available providers and setup instructions. diff --git a/website/src/content/docs/docs/bindings.mdx b/website/src/content/docs/docs/bindings.mdx index 4c57593f6..a04aaafc7 100644 --- a/website/src/content/docs/docs/bindings.mdx +++ b/website/src/content/docs/docs/bindings.mdx @@ -8,7 +8,7 @@ Expose your host JavaScript functions (defined with Zod input schemas) to agents ## Getting started -Define a bindings group with Zod input schemas and pass it to `agentOS()`. Each binding becomes a CLI command inside the VM. +Define a bindings group with Zod input schemas and pass it to `agentOS({ bindings: [...] })`. Each binding becomes a CLI command inside the VM. diff --git a/website/src/content/docs/docs/core.mdx b/website/src/content/docs/docs/core.mdx index f38f32145..fe566b7e7 100644 --- a/website/src/content/docs/docs/core.mdx +++ b/website/src/content/docs/docs/core.mdx @@ -94,6 +94,12 @@ When you use the [`agentOS()` actor](/docs/quickstart), all VM configuration is The top-level fields are documented inline above. See [Mounts](#mounts), [Software](/docs/software), and (for the hooks) [Approvals](/docs/approvals). +### Dynamic per-actor resources + +Dynamic per-actor VM options are not exposed yet. The native RivetKit actor config crosses a JSON boundary today, so every value in `agentOS(...)` must be serializable at actor-definition time. There is no supported `createOptions` callback on `AgentOs.create(...)` or `agentOS(...)` today. + +Do not create one host client, such as a Sandbox Agent client, and pass it into static actor config for multiple actors; that would share the same backing resource across actor instances. + ### Lifecycle hooks `onPermissionRequest(sessionId, request)` fires when an agent requests permission. `onSessionEvent(sessionId, event)` is a server-side hook called once for every session event: unlike the client-side `sessionEvent` connection subscription, it runs in the actor for every event regardless of connected clients, making it the place for server-side logging, persistence, or side effects. diff --git a/website/src/content/docs/docs/crash-course.mdx b/website/src/content/docs/docs/crash-course.mdx index 2fa9922b0..309e9b79c 100644 --- a/website/src/content/docs/docs/crash-course.mdx +++ b/website/src/content/docs/docs/crash-course.mdx @@ -150,7 +150,7 @@ Schedule recurring commands and agent sessions with cron expressions. ### Sandbox Mounting -agentOS uses a hybrid model: agents run in a lightweight VM by default and mount a full sandbox on demand for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. Mount the sandbox as a filesystem and expose its process management as bindings. +agentOS uses a hybrid model: agents run in a lightweight VM by default and can mount a full sandbox for heavy workloads like browsers, compilation, and desktop automation. Sandboxes are powered by [Sandbox Agent](https://sandboxagent.dev), so you can swap providers without changing agent code. In direct `AgentOs.create(...)` code, start one sandbox for that VM, mount its filesystem, expose its process management as bindings, and dispose both resources together. diff --git a/website/src/content/docs/docs/sandbox.mdx b/website/src/content/docs/docs/sandbox.mdx index 652c300cb..da009479b 100644 --- a/website/src/content/docs/docs/sandbox.mdx +++ b/website/src/content/docs/docs/sandbox.mdx @@ -4,11 +4,11 @@ description: "Extend agentOS with full sandboxes for heavy workloads like browse skill: true --- -For heavy workloads like browsers, desktop automation, and compilation, pair agentOS with a full sandbox on demand. Its filesystem mounts into the VM as a native directory, and its process management is exposed as [bindings](/docs/bindings), all provider-agnostic through [Sandbox Agent](https://sandboxagent.dev). +For heavy workloads like browsers, desktop automation, and compilation, pair agentOS with a full sandbox on demand. Its filesystem mounts into the VM as a native directory, and its process management is exposed through host bindings, all provider-agnostic through [Sandbox Agent](https://sandboxagent.dev). ## Why use agentOS with a sandbox? -agentOS is an alternative to sandboxes that covers most use cases, but some workloads need a full sandbox for special kinds of software (browsers, desktop automation, heavy compilation). Sandbox mounting lets you lazily start a sandbox on demand, only when it is needed, and project it into the VM. The hybrid model means one agent session can handle both lightweight coding tasks and heavy system operations, using the right tool for each. +agentOS is an alternative to sandboxes that covers most use cases, but some workloads need a full sandbox for special kinds of software (browsers, desktop automation, heavy compilation). Sandbox mounting lets you lazily start a sandbox on demand, only when it is needed, and project it into the VM. The hybrid model means one agent session can handle both lightweight coding tasks and heavy system operations, using the right binding for each. See [agentOS vs Sandbox](/docs/versus-sandbox) for a detailed comparison. @@ -27,7 +27,7 @@ Start with the default agentOS VM for all workloads, and only spin up a sandbox The sandbox integration ships as the `@rivet-dev/agentos-sandbox` package. It works through two mechanisms: - **Filesystem mount**: Projects the sandbox into the VM as a native directory, like mounting a hard drive on your own machine. Read and write files through the mount directly. -- **Bindings**: Exposes sandbox process management as [bindings](/docs/bindings). Execute commands on the sandbox from within the VM. +- **Bindings**: Exposes sandbox process management as the `agentos-sandbox` command inside the VM. Both are powered by [Sandbox Agent](https://sandboxagent.dev), and you can swap providers without changing agent code. Install both packages: @@ -37,13 +37,15 @@ npm install @rivet-dev/agentos-sandbox sandbox-agent `createSandboxFs` and `createSandboxBindings` come from `@rivet-dev/agentos-sandbox`. `SandboxAgent` and the provider helpers (such as `docker`) come from the `sandbox-agent` package. +**Warning:** do not create one `SandboxAgent` at module scope and reuse it for multiple actor instances. Direct `AgentOs.create(...)` examples should start one sandbox for the one VM they create and dispose both together. Dynamic per-actor sandbox creation for `agentOS(...)` needs a future actor-scoped options hook; no `createOptions` callback is supported today. + -## Calling the mounted bindings + -Once the sandbox is mounted, write code through the filesystem and run it inside the sandbox. The sandbox bindings are exposed inside the VM as a CLI command, so you call it through the same `exec`/`spawn` surface as any other command. +## Calling the mounted bindings - +Once the sandbox is mounted, write code through the filesystem and run it inside the sandbox. The sandbox bindings are exposed inside the VM as a CLI command, so you call them through the same `exec`/`spawn` surface as any other command. ## Bindings reference @@ -73,4 +75,3 @@ agentos-sandbox send-input --id "proc_abc123" --data "yes" ## Sandbox providers The extension works with any [Sandbox Agent](https://sandboxagent.dev) provider. See the [Sandbox Agent documentation](https://sandboxagent.dev) for available providers and setup instructions. - diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 4fdfdbd87..38eb05da8 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -12,14 +12,14 @@ import type { registry } from "./server"; const client = createClient("http://localhost:6420"); const agent = client.vm.getOrCreate("my-agent").connect(); -// Stream events (tool calls, text output, etc.) +// Stream events (binding calls, text output, etc.) agent.on("sessionEvent", (data) => console.log(data.event)); // Create a session and send a prompt const session = await agent.createSession("pi", { env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! } }); await agent.sendPrompt(session.sessionId, "Write a Python script that calculates pi");` }, - { key: 'tools', fileName: 'tools.ts', code: `// server.ts + { key: 'bindings', fileName: 'bindings.ts', code: `// server.ts import { agentOS, setup } from "@rivet-dev/agentos"; import common from "@agentos-software/common"; import { z } from "zod"; @@ -291,38 +291,35 @@ await agent.exec("git -C /home/agentos/repo commit -m 'add feature'"); const content = await agent.readFile("/home/agentos/repo/feature.txt"); console.log(new TextDecoder().decode(content));` }, - { key: 'sandbox', fileName: 'sandbox.ts', code: `// server.ts -import { agentOS, setup } from "@rivet-dev/agentos"; -import { createSandboxFs, createSandboxBindings } from "@rivet-dev/agentos-sandbox"; + { key: 'sandbox', fileName: 'server.ts', code: `// server.ts +import { AgentOs } from "@rivet-dev/agentos-core"; +import { createSandboxBindings, createSandboxFs } from "@rivet-dev/agentos-sandbox"; import { SandboxAgent } from "sandbox-agent"; import { docker } from "sandbox-agent/docker"; -// Start a Docker-backed sandbox -const sandbox = await SandboxAgent.start({ sandbox: docker() }); - -// Mount its filesystem and register the sandbox bindings -const vm = agentOS({ - mounts: [{ path: "/home/agentos/sandbox", plugin: createSandboxFs({ client: sandbox }) }], - bindings: [createSandboxBindings({ client: sandbox })], -}); -export const registry = setup({ use: { vm } }); -registry.start(); +export async function createSandboxVm() { + const sandbox = await SandboxAgent.start({ sandbox: docker() }); + const vm = await AgentOs.create({ + mounts: [ + { path: "/home/agentos/sandbox", plugin: createSandboxFs({ client: sandbox }) }, + ], + bindings: [createSandboxBindings({ client: sandbox })], + }); + return { vm, sandbox }; +} // client.ts -import { createClient } from "@rivet-dev/agentos/client"; -import type { registry } from "./server"; +import { createSandboxVm } from "./server"; -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate("my-agent"); +const { vm, sandbox } = await createSandboxVm(); // Write a file through the mounted sandbox filesystem -await agent.writeFile("/home/agentos/sandbox/hello.txt", "Hello from agentOS!"); -const content = await agent.readFile("/home/agentos/sandbox/hello.txt"); +await vm.writeFile("/home/agentos/sandbox/hello.txt", "Hello from agentOS!"); +const content = await vm.readFile("/home/agentos/sandbox/hello.txt"); console.log(new TextDecoder().decode(content)); -// Run a command inside the Docker sandbox via the mounted toolkit -const result = await agent.exec("agentos-sandbox run-command --command echo --json '{\\"args\\": [\\"hello from Docker\\"]}'"); -console.log(result.stdout);` }, +await vm.dispose(); +await sandbox.dispose();` }, { key: 'permissions', fileName: 'permissions.ts', code: `import { createClient } from "@rivet-dev/agentos/client"; import type { registry } from "./server"; diff --git a/website/src/pages/registry/[slug].astro b/website/src/pages/registry/[slug].astro index 2df586df6..3c1d94342 100644 --- a/website/src/pages/registry/[slug].astro +++ b/website/src/pages/registry/[slug].astro @@ -72,20 +72,23 @@ const vm = agentOS({ software: [${ident}] }); export const registry = setup({ use: { vm } });`; } else if (isSandboxExt) { - exampleSource = `import { agentOS, setup } from "@rivet-dev/agentos"; -import { createSandboxFs, createSandboxBindings } from "@rivet-dev/agentos-sandbox"; + exampleSource = `import { AgentOs } from "@rivet-dev/agentos-core"; +import { createSandboxBindings, createSandboxFs } from "@rivet-dev/agentos-sandbox"; import { SandboxAgent } from "sandbox-agent"; import { ${entry.slug} } from "sandbox-agent/${entry.slug}"; -// Start a ${entry.title}-backed sandbox and mount it into the VM const sandbox = await SandboxAgent.start({ sandbox: ${entry.slug}() }); -const vm = agentOS({ - mounts: [{ path: "/home/agentos/sandbox", plugin: createSandboxFs({ client: sandbox }) }], +const vm = await AgentOs.create({ + mounts: [ + { path: "/home/agentos/sandbox", plugin: createSandboxFs({ client: sandbox }) }, + ], bindings: [createSandboxBindings({ client: sandbox })], }); -export const registry = setup({ use: { vm } });`; +await vm.dispose(); +await sandbox.dispose(); +`; } else if (isDocs && entry.status === "docs" && entry.package && entry.agentId) { // Built-in agent adapter: register it as VM software, then open a session // with its agent id. The import identifier matches the agent id (e.g. diff --git a/website/src/pages/registry/index.astro b/website/src/pages/registry/index.astro index f9e0dce8b..216e3640f 100644 --- a/website/src/pages/registry/index.astro +++ b/website/src/pages/registry/index.astro @@ -9,7 +9,7 @@ import { HERO_H1_CLASS } from "../../components/marketing/typography";
@@ -19,7 +19,7 @@ import { HERO_H1_CLASS } from "../../components/marketing/typography"; agentOS Registry

- Browse agents, tools, file systems, and sandbox mounting for agentOS. + Browse agents, bindings, file systems, and sandbox mounting for agentOS.