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:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user