# Compute: nodes, functions and machines

> How the dial-out compute fleet enrolls nodes, what the secure and community trust tiers mean for placement, and how to deploy WASM functions and machines with the compute.* procedures.

Compute is the ClutchCall fleet that runs your code. A **node** is a
machine that dials out to the engine and offers capacity. A **function**
is a WASM component that the engine hands to a node to run. A
**machine** is a microVM workload. All three are scoped to an org, and
all three are reachable from the **Inference → Compute** screen, which
has one tab per resource.

| Resource   | What it is                                   | Procedure namespace   |
| ---------- | -------------------------------------------- | --------------------- |
| Nodes      | Enrolled hardware that dials out and runs work | `compute.nodes.*`     |
| Functions  | WASM components submitted inline               | `compute.functions.*` |
| Machines   | microVM workloads                              | `compute.machines.*`  |

## The dial-out model

A node is never dialled into. It opens an outbound connection to the
engine and registers itself, so a node needs only **outbound UDP/443**
and no inbound ports, no public address, and no port forwarding. This is
why a laptop behind NAT and a rack box are enrolled the same way.

The engine — `mod_compute` — owns the session. It holds the heartbeat
clock, tracks which nodes are currently registered, and decides where a
workload is placed. Nothing in the console places work directly.

## Enrolling a node

`compute.nodes.enroll` creates the node record and mints its credential
in one call. The enroll form on the Compute screen collects:

| Field         | Required | Meaning                                                                          |
| ------------- | -------- | -------------------------------------------------------------------------------- |
| `nodeId`      | yes      | The name the machine presents at REGISTER. Stable across reconnects.             |
| `label`       | no       | Free text for humans, e.g. `rack 3, spare box`.                                  |
| `region`      | no       | Pins placement server-side, e.g. `blr1`.                                         |
| `trustTier`   | yes      | `secure` or `community`. See below.                                              |
| `contributor` | no       | Who contributed the hardware. Community tier only. An operator note — the scheduler never reads it. |

`nodeId` must match `^[a-z0-9][a-z0-9-]*$`: lowercase letters, digits and
dashes, starting with a letter or a digit.

## The one-time credential

`compute.nodes.enroll` returns `{ node, token }`. The token is returned
**exactly once**. Only its hash is stored, so there is no later
procedure that can hand it back; the console keeps it in component state
until you dismiss the panel, and re-fetching the node list will not
reproduce it. If you lose it, revoke the node and enroll again.

Start the agent with the credential:

```bash
clutch-noded \
  --server <engine>:443 \
  --node node-a1 \
  --token <token> \
  --runtimes wasm
```

| Flag          | Effect                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `--server`    | Engine host and port the agent dials out to.                                                      |
| `--node`      | The `nodeId` presented at REGISTER. Must match the enrolled record.                               |
| `--token`     | The one-time credential from enroll.                                                              |
| `--runtimes`  | Which runtimes this node offers, e.g. `wasm`. Reported in the node manifest.                      |
| `--community` | Declares the box as contributed hardware. Refuses to boot microVMs unless `--jailer` is also set. |
| `--jailer`    | Enables the jailer required to boot microVMs on a `--community` node.                             |

Once registered, the node reports a manifest. The console reads
`cpu_millis`, `mem_mb` and `runtimes` out of it for the Capacity column.

## Trust tiers and the workload opt-in

A node is either **secure** — hardware you operate — or **community** —
somebody else's hardware, running other people's code. The tier is one
half of a pair:

- Every node shows its tier (`SECURE` / `COMMUNITY`).
- Every function and machine shows whether it opted in
  (`SECURE ONLY` / `COMMUNITY OK`).

A workload is only ever placed on a community node if its own
declaration opted in. For functions that is the `allowCommunity` flag on
`compute.functions.deploy`, surfaced in the Placement column. A fleet
where you cannot see which machines are yours is a fleet where the
opt-in means nothing, which is why the screen always shows both halves
side by side.

`compute.nodes.setTrustTier({ orgId, id, trustTier })` flips a node
between the two tiers. It takes effect on the node's **next REGISTER**
— it does not evict what is already running there.

## Node liveness and last seen

The node list is polled every 15 seconds and shows a **Last seen**
column derived from `last_seen_at`. Treat that column as **advisory
only**. Liveness belongs to the engine: `mod_compute` holds the
heartbeat clock and decides what may receive work. A recent `last seen`
is not permission to place.

A failed list request is not an empty fleet. The screen distinguishes
"couldn't load nodes" (with the error message) from "no nodes enrolled",
and keeps polling.

## Draining and revoking

| Action                                                | Effect                                                                 |
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
| `compute.nodes.setDraining({ orgId, id, draining })`  | A draining node keeps what it is running and receives nothing new. Reversible — the same procedure with `draining: false` resumes it. |
| `compute.nodes.revoke({ orgId, id })`                 | Removes the node from the fleet. Its credential no longer registers.   |

Drain first when you want to take hardware out of rotation without
interrupting in-flight work.

## Deploying a WASM function

A function is a WASM component that is **submitted and collected**: the
component bytes travel **inline** to the node, base64-encoded, so nodes
need no registry credential and make no outbound fetch of their own.

The deploy form reads the file in the browser, checks the first bytes for
the WebAssembly magic (`00 61 73 6d`, in a file of at least 8 bytes),
and base64-encodes it. The engine checks the magic too and refuses a bad
module at deploy time rather than at first invoke — the browser-side
check exists only so the person who picked the file finds out while they
are still looking at it.

`compute.functions.deploy` takes:

| Field            | Notes                                                                          |
| ---------------- | ------------------------------------------------------------------------------ |
| `orgId`          | Owning org.                                                                    |
| `name`           | Must match `^[a-z0-9][a-z0-9-]*$`, e.g. `resize-thumbnails`. Defaults from the filename with `.wasm` stripped. |
| `component`      | The base64-encoded `.wasm` component.                                          |
| `cpuMillis`      | CPU request in millicores.                                                     |
| `memMb`          | Memory request in MB.                                                          |
| `allowCommunity` | Whether this function may be placed on community-tier nodes.                   |

### Resource requests

The deploy form validates these as integers in JavaScript on submit —
HTML `min`/`max` on a number input is advisory, and an emptied field
reads as `0`, so the bounds are enforced in code:

| Field       | Default | Accepted range      |
| ----------- | ------- | ------------------- |
| `cpuMillis` | `1000`  | 100 – 16000         |
| `memMb`     | `64`    | 16 – 4096           |

The function list renders these back as cores and MB, alongside the
component size in KB and the `region` when one is set on the record.

## Invoking a function

`compute.functions.invoke({ orgId, name })` runs a deployed function.
The **Run** button on each row calls it and renders the result:

| Field        | Meaning                                    |
| ------------ | ------------------------------------------ |
| `ok`         | Whether the invocation succeeded.           |
| `exitCode`   | The component's exit code.                  |
| `durationMs` | Wall-clock duration of the invocation.      |
| `error`      | Error text, when the invocation failed.     |

## Machines

Machines are microVM workloads, declared per org and listed on the
Machines tab. They carry the same community opt-in as functions, shown
per machine, and the same trust-tier rule applies to placement.

Because a microVM is a stronger isolation boundary to hand to
contributed hardware, the agent enforces the pairing itself: a node
started with `--community` refuses to boot microVMs unless `--jailer` is
also set. A community node without the jailer can still take WASM
functions.

## Tenant scoping

The console shell is operator-global. `inference.*` procedures take no
org, but every `compute.*` procedure is **org-scoped** and takes an
`orgId`. The Compute screen therefore uses the org the operator selected
on the portal, and says so plainly when none is selected.

It does not substitute a placeholder org. A fabricated tenant renders an
empty fleet, and "this tenant has no nodes" is a very different fact
from "no tenant is selected".

## The compute API surface

| Procedure                     | Input                                                                     | Returns                    |
| ----------------------------- | ------------------------------------------------------------------------- | -------------------------- |
| `compute.nodes.list`          | `{ orgId }`                                                               | `{ nodes: [...] }`         |
| `compute.nodes.enroll`        | `{ orgId, nodeId, label?, region?, trustTier, contributor? }`             | `{ node, token }` — token shown once |
| `compute.nodes.setTrustTier`  | `{ orgId, id, trustTier }`                                                | Applies at next REGISTER   |
| `compute.nodes.setDraining`   | `{ orgId, id, draining }`                                                 | —                          |
| `compute.nodes.revoke`        | `{ orgId, id }`                                                           | —                          |
| `compute.functions.list`      | `{ orgId }`                                                               | `{ functions: [...] }`     |
| `compute.functions.deploy`    | `{ orgId, name, component, cpuMillis, memMb, allowCommunity }`            | —                          |
| `compute.functions.invoke`    | `{ orgId, name }`                                                         | `{ ok, exitCode, durationMs, error? }` |

Node records expose `id`, `node_id`, `label`, `contributor`,
`trust_tier`, `region`, `draining`, `last_seen_at` and the reported
`manifest` (`cpu_millis`, `mem_mb`, `runtimes`). Function records expose
`id`, `name`, `size_bytes`, `cpu_millis`, `mem_mb`, `allow_community`
and `region`.

## Related

- [Authentication](/concepts/authentication) — API keys and org scoping for control-plane calls
- [Telemetry](/platform/telemetry) — metrics and traces emitted by the platform
