implementation: complete
Assistant Contracts (BYOK)
The assistant answers questions about servers using the operator's own LLM key. It exists in two places with deliberately different powers: beside a terminal, where it talks about one machine, and on the dashboard, where it talks about the estate.
Everything below follows from one rule: the model proposes, the operator disposes. Nothing the model says executes itself, except through a setting the operator turned on knowingly.
Providers are configuration, not code
Two wire protocols cover the field:
| Protocol | Endpoint shape | Auth |
|---|---|---|
anthropic | POST {base}/v1/messages | x-api-key + anthropic-version |
openai | POST {base}/chat/completions | Authorization: Bearer |
A vendor is a base URL and a model name, so the dashboard ships presets rather than the server shipping adapters: Anthropic and Z.AI (whose compatible endpoint speaks the Anthropic shape) use the first; OpenAI, OpenRouter, vLLM and ollama use the second. Adding a vendor that speaks either shape needs no code.
The key
Stored in the operator's own SQLite file, next to the API token, on a tool that binds loopback only. Three rules keep it there:
GET /api/v1/assistant/configreturns a view — every setting except the key, plushas_api_keyso the form can say "저장됨".- A
PUTthat omits the key keeps the stored one. The dashboard never receives the key, so it cannot echo it back, and saving an unrelated setting cannot wipe the secret. - The audit row for a settings change records endpoint, model and flags — never the key.
Host assistant
Beside the terminal. Context is assembled from what the operator allows, each with its own switch:
- host facts and the latest metrics (
send_metrics) - the terminal screen, last 2000 characters, read from xterm in the
browser (
send_terminal) - recent commands from
exec_log(send_exec_history)
The reply may carry one command, marked by a CMD: line the server
strips out. The dashboard types it onto the shell prompt without a
newline — the operator's Enter is what runs it.
auto_run (off by default, migration 0009) removes that keystroke. The
server decides per reply: auto-run applies only when the setting is on
AND a command was actually suggested, so the dashboard cannot run
anything on its own initiative, and the run happens once when the answer
arrives rather than from a view closure that re-renders.
Fleet assistant
On the dashboard, collapsed until asked for. Context is a snapshot: fleet counts, every host's state with its tags and flags, and what is firing. Down hosts sort first, so truncation never hides exactly what the question is about.
A fleet answer may carry a plan — a CMD: line plus a HOSTS: line
naming targets. The plan is rendered, never executed:
- Asking and running are separate endpoints (
POST /assistant/fleetandPOST /assistant/fleet/run). The run request restates the hosts, so what executes is what the dashboard showed, not whatever the model said last. auto_rundeliberately does not reach here. "Run this after I read it" and "run this on twenty servers" are different decisions, and the second always asks.- Targets are checked against the catalog. Invented aliases come back in
unknown_hosts— a misunderstanding is shown rather than quietly shrinking the run — and the run endpoint refuses unknown, disabled or stale hosts outright rather than skipping them. - Fan-out is capped: 50 hosts per run, 8 at a time, a per-host timeout.
Every host's run lands in
exec_logthrough the same path a single exec uses, so a batch is as auditable as a keystroke.
Failure modes
Errors map to the status a caller can act on: 412 when the assistant is
off or has no key (fixable in settings), 404 for an unknown host, 502
when the LLM endpoint itself failed, 400 for an empty or oversized
question. A timeout is 60 seconds — long enough for a real answer, short
enough not to hold a tab open forever.
Surfaces
GET/PUT /api/v1/assistant/configPOST /api/v1/hosts/{alias}/assistantPOST /api/v1/assistant/fleet,POST /api/v1/assistant/fleet/run- Dashboard: the fleet panel; host detail: the strip under the terminal;
/settings: the single home for provider, key and context switches. - MCP: none. An agent already has the tools the assistant would call, and proxying one model through another buys nothing.