Skip to main content
Custom Software

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

  1. 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.)

  2. Compile it to WebAssembly — see Building Binaries. There’s no pack step: WASM binaries are self-contained, so the compile output is already the package — a bin/ of \0asm files plus a package.json for the name/version:

    my-cmds/
    ├── package.json
    └── bin/
        ├── tool-a       # \0asm WebAssembly
        └── tool-b
    
  3. 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 });
    
  4. Use it — pass it to a VM; the commands are on $PATH:

    import { agentOS, setup } from "@rivet-dev/agentos";
    import myCmds from "./my-cmds.ts";
    
    // The compiled commands are now on $PATH inside the VM.
    const vm = agentOS({ software: [myCmds] });
    
    export const registry = setup({ use: { vm } });
    registry.start();
    

Node.js

  1. You have a local project whose package.json bin names its commands:

    my-tool/
    ├── package.json     # "bin": { "my-tool": "cli.js" }
    └── cli.js           # #!/usr/bin/env node
    
  2. Package itpack installs the full dependency closure into a self-contained package directory (a flat node_modules plus a bin map 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 closure
    

    Commands come from the package’s package.json bin map — real files, no symlinks — so the result ships cleanly as an npm dependency. (The runtime makes the /opt/agentos/bin symlinks itself when it mounts the package.) A native .node addon is an error (it can’t run in V8); re-run with --prune-native to drop unreachable ones.

  3. 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 });
    
  4. Use it — pass it to a VM; my-tool is now on $PATH:

    import { agentOS, setup } from "@rivet-dev/agentos";
    import myTool from "./my-tool.ts";
    
    // `my-tool` is now on $PATH inside the VM.
    const vm = agentOS({ software: [myTool] });
    
    export const registry = setup({ use: { vm } });
    registry.start();
    

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.

  1. You have an npm package with a bin/ command that speaks ACP over stdio.

  2. Package it — same pack as Node.js, with --agent naming the ACP entrypoint. That writes the agent block into the package’s agentos-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)
    
  3. Define it — point defineSoftware() at the packaged directory; the agent block is already in its agentos-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 });
    
  4. Use itcreateSession() launches the agent by spawning its acpEntrypoint:

    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 — the bin/ 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" } so pi-acp can spawn the pi CLI.
  • agent.launchArgs — extra CLI args prepended when launching the adapter.
  • agent.snapshot (default false) — 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 missing source is 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 .node addon, instantiate WebAssembly, or produce a V8 External/Foreign at top level.
  • Open a file descriptor, socket, timer, or worker, or leave a pending promise.
  • Bake in non-deterministic or per-session stateprocess.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