Publishes the M2-S1 contacts engine (nuncio-contacts: ContactsBackend/MockContactsBackend, DatabaseEngine::save_contact/list_contacts/get_contact) over the daemon's authenticated gRPC API, mirroring the Calendar vertical (story/m1-calendar-api) throughout.
Proto: adds service Contacts (Sync, ListContacts, GetContact, CreateContact) plus Contact/ContactEmail/ContactPhone messages to nuncio.v1. Golden descriptor regenerated; contract_stability::descriptor_matches_committed_golden passes.
Daemon: ContactsEngineOverrides + ContactsGrpcService in crates/nunciod/src/grpc.rs, mounted behind its ownBearerAuthInterceptor, threaded through serve_on_listener_with_overrides alongside the existing Mail/Calendar overrides. crates/nunciod/src/contacts_sync.rs mirrors calendar_sync.rs (pure fetch-and-persist, no production entry point since AccountConfig has no CardDAV fields yet).
Client: connect_contacts/AuthenticatedContactsClient in crates/nuncio-proto/src/client.rs, matching every other connect_* helper's shape and test coverage.
CLI: nuncio contact list/search/add/sync are now real gRPC clients (crates/nuncio-cli/src/runner.rs), replacing the old nuncio_contacts::ContactsDatabase::in_memory() throwaway-per-invocation database. nuncio-cli no longer depends on nuncio-contacts at all.
E2E: crates/nunciod/tests/contacts_e2e_test.rs boots a real daemon with an injected MockContactsBackend, drives Sync → ListContacts/GetContact, proves CreateContact persists visibly to a second, independent client connection (proving real daemon-side persistence, not process-local state), and asserts an unauthenticated call is rejected with Code::Unauthenticated.
Deviations from the issue's literal proto sketch (flagged for PO review)
Added CreateContact (not listed in the issue's message bullets). The acceptance criteria explicitly requires contact add to persist across CLI invocations, but there is no per-account CardDAV credential to write back to a real server (same gap Calendar has). CreateContact only ever writes to the daemon's own local SQLite 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 never pretending to write to the remote server, not about refusing to persist locally-authored data at all). Without this RPC, contact add could not honestly satisfy the acceptance criteria.
account_id is required on ListContactsRequest/CreateContactRequest (the issue sketched a bare ListContactsRequest {}), because the real DatabaseEngine::list_contacts/save_contact are account-scoped. ContactSubcommand::List/Search/Add/Sync all gained a --account flag to match (mirroring CalSubcommand).
No server-side search RPC.contact search fetches the account's full ListContacts result and filters client-side (display name / organization / email, case-insensitive) — documented in ContactSubcommand::Search's doc comment.
Timestamps omitted.Contact's created_at/updated_at/last_interacted_at are not on the wire — no real client use case identified for them yet; can be added additively later.
Representative counts from the full run: nuncio-proto 23 unit tests (incl. connect_contacts_* and the contract-stability golden check), nunciod lib 100 unit tests (incl. contacts_rpcs_reject_missing_bearer_token, contacts_sync_without_override_reports_honest_error, contacts_sync_with_injected_backend_persists_and_is_immediately_visible, create_contact_persists_and_is_immediately_visible, list_contacts_returns_only_genuinely_persisted_contacts, get_contact_reports_not_found_for_unknown_contact_id), nunciod/tests/contacts_e2e_test.rs 1 full-daemon E2E, nuncio-cli lib 29 unit tests (incl. contacts_rpcs_round_trip_over_grpc_to_a_stub_daemon). Local pre-commit hook (fmt + clippy + full test-all) passed cleanly on the commit.
Test plan
cargo fmt --all -- --check
cargo check-all
cargo test -p nuncio-proto (golden regenerated and passing)
Closes #188.
## Summary
Publishes the M2-S1 contacts engine (`nuncio-contacts`: `ContactsBackend`/`MockContactsBackend`, `DatabaseEngine::save_contact`/`list_contacts`/`get_contact`) over the daemon's authenticated gRPC API, mirroring the Calendar vertical (`story/m1-calendar-api`) throughout.
- **Proto**: adds `service Contacts` (`Sync`, `ListContacts`, `GetContact`, `CreateContact`) plus `Contact`/`ContactEmail`/`ContactPhone` messages to `nuncio.v1`. Golden descriptor regenerated; `contract_stability::descriptor_matches_committed_golden` passes.
- **Daemon**: `ContactsEngineOverrides` + `ContactsGrpcService` in `crates/nunciod/src/grpc.rs`, mounted behind its **own** `BearerAuthInterceptor`, threaded through `serve_on_listener_with_overrides` alongside the existing Mail/Calendar overrides. `crates/nunciod/src/contacts_sync.rs` mirrors `calendar_sync.rs` (pure fetch-and-persist, no production entry point since `AccountConfig` has no CardDAV fields yet).
- **Client**: `connect_contacts`/`AuthenticatedContactsClient` in `crates/nuncio-proto/src/client.rs`, matching every other `connect_*` helper's shape and test coverage.
- **CLI**: `nuncio contact list/search/add/sync` are now real gRPC clients (`crates/nuncio-cli/src/runner.rs`), replacing the old `nuncio_contacts::ContactsDatabase::in_memory()` throwaway-per-invocation database. `nuncio-cli` no longer depends on `nuncio-contacts` at all.
- **E2E**: `crates/nunciod/tests/contacts_e2e_test.rs` boots a real daemon with an injected `MockContactsBackend`, drives `Sync` → `ListContacts`/`GetContact`, proves `CreateContact` persists visibly to a **second, independent** client connection (proving real daemon-side persistence, not process-local state), and asserts an unauthenticated call is rejected with `Code::Unauthenticated`.
## Deviations from the issue's literal proto sketch (flagged for PO review)
1. **Added `CreateContact`** (not listed in the issue's message bullets). The acceptance criteria explicitly requires `contact add` to persist across CLI invocations, but there is no per-account CardDAV credential to write back to a real server (same gap Calendar has). `CreateContact` only ever writes to the daemon's own local SQLite 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 never pretending to write to the *remote* server, not about refusing to persist locally-authored data at all). Without this RPC, `contact add` could not honestly satisfy the acceptance criteria.
2. **`account_id` is required** on `ListContactsRequest`/`CreateContactRequest` (the issue sketched a bare `ListContactsRequest {}`), because the real `DatabaseEngine::list_contacts`/`save_contact` are account-scoped. `ContactSubcommand::List`/`Search`/`Add`/`Sync` all gained a `--account` flag to match (mirroring `CalSubcommand`).
3. **No server-side search RPC.** `contact search` fetches the account's full `ListContacts` result and filters client-side (display name / organization / email, case-insensitive) — documented in `ContactSubcommand::Search`'s doc comment.
4. **Timestamps omitted.** `Contact`'s `created_at`/`updated_at`/`last_interacted_at` are not on the wire — no real client use case identified for them yet; can be added additively later.
## Gate evidence
```
cargo fmt --all -- --check # clean
cargo check-all # clippy --all-targets --workspace -D warnings: clean
cargo test-all # all suites green, 0 ignored (see below)
```
Representative counts from the full run: `nuncio-proto` 23 unit tests (incl. `connect_contacts_*` and the contract-stability golden check), `nunciod` lib 100 unit tests (incl. `contacts_rpcs_reject_missing_bearer_token`, `contacts_sync_without_override_reports_honest_error`, `contacts_sync_with_injected_backend_persists_and_is_immediately_visible`, `create_contact_persists_and_is_immediately_visible`, `list_contacts_returns_only_genuinely_persisted_contacts`, `get_contact_reports_not_found_for_unknown_contact_id`), `nunciod/tests/contacts_e2e_test.rs` 1 full-daemon E2E, `nuncio-cli` lib 29 unit tests (incl. `contacts_rpcs_round_trip_over_grpc_to_a_stub_daemon`). Local pre-commit hook (fmt + clippy + full test-all) passed cleanly on the commit.
## Test plan
- [x] `cargo fmt --all -- --check`
- [x] `cargo check-all`
- [x] `cargo test -p nuncio-proto` (golden regenerated and passing)
- [x] `cargo test -p nunciod` (unit + `contacts_e2e_test`, `calendar_e2e_test`, `spine_e2e_test`, `grpc_auth_test`, `daemon_e2e_test`)
- [x] `cargo test -p nuncio-cli`
- [x] `cargo test-all` (full workspace, 0 ignored)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #188.
Summary
Publishes the M2-S1 contacts engine (
nuncio-contacts:ContactsBackend/MockContactsBackend,DatabaseEngine::save_contact/list_contacts/get_contact) over the daemon's authenticated gRPC API, mirroring the Calendar vertical (story/m1-calendar-api) throughout.service Contacts(Sync,ListContacts,GetContact,CreateContact) plusContact/ContactEmail/ContactPhonemessages tonuncio.v1. Golden descriptor regenerated;contract_stability::descriptor_matches_committed_goldenpasses.ContactsEngineOverrides+ContactsGrpcServiceincrates/nunciod/src/grpc.rs, mounted behind its ownBearerAuthInterceptor, threaded throughserve_on_listener_with_overridesalongside the existing Mail/Calendar overrides.crates/nunciod/src/contacts_sync.rsmirrorscalendar_sync.rs(pure fetch-and-persist, no production entry point sinceAccountConfighas no CardDAV fields yet).connect_contacts/AuthenticatedContactsClientincrates/nuncio-proto/src/client.rs, matching every otherconnect_*helper's shape and test coverage.nuncio contact list/search/add/syncare now real gRPC clients (crates/nuncio-cli/src/runner.rs), replacing the oldnuncio_contacts::ContactsDatabase::in_memory()throwaway-per-invocation database.nuncio-clino longer depends onnuncio-contactsat all.crates/nunciod/tests/contacts_e2e_test.rsboots a real daemon with an injectedMockContactsBackend, drivesSync→ListContacts/GetContact, provesCreateContactpersists visibly to a second, independent client connection (proving real daemon-side persistence, not process-local state), and asserts an unauthenticated call is rejected withCode::Unauthenticated.Deviations from the issue's literal proto sketch (flagged for PO review)
CreateContact(not listed in the issue's message bullets). The acceptance criteria explicitly requirescontact addto persist across CLI invocations, but there is no per-account CardDAV credential to write back to a real server (same gap Calendar has).CreateContactonly ever writes to the daemon's own local SQLite 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 never pretending to write to the remote server, not about refusing to persist locally-authored data at all). Without this RPC,contact addcould not honestly satisfy the acceptance criteria.account_idis required onListContactsRequest/CreateContactRequest(the issue sketched a bareListContactsRequest {}), because the realDatabaseEngine::list_contacts/save_contactare account-scoped.ContactSubcommand::List/Search/Add/Syncall gained a--accountflag to match (mirroringCalSubcommand).contact searchfetches the account's fullListContactsresult and filters client-side (display name / organization / email, case-insensitive) — documented inContactSubcommand::Search's doc comment.Contact'screated_at/updated_at/last_interacted_atare not on the wire — no real client use case identified for them yet; can be added additively later.Gate evidence
Representative counts from the full run:
nuncio-proto23 unit tests (incl.connect_contacts_*and the contract-stability golden check),nunciodlib 100 unit tests (incl.contacts_rpcs_reject_missing_bearer_token,contacts_sync_without_override_reports_honest_error,contacts_sync_with_injected_backend_persists_and_is_immediately_visible,create_contact_persists_and_is_immediately_visible,list_contacts_returns_only_genuinely_persisted_contacts,get_contact_reports_not_found_for_unknown_contact_id),nunciod/tests/contacts_e2e_test.rs1 full-daemon E2E,nuncio-clilib 29 unit tests (incl.contacts_rpcs_round_trip_over_grpc_to_a_stub_daemon). Local pre-commit hook (fmt + clippy + full test-all) passed cleanly on the commit.Test plan
cargo fmt --all -- --checkcargo check-allcargo test -p nuncio-proto(golden regenerated and passing)cargo test -p nunciod(unit +contacts_e2e_test,calendar_e2e_test,spine_e2e_test,grpc_auth_test,daemon_e2e_test)cargo test -p nuncio-clicargo test-all(full workspace, 0 ignored)