# Enroll a host

**Released clients:** no-save SSH enrollment is available in CLI 0.13/Python 0.10 and later. CLI 0.16 (including its npm wrapper)/Python 0.13 support post-enrollment saving; [real hosted staging acceptance](https://github.com/dreamlake-ai/dreamlake-workspace/blob/main/infra/vault/hosted-host-saving-acceptance.md) covers Nymph installation, saving, restored SSH and tracked execution. Client release, backend rollout and each environment's live verification remain separate.

Use CLI **0.21.2**, Python **0.16.2** and Nymph **0.1.8** for the current [testing guide](/lakeshore/hosts/testing.md). Existing services are not upgraded by installing a new client; inspect the installed daemon version before testing recovery.

A **host** is a machine that executes or manages work, including a Slurm control node. Nymph maintains its connection independently after SSH bootstrap. Providers are not required to enroll an existing machine. Start with an SSH-accessible host; use [Resource setup](/lakeshore/providers/resource-setup.md) only when infrastructure is needed.

## Inspect in Compute

The [Compute dashboard](https://github.com/dreamlake-ai/dreamlake-ai/pull/310) uses `/:namespace/compute` for the host inventory and `/:namespace/compute/hosts/:hostId` for details. It preserves enrollment instructions, Unix-user identity, verified heartbeat status and a link to tracked runs. Search a group with a pattern such as `fortyfive/bos14/*`, or search `offline`. **Enroll host** opens the CLI handoff; enrollment still starts from an SSH-accessible machine.

Deployed to production on September 15, 2026 (05:10 UTC September 16). The old `/:namespace/hosts` list and detail links redirect to Compute. Host APIs, CLI commands and these guide URLs retain their existing names. See the [UI review guide](/lakeshore/hosts/testing/#review-the-live-host-ui).

## Repair an existing enrollment

CLI **0.20.1** and Python **0.16.1** configure persistent enrolled services with
`runtime.keep_alive_s = -1`. Re-enrollment regenerates `nymph.toml` and its systemd
user unit. Before repair, privately back up those two files on the target; retain
the original identity key. After repair, verify the generated idle setting, then
restore only required custom sections such as `private_tracked` and any reviewed
service overrides. Reload the user manager and restart the same unit if you edit
it, then wait for a fresh verified host status.

For an unresolved response, reuse the original request ID to reconcile it. A
confirmed `request_expired` receipt requires a new explicit repair operation ID;
a separately intended repair after a known completed enrollment is also a new
operation. If the server reports `enrollment_busy`, wait until its `retryAt`
window instead of issuing more operation IDs. Keep the same host name, SSH
profile, Unix user and identity key: the existing host/enrollment is reused,
and changing the identity is rejected. Keep the new ID for reconciliation:

**CLI**

```shell
dreamlake hosts enroll -n fortyfive/bos14/bos14-ctrl \
  --ssh bos14-ctrl --request-id bos14-idle-repair-001 \
  --no-save-credentials
dreamlake hosts status fortyfive/bos14/bos14-ctrl
```

**Python**

```python
from dreamlake import DreamLakeClient

client = DreamLakeClient()
result = client.hosts.enroll(
    "fortyfive/bos14/bos14-ctrl",
    ssh="bos14-ctrl",
    request_id="bos14-idle-repair-001",
    save_credentials=False,
)
host = client.hosts.status("fortyfive/bos14/bos14-ctrl")
```

See the [patch release and owned-host idle validation](/dev/notes/host-enrollment)
for evidence and the distinction between service restart and machine reboot.

## Enroll and inspect

The target needs Python 3, OpenSSL, a systemd user manager with linger enabled by its administrator, and control-plane connectivity. Bootstrap reuses installed nymph or invokes the official installer; it does not install uv. Authenticate your client, verify SSH access, then enroll:

**CLI**

```shell
ssh bos14-ctrl true
dreamlake hosts enroll -p fortyfive/bos14 -n bos14-ctrl \
  --ssh bos14-ctrl --request-id bos14-first-enrollment
dreamlake hosts status fortyfive/bos14/bos14-ctrl
dreamlake hosts status 'fortyfive/bos14/*'
```

**Python**

```python
from dreamlake import DreamLakeClient
client = DreamLakeClient()
result = client.hosts.enroll(
    name="bos14-ctrl", prefix="fortyfive/bos14", ssh="bos14-ctrl",
    request_id="bos14-first-enrollment", wait_seconds=60,
)
host = client.hosts.status("fortyfive/bos14/bos14-ctrl")
group = client.hosts.status("fortyfive/bos14/*")
```

Names are `<namespace>/<group>/<host-name>`. `-p fortyfive/bos14 -n bos14-ctrl` and `-n fortyfive/bos14/bos14-ctrl` identify the same host; conflicting prefixes fail. Keep the machine name (`bos14-ctrl`), and use `bos14-node-000` for another host. Wildcards inspect existing hosts only. A namespace prefix grants no authorization.

The API checks namespace access before target identity creation. Nymph's private key stays on the target; a short-lived grant binds its public key to host, Unix user, and enrollment. The initial implementation reserves the name to its owner and rejects another identity. Adding a second Unix user to the same host requires a future explicit linking workflow; names alone cannot authorize it.

## SSH, passwords, and jump hosts

`--ssh` accepts an SSH-config alias, `user@host`, or a quoted argument vector without a repeated `ssh` executable. Existing local SSH configuration remains useful:

```ssh-config
Host bos14-ctrl
  HostName bos14-ctrl.internal
  User geyang
  Port 2222
  ProxyJump ge@bastion.example.com
  IdentityFile ~/.ssh/bos14_ed25519
```

```shell
dreamlake hosts enroll -p fortyfive/bos14 -n bos14-ctrl \
  --ssh '-J ge@bastion.example.com
    -p 2222
    -i /Users/ge/.ssh/bos14_ed25519
    geyang@bos14-ctrl.internal'
```

Multiline strings preserve argument boundaries without shell expansion. There must be one destination and no appended remote command. Agent forwarding is not required. Never put passwords or private-key contents in arguments or JSON. Verify password access with `ssh geyang@bos14-ctrl.internal`; CLI terminal/password and separate jump-host prompt behavior still need real acceptance. Python always uses noninteractive SSH and fails if a password or passphrase prompt is needed.

## Configuration and dry-run

Save this as `bos14-host.json`. Identity-file paths refer to local bootstrap credentials, not uploaded key contents.

```json
{
  "name": "fortyfive/bos14/bos14-ctrl",
  "ssh": {
    "host": "bos14-ctrl.internal",
    "user": "geyang",
    "port": 2222,
    "identityFile": "/Users/ge/.ssh/bos14_ed25519",
    "jumpHost": "ge@bastion.example.com",
    "options": { "ServerAliveInterval": "30" }
  }
}
```

**CLI**

```shell
dreamlake hosts enroll --config ./bos14-host.json --dry-run --json
```

**Python**

```python
plan = client.hosts.plan(config="bos14-host.json")
preview = client.hosts.enroll(config="bos14-host.json", dry_run=True)
```

Explicit name/prefix fields override configuration fields; SSH replaces the whole SSH object. Unknown fields, invalid ports, unsupported options, and conflicts fail before effects. Dry-run returns `status: "validated"`, `enrolled: false`, and `authorizationVerified: false`; it contacts no server/host and reads no key. Direct-on-target enrollment without SSH is not implemented.

## Readiness, retry, and credentials

`online` means the backend verified a fresh active worker and matching bound identity; it does not prove runner readiness or successful execution. Timeout can return `pending`. Results include stable host/enrollment IDs and operation/request IDs, never bootstrap grants. Reuse the same request ID to reconcile an uncertain write; changed payloads conflict. Exact/group status uses the caller's access. Python raises sanitized exceptions rather than prompting or exiting.

Repeated enrollment and signed restart passed against real services with local transport/supervision adapters. Still verify actual Linux supervision, reboot recovery, and a workload on the intended host. Installation or a heartbeat alone is insufficient.

Credential saving happens after enrollment in native CLI 0.15/Python 0.12 with a compatible backend. Interactive consent defaults to `[y/N]`; `--save-credentials` consents, `--no-save-credentials` declines, and `--quiet` only suppresses progress. Password prompts are masked; Python accepts explicit `HostCredential` inputs and never prompts. Target and jump credentials are selected separately. Saving failure leaves the successful enrollment intact and does not authorize backend SSH delegation. Use CLI 0.16 or later for npm-wrapper password descriptors; the 0.15 wrapper dropped descriptors above 2. See [Managing host credentials](/lakeshore/hosts/credentials.md) for status and the paired acceptance examples.

## Live infrastructure is a separate delivery state

The September 12 bos14 demonstration had a signed Slurm heartbeat directly to `dev-api.lakeshore.dreamlake.ai:443` with trusted TLS and no laptop tunnel. Operator SSH still needs VPN. That recorded deployment required a fresh token after process restart; merged reconnect code does not prove it was upgraded. Production `api.lakeshore.dreamlake.ai` was verified as a separate fresh TLS-protected instance with zero workers. Neither observation establishes production host-API rollout or workload execution.

## Related documentation

- **[Python host API](https://github.com/fortyfive-labs/dreamlake/blob/main/docs/hosts.md)** — Actual methods, errors, pagination, and noninteractive behavior.
- **[Host credentials](/lakeshore/hosts/credentials.md)** — Vault operations and the optional enrollment-saving boundary.
- **[Resource setup](/lakeshore/providers/resource-setup.md)** — EC2/EKS Terraform examples; existing hosts need no provisioning.
- **[Python follow-up plan](/dev/plans/hosts-python-api)** — Remaining parity and acceptance work.
- **[Provider plan](/dev/plans/providers)** — Separate Slurm/Kubernetes registration, provisioning, and workload acceptance.
