---
name: "assemble-vms"
description: "Set up and use Assemble VMs in an existing project. Use when adding the SDK, connecting a harness, running commands, working with session files, or verifying pause and reopen behavior."
compatibility: "Node.js 22.19 or later, an Assemble SDK checkout or local package archives, and an operator-issued product key."
---

# Add Assemble VMs to a project

You are implementing a basic Assemble VMs integration in the user's existing project. Read this entire guide, inspect the project's conventions, then implement and verify the smallest useful integration. The default is a remote harness: the application stays on the user's infrastructure and accesses a persistent Linux VM through the SDK.

This is the canonical Assemble VMs agent skill at `https://docs.assemble.ai/skill.md`. Read every section before implementing the integration; a summary may omit required verification and cleanup behavior.

Assemble is currently in development preview. The hosted API is available at `https://assemble-vms-api.fly.dev` with an operator-issued product key. npm packages remain unpublished, so use the Assemble checkout or package files the user supplies. Customer signup and key self-service are not implemented. Do not invent a signup page, registry install, or alternative hosted endpoint.

## 1. Inspect the project and inputs

- Find the project's package manager, server entry points, module format, and existing environment-variable pattern. Preserve the current application structure. Add a small server-side client module and a runnable verification script; a new UI is unnecessary for this task.
- Use Node.js 22.19 or later. The Assemble source repository uses npm 11 or later; respect the target project's package manager when installing its dependencies.
- Locate the supplied Assemble checkout or trusted package files for SDK installation. Reading this guide does not require a checkout. If one is available, read `packages/sdk/src/index.ts`, `docs/reference/typescript.mdx`, and `examples/first-session.mjs` for the exact API and runnable reference.
- Required runtime variables are `ASSEMBLE_BASE_URL` and `ASSEMBLE_API_KEY`. Use a dedicated stable session name in `ASSEMBLE_SESSION_NAME`, or choose a descriptive name for this project's setup check. Names must match `^[a-z0-9][a-z0-9_-]*$` and be 1–64 characters long.
- The API key is a product key issued by the service operator. Your Assemble API key is the only service credential required for this setup.

Never put a key in a prompt, committed file, browser bundle, URL, or log. Do not read unrelated credential files or copy infrastructure credentials from another project. An `.env.example` may contain empty variable names. Keep real values in the user's ignored local environment or server secret store. Use the project's existing environment loader; Node does not load an `.env` file automatically.

If the API URL, key, or package files are unavailable, implement the parts that can be completed, validate them locally, and report the exact missing input by name. The user can supply the key through their environment without sending it in chat. Do not claim live verification succeeded until it runs.

## 2. Install the current SDK

Inside the Assemble checkout, the npm workspace makes `@assemble-workspace/sdk` available after:

```sh
npm install
npm run build
```

For another project, use local package archives until publication. From the Assemble repository root, build and pack both the contracts and SDK:

```sh
npm run build
mkdir -p artifacts
npm pack --workspace @assemble-workspace/contracts --pack-destination artifacts
npm pack --workspace @assemble-workspace/sdk --pack-destination artifacts
```

Install both generated archives into the target project in the same command. Replace these paths with the actual supplied or generated files:

```sh
npm install /absolute/path/assemble-workspace-contracts-0.1.0.tgz /absolute/path/assemble-workspace-sdk-0.1.0.tgz
```

Use the filenames returned by `npm pack` if their versions differ. Preserve the target project's lockfile conventions. The CLI is optional; the SDK alone completes this check. Do not use an unverified registry install as a fallback.

## 3. Add the server-side client

Import `AssembleClient` from `@assemble-workspace/sdk`. Read and validate the two runtime variables before constructing it:

```js
import { AssembleClient } from "@assemble-workspace/sdk";

const apiKey = process.env.ASSEMBLE_API_KEY;
const baseUrl = process.env.ASSEMBLE_BASE_URL;
if (!apiKey || !baseUrl) {
  throw new Error("Set ASSEMBLE_API_KEY and ASSEMBLE_BASE_URL in the server environment.");
}

const client = new AssembleClient({ apiKey, baseUrl });
```

Keep this module on the server or in a local Node script. Never use public environment prefixes such as `NEXT_PUBLIC_` or `VITE_` for the product key. Set `ASSEMBLE_BASE_URL=https://assemble-vms-api.fly.dev` for the hosted API, or use the operator-supplied URL for a different service. Remote service URLs require HTTPS; `http://localhost:8787` is the default local API URL when that service has been started. `https://docs.assemble.ai` serves documentation; send SDK requests to the API URL.

## 4. Implement the first-session verification

If a checkout is available, use its `examples/first-session.mjs` as the runnable reference, adapting its location and imports to the target project. When installing from package archives, implement the same flow directly. The flow must perform these steps:

1. Open a dedicated named session with `await client.sessions.open({ name })`. Save its `id`. The default image is `terminal`.
2. Write a small UTF-8 verification message to a fresh path such as `.assemble-setup/<random-identifier>.txt` with `await session.files.write(path, body)`. Use a unique path so existing project files are preserved. Keep the message and exact path available for later comparisons.
3. Run a command that reads that file using `await session.exec({ command, timeoutSeconds: 30 })`. Commands start in `session.rootDirectory`, normally `/workspace`. Correctly shell-quote the path; do not insert arbitrary user text into a shell command.
4. Iterate `execution.events()` to stream `stdout` and `stderr`. Capture stdout as well so it can be compared with the expected message.
5. Await `execution.wait()`. Require both `result.state === "completed"` and `result.exitCode === 0`. Waiting does not turn a failed command into a thrown exception automatically. Compare stdout with the message.
6. Call `await session.files.read(path)`, which returns `Uint8Array`. Compare its exact bytes with those written. `TextDecoder` is appropriate for this UTF-8 example; preserve byte comparisons for binary data.
7. Pause with `await session.pause()`.
8. Reopen using `await client.sessions.open({ name })` under the same API-key owner. Assert the reopened ID equals the original ID and its file contents still match. Opening the same name reuses and resumes the saved session. `client.sessions.get(id)` only retrieves metadata; it does not resume a paused session.
9. Pause the reopened session and report its ID, name, and verification file path. Keep these available for the next run. Use error handling that attempts to pause a created session after a failed check without hiding the original error or a pause failure.

Use `session.files` and `session.exec` for remote operations. Local `fs` and `child_process` calls operate on the machine running this integration, so they cannot substitute for the remote file and command checks.

Add a clear run command to the target project's README or script list. If the shell already provides the variables, a standalone ESM example can run with:

```sh
node examples/first-session.mjs
```

When using an ignored local environment file, load it using the project's existing tooling or Node's explicit `--env-file` option. Do not print environment values in the handoff.

## 5. Respect persistence and cleanup behavior

A session is a retained Assemble VM with its own persistent filesystem. The file API and commands share `/workspace`. Opening the same name under the same owner returns that saved session. Pausing keeps both the session and its files; stopping the local script does not delete the VM or disk.

This check verifies pause/reopen retention in the same environment. It does not prove VM-replacement recovery or crash durability. The session filesystem stages dirty data until it is flushed; a healthy mount or ordinary command success alone is not evidence that all new bytes are durable. The file API explicitly flushes its writes, while arbitrary applications must flush their own data. There is no automatic per-command rollback. A failed or cancelled command may leave changes behind.

Leave the verification session paused by default. Delete only when the user explicitly asks to discard it. `await session.delete()` permanently removes both the VM and its session filesystem. Do not delete the session merely because a test finished or failed, and do not delete unrelated sessions. Report a session that could not be paused so the user can act on it.

## 6. Connect the user's harness after the check

The initial setup needs no model invocation. Give the user a working session integration first. Then follow their chosen mode:

- **Remote harness:** Keep the agent loop in the user's application and map its file and shell tools to `session.files` and `session.exec`. Reconnect to an execution with `await session.execution(executionId)`. Execution event readers can reconnect using `events({ after: sequence })`; disconnecting the reader does not cancel the command. Request cancellation with `await execution.cancel()`, then use `await execution.wait()` and inspect the confirmed terminal state.
- **Harness inside the VM:** Obtain short-lived access with `await session.ssh.create({ expiresInMinutes: 15 })`, then use its returned command. Treat that command as a credential: keep it out of prompts, logs, source control, and screenshots. Raw SSH starts in the image's login home and directory. Explicitly change to `session.rootDirectory`; export `HOME` to `session.homeDirectory` if shared managed-command configuration is needed. Both home directories are VM-local. Revoke the grant with `await session.ssh.revoke(access.id)`. The basic setup does not need to install a CLI agent or its credentials.
- **Provided Pi harness:** Iterate `session.agent.run({ message })` if the operator configured a default model. Optional `provider` and `model` overrides must be supplied together. This is a fresh one-shot agent conversation using the existing session's remote tools; the harness itself runs in the service. Reusing the session retains files, while multi-turn conversation persistence is not implemented. Do not silently switch the user's harness to this mode.

## 7. Report the result accurately

Finish with the files changed, the exact run command, and a compact verification report covering SDK loading, file write/read, command stdout and exit code, pause/reopen identity, and retained file contents. Include the session ID and whether it was successfully paused. Distinguish checks that passed, checks that failed, and checks blocked by missing service access. Never include credentials.

The completed integration should be ready for the user's backend to call and should preserve their existing application behavior. Publication, service deployment, account creation, and destructive cleanup are separate actions from adding this SDK integration.
