Files
boocode/openspec/changes/boocontrol-ssh-verbmode/proposal.md
indifferentketchup b18de2a331 chore: snapshot working tree - pty_exited notifications + in-flight inference WIP
feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean).

wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes.

openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
2026-06-14 12:48:47 +00:00

2.9 KiB

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.