Make delivery and integration failures explicit

Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
2026-08-13 02:50:59 +04:00
parent da9114b623
commit 35c6ff5a71
67 changed files with 3174 additions and 477 deletions
+5
View File
@@ -11,6 +11,11 @@ dir and read-only models.
cp deploy/db_key.env.example deploy/db_key.env
printf 'MAVEN_DB_KEY=%s\n' "$(openssl rand 32 | base64 -w0)" > deploy/db_key.env
# 1b. review the complete secret inventory. Copy homesrv values to
# deploy/telegram.env, ambient enable/token values to .env, and workpc values
# to the destinations named in the example. Live blocks reject empty secrets.
cp deploy/telegram.env.example deploy/telegram.env
# 2. build + start
docker compose build
docker compose up -d
+17 -2
View File
@@ -20,6 +20,7 @@ every stutter on the way there.
"""
import hmac
import ipaddress
import json
import logging
import os
@@ -71,6 +72,18 @@ def authorised(headers):
return hmac.compare_digest(got, "Bearer " + TOKEN)
def validate_listener_auth(host, token):
"""Refuse a network listener without its bearer boundary."""
try:
loopback = ipaddress.ip_address(host).is_loopback
except ValueError:
loopback = host.casefold() == "localhost"
if not loopback and not token.strip():
raise ValueError(
f"CW2_TOKEN is required while CW2_HOST={host!r} is reachable from the network"
)
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
@@ -142,8 +155,10 @@ class Handler(BaseHTTPRequestHandler):
def main():
if not TOKEN:
log.warning("no CW2_TOKEN set: anything on the LAN can post audio here")
try:
validate_listener_auth(HOST, TOKEN)
except ValueError as exc:
raise SystemExit(str(exc)) from exc
# Bind before loading, so a restart answers 503 rather than refusing the
# connection. Both make Maven fall back, but only one of them says why.
srv = ThreadingHTTPServer((HOST, PORT), Handler)
+24
View File
@@ -0,0 +1,24 @@
import unittest
import serve
class ListenerAuthTest(unittest.TestCase):
def test_network_listener_requires_token(self):
for host in ("0.0.0.0", "192.168.1.105", "::"):
for token in ("", " "):
with self.subTest(host=host, token=token):
with self.assertRaises(ValueError):
serve.validate_listener_auth(host, token)
def test_loopback_listener_may_be_explicitly_unauthenticated(self):
for host in ("127.0.0.1", "::1", "localhost"):
with self.subTest(host=host):
serve.validate_listener_auth(host, "")
def test_network_listener_accepts_token(self):
serve.validate_listener_auth("0.0.0.0", "secret")
if __name__ == "__main__":
unittest.main()
+7 -4
View File
@@ -31,14 +31,14 @@
"rule — three things in series that have each failed once, and when they do",
"a sev4 nudge has nowhere to go. ntfy shares none of them: it is reached",
"directly, no relay.",
"It is not only a spare. The routing table sends sev3-away and away",
"reminders here and NOWHERE else, so with this block absent those two",
"routes hit a nil sink and vanish without a log or an outbox row.",
"It is the preferred away reach. Reminders fall back to Telegram in order,",
"and every missing reach is recorded in the outbox rather than disappearing.",
"The credential is an ntfy access token, scoped write-only to this one",
"topic, so a popped sink can push to it and cannot read it back. Set it in",
"deploy/telegram.env beside the telegram secrets; that file is gitignored."
],
"ntfy": {
"disabled": true,
"base_url": "https://ntfy.kvmx.ru",
"topic": "maven",
"token": "${NTFY_TOKEN}"
@@ -76,7 +76,9 @@
"resident model without saying so. Deleting this block restores exactly",
"the behaviour homesrv had before it existed.",
"Addressed by LAN address, not container name: mavgpud runs on another",
"machine and there is no shared docker network to name it on."
"machine and there is no shared docker network to name it on.",
"model_disabled keeps only this model arm dark until MAVEN_GPU_TOKEN is",
"provisioned; the independently authenticated STT arm below stays live."
],
"//workstation.stt": [
"CrisperWhisper 2.0 turbo on the same machine, a second service on port",
@@ -93,6 +95,7 @@
"The token is what stops anything on the LAN posting audio to that port."
],
"workstation": {
"model_disabled": true,
"url": "http://192.168.1.105:8080",
"token": "${MAVEN_GPU_TOKEN}",
"probe": "15s",
+30 -2
View File
@@ -16,6 +16,34 @@ NTFY_TOKEN=
# the card. Any long random string; mint one with:
# openssl rand -hex 32
# The same value goes in a file on workpc, named by token_file in
# deploy/mavgpud.json. Unset here and every workstation turn falls back to the
# resident model, because mavgpud answers 401 and Maven reads that as down.
# deploy/mavgpud.json. Empty is valid only while workstation.model_disabled is
# true; enabling the LAN model arm without it fails Maven's config validation.
MAVEN_GPU_TOKEN=
# Bearer token used by Maven when it sends audio to the workstation's
# CrisperWhisper service. The matching CW2_TOKEN is installed on workpc; these
# two values must be identical. Leaving both unset is only safe while that
# remote STT service is bound to loopback or its STT block is disabled.
MAVEN_STT_TOKEN=
# Home Assistant is currently explicitly disabled in deploy/mavend.json. Keep
# this name in the canonical inventory so enabling that block cannot turn into
# a valid-looking configuration with an expanded-empty token.
HA_TOKEN=
# docker compose reads these from the repository-root `.env`, before it starts
# containers; they are listed here even though their destination differs so
# this remains the complete deployment inventory. Copy both lines to `.env`.
# Enabling with an empty token fails startup; false is the explicit dark state.
# MAVEN_AMBIENT_ENABLED=false
# MAVEN_AMBIENT_TOKEN=
# Workstation-side counterpart to MAVEN_STT_TOKEN. Install this line in the
# EnvironmentFile named by deploy/mavgpud.service (currently
# %h/Programs/cw2-service/cw2.env), not in homesrv's telegram.env.
# CW2_TOKEN=
# The encrypted database key has its own narrowly mounted file,
# deploy/db_key.env, and its own generator in deploy/README.md. It is named
# here so an operator auditing this one inventory sees every deployed secret.
# MAVEN_DB_KEY=