Give the audit's open findings a home and a trigger (V-674)
Nineteen of the twenty findings were open, and they lived in an untracked audit.md at the repo root that no next session would have read. The one that is closed, the unauthenticated mavgpud proxy, went out as V-673. The report is now a frozen measurement under docs/evals/, dated and never edited again — including when a finding it names gets fixed. The live state moved to docs/caveats/, one entry per limit, each carrying its Vikunja id and the condition that makes it worth fixing. A caveat with no revisit trigger is a complaint, so every entry has one. Closing a limit deletes its entry rather than editing the measurement that found it. Two directory indexes come with it. docs/CLAUDE.md states the tier rule the repo already followed by convention: living docs corrected in place, evals frozen by date, caveats deleted when fixed. docs/caveats/CLAUDE.md indexes the nineteen by claim and severity, because an index of filenames adds nothing a directory listing does not. Tasks V-675 through V-693 carry the plans. The doc line and the tracker now join in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ESv8hqNPseYt1CnotZpqDz
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# docs/caveats/
|
||||
|
||||
One entry per known limit: something broken, deferred or unsafe that a session
|
||||
will otherwise walk into. An entry names what fails, who it costs, and the
|
||||
condition that makes it worth fixing.
|
||||
|
||||
Two things do not belong here. The evidence is a dated file under `docs/evals/`.
|
||||
The reasoning behind a subsystem is its living doc directly under `docs/`. A
|
||||
caveat is the pointer between them plus the trigger.
|
||||
|
||||
## Rules for this directory
|
||||
|
||||
* One file per area, one `##` section per limit, each carrying its task id.
|
||||
* **A caveat with no revisit trigger is a complaint.** Give it one or delete it.
|
||||
* Closing a limit deletes its entry. It does not edit it to say "fixed", and it
|
||||
never edits the frozen measurement it came from. The durable record of a fix
|
||||
is the commit and the subsystem's living doc.
|
||||
* An entry whose task is closed but whose limit is still live is the failure
|
||||
mode to watch for. The id joins the two directions, so check both.
|
||||
|
||||
## Index
|
||||
|
||||
Every entry below came from the 2026-08-10 deep audit
|
||||
(`docs/evals/2026-08-10-repo-audit.md`). One of the twenty findings, the
|
||||
unauthenticated mavgpud proxy, was fixed as V-673 and has no entry.
|
||||
|
||||
| limit | severity |
|
||||
| --- | --- |
|
||||
| [Go 1.25.5 and x/text 0.14.0 carry 20 reachable advisories](dependencies.md#toolchain) | high |
|
||||
| [Anyone past the proxy can enroll a passkey](security.md#enrollment) | high |
|
||||
| [Passkey credentials are rewritten in place](security.md#credentials) | medium |
|
||||
| [An empty STT transcript reads as a successful one](external-inputs.md#stt) | medium |
|
||||
| [Open-Meteo's empty body becomes 0°C](external-inputs.md#weather) | 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 |
|
||||
| [Closing a TCP listener can strand Accept](transport.md#accept) | medium |
|
||||
| [PTT reads an unbounded body](transport.md#ptt) | 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 |
|
||||
| [heads_path may equal model_path](invariants.md#heads) | medium |
|
||||
| [baselineGrammars is mirrored by hand](invariants.md#grammars) | medium |
|
||||
| [Committed absolute paths pin the build to this box](config.md#paths) | medium |
|
||||
| [The env example omits deployed variables](config.md#secrets) | medium |
|
||||
| [Domain packages depend on store and IPC types](layering.md#dtos) | low |
|
||||
| [Eleven symbols are unreachable](layering.md#deadcode) | low |
|
||||
@@ -0,0 +1,21 @@
|
||||
# Configuration and environment
|
||||
|
||||
## Committed absolute paths pin the build to this box [#690] {#paths}
|
||||
|
||||
Costs: `go.mod` replaces Hexis with `/home/kami/apps/hexis`, `start-maven.sh`
|
||||
hardcodes the checkout and the data directory, and `deploy/mavgpud.json` holds
|
||||
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.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Dependencies
|
||||
|
||||
## Go 1.25.5 and x/text 0.14.0 carry 20 reachable advisories [#682] {#toolchain}
|
||||
|
||||
Costs: `govulncheck` found 20 reachable advisories, one in `x/text` and 19 in
|
||||
the standard library. They include template XSS, parser denial of service and
|
||||
TLS issues. Reachable traces run through the ONNX embedder's normalization,
|
||||
mavweb's HTML rendering, email header decoding and the mavgpud proxy. The
|
||||
vendored toolchain was built 2025-11-26.
|
||||
Revisit when: now. This is the highest-severity open entry and the fix is
|
||||
mechanical, so it ages badly for no reason.
|
||||
Workaround: none.
|
||||
|
||||
None of `staticcheck`, `govulncheck` or `deadcode` is installed on this box or
|
||||
wired into a make target. `make audit` is a git-grep inventory over loc, todo,
|
||||
stubs, docs, tests and gaps. **Do not read it as a static-analysis gate.** That
|
||||
gate is part of this entry.
|
||||
@@ -0,0 +1,20 @@
|
||||
# External inputs
|
||||
|
||||
What arrives from a service Maven does not run, and what happens when it
|
||||
arrives malformed. The shared shape: a JSON decode into value fields cannot
|
||||
tell "absent" from "zero", so a degraded response becomes a confident answer.
|
||||
|
||||
## An empty STT transcript reads as a successful one [#675] {#stt}
|
||||
|
||||
Costs: one dropped voice turn per malformed 200 from workpc. The mavsttd floor
|
||||
is never asked, because `stt.Pair` falls back on a non-nil error alone.
|
||||
Revisit when: CW2 returns a 200 with no text. Sooner if a proxy is put between
|
||||
homesrv and port 8081.
|
||||
Workaround: none. It is silent by design and the fallback is never spoken.
|
||||
|
||||
## Open-Meteo's empty body becomes 0°C [#676] {#weather}
|
||||
|
||||
Costs: he is told the weather is clear and 0°C when the service answered
|
||||
nothing. Distinct from V-589, which covered the HTTP status and not the body.
|
||||
Revisit when: a weather answer is reported as wrong, or the geocoder changes.
|
||||
Workaround: none.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Unguarded invariants
|
||||
|
||||
`CLAUDE.md` names these as load-bearing. Nothing enforces either one. A rule
|
||||
that lives only in prose gets broken by whoever did not read the prose. Both of
|
||||
these fail silently when broken.
|
||||
|
||||
`tokenizerRev` and `preRouteLadder` were checked and need nothing. The rev is
|
||||
baked into the embedder key, so a bump triggers re-embedding. A missing ladder
|
||||
rung is observable in the decision record.
|
||||
|
||||
## heads_path may equal model_path [#692] {#heads}
|
||||
|
||||
Costs: the routing heads then score with the same graph the resident e5-small
|
||||
uses, and recall degrades. There is no error and no log line, so it reads as
|
||||
ordinary drift rather than a misconfiguration.
|
||||
Revisit when: `deploy/mavend.json` is edited by hand, or a fine-tuned heads
|
||||
graph is swapped in.
|
||||
Workaround: check the two keys by eye. That is the whole guard today.
|
||||
|
||||
## baselineGrammars is mirrored by hand [#693] {#grammars}
|
||||
|
||||
Costs: the eval fixture restates the stage 0 rule set in the daemon's order,
|
||||
and its own comment says so. Three test files score against it. A grammar added
|
||||
to `buildRouter` alone means every routing measurement scores a set nobody
|
||||
runs. `CLAUDE.md` warns about this failure by name.
|
||||
Revisit when: the next stage 0 grammar is added. That is when it bites.
|
||||
Workaround: add to both lists, which is what the rule already says.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Layering and dead surface
|
||||
|
||||
Neither entry breaks anything today. Both make a later change cost more than it
|
||||
should, which is why they are low and not medium.
|
||||
|
||||
## Domain packages depend on store and IPC types [#685] {#dtos}
|
||||
|
||||
Costs: dialogue exposes `store.DialogueSessionRow` in its port, the pure
|
||||
morning planner takes a `store.Fact`, and auth policy imports IPC method and
|
||||
caller types. There is no Go import cycle. A schema change reaches further than
|
||||
it should.
|
||||
Revisit when: the dialogue or fact schema changes, or a second transport
|
||||
appears beside IPC.
|
||||
Workaround: none needed. It compiles and it is correct.
|
||||
|
||||
## Eleven symbols are unreachable [#686] {#deadcode}
|
||||
|
||||
Costs: extra API and test surface, and comments that claim callers which no
|
||||
longer exist. Three of the eleven must not be deleted. `HisGender` is a
|
||||
documented seam tied to V-399. `AudioDuration` duplicates `internal/audio` and
|
||||
should call it. `CountWord` is a one-line alias nobody uses and can go.
|
||||
Revisit when: `deadcode` is wired into the audit gate, which needs the
|
||||
allowlist this entry describes. **An unannotated list invites deleting the
|
||||
three above.**
|
||||
Workaround: none needed.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Security
|
||||
|
||||
Both entries are mavweb's passkey seam. Assertion itself is sound and is not
|
||||
the problem: `userVerification` is required, and a sign count that does not
|
||||
increase is rejected.
|
||||
|
||||
## Anyone past the proxy can enroll a passkey [#683] {#enrollment}
|
||||
|
||||
Costs: registration is gated on nothing, so any client that reaches mavweb can
|
||||
enroll its own key and become him. Step-up is worse than per-client: one
|
||||
process-global `assertedAt` means every client inherits the same five-minute
|
||||
window after any successful assertion. The voice WebSocket accepts every
|
||||
origin, which makes cross-site use easier. V-317 covers which routes are gated
|
||||
and V-605 covers challenge-map growth. Neither covers this.
|
||||
Revisit when: mavweb is reachable from anything but the tunnel, and before any
|
||||
new credential is enrolled.
|
||||
Workaround: the reverse proxy is the only boundary today. That is the finding.
|
||||
|
||||
## Passkey credentials are rewritten in place [#684] {#credentials}
|
||||
|
||||
Costs: `os.WriteFile` over the live file. A crash, a full disk or an
|
||||
interrupted write corrupts every enrolled credential at once, and mavweb will
|
||||
not start afterwards.
|
||||
Revisit when: a second credential is enrolled, since the blast radius grows
|
||||
with the count. Sooner if the box loses power unexpectedly.
|
||||
Workaround: back the file up before enrolling.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Storage
|
||||
|
||||
The DB seam: what it loses quietly, and what it reads more of than it needs.
|
||||
|
||||
## Dialogue persistence errors are swallowed [#677] {#dialogue}
|
||||
|
||||
Costs: restart continuity can vanish with nothing in the log, and a failed
|
||||
delete can bring stale conversation state back. Current-turn dialogue is
|
||||
unaffected, which is why this has never been noticed.
|
||||
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
|
||||
sorts every note vector. Not an N+1, and the memory scan is cheap per losing
|
||||
row on purpose. The duplicated decode is the legacy notes path alone.
|
||||
Revisit when: the note count makes a miss measurably slow. Also when the two
|
||||
exclusion filters are proven to agree. `QueryNotes` uses `notHisWordsSQL` and
|
||||
`Search` uses `memory.NonRecallPrefix`. The fallback cannot go until they match.
|
||||
Workaround: none needed at today's row counts.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Transport
|
||||
|
||||
The HTTP and socket seams. What a client can do to them, and what a shutdown
|
||||
can do to us.
|
||||
|
||||
## Closing a TCP listener can strand Accept [#679] {#accept}
|
||||
|
||||
Costs: during close, both `errc` and `done` are ready in `acceptLoop`'s select.
|
||||
Go picks uniformly. So roughly one close in two leaves a waiting `Accept`
|
||||
blocked forever on a TCP seam. Unix sockets are unaffected.
|
||||
Revisit when: a daemon is seen hanging on shutdown, or before any new TCP
|
||||
listener is added.
|
||||
Workaround: the process usually exits anyway, which hides it.
|
||||
|
||||
## PTT reads an unbounded body [#688] {#ptt}
|
||||
|
||||
Costs: `handlePTT` does an unlimited `io.ReadAll`, and mavweb sets no header or
|
||||
idle timeouts. A client can force unbounded allocation or hold a connection
|
||||
open. mavgpud's half of this was fixed in V-673.
|
||||
Revisit when: mavweb is reachable from anything but the tunnel.
|
||||
Workaround: mavweb is not LAN-exposed today.
|
||||
|
||||
`/ws` rides along with this entry. It never calls `SetReadLimit`, so the
|
||||
dependency default of 32,768 bytes applies, about a second of audio. Nothing
|
||||
reaches it: the browser posts PCM to `/api/ptt`, and only `handlers_test.go`
|
||||
opens `/ws`. It gets a caller and a real limit, or it gets deleted.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Background workers
|
||||
|
||||
Both entries are a tick doing expensive work it did not need to do.
|
||||
|
||||
## Fact enrichment is a 20-call serial waterfall [#680] {#enrichment}
|
||||
|
||||
Costs: each fact is resolved in turn and each ecosystem call can spend ten
|
||||
seconds. A slow but reachable Nexus holds one tick for minutes, so the worker
|
||||
stops observing its configured interval. V-647 covered the duplicate queue
|
||||
scan, not this.
|
||||
Revisit when: Nexus gets slow, or when a batch-resolution endpoint exists.
|
||||
Workaround: an unreachable Nexus is fine. It is the slow-but-answering case
|
||||
that hurts.
|
||||
|
||||
## A suppressed nudge is phrased anyway [#687] {#nudges}
|
||||
|
||||
Costs: `PhraseNudge` runs before the dedupe is known. The `continue` meant to
|
||||
skip it is the last statement in the loop body. Every tick that
|
||||
keeps suppressing the same rule pays the resident model again. The comment
|
||||
above it claims the opposite.
|
||||
Revisit when: digestion ticks show up in the model's load, or when nudge rules
|
||||
grow past a handful.
|
||||
Workaround: none.
|
||||
Reference in New Issue
Block a user