Software Definition
The software-package definition for custom commands and agents in an agentOS VM: a package is a packed .aospkg (or a package directory), declared with defineSoftware({ packagePath }).
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 self-contained: package it first, then point defineSoftware() at it with { packagePath } — the packed .aospkg the toolchain emits, or (for local development) the package directory itself. The package’s name, optional agent block, and any files/env it provides are authored in an agentos-package.json next to your sources; the toolchain compiles that JSON into the .aospkg’s embedded manifest at pack time (the JSON itself is never shipped into the VM). 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 packagePath = resolve(dirname(fileURLToPath(import.meta.url)), "my-cmds"); export default defineSoftware({ packagePath }); -
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 packagePath = resolve( dirname(fileURLToPath(import.meta.url)), "my-tool-package", ); export default defineSoftware({ packagePath }); -
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 packagePath = 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({ packagePath }); -
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 packed package:
defineSoftware({
packagePath: string, // absolute host path to the packed .aospkg
// (or a package directory, for local development)
})
The normal packagePath is the dist/package.aospkg that agentos-toolchain build/pack emit —
a single file holding the package manifest, a precomputed mount index, and the package’s mount tar.
A directory is accepted for local development; it 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.
pack already emits the packed .aospkg. For a package you build by hand (e.g. compiled WASM),
run agentos-toolchain build to assemble dist/package/ and pack dist/package.aospkg, then
point packagePath there — never at the workspace root:
const packagePath = resolve(import.meta.dirname, "dist/package.aospkg");
export default defineSoftware({ packagePath });
agentos-package.json
The package’s name, optional agent block, and any files/env it provides are authored in an
agentos-package.json at the package root. It is toolchain input: at pack time it is compiled
into the .aospkg’s embedded manifest (which is what the sidecar reads) and stripped from the
packed files, so the JSON never ships into the VM and the 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.