Commit Graph

13 Commits

Author SHA1 Message Date
claude bec7362b7b config: clear three of 472's five QA blockers (V-459)
morning_routines, feeds, crawl.on_demand and netscan.enabled in
deploy/mavend.json; -ambient-token on mavweb, interpolated from a gitignored
/.env. All verified on the box: the dispatcher builds the morning plan,
/api/ambient answers 401/201, the crawler reads a named page, netscan finds 3
devices, and /events fills with scan:lan and ambient:notif.

Filed 482 (ambient reads a notification's wall clock as UTC). Corrected 479:
both capabilities work once configured, so it is not a routing defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJYcaBiuny9UGSpFweQVQ1
2026-08-02 15:52:56 +04:00
kami 8c6332f95c mavmaild: own its state volume, retire aged-out UIDs, stop restart-looping
The commented compose service mounted dbdata, the encrypted database volume,
read-write, for one JSON file of UIDs. The header of that same file says only
mavend holds the key and the db volume, and the whole argument for a separate
reader is that a compromise on either side does not reach the other. It gets its
own volume now, at its own path, so neither can be restored from a backup of the
other.

The high-water mark only advances through a contiguous run, and a failed ingest
is deliberately not marked. One message that never ingested therefore pinned the
mark forever: after the lookback window passed it could never be fetched again,
so the gap never closed, every UID above it stayed in the explicit set, and save
rewrote all of them every poll. FetchSince now reports the SEARCH window and the
poller retires everything below it, since a UID that can no longer be searched
for can never be read.

On ErrUnknownMethod the daemon logged "stopping" and then exited at the next
tick with status 0. The compose service inherits restart: unless-stopped, which
restarts a clean exit, so the real behaviour was a loop of four IMAP logins an
hour against a mailbox core would not accept anything from. It now stays up and
polls nothing.

The reader also sends the Junk verdict instead of counting bulk locally, which
is what the wire doc says it does. The verdict carries no mail content, since
nothing on the other side will read it. RunWith is gone, so the tests fake the
read rather than the transport.
Found in review of #65.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
2026-08-01 14:08:32 +04:00
kami ee7bec11e3 Add mavmaild, the read-only IMAP poller that feeds mail intake (#246)
The extraction seam landed on the previous branch but nothing fed it. This
adds the daemon that does: every interval it opens one mailbox read-only
(EXAMINE + BODY.PEEK, so reading leaves no \Seen behind), fetches the UIDs
it has not handed over yet, and posts each message to core over
ingest_mail. Core runs the model and writes task candidates; this daemon
writes nothing and cannot create a reminder.

It is a separate daemon because of the credential. mavpoll set the
precedent with the zenmoney token (#125): the module talking to the third
party holds the secret, reads it from a file so it never lands in argv, in
docker-compose.yml or in shell history, and core never sees it. There is
deliberately no -password flag, and a test asserts that.

Off unless configured at both ends: without -password-file the daemon
refuses to start, and if core has no email block the first ingest returns
ErrUnknownMethod, which disables the reader instead of hammering a socket
that will keep refusing. A seen-UID state file (0600, atomic write) keeps a
restart from re-extracting the whole lookback window; correctness does not
depend on it, since capture dedupes on normalised text. Logs are counts and
UIDs — no subject, sender or body.

Verified with an in-process IMAP server and a fake core: bulk mail is
filtered before core is asked, seen UIDs are not re-fetched, a failed
ingest is retried next poll, ErrUnknownMethod stops at the first message,
and state survives a restart. The live half is untested by design — no IMAP
credential exists on this box; setup is written up as QA steps.

Vikunja #246
2026-08-01 03:13:18 +04:00
kami da647e87d0 Read spending from zenmoney in the poller, answer it from facts (#125)
The trust boundary is zenmoney, not maven — they already hold his bank
sessions. So the poller reads /v8/diff/ and writes totals as
facts(kind=env, source=poll:zenmoney); core reads those back when he asks
and never sees the token.

internal/zenmoney sums transactions per currency over a window, skipping
tombstoned rows and transfers between his own accounts, and refuses to
encode a summary built from zero transactions. That refusal is the whole
design: a failed or empty read writes nothing and leaves the last good
total alone, because a zero recited as fact is worse than silence. No
currency conversion either — a figure he can check against his bank beats
one he cannot.

Off unless configured, and the token is read from a FILE rather than a
flag so it never lands in `ps`, in docker-compose.yml, or in shell
history. Nothing about the money is search input, no tick rule reads the
keys, and the log lines name keys, never figures.

The live-credential half is BLOCKED: there is no zenmoney account or token
here, so everything is verified against a recorded diff fixture.
2026-08-01 02:50:27 +04:00
kami 691c3660d2 Bind mavweb to loopback and make the step-up fail-open loud and overridable
The /tools enable action takes name+cmd from form fields and calls
core.EnableTool, so it defines arbitrary argv that internal/tool then executes.
Its step-up gate read `session != nil && !session.IsStepUp()`, and
stepUpSession is nil unless both -webauthn-origin and -webauthn-rpid are set —
so with neither flag the gate was skipped entirely. compose passed neither and
published 9201 on every host interface, while /ptt proxies to the voice server
unauthenticated, so a caller could enable a tool, trigger it, and answer its
own confirm turn. internal/tool's boundary reasoning ("a compromised router
can't grant itself a capability") held; the outer boundary it depends on was an
unwritten deployment assumption.

The fail-open itself stays: gating on a session that can never be asserted
would 403 permanently, and that reasoning is sound. What was missing is the
compensating control.

- compose publishes 127.0.0.1:9201 so reaching the UI requires the wg tunnel by
  construction rather than by convention. Verified no other service reaches
  mavweb by host-published port; mavpoll is host-networked but only dials
  netdata and kuma.
- stepUpOK() replaces the two inline gates in handleTools and handleRevert, so
  one decision point covers both surfaces.
- -require-stepup (default false, behaviour byte-for-byte unchanged) fails those
  actions closed when step-up cannot be asserted.
- A startup warning names both unguarded surfaces when stepUpSession is nil,
  in fail-open and fail-closed variants.

Also repoints one doc comment at DESIGN.md, since it shared a hunk with the
warning block.

The committed kuma key is deliberately left for a separate change: the old
value is in git history forever, so rotation means a genuinely new key, not a
re-commit under a variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:39:07 +04:00
kami 5fe8f228c1 feat(mavweb): /ecosystem page consuming Nexus/Praxis/Hexis + shell fixes
Add a read-only /ecosystem page that consumes the sibling services'
JSON APIs (Nexus entities, Praxis attention, Hexis capabilities),
fetched concurrently with honest per-panel error states. Siblings stay
headless — mavweb is their human surface (arch §16). Wired via mavweb
-nexus/-praxis/-hexis flags; mavweb joins the ecosystem compose network.

Fix mobile horizontal overflow across all pages: .content is a flex
child with default min-width:auto, so it refused to shrink below the
tables' intrinsic width. min-width:0 lets wide tables pan inside .scroll
instead of dragging the page sideways. Verified via CDP geometry check
(scrollWidth === clientWidth at 430px).

Also includes in-progress Ethos UI redesign, ecosystem deploy compose,
and planning docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:04:23 +04:00
kami da60c14399 chore: docker, config, delivery sinks, dialogue, and agent docs
- Dockerfile: multi-stage build with CGO_ENABLED=0, embedder model copy,
  non-root user, healthcheck, and /data volume.
- docker-compose.yml: mavend + mavweb services with shared volume, health
  checks, and restart policy.
- .gitignore: ignore models/llm/*.gguf, deploy/telegram.env, tmp artifacts.
- deploy/mavend.json: add LLM, phraser, voice sections (embedder, model
  paths, wake sensitivity). Add telegram token env-var expansion.
- deploy/telegram.env.example: template for telegram bot token.
- internal/config/config.go: add LLM config struct, voice config struct
  (embedder, llama, wake sensitivity), telegram token loading.
- telegramsink: add chat intent delivery support alongside existing types.
- voicesink: skip empty payloads in delivery.
- dialogue/session: add chat intent to anaphora resolution, test coverage.
- AGENTS.md: update with LLM embedder, LFM model download/configure steps,
  new UI conventions.
- REARCH.md: architecture research document.
- cmd/mavend/main.go: wire LLM config, phraser, embedder, telegram config,
  WebAuthn, IPC event/routine handlers, and reactive notes.
2026-07-10 15:49:27 +04:00
kami eda434fe0b ops: kuma api key, voice verification, desk_active doc
- created kuma API key uk5_mavpoll-key, wired into mavpoll
- fixed basic auth field (kuma expects key as password, not username)
- switched mavpoll to network_mode: host (compose bridge can't reach host)
- fixed stale voice bind comment in docker-compose.yml
- verified voice listening on :9100, cross-container reachable
- updated PROGRESS.md ops footnote
- added ROADMAP.md
2026-07-06 13:13:23 +04:00
kami a38e733514 fix: local timezone for replies + quiet-hours; guard presence "ago" overflow
Timezone: the container ran in UTC, so mavend answered clock/date queries
(voice.go replySystem) and evaluated quiet-hours (gather.go) in UTC. Fixed at
the root — process TZ — rather than per-call: TZ=Europe/Samara in compose +
tzdata in the image (debian-slim strips it, without which Go ignores TZ and
stays UTC). One knob fixes replies and quiet-hours for every daemon; change the
zone in compose.

Overflow: the dash "ago" helper ran time.Since on a zero timestamp (no presence
yet / fresh db), saturating to ~292y and rendering "2562047h47m…". Guard zero →
"never".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 00:13:13 +04:00
kami de8fd9ba90 deploy: build on every service so build <svc> can't no-op
build: . lived only on mavend, so `docker compose build mavweb` (or any other
module) silently built nothing and redeployed a stale mavenai:latest — a fixed
binary looked deployed but wasn't. Moved build into the shared anchor; same
image name means it's still built once, but naming any service now rebuilds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 23:40:10 +04:00
kami b129935995 deploy: give mavsttd the render group so whisper hits the GPU
Mapping /dev/dri alone wasn't enough — the container runs as unprivileged
uid 10001, and renderD128 is root:render (mode crw-rw----). Without membership
in the host render gid, Vulkan enumerates zero devices and whisper silently
falls back to CPU. Added group_add 993 (host 'render'); whisper now loads on
RADV RENOIR (AMD Radeon).
2026-07-03 23:00:59 +04:00
kami 94a1c8e979 deploy: fix image build — trixie/go1.24 base, rename off the maven collision
Three fixes found bringing the stack up on the host daemon:

- base was golang:1.23-bookworm: bookworm's glibc 2.36 / GLIBCXX 3.4.30 is too
  old to link the prebuilt deps/lib/*.so (built on Arch against glibc 2.38 /
  GLIBCXX 3.4.32). Moved build+runtime to trixie (glibc 2.40). golang trixie
  images start at 1.24, which builds the go 1.23 module fine.
- builder now installs libvulkan-dev — libggml-vulkan.so needs libvulkan.so.1
  at link time.
- image renamed maven:latest -> mavenai:latest with pull_policy:never. "maven"
  is Apache Maven on Docker Hub; compose was silently pulling it, so every
  container ran mvn-entrypoint.sh and exited 127.

Verified on the host: all six build, five-service stack stays up, mavend opens
the encrypted db, mavweb GET :9201 -> 200.
2026-07-03 22:00:19 +04:00
kami 04c8dd1406 deploy: dockerize — one image, one container per daemon
Compose stack replacing start-maven.sh's bare `&`-backgrounded processes.
Single multi-stage image builds all six daemons (CGO + prebuilt native libs
from deps/); compose runs one container each with a different command. Only
mavend mounts the encryption key (env_file, gitignored) and the db volume; the
modules mount just the shared unix-socket dir and read-only models — so the
"key-free modules" boundary is OS-enforced (separate namespaces), not just a
code convention. IPC stays unix-domain over a shared volume: zero code change,
paths move to /run/maven. Encrypted db at rest on a named volume, decrypted
working copy in tmpfs (RAM) per the at-rest encryption landed earlier.

Validated: `docker compose config` clean, mavend.json parses, all daemon flags
confirmed. NOT build-tested (no docker/GPU in authoring env) — deploy/README.md
lists the host-dependent tweak points (GPU passthrough, onnxruntime path,
cross-container voice bind, netdata host).

Chosen Docker over interim systemd units per the "dockerize soon" call — no
throwaway supervisor built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 21:38:21 +04:00