Added
-
A server's API key can come from a command instead of the config file. A
servers:entry may now setapi_key_cmd— a line whose standard output is the key, typically a lookup in the machine's secret store (security find-generic-password -s llama-launcher -a llamacpp -w). The line goes to the platform shell (sh -c,cmd /Con Windows) so a pipeline works without a wrapper script, and is resolved once at config load for enabled entries only. The command gets no stdin and neither of the launcher's standard streams — it may run under the menu, which owns the terminal — so a store that must ask the human to unlock has to prompt through its own GUI agent. It is bounded by a 60-second timeout and a 64 KiB output cap, and a command that fails, times out, overruns the cap or prints nothing fails the load naming the entry and quoting its stderr: a key source that answers with nothing is a broken source, not a keyless server. Setting bothapi_keyandapi_key_cmd, or anapi_key_cmdholding only whitespace, also fails the load. The companion markerplaintext_key_ok: truerecords that a literal key is meant to stay in the file. -
The interactive menu offers, once per launch, to move a plaintext key into the machine's secret store. Finding enabled entries that still keep
api_keyas a literal withoutplaintext_key_ok: true, the launcher raises a single offer before the menu is drawn, naming those entries and their file: move it into the store (the entry reads the key withapi_key_cmdafterwards), not now, or never for these entries. Finding none it does nothing at all — not even a store probe. Moving is one ordered move per entry: write the secret, read it back by running the exactapi_key_cmdabout to be persisted, and rewrite the entry only if what comes back matches, so a failed read-back leaves the config byte-identical and the run keeps working off the literal it has. The rewrite splices text rather than re-marshalling, so comments, blank lines and key order survive; anything it cannot do safely is refused with an edit-by-hand error, and the spliced bytes are re-parsed before an atomic 0600 write. Subcommands never prompt — they emit a one-linewarning:on stderr naming the entries, the config file this run read, and the three ways out by hand.
Changed
- Starting a managed server now checks the target port first and names whoever holds it. The start path refused to spawn where a server of the same backend was still coming up, but had nothing to say about a foreign occupant: it forked, llama-server died inside the grace window with
couldn't bind HTTP server socket, and that surfaced as a generic "server exited immediately after start". A pre-flight check now runs between the Starting probe and the binary lookup: if anything is listening on the port, the start is refused (exit code 3) with the port, the address the backend cannot bind, every listening PID with its executable name, and guidance to stop the occupant or configure a differentport. The check is one-directional — whenlsofcannot answer it reports no occupant and the start proceeds — and deliberately port-wide, since a listener on any interface fails the bind. That asymmetric case also shadows loopback for every other launcher command, so astatusthat normally answers in under 0.1 s takes 4 s; VS Code's Remote-SSH and Dev Containers port auto-forwarding is a common source, and the README's new "When the port is already taken" section documents the symptom pair.
Security
-
The
api_keyfor llamacpp reaches llama-server through the environment instead of the command line. It used to be appended as--api-key <key>, putting the credential inpsoutput for every user on the machine for as long as the server ran. It is now exported asLLAMA_API_KEY, which llama-server reads only when no--api-keyflag is present, so a user's ownextra_argsoverride still wins — and that literal override is still visible inps, as the README says. -
The launcher no longer follows redirects from configured LLM servers.
authedGetandauthedPostJSONeach built a defaulthttp.Client, which follows up to ten hops — so whatever answers on a configured port could steer an outbound request carrying the launcher'sAuthorization: Bearer <key>header at a destination of its choosing. Both now share a client whoseCheckRedirectreturnshttp.ErrUseLastResponse: the first 3xx comes back unfollowed and no second request is issued. -
The MCP adapter enables cross-origin protection on its streamable HTTP handler. The IP allowlist admits a whole machine, so a browser page on an allowlisted machine could drive the control plane from a foreign origin.
crossOriginHandlernow sits between the allowlist and the MCP handler, always on: real MCP clients send noOriginheader, or a same-origin one, and pass straight through.
Full details in CHANGELOG.md.