Files
Maven/Dockerfile
kami ee7bec11e3 Add mavmaild, the read-only IMAP poller that feeds mail intake (#246)
The extraction seam landed on the previous branch but nothing fed it. This
adds the daemon that does: every interval it opens one mailbox read-only
(EXAMINE + BODY.PEEK, so reading leaves no \Seen behind), fetches the UIDs
it has not handed over yet, and posts each message to core over
ingest_mail. Core runs the model and writes task candidates; this daemon
writes nothing and cannot create a reminder.

It is a separate daemon because of the credential. mavpoll set the
precedent with the zenmoney token (#125): the module talking to the third
party holds the secret, reads it from a file so it never lands in argv, in
docker-compose.yml or in shell history, and core never sees it. There is
deliberately no -password flag, and a test asserts that.

Off unless configured at both ends: without -password-file the daemon
refuses to start, and if core has no email block the first ingest returns
ErrUnknownMethod, which disables the reader instead of hammering a socket
that will keep refusing. A seen-UID state file (0600, atomic write) keeps a
restart from re-extracting the whole lookback window; correctness does not
depend on it, since capture dedupes on normalised text. Logs are counts and
UIDs — no subject, sender or body.

Verified with an in-process IMAP server and a fake core: bulk mail is
filtered before core is asked, seen UIDs are not re-fetched, a failed
ingest is retried next poll, ErrUnknownMethod stops at the first message,
and state survives a restart. The live half is untested by design — no IMAP
credential exists on this box; setup is written up as QA steps.

Vikunja #246
2026-08-01 03:13:18 +04:00

118 lines
5.9 KiB
Docker

# syntax=docker/dockerfile:1
#
# Single image, all six daemons. docker-compose runs one container per daemon
# off this image with a different command — the native-lib + toolchain surface
# is shared, so separate images would duplicate ~all of it. Isolation still
# holds: each daemon is its own container/namespace, only mavend mounts the key
# and the db volume.
#
# Native deps are the prebuilt artifacts the repo already carries under deps/
# (libwhisper+ggml-vulkan, onnxruntime, piper/espeak). We do NOT build
# whisper.cpp from source here — COPY the prebuilt .so and headers.
# ponytail: prebuilt-lib copy, not a from-source build. Add a whisper.cpp build
# stage if you ever need reproducibility / a different arch than the host libs.
# trixie, not bookworm: the prebuilt deps/lib/*.so are linked against
# glibc 2.38 + GLIBCXX 3.4.32 (built on the Arch host). bookworm's glibc 2.36
# is too old and the link fails on missing symbols. trixie ships glibc 2.40.
# 1.25 to match go.mod's `go 1.25.5` directive: the official golang images pin
# GOTOOLCHAIN=local, so a 1.24 builder refuses the module instead of fetching a
# newer toolchain. Bump this in lockstep with the go.mod directive.
FROM golang:1.25-trixie AS build
WORKDIR /src
# libvulkan-dev: libggml-vulkan.so needs libvulkan.so.1 at link time.
RUN apt-get update && apt-get install -y --no-install-recommends libvulkan-dev \
&& rm -rf /var/lib/apt/lists/*
# native build inputs (prebuilt libs + headers), then module cache, then source
COPY deps/lib/ /src/deps/lib/
COPY deps/piper/ /src/deps/piper/
COPY deps/include/ /src/deps/include/
COPY deps/whisper.cpp/ggml/include/ /src/deps/whisper.cpp/ggml/include/
COPY go.mod go.sum ./
# vendored deps: the ecosystem clients (hexis) are replace-directed at sibling
# repos outside the build context, so we can't `go mod download` them here.
# `go mod vendor` (run on a host where the siblings exist) bakes them into
# vendor/; with it present, go build uses it and never touches the replace
# paths. Regenerate with `go mod vendor` whenever a sibling client API changes.
COPY vendor/ ./vendor/
COPY cmd/ ./cmd/
# force rebuild 2026-07-10T16:50
COPY internal/ ./internal/
# CGO wiring mirrors the Makefile; rpath points at the RUNTIME lib location so
# the binaries find their .so at /opt/maven/lib regardless of LD_LIBRARY_PATH.
ENV CGO_ENABLED=1 \
CGO_CFLAGS="-I/src/deps/include -I/src/deps/whisper.cpp/ggml/include" \
CGO_LDFLAGS="-L/src/deps/lib -L/src/deps/piper -Wl,-rpath,/opt/maven/lib"
RUN go build -o /out/mavend ./cmd/mavend && \
go build -o /out/mavsttd ./cmd/mavsttd && \
go build -o /out/mavttsd ./cmd/mavttsd && \
go build -o /out/mavweb ./cmd/mavweb && \
go build -o /out/mavpoll ./cmd/mavpoll && \
go build -o /out/mavcaldav ./cmd/mavcaldav && \
go build -o /out/mavmaild ./cmd/mavmaild
# llama.cpp Vulkan build — the phraser/router LFM engine (llama-server). Built
# from source (not a prebuilt vendored blob) so the binary's glibc/GLIBCXX match
# the trixie runtime and GPU offload rides mesa's RADV Vulkan driver — the same
# path whisper already uses on homesrv's AMD iGPU (RADV RENOIR). Pinned to b9601 (parity with
# the host's known-good build). Static (BUILD_SHARED_LIBS=OFF) ⇒ one self-
# contained binary, no libggml/libllama .so to juggle in the runtime; only
# libvulkan.so.1 + libgomp (both already in the runtime) are needed at load.
FROM debian:trixie-slim AS llama
ARG LLAMA_REF=b9601
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git cmake build-essential libvulkan-dev \
glslc glslang-tools spirv-headers spirv-tools \
&& rm -rf /var/lib/apt/lists/*
RUN git clone --depth 1 --branch ${LLAMA_REF} \
https://github.com/ggml-org/llama.cpp /src/llama.cpp
WORKDIR /src/llama.cpp
RUN cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_VULKAN=ON \
-DBUILD_SHARED_LIBS=OFF \
-DLLAMA_CURL=OFF \
-DLLAMA_BUILD_SERVER=ON \
&& cmake --build build --config Release -j"$(nproc)" --target llama-server
FROM debian:trixie-slim AS runtime
# tzdata so the TZ env (set in compose) resolves — otherwise Go can't load the
# zone and time.Now() stays UTC, and mavend answers clock/date queries and
# evaluates quiet-hours in UTC.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libvulkan1 mesa-vulkan-drivers libgomp1 tzdata \
python3 python3-pip && \
pip3 install --no-cache-dir --break-system-packages 'dateparser==1.4.1' && \
apt-get purge -y --auto-remove python3-pip && \
rm -rf /var/lib/apt/lists/*
# runtime native libs: whisper/ggml (incl. vulkan) are real files in deps/lib.
COPY deps/lib/ /opt/maven/lib/
COPY deps/piper/ /opt/maven/piper/
# The router embedder needs onnxruntime 1.26, but deps/lib only carries dangling
# symlinks to it (they point at an absolute HOST path that isn't in the image).
# Ship the real 1.26 lib and repoint the symlinks. Piper keeps its OWN
# onnxruntime 1.14 under /opt/maven/piper (found via piper's $ORIGIN runpath +
# its exact soname libonnxruntime.so.1.14.1) — do NOT copy piper's .so into
# /opt/maven/lib, that clobbers 1.26 and mavend dies with "ORT API base: 2".
COPY deps/onnxruntime-linux-x64-1.26.0/lib/libonnxruntime.so.1.26.0 /opt/maven/lib/
RUN cd /opt/maven/lib \
&& ln -sf libonnxruntime.so.1.26.0 libonnxruntime.so \
&& ln -sf libonnxruntime.so.1.26.0 libonnxruntime.so.1
COPY --from=build /out/ /opt/maven/bin/
# the LFM engine: static Vulkan llama-server on PATH; the phraser spawns it by
# name (bin_path "llama-server"). GPU offload needs /dev/dri passed to mavend.
COPY --from=llama /src/llama.cpp/build/bin/llama-server /opt/maven/bin/
ENV LD_LIBRARY_PATH=/opt/maven/lib PATH=/opt/maven/bin:$PATH
# unprivileged; core owns the key + db, modules own nothing.
RUN useradd -r -u 10001 -m maven \
&& mkdir -p /run/maven /var/lib/maven \
&& chown maven:maven /run/maven /var/lib/maven
USER maven
WORKDIR /opt/maven