c5264deb46
mavwaked and mavenclient have been written, tested and deployed nowhere since V-463 parked them. homesrv has a microphone because it is a laptop, but it is in the wrong room. workpc is where he sits, and it has a fifine on card 0. V-515 said this was a config line: "ipc.Dial already speaks tcp://host:port?token=... so this is config, not protocol work". That premise is wrong and it is worth writing down. Both mavwaked and mavenclient speak internal/voice through voice.Dial, not internal/ipc. The netaddr token guards the daemon-to-daemon IPC seam and never touches the voice wire. That wire is plaintext with no auth at all, and voice/server.go says so: production binds inside the wg tunnel, because "the wg layer IS the L0 floor". workpc is not a wg peer. It sits on wlan0. So the floor here is ssh: mavend publishes the voice port to homesrv loopback only (127.0.0.1:9110, since host 9100 is Vikunja's MCP), and a user unit on workpc forwards it over his key. Nothing new is on the LAN. That mattered more than it looks: SurfaceVoice caps acts at L0, so an unauthorized speaker could not run a destructive tool, but L0 does not cap reading. A LAN bind would let anyone on the wifi hear his facts, his notes and his calendar read back. Two things the deployment found that no test could: The vendored onnxruntime under deps/ has two copies and the stale one is 1.17.1. The Go binding asks for API 26, so silero refused to load until 1.26.0 was shipped instead. mavwaked logged it and kept running on the energy threshold, which is the designed fallback working. The fifine offers 2 channels at 44100 or 48000 and nothing else. mavwaked asks arecord for 16kHz mono, so hw:0,0 dies on "Channels count non available" before a frame is read. The unit uses plughw:0,0 so ALSA downmixes and resamples. Verified end to end through mavwaked's own -test mode, so no human had to speak: a 2.43s Russian fixture reached mavend over the tunnel, was transcribed on the workstation by CW2, routed intent=query, claimed by the calendar source, and came back as 3.68s of piper audio. There is still no wake word (V-487 stage two), so the loop runs open. Silero is passed on purpose, since it declines white noise the energy floor accepts. Barge-in is not, because its threshold is room-specific and this room has no number yet.
222 lines
10 KiB
YAML
222 lines
10 KiB
YAML
name: maven
|
|
|
|
# One image (built once), one container per daemon. Only mavend holds the key
|
|
# and the db volume; the modules mount just the shared socket dir + models.
|
|
# IPC stays unix-domain over the shared `sockets` volume — no code change from
|
|
# the bare-metal setup, only paths move to /run/maven.
|
|
|
|
x-image: &image
|
|
# NOT "maven" — that's Apache Maven on Docker Hub and compose will happily
|
|
# pull it, giving every container `mvn-entrypoint.sh` and exit 127.
|
|
image: mavenai:latest
|
|
# build on EVERY service (same image name ⇒ built once) so `docker compose
|
|
# build <anyservice>` actually rebuilds. With build on only one service, the
|
|
# others silently no-op and you deploy a stale binary.
|
|
build:
|
|
context: .
|
|
# the zone is declared once, here. The image points /etc/localtime at it so
|
|
# a caller reading the system zone agrees with one reading TZ (V-545).
|
|
args:
|
|
TZ: Europe/Samara
|
|
pull_policy: never # only ever the locally-built image
|
|
restart: unless-stopped
|
|
# local time for clock/date replies AND quiet-hours evaluation. Change to
|
|
# your zone; needs tzdata in the image (installed in the Dockerfile).
|
|
environment:
|
|
- TZ=Europe/Samara
|
|
|
|
services:
|
|
mavend:
|
|
<<: *image
|
|
command: ["mavend", "-config", "/opt/maven/config/mavend.json"]
|
|
# both nets: `default` keeps the `mavend` DNS name the other daemons reach
|
|
# (mavweb -> mavend:9100); `ecosystem` reaches nexus/praxis/hexis by name.
|
|
networks: [default, ecosystem]
|
|
# the key lives ONLY here. deploy/db_key.env holds MAVEN_DB_KEY=<base64-32B>.
|
|
env_file:
|
|
- ./deploy/db_key.env
|
|
- ./deploy/telegram.env
|
|
volumes:
|
|
- dbdata:/var/lib/maven # encrypted db at rest
|
|
- sockets:/run/maven # IPC socket dir
|
|
- ./deploy/mavend.json:/opt/maven/config/mavend.json:ro
|
|
- ./models:/opt/maven/models:ro
|
|
- /mnt/hdd1/llms:/opt/maven/models/llm:ro # LFM gguf library
|
|
# the LFM engine (llama-server) offloads onto the AMD iGPU (RADV RENOIR,
|
|
# Ryzen 5 5600U) via Vulkan — same device + render gid as mavsttd, which
|
|
# uses the same driver for whisper. Without these Vulkan
|
|
# enumerates zero devices and llama-server silently falls back to CPU.
|
|
devices:
|
|
- "/dev/dri:/dev/dri"
|
|
group_add:
|
|
- "993" # host 'render' gid owning /dev/dri/renderD128 (getent group render)
|
|
# the decrypted working copy lives in RAM (see db_tmpfs in mavend.json).
|
|
tmpfs:
|
|
- /dev/shm
|
|
# the voice wire, for mavwaked and mavenclient on workpc (V-515).
|
|
#
|
|
# LOOPBACK ONLY, and that is the whole security argument. internal/voice
|
|
# is plaintext with no auth: its own server doc says production binds
|
|
# inside the wg tunnel, "the wg layer IS the L0 floor". workpc is not a wg
|
|
# peer, it is on wlan0. So the tunnel is ssh instead, terminated on this
|
|
# loopback address, and nothing new is on the LAN. Anyone who could reach
|
|
# a LAN-bound port here could push audio and hear his facts read back.
|
|
# SurfaceVoice caps acts at L0; it does not cap reading.
|
|
#
|
|
# Host 9100 is Vikunja's MCP, hence 9110. The container side stays 9100
|
|
# so mavweb keeps reaching mavend:9100 by name.
|
|
ports: ["127.0.0.1:9110:9100"]
|
|
|
|
mavsttd:
|
|
<<: *image
|
|
command: ["mavsttd", "-socket", "/run/maven/stt.sock", "-model", "/opt/maven/models/stt/ggml-small.bin"]
|
|
depends_on: [mavend]
|
|
# whisper uses libggml-vulkan (RADV on the host's AMD GPU) → needs the
|
|
# render node AND membership in the group that owns it, or Vulkan enumerates
|
|
# zero devices and falls back to CPU.
|
|
devices:
|
|
- "/dev/dri:/dev/dri"
|
|
group_add:
|
|
- "993" # host 'render' gid owning /dev/dri/renderD128 (getent group render)
|
|
volumes:
|
|
- sockets:/run/maven
|
|
- ./models:/opt/maven/models:ro
|
|
|
|
mavttsd:
|
|
<<: *image
|
|
command: ["mavttsd", "-socket", "/run/maven/tts.sock",
|
|
"-piper", "/opt/maven/piper/piper",
|
|
"-model", "/opt/maven/models/tts/ru_RU-irina-medium.onnx",
|
|
"-espeak_data", "/opt/maven/piper/espeak-ng-data"]
|
|
depends_on: [mavend]
|
|
volumes:
|
|
- sockets:/run/maven
|
|
- ./models:/opt/maven/models:ro
|
|
|
|
mavweb:
|
|
<<: *image
|
|
# voice.bind is 0.0.0.0:9100 in deploy/mavend.json so mavweb can reach it
|
|
# cross-container. Verified 2026-07-06.
|
|
# -ambient-token turns on POST /api/ambient (Vikunja #126): the phone posts
|
|
# notification text, mavweb keeps only a meeting time. Empty ⇒ no route at
|
|
# all, which is what a missing MAVEN_AMBIENT_TOKEN gives. The value comes
|
|
# from the gitignored .env docker compose reads for interpolation, NOT from
|
|
# an env_file — flags are interpolated before any service env exists.
|
|
# Weakness worth naming: mavweb takes this as a flag, so it is visible in
|
|
# `ps` inside this container, unlike the zenmoney and IMAP secrets which are
|
|
# read from files.
|
|
command: ["mavweb", "-addr", ":9201", "-voice", "mavend:9100", "-core", "/run/maven/mavend.sock",
|
|
"-ambient-token", "${MAVEN_AMBIENT_TOKEN:-}",
|
|
"-nexus", "http://nexus:9740", "-praxis", "http://praxis:8989", "-hexis", "http://hexis:9741"]
|
|
depends_on: [mavend]
|
|
# loopback-only on purpose: /tools defines+executes arbitrary argv and
|
|
# mavweb has no in-process auth of its own without -webauthn-origin/-rpid.
|
|
# Reaching the UI therefore requires the wg tunnel (or the local nginx) by
|
|
# construction, not by convention. The other daemons talk to mavweb over
|
|
# the compose network, not this published port.
|
|
ports: ["127.0.0.1:9201:9201"]
|
|
# ecosystem: reach the siblings by name for the read-only /ecosystem panel;
|
|
# default: keep resolving mavend:9100 for voice + the IPC socket peers.
|
|
networks: [default, ecosystem]
|
|
volumes:
|
|
- sockets:/run/maven
|
|
|
|
mavpoll:
|
|
<<: *image
|
|
network_mode: host
|
|
command: ["mavpoll", "-socket", "/run/maven/mavend.sock",
|
|
"-netdata", "http://127.0.0.1:19999",
|
|
"-kuma", "http://127.0.0.1:3001/metrics",
|
|
"-kuma-key", "uk5_mavpoll-key"]
|
|
# Money tracking (Vikunja #125) is OFF: it needs a zenmoney token,
|
|
# which mavpoll reads from a FILE so it never appears in `ps`, in
|
|
# this file, or in shell history. To enable, mount the token and
|
|
# append: "-zenmoney-token-file", "/run/secrets/zenmoney.token"
|
|
# (optionally "-zenmoney-interval", "1h"). Core never sees the
|
|
# token — the poller writes facts(kind=env, source=poll:zenmoney)
|
|
# and mavend only reads those back when he asks.
|
|
depends_on: [mavend]
|
|
volumes:
|
|
- sockets:/run/maven
|
|
# - ./deploy/zenmoney.token:/run/secrets/zenmoney.token:ro
|
|
|
|
# The mail reader (Vikunja #246) is OFF and commented out: it needs an IMAP
|
|
# account, and there is none on this box. mavmaild reads the password from a
|
|
# FILE so it never appears in `ps`, in this file, or in shell history — the
|
|
# same rule mavpoll follows for the zenmoney token. Core never sees the
|
|
# password: the reader hands core message text on one IPC method, and core
|
|
# writes what the model extracts as task CANDIDATES he reviews on /tasks.
|
|
# Nothing here can create a reminder, so a misread mail cannot fire.
|
|
#
|
|
# To enable: write the password to deploy/imap.password (0600, gitignored),
|
|
# add an "email": {} block to deploy/mavend.json, and uncomment this service.
|
|
# mavmaild:
|
|
# <<: *image
|
|
# command: ["mavmaild", "-socket", "/run/maven/mavend.sock",
|
|
# "-imap", "imap.example.org:993",
|
|
# "-user", "kami@example.org",
|
|
# "-password-file", "/run/secrets/imap.password",
|
|
# "-mailbox", "INBOX",
|
|
# "-interval", "15m",
|
|
# "-state", "/var/lib/mavmaild/mail-seen.json"]
|
|
# depends_on: [mavend]
|
|
# volumes:
|
|
# - sockets:/run/maven
|
|
# # Its OWN volume, not dbdata. The whole point of a separate reader is
|
|
# # that a compromise on either side does not reach the other, and dbdata
|
|
# # is the encrypted database. The reader needs one JSON file of UIDs and
|
|
# # gets a volume that holds nothing else, so neither can be restored from
|
|
# # a backup of the other.
|
|
# - maildata:/var/lib/mavmaild
|
|
# - ./deploy/imap.password:/run/secrets/imap.password:ro
|
|
|
|
# The calendar reader (Vikunja #644) is OFF and commented out: it needs a
|
|
# CalDAV account, and there is none on this box. It was built, listed in
|
|
# `make build`, and deployed nowhere, which is the worst of the three states —
|
|
# this block records the decision instead.
|
|
#
|
|
# What its absence costs, so the cost is visible from here:
|
|
# - Agenda questions route correctly and answer from nothing. Stage 0 sends
|
|
# "что у меня сегодня" to IntentQuery (V-498) and the `calendar` query
|
|
# source reads facts(kind=env, source=caldav:*) that nobody writes.
|
|
# - The nudge gate loses a suppressor. loop.State.CalendarBusy is fed by
|
|
# those same facts, so "do not nag mid-meeting" is permanently false.
|
|
#
|
|
# Core never sees the CalDAV password: the reader polls the collection itself
|
|
# and hands core one fact per event over WriteFact. Nothing here can create a
|
|
# reminder, so a misread event cannot fire.
|
|
#
|
|
# The password is read from a FILE, so it never appears in `ps`, in this file,
|
|
# or in shell history — the same rule mavpoll and mavmaild follow.
|
|
#
|
|
# To enable: write the password to deploy/caldav.password (0600, gitignored),
|
|
# point -url at the collection, and uncomment this service. No mavend.json
|
|
# block is needed — events arrive over IPC as facts. -render-url is optional
|
|
# and OFF here: it publishes Maven's own reminders back as events, and it must
|
|
# not name the collection -url reads, or the poller reads its own writes back
|
|
# in (checkRenderTarget refuses that). It takes -render-pass-file, and falls
|
|
# back to this password when that is not given.
|
|
# mavcaldav:
|
|
# <<: *image
|
|
# command: ["mavcaldav", "-socket", "/run/maven/mavend.sock",
|
|
# "-url", "http://localhost:5232/kami/personal",
|
|
# "-user", "kami",
|
|
# "-pass-file", "/run/secrets/caldav.password",
|
|
# "-interval", "5m"]
|
|
# depends_on: [mavend]
|
|
# volumes:
|
|
# - sockets:/run/maven
|
|
# - ./deploy/caldav.password:/run/secrets/caldav.password:ro
|
|
|
|
volumes:
|
|
dbdata:
|
|
sockets:
|
|
# maildata — the mail reader's seen-UID file, and nothing else. See mavmaild.
|
|
maildata:
|
|
|
|
networks:
|
|
default:
|
|
ecosystem:
|
|
external: true # created by deploy/ecosystem/docker-compose.yml
|
|
name: ecosystem
|