17 KiB
Nuncio Roadmap (authoritative)
Purpose of this document: the single source of truth for where Nuncio is and the path from here to a frozen, client-ready backend — a solid engine, a reference CLI, and a published, versioned gRPC API that front-end repos (TUI, native macOS/Windows, MCP bridge) can begin consuming with contract-guaranteed parity.
It supersedes the Antigravity-era
PLAN-*,TODO.md,TICKET-*,EXECUTIVE-*documents (systematic fabrication — removed 2026-07-26, in git history only; do not trust them). Ground truth for the starting state: the 2026-07-26 forensic assessment. Governing decision:docs/adr/0001-engine-first-grpc-architecture.md. Phase 0/1 story-level detail lives indocs/BACKLOG.md.
North star
Nuncio is a local-first (sovereign) mail, calendar, and contacts engine: a
background daemon (nunciod) that owns all state, credentials, and protocol
logic, and publishes a versioned gRPC API over loopback. User interfaces are
separate native client projects that consume that API — they hold no
business logic and no data of their own. Feature parity across clients is a
property of the contract, not of anyone's discipline.
Definition of "done" (anti-theater guardrails)
These are non-negotiable and apply to every item below:
- A capability is done only when engine + proto + CLI command + offline E2E
test all exist together, and the local gate (
cargo fmt --all -- --check,cargo check-all(clippy-D warningsall-targets),cargo test-all) is green. - No fabricated success. A path that cannot do the real thing returns an
honest
Unimplemented/error — never canned data, never a fake "sent". - The
.protofiles are the contract. Thenuncio-protocontract-stability test fails on any accidental wire change; breaking changes go to a new package version, never a silent mutation ofnuncio.v1. - All external protocols are mocked in tests (
wiremock/Mock*Backend); live network calls in the test suite are forbidden. - "Done" is re-derived from tested behavior, never inherited from a label.
Where we are today (2026-08-07)
Phases 0–2 are complete and on dev. The daemon boots gRPC-only on loopback
127.0.0.1:9420 behind a keyring-minted bearer token, and serves eight
services, each mounted behind the auth interceptor: System, Accounts,
Mail, Filters, Calendar, Contacts, Export, Audit (the Calendar
and Contacts verticals landed in M1/M2). The IMAP→store→read→SMTP-send spine works
end-to-end over gRPC, driven by the nuncio-cli reference client, tested
offline. The legacy JSON-RPC IPC is removed. The nuncio.v1 contract is
published with a byte-deterministic FileDescriptorSet golden guarding it, plus
external-codegen docs for Swift/C#/TypeScript.
The feature surface — Phase 3 through M5 — is now COMPLETE, via milestones
M1–M5, all merged to dev (M1–M4 are already promoted to main; M5 promotes
next):
- 3.A Filter correctness — done (account scoping,
ON ACCOUNT, non-ASCII offsets,NOT IN). - 3.B Calendar — done (M1). Real CalDAV
REPORTtransport,TZIDfix,CalendarBackendtrait,calendar_eventsstore CRUD, theCalendargRPC service + CLI commands, and RRULE recurrence wired into the event-query path. - 3.C Contacts (CardDAV) — done (M2). A real, wiremock-tested CardDAV
client replaced the fabricated one; contacts persist through the daemon's
store and are exposed over a
ContactsgRPC service + CLI. - 3.D JMAP — done (M3). A real JMAP HTTP client is wired into the same
MailBackendfetch→store path as IMAP. - Filters made live — done (M4).
FilterEngine::evaluateis wired into the live sync path with a fire-once-per-new-message guard, theaccountcondition-field bug is fixed, unsupportedheader[...]conditions are honestly rejected, filter edit/export/import/logs moved onto theFiltersgRPC service, and aFilters.Triageserver-streaming RPC does retroactive bulk rescan. - Mail lifecycle & incremental sync — done (M5). Account lifecycle
(
UpdateAccount/RemoveAccount/TestAccountConnection) is real over gRPC, withimap_tls_mode/smtp_tls_modepersisted and actually driving the real IMAP (implicit/STARTTLS/plain) and SMTP connection. Sync is genuinely incremental: a real per-folder{uidvalidity}:{uidnext}checkpoint narrows the FETCH instead of re-fetching everything, a UIDVALIDITY change forces a safe full re-fetch (no silent mail loss), a per-item FETCH timeout replaces the indefinite hang, and aSyncProgressevent is emitted.SendMessagetakes an explicitaccount_id(honestAccountNotFound, no silent wrong-account fallback), andAddAccountrolls back an orphaned keyring secret if persistence fails.
M6 — Security & release hardening is nearly complete (13 of 16 stories
closed). Landed: fail-closed encryption, zeroized key material, WORM/ledger
key-length validation, non-loopback gRPC bind rejection plus the auth-coverage
canary, the fail-closed updater on missing SHA256SUMS, and real CIDR/DNS SSRF
checks on webhook egress. Still open: inbound HTML email sanitization (#204),
Rust static analysis in CodeQL (#250 — CodeQL currently scans JS and actions
only), and removal of assertion-free test theater (#251).
Work reorganized after M5 into three tracks that now carry the bulk of the remaining effort. These are GitHub milestones in their own right; they are not part of the original M1–M7 numbering:
- M6.5 — Feature completeness & de-fabrication (3 closed / 8 open).
Eliminates the remaining fabricated or silently-wrong paths: production-wiring
Contacts sync with per-account CardDAV config (#255), namespace-aware DAV XML
parsing to replace substring splitting and silent drops (#257), the
tls_mode_from_dbsilentImplicitTlsfallback (#271), theNaturalLanguageSchedulerthat returns wrong times (#258), plus CLI and audit cleanups. - WS-A … WS-F — the pre-freeze reshape (22 closed / 56 open). Six tracks
that must land before
nuncio.v1can be frozen: WS-A contract hardening (typed errors,Timestamp/Durationunification, keyset pagination, id conventions, enum hygiene, account transportoneof); WS-B mail model and mutations; WS-C sync, push and lifecycle; WS-D security to 9/10 (whole-DB SQLCipher encryption, OS-permissioned UDS / named-pipe IPC replacing loopback TCP, signed updates, HTML sanitization); WS-E calendar and contacts write-back plus structured search; WS-F ops, usability and maintainability, including the behavior-preservinggrpc.rs(#333) andDatabaseEngine(#334) decompositions. - OBS-1 … OBS-9 — observability — COMPLETE. All nine stories merged
(#357–#365; no milestone was assigned). Delivered: a layered
tracingsubscriber withEnvFilter, rotating file and JSON output; per-RPC spans with request-id correlation and auth accept/reject logging; domain and lifecycle coverage across handlers, syncs, outbox and shutdown; protocol-sync instrumentation for mail, calendar and contacts; a realSystem.GetStatushealth and readiness surface; CLI-v/-vvwith typedErrorInforendering; and a redaction policy with aRedacted<T>newtype plus a no-secrets-in-logs canary. Reference:docs/LOGGING.md.
M7 (API freeze) has not started — 5 stories open, 0 closed. It gates on WS-A finishing the contract reshape.
A handful of follow-ups were filed during M1–M5 and remain open, tracked as
their own backlog items rather than blocking completion: calendar
windowing/predicate refinement (#216), contacts notes handling (#219), a
filter edit bug where re-saving a rule silently re-enables it and resets
created_at (#225), and further mail sync/SMTP robustness polish identified
during M5 (#233). Also open and worth early attention: the ReDoS guard
evaluate_with_timeout exists but no production caller uses it, so the live
sync path still calls the unbounded evaluate (#370).
Explicitly deferred (out of scope until the backend is client-ready and each is re-justified as a genuine engine capability): OpenPGP/S-MIME E2EE (#79), WASM/QuickJS plugins (#80), local-LLM summarization (#81).
The path to client-ready
The original plan was seven milestones: M1–M5 complete the feature surface;
M6 hardens security; M7 is the API-freeze gate that declares the backend
ready for front-end repos. Milestones are ordered but M1–M4 are largely
independent (they touch different engine crates); the only hard serialization is
the shared nuncio.proto (and its golden), so proto additions land one at a
time.
That structure still holds, with one amendment made after M5: the gap between "the feature surface exists" and "the contract can be frozen" turned out to be larger than M6 alone, so M6.5, the WS-A … WS-F workstreams, and the OBS observability epic were opened between M6 and M7. They are described under M6.5, WS and OBS below.
M1 — Finish Calendar (closes #178)
Complete the Calendar vertical behind the API.
- Proto
Calendarservice (Sync/ListEvents/GetEvent— honest surface only, no fabricated create/delete), mounted behind the bearer-auth interceptor; regenerate the contract golden. - Daemon
CalendarGrpcService+ injection seam +calendar_syncmodule; wire the (currently orphaned)nuncio-calcrate intonunciod. - Wire
RecurrenceEngine(rrule) into the fetch/store/query path so recurring events actually materialize (currently dead code). - CLI
calendarcommands replacing the hardcoded stub; offlinesync → list → getE2E incl. an unauthenticated-rejection assertion. - Exit: create a rule-free Calendar view over gRPC end-to-end, offline-tested, recurrence expanded.
M2 — Contacts / CardDAV (closes #179)
Bring Contacts to the same bar as Calendar.
- Replace the fake
CardDavClient(returns a hardcoded contact) with a realwiremock-tested CardDAV client; vCard parse → model → regenerate round-trip. - Persist contacts through the daemon's store (today the CLI writes a fresh
in-memory DB per invocation — nothing persists); add a
Contactsproto service- daemon wiring + CLI + offline
sync → listE2E.
- daemon wiring + CLI + offline
- Exit: contacts sync and persist across restarts, driven over gRPC.
M3 — JMAP (closes #180)
Make JMAP a real second mail backend alongside IMAP/SMTP.
- Real JMAP HTTP client (session,
Email/query/get/set), camelCase wire fix, wired into the sameMailBackendfetch→store path;wiremock-tested. - Exit: a JMAP account fetches → stores → lists offline, via the existing Mail service.
M4 — Filters made live + complete (closes #175, #176, #181)
Today rules can be created, validated, and previewed as "MATCH" but never fire.
- Wire
FilterEngine::evaluateinto the real sync/orchestrator path so a synced message that matches a rule actually triggers its action (move/flag/ delete/forward/webhook — the outbox is built but never invoked end-to-end). - Fix
FilterField::Header(silently hardcodedfalse) and theeval_leafaccount-field bug (#181, readsfolder_id). - Re-expose filter edit/export/import/logs (#175) and bulk triage (#176) over the
Filtersservice (lost with the IPC). - Exit: an offline E2E proves a matching rule fires an action against synced mail; no silent no-match, no fabricated preview.
M5 — Mail spine completeness & correctness (closes #170, #169, #182, #183, #184)
Round out real account and sync management.
- Account lifecycle over gRPC: edit / delete / test-connection, with TLS-mode (implicit / STARTTLS / plain) actually persisted and wired (#182).
Mail.SendMessageexplicit account selection (#170); avoid orphaned keyring entries on partial account add (#169).- Incremental sync: honor
since_stateso sync isn't a full historical re-fetch every time (#184); sync progress reporting + bounded per-item stall timeout (#183). - Exit: full account lifecycle + incremental sync, offline-E2E-proven, no fabricated fallbacks in the IMAP path.
M6 — Security & release hardening (Phase 4; closes #162, #163, #164, #165)
The engine becomes production-solid.
- Fail-closed everywhere: fix
encrypt_text_at_restsilent empty-ciphertext data loss (#162); a fail-closed, signature-verifying updater (no apply whenSHA256SUMSis absent). - Key hygiene: zeroize engine key material on drop (#163); validate WORM/ledger key length from the vault (#164).
- Transport/egress: gRPC bind hardening + a guard that every service is wrapped in the interceptor (#165); real CIDR-based SSRF checks on webhook egress (replace the substring blocklist); sandboxed HTML email rendering pipeline for clients.
- Exit: a security review passes; no fail-open crypto, update, or egress path remains.
M6.5, WS and OBS — the work between M6 and M7
Opened after M5, when the reshape needed before an honest v1 freeze proved
larger than M6. Status as of 2026-08-12:
| Track | Closed / Open | Scope |
|---|---|---|
| M6.5 Feature completeness & de-fabrication | 3 / 8 | Remove the remaining fabricated or silently-wrong paths (Contacts sync wiring #255, DAV XML namespaces #257, TLS-mode fallback #271, NL scheduler #258) |
| WS-A Contract hardening (pre-freeze reshape) | 6 / 3 | Typed errors, Timestamp/Duration, keyset pagination, id conventions, enum hygiene, account transport oneof — gates M7 |
| WS-B Mail model & mutations | 1 / 14 | Message identity, drafts, folder management, attachments on receive |
| WS-C Sync, push & lifecycle | 4 / 9 | Autonomous scheduler, IMAP IDLE, resumable event stream, graceful shutdown |
| WS-D Security to 9/10 | 4 / 8 | Whole-DB SQLCipher (#299), UDS / named-pipe IPC (#305), signed updates (#300), HTML sanitization (#302) |
| WS-E Calendar/Contacts write & Search | 2 / 12 | DAV write-back with conditional PUT/DELETE, recurrence editing, structured Search service |
| WS-F Ops, usability & maintainability | 5 / 10 | CLI polish, System.Backup, and the grpc.rs (#333) / DatabaseEngine (#334) decompositions |
| OBS-1 … OBS-9 Observability | 9 / 0 | COMPLETE. Tracing subscriber, RPC spans + request-id, domain/lifecycle coverage, protocol instrumentation, GetStatus, CLI verbosity, redaction canary |
WS-B and WS-C grew when ADR 0002 decided
the convergent multi-engine sync model and broke it into #386–#397. Those are
in flight, not closed: the message/placement identity split, Message-ID and
content-hash capture, user_version gating, the raw IMAP command layer, the
four-rung change enumeration ladder, and three-state verified mutations are all
implemented and green on a branch, and the counts above move when it merges.
Still open behind them: the mutation RPCs and durable conflict surface (#395),
DAV href/ETag identity and sync-collection (#392, #393), the scheduler and
rate limiting (#396), and the ≥3-engine convergence harness (#397).
The OBS stories (#357–#365) were merged without a milestone assignment; the
epic is complete regardless, and docs/LOGGING.md is its reference.
M7 — API freeze & client-readiness gate ← the goal line
Declare the backend ready for front-end repos.
- Freeze
nuncio.v1: every backend capability is represented; the contract-stability golden covers the full surface; the versioning policy (additive-only in v1) is enforced by test. - Generated SDK / codegen docs verified for Swift, C#, and TypeScript; a minimal "hello, daemon" example per language.
- The CLI reference client exercises every RPC in offline E2E — the CLI is the proof the contract is complete and usable.
- Replace the gamed 100%-line coverage gate with a realistic engine-crate
threshold + a required mock-server E2E job (#143); CI green on
dev/main. - Tag a pre-alpha backend release whose notes state exactly what is real.
- Exit / definition of client-ready: the daemon publishes a frozen, documented, contract-tested gRPC API that the CLI fully exercises offline. At this point Phase 5 front-end repos may begin.
Phase 5 — Client projects (after M7)
Separate repositories consuming the frozen API — nuncio-tui (Rust),
nuncio-gui-macos (Swift + grpc-swift), nuncio-gui-windows (WinUI/C# +
Grpc.Net), nuncio-mcp (MCP↔gRPC bridge). Parity is guaranteed by the contract.
The deferred V2/V3 features (E2EE, plugins, local AI) are re-justified and
scheduled here or folded back into the engine as they earn their place.
Tracking
Each milestone maps to GitHub issues (numbers above); M6.5 and WS-A … WS-F are
GitHub milestones too, so gh issue list --milestone "<title>" is the live
status for any track in the table above. Every capability is built
via one implementer + a spec/quality review gate + an offline E2E, committed only
when the local gate (fmt + clippy -D warnings + tests) is green and the proto
golden is consistent. Issues close only after the review step — never on a green
build alone.