Repository navigation
ROADMAP
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.
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.
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.
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.
Next up: M6 — Security & release hardening (fail-closed encryption,
zeroized key material, WORM/ledger key-length validation, a rejection of
non-loopback gRPC binds plus an auth-coverage canary, a fail-closed updater on
missing SHA256SUMS, real CIDR/DNS SSRF checks for webhooks, inbound HTML
email sanitization). Then M7 (API freeze / client-readiness).
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).
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 remaining work is organized into 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.
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.
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.
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.
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.
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.
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.
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.
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.
Each milestone maps to GitHub issues (numbers 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.