# SSH Provider

**`[ dev ]`** — you can register an SSH provider and drive it from the CLI, but
no server-side launch route accepts one, so a [queue](/lakeshore/queues.md) cannot
grow workers on it. See [Providers](/lakeshore/providers.md) for what the marker
means.

`--launcher SSH` means "connect to a host that is already running."
Nothing is provisioned. The default `dispatch` is `direct`: the CLI opens
an SSH session per call and runs the script. Use it for lab boxes, dev
machines, and any server you keep around by hand.

| Phase | What happens |
| ----- | ------------ |
| Provision | Nothing. The host is expected to be up and reachable. |
| Launch | An `ssh` session runs the script. |
| Default dispatch | `direct` |

SSH providers are the one launcher the CLI drives itself — the control
plane's `/providers/:name/launch` routes support only `EC2`, `GCE`, and
`Kube`, and answer 422 for an SSH provider. `lakeshore providers test`
and `lakeshore providers instances` both shell out to the local `ssh`
binary.

## Kwargs

These are the keys the CLI's SSH paths actually read:

| Field | Type | Default | Meaning |
| ----- | ---- | ------- | ------- |
| `ip` (or `host`) | string | — | **Required.** Host IP or DNS name. |
| `username` (or `user`) | string | `ubuntu` | SSH user. |
| `port` | int | 22 | SSH port. |
| `pem` | string | — | Path to a private key on the *local* machine. `~` is expanded. |

`lakeshore ssh upload` writes a few more keys that survive on the row and
are handed to whatever consumes them later: `proxy_jump`,
`proxy_command`, `ssh_options` (a bag of leftover `ssh_config`
directives), and `_meta.sshFingerprint`.

Kwargs are an open bag, so any other key you set round-trips
untouched — but only the four above change how the CLI dials the host
today.

> **Warning:** `lakeshore ssh upload --with-key` stores the private key as an `ssh_key`
> Secret and writes `ssh_key: { "$secret": "<name>" }` into the kwargs. The
> CLI's own `ssh` invocation reads only `pem` — a local file path — so a
> provider whose key lives in a Secret has no key for `providers test` to
> pass to `ssh -i`. Keep a local `pem` path as well if you want the direct
> path to work.

## RunConfig fields

None. An SSH host is fixed, so there is no per-machine choice to make at
call time and a mode pointing at an SSH provider adds no
launcher-shaped fields.

Hardware on an SSH box is not declarative either. A daemon on the host
detects CPU / GPU / kernel / distro at registration and advertises them
as capabilities and tags; match on those from a mode if you want to
target specific boxes in a pool.

## Example

**CLI tab:** `--field` values that start with `[`, `{`, or `"` are parsed as JSON;
`true` / `false` / `null` and anything number-shaped are converted;
everything else is a verbatim string. Dotted keys nest into objects, so
build lists with JSON rather than an index.

**.dreamrc tab:** The same shape in a local `.dreamrc`:

**CLI**

```bash
lakeshore secrets add my-dev-key --kind ssh_key --from-file ~/.ssh/id_ed25519

lakeshore providers add my-dev-box --launcher SSH \
  --kwarg ip=192.168.1.42 \
  --kwarg username=gyang \
  --kwarg pem=~/.ssh/id_ed25519

lakeshore modes add dev \
  --field provider=my-dev-box \
  --field runner=process \
  --field tags='["dev"]'
```

**.dreamrc**

```yaml file=".dreamrc"
providers:
  my-dev-box: !providers.SSH
    ip: 192.168.1.42
    username: gyang
    pem: ~/.ssh/id_ed25519

modes:
  dev:
    provider: my-dev-box
    runner: process
    tags: [dev]
```

## Smoke test

```bash
lakeshore providers test my-dev-box                      # built-in hello.py
lakeshore providers test my-dev-box --script ./mine.py   # your own
lakeshore providers instances my-dev-box                 # one row: running / unknown
```

`providers test` accepts `--timeout <seconds>` (default 600) and
`--dreamrc <path>`. `providers instances` pings the host with a fast
`ssh … echo ok` and reports `running` on success, `unknown` otherwise;
its `--timeout` defaults to 10 and is clamped to at most 5 seconds for
the ping itself.

If a test hangs, take Lakeshore out of the picture first:

```bash
ssh -i <pem> <user>@<ip> echo ok
```

## Enroll a host

Host enrollment and daemon bootstrap documentation now lives under Hosts:

- [Enroll a host](/lakeshore/hosts/enroll.md) — planned names, SSH transport, JSON configuration, and readiness.

## Importing from `~/.ssh/config`

You probably already have the hosts. `lakeshore ssh discover` reads
`~/.ssh/config` and shows which aliases exist locally and which are
already Providers; `lakeshore ssh upload` registers the ones you pick.

### `ssh discover`

```bash
lakeshore ssh discover                       # every entry
lakeshore ssh discover --filter new          # not yet on the server
lakeshore ssh discover --filter existing     # already a Provider
lakeshore ssh discover --show-stripped       # per-entry stripped-directive detail
```

Rows print `ALIAS`, `TARGET`, `KEY`, `TAGS`. Two tags appear in the
`TAGS` column:

| Tag | Meaning |
| --- | ------- |
| `#stripped` | The entry carries local-only directives that will be dropped on upload. |
| `#exists` | The alias is already a Provider on the control plane. |

A footer explains whichever tags actually appeared. Any bad `--filter`
value exits 2.

Entries that resolve to the same target collapse into one row. The
dedup fingerprint is a short hash of `(host, user, port, expanded key
path, ProxyJump, ProxyCommand)` — so two aliases differing only in
comment or `RemoteCommand` are one entry.

### `ssh upload`

```bash
lakeshore ssh upload my-dev-box                   # one alias
lakeshore ssh upload --all                        # every dedup-group head, non-interactive
lakeshore ssh upload                              # no alias → interactive multi-select
lakeshore ssh upload --interactive my-dev-box     # force the picker anyway
```

`--all` wins and never prompts (CI-friendly). Otherwise `--interactive`
or a missing alias opens a checkbox picker; **Space** toggles, **Enter**
confirms. Overrides for a single upload: `--name` (provider name,
defaults to the alias), `--user`, `--host`, `--port`, `--pem`, and
`--dispatch` (silently ignored unless it is exactly `direct` or
`daemon`).

`--with-key` reads the `IdentityFile` and uploads its bytes as an
`ssh_key` Secret named `ssh-<alias>` (override with
`--key-secret <name>`), then references it from the provider. The
default records the local path instead and leaves the key on disk —
uploading a private key crosses a real trust boundary, so it is opt-in.

### Stripped directives

These exist for the local user's interactive convenience and mean
nothing to a different machine dialing the same host, so they are
dropped before the kwargs reach the server:

```text
RemoteCommand, RequestTTY, ForwardAgent, ForwardX11, ForwardX11Trusted,
LocalCommand, PermitLocalCommand, SendEnv, SetEnv,
ControlMaster, ControlPath, ControlPersist, Compression,
Tunnel, TunnelDevice, StreamLocalBindUnlink, StreamLocalBindMask,
LogLevel, VisualHostKey, Include
```

It is a denylist: anything not on it survives — `ProxyJump`,
`ProxyCommand`, hostname / port / user / `IdentityFile`, and any other
option, which lands in the `ssh_options` bag. Over-sending beats silently
dropping something the server needed.

## Probing and status

```bash
lakeshore ssh probe my-dev-box --timeout 5   # or `--all` for every alias
lakeshore ssh status --history 3
```

`probe` attempts a connection and records the outcome in a local cache
(`~/.config/dreamlake/ssh-status.json` unless `DREAMLAKE_STATE_DIR` says
otherwise); `status` prints the last few results per alias.
