Closes#185. Wires the previously-orphaned nuncio-cal engine into nunciod as a real, authenticated gRPC vertical, mirroring the existing Mail vertical end to end (proto -> daemon -> CLI -> E2E).
Proto: added service Calendar (Sync / ListEvents / GetEvent — no create/delete) plus CalendarEvent, CalendarSyncRequest/Response, ListEventsRequest/Response, GetEventRequest/Response to nuncio.proto. Regenerated descriptor.bin; nuncio-proto's contract_stability::descriptor_matches_committed_golden test passes.
Client: connect_calendar / AuthenticatedCalendarClient in nuncio-proto/src/client.rs, with fail-closed / transport-error unit tests mirroring connect_mail.
Daemon: new nunciod::calendar_sync::sync_with_backend (pure fetch-and-persist against &dyn CalendarBackend, mirrors sync.rs's fetch_and_persist — deliberately no production entry point yet, since AccountConfig has no CalDAV fields to build a real backend from). CalendarEngineOverrides + CalendarGrpcService in grpc.rs, mounted behind its ownBearerAuthInterceptor in serve_on_listener_with_overrides (new calendar_overrides parameter; serve_on_listener/serve pass CalendarEngineOverrides::default(), so production behavior is unchanged for every existing caller).
CLI: real nuncio cal sync / nuncio cal list gRPC clients in runner.rs, replacing the fabricated "Calendar Events: 0 events found" / "calendar_sync_started" strings. CalSubcommand::List/Sync gained --account/--calendar/--start/--end flags (account is required; the rest have sensible defaults) since the RPCs need an account/window to query — there's no persisted "current account" concept to default from yet.
Tests: unit tests for calendar_sync::sync_with_backend, grpc.rs unit tests (list/get honest-data + not-found, sync honest-error-with-no-backend, sync-with-injected-MockCalendarBackend persists and is immediately visible, and calendar_rpcs_reject_missing_bearer_token), a CLI stub-daemon round-trip test, and a new offline full-daemon E2E (crates/nunciod/tests/calendar_e2e_test.rs) that boots a real daemon with an injected MockCalendarBackend, drives Sync -> ListEvents/GetEvent over the authenticated API, and asserts an unauthenticated call is rejected with Code::Unauthenticated.
Key behavior / honesty invariants
ListEvents/GetEventalways read genuinely persisted data via DatabaseEngine::list_calendar_events/get_calendar_event — never fabricated.
Sync with no injected backend (the real production path today) returns Status::internal("no CalDAV configuration exists for account '...'; per-account CalDAV configuration is not yet implemented") — never a fabricated synced_count. Only the test-injection seam (CalendarEngineOverrides::calendar_backend, standing in a MockCalendarBackend) exercises a working Sync.
Calendar is mounted behind its own BearerAuthInterceptor, exactly like every other service — see serve_on_listener_with_overrides's security doc comment and the new unauth-reject tests.
No unsafe/unwrap/expect/panic/todo in non-test code; all comments are intent-only (no story/GH/phase refs in .rs/.proto).
Gate output (all green, run locally + via the pre-commit hook)
CLI flags added to cal list/cal sync (--account required, --calendar default "default", --start/--end default 0/i64::MAX) rather than keeping them argument-free — the RPCs genuinely need an account/window to query and there's no "current account" concept to default from, so argument-free would have meant hardcoding a meaningless account id. Flagged in the issue as an acceptable choice.
ListEventsRequest.calendar_id is accepted but not applied as a store filter — DatabaseEngine::list_calendar_events only filters by account_id + time window (no per-calendar-collection column filter exists in the store today). Documented inline in CalendarGrpcService::list_events; not a fabrication (still returns every genuinely persisted event for the account/window), but a known gap for a follow-up story if per-calendar filtering matters before per-account CalDAV config lands.
No production run_account_sync-equivalent entry point was added for Calendar (explicitly out of scope per the issue) — Sync always errors in production until a follow-up story adds persisted per-account CalDAV configuration.
## Summary
Closes #185. Wires the previously-orphaned `nuncio-cal` engine into `nunciod` as a real, authenticated gRPC vertical, mirroring the existing `Mail` vertical end to end (proto -> daemon -> CLI -> E2E).
- **Proto**: added `service Calendar` (`Sync` / `ListEvents` / `GetEvent` — no create/delete) plus `CalendarEvent`, `CalendarSyncRequest/Response`, `ListEventsRequest/Response`, `GetEventRequest/Response` to `nuncio.proto`. Regenerated `descriptor.bin`; `nuncio-proto`'s `contract_stability::descriptor_matches_committed_golden` test passes.
- **Client**: `connect_calendar` / `AuthenticatedCalendarClient` in `nuncio-proto/src/client.rs`, with fail-closed / transport-error unit tests mirroring `connect_mail`.
- **Daemon**: new `nunciod::calendar_sync::sync_with_backend` (pure fetch-and-persist against `&dyn CalendarBackend`, mirrors `sync.rs`'s `fetch_and_persist` — deliberately **no** production entry point yet, since `AccountConfig` has no CalDAV fields to build a real backend from). `CalendarEngineOverrides` + `CalendarGrpcService` in `grpc.rs`, mounted behind its **own** `BearerAuthInterceptor` in `serve_on_listener_with_overrides` (new `calendar_overrides` parameter; `serve_on_listener`/`serve` pass `CalendarEngineOverrides::default()`, so production behavior is unchanged for every existing caller).
- **CLI**: real `nuncio cal sync` / `nuncio cal list` gRPC clients in `runner.rs`, replacing the fabricated `"Calendar Events: 0 events found"` / `"calendar_sync_started"` strings. `CalSubcommand::List`/`Sync` gained `--account`/`--calendar`/`--start`/`--end` flags (account is required; the rest have sensible defaults) since the RPCs need an account/window to query — there's no persisted "current account" concept to default from yet.
- **Tests**: unit tests for `calendar_sync::sync_with_backend`, `grpc.rs` unit tests (list/get honest-data + not-found, sync honest-error-with-no-backend, sync-with-injected-`MockCalendarBackend` persists and is immediately visible, and `calendar_rpcs_reject_missing_bearer_token`), a CLI stub-daemon round-trip test, and a new offline full-daemon E2E (`crates/nunciod/tests/calendar_e2e_test.rs`) that boots a real daemon with an injected `MockCalendarBackend`, drives `Sync` -> `ListEvents`/`GetEvent` over the authenticated API, and asserts an **unauthenticated** call is rejected with `Code::Unauthenticated`.
## Key behavior / honesty invariants
- `ListEvents`/`GetEvent` **always** read genuinely persisted data via `DatabaseEngine::list_calendar_events`/`get_calendar_event` — never fabricated.
- `Sync` with **no** injected backend (the real production path today) returns `Status::internal("no CalDAV configuration exists for account '...'; per-account CalDAV configuration is not yet implemented")` — never a fabricated `synced_count`. Only the test-injection seam (`CalendarEngineOverrides::calendar_backend`, standing in a `MockCalendarBackend`) exercises a working `Sync`.
- `Calendar` is mounted behind its own `BearerAuthInterceptor`, exactly like every other service — see `serve_on_listener_with_overrides`'s security doc comment and the new unauth-reject tests.
- No `unsafe`/`unwrap`/`expect`/`panic`/`todo` in non-test code; all comments are intent-only (no story/GH/phase refs in `.rs`/`.proto`).
## Gate output (all green, run locally + via the pre-commit hook)
```
cargo fmt --all -- --check -> clean, no diff
cargo check-all -> clippy --all-targets --workspace -D warnings: 0 warnings
cargo test-all -> 0 failed, 0 ignored across the workspace
nuncio-proto: 21 passed (incl. contract_stability::descriptor_matches_committed_golden)
nunciod (lib): 85 passed (incl. calendar_sync::*, grpc::tests::calendar_*, list_events_*, get_event_*)
nunciod (calendar_e2e_test.rs): 1 passed
nunciod (spine_e2e_test.rs, daemon_e2e_test.rs, grpc_auth_test.rs): all passing, unaffected
nuncio-cli: 28 passed (incl. calendar_rpcs_round_trip_over_grpc_to_a_stub_daemon)
nuncio-cli (cli_system_test.rs): 4 passed
nuncio-cal, nuncio-core, nuncio-store, nuncio-mail, nuncio-filter, nuncio-contacts: unaffected, all passing
```
## Decisions for PO review
1. **CLI flags added** to `cal list`/`cal sync` (`--account` required, `--calendar` default `"default"`, `--start`/`--end` default `0`/`i64::MAX`) rather than keeping them argument-free — the RPCs genuinely need an account/window to query and there's no "current account" concept to default from, so argument-free would have meant hardcoding a meaningless account id. Flagged in the issue as an acceptable choice.
2. **`ListEventsRequest.calendar_id` is accepted but not applied** as a store filter — `DatabaseEngine::list_calendar_events` only filters by `account_id` + time window (no per-calendar-collection column filter exists in the store today). Documented inline in `CalendarGrpcService::list_events`; not a fabrication (still returns every genuinely persisted event for the account/window), but a known gap for a follow-up story if per-calendar filtering matters before per-account CalDAV config lands.
3. No production `run_account_sync`-equivalent entry point was added for Calendar (explicitly out of scope per the issue) — `Sync` always errors in production until a follow-up story adds persisted per-account CalDAV configuration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Summary
Closes #185. Wires the previously-orphaned
nuncio-calengine intonunciodas a real, authenticated gRPC vertical, mirroring the existingMailvertical end to end (proto -> daemon -> CLI -> E2E).service Calendar(Sync/ListEvents/GetEvent— no create/delete) plusCalendarEvent,CalendarSyncRequest/Response,ListEventsRequest/Response,GetEventRequest/Responsetonuncio.proto. Regenerateddescriptor.bin;nuncio-proto'scontract_stability::descriptor_matches_committed_goldentest passes.connect_calendar/AuthenticatedCalendarClientinnuncio-proto/src/client.rs, with fail-closed / transport-error unit tests mirroringconnect_mail.nunciod::calendar_sync::sync_with_backend(pure fetch-and-persist against&dyn CalendarBackend, mirrorssync.rs'sfetch_and_persist— deliberately no production entry point yet, sinceAccountConfighas no CalDAV fields to build a real backend from).CalendarEngineOverrides+CalendarGrpcServiceingrpc.rs, mounted behind its ownBearerAuthInterceptorinserve_on_listener_with_overrides(newcalendar_overridesparameter;serve_on_listener/servepassCalendarEngineOverrides::default(), so production behavior is unchanged for every existing caller).nuncio cal sync/nuncio cal listgRPC clients inrunner.rs, replacing the fabricated"Calendar Events: 0 events found"/"calendar_sync_started"strings.CalSubcommand::List/Syncgained--account/--calendar/--start/--endflags (account is required; the rest have sensible defaults) since the RPCs need an account/window to query — there's no persisted "current account" concept to default from yet.calendar_sync::sync_with_backend,grpc.rsunit tests (list/get honest-data + not-found, sync honest-error-with-no-backend, sync-with-injected-MockCalendarBackendpersists and is immediately visible, andcalendar_rpcs_reject_missing_bearer_token), a CLI stub-daemon round-trip test, and a new offline full-daemon E2E (crates/nunciod/tests/calendar_e2e_test.rs) that boots a real daemon with an injectedMockCalendarBackend, drivesSync->ListEvents/GetEventover the authenticated API, and asserts an unauthenticated call is rejected withCode::Unauthenticated.Key behavior / honesty invariants
ListEvents/GetEventalways read genuinely persisted data viaDatabaseEngine::list_calendar_events/get_calendar_event— never fabricated.Syncwith no injected backend (the real production path today) returnsStatus::internal("no CalDAV configuration exists for account '...'; per-account CalDAV configuration is not yet implemented")— never a fabricatedsynced_count. Only the test-injection seam (CalendarEngineOverrides::calendar_backend, standing in aMockCalendarBackend) exercises a workingSync.Calendaris mounted behind its ownBearerAuthInterceptor, exactly like every other service — seeserve_on_listener_with_overrides's security doc comment and the new unauth-reject tests.unsafe/unwrap/expect/panic/todoin non-test code; all comments are intent-only (no story/GH/phase refs in.rs/.proto).Gate output (all green, run locally + via the pre-commit hook)
Decisions for PO review
cal list/cal sync(--accountrequired,--calendardefault"default",--start/--enddefault0/i64::MAX) rather than keeping them argument-free — the RPCs genuinely need an account/window to query and there's no "current account" concept to default from, so argument-free would have meant hardcoding a meaningless account id. Flagged in the issue as an acceptable choice.ListEventsRequest.calendar_idis accepted but not applied as a store filter —DatabaseEngine::list_calendar_eventsonly filters byaccount_id+ time window (no per-calendar-collection column filter exists in the store today). Documented inline inCalendarGrpcService::list_events; not a fabrication (still returns every genuinely persisted event for the account/window), but a known gap for a follow-up story if per-calendar filtering matters before per-account CalDAV config lands.run_account_sync-equivalent entry point was added for Calendar (explicitly out of scope per the issue) —Syncalways errors in production until a follow-up story adds persisted per-account CalDAV configuration.🤖 Generated with Claude Code