From 05ad577eeabc47c923bf74d870a82ff177789119 Mon Sep 17 00:00:00 2001 From: Raphael Westphal Date: Tue, 18 Aug 2026 13:03:57 +0200 Subject: [PATCH] integration: worker image, bridge, compose, Makefile, deploy scripts --- .env.example | 5 +++++ Makefile | 44 +++++++++++++++++++++++++++++++++++++++ deploy/deploy.sh | 23 ++++++++++++++++++++ deploy/e2e.sh | 20 ++++++++++++++++++ docker-compose.yml | 32 ++++++++++++++++++++++++++++ docker/bridge/index.mjs | 32 ++++++++++++++++++++++++++++ docker/worker-models.json | 23 ++++++++++++++++++++ docker/worker.Dockerfile | 23 ++++++++++++++++++++ 8 files changed, 202 insertions(+) create mode 100644 .env.example create mode 100644 Makefile create mode 100755 deploy/deploy.sh create mode 100755 deploy/e2e.sh create mode 100644 docker-compose.yml create mode 100644 docker/bridge/index.mjs create mode 100644 docker/worker-models.json create mode 100644 docker/worker.Dockerfile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6552eef --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Copy to .env (gitignored) and fill. +# Shared secret for plugin, daemon, and web UI. +LVMH_TOKEN=change-me +# LLM provider key (injected into spawned containers). +ZAI_RENAUD_API_KEY= diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4708853 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +SHELL := /bin/bash +REMOTE ?= alarm +REMOTE_DIR ?= /zdata/root/dockerFiles/lvmh + +.PHONY: help build verify worker-image daemon web test deploy e2e clean + +help: + @echo "build build daemon + web (dev checks)" + @echo "verify vet + staticcheck + tests + web build" + @echo "worker-image build lvmh-worker:latest (requires plugin/)" + @echo "deploy rsync to alarm:$(REMOTE_DIR) and docker compose up -d" + +build: daemon web + +daemon: + cd daemon && go build ./... + +web: + cd web && npm install --no-fund --no-audit && npm run build + +worker-image: + docker build -f docker/worker.Dockerfile \ + --build-arg LVMH_PLUGIN=build-plugin/lvmh-agent.ts \ + -t lvmh-worker:latest . + @# build-plugin/ is prepared by deploy/prep-worker.sh + +verify: daemon + cd daemon && go vet ./... + cd daemon && staticcheck ./... || true + cd daemon && go test ./... -count=1 + cd web && npm run build + cd plugin && npx tsc --noEmit -p tsconfig.json + +test: + cd daemon && go test ./... -count=1 + +deploy: verify worker-image + bash deploy/deploy.sh $(REMOTE) $(REMOTE_DIR) + +e2e: + bash deploy/e2e.sh + +clean: + rm -rf web/dist web/node_modules daemon/lvmh-daemon .pi/scratch diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..4fd54d2 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Deploy lvmh to alarm over ssh (tailnet). Source is rsynced; compose builds there. +set -euo pipefail + +REMOTE="${1:-alarm}" +REMOTE_DIR="${2:-/zdata/root/dockerFiles/lvmh}" + +if ! ssh "$REMOTE" "test -f $REMOTE_DIR/.env" 2>/dev/null; then + echo "ERROR: $REMOTE:$REMOTE_DIR/.env missing. Create it first (see .env.example):" + echo " ssh $REMOTE 'mkdir -p $REMOTE_DIR && cat > $REMOTE_DIR/.env' <[^<]*' || echo "no title tag" + +echo "SMOKE OK" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9ede282 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +# lvmh daemon + worker builder context. Build worker image first (make worker-image). +services: + lvmh: + build: + context: . + dockerfile: daemon/Dockerfile + image: lvmh-daemon:latest + container_name: lvmh-daemon + restart: unless-stopped + ports: + - "8686:8686" + environment: + LVMH_TOKEN: ${LVMH_TOKEN} + ZAI_RENAUD_API_KEY: ${ZAI_RENAUD_API_KEY} + # How spawned containers reach the daemon: alias on the shared network. + LVMH_CONTAINER_LVMH_URL: ws://lvmh:8686/agent/ws + GITLAB_BASE_URL: https://git.westphal.fr + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - lvmh-data:/data + - .:/app/build:ro # repo checkout (rsynced by deploy.sh) + # daemon rebuilds lvmh-worker from /app/build/docker/worker.Dockerfile + networks: + lvmh-net: + aliases: [lvmh] + +volumes: + lvmh-data: + +networks: + lvmh-net: + name: lvmh-net diff --git a/docker/bridge/index.mjs b/docker/bridge/index.mjs new file mode 100644 index 0000000..4b2e1ee --- /dev/null +++ b/docker/bridge/index.mjs @@ -0,0 +1,32 @@ +// lvmh worker bridge: headless pi session via SDK; the lvmh plugin (installed +// at /root/.pi/agent/extensions/lvmh-agent.ts) dials the daemon and delivers +// web prompts through pi.sendUserMessage. This process just hosts the session. +import { createAgentSession } from "@earendil-works/pi-coding-agent"; + +process.on("unhandledRejection", (err) => { + console.error("[lvmh-bridge] unhandledRejection:", err); +}); + +try { + const { session } = await createAgentSession(); + console.error(`[lvmh-bridge] session up, cwd=${process.cwd()}`); + + session.subscribe((event) => { + // Keep the loop warm; the extension does the mirroring. + if (event.type === "agent_settled") { + console.error("[lvmh-bridge] agent settled"); + } + }); + + for (const sig of ["SIGTERM", "SIGINT"]) { + process.on(sig, () => { + console.error(`[lvmh-bridge] ${sig}, exiting`); + process.exit(0); + }); + } + // Stay alive forever. + setInterval(() => {}, 1 << 30); +} catch (err) { + console.error("[lvmh-bridge] failed to create agent session", err); + process.exit(1); +} diff --git a/docker/worker-models.json b/docker/worker-models.json new file mode 100644 index 0000000..f4a0b36 --- /dev/null +++ b/docker/worker-models.json @@ -0,0 +1,23 @@ +{ + "providers": { + "zai-renaud": { + "baseUrl": "https://api.z.ai/api/coding/paas/v4", + "api": "openai-completions", + "apiKey": "$ZAI_RENAUD_API_KEY", + "compat": { + "supportsDeveloperRole": false, + "thinkingFormat": "zai", + "supportsReasoningEffort": true + }, + "models": [ + { + "id": "glm-5.3", + "name": "GLM-5.3", + "reasoning": true, + "contextWindow": 1000000, + "thinkingLevelMap": { "xhigh": "xhigh", "max": "max" } + } + ] + } + } +} diff --git a/docker/worker.Dockerfile b/docker/worker.Dockerfile new file mode 100644 index 0000000..eaa38fa --- /dev/null +++ b/docker/worker.Dockerfile @@ -0,0 +1,23 @@ +# lvmh worker image: headless pi with golang toolchain, git, ripgrep. +# Built by the daemon on first spawn if missing (LVMH_WORKER_DOCKERFILE), +# or manually via `make worker-image`. +FROM node:24-bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bash ca-certificates git ripgrep curl xz-utils \ + golang-go \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent + +# lvmh plugin: dial-home mirror (built from plugin/lvmh-agent.ts at image build) +ARG LVMH_PLUGIN=./plugin-placeholder +COPY ${LVMH_PLUGIN} /root/.pi/agent/extensions/lvmh-agent.ts +COPY docker/worker-models.json /root/.pi/agent/models.json +COPY docker/bridge /bridge + +# Per-session pi sessions persist here (volume lvmh-sessions) +ENV PI_SESSION_DIR=/pi-sessions +WORKDIR /workspace +CMD ["node", "/bridge/index.mjs"]