Cerberus is Carino Systems’ self-hosted LLM runtime: one small Go service that does three jobs.
llama-server), LM Studio
(lms) or Ollama, unloads them when they go idle, and makes room in VRAM before a load.
Every request is recorded with its time to first token, tokens per second and energy use./v1) that routes each request to a
local model or to a cloud provider that you reach with your own API key.Cerberus is the service half of a service plus desktop pair. Today the desktop half is Atrium’s LLM chat, which uses Cerberus as an ordinary OpenAI-compatible server (see docs/DESKTOP.md).
Status: Phase 1, Linux first. The code is pure Go (CGO_ENABLED=0) and also builds for macOS
and Windows. Agent sessions need a PTY, which is not available on Windows yet, and
install-service is Linux only.
Requirements: Go 1.26+ to build; git and bash for sessions. Optional: llama-server, LM Studio
(lms), Ollama, nvidia-smi (NVIDIA) or the amdgpu driver (AMD) for GPU readings.
make build # dist/cerberus, static, version from git describe
./dist/cerberus serve # first run writes ~/.config/cerberus/config.json with a fresh token
./dist/cerberus url # UI links with ?token= for every listen address (pair a phone)
./dist/cerberus install-service # copy to ~/.local/bin and start the systemd user unit
The daemon listens on 127.0.0.1:7340. Open http://127.0.0.1:7340/, or the link from
cerberus url, which signs the browser in and drops the token from the address bar.
| Command | What it does |
|---|---|
cerberus serve [-config PATH] [-debug] |
Run the daemon: GPU monitor, model manager, gateway, sessions, web UI. SIGTERM shuts down in order: HTTP, sessions, spawned model servers. |
cerberus status |
One-screen summary of the running daemon (GPU, models, sessions, guard). |
cerberus task [--repo DIR] [--agent A] [--model M] [--escalate P] [--count N] [--title T] "task" |
Start a session. --repo defaults to the git top level of the current folder. Prints each session’s UI link. |
cerberus url |
Prints PublicURL, then each listen address with ?token=. A wildcard bind expands to loopback plus every LAN and Tailscale address. |
cerberus install-service |
Installs ~/.config/systemd/user/cerberus.service, then daemon-reload and enable --now (restarting if the binary changed). |
cerberus agent … |
The built-in headless coding agent. Sessions start it; you rarely run it by hand. |
cerberus version |
Version, OS/arch and Go version. |
~/.config/cerberus/config.json (mode 0600), created with defaults on first run. The full list of
fields and defaults is in internal/config/config.go; the behaviour each
one controls is specified in docs/SCOPE.md. The fields most people change:
{
"server": { "listen": "127.0.0.1:7340", "extra_listen": ["100.64.0.10:7340"], "public_url": "" },
"engines": { "default_model": "qwen3-coder-30b", "idle_ttl_sec": 600,
"llamacpp": { "model_dirs": ["/home/me/models"] } },
"providers": [ { "name": "openrouter", "base_url": "https://openrouter.ai/api/v1",
"key_env": "OPENROUTER_API_KEY", "enabled": true } ],
"repo_roots": ["/home/me/Github"],
"repos": [ { "path": "/home/me/Github/site", "checks": ["make test"], "preview": { "dir": "." } } ],
"guard": { "max_sessions": 3, "min_free_mib": 4096 },
"git": { "name": "Your Name", "email": "you@example.com" }
}
<provider>/<model>. default
resolves to engines.default_model. A unique prefix or suffix of an ID also works.key_env, or from secrets.json next to the config (mode 0600,
{"openrouter": "sk-…"}).repos entry can declare its own gate in .cerberus.json:
{"checks": ["go test ./..."], "preview": {"dir": "dist"}}.Data lives in ~/.local/share/cerberus/ (sessions/, worktrees/, usage.jsonl, logs/).
A session is one agent run in one worktree:
queued → starting → running ⇄ waiting → checking → passed | failed, then accepted, discarded,
stopped or error.
cerberus (built-in agent through the gateway, any local or API model), claude
and claude-headless, opencode, aider, and shell. Profiles are plain argv templates; add your
own under agents. A profile whose binary is missing is shown as unavailable.agent.approve).
The session turns waiting, a notification goes out, and you answer from the UI with y / a / n or a
reason. Paths matching agent.deny_paths are always refused, and writes outside the worktree are
refused.max_attempts the task can escalate to another profile.main and fast-forwards main in your checkout, then optionally pushes. Cerberus never
creates a branch; worktrees use a detached HEAD.agents.slice with a memory cap.127.0.0.1. Extra addresses (for example a Tailscale IP) are
opt-in through extra_listen. There is no TLS of its own; reach it over loopback, Tailscale or a
reverse proxy you trust./healthz, /login and /assets/*. It is
accepted as Authorization: Bearer, or as the cerberus_token cookie (HttpOnly, SameSite=Lax).
?token= on a GET sets the cookie and redirects without it. Comparisons are constant-time.X-Cerberus: 1; the web UI
sends it, and a cross-site page cannot without a CORS preflight, which Cerberus never grants.server.local_no_auth (default on), /v1/* from a loopback peer needs
no token, so desktop clients work without a key. Only the socket’s peer address counts;
X-Forwarded-For is ignored. A request that looks like it came from a web page in the local
browser (an Origin header, a Host that isn’t loopback or ours, a write without a JSON
Content-Type) still needs the token. /api/* always needs the token./v1/* only, and
the built-in agent’s commands get neither it nor the CERBERUS_* variables.Authorization header is never forwarded upstream; providers get
only their configured key. Consumer subscriptions are never proxied: they are used only by running
their official CLI as a session.os.OpenRoot (no symlink escape) and never
serve dot-files such as .git or .env. They are sandboxed (Content-Security-Policy: sandbox)
into an opaque origin, so an agent-written page cannot call /api with your cookie; their own
files load through a per-session capability URL.Atrium’s LLM chat works with Cerberus unchanged:
~/.config/hypr/scripts/llm_models.py set server_url http://127.0.0.1:7340/v1
No key is needed from the same machine. Notifications from sessions go through Atrium’s
~/.config/hypr/scripts/notify when it exists. Details and the desktop roadmap are in
docs/DESKTOP.md.
The control plane stays out of the models’ way: no polling while idle (the GPU monitor slows to one query every 15 s and stops fast polling when nothing is loaded; the session scheduler and model reaper block until there is work), and the web UI closes its streams while the tab is hidden. Measured on Linux with an RTX 4090 and no model loaded: 13 MB RSS, about 0.1% CPU over a 10 s idle window.
make test # go test -race ./... (the race detector needs cgo; the binary does not)
make vet # go vet for linux, darwin and windows
make test-web # node --test for the UI helpers, node --check on every UI module
make cross # static binaries for linux, darwin and windows, amd64 and arm64
Dependencies are the Go standard library and github.com/creack/pty. The web UI is vanilla ES
modules with no build step; xterm.js and the fonts are vendored under web/dist/assets/, never
loaded from a CDN.
| Phase | Contents |
|---|---|
| 1 (current) | Model hosting and gateway; agent sessions with worktrees, test gates, previews, resource guard and queue, escalation and remote approval; web UI; CLI; systemd user unit |
| 2 | Usage and limit tracking per subscription, searchable session history and resume, voice tasks (local Whisper), secrets vault, fleet-wide tasks |
| 3 | GPU power-cap auto-tuner (tokens per joule), speculative-decoding and quantisation presets, vLLM backend, multiple GPUs and hosts |
| 4 (client edition) | Plain chat for non-developers (Spanish first), RAG over documents, users and roles, budgets and audit log, fully offline, remote support |
| 5 (desktop) | Desktop client grown from Atrium LLM chat, island agent status, launcher “new task”, bar readout of GPU power and tok/s; macOS and Windows builds |