diff --git a/deploy/maven-voice-tunnel.service b/deploy/maven-voice-tunnel.service new file mode 100644 index 0000000..3985373 --- /dev/null +++ b/deploy/maven-voice-tunnel.service @@ -0,0 +1,40 @@ +# maven-voice-tunnel — the ssh leg that carries the voice wire to homesrv. +# +# Runs on workpc, as a user unit (`systemctl --user`), beside mavgpud.service. +# +# WHY THIS EXISTS AT ALL. internal/voice is plaintext and unauthenticated. +# Its own server doc says 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 ssh +# is the substitute floor: it authenticates with his key and encrypts the leg, +# and mavend's published port stays on homesrv loopback (127.0.0.1:9110). +# Nothing about this puts a Maven port on the LAN. +# +# Do not replace this with a LAN bind. SurfaceVoice caps acts at L0, so an +# unauthorized speaker could not run a destructive tool. It would still hear +# his facts, his notes and his calendar read back, and L0 does not cap reading. +# +# install: cp to ~/.config/systemd/user/ on workpc +# systemctl --user enable --now maven-voice-tunnel.service + +[Unit] +Description=SSH tunnel to mavend's voice wire on homesrv +After=network-online.target +Wants=network-online.target + +[Service] +# -N: no remote command, forwarding only. +# ExitOnForwardFailure: fail loudly rather than sit up with a dead forward, +# which is what makes Restart meaningful. +# ServerAlive*: a laptop that suspends drops the tunnel silently otherwise. +ExecStart=/usr/bin/ssh -N \ + -o ExitOnForwardFailure=yes \ + -o ServerAliveInterval=30 \ + -o ServerAliveCountMax=3 \ + -o BatchMode=yes \ + -L 127.0.0.1:9100:127.0.0.1:9110 \ + kami@192.168.1.104 +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/deploy/mavwaked.service b/deploy/mavwaked.service new file mode 100644 index 0000000..c569e2b --- /dev/null +++ b/deploy/mavwaked.service @@ -0,0 +1,53 @@ +# mavwaked — always-on listening, on workpc where the microphone is. +# +# User unit, beside mavgpud.service and maven-voice-tunnel.service. It is a +# user unit because it needs his ALSA session and his ssh agent, and because +# it should stop when he logs out. +# +# THERE IS NO WAKE WORD YET (V-487 stage two). Anything spoken near the fifine +# becomes a turn. What makes that safe rather than expensive is voiceSender: +# it sends Surface=SurfaceVoice, which caps every command at L0, so no +# accidental trigger runs a destructive act. It does not stop her answering +# out loud, so this unit is his to stop when the room is not his alone. +# +# -vad-model is passed on purpose. Silero answers "is this frame speech" where +# the energy floor answers "is this frame loud". It declines white noise at +# the same RMS 0 frames to 68-99, and still hears all four spoken fixtures +# (docs/evals/2026-08-09-silero-vad.md). It costs 509us a frame, 1.7% of one +# core, and never touches the GPU. Drop the flag and the energy floor is back. +# +# -barge-in is NOT passed. The threshold is room-specific and this room has no +# number yet. Turn it on only after reading the "suppressed while speaking" +# means out of this unit's own journal, never by guessing. +# +# install: cp to ~/.config/systemd/user/ on workpc +# systemctl --user enable --now mavwaked.service + +[Unit] +Description=Maven always-on listening (VAD, no wake word yet) +# The tunnel is the only path to mavend and the only thing authenticating it. +Requires=maven-voice-tunnel.service +After=maven-voice-tunnel.service + +[Service] +# card 0 is the fifine USB microphone. Named, and not "default", because the +# default device follows whatever pipewire last decided and this daemon should +# not change ears when he plugs in a headset. +# +# plughw and not hw. mavwaked asks arecord for 16kHz mono, which is what the +# whole pipeline is canonical in. The fifine offers 2 channels at 44100 or +# 48000 and nothing else, so bare hw:0,0 dies on "Channels count non +# available" before a frame is read. plughw puts ALSA's downmix and resampler +# in front. Any replacement microphone wants the same treatment. +Environment=LD_LIBRARY_PATH=%h/.local/lib +ExecStart=%h/.local/bin/mavwaked \ + -device plughw:0,0 \ + -addr 127.0.0.1:9100 \ + -lang ru \ + -vad-model %h/.local/share/maven/models/silero_vad.onnx \ + -onnx-lib %h/.local/lib/libonnxruntime.so +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=default.target diff --git a/docker-compose.yml b/docker-compose.yml index 2eedef6..9d3f9d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,6 +53,19 @@ services: # 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