mavcaldav was built, in `make build`, listed in CLAUDE.md's daemon table, and
deployed nowhere. Not commented out the way mavmaild is, which at least
records the decision and the enable steps. Built and mentioned nowhere is the
worst of the three states, so this writes the decision down.
The box has no CalDAV account, so the block stays commented. It names what the
absence costs, because both costs are invisible from the daemon table. Agenda
questions route correctly and answer from nothing: stage 0 sends "что у меня
сегодня" to IntentQuery (V-498) and the calendar query source then reads facts
nobody writes. And loop.State.CalendarBusy is fed by those same facts, so the
gate's "do not nag mid-meeting" is permanently false.
CLAUDE.md said the absence was an oversight. It is a decision now.
compose set TZ=Europe/Samara and Go read it, so clock replies and quiet
hours were already local. But /etc/localtime in the image pointed at
Etc/UTC, so a caller asking the system zone instead of the environment
answered UTC. The reminder path shells out to python dateparser, which is
such a caller.
TZ is now a build arg on the runtime stage. It points the symlink, writes
/etc/timezone and sets ENV TZ, so the image is local on its own. Compose
passes the zone it already declares, so the zone stays written in one
place.
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
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
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
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.
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
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>
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>
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>
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).
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.
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>