# nodes (/docs/reference/api/nodes)

> Nodes are machines that joined the control plane, as their tenant sees them: a tenant sees the nodes the authorization projection holds for it. Every method except `list` is decided on `compute-node/{nodeId}`. Mutations take an optional `version`; when the node has moved past it, the call is `CONFLICT`. See [Operating nodes](/docs/guides/nodes/operating-nodes).



Nodes are machines that joined the control plane, as their tenant sees them: a tenant sees the nodes the authorization projection holds for it. Every method except `list` is decided on `compute-node/{nodeId}`. Mutations take an optional `version`; when the node has moved past it, the call is `CONFLICT`. See [Operating nodes](/docs/guides/nodes/operating-nodes).

All 14 methods are served by this release.

| Method                              | Kind     | Authorization                                    | Phase | Status     |
| ----------------------------------- | -------- | ------------------------------------------------ | ----- | ---------- |
| [`list`](#list)                     | read     | `compute:nodes:read` @ `compute-tenant` + items  | v0.1  | registered |
| [`get`](#get)                       | read     | `compute:nodes:read` @ `compute-node`            | v0.1  | registered |
| [`events`](#events)                 | read     | `compute:nodes:read` @ `compute-node`            | v0.1  | registered |
| [`cordon`](#cordon)                 | mutation | `compute:nodes:cordon` @ `compute-node`          | v0.1  | registered |
| [`uncordon`](#uncordon)             | mutation | `compute:nodes:cordon` @ `compute-node`          | v0.1  | registered |
| [`drain`](#drain)                   | mutation | `compute:nodes:drain` @ `compute-node`           | v0.1  | registered |
| [`label`](#label)                   | mutation | `compute:nodes:update` @ `compute-node`          | v0.1  | registered |
| [`taint`](#taint)                   | mutation | `compute:nodes:update` @ `compute-node`          | v0.1  | registered |
| [`adopt`](#adopt)                   | mutation | `compute:nodes:provision` @ `compute-node`       | v0.1  | registered |
| [`revokeIdentity`](#revokeidentity) | mutation | `compute:nodes:revoke-identity` @ `compute-node` | v0.1  | registered |
| [`delete`](#delete)                 | mutation | `compute:nodes:delete` @ `compute-node`          | v0.2  | registered |
| [`issueSshCert`](#issuesshcert)     | mutation | `compute:nodes:ssh-root` @ `compute-node`        | v0.3  | registered |
| [`revokeSshCert`](#revokesshcert)   | mutation | `compute:nodes:ssh-root` @ `compute-node`        | v0.3  | registered |
| [`listSshCerts`](#listsshcerts)     | read     | `compute:nodes:read`                             | —     | registered |

## list [#list]

`POST /api/compute/nodes/list`

Lists the nodes visible to the tenant in id order, filtered by pool, site, `cordoned` or `provisionState`.

The call is decided once on `compute-tenant/{tenantId}`, then each node is checked for `compute:nodes:read`; nodes the caller may not read are left out, so a page can hold fewer than `limit` items. Pages hold up to 50 nodes.

* **Kind:** read
* **Authorization:** `compute:nodes:read` @ `compute-tenant` + items
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field            | Type                                                                                                                                                               | Required | Constraints                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | --------------------------------------------------------------------- |
| `tenantId`       | string                                                                                                                                                             | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`            |
| `poolId`         | string                                                                                                                                                             |          | at most 128 characters; matches `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$` |
| `siteId`         | string                                                                                                                                                             |          | at most 64 characters; matches `^site_[0-9a-hjkmnp-tv-z]{26,52}$`     |
| `cordoned`       | boolean                                                                                                                                                            |          |                                                                       |
| `provisionState` | one of `adopting`, `adopted`, `enrolling`, `inspecting`, `commissioning`, `ready`, `deploying`, `deployed`, `releasing`, `rescue`, `failed`, `retiring`, `retired` |          |                                                                       |
| `limit`          | integer                                                                                                                                                            |          | 1-50; default `50`                                                    |
| `cursor`         | string                                                                                                                                                             |          | at most 256 characters; matches `^[A-Za-z0-9._:-]{1,256}$`            |

## get [#get]

`POST /api/compute/nodes/get`

Returns one node with its labels, conditions and session (epoch, gateway and connection times).

A node the tenant cannot see is `NOT_FOUND`.

* **Kind:** read
* **Authorization:** `compute:nodes:read` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type   | Required | Constraints                                                       |
| ---------- | ------ | -------- | ----------------------------------------------------------------- |
| `tenantId` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |

## events [#events]

`POST /api/compute/nodes/events`

Returns a node's events from the event outbox, oldest first.

`type` keeps only events whose type starts with the given prefix, such as `node.` or `security.`. Pages hold up to 200 events (100 by default). A node the tenant cannot see is `NOT_FOUND`.

* **Kind:** read
* **Authorization:** `compute:nodes:read` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `type`     | string  |          | at most 64 characters; matches `^[a-z][a-z0-9.-]{0,63}$`          |
| `limit`    | integer |          | 1-200; default `100`                                              |
| `cursor`   | string  |          | at most 41 characters; matches `^\d{1,20}:\d{1,20}$`              |

## cordon [#cordon]

`POST /api/compute/nodes/cordon`

Marks the node cordoned so placement puts no new work on it; existing allocations stay.

Cordoning a cordoned node returns it unchanged and publishes nothing. Otherwise the node's version goes up, its reconciler is queued, and `node.cordoned` is published.

* **Kind:** mutation
* **Authorization:** `compute:nodes:cordon` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `version`  | integer |          | at least 1                                                        |
| `reason`   | string  |          | at most 256 characters; matches `^[^\u0000-\u001f\u007f]*$`       |

## uncordon [#uncordon]

`POST /api/compute/nodes/uncordon`

Clears `cordoned` and ends a drain in progress, so the node is placeable again.

Decided as `compute:nodes:cordon`. A node that is neither cordoned nor draining is returned unchanged. Otherwise the `draining` reason is removed and `node.uncordoned` is published.

* **Kind:** mutation
* **Authorization:** `compute:nodes:cordon` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `version`  | integer |          | at least 1                                                        |
| `reason`   | string  |          | at most 256 characters; matches `^[^\u0000-\u001f\u007f]*$`       |

## drain [#drain]

`POST /api/compute/nodes/drain`

Cordons the node, adds the `draining` NoSchedule reason and hands the node to the Drain controller.

The Drain controller releases nothing in this release: it waits until the node holds no live allocation, then removes `draining` and publishes `node.drained`. The node stays cordoned until `uncordon`. Draining a node that is already draining returns it unchanged. A new drain publishes `node.draining`.

* **Kind:** mutation
* **Authorization:** `compute:nodes:drain` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `version`  | integer |          | at least 1                                                        |
| `reason`   | string  |          | at most 256 characters; matches `^[^\u0000-\u001f\u007f]*$`       |

## label [#label]

`POST /api/compute/nodes/label`

Sets and removes operator labels on the node and returns the node with its labels.

`set` takes up to 64 labels and `remove` up to 64 keys. The call is `INVALID_INPUT` when it names nothing, names a key in both `set` and `remove`, or sets a key that inventory or the system owns. `remove` removes only operator labels. The node's version goes up and `node.labeled` is published.

* **Kind:** mutation
* **Authorization:** `compute:nodes:update` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type           | Required | Constraints                                                                          |
| ---------- | -------------- | -------- | ------------------------------------------------------------------------------------ |
| `tenantId` | string         | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`                           |
| `nodeId`   | string         | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$`                    |
| `set`      | map of string  |          | at most 64 entries; keys match `^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,126}[A-Za-z0-9])?$` |
| `remove`   | list of string |          | 0-64 items; no repeats                                                               |
| `version`  | integer        |          | at least 1                                                                           |

## taint [#taint]

`POST /api/compute/nodes/taint`

Adds and removes operator NoSchedule taints, stored in `noScheduleReasons` as `taint:<key>`.

`add` and `remove` take up to 32 keys each. The call is `INVALID_INPUT` when it names nothing or names a key in both lists. Only operator taints change; conditions, `unreachable` and `draining` belong to their controllers. When the result equals the current reasons the node is returned unchanged; otherwise `node.tainted` is published.

* **Kind:** mutation
* **Authorization:** `compute:nodes:update` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type           | Required | Constraints                                                       |
| ---------- | -------------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string         | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string         | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `add`      | list of string |          | 0-32 items; no repeats                                            |
| `remove`   | list of string |          | 0-32 items; no repeats                                            |
| `version`  | integer        |          | at least 1                                                        |

## adopt [#adopt]

`POST /api/compute/nodes/adopt`

Re-runs acceptance checks on a node in `adopting` or `adopted`, moving it to `adopting`.

Decided as `compute:nodes:provision`. Any other provision state is `INVALID_STATE`. The node leaves placement until acceptance passes. Publishes `node.adopting`.

* **Kind:** mutation
* **Authorization:** `compute:nodes:provision` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `version`  | integer |          | at least 1                                                        |
| `reason`   | string  |          | at most 256 characters; matches `^[^\u0000-\u001f\u007f]*$`       |

## revokeIdentity [#revokeidentity]

`POST /api/compute/nodes/revokeIdentity`

Revokes every live certificate of the node and raises its identity generation, so no certificate of the old generation can renew.

Returns the node and `revokedCertificates`, the number of certificates revoked; the revocation reason is `reason`, or `identity revoked`. Each call raises the generation again. The machine comes back only by joining again. Publishes `security.node.identity-revoked`.

* **Kind:** mutation
* **Authorization:** `compute:nodes:revoke-identity` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.1 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `version`  | integer |          | at least 1                                                        |
| `reason`   | string  |          | at most 256 characters; matches `^[^\u0000-\u001f\u007f]*$`       |

## delete [#delete]

`POST /api/compute/nodes/delete`

Tombstones a node that is `retired`, `adopting` or `enrolling` (or failed from `adopting` or `enrolling`), or an `adopted` node that is cordoned, and has no open allocation. An adopted host has no retire workflow: cordon it, wait until it holds nothing, then delete it. For example, after its TPM or disk was replaced, deleting it lets the machine join again under a new identity.

Any other state, or an open allocation, is `INVALID_STATE`; a node that changed pools during the call is `CONFLICT`. The node's devices stop being bookable, its scalar resources are removed, its certificates are revoked, its hardware key and attestor bindings are freed so the machine can join again as a new node, and its pending commands are cancelled. The node leaves its pool and the projection, so nobody sees it; audit rows and past allocations keep referencing it.

A retry after the delete returns `deleted: false`. Publishes `node.deleted`.

* **Kind:** mutation
* **Authorization:** `compute:nodes:delete` @ `compute-node`
* **Module:** `server`
* **Phase:** v0.2 (registered)

**Input**

| Field      | Type    | Required | Constraints                                                       |
| ---------- | ------- | -------- | ----------------------------------------------------------------- |
| `tenantId` | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$`        |
| `nodeId`   | string  | yes      | at most 64 characters; matches `^node_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `version`  | integer |          | at least 1                                                        |
| `reason`   | string  |          | at most 256 characters; matches `^[^\u0000-\u001f\u007f]*$`       |

## issueSshCert [#issuesshcert]

`POST /api/compute/nodes/issueSshCert`

Issues a short-lived break-glass OpenSSH user certificate for one node, bound to the principal `<account>@<nodeId>`.

Decided as `compute:nodes:ssh-root`. The method comes from the tunnels module (v0.3) and is `UNSUPPORTED` when no compute SSH CA is configured. It needs an MFA session by default (`ACCESS_DENIED` with reason `mfa-required`), and a session that ends within a minute is `ACCESS_DENIED`. `publicKey` must be an Ed25519 or ECDSA P-256 key, and `principal` (default `root`) one of the configured accounts; otherwise `INVALID_INPUT`.

The lifetime is `ttlSeconds` (60 to 3,600, default 900), never past the caller's session. Issuance is rate-limited per identity (20 per hour by default, then `RATE_LIMITED`). Every certificate is recorded and `node.ssh-cert.issued` is published.

* **Kind:** mutation
* **Authorization:** `compute:nodes:ssh-root` @ `compute-node`
* **Module:** `tunnels`
* **Phase:** v0.3 (registered)

**Input**

| Field        | Type    | Required | Constraints                                                |
| ------------ | ------- | -------- | ---------------------------------------------------------- |
| `tenantId`   | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$` |
| `nodeId`     | string  | yes      | at most 57 characters; matches `^node_[0-9a-z]{26,52}$`    |
| `publicKey`  | string  | yes      | at most 4096 characters                                    |
| `principal`  | string  |          | matches `^[a-z_][a-z0-9_-]{0,31}$`; default `"root"`       |
| `ttlSeconds` | integer |          | 60-3600; default `900`                                     |

## revokeSshCert [#revokesshcert]

`POST /api/compute/nodes/revokeSshCert`

Revokes a break-glass SSH certificate before it expires; the node's revocation list carries it from the next NodeSpec on.

Decided as `compute:nodes:ssh-root` and hosted by the tunnels module (v0.3). Revoking again returns the same revocation. A serial not issued for this node is `NOT_FOUND`. Publishes `node.ssh-cert.revoked`.

* **Kind:** mutation
* **Authorization:** `compute:nodes:ssh-root` @ `compute-node`
* **Module:** `tunnels`
* **Phase:** v0.3 (registered)

**Input**

| Field      | Type   | Required | Constraints                                                |
| ---------- | ------ | -------- | ---------------------------------------------------------- |
| `tenantId` | string | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$` |
| `nodeId`   | string | yes      | at most 57 characters; matches `^node_[0-9a-z]{26,52}$`    |
| `serial`   | string | yes      | matches `^[0-9]{1,20}$`                                    |
| `reason`   | string |          | at most 500 characters                                     |

## listSshCerts [#listsshcerts]

`POST /api/compute/nodes/listSshCerts`

Lists the break-glass SSH certificates the tenant issued for one node, newest first, with each one's principal, issuer, lifetime and state; never the certificate itself.

Decided as `compute:nodes:read` on `compute-node/{nodeId}` and hosted by the tunnels module. `state` is `active`, `expired` or `revoked`; a revoked certificate also carries `revokedAt`, `revokedBy` (an identity, or `iam` when re-authorization revoked it) and `revokeReason` when one was given.

Only unexpired certificates are listed unless `includeExpired` is true; expired certificates are kept for one day, then removed by an hourly clean-up. Pages hold 1 to 500 certificates (100 by default), and `nextCursor` is set while more remain. A node the tenant cannot see is `NOT_FOUND`, and a malformed `cursor` is `INVALID_INPUT`.

* **Kind:** read
* **Authorization:** `compute:nodes:read`
* **Module:** `tunnels`
* **Phase:** not in PLAN.md §9.2 (registered)

**Input**

| Field            | Type    | Required | Constraints                                                |
| ---------------- | ------- | -------- | ---------------------------------------------------------- |
| `tenantId`       | string  | yes      | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$` |
| `nodeId`         | string  | yes      | at most 57 characters; matches `^node_[0-9a-z]{26,52}$`    |
| `includeExpired` | boolean |          | default `false`                                            |
| `limit`          | integer |          | 1-500; default `100`                                       |
| `cursor`         | string  |          | at most 128 characters                                     |
