Publish the M2-S1 contacts engine over the daemon's authenticated gRPC
API, mirroring the Calendar vertical: add nuncio.v1.Contacts (Sync,
ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/
ContactPhone messages to the proto contract and regenerate the golden
descriptor.
Wire ContactsEngineOverrides + ContactsGrpcService into nunciod,
mounted behind its own BearerAuthInterceptor exactly like every other
service. ListContacts/GetContact always read genuinely persisted data
from DatabaseEngine; Sync fetches from an injected ContactsBackend
(MockContactsBackend in tests) and persists via
contacts_sync::sync_with_backend, mirroring calendar_sync.rs; with no
backend injected, production Sync returns an honest error since
AccountConfig carries no per-account CardDAV configuration yet.
Add connect_contacts/AuthenticatedContactsClient to nuncio-proto's
shared client helpers. Replace the CLI's Commands::Contact handlers
(previously a disposable in-memory ContactsDatabase.in_memory() per
invocation) with real gRPC calls against the daemon, so a contact
added via `contact add` persists and is visible to a later, separate
CLI invocation.
Deviations from the issue's literal proto sketch, flagged for review:
- Added CreateContact (not in the issue's message list): the
acceptance criteria requires `contact add` to persist a locally-
authored contact across CLI invocations, but the daemon has no
per-account CardDAV credential to write back to a real server. This
RPC only ever writes to the daemon's own local store -- never a
fabricated CardDAV write-back -- which is the same honest-surface
spirit as Calendar's "no create/delete" note (that note is about not
pretending to write to the remote server, not about never
persisting locally-authored data at all).
- ListContacts/CreateContact take a required account_id (the issue
sketched a bare ListContactsRequest{}), because the real
DatabaseEngine::list_contacts/save_contact are account-scoped;
ContactSubcommand::List/Search/Add/Sync now all take --account to
match.
- Contact search has no server-side RPC; `contact search` fetches the
full ListContacts result and filters client-side by display name,
organization, and email.
- Contact's created_at/updated_at/last_interacted_at are omitted from
the proto message -- no client use case identified for them yet.
Full-daemon offline E2E (contacts_e2e_test.rs) proves Sync -> List/Get
over the authenticated API, CreateContact's persistence surviving a
second, independent client connection, and an unauthenticated call
rejected with Code::Unauthenticated.