# Computer

URL: https://helmr.dev/docs/reference/sdk/computer
Description: template(), image(), Secret bindings, and client methods for Computers, Template definitions and Commands.

# Computer

This page lists the Computer authoring API and the `HelmrClient` methods for
Computers and Commands. For what a Computer is and how it persists, see
[Computers](/docs/concepts/computers). For step-by-step instructions, see
[Prepare a Computer](/docs/guides/prepare-a-computer) and
[Run commands](/docs/guides/run-commands).

## `template()`

```ts
function template(config: Omit<TemplateDefinition, "kind">): TemplateDefinition

interface TemplateDefinition {
  readonly kind: "template"
  readonly id: string
  readonly image: ImageBuilder
  readonly resources: { readonly cpu: number; readonly memory: string; readonly disk?: string }
  readonly network?: NetworkPolicy
  readonly prepareNetwork?: NetworkPolicy
  readonly prepare?: (prepare: PrepareContext) => MaybePromise<void>
  readonly refresh?: { readonly every: Duration }
  readonly deleteAfterUnused?: Retention
  readonly secrets?: readonly SecretBinding[]
  readonly prepareSecrets?: readonly SecretBinding[]
}
```

Returns a frozen definition with `kind: "template"`. Throws `TypeError` when
`id` is blank or `resources.cpu`/`resources.memory` is missing.

| Field | Required | Description |
| --- | --- | --- |
| `id` | yes | Definition ID, matching `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`. |
| `image` | yes | Root filesystem, built with [`image()`](#images). |
| `resources.cpu` | yes | Positive number of CPUs; must resolve to whole milliCPU (for example `0.5`, `1`, `2`). |
| `resources.memory` | yes | Positive integer with `MiB` or `GiB`, for example `"4GiB"`. |
| `resources.disk` | no | Must be `"32GiB"` in this release; omit it to use that size. |
| `network` | no | Outbound network maximum for Computers created from this Template. Omission keeps ordinary IPv4 connectivity under installation restrictions. |
| `prepareNetwork` | no | Replaces the network policy during `prepare`; omission inherits `network`. Requires a callback. |
| `prepare` | no | Preparation function. Runs with a `PrepareContext` to set up the Computer's filesystem (for example cloning a repository and installing dependencies) before the Computer is used. |
| `refresh.every` | yes, with `refresh` | Best-effort interval between background preparations after promotion. A usable build already in progress satisfies the interval; missed intervals are coalesced. A new Computer does not start from a prepared image older than three intervals; an older one is prepared again first. Without `refresh`, images have no age limit. |
| `deleteAfterUnused` | no | Deletes a Computer created from this Template after it stays unused this long: a `Duration` or `"never"`. Default `"7d"`. A Computer always uses its origin Template's value; to keep a long-lived Computer longer, create it from a Template with a longer period. |
| `secrets` | no | Secret bindings available to processes on the Computer. |
| `prepareSecrets` | no | Secret bindings available to `prepare`. Nonempty bindings require a callback. |

```ts
interface PrepareContext {
  readonly signal: AbortSignal
  exec(command: string | readonly string[], options?: { cwd?: string; env?: Record<string, string> }): Promise<void>
}
```

```ts
import { template, image } from "@helmr/sdk"

export const repoComputer = template({
  id: "app",
  image: image("app").from("node:24-bookworm"),
  resources: { cpu: 2, memory: "4GiB" },
  prepareSecrets: [{ secret: "SOURCE_TOKEN", env: { name: "SOURCE_TOKEN", mode: "raw" } }],
  secrets: [{
    secret: "MODEL_API_KEY",
    env: { name: "MODEL_API_KEY", mode: "protected", allowedOrigins: ["https://api.example.com"] },
  }],
  async prepare(build) {
    await build.exec(["git", "clone", "https://github.com/acme/app", "/app"])
    await build.exec(["npm", "ci"], { cwd: "/app" })
  },
  refresh: { every: "1d" },
})
```

## Outbound network policy

```ts
export type AllowedDomain =
  | string
  | { readonly domain: string; readonly ipRanges: readonly string[] }

export type NetworkPolicy =
  | { readonly blockAll: boolean; readonly allowDomains?: never }
  | { readonly allowDomains: readonly AllowedDomain[]; readonly blockAll?: never }
```

| Value | Outbound access |
| --- | --- |
| Omitted or `{ blockAll: false }` | Ordinary IPv4 TCP, UDP and ICMP, subject to mandatory denies and the installation's approved private ranges. |
| `{ blockAll: true }` | No external guest traffic or external DNS. |
| `{ allowDomains: [...] }` | Matching TLS SNI names on TCP 443, subject to each entry's address bounds. |
| `{ allowDomains: [] }` | Full blocking; normalized to `{ blockAll: true }`. |

The two branches are exclusive, including `blockAll: false`. Empty objects,
null, bare booleans and unknown fields are invalid.

`allowDomains` accepts at most 256 unique normalized patterns. Names use ASCII
DNS A-labels with at least two labels, normalized to lowercase with one final
dot removed. The maximum length is 253 characters, including a leading `*.`;
labels contain 1–63 letters, digits or hyphens without leading/trailing hyphens.
IP literals, numeric-only names, localhost names, Unicode, URLs, ports, paths and
whitespace are invalid. Use explicit IDNA A-labels for international names.

`*.example.com` matches `api.example.com` and `a.b.example.com`, but excludes
`example.com`. Exact names grant no subdomains. A wildcard can appear only once,
at the beginning, before a multi-label suffix; `*` and `*.com` are invalid.
Overlapping entries are positive grants: any one matching entry must satisfy
its own address bounds. An exact entry cannot narrow a broader wildcard.

Strings permit public IPv4 destinations only. Objects require 1–64 unique
canonical IPv4 CIDRs in `ipRanges`; host bits are rejected. Those ranges bound
all addresses for that entry and grant no direct-IP or non-SNI access. Private
RFC1918 destinations additionally require containment in the installation's
approved business ranges, which default to empty. Management, metadata, host,
peer and other mandatory denies always apply. IPv6 and other non-global address
classes have no business exception.

With a callback, `prepareNetwork` replaces the entire preparation policy or
inherits `network` when omitted. Without a callback, preparation is fully
blocked, and explicit `prepareNetwork` or nonempty `prepareSecrets` is invalid.
Seed stages always block guest egress. The resolved phase policy contributes
to preparation cache identity. Image-building host networking and project
`config.build.secrets` are separate from VM preparation.

Protected Secret origins must match the phase's network policy. Full blocking
rejects protected bindings. Allowlisted origins require port 443 and a matching
concrete hostname; their later connections must satisfy the matching entry's
address bounds. Ordinary connectivity retains explicit HTTPS origin ports.
Raw and file bindings never grant additional network access.

See [Execution policies](/docs/concepts/execution-policies) for authoring,
placement, MCP and trust boundaries.

## Images

```ts
function image(key: string): ImageBuilder

interface ImageBuilder {
  readonly key: string
  from(ref: string): ImageBuilder
  run(argv: readonly string[]): ImageBuilder
  copy(source: SourceFile | SourceDirectory, destination: string): ImageBuilder
  copyFrom(destination: string, source: ImageBuilder, sourcePath: string): ImageBuilder
  workdir(path: string): ImageBuilder
  env(key: string, value: string): ImageBuilder
  user(name: string): ImageBuilder
}

const source: {
  file(path: string): SourceFile
  directory(path: string): SourceDirectory
}
```

Each method returns a new builder with one more step.

| Step | Description |
| --- | --- |
| `from(ref)` | Base image reference, for example `"node:24-bookworm-slim"`. |
| `run(argv)` | Runs an argv array without a shell. |
| `copy(source, destination)` | Copies a project file or directory, selected with `source.file()` or `source.directory()`. |
| `copyFrom(destination, image, sourcePath)` | Copies `sourcePath` from another `image()` into `destination`. |
| `workdir(path)`, `env(key, value)`, `user(name)` | Set the working directory, an environment variable, or the user for later steps and processes. |

Native addons loaded by Agent code and the shared libraries they need are
covered in [Configuration](/docs/reference/config#runtime-dependencies).

## Secret bindings

```ts
type SecretBinding = { readonly secret: string } & (
  | { readonly env: { readonly name: string; readonly mode: "protected"; readonly allowedOrigins: readonly string[] } }
  | { readonly env: { readonly name: string; readonly mode: "raw" } }
  | { readonly file: { readonly path: string } }
)
```

| Rule | Value |
| --- | --- |
| Bindings per Computer | At most 64 |
| Placement | Exactly one of `env` or `file` |
| `env.name` | Matches `^[A-Za-z_][A-Za-z0-9_]*$`; unique per Computer. Names starting with `HELMR_` or `LD_`, and `NODE_OPTIONS`, `NODE_PATH`, `NODE_ICU_DATA`, `OPENSSL_CONF`, `OPENSSL_MODULES`, `OPENSSL_ENGINES`, `GCONV_PATH`, `LOCPATH`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `NODE_EXTRA_CA_CERTS`, `NODE_USE_SYSTEM_CA` are reserved. |
| `protected` mode | Requires 1–16 exact HTTPS `allowedOrigins`; at most 256 origins per Computer. |
| `raw` mode | Does not accept `allowedOrigins`. |
| `file.path` | Absolute, normalized, at most 4096 bytes. Not under `/workspace`, `/var/lib/helmr`, `/dev`, `/opt/helmr`, `/proc`, `/sys`, `/run/helmr` or `/.helmr-old-root`. Paths must not repeat or nest. |

Bindings reference Secrets by stable ID and never contain values. The same
field names (`secret`, `allowedOrigins`) are used in REST request bodies. See
[Secrets](/docs/concepts/secrets) for the protection modes.

## Client: Templates

```ts
client.templates.get(id: string, query?: { deploymentId?: string }, options?): Promise<TemplateInfo>
client.templates.list(query?: { deploymentId?: string; cursor?: string; limit?: number }, options?): Promise<TemplatePage>
```

`TemplateInfo` includes `template`, `deploymentId`, `resources`, normalized
`network`, resolved `prepareNetwork`, and `prepareSecrets`. List items have the
same fields except `deploymentId`, which belongs to the page. The preparation
policy is explicit or inherited when a callback exists, and `{ blockAll: true }`
otherwise. Omitted runtime network reads as `{ blockAll: false }`.

Omitting `deploymentId` reads the current Deployment; a list cursor keeps its
original Deployment. `limit` is 1–100. `resources` contains normalized `milliCpu`,
`memoryMiB` and optional `diskMiB`. These are pinned configuration views, not a
live acknowledgement of packet enforcement. To inspect an existing Computer,
select its own `template` and `deploymentId`; see
[Inspect the pins](/docs/concepts/execution-policies#inspect-the-pins).

## Client: Computers

```ts
client.computers.create(request: ComputerCreateRequest, options?): Promise<ComputerRef>
client.computers.get(id: string, options?): Promise<Computer>
client.computers.list(query?: ComputerListQuery, options?): Promise<CursorPage<ComputerListItem>>
client.computers.ref(id: string): ComputerRef

interface ComputerCreateRequest {
  readonly template: string
  readonly key?: string
  readonly secrets?: readonly SecretBinding[]
  readonly idempotencyKey?: string
}
```

`create` uses the current Deployment. `template` is the identifier declared in `template({ id, ... })`. `key` is an optional immutable lookup value; it does not make creation
get-or-create. Unused retention comes from the Template definition.

`ComputerListQuery` is either `{ secret?, cursor?, limit? }` or an exact lookup
`{ key }`, which does not accept `secret`, `cursor` or `limit`. `ComputerListItem` is a
`Computer` without `secrets`.

`secret` lists only non-deleted Computers that received a raw value of that
Secret. Each item then has `secretExposure`: `"received"` when a process or
Command on the Computer received the value directly, or `"image"` when its image
was prepared with the value, so the Computer may hold it. Use it after revoking
or rotating a Secret to find the raw copies that stay on Computers.

```ts
const holders = await client.computers.list({ secret: "DATABASE_PASSWORD" })
```

### `ComputerRef`

| Method | Returns | Description |
| --- | --- | --- |
| `get(options?)` | `Computer` | Current state, including Secret bindings. |
| `sessions.list({ cursor?, limit? }?, options?)` | `CursorPage<ComputerUsage>` | Sessions currently using the Computer, including pending cleanup; `limit` 1–100. |
| `commands.list({ cursor?, limit? }?, options?)` | `CursorPage<ComputerUsage>` | Commands currently using the Computer, including pending cleanup; `limit` 1–100. |
| `exec(request, options?)` | `CommandRef` | Admits a Command. See [Commands](#client-commands). |
| `delete({ idempotencyKey?, dryRun? }?, options?)` | `{ computerId, sessionsCancelled, commandsCancelled }` | Requests deletion. Deleting cancels its Sessions and the Sessions they started, and its unfinished Commands. `dryRun` reports the counts without changing anything. |

```ts
interface ComputerCommandRequest {
  readonly command: readonly string[]
  readonly idempotencyKey?: string
  readonly cwd?: string
  readonly env?: Readonly<Record<string, string>>
  readonly stdin?: Uint8Array
  readonly timeout?: Duration
}
```

`exec()` returns after admission; use the returned Command's `wait()` to observe
completion. `timeout` limits execution, defaults to `5m`, and accepts `1ms` through
`24h`. It is separate from the timeout on `CommandRef.wait()`. Without
`idempotencyKey`, each call admits a new Command; reuse a key to retry one admission.

`ComputerUsage` is `{ id, usageState, createdAt }`,
where `usageState` is `admitted`, `running`, `waiting`, `parked`, `draining` or
`unreconciled`. These are current occupancy summaries, not full history or
Session/Command lifecycle statuses. Each collection has a separate cursor.

### `Computer`

| Field | Type |
| --- | --- |
| `id`, `template`, `deploymentId` | `string` |
| `key` | `string`, optional |
| `status` | `"available" \| "deleting" \| "deleted"` |
| `residency` | `"cold" \| "starting" \| "running" \| "parking" \| "parked" \| "restoring" \| "unavailable"` |
| `executionWait` | `{ reason: "scheduling" \| "environment_limit" \| "saving" }` or `null` |
| `error` | `{ code, message }`, present when `residency` is `unavailable` |
| `deleteAfterUnused` | `Duration` (`"never"` when disabled), from the origin Template |
| `unusedSince` | `string \| null` |
| `secrets` | `SecretBinding[]` |
| `createdAt`, `updatedAt` | `string` |

## Client: Commands

```ts
client.commands.ref(id: string): CommandRef

interface CommandRef {
  readonly id: string
  get(options?): Promise<CommandInfo>
  wait(options?: { signal? }): Promise<CommandOutcome>
  wait(options: { timeout: Duration; signal? }): Promise<TimedCommandWaitResult>
  cancel(options?): Promise<CommandCancelReceipt>
  readonly logs: {
    list(query?: { cursor?: string; limit?: number }, options?): Promise<CommandLogReadPage>
    stream(query?: { after?: string }, options?): AsyncIterable<CommandLogRecord>
  }
}
```

| Member | Description |
| --- | --- |
| `get()` | `CommandInfo`: `{ id, computerId, status, processReconciled, outcome? }`. `status` is `pending`, `starting`, `running`, `stopping`, `exited`, `failed`, `cancelled`, `timed_out` or `lost`. |
| `wait()` | Resolves with the `CommandOutcome`. With `timeout`, resolves with `{ status: "timeout" }` or `{ status: "settled", outcome }`; a timeout does not cancel the Command. |
| `cancel()` | Requests termination. Returns `{ id, targetId, status: "accepted" }`; repeated calls return the same receipt. |
| `logs.list()` | One page: `{ items, outputState, nextCursor? }`, where `outputState` is `open`, `closed` or `unavailable`. `limit` 1–100. |
| `logs.stream()` | Retained output followed by new output until the output closes. Resume with `after: record.cursor`. |

The Command process scope ends when its main process exits, including exit code
0, or when it is cancelled or times out. Remaining descendants are terminated;
backgrounding a child does not extend the scope. For example, `sh -c 'server &'`
does not leave a server running after the shell exits. Run the program as the Command's main process
and retain its Command handle when you need to observe or cancel it within the
execution limit.

Commands share the Computer's filesystem, but each has a private process scope;
they do not provide a shared shell or cross-command process-ID visibility.
Commands that have not finished, and finished Commands awaiting physical cleanup,
prevent idle checkpoint capture.
These boundaries apply to external Commands; native children inside an Agent
Session follow the Session's lifecycle.

An execution timeout or `cancel()` requests termination of the Command scope.
A client wait timeout or disconnection does not cancel an admitted Command.
Cancelling a terminal Command leaves its outcome unchanged. `processReconciled`
reports physical cleanup separately from the outcome; a cancellation receipt
alone does not establish that cleanup has finished.

`CommandOutcome` has `commandId`, `terminalAt` and a `kind`:

| `kind` | Extra fields |
| --- | --- |
| `exited` | `exitCode` |
| `cancelled`, `timed_out` | none |
| `system_failed` | `failure.reason`: `guest_failure`, `dispatch_failed` or `scope_termination_failed` |

`CommandLogRecord` has `cursor` and `stream` (`stdout` or `stderr`), and is one of:

| `kind` | Fields |
| --- | --- |
| `output` | `content: Uint8Array`, `observedAt` |
| `gap` | `fromSequence`, `throughSequence` (records that are no longer available) |

Output chunks are bytes, not necessarily whole lines or characters. Ordering
holds within each stream only.

```ts
const command = await client.computers.ref(computerId).exec({
  command: ["npm", "test"],
  idempotencyKey: "test:revision-42",
})
for await (const record of command.logs.stream({}, { signal })) {
  if (record.kind === "output") process.stdout.write(record.content)
}
const outcome = await command.wait()
```

Agent code that needs to run local processes uses ordinary child processes; a
`HelmrClient` inside Agent code acts with its own API key.

`executionWait` describes an observed wait for accepted runnable work. `scheduling`
means placement is pending, `environment_limit` means the Environment's execution
limit prevents admission, and `saving` means work is waiting for a save or
checkpoint transition. Held work is not reported as waiting for
capacity. `null` does not promise guest readiness; use residency and errors too.
The HTTP field is `execution_wait`. Existing polling refreshes this observation.
