Software Definition
The software-package definition for custom commands and agents in an agentOS VM: a package is a directory, declared with defineSoftware({ packageDir }).
Software is anything you install into a VM — commands (executables in a package’s bin/) or an agent (a package that also exposes an ACP session).
A package is a self-contained directory: package it first, then point defineSoftware() at it with { packageDir }. The package’s name, optional agent block, and any files/env it provides all live in an agentos-package.json at the root of that directory — the sidecar reads it when it mounts the package, so the client only forwards the directory. Pick the quickstart that matches what you’re packaging.
Quickstart
WebAssembly
-
You have C or Rust source for a command. (Most common commands already ship as
@agentos-software/*packages you can use directly — compile only new or custom ones.) -
Compile it to WebAssembly — see Building Binaries. There’s no
packstep: WASM binaries are self-contained, so the compile output is already the package — abin/of\0asmfiles plus apackage.jsonfor the name/version:my-cmds/ ├── package.json └── bin/ ├── tool-a # \0asm WebAssembly └── tool-b -
Define it — point
defineSoftware()at that directory:import { defineSoftware } from "@rivet-dev/agentos"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; // WASM output is already a package - no `pack` step. const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "my-cmds"); export default defineSoftware({ packageDir }); -
Use it — pass it to a VM; the commands are on
$PATH:
Node.js
-
You have a local project whose
package.jsonbinnames its commands:my-tool/ ├── package.json # "bin": { "my-tool": "cli.js" } └── cli.js # #!/usr/bin/env node -
Package it —
packinstalls the full dependency closure into a self-contained package directory (a flatnode_modulesplus abinmap of real files):npx @rivet-dev/agentos-toolchain pack ./my-tool # writes ./my-tool-package/ (override the location with --out <dir>) # my-tool-package/ # ├── package.json # "bin": { "my-tool": "node_modules/my-tool/cli.js" } # └── node_modules/ # flat, self-contained closureCommands come from the package’s
package.jsonbinmap — real files, no symlinks — so the result ships cleanly as an npm dependency. (The runtime makes the/opt/agentos/binsymlinks itself when it mounts the package.) A native.nodeaddon is an error (it can’t run in V8); re-run with--prune-nativeto drop unreachable ones. -
Define it — point
defineSoftware()at the packaged directory:import { defineSoftware } from "@rivet-dev/agentos"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; // Point at the self-contained directory produced by `agentos-toolchain pack`. const packageDir = resolve( dirname(fileURLToPath(import.meta.url)), "my-tool-package", ); export default defineSoftware({ packageDir }); -
Use it — pass it to a VM;
my-toolis now on$PATH:
Agent
An agent is a Node.js or WASM package (packaged exactly as above) whose agentos-package.json carries an agent block naming a bin/ command that speaks ACP over stdio.
-
You have an npm package with a
bin/command that speaks ACP over stdio. -
Package it — same
packas Node.js, with--agentnaming the ACP entrypoint. That writes theagentblock into the package’sagentos-package.json:npx @rivet-dev/agentos-toolchain pack @scope/my-agent --out ./packages --agent my-agent-acp # → ./packages/my-agent/current (its agentos-package.json now has the agent block) -
Define it — point
defineSoftware()at the packaged directory; the agent block is already in itsagentos-package.json:import { defineSoftware } from "@rivet-dev/agentos"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const packageDir = resolve( dirname(fileURLToPath(import.meta.url)), "packages/my-agent/current", ); // The agent block lives in the package's agentos-package.json, generated by `agentos-toolchain pack --agent`. export default defineSoftware({ packageDir }); -
Use it —
createSession()launches the agent by spawning itsacpEntrypoint:import { agentOS, setup } from "@rivet-dev/agentos"; import myAgent from "./my-agent.ts"; const vm = agentOS({ software: [myAgent] }); // createSession() launches the agent by spawning its acpEntrypoint: // const session = await vm.createSession("my-agent"); export const registry = setup({ use: { vm } }); registry.start();
Reference
The descriptor
A software entry is just a pointer to the packaged directory:
defineSoftware({
packageDir: string, // absolute host path to the self-contained package directory
})
packageDir must contain only the package — a package.json with a bin map, the runtime
files (bin/, a flat node_modules), and an agentos-package.json. It is mounted read-only, so
don’t point it at a source root: that drags src/, dev node_modules/, tsconfig, and build
caches into the VM. Point it at a clean build output — pack and the WASM assemble step both emit
one.
pack already emits a clean directory. For a package you build by hand (e.g. compiled WASM),
assemble a dist/package/ holding just package.json + bin/ and point packageDir there —
never at the workspace root:
// dist/package/ ← { package.json (name, version, bin), bin/<cmd>…, agentos-package.json }
const packageDir = resolve(import.meta.dirname, "dist/package");
export default defineSoftware({ packageDir });
agentos-package.json
The package’s name, optional agent block, and any files/env it provides live in an
agentos-package.json at the root of packageDir. The sidecar reads it when it mounts the
package, so this metadata never travels on the wire. For command/WASM packages it is generated
for you (name from package.json); for agents you author the agent block (or
agentos-toolchain pack --agent <cmd> writes it).
{
"name": "my-agent", // → /opt/agentos/<name>
"agent": { // optional — also exposes an agent session
"acpEntrypoint": "my-agent-acp", // bin/ command that speaks ACP over stdio
"env": { }, // static env for the adapter
"launchArgs": [],
"snapshot": false // SDK snapshot optimization
},
"provides": { // optional — files + env the package contributes
"env": { "EXAMPLE_HOME": "/opt/agentos/my-agent" },
"files": [{ "source": "etc/example.conf", "target": "/etc/example.conf" }]
}
}
name— the package name; commands and the package mount under/opt/agentos/<name>.agent.acpEntrypoint— thebin/command spawned to start a session; speaks ACP over stdio.agent.env— static env vars for the adapter, merged under the user env. Every command is on$PATH, so point at one directly, e.g.{ "PI_ACP_PI_COMMAND": "/opt/agentos/bin/pi" }sopi-acpcan spawn thepiCLI.agent.launchArgs— extra CLI args prepended when launching the adapter.agent.snapshot(defaultfalse) — load the SDK once per sidecar via a shared V8 heap snapshot instead of per session. Falls back to per-session loading if the SDK isn’t snapshot-safe, so it only affects startup latency.provides.env— env vars merged into the VM’s base environment (existing values win — a package never clobbers the user env).provides.files— read-only files overlaid into the VM filesystem. Each{ source, target }maps a path inside the package to an absolute VM path; the sidecar mounts them as zero-copy read-only lower layers (a guest write copies-up, never touching the host). A missingsourceis a fatal packaging error.
Advanced
Meta-packages
A software entry may be an array of descriptors, so one package can bundle several. Pass arrays directly to software:
const vm = agentOS({
software: [pi, buildEssential /* = [coreutils, make, git, curl] */],
});
SDK snapshotting & snapshot-safety
A V8 heap snapshot freezes the heap after the SDK’s modules are evaluated, then seeds each new session’s isolate from it. This works only if the SDK’s module-init code (everything that runs at import/require time) doesn’t:
- Create native handles — load a
.nodeaddon, instantiate WebAssembly, or produce a V8External/Foreignat top level. - Open a file descriptor, socket, timer, or worker, or leave a pending promise.
- Bake in non-deterministic or per-session state —
process.env, cwd,Date.now(),Math.random(), a UUID.
Defer all of the above behind functions or lazy import() that run per session. Leave agent.snapshot: false for any SDK that can’t — the agent still runs, just without the speedup.
Next steps
- Custom Agents: the agent-focused guide.
- Building Binaries: compile WASM commands and use the registry.
- Packages & command resolution: how packages mount and resolve.
- Request Software: ask for a package you need.