# 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 ```sh 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`): ```sh 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 (`` 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. 1. Per-repo-slug mutex; `git clone`/`git pull --ff-only` into `$LVMH_REPO_DIR/`. 2. Ensure image `lvmh-worker:latest` — built from `LVMH_WORKER_DOCKERFILE` when missing (502-style clear error from `POST /api/spawn` if neither image nor Dockerfile exist). 3. Named volume `lvmh-repo-` (seeded from the clone on first use), `lvmh-sessions` volume, `lvmh.session=` label, network `lvmh-net`, env `ZAI_RENAUD_API_KEY`, `LVMH_TOKEN`, `LVMH_URL`, `LVMH_SESSION_ID`. 4. `DELETE /api/sessions/:id/container` stops and removes the container. ## Tests ```sh 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 `hello` example shows). The daemon persists the full frame verbatim; `GET /api/sessions/:id/events` replays it as-is. - **`events` frame `after`**: exclusive lower bound = `seq` of the first event in the batch minus 1, matching the REST `?after=` semantics. - **`POST /api/spawn` response**: returns immediately with `{sessionId, containerId: ""}` — `containerId` appears 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 replayed `message_update` content is reconstructed from `message_end`. - **Repo workspace**: clone cache on the daemon filesystem under `LVMH_REPO_DIR`; the container mounts named volume `lvmh-repo-` seeded from that clone on first spawn (protocol: "clone into volume").