fix(caldav): honor time range in list_events via server-side calendar… - #1147
fix(caldav): honor time range in list_events via server-side calendar…#1147thoscut wants to merge 2 commits into
Conversation
…-query The range parameter of CalDavClient::list_events was bound as _range and never used: the client always fetched every resource in the calendar, so the start/end parameters of the list_events tool had no effect on any server, contradicting the docs. list_events now issues a CalDAV calendar-query REPORT (RFC 4791) with a time-range filter nested inside comp-filter VCALENDAR > VEVENT — the only placement Nextcloud and other servers honor — and then fetches only the matching resources via calendar-multiget. Range bounds are parsed from ISO 8601 (offset, naive, or date-only) and serialized as UTC with a trailing Z (e.g. 20260101T000000Z) as the spec requires; naive inputs are treated as UTC. When range is None, behavior is unchanged. Date/time handling uses the time crate through a new re-export from moltis-agents (which already depends on it), so no new direct dependency or lockfile change is needed. The mock client applies equivalent client-side overlap filtering (new time_filter module) so tests exercise the range semantics: events fully outside the range are dropped, boundary-overlapping events are kept, all-day events span a full day, and events without DTEND are treated as instantaneous. A unit test also asserts the generated REPORT XML nests time-range under VCALENDAR > VEVENT with UTC Z timestamps. Claude-Session: https://claude.ai/code/session_012UDpqLhTib2oMkPj2roqaB
Greptile SummaryThis PR fixes a long-standing bug where
Confidence Score: 4/5The core fix is correct — the two-phase REPORT/multiget approach is the right CalDAV pattern and the UTC conversion is well-tested. The only concerns are a subtle boundary-semantics gap in the mock and a missing hint in the agent tool description. The implementation correctly issues a server-side calendar-query REPORT and only fetches matching resources. The to_ical_utc path is covered by unit tests including offset conversion. Two issues are worth noting: the mock's overlap predicate is slightly more inclusive than RFC 4791 (inclusive vs strict inequalities at exact boundaries), and the in-process agent description string doesn't warn that both start and end are needed for filtering to activate. crates/caldav/src/time_filter.rs — the event_overlaps function's boundary semantics vs RFC 4791
|
| Filename | Overview |
|---|---|
| crates/caldav/src/client.rs | Implements two-phase server-side filtering: calendar-query REPORT to get matching hrefs, then calendar-multiget. Logic is correct; the build_time_range_query helper and UTC conversion path are well tested. |
| crates/caldav/src/time_filter.rs | New module providing ISO 8601 → iCal UTC conversion (production) and test-only event overlap helpers. The overlap predicate uses inclusive boundaries rather than the strict inequalities defined in RFC 4791 §9.9.1, creating a latent divergence from real-server behaviour at exact boundaries. |
| crates/caldav/src/tool.rs | Wires the new range path into the agent tool and the mock client. The single-boundary silent fallthrough is documented in docs but the in-process tool description string doesn't warn about it. |
| crates/agents/src/lib.rs | Adds pub use time; re-export so the caldav crate can use the same time dependency without duplicating it. Change is minimal and correct. |
| crates/caldav/src/lib.rs | Adds mod time_filter; declaration (private, crate-internal). No issues. |
| docs/src/caldav.md | Updates list_events docs to describe server-side RFC 4791 filtering and the naive-as-UTC convention. Accurate and consistent with the implementation. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Tool as CalDavTool (tool.rs)
participant Client as LibDavCalDavClient
participant TF as time_filter
participant Server as CalDAV Server
Tool->>Tool: extract start + end from params
alt both start and end provided
Tool->>Client: list_events(calendar_href, Some(TimeRange))
Client->>TF: to_ical_utc(start), to_ical_utc(end)
TF-->>Client: YYYYMMDDTHHMMSSZ strings
Client->>Server: "REPORT calendar-query (VCALENDAR > VEVENT > time-range)"
Server-->>Client: matching resource hrefs
Client->>Server: REPORT calendar-multiget (hrefs only)
Server-->>Client: full iCal data for matched events
Client-->>Tool: Vec EventSummary
else only one or neither provided
Tool->>Client: list_events(calendar_href, None)
Client->>Server: REPORT calendar-multiget (entire collection)
Server-->>Client: all iCal data
Client-->>Tool: Vec EventSummary
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Tool as CalDavTool (tool.rs)
participant Client as LibDavCalDavClient
participant TF as time_filter
participant Server as CalDAV Server
Tool->>Tool: extract start + end from params
alt both start and end provided
Tool->>Client: list_events(calendar_href, Some(TimeRange))
Client->>TF: to_ical_utc(start), to_ical_utc(end)
TF-->>Client: YYYYMMDDTHHMMSSZ strings
Client->>Server: "REPORT calendar-query (VCALENDAR > VEVENT > time-range)"
Server-->>Client: matching resource hrefs
Client->>Server: REPORT calendar-multiget (hrefs only)
Server-->>Client: full iCal data for matched events
Client-->>Tool: Vec EventSummary
else only one or neither provided
Tool->>Client: list_events(calendar_href, None)
Client->>Server: REPORT calendar-multiget (entire collection)
Server-->>Client: all iCal data
Client-->>Tool: Vec EventSummary
end
Comments Outside Diff (1)
-
crates/caldav/src/tool.rs, line 211-220 (link)Single-boundary filter silently falls through to full fetch
When a caller passes only
startor onlyend, the_ => Nonearm discards the supplied value and fetches all events without any filtering. The markdown docs mention this ("If either is omitted, all events are returned"), but thedescription()string exposed to the AI agent model only saysstart/end (ISO 8601, optional)with no hint that both are required together. An agent that provides juststartto bound a range will silently receive every event in the calendar.
Reviews (1): Last reviewed commit: "fix(caldav): honor time range in list_ev..." | Re-trigger Greptile
The mock client's event_overlaps used inclusive inequalities (end >= range.start && start <= range.end), making it slightly more permissive at exact boundaries than a compliant CalDAV server. Per RFC 4791 §9.9 a time-range match is strict for events with a DTEND (range.start < event_end AND event_start < range.end) and only the lower bound is inclusive for instantaneous DATE-TIME events (range.start <= event_start < range.end). The looser mock could keep an event that a real server drops, letting tests pass against behavior production would not reproduce. Reimplement event_overlaps to branch on the RFC cases (explicit DTEND, all-day spanning one day, instantaneous) and drop the now-unused event_interval helper. Add tests pinning the boundary cases: an event ending exactly at range.start and one starting exactly at range.end are excluded, while an instantaneous event at range.start is included. Also clarify the caldav tool description and start/end parameter docs that server-side filtering only activates when both start and end are provided.
…-query
The range parameter of CalDavClient::list_events was bound as _range and never used: the client always fetched every resource in the calendar, so the start/end parameters of the list_events tool had no effect on any server, contradicting the docs.
list_events now issues a CalDAV calendar-query REPORT (RFC 4791) with a time-range filter nested inside comp-filter VCALENDAR > VEVENT — the only placement Nextcloud and other servers honor — and then fetches only the matching resources via calendar-multiget. Range bounds are parsed from ISO 8601 (offset, naive, or date-only) and serialized as UTC with a trailing Z (e.g. 20260101T000000Z) as the spec requires; naive inputs are treated as UTC. When range is None, behavior is unchanged.
Date/time handling uses the time crate through a new re-export from moltis-agents (which already depends on it), so no new direct dependency or lockfile change is needed.
The mock client applies equivalent client-side overlap filtering (new time_filter module) so tests exercise the range semantics: events fully outside the range are dropped, boundary-overlapping events are kept, all-day events span a full day, and events without DTEND are treated as instantaneous. A unit test also asserts the generated REPORT XML nests time-range under VCALENDAR > VEVENT with UTC Z timestamps.
Claude-Session: https://claude.ai/code/session_012UDpqLhTib2oMkPj2roqaB