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
+4 -5
View File
@@ -22,27 +22,26 @@ caveat is the pointer between them plus the trigger.
Every entry below came from the 2026-08-10 deep audit
(`docs/evals/2026-08-10-repo-audit.md`), except the last, which came from wiring
the gate the audit asked for. Nine of the twenty findings are fixed and have no
the gate the audit asked for. Twelve of the twenty findings are fixed and have no
entry. The unauthenticated mavgpud proxy was V-673. The 20 reachable advisories
in the toolchain and `x/text` were V-682. The missing analyzers were V-694, and
what they now report is the baseline entry under V-701. The invalid STT and
weather responses were V-675/V-676, the stuck TCP close was V-679, and the
unbounded PTT path was V-688. The two unguarded
invariants were V-692 and V-693, and their guards are described in
`docs/routing.md`.
`docs/routing.md`. The complete secret inventory and fail-closed integration
validation were V-691. The atomic reminder transition was V-678. The traceable,
sanitized mavweb problem contract was V-689.
| limit | severity |
| --- | --- |
| [Anyone past the proxy can enroll a passkey](security.md#enrollment) | high |
| [Passkey credentials are rewritten in place](security.md#credentials) | medium |
| [Dialogue persistence errors are swallowed](storage.md#dialogue) | medium |
| [The reminder transition is a lost update](storage.md#reminders) | medium |
| [A recall miss scans two whole tables](storage.md#recall) | medium |
| [mavweb errors cannot be traced](transport.md#errors) | medium |
| [Fact enrichment is a 20-call serial waterfall](workers.md#enrichment) | medium |
| [A suppressed nudge is phrased anyway](workers.md#nudges) | medium |
| [Committed absolute paths pin the build to this box](config.md#paths) | medium |
| [The env example omits deployed variables](config.md#secrets) | medium |
| [The analyzers pass against a baseline, not zero](dependencies.md#baseline) | medium |
| [Domain packages depend on store and IPC types](layering.md#dtos) | low |
| [Eleven symbols are unreachable](layering.md#deadcode) | low |
-11
View File
@@ -8,14 +8,3 @@ workstation model and Python paths. Vendoring hides the `go.mod` problem for an
ordinary build. `-mod=mod`, `go mod tidy` and a fresh checkout all fail.
Revisit when: anyone clones this repo elsewhere, or a `tidy` is needed.
Workaround: build only from this checkout, with the vendor directory.
## The env example omits deployed variables [#691] {#secrets}
Costs: a fresh deploy can lose remote speech-to-text or ambient authentication
and run on fallback behaviour with an apparently valid config. Three variables
are referenced and undocumented: `MAVEN_STT_TOKEN`, `MAVEN_AMBIENT_TOKEN` and
`CW2_TOKEN`. V-673 added `MAVEN_GPU_TOKEN` to the example.
It is not silent. The loader logs which variables were unset and says whatever
they configure is off. What is missing is a startup failure.
Revisit when: the box is redeployed from scratch, or a new secret is added.
Workaround: read that log line at startup.
-8
View File
@@ -11,14 +11,6 @@ Revisit when: a restart is reported as losing context. Sooner if a turn starts
reading dialogue rows back to him.
Workaround: none. The failure is invisible from outside.
## The reminder transition is a lost update [#678] {#reminders}
Costs: a concurrent fire and cancel both succeed and the last writer wins.
Medium today because cancellation has no surface. High the moment V-622 adds
one, and V-622 does not describe this invariant.
Revisit when: V-622 starts, whichever comes first.
Workaround: none, but the window is small while nothing can cancel.
## A recall miss scans two whole tables [#681] {#recall}
Costs: every missed recall reads all of `memory_vectors` and then decodes and
-12
View File
@@ -1,12 +0,0 @@
# Transport
The HTTP and socket seams. What a client can do to them, and what a shutdown
can do to us.
## mavweb errors cannot be traced [#689] {#errors}
Costs: some handlers return the raw internal error, which discloses internals.
Others return a generic one with no identifier, which cannot be joined to its
log line. There is no request-id middleware to join them.
Revisit when: a reported UI failure cannot be found in the log.
Workaround: read the log by timestamp.
+16 -1
View File
@@ -78,7 +78,10 @@ vocabulary size. CW2's 51897 tokens shift seven special token ids. So CW2 is its
own transformers service on port 8081 (`deploy/cw2/serve.py`).
`stt.HTTPTranscriber` posts raw PCM to it with a bearer token, because audio is
the most sensitive thing that crosses this seam. The switch is `workstation.stt`
in `deploy/mavend.json`, and deleting the block sends every utterance to mavsttd.
in `deploy/mavend.json`. A LAN URL with an empty token fails config validation;
`workstation.stt.disabled: true` is the STT-only dark state;
`workstation.disabled: true` disables both remote arms. The model has its own
`model_disabled` switch because CW2 can remain live without the model proxy.
**mavgpud runs that service as a second child.** This is not an optimisation.
CW2 is a ROCm process on the same card, so it registers on the KFD like any
@@ -208,6 +211,18 @@ rule.
## Web UI conventions
Every HTTP response carries a server-generated `X-Request-ID`. Error responses
use `application/problem+json` with three fields: a sanitized `error`, a stable
low-cardinality `code`, and that `request_id`. `writeProblem` is the only error
writer: it records the full wrapped error beside the same ID in the mavweb log,
while implementation paths, transport messages and credentials never reach the
browser. A page that can usefully degrade instead of failing keeps HTTP 200 and
renders the same code/request reference beside its sanitized panel error.
The `/ecosystem` panel uses its HTTP request ID as `X-Correlation-ID` on its
direct Nexus, Praxis and Hexis reads. Calls routed through mavend retain
mavend's action-scoped correlation contract instead.
Server-rendered pages share `cmd/mavweb/static/ui.css` (served at `/ui.css`) and
the shell partial in `cmd/mavweb/shell.html`. A page opens with
`{{template "shellTop" "<page-key>"}}` and closes with `{{template "shellBottom"}}`,
+5 -2
View File
@@ -113,8 +113,11 @@ read `/slots`, which returns other callers' prompts. mavgpud now reads
network without one. Downgrading to loopback instead would look safe and take
the model arm down. Maven sends the same token from `workstation.token`, on the
completion and on the `/health` probe alike. An unsigned probe answers 401,
which Pair reads as a busy card, so a missing token degrades to the resident
model rather than breaking a turn. The proxy also
which Pair reads as a busy card, but Maven now rejects a live LAN workstation
block with a missing token at startup: fallback must describe a temporarily
unavailable machine, not hide a broken secret source. `model_disabled: true`
is the explicit resident-only state and does not take CW2 down with it. The
proxy also
allowlists the five paths Maven calls, so a leaked token buys the model API and
not llama-server's admin surface.
+18 -2
View File
@@ -67,14 +67,30 @@ Config path: `~/.config/maven/mavend.json`. Full example with all options.
Omit the `embedder` block entirely to use the deterministic HashEmbedder floor (no ML, no ONNX runtime dependency). Useful for testing or low-resource setups.
`${NTFY_TOKEN}` and the `${TELEGRAM_*}` vars are expanded from `deploy/telegram.env`, which is gitignored. Copy `deploy/telegram.env.example` and fill it in. Mint a scoped token rather than reusing an admin one. It needs write access to the `maven` topic and nothing else:
`deploy/telegram.env.example` is the canonical inventory for every deployed
secret, including values whose destination is the root `.env`, the workpc CW2
environment file, or `deploy/db_key.env`. Copy values only to the destination
named beside them; never commit the populated files. Maven expands the homesrv
sink, workstation, and Home Assistant variables from `deploy/telegram.env`.
Every written integration block is either explicitly disabled or live. A live
Telegram, ntfy, LAN workstation model/STT, Home Assistant, ambient, CW2, or
encrypted-database configuration with an empty credential fails startup. This
keeps a missing env file from quietly becoming fallback behavior. The deployed
ntfy is currently `disabled: true`, and the workstation model arm is
`model_disabled: true`; the separately credentialed CW2 STT arm remains live.
Remove a dark-state flag only after provisioning that arm's credential.
Mint a scoped ntfy token rather than reusing an admin one. It needs write access
to the `maven` topic and nothing else:
```sh
ntfy access maven maven write-only
ntfy token add --expires=never maven
```
Deleting the `ntfy` block turns the reach off, and that is not a no-op. The routing table sends sev3-away nudges and away reminders to ntfy and nowhere else. With no sink wired they hit a nil and vanish, leaving no log line and no `delivery_attempts` row (V-649).
`disabled: true` keeps a documented ntfy block dark. When enabled, reminders
try ntfy and fall through to Telegram; delivery attempts record each reach.
## mavsttd — STT worker (optional, remote whisper.cpp)