# 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 `). 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 ` 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. |