lvmh daemon
Go daemon binding the lvmh wire protocol (../PROTOCOL.md): agent websocket
(/agent/ws), REST API (/api/*), web websocket (/ws), GitLab PAT
management and worker-container spawning over the Docker API.
Layout
| file | role |
|---|---|
main.go |
flags, env wiring, HTTP server lifecycle |
hub.go |
agent WS hub (/agent/ws) + web WS hub (/ws), prompt/abort routing, online state |
api.go |
REST handlers, bearer auth, recover middleware, embedded web UI |
gitlab.go |
PAT connect/validate/list member projects |
docker.go |
spawn pipeline: clone → ensure image → create/start container, job status |
store.go |
SQLite (modernc, CGO-free): sessions, events, settings, containers |
webdist/ |
embedded web UI placeholder (replace with web/dist at build time) |
Build & run
cd daemon
go build ./...
LVMH_TOKEN=secret LVMH_DB=/tmp/lvmh.db ./lvmh-daemon --addr :8686
Flags: --addr (default :8686), --db (default $LVMH_DB or /data/lvmh.db),
--webdist (default /app/web-dist when that directory exists, else the
embedded webdist/).
Docker image (multi-stage, CGO_ENABLED=0):
docker build -f daemon/Dockerfile -t lvmh-daemon:latest .
To bake the real web UI into the binary, copy the built frontend over the
placeholder before building: cp -r web/dist daemon/webdist/. Otherwise mount
it at /app/web-dist (the default --webdist path) or pass --webdist.
Environment
| var | default | meaning |
|---|---|---|
LVMH_TOKEN |
— (required) | bearer token for REST + WS auth |
LVMH_DB |
/data/lvmh.db |
sqlite path (WAL mode) |
GITLAB_BASE_URL |
https://git.westphal.fr |
GitLab instance |
LVMH_WORKER_DOCKERFILE |
/app/build/docker/worker.Dockerfile |
image build recipe |
LVMH_WORKER_CONTEXT |
parent of dockerfile dir | docker build context |
LVMH_REPO_DIR |
/data/repos |
host-side clone cache (<slug> subdirs) |
LVMH_CONTAINER_LVMH_URL |
ws://lvmh:8686/agent/ws |
daemon URL handed to spawned containers |
LVMH_CONTAINER_NETWORK |
lvmh-net |
network for spawned containers |
LVMH_WORKER_MODELS |
/app/build/docker/worker-models.json |
mounted read-only as models.json when present |
ZAI_RENAUD_API_KEY |
— | injected into spawned containers |
DOCKER_HOST |
— | honored by the docker client |
Spawn pipeline
POST /api/spawn {repo, branch?} returns {sessionId, containerId:""}
immediately (async). The job walks cloning → building → creating → running
(or error + message) in GET /api/spawn/status and on the web WS.
- Per-repo-slug mutex;
git clone/git pull --ff-onlyinto$LVMH_REPO_DIR/<slug>. - Ensure image
lvmh-worker:latest— built fromLVMH_WORKER_DOCKERFILEwhen missing (502-style clear error fromPOST /api/spawnif neither image nor Dockerfile exist). - Named volume
lvmh-repo-<slug>(seeded from the clone on first use),lvmh-sessionsvolume,lvmh.session=<sessionId>label, networklvmh-net, envZAI_RENAUD_API_KEY,LVMH_TOKEN,LVMH_URL,LVMH_SESSION_ID. DELETE /api/sessions/:id/containerstops and removes the container.
Tests
go vet ./... && staticcheck ./... && go test ./... -count=1
Covered: store persistence + replay-after-seq, agent handshake
(hello→welcome.lastSeq), reconnect replacement, live message_update fan-out
(no persistence), prompt routing + 409 offline, bearer 401s, GitLab user/
projects mapping against an httptest upstream.
Protocol ambiguities (daemon-side resolutions)
- Envelope payload shape: payload fields are flattened into the envelope
object (as the
helloexample shows). The daemon persists the full frame verbatim;GET /api/sessions/:id/eventsreplays it as-is. eventsframeafter: exclusive lower bound =seqof the first event in the batch minus 1, matching the REST?after=semantics.POST /api/spawnresponse: returns immediately with{sessionId, containerId: ""}—containerIdappears later via/api/spawn/status(async spawn per spec; the protocol table's synchronous shape is not achievable for clones that take minutes).- Events persistence: everything except
message_update; both live and replayedmessage_updatecontent is reconstructed frommessage_end. - Repo workspace: clone cache on the daemon filesystem under
LVMH_REPO_DIR; the container mounts named volumelvmh-repo-<slug>seeded from that clone on first spawn (protocol: "clone into volume").