# BooControl SSH editor verb-mode + model pull — proposal

**Status:** READY. Extends BooControl P9.1 (the SSH config editor) so it works
against a forced-command-locked SSH key and can pull HuggingFace models into a
host's models directory.

## Why

P9.1 shipped the SSH config editor sending raw shell commands (`cat`, `cp`,
`cat >`, the restart command) over SSH. To restrict the BooControl key to a
single drive/folder, the operator has deployed an `authorized_keys`
**forced command** on the GPU hosts that binds the key to a wrapper script
(`apps/control/remote/boocontrol-edit.{ps1,sh}`). A forced command ignores the
client's command string and only honors fixed **verbs** (`read` / `backup` /
`write` / `restart` / `pull <repo>`). So the editor's raw-shell commands are now
rejected by those hosts, and there is no way to drive the wrapper's `pull` verb.

This change teaches the editor to speak verbs (per host) and adds a model-pull
capability, closing the loop so a locked-down key is fully usable from the
cockpit.

## What changes

1. **Per-host SSH mode.** `control_hosts.ssh_mode` (`shell` | `wrapper`, default
   `shell` for backward compatibility). `shell` keeps today's raw-command
   behavior for hosts without a wrapper; `wrapper` sends verbs.
2. **Verb-mode remote ops.** `ssh-config.ts` gains a `RemoteOps` seam with two
   implementations (`shellOps`, `wrapperOps`). `applyRemoteConfig` and the
   read/diff paths route through it. The pipeline (validate -> read -> diff ->
   backup -> write -> restart -> health-wait) is unchanged; only the wire
   commands differ.
3. **Model pull.** `POST /api/hosts/:id/pull {repo}` runs a non-blocking job that
   invokes the host's `pull <repo>` verb, streaming progress over the existing
   `control_job` frame (jobType `action`, `detail.kind = "pull"`). The repo id is
   validated server-side (`^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$`) as defense in depth
   on top of the wrapper's own check.
4. **UI.** The Host config editor gains an SSH-mode selector and a "Pull model"
   field that posts a repo id and shows job progress.

## Out of scope

- Changing the wrapper scripts (already in `apps/control/remote/`).
- A new `control_job` jobType (reuse `action` to avoid a contracts change).
- Progress percentage parsing from `huggingface-cli` output (stream raw lines).

## Risks

| Risk | Mitigation |
|---|---|
| Refactor breaks existing P9.1 shell-mode tests | `shellOps` emits the identical `cat`/`cp`/`cat >`/restart command strings; existing assertions hold. `mode` defaults to `shell`. |
| Repo id injection via the pull verb | server-side regex validation + the wrapper's own regex; repo passed as a single token. |
| Long pull blocks the HTTP request | non-blocking job (fire-and-forget like bench/eval), progress over `control_job`. |
| Operator points a `wrapper`-mode host at a box without the wrapper | verbs fail loudly (the forced command / shell returns "denied"/127); reported per step, no silent fallback. |
