bd1e2789eb
The doc was from the initial commit and predated all open-item work. Update the works-end-to-end list (protocol doc, away-fallthrough, mavcaldav, quiet-hours schedule, tools enable/disable, note RAG, passkey step-up), rework the not-built-yet ranking (at-rest crypto, mavweb/mavcaldav tests, systemd), and add the rationale for deferring multi-user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
185 lines
12 KiB
Markdown
185 lines
12 KiB
Markdown
## Maven — current state (2026-07-03)
|
||
|
||
Consolidated status. The reactive↔proactive core is closed and testable through
|
||
the web PWA. The SPEC's open items 1–7 are now landed (protocol doc, away-channel
|
||
fallthrough, CalDAV poller, quiet-hours schedule, tools enable/disable, note RAG,
|
||
passkey step-up); item 8 (multi-user) is deliberately deferred — see the tail.
|
||
Big untouched infra remaining: **at-rest security** (sqlcipher) and **systemd/
|
||
container deployment** (still scripts + tmux). ~13.1k LOC + ~5.3k test, 202 tests.
|
||
|
||
### Access model
|
||
|
||
- **Phone** → needs the wg tunnel to reach homesrv (no homesrv DNS otherwise;
|
||
raw IP or a DNS tweak can bypass, not the default).
|
||
- **PC** → uses homesrv DNS, resolves the domains over local-net, **no wg needed**.
|
||
- nginx + ufw both scope to `10.42.0.0/24` (wg) + `192.168.1.0/24` (LAN), deny all else.
|
||
- **Surface in use now: the web PWA (`mavweb`).** Voice PTT + in-app nudges both ride it.
|
||
|
||
### Works end-to-end (tested)
|
||
|
||
- **Reactive voice:** PWA record → Whisper STT (`mavsttd`) → ONNX classifier →
|
||
LFM 2.5-1.2B phraser (llama-server subprocess) → Piper TTS (`mavttsd`) → reply.
|
||
HTTP POST path (mobile-Chrome drops WS for the audio).
|
||
- **Capture:** `fact` (EN **and RU** — root-substring recognizers) + `reminder`
|
||
persist through CoreAPI (`source=tap:voice`). This is the substrate the care
|
||
rules read.
|
||
- **Notes / query (semantic recall, sqlite — no chroma):** `note` → embed (the
|
||
classifier's ONNX embedder) → `notes` table. `query` → embed → brute-force
|
||
cosine top-k → confidence-gated (below `queryMinScore` 0.55 ⇒ "no note", not a
|
||
guess). **Note RAG (SPEC item 6):** the gated top-k feed the phraser
|
||
(`PhraseQuery`) to compose a natural answer ("вот что я нашла: …") instead of
|
||
a verbatim dump; raw-notes fallback on any LLM error. Stub is deterministic.
|
||
- **Monitoring (`/dash`):** mavweb server-renders presence + recent nudges (by
|
||
outcome) + recent facts from the append-only store via CoreAPI. Read-only,
|
||
meta-refresh, no JS.
|
||
- **Proactive loop:** 60s dumb ticker, pure predicates over a State snapshot,
|
||
universal gate (quiet-hours/presence/cooldown/snooze/calendar), one-nudge-per-
|
||
tick max-severity, reminders (gate-bypassing), sev4 repeat-til-ack, feedback
|
||
auto-tuner (outcome ratio → bounded cooldown, persisted as `source=feedback`).
|
||
- **Rules:** water/meal/break (sev1–2 care), service_down (sev4, `poll:uptimekuma`),
|
||
netdata_critical (sev3, `poll:netdata`).
|
||
- **Env facts (`mavpoll`):** netdata alarms → `netdata_alarm` (fires immediately
|
||
on a real CRITICAL); kuma monitor_status → `service_down`. Writes only on
|
||
value-change (no append-only churn).
|
||
- **Presence:** noisy-OR decay + Schmitt hysteresis. Live via `page_heartbeat`
|
||
(PWA auto-pings `/api/signal` every 30s → present when a tab's open).
|
||
- **Delivery:** ntfy / telegram / voice by `f(severity, presence)`; minimal body
|
||
on away channels. PWA subscribes to ntfy over **WebSocket** for in-app nudges.
|
||
- **Away-channel fallthrough (SPEC item 2):** when the router picks voice but no
|
||
live session exists at push time (presence guess was wrong), the dispatcher
|
||
reroutes through the AWAY table — sev3→ntfy, sev4→telegram-repeat-til-ack,
|
||
sev≤2→drop — instead of silently dropping. Covers nudges + reminders.
|
||
- **Calendar busy (SPEC item 3, `mavcaldav`):** new poller queries a self-hosted
|
||
**Radicale** CalDAV server on an interval, writes `calendar_busy` + event facts
|
||
through CoreAPI (value-change only). The loop gate already consumes `calendar_busy`.
|
||
- **Quiet-hours schedule (SPEC item 4):** the gate reads `quiet_hours`; a config
|
||
time window (`voice.quiet_hours`, HH:MM, midnight-crossing handled) now sets it
|
||
on each tick — in addition to the "тихий режим" voice toggle. Both activate quiet.
|
||
- **Client protocol (SPEC item 1):** the voice wire format (length-prefixed JSON
|
||
frames) is published in `PROTOCOL.md`, generated from `internal/voice/wire.go`
|
||
so third-party clients don't need the Go source.
|
||
- **Passkey step-up (SPEC item 7):** `internal/webauthn` does real WebAuthn —
|
||
ES256/P-256 register + assert, ecdsa signature verification, rpIdHash + UP/UV
|
||
flag binding (UV = the gesture), sign-count regression check. `PasskeySession`
|
||
bumps the auth session L2→L3 for a TTL on assert. mavweb serves `/auth/passkey`
|
||
(enroll + step-up) + the begin/finish endpoints. Crypto is round-trip tested
|
||
(incl. tampered-sig / missing-UV / wrong-origin negatives).
|
||
- **Stability:** llama-server orphan leak fixed (`Pdeathsig` kills the child on
|
||
any mavend death); `kill-maven.sh` reaps strays (matches the model, not a
|
||
bogus `llama-server.*maven` pattern); `start-maven.sh` wires `-core` + poller.
|
||
|
||
### Wired but needs a deploy action (not code)
|
||
|
||
- **`desk_active`** (strongest presence signal) — `scripts/desk-active.sh` runs
|
||
on the **desk PC** (hypridle-gated systemd timer), posts over wg to mavweb.
|
||
- **Kuma `service_down`** — needs an API key created in Kuma → Settings → API Keys,
|
||
passed to `mavpoll -kuma-key`.
|
||
|
||
Caveats / gotchas:
|
||
- **desk_active is a workstation deploy, not code** — 0 facts ever written; presence
|
||
runs on page_heartbeat alone (dash reads "away"/"never at desk"). `scripts/desk-active.sh`
|
||
+ a hypridle-gated `maven-desk` timer must be installed on the desk PC (not homesrv).
|
||
- **Notes recall needs the ONNX embedder** — under the HashEmbedder floor, cosine is
|
||
lexical (token overlap), not semantic; scores are low, so most RU commands sit under
|
||
the 0.35 route threshold and clarify. Configure `voice.embedder` for confident recall+routing.
|
||
(The floor now at least tokenizes Cyrillic — see below — so it ranks correctly, just weakly.)
|
||
- **Switching the embedder model silently breaks old notes** — different dim ⇒
|
||
cosine 0 ⇒ they stop matching; brute-force can't re-embed. Re-embed on a model change.
|
||
- **`wg_handshake` is OFF and should stay off** — in this topology the phone only
|
||
runs wg when *outside*, so a fresh handshake means AWAY, not here. The `mavpoll
|
||
-wg` flag exists (defaults `""`) and could later back the spec's "away override"
|
||
by flipping the sign; as a presence-*here* signal it's inverted. desk_active +
|
||
page_heartbeat cover home presence.
|
||
|
||
### Not built yet (ranked by ROI)
|
||
|
||
1. **At-rest encryption** — sqlcipher + cold-start unlock. Today the store is
|
||
plain sqlite: disk access = full read. Passkey step-up now gates *actions*,
|
||
but not the data at rest.
|
||
2. **`cmd/mavweb` / `cmd/mavcaldav` tests** — both are untested (0 test files).
|
||
mavweb now carries the passkey + tools HTTP flow (the webauthn *crypto* is
|
||
tested; the handlers and browser JS are not). Highest-value test gap.
|
||
3. **Deployment** — still `start-maven.sh` + tmux. systemd user units (one per
|
||
binary, socket-activated, `After=mavend.socket`, `Restart=on-failure`) are
|
||
specced, not written. Docker/Podman is a later phase.
|
||
4. **Multi-user (SPEC item 8)** — deliberately deferred, see the tail.
|
||
|
||
Housekeeping: `go.mod` isn't tidied — `onnxruntime_go` and `coder/websocket`
|
||
are used but marked `// indirect`. Passkey credentials are mavweb in-memory
|
||
(re-enroll after a mavweb restart — acceptable for single-user, by design).
|
||
|
||
Done since last revision: **act tool executor, store-backed, full flow**
|
||
(`internal/tool` + `internal/store/tools.go` + `tools` CoreAPI methods).
|
||
- **Execution:** IntentAct runs the matched fn against the store's ENABLED
|
||
allowlist. argv, no shell → STT text can't inject. Live store read, so a
|
||
newly-enabled tool runs without a daemon restart.
|
||
- **proposed→enabled→disabled (SPEC item 5):** an act whose verb isn't enabled is
|
||
scaffolded as a `proposed` tool (maven suggests). A human enables it (fills argv
|
||
+ destructive) on the authed **`mavweb /tools`** page — never voice — and can
|
||
disable it back to `proposed` (kept in the store, won't run). `EnableTool`/
|
||
`DisableTool` sit at `AuthStepUp`; the gate is now **live** via `PasskeySession`,
|
||
so /tools enable requires a passkey assertion at `/auth/passkey` first.
|
||
- **Confirm turn:** a destructive enabled tool replies "выполнить X? да/нет" and
|
||
parks; the next utterance (ru/en yes-no) confirms or cancels (90s TTL).
|
||
- **Config:** `voice.tools` seeds enabled tools at boot (editing mavend.json =
|
||
the human enable act); mavweb enables ad-hoc ones on top.
|
||
- **Russian:** fixed grammar in reply strings + seed files; maven's self-
|
||
reference is feminine ("she") — [[maven-persona-gender]].
|
||
|
||
Also fixed:
|
||
- **HashEmbedder was blind to Cyrillic** (`tokenize` iterated bytes, kept only
|
||
`a-z0-9`) → every RU utterance embedded to the zero vector → cosine 0 across
|
||
all intents → misrouted to `act` (alphabetical tie-break). Now rune-based
|
||
(`unicode.IsLetter`). This was the real cause of "Найди заметку" (a query)
|
||
landing in `notes`; added note-retrieval query seeds too.
|
||
- **Notes are now browsable on `/dash`** — `RecentNotes` plumbed through the
|
||
store + CoreAPI; voice-captured notes were previously only reachable via
|
||
semantic `query`.
|
||
Earlier: notes/query recall, `/dash` monitoring, `wg_handshake` poller (NO-OP).
|
||
|
||
### Future / logged, not now
|
||
|
||
Personality prompt; custom TTS voice training (kami-picked voice, replaces irina
|
||
floor); listening modes 2–3 (meeting-record, ambient-derive).
|
||
|
||
### Services & layout
|
||
|
||
- `mavend` (core, IPC unix socket) — store + loop + phraser; the only key-holder.
|
||
- `mavsttd` / `mavttsd` — STT/TTS worker modules (unix sockets).
|
||
- `mavweb` — PWA bridge (HTTP), `/api/ptt` voice, `/api/signal` presence ingest,
|
||
`/api/ntfy` WS-subscribe config, `/dash` read-only monitoring.
|
||
- `mavpoll` — env poller (netdata/kuma → facts via CoreAPI).
|
||
- `mavcaldav` — CalDAV poller (Radicale → `calendar_busy` + events via CoreAPI).
|
||
- All behind wg + nginx deny-all; no phone-home. CGo only in `mavsttd`.
|
||
- Start/stop: `./start-maven.sh [build]`, `./kill-maven.sh`.
|
||
- Config: `~/.config/maven/mavend.json` (or `mavend.json` in repo root).
|
||
|
||
### Key files
|
||
|
||
- `cmd/mavend/{main,tick,voice}.go` — daemon wiring, loop driver, voice handler
|
||
- `internal/loop/{loop,rules,gather,feedback}.go` — proactive engine
|
||
- `internal/store/` — append-only facts/reminders/nudges/presence/notes
|
||
- `cmd/mavweb/{main.go,dash.html}` — PWA bridge + `/dash` monitoring
|
||
- `internal/router/{classifier,slots,stage0}.go` — reactive routing + slot parse
|
||
- `internal/delivery/` — dispatcher + ntfy/telegram/voice sinks
|
||
- `internal/auth/` — scope/gate/policy; `FloorEnrollment` (same-uid = device
|
||
trust) + `webauthn.PasskeySession` (real step-up for L3)
|
||
- `internal/webauthn/`, `cmd/mavweb/webauthn.go` — passkey register/assert
|
||
- `cmd/mavcaldav/`, `cmd/mavpoll/`, `scripts/desk-active.sh` — env producers
|
||
|
||
### Why multi-user (SPEC item 8) is deferred
|
||
|
||
Not neglect — the one item where doing nothing now beats doing something:
|
||
|
||
- **No second user exists yet** (the "gf phase"). Building per-user partitioning
|
||
now means code exercised by zero users and validated by nobody — YAGNI.
|
||
- **The append-only schema makes it a migration, not a rewrite.** No row is ever
|
||
mutated, so adding `facts/notes/reminders.user_id` later is add-columns +
|
||
backfill-to-"kami" — no reshaping, no dual-write window. Deferral is cheap.
|
||
- **The hard part is speaker attribution, and it needs the second voice.** A
|
||
voice-print discriminator (kami vs gf vs unknown) can't be trained or tuned
|
||
with one voice in the house. Plumbing before the model is pipe with no water.
|
||
- **It's fenced deliberately** (`DO NOT TOUCH THIS PHASE` in SPEC.md) so an
|
||
autonomous agent doesn't add `user_id` columns while touching the store and
|
||
commit us to a schema before the constraints that shape it exist.
|