Open msgvault.beachlab.org and sign in with
the existing Authentik passkey. Access is assigned only to
fran. Nginx protects the UI, API, health endpoint and embedded assets; the
backend listens only on 127.0.0.1:8096.
The NUC runs 0.21.0, from the official Linux amd64
release.
The downloaded archive's SHA-256 matched
56b2438eab14f3cadd15b6edecc4ea30454b6584c1039c2d5593a0afa77d7497.
The binary includes the browser application; no separate Node process is needed
(0.21.0 Web UI).
On 2026-10-12 the NUC returned from the local email/discovery builds to this unmodified official executable. The existing archive, profiles and summaries were retained. See the current runtime and return checkpoint. The three local extension sections below are historical recovery information; their patches and activation commands are not part of the active installation.
Installation on 2026-10-10 initialized an empty archive. On 2026-10-11,
the German iCloud account was connected and its first
full sync completed: 2,974 messages and 868 attachments, with automatic
sync every 15 minutes. Source: NUC daemon-backed sync and source status,
the independent read-only IMAP UID inventory, and MCP get_stats / query_sql.
The subsequent English topic filing organizes
the iCloud history and provides a source-specific Inbox view. The separate
daswerklab@gmail.com Gmail connection is now active as
source 2, with 250 messages imported and semantically indexed at the verified
2026-10-11 readback. Both accounts sync every 15 minutes. The Google grant
includes the owner's requested ability to move approved junk to Gmail Trash;
the MCP transports retain read-only tools.
/opt/msgvault/0.21.0/msgvault
/usr/local/bin/msgvault -> /opt/msgvault/0.21.0/msgvault
/opt/msgvault/deployment/
/opt/msgvault/backups/
/var/lib/msgvault/config.toml
/var/lib/msgvault/mcp.toml
/var/lib/msgvault/msgvault.db
/var/lib/msgvault/attachments/
/var/lib/msgvault/analytics/
/var/www/msgvault-info/index.html
/etc/systemd/system/msgvault.service
/etc/systemd/system/msgvault-mcp.service
/etc/systemd/system/msgvault-backup.service
/etc/systemd/system/msgvault-backup.timer
/etc/nginx/sites-available/msgvault.beachlab.org
/etc/nginx/snippets/msgvault-api-key.conf
/etc/letsencrypt/live/msgvault.beachlab.org/
/etc/letsencrypt/renewal-hooks/deploy/50-msgvault-nginx
/usr/local/bin/msgvault-mcp-client
/opt/msgvault/mcp-venv/
/usr/local/sbin/msgvault-backup
/var/lib/msgvault-backup/
/var/lib/msgvault/backup-staging/
/mnt/nas-downloads/backups/msgvault-restic/
The service runs as msgvault, with a private data directory (0700) and
configuration/database files (0600). Systemd restarts it after failure and
starts it at boot. Limits are 2 GiB RAM and two CPU.
Nginx adds the backend API key only after Authentik authorizes the request.
The include containing that key is root-only (0600), and the key remains out
of Git. This avoids a second login in msgvault. Only localhost is listed in
server.trusted_proxies, so the daemon recognizes the forwarded HTTPS scheme.
Configuration reference:
0.21.0 server settings.
API-key authentication skips msgvault's browser-session CSRF checks, as shown in 0.21.0 csrf.go. The Nginx configuration therefore rejects mutations with a foreign Origin or cross-origin Fetch Metadata before adding the key. It replaces forwarded host and scheme headers and clears client Authorization and Forwarded headers.
The REST API is active on 127.0.0.1:8096. Its OpenAPI document is at
/openapi.json; the installed 0.21.0 daemon reports API schema 3.0.0.
GET /api/v1/settings reads configuration and PATCH /api/v1/settings changes
editable settings. The public HTTPS routes still require the Authentik session,
including requests with an API key. Local scripts use the daemon's owner key
over loopback or SSH. See the
0.21.0 API reference.
msgvault-mcp.service exposes a separate, private MCP endpoint at
http://127.0.0.1:8097/mcp. It runs without --http-allow-writes, so deletion
staging, attachment export, Saved View changes and profile writes are absent.
The inbound MCP key differs from the owner REST key. Its private mcp.toml
uses [remote] to authenticate to the existing daemon; the MCP key itself
cannot authenticate to the owner REST API (verified 401).
Source: 0.21.0 MCP transport and write controls.
The stdio bridge reads its credential on the NUC. Neither client configuration contains a key:
| Client | Registration | Connection |
|---|---|---|
| Mac Codex | ~/.codex/config.toml, server msgvault |
SSH pink-sudo, then sudo -n -u msgvault /usr/local/bin/msgvault-mcp-client |
| Watson | /home/pink/.openclaw/openclaw.json, mcp.servers.msgvault |
Same local stdio client, with codex.agents = ["main"] |
The OpenClaw agent scope applies to native Codex projection; it is not a policy
for other runtime adapters. Mckenzie's existing group:plugins denial was
preserved; configured MCP tools are owned by the bundle-mcp plugin
(tool policy).
Existing model, channel and other agent settings were preserved. Templates are
codex-mcp.toml and
openclaw-mcp.json.
Sources: Codex MCP configuration and
OpenClaw registry and agent scope.
The bridge uses mcp-proxy 0.13.0 and MCP Python SDK 1.30.0 in its
private virtual environment. All resolved versions are pinned in
mcp-requirements.txt.
install-mcp.py installs the endpoint and
bridge on an existing Msgvault host; it refuses existing MCP installation files.
OpenClaw 2026.9.9 initially rejected the entire tool list because query_sql
declares oneOf two object output schemas without an explicit root type.
mcp-proxy-compat.py adds the redundant
type: object only for that schema, preserving both branches and all results.
The adapter leaves installed Msgvault/OpenClaw packages unchanged. The original
diagnostic and successful probe are retained privately in Watson's
~/.openclaw/backups/msgvault-mcp-20261010T193147Z/. Check for later changes
before recovery.
Check the endpoint and bridge on the NUC:
sudo systemctl status msgvault-mcp
sudo journalctl -u msgvault-mcp -n 30 --no-pager
sudo /opt/msgvault/mcp-venv/bin/python /opt/msgvault/deployment/probe-mcp.pyThe probe checks authentication separation, absence of write tools,
get_stats, the corrected schema and a read-only SELECT 1 through stdio.
On 2026-10-10, the Mac's exact registered SSH command also initialized MCP,
listed 36 tools and called get_stats. OpenClaw's native probe succeeded with
38 tools (including its generated resource utilities), with no diagnostics.
These checks exercise the MCP transports and clients, not an AI conversation.
OpenClaw's saved-server mutations and live CLI probes require its gateway to
be stopped for exclusive state ownership. Use its supported CLI with the
managed Node runtime, then start the gateway again. Read-only mcp status and
mcp doctor without --probe can run while it is active. Client configuration
backups stay private under each client's backup directory; restore only this
server's entry after checking later changes. Compare parsed Codex configuration
before and after registration: this run's readback showed node_repl.args
missing. It was restored from the baseline, then all prior settings matched.
On 2026-10-11, message semantic search, semantic people search and automatic
people inference are active. Both systemd services are running; authenticated
/health reports ok, vector ready and DuckDB. The settings endpoints report
no pending restart, and the configured and running people-policy fingerprints
match. The initial vector generation was created against an empty archive,
and a synthetic MCP hybrid query completed successfully with zero results.
The subsequent iCloud import completed with 2,974 messages. The populated
index checks are recorded under Embedding input limits and recovery.
Email profile inference is evaluated separately under Automatic profiles from
email below.
The personal OpenAI key is msgvault-restricted, owned by Francisco Sanchez
in Default project, with Restricted permissions and expiration on
2027-01-01 (confirmed by the OpenAI Administration API on 2026-10-11).
Its creation form and saved key table were read back. Only model
listing, chat completions and embeddings were allowed. With that key,
GET /v1/models returned HTTP 200 and listed both configured models;
GET /v1/files returned HTTP 401 with explicit missing api.files.read
scope. The fixed synthetic embedding test subsequently returned HTTP 200,
1536 finite dimensions and six input tokens. The native people check returned
HTTP 200 for gpt-6-luna, with 60 input and 12 output tokens. No archive text
was needed for either capability check.
The approved Mac recovery copy is /Users/Papi/.config/msgvault/openai.env,
mode 0600 inside a 0700 directory. The daemon holds the same key in two native,
owner-only stores:
| Feature | Active configuration | Credential file on the NUC |
|---|---|---|
| Message and people embeddings | text-embedding-3-small, 1536 dimensions, https://api.openai.com/v1 |
/var/lib/msgvault/tokens/provider-credentials.json |
| Historical people inference; automation disabled | Profile openai, model gpt-6-luna, openai_chat, json_object |
/var/lib/msgvault/tokens/people-providers/openai.json |
Both NUC files were compared with the approved local copy without displaying
the secret and checked as mode 0600, owned by msgvault. The former service
account key was replaced in both live stores; its private Mac recovery copy
is /Users/Papi/.config/msgvault/openai-service-account-20261011.env.
The old cloud key has not been revoked. Provider keys are outside config.toml,
Git and command arguments. Embedding credentials are bound to the configured
endpoint origin; the people profile uses the native OpenAI preset. Sources:
0.21.0 stored provider credentials
and people provider lifecycle.
ai-settings.json is the applied,
active settings profile. Message embeddings run every 15 minutes and after
supported scheduled source syncs. The daemon timezone is Europe/Madrid;
the native people sweep default is 15 2 * * * (02:15), but the sweep and brief
automation are now disabled after the return to official. Native inference
budgets remain at their release defaults: up to 100 requests, one million
input tokens and 160,000 output tokens per run; up to 500 requests, five
million input tokens and 800,000 output tokens per day. No provider prices or
monetary caps were added. Review these limits before the first real run;
a local cost estimate is not an OpenAI billing cap. Sources:
recommended scheduling and budgets,
sweep defaults.
people-openai.json is the policy body
used to create the retained, inactive people provider. Its sources are conversation and
meeting text from 2023-01-01 onward. This includes all archived correspondence
currently linked to the eight selected profiles. allow_sensitive records the operator's
choice to permit private archive text; this release uses the same flag for
sensitive profile fields. Its saved check and consent belong to the historical
email-evidence program; reverify and consent to the official program before any
future activation for supported sources. The separate curated-person semantic
embedding consent and vector indexing remain active.
provider-declared is a posture assertion, not verification of zero retention.
Profile automation requires choosing people to track; conversation briefs
require their own enrollment. Eight profiles are selected for maintenance.
See the 0.21.0 policy boundary.
The native 0.21.0 identity scorer uses TypeSafe Jev at the fixed HTTPS endpoint
https://api.typesafe.ai/v1/systemone and the pinned model jev-1.13.0.
Its configuration is separate from the existing OpenAI profile provider.
Source: identity scoring configuration
and native review workflow.
The provider receives a bounded pair of observed participants: names, email addresses, phones, up to eight typed identifiers each, and identity evidence classes, values and source counts. Message bodies and private Notes are outside this packet. The native worker applies local blockers before a provider request. Scoring records suggestions and judgments; accepting a participant link remains a separate operation with a current review token. This upstream scorer evaluates already seeded candidates when invoked. The historical mailbox discovery extension below handled names and phones in email signatures. That extension is retired; the native candidate scorer remains configured and neither path accepts participant merges automatically.
identity-jev.toml sets a batch limit of 20 and the native threshold of 0.80. Its exact retention declaration references the TypeSafe hosted terms and privacy policy, reviewed 2026-10-12. The provider states it does not train models on inputs and retains personal data as reasonably necessary for its stated purposes; this setup does not assert zero retention.
Local credential entry uses /Users/Papi/.config/typesafe/jev.env, mode 0600,
with one TYPESAFE_API_KEY= assignment. The deployment copy is
/var/lib/msgvault/tokens/identity-jev.env, owned by msgvault, mode 0600.
identity-jev.conf supplies it only to
the serving daemon as an EnvironmentFile. The existing encrypted NAS backup
captures both this token file and the daemon drop-in. Credentials never enter Git.
The upstream extras selection
includes the complete tokens directory when --include-tokens is set.
After securely installing the private env file on the NUC, place these three deployment files together in a private directory and run:
sudo python3 install-identity-jev.pyinstall-identity-jev.py rejects conflicting configuration or drop-ins, verifies the pinned model and a synthetic Noul response, completes an encrypted NAS backup, preserves a private checkpoint, activates the configuration and records native consent to its exact disclosure. It checks live readiness and unchanged existing provider credential hashes; activation failure restores its own configuration changes and service drop-in. It neither changes the binary nor runs archive scoring by itself.
For diagnosis of the upstream candidate scorer, use the msgvault_cli wrapper
under Maintain (the unattended discovery worker does not need these commands):
msgvault_cli person scoring status --json
msgvault_cli person scoring run --limit 20 --json
msgvault_cli person scoring history --limit 20 --json
msgvault_cli identity matches list --jsonNo scored candidate can replace missing identity evidence or overcome local
blockers for a shared contact point. The Geier professional address and the
GMX mailbox with a joint Edwin Geier & Thomas Happel signature were observed
as separate participants on 2026-10-12 (MCP search_people, and messages 2378
and 2707). Activating the scorer alone does not establish an exclusive-person
binding for that shared mailbox.
At initial Jev activation on 2026-10-12, the native scorer reported enabled,
credential_available, consent_active and ready as true, using
jev-1.13.0, threshold 0.80 and batch limit 20. Both Msgvault services are
active and the then-current 0.21.0-email-2 binary was unchanged. The synthetic
provider request returned the exact pinned model and a valid Noul answer;
the model-list endpoint exposed only jev-latest and jev-preview, so the
installer verifies the exact-version request instead of substituting an alias.
The first native batch completed without errors with processed = 0; the
participant-link count stayed at one, and Geier's profile then contained only
his professional address. That first activation enabled scoring with no current
candidate to evaluate; it did not discover the other mailbox. Primary private
receipts: /var/lib/msgvault-backup/identity-jev-20261012T080947Z/activation.json
and first-batch-summary.json; registered MCP status, judgment history and
profile reads confirmed the saved state.
The final encrypted NAS snapshot is ca9e56bc; restoring the Jev service
drop-in matched its live SHA-256, and the NAS staging payload was removed.
Receipt: nas-final-proof.json in the same private checkpoint directory.
Retired on 2026-10-12. The discovery timer, service, runner and opt-in were removed from their active locations. The already recorded contacts, labels and auxiliary evidence tables remain in the archive; the official daemon does not run this extension.
The local native build v0.21.0-discovery-3 retains the email extraction patch
and adds mailbox-discovery-v0.21.0.patch
on pinned upstream commit 598ca5cc89042ded95e484b5315cc29b9342fa42.
The implementation lives in the Msgvault daemon (internal/mailboxdiscovery,
Store operations and /api/v1/identity/discovery/*); it is not a Codex task.
The native worker finds another sender's own footer containing a tracked
person's complete name and phone digits that match the person's known sender
signature. It examines Gmail/IMAP only, excludes owner-sent/deleted/forwarded
messages, strips quoted replies (including decorated GMX Gesendet:/Von: headers)
and requires a signature name line, not a name
mentioned in prose. Body hydration is by message primary key; list queries do
not join message_bodies. A maximum of 5,000 metadata rows are read per run;
a persistent cursor rotates larger archives and holds a page when the provider
request limit is reached. Anchors use the last 16 own messages per bound sender.
The initial scope is the existing tracked profiles (at most 1,000 per scan).
A missing own signature, complete name or matching phone is left unresolved.
Jev jev-1.13.0 receives a separate mailbox-signature-packet-v2: tracked name,
known/candidate sender emails, one own signature each (768 characters maximum),
matching phone digits and evidence message IDs. Private message prose and quoted
threads do not enter this packet. This broader signature disclosure has its own
native consent fingerprint, independently revocable at
POST /api/v1/identity/discovery/revoke; it keeps the existing TypeSafe retention
declaration. The consent gate runs again immediately before the provider request
and before publishing a contact point. API calls reject redirects and validate
the exact returned model and both probabilities.
An ordinary related contact requires probability >= 0.95. A possible shared
contact instead requires related probability >= 0.50, shared probability >= 0.80
and an independently matching mobile number in both own signatures, in addition
to the complete-name match. This is a possible contact point, not an exclusive
identity judgment. The native transaction saves source=extraction, the actual
related confidence and an audit reference; it labels shared contacts
posiblemente compartido. It does not bind the
candidate participant, merge identities, replace the preferred address or assign
all messages from a household mailbox to one person's profile. Existing curated
values and superseded addresses win; changed/deleted evidence, untracking or a
profile revision during scoring prevents publication. A shared address is a
possible contact for this person, not proof of who authored a particular email.
msgvault-mailbox-discovery.timer runs on the NUC shortly after activation/boot and every 15 minutes, with up to 30 seconds of jitter. mailbox-discovery.py only calls the native daemon over loopback and logs counts. It does not read email or call Jev. It supports the NUC system Python 3.10 without extra dependencies. Each run permits eight provider requests, with a durable ceiling of 40 per UTC day across all profiles. Successful and inconclusive judgments are cached by person/evidence/disclosure fingerprint. Provider errors retry after 24 hours; an interrupted pending claim becomes eligible after five minutes. More evidence or a policy change can produce a new fingerprint. No manual batches are needed.
The setup requires the already configured private Jev key and a tested native
build. build-mailbox-discovery.sh
verifies upstream, Go and Bun archive hashes, applies both patches without fuzz,
runs go fmt, go vet, native regression tests (including the serving adapter)
and builds on Linux amd64.
install-mailbox-discovery.py
requires the verified previous daemon and backup hashes, performs an encrypted
NAS backup first, preserves a private activation checkpoint, switches the binary,
records the separately authorized signature consent and enables the timer. It
preserves the existing config and credential files and rolls back its own runtime
files if activation fails. backup.py also captures
the runner, timer/service, drop-in and complete recovery sources on the NAS, then
removes its temporary staging only after a verified encrypted snapshot.
Verified on 2026-10-12: v0.21.0-discovery-3 is serving on the NUC;
its SHA-256 is f4b5b18271d014b2310a6b297206b0c77d858c4d84c2ce23ecb23e1426daab95.
The timer triggered the native worker at 09:40:16 UTC and finished successfully:
2,825 scanned messages, one candidate, one Jev request and one shared contact
saved. MCP get_person_profile(person_id=8) returned both mailboxes; the new
point has source=extraction and type_label=posiblemente compartido, while
the professional address remains preferred. The native journal retained actual
related/shared probabilities 0.76/0.91 and evidence IDs 2232/2707. Confidence is
not upgraded to a confirmed identity. Participant 9 remains separate; person 8
is still bound only to participant 54, and the existing participant-link count
stays one. All 2,837 original archived message/body hashes were unchanged.
The v2 packet contains only the own footer, with the GMX quoted reply removed.
Primary private receipts are activation.json, verified-history.json and
runtime-proof.json under
/var/lib/msgvault-backup/mailbox-discovery-20261012T094014Z/.
The previous v1 discovery consent was revoked after v2 activation; its audit
was retained. These observations are deployment evidence, not guarantees about
unseen signatures or who authored mail from a shared address.
Retired on 2026-10-12. Existing brief versions and their citations remain readable through the official API/MCP. New shared-mail messages no longer trigger this specialized summary worker.
The native v0.21.0-signed-brief-1 extension uses the existing Jev-discovered
contact as a candidate mailbox. It requires a personal closing in each message's
own text, naming the tracked person alone or together with another signer. A
joint address footer by itself, another person's closing, quoted/forwarded
replies, owner mail and unsigned messages do not qualify. A first name qualifies
only at a personal closing in this already related mailbox. This is signing
text evidence, not cryptographic authentication.
The native patch indexes eligible message IDs, their body hashes and individual/joint authorship. The daemon rechecks current text, tracking, brief enrollment, the active contact and its consented Jev judgment before hydration and citation alignment. Body edits, removals and membership changes wake the native brief journal. An edit fails closed before the next index pass. This read scope exists only during brief assembly: shared-mail evidence cannot become extraction claims or a participant binding. The primary address and shared label retain their existing meaning. Joint authorship is supplied to the brief model explicitly.
The existing NUC discovery timer also wakes this native brief worker every 15 minutes, after archive ingestion. Changed signed mail publishes work for tracked and enrolled people. The watched scope uses a one-minute minimum brief interval; other people's configured cadence stays in place. Unchanged evidence makes no new model call. Existing provider consent, leases, retry backoff and usage caps still apply. Jev handles mailbox relations; the configured OpenAI profile provider continues generating prose. Python only sends the loopback wake-up and logs counts; Codex and the Mac are not required for ongoing work.
build-signed-mail-brief.sh
applies the email, discovery and signed-brief patches to the pinned upstream
source and runs native checks on Linux amd64.
install-signed-mail-brief.py
requires the verified discovery-3 binary and previous runner, takes an encrypted
NAS backup, preserves configuration/credentials and a rollback checkpoint,
then enables the same timer. Recovery sources stay under the already backed-up
/opt/msgvault/deployment/mailbox-discovery tree.
Verified on 2026-10-12: the NUC is serving v0.21.0-signed-brief-1, binary SHA-256
1624c33470cfc51d2b845b344b58d9da5b596ce1cd649236618225c5779cf3f0.
The timer fired at 18:35:20 UTC and the native worker generated Geier's brief
version 2 at 18:35:38 UTC using openai_chat, gpt-6-luna, program
v1-signed-mail-1. The input held 34 messages: 16 from the professional sender
and 18 signed GMX messages (15 individual, three joint), 55,827 input bytes.
The new brief cites GMX message 2695 for the September 20 meeting and professional
message 2707 for the Phase 2 discussion. Unsigned GMX message 2829 is excluded.
The first timer response reported 18 signed changes and one brief, with zero
new Jev requests because the mailbox judgment was cached. A separate validation
wake-up with unchanged mail reported zero signed changes, zero Jev requests and
zero briefs; the next calendar trigger was observed at 18:45:13 UTC.
All 2,840 pre-upgrade message/header/body/raw-content hashes matched after the
native update. Person 8 still has only participant 54, participant 9 remains
unbound, and the global participant-link count remains one. MCP readback kept
the preferred professional address and the GMX posiblemente compartido label.
Primary private receipts: /var/lib/msgvault-backup/signed-brief-20261012T183418Z/
(activation.json, archive-before.json, runtime-proof.json). These are
runtime observations, not conclusions from a Codex-written profile.
The final encrypted NAS snapshot is 777aaeb7, completed at 18:38:48 UTC.
Eight restored runtime/recovery files matched their live SHA-256, including the
new builder, installer, native patch and build manifest. Temporary NAS staging
was removed. Receipt: nas-final-proof.json in that checkpoint.
Retired on 2026-10-12. Existing facts and summaries are preserved. The active
official daemon has people.sweep.enabled=false and
people.sweep.brief.enabled=false; its Gmail/IMAP evidence policy is unchanged
from the official release.
The stock 0.21.0 extractor excludes Gmail and IMAP as subject-authored evidence. Enabling the provider alone therefore produces no email-derived profile facts. This restriction is implemented in sender attribution and the exact-subject gate.
The local native build v0.21.0-email-2 adds explicit email support through
people-email-evidence-v0.21.0.patch.
The systemd drop-in
people-email-evidence.conf sets
MSGVAULT_PEOPLE_EMAIL_EVIDENCE=1. Only tracked, sender-bound Gmail/IMAP messages
become seeds. The archived sender/profile binding is a declared identity, not
cryptographic authentication of the From header. Recipient-only and group
participation cannot assert that the person authored the text. Attachment
authorship keeps the upstream authentication requirement.
The email path removes quoted replies and common English, German, Spanish and French forwarding separators, retaining the sender's own signature. Extraction uses the native provider, typed catalog, evidence alignment, claim resolver, audit history and user-value protection. It accepts explicit facts and leaves unstated or ambiguous details empty. An organization address in a signature does not establish the person's residence. Briefs summarize the person's own messages; they cannot reconstruct owner replies or prove an old request is still pending. The extraction and brief programs have new fingerprints, so the configured provider must be checked and consented again after deployment.
The configured OpenAI profile uses json_object. OpenAI rejected the stock
extraction schema's untyped claims.items.value during the live check. JSON
object mode is supported by the
native driver;
the local parser still validates the closed output schema, typed values and
evidence. The patch also accepts an organization name supplied as a string,
normalizing it to the existing organization object before the same validation.
It preserves the original model output in the claim audit. Empty names,
wrong field types, duplicate fields and unknown fields remain rejected.
Build on Linux x86_64 with build-email-profiles.sh, passing a new output directory. It verifies the pinned upstream, Go and Bun archives, runs formatting, vet and the native evidence, sweep, brief and resolver tests, and compiles the embedded browser UI. Temporary build dependencies are removed on exit. It does not modify the live installation.
Deploy with activate-email-profiles.py
beside the drop-in. It requires the verified stock or previous local binary, validates the built
checksums, completes the existing encrypted NAS backup, records a private
checkpoint, sets only the provider's date bound and JSON object mode while the
daemon is stopped, switches the binary symlink and starts both services. Failed health
or credential checks restore the original binary and remove only its own
drop-in and its configuration changes. It preserves an already installed,
matching drop-in when upgrading the local build. The original release remains
at /opt/msgvault/0.21.0/msgvault.
After activation, use the native CLI wrapper described under Maintain:
msgvault_cli person provider reverify openai --yes --json
sudo systemctl restart msgvault.service msgvault-mcp.service
msgvault_cli person brief enroll PERSON_ID --track --json
msgvault_cli person sweep run --backstop --limit 8 --json
msgvault_cli person sweep status --jsonEnroll profiles with sender-authored evidence separately. Keep recipient-only profiles tracked without brief enrollment: this release's manual idle-brief path can otherwise leave a retry for a profile with nothing to summarize. The daily 02:15 Europe/Madrid sweep maintains tracked facts and eligible briefs with the existing request/token budgets. Briefs retain the configured minimum interval of 168 hours. A run marked successful can still have no facts: check native request counts, evidence, claim decisions and the saved profile. A contact present only as a recipient has no self-authored evidence and retains its existing fields. Upstream upgrades require reviewing and rebasing the patch; a stock replacement does not inherit this email support from the drop-in alone.
Verified on 2026-10-12: eight profiles remain tracked, seven have generated
briefs, and the native model has projected six employment records across three
profiles with source = extraction. The recipient-only profile stays tracked
without a brief. The incremental run completed with zero provider requests;
the queue has no dirty, leased or retrying work. The original 2,827 message
content hashes matched after inference. Later imports continued normally,
and live semantic coverage reached 2,831/2,831 messages, generation 3, ready.
Private primary evidence is retained under
/var/lib/msgvault-backup/email-profiles-first-run-20261011T220915Z/ and
/var/lib/msgvault-backup/email-profiles-reprocess-20261011T222857Z/;
activation checkpoints record the binary and credential hashes. The four
native test packages and embedded UI asset validation passed on the deployed
build. Registered MCP reads confirmed the saved briefs and employment records.
Encrypted backups also capture the daemon's regular systemd drop-ins, including
the email evidence opt-in, so restoring host configuration preserves it.
MCP discovers the daemon's vector capabilities at startup; its separate
client-facing configuration does not need a copy of the vector settings.
Restart both services after changing those capabilities. After activation,
the HTTP and stdio probes each listed 37 read-only tools, preserved owner
REST/MCP authentication separation and called get_stats successfully. Codex's
registered semantic_search_messages tool also completed a hybrid query against
active generation 1 (text-embedding-3-small, 1536 dimensions). Sources:
pinned MCP capability wiring
and the live probe and connector responses.
The first generation required embeddings build --full-rebuild --yes; the
incremental command reported that no active generation existed. It scanned
zero messages and activated generation 1 without inserting synthetic archive
data. Later manual imports require an incremental build; supported scheduled
syncs and the 15-minute cron trigger embedding automatically. Use the CLI
wrapper under Maintain, which sets a working directory accessible
to the service user. Sources:
vector build and sync triggers.
The applied profile sets vector.embeddings.max_input_chars = 2000.
This is a limit per overlapping chunk, not a cutoff for the whole message;
long messages produce multiple embedding inputs. The document prefix remains
empty. OpenAI limits text-embedding-3-small inputs to 8192 tokens;
characters and tokens are different units, so the previous 32768-character
default could produce an oversized request. Sources:
OpenAI embedding model limits
and Msgvault chunk sizing.
Changing this limit changes the generation fingerprint. Save it through
PATCH /api/v1/settings with the current ETag, restart the daemon, then use
the Maintain wrapper:
msgvault_cli embeddings build --full-rebuild --yesThe native rebuild creates a new generation and activates it on completion;
the previous generation is retained. Vector and hybrid search report
index_stale while the configured fingerprint differs from the serving
generation. Full-text search remains available. Source:
Msgvault generation rotation.
Check both authenticated GET /api/v1/stats and
POST /api/v1/search/coverage with {"filters":[]} after the build.
missing_embeddings_total = 0 alone is insufficient: the worker also stamps
message-specific permanent HTTP 4xx failures so subsequent scheduled passes
skip them. A populated index is complete only when the eligible and embedded
counts agree. Sources: the installed OpenAPI getSearchCoverage contract and
Msgvault permanent-failure handling.
Verified on 2026-10-11: generation 1 had 2,922 of 2,974 messages indexed
(98.3%). The daemon journal identified exactly 52 distinct messages rejected
with HTTP 400 input too long. After reducing the chunk limit from 32768
to 2000 and running the native full rebuild, generation 2 activated in
162.2 seconds with 2,974 succeeded and zero failed. Authenticated coverage
reported 2,974 eligible / 2,974 embedded (100%, ready). All 52 previously
rejected message IDs have embeddings in generation 2. Its 6,583 chunks are
at most 2,000 characters each, and no message reached the 64-chunk ceiling
(maximum 35). The native truncated: 223 counter includes hard chunk boundaries
covered by overlap; it is not a count of dropped messages. Source:
Msgvault chunk accounting.
The logical hash of all message bodies, subjects and source identities matched
before and after the rebuild. The two provider credential files were unchanged,
both services reported active, and no restart was pending. A registered Codex
MCP vector search returned a result from generation 2. Live proof and the
private pre-change configuration are under
/opt/msgvault/backups/fragmentation-20261011T112151Z/; receipt.json records
the coverage, recovered IDs, content hashes and backup snapshots.
Settings were saved through the daemon API with the current strong config
ETag; the separate provider-store ETag and people credential's opaque revision
were used for credential writes. Immediately after these API writes, parsed
TOML comparison showed only the six intended changes: vector enablement, semantic people
enablement, embedding cron, post-sync embedding, people sweep enablement and
selected people provider. Unrelated settings were preserved. The subsequent
source import's configuration and data changes were also preserved; final
readback still showed the checked, consented people policy running without a
pending restart. The private pre-activation copy is /opt/msgvault/backups/ai-activation-20261011T094614Z/;
its synthetic-check.json records the successful native provider check.
The encrypted backup service succeeded after activation. Snapshot 5aa04599
was restored into an isolated private directory and passed the native integrity
check. At that point, recovered configuration and both credential files
matched the live files byte for byte; both active consent records were present
in the restored archive database. This snapshot predates the concurrent source
import and validates the initial AI setup, not populated-archive recovery.
The temporary restore was removed. The private receipt is
/var/lib/msgvault-backup/ai-active-restore-20261011T095118Z.json.
Earlier credential-only receipts remain under /var/lib/msgvault-backup/.
The service-account alternative was abandoned after assigning the saved
Msgvault AI role failed twice with Organization ... not found. The account
retained Member, and its former key could read /v1/files; the cause of that
assignment failure remains unverified. The operator approved the restricted
personal replacement. Before credit was added, a synthetic embedding request
returned HTTP 429, insufficient_quota and credit_balance_exhausted, while
Billing showed zero API credit. The same configured key now passes both
provider checks. No credit purchase was performed by this setup.
storage-settings.json is the
applied NUC profile. The settings API readback and authenticated /health
confirmed these values after restarting the daemon; DuckDB is active and
pending_restart is false.
| Setting | Value |
|---|---|
| Cache builder memory / threads | 512 MB / 1 |
| Cache builder temporary storage | 2 GB |
| Minimum automatic cache rebuild interval | 15 minutes |
| Beeper, Slack and Teams attachment size | 100 MiB per attachment |
| Discord attachment size | 50 MiB per attachment |
| Chat media participant threshold | 20 |
The media limits apply to future downloads from those four chat providers. They do not impose a Gmail/IMAP attachment cap or remove existing files. The daemon's systemd limit remains 2 GiB; a DuckDB buffer limit is not a total process memory limit. Sources: 0.21.0 analytics settings and media policy.
msgvault-backup.timer runs daily at 02:30 Europe/Madrid, with up to five
minutes of random delay and catch-up after downtime. Its separate encrypted
Restic repository is /mnt/nas-downloads/backups/msgvault-restic; the existing
server backup's script, timer, repository and retention were left intact.
Daily snapshots tagged msgvault keep 7 daily, 4 weekly and 6 monthly copies.
The job checks 5% of repository data nightly and all data on Sundays. Sources:
service,
timer,
runner, and
Restic 0.12.1 retention.
Each run first uses msgvault backup create to capture a coherent database,
attachments, deletion audit, configuration and OAuth tokens while the daemon
continues serving. The temporary capture is now created directly on the NAS at
/mnt/nas-downloads/backups/msgvault-staging/payload, mode 0700. It adds
mcp.toml, service units and Nginx access files, then encrypts the capture into
the separate msgvault-restic repository on the same NAS.
Msgvault 0.21.0's native repository has no encryption or retention; its temporary
repository contains private plaintext, including the selected credentials. The
explicit plaintext-secrets flag applies only to this private NAS capture. The
stage is removed after the encrypted snapshot, checks and retention succeed.
Source: 0.21.0 capture and restore contract.
The runner estimates space without assuming compression: the NAS must have room for both the temporary capture and its encrypted backup, plus 10 GiB free; the NUC retains a 10 GiB reserve for the live service and its caches. This is a backup preflight, not a total archive quota. It refuses a missing/wrong NFS mount or a changed repository identity. An upload failure retains the completed NAS capture for retry; an incomplete capture is preserved for operator inspection rather than deleted automatically. An uploaded capture whose cleanup fails is retried before any new capture. The guards and recovery-before-deletion tests are in test_msgvault_backup.py.
Native msgvault gc fixes its pre-delete database backup beside the live DB;
there is no destination flag in v0.21.0 gc.go.
Each daily backup collects exact msgvault.db.gc-backup-YYYYMMDD-HHMMSS recovery
files into the NAS stage and preserves them in a separate encrypted snapshot
tagged msgvault-gc-recovery. This tag is excluded from daily-current-state
retention so a later run on the same day cannot erase the sole pre-GC database.
These recovery snapshots remain until explicitly retired. Before deleting a
local GC file, the runner recovers every byte from that exact NAS snapshot and
compares size and SHA-256, then checks that the original file has not changed.
The temporary NAS stage is deleted after verification; a failed check preserves
the local original and the stage. Proof and per-file cleanup results are in
/var/lib/msgvault-backup/last-success.json. Source: runner.
The daemon can write only the staging directory on the NAS; the root backup
service also writes the encrypted repository. The staging parent is created
before restarting these units, and service-account write access is checked.
Upgrades must preserve any existing pending.json or old local payload:
the runner refuses an unexplained legacy capture instead of deleting it.
Verified on 2026-10-11 after moving staging to the NAS: daily snapshot
03827d7c and separate GC recovery snapshot 39dc8ca5 both exist.
The two local GC files (602,353,664 and 570,032,128 bytes) were removed only
after full recovery from the encrypted snapshot matched their SHA-256 and size.
A NAS-only restore using dump/tar --no-same-owner, full native repository
verification and native restore recovered 2 accounts, 2,825 messages/raw-MIME
rows and 898 attachment parts. SQLite integrity was ok; configuration and
both OpenAI credential files matched the live SHA-256 hashes. The temporary
capture and both restore attempts were removed. Primary private receipt:
/var/lib/msgvault-backup/nas-staging-restore-proof-20261011.json.
Deployment and unchanged-configuration evidence:
/var/lib/msgvault-backup/nas-staging-deployment-20261011.json.
The job reuses the existing root-only Restic password file. Its private NAS
recovery copy was compared successfully before installation; locations and
access are in Backups. No key values enter Git or command arguments.
The repository identity, last successful run and restore receipt are private
files under /var/lib/msgvault-backup/. Command failure diagnostics also stay
there, mode 0600. Original pre-profile configuration is retained under
/opt/msgvault/backups/storage-<UTC timestamp>/.
First installation on this existing NUC layout:
sudo python3 services/msgvault/install-backup.pyThe installer refuses existing backup paths and uses the settings API's current
strong ETag in If-Match so concurrent edits are rejected. It changes only
the profile's named settings. Contract: the installed OpenAPI patchSettings
operation and API reference.
Run and inspect:
sudo systemctl start msgvault-backup.service
systemctl list-timers msgvault-backup.timer
systemctl show msgvault-backup.service -p Result -p ExecMainStatus
sudo journalctl -u msgvault-backup.service -n 30 --no-pagerFor a restore test, run in a root Bash shell and restore into a new private NAS
directory. This Synology export rejects Restic's Lchown with operation not permitted; use dump and extract with NAS ownership, then verify every native
pack before restoring the database. This does not change export permissions.
set -euo pipefail
umask 077
backup_restore_dir=$(mktemp -d /mnt/nas-downloads/backups/msgvault-staging/restore-XXXXXX)
mkdir -m 700 "$backup_restore_dir/restic"
restic -r /mnt/nas-downloads/backups/msgvault-restic \
--password-file /root/.config/thebeachlab-backup/restic-password \
--cache-dir /var/lib/msgvault-backup/restic-cache \
dump latest --tag msgvault /mnt/nas-downloads/backups/msgvault-staging/payload \
| tar --extract --file - --directory "$backup_restore_dir/restic" \
--no-same-owner --no-same-permissions
msgvault --home /var/lib/msgvault --no-log-file backup verify \
--repo "$backup_restore_dir/restic/mnt/nas-downloads/backups/msgvault-staging/payload/archive" \
--all --jobs 1
msgvault --home /var/lib/msgvault --no-log-file backup restore \
--repo "$backup_restore_dir/restic/mnt/nas-downloads/backups/msgvault-staging/payload/archive" \
--target "$backup_restore_dir/archive" --jobs 1 --integrity-checkReview machine-specific paths in the restored config.toml before starting
any daemon from it. The live archive is not overwritten by these commands.
The separate host-files tree contains MCP and Nginx configuration for recovery.
Keep test directories private and remove only the test directory after review.
Snapshots from the previous staging layout require dumping their old
/var/lib/msgvault/backup-staging/payload path and contain the native repository
under restic/var/lib/msgvault/backup-staging/payload/archive instead.
See Restic restore
and the native restore contract above.
Verified on 2026-10-10: encrypted snapshot e26ba98c, full Restic data check,
native repository verification, temporary restore and SQLite integrity_check
all passed. Restored configuration and MCP access configuration matched the
originals by SHA-256; a wrong repository password was rejected. Restored
sources, messages and attachments each contained zero rows. This checks
the initial empty archive. A populated iCloud restore
passed on 2026-10-11 from encrypted snapshot 35144baa, including all
2,974 messages and 868 attachments. Both test and capture directories
were removed. Live proof: /var/lib/msgvault-backup/restore-proof.json, service
readback, authenticated /health, the MCP get_stats call and public HTTPS
302 into Authentik from the Mac.
Verified on 2026-10-12 at 19:40 UTC: /usr/local/bin/msgvault selects
/opt/msgvault/0.21.0/msgvault, SHA-256
2e00619b03f9914aaece4f6458299dc66bc9f6386856098ea9329c889d0a5a1e.
This matches the executable in the checksummed official release archive.
The guarded return-to-official.py
required a fresh encrypted NAS backup and a full official pack verification and
restore before changing the executable. The current msgvault.db, attachments,
vectors.db, configuration and credentials stayed in place; no old database
was restored over the live archive. All 40 protected tables and official
profile/brief API responses matched before/after. The archive retained 2,841
messages, 901 attachment records, eight people, seven current briefs and eight
total brief versions. Geier retained both emails, the GMX shared label, brief
version 2 and its citations.
Only the two people automation booleans changed in config.toml. The two patch
opt-in drop-ins and the discovery timer/service/runner were retired to the private
checkpoint. The native Jev credential drop-in, both account schedules, all vector
settings, the MCP bridge and encrypted NAS backup service/timer were retained.
The five auxiliary extension tables were left intact for evidence and recovery.
A real MCP vector query returned results from active generation 3 using
text-embedding-3-small, 1536 dimensions and the exact accelerator. Native
embeddings list reported 2,841 / 2,841 embedded, zero missing, with the
unchanged 2,000-character chunk policy.
The pre-return snapshot was created and fully restored as bd350a01. It was
then retagged to protected snapshot ddeff6ac, preserving exactly the same
Restic tree, under msgvault-official-return-before-20261012T193944Z. This tag
is outside the daily job's --tag msgvault retention filter, so a later backup
on the same day cannot prune the return checkpoint. The temporary restore was
removed. The original patched executable remains available for rollback.
The supervised backup was then run again with the official executable. It
completed at 19:44:25 UTC, encrypted snapshot c744b924, with
Result=success and ExecMainStatus=0. Its capture.json, read back from the
encrypted snapshot, identifies
msgvault v0.21.0. NAS staging and the pending-capture marker were removed;
the protected pre-return snapshot still exists after normal retention/pruning.
The existing daily 02:30 Europe/Madrid timer (up to five minutes of jitter)
remains enabled and active. Native Jev candidate scoring also remains ready,
with its existing consent and credential; the automatic signature scanner is
retired.
Primary private proofs on the NUC:
/var/lib/msgvault-backup/official-pre-restore-proof-ddeff6ac.json
/var/lib/msgvault-backup/official-return-20261012T193944Z/activation.json
/var/lib/msgvault-backup/official-return-20261012T193944Z/protected-before.json
/var/lib/msgvault-backup/official-return-20261012T193944Z/nas-checkpoints.json
/var/lib/msgvault-backup/official-return-20261012T193944Z/live-readback.json
The checkpoint also retains the previous configuration, extension files and their ownership/modes. The encrypted pre-return snapshot contains the previous configuration and credentials. Do not run the historical patch installers as part of official updates. This one-off return helper only accepts the two audited executables; it is not a generic upgrade tool. Ordinary future updates should use official release checksums and a fresh verified NAS backup while preserving the current archive home.
Sources: official release artifact comparison, full NAS restore, frozen SQLite row hashes, before/after profile API hashes and live native/MCP readback. Tests: seven official-return guards (including startup-failure rollback and concurrent file preservation), plus ten existing NAS-backup guards.
On the NUC (ssh pink-sudo):
msgvault_cli() {
sudo -n -u msgvault sh -c \
'cd /var/lib/msgvault && exec /usr/local/bin/msgvault --home /var/lib/msgvault --no-log-file "$@"' msgvault "$@"
}
sudo systemctl status msgvault
sudo journalctl -u msgvault -n 50 --no-pager
sudo systemctl restart msgvault
msgvault_cli list-accounts
msgvault_cli stats
curl http://127.0.0.1:8096/healthAlways use MSGVAULT_HOME=/var/lib/msgvault and the service user for archive
commands. The serving process owns database writes; daemon_auto_start=false
and daemon_auto_restart="never" leave lifecycle management to systemd.
serve initializes the database itself. init-db cannot bootstrap this
configuration while its supervised daemon is stopped.
The CLI proxy preserves the caller's working directory: invoking it from
/home/pink failed with chdir /home/pink: permission denied. The wrapper
above was verified from the SSH login directory. This host's sudo policy
rejects --chdir for the executable, so the service user's shell changes
directory before executing the native CLI.
Sources: installation readback and the
0.21.0 configuration reference.
Before updating or restoring, complete and verify the existing encrypted NAS backup, including the database, attachments, configuration and tokens. Keep the live archive home and its vector database in place when switching executables. Run the supervised backup while the daemon is healthy:
sudo systemctl start msgvault-backup.service
systemctl show msgvault-backup.service -p Result -p ExecMainStatus
systemctl list-timers msgvault-backup.timerThe supervised capture requires a healthy daemon. Recovery tests use new private NAS directories as documented under Backups; they never replace the live archive. Temporary capture and restore payloads are removed after verification.
Read the selected release's migration notes before opening this archive with a newer binary. No automatic binary updates are configured.
The source package is services/msgvault. install.sh installs the pinned native release, verifies its archive checksum, creates the service user and random API key, and starts systemd. It refuses existing installation paths or an occupied port. It is for a first installation, not an upgrade.
sudo bash services/msgvault/install.sh
sudo python3 services/msgvault/configure-authentik.py --applyThe Authentik script defaults to a read-only plan. It requires Fran's enrolled
passkey and the existing beachlab-passkey flow, creates only the msgvault
provider/application and its Fran binding, and adds that provider to the
embedded outpost while preserving other providers and its configuration.
The NUC's Authentik 2026.8.2 API schema was checked before provisioning.
Do not rerun the global Authentik configure.py for this service.
Sources: forward auth
and application access.
For the HTTPS host:
- Install
http-bootstrap.confas/etc/nginx/sites-available/msgvault.beachlab.org, enable its symlink, and runnginx -tbefore reloading. This serves only ACME challenges. - Issue the certificate with the existing Certbot account:
sudo certbot certonly --webroot -w /var/www/letsencrypt --cert-name msgvault.beachlab.org -d msgvault.beachlab.org --non-interactive. - Create
/etc/nginx/snippets/msgvault-api-key.confas root, mode 0600, containingproxy_set_header X-API-Key "<server.api_key>";with the current key from the data configuration. Do not paste the key into shell history. - Install
msgvault.beachlab.org.confover the bootstrap, validate and reload Nginx. Keep a private copy of the previous file for rollback. - Install
50-msgvault-nginxas the executable Certbot deploy hook. The existingcertbot.timerrenews the certificate; this hook validates and reloads Nginx only for msgvault's renewed lineage.
The first deployment's private backups are under /opt/msgvault/backups/:
the Authentik inventory, initial backend configuration and HTTP bootstrap.
Check current ownership/state before restoring them. A backend rollback to
the first configuration would reopen the initial Tailscale listener.
Verified on 2026-10-10: systemd enabled/running, loopback listener, SQLite
PRAGMA quick_check returned ok, DuckDB cache initialized, and the public
certificate matched msgvault.beachlab.org. Unauthenticated requests to the
UI, API, health, OpenAPI and assets returned a redirect into Authentik, including
requests presenting the backend key or forged identity/forwarding headers.
Foreign-origin mutations returned 403. Direct Tailscale access to port 8096
failed after moving the listener to localhost.
The provider uses the existing WebAuthn-only flow with required user
verification, and its application has only Fran's access binding. Backend
requests carrying Nginx's key and HTTPS headers reported auth_mode=api_key,
https=true, no plain-HTTP warning, and zero archived messages/accounts.
Native Safari requested Touch ID for Fran's existing passkey, then loaded the
authenticated msgvault Settings page without a second API-key login.
Source: live Authentik API readback, NUC systemd/socket/SQLite checks, and HTTPS
requests from the Mac.