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).
3.4 KiB
3.4 KiB
Design — BooControl SSH editor verb-mode + model pull
Files touched
apps/control/src/services/ssh-config.ts— add theRemoteOpsseam +shellOps/wrapperOps; threadmodethroughreadRemoteConfig/applyRemoteConfig.apps/control/src/services/model-pull.ts(new) — non-blocking pull job runner.apps/control/src/routes/ssh-config.ts— acceptsshModein PATCH; pass mode to read/diff/apply; addPOST /api/hosts/:id/pull.apps/control/src/schema.sql—ALTER TABLE control_hosts ADD COLUMN IF NOT EXISTS ssh_mode TEXT NOT NULL DEFAULT 'shell'.apps/web/src/components/control/HostConfigEditor.tsx— SSH-mode selector + Pull-model field.apps/control/src/services/__tests__/ssh-config.test.ts— add wrapper-mode mapping tests (keep existing shell-mode tests).apps/control/src/services/__tests__/model-pull.test.ts(new) — repo-id validation + verb emission.
RemoteOps seam
interface RemoteOps {
read(): Promise<string>; // throws on failure
backup(now: Date): Promise<string>; // returns backup path
write(content: string): Promise<void>; // throws on failure
restart(restartCmd: string): Promise<void>;
}
// shell: today's behavior — emits `cat 'p'`, `cp 'p' 'p.bak-ts'`, `cat > 'p'`, restartCmd.
function shellOps(target, configPath, exec): RemoteOps
// wrapper: emits the verbs `read` / `backup` / `write`(stdin) / `restart`.
function wrapperOps(target, exec): RemoteOps
applyRemoteConfig selects ops from opts.mode (default 'shell'). Shell backup
computes the name via backupFilename then cp; wrapper backup sends the
backup verb and reads the returned path from stdout (the wrapper stamps it).
Everything else (validate, diff via computeDiff, health-wait) is unchanged, so
the existing shell-mode tests pass byte-for-byte.
Pull job
runModelPull({ target, repo, mode }, exec, emitter):
- Validate
repoagainst^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$; reject early. exec(target, 'pull ' + repo)(wrapper) orexec(target, 'huggingface-cli download ' + repo + ' --local-dir <modelsDir>/...')(shell). Wrapper mode is the supported path; shell mode requires amodels_dirand is best-effort.- Publish
control_jobframes:runningat start,completed/failedat end,detail.kind = 'pull',detail.repo, and tail output indetail.line.
Reuses jobType action from the existing ControlJobFrame (no contracts change).
Backward compatibility
ssh_modedefaults toshell-> existing hosts behave exactly as P9.1.applyRemoteConfigmodedefaults toshell-> existing call sites + tests unchanged.- No
control_jobschema change; the webuseControlStreamalready acceptsjobType: 'action'.
Validation lenses folded in
- V1 (adversarial): wrapper
backupmust return the path the wrapper chose, not a client-computed one (clock skew between control host and GPU host) -> wrapperbackupreads stdout. - V2 (adversarial): a
wrapper-mode host without the script must fail loudly -> verbs surface the non-zero exit + stderr per pipeline step; no shell fallback. - JD1 (junior): server-side repo validation duplicates the wrapper's -> intentional defense in depth; documented.
- JD2 (junior): reusing jobType
actionkeeps the change additive; a dedicatedpulltype is deferred (would touch contracts + web union) with reopen trigger "if pull needs distinct UI filtering."