# Software Definition

**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](/agentos/docs/custom-software/building-wasm). 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:

   
examples/software/quickstart-wasm/my-cmds.ts:

```ts
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`:

   
examples/software/quickstart-wasm/index.ts:

```ts
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 it** — `pack` installs the full dependency closure into a self-contained package directory (a flat `node_modules` plus a `bin` map of real files):

   ```bash
   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:

   
examples/software/quickstart-node/my-tool.ts:

```ts
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`:

   
examples/software/quickstart-node/index.ts:

```ts
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`:

   ```bash
   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`:

   
examples/software/quickstart-agent/my-agent.ts:

```ts
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 it** — choose a session ID, then `openSession({ sessionId, agent })` launches the agent by spawning its `acpEntrypoint`:

   
examples/software/quickstart-agent/index.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import myAgent from "./my-agent.ts";

const vm = agentOS({ software: [myAgent] });
// openSession() launches or restores the agent through its acpEntrypoint:
//   await vm.sessions.open({ agent: "my-agent" });

export const registry = setup({ use: { vm } });
registry.start();
```

## Reference

### The descriptor

A software entry is just a pointer to the packed package:

```ts
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.

> **NOTE:** `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: ```ts
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).

```json
{
  "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](#sdk-snapshotting--snapshot-safety) 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`:

```ts
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 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](/agentos/docs/agents/custom): the agent-focused guide.
- [Building Binaries](/agentos/docs/custom-software/building-wasm): compile WASM commands and use the registry.
- [Packages & command resolution](/agentos/docs/architecture/packages-and-command-resolution): how packages mount and resolve.
- [Request Software](https://github.com/rivet-dev/agentos/issues/new/choose): ask for a package you need.
