Feat: CalDAV REPORT sync, multi-calendar & agent datetime - #15
Conversation
…wareness Replace inefficient PROPFIND+GET per-event sync with CalDAV REPORT time-range queries (RFC 4791), reducing ~2900 HTTP requests to ~6 and sync time from ~10 minutes to ~2 seconds. Add multi-calendar discovery and selection via Settings GUI. Inject current datetime into agent system prompt and strengthen tool-use instructions so the LLM reliably calls find_event instead of hallucinating calendar data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@claude, please review the PR, especially security-wise |
|
Claude finished @gerfru's task in 3m 7s —— View job PR Security ReviewTasks:
Overall this is a well-structured PR with several good security practices in place. Below are the findings, ordered by severity. 🔴 Medium: SSRF via absolute hrefs in CalDAV REPORT responses
return [
self._base_url + h if not h.startswith("http") else h
for h in collections
]If the CalDAV server returns response blocks with absolute URLs ( Recommendation: Validate that discovered hrefs share the same origin as 🟠 Medium: No response size limit on CalDAV HTTP responses
Both Recommendation: Add a if len(response.content) > 10 * 1024 * 1024:
raise ValueError("CalDAV response too large")🟠 Medium: Prompt injection via CalDAV event data
Event summaries, descriptions, and locations from the CalDAV server flow into the LLM via The Recommendation: Consider sanitizing event fields before returning them from 🟡 Low:
|
1. SSRF: Reject CalDAV hrefs with foreign origins (non-matching base URL) 2. Response size: Cap CalDAV REPORT/PROPFIND responses at 10 MB 3. Prompt injection: Sanitize event fields (strip control chars, truncate) before passing to LLM context 4. Value validation: Reject settings values exceeding 4096 characters 5. Timezone validation: Verify IANA timezone in SettingsStore.set() and graceful fallback in build_system_prompt() 6. XML parsing: Replace all regex-based XML parsing with xml.etree.ElementTree for robustness against comments, CDATA, and namespace variations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@claude: rerview the changes |
|
Claude finished @gerfru's task in 4m 31s —— View job PR ReviewTasks:
Good overall PR — the REPORT-based sync is a substantial improvement and all six findings from the previous security review have been addressed. A few new issues below, plus notes on code quality. 🔴 Bug: CSP blocks the
|
| Finding | Fix location |
|---|---|
| SSRF via absolute hrefs | caldav.py:492-498 — origin validated, foreign hrefs skipped |
| No response size limit | caldav.py:437-440, 560-563 — 10 MB cap enforced |
| Prompt injection via event data | calendar.py:73-83 — control-char strip + 500-char truncation |
caldav_calendars value unbounded |
settings_store.py:239-242 — 4096-char limit |
| Timezone injected unsanitized | settings_store.py:244-251 — IANA validation before save |
| Regex XML parsing | caldav.py:306-307 — migrated to xml.etree.ElementTree |
✅ What's done well
- Performance: REPORT replaces ~2900 GET requests with ~6. The sync window (
_SYNC_DAYS_PAST=30,_SYNC_DAYS_FUTURE=365) is well-chosen. - Datetime injection: Clean addition to the system prompt with proper fallback on invalid timezone.
- Soul.md improvements: Explicit, concrete examples (
"Zahnarzt","Padel") and the NEVER-hallucinate instruction are much stronger than the original. - Htmx lazy-loading: Calendar list is loaded on demand — settings page still renders if CalDAV is slow.
- CSRF: Correctly handled globally via
htmx:configRequestinapp.js— no per-form workaround needed. discover_collectionsvs sync isolation: The UI discovery endpoint shows all calendars; the sync respects the user's selection. Correct scoping.
1. CSP: Move inline onclick handler in calendars.html to event-delegated handler in app.js (inline scripts blocked by CSP) 2. Test: Add test_filters_by_caldav_calendars_setting for calendar selection filter path 3. API: Rename _allowed_collections to allowed_collections (public, called from web.py) 4. Style: Remove unnecessary f-prefix in prompts.py, fix extra blank line in caldav.py 5. Docker: Revert postgres port exposure to commented-out default Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use POSTGRES_HOST_PORT env var (default: 0 = random ephemeral port on loopback, effectively hidden). Set POSTGRES_HOST_PORT=5432 in .env to expose for local debugging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
please re-review the code now @claude |
|
Claude finished @gerfru's task in 5m 29s —— View job PR Re-ReviewTasks:
All 12 findings from the previous two reviews have been addressed. One new correctness bug and two minor issues were found. 🟠 Bug:
|
| # | Finding | Fix location | Status |
|---|---|---|---|
| 1 | SSRF via absolute hrefs | caldav.py:492-498 — origin validated |
✅ |
| 2 | No response size limit | caldav.py:329-332, 437-440 — _MAX_RESPONSE_BYTES enforced |
✅ |
| 3 | Prompt injection via event data | calendar.py:82-93 — _sanitize_field strips control chars + truncates |
✅ |
| 4 | caldav_calendars value unbounded |
settings_store.py:79-83 — 4096-char limit |
✅ |
| 5 | Timezone injected unsanitized | settings_store.py:85-92 — ZoneInfo validation before save |
✅ |
| 6 | Regex XML parsing | caldav.py:6, 247-303 — migrated to xml.etree.ElementTree |
✅ |
| 7 | CSP blocks inline onclick |
calendars.html:14, app.js:45-51 — data-calendar-save + event delegation |
✅ |
| 8 | Missing filter path test | tests/test_caldav.py:213-234 — test_filters_by_caldav_calendars_setting |
✅ |
| 9 | Private method accessed from another module | caldav.py:307 — renamed to allowed_collections (no underscore) |
✅ |
| 10 | Unnecessary f prefix |
prompts.py:46 — first string in time_section no longer has f prefix |
✅ |
| 11 | Extra blank line between methods | caldav.py:305-307 — now a single blank line |
✅ |
| 12 | Postgres port always exposed | docker-compose.yml:10-11 — ${POSTGRES_HOST_PORT:-0} pattern |
✅ |
✅ What's done well (new in this pass)
allowed_collectionsnaming: dropping the underscore and calling it fromweb.pyis correct — the rename makes the public API explicit.test_filters_by_caldav_calendars_setting: good test, correct assertion thatschedule-*collections are excluded.data-calendar-savepattern: CSP-safe and consistent with the existingdata-flag-toggleapproach inapp.js._sanitize_fieldwith 500-char truncation: conservative and correct. The...suffix on truncated strings is a nice UX touch.apply_overrideswithmodel_copy: the Pydantic v2 approach is clean; the only issue is thatCalDAVSync.configisn't kept in sync (see above).
…cache 1. Stale config: Update caldav.config after runtime settings change so allowed_collections() reads fresh caldav_calendars without restart 2. Empty selection UX: Disable save button when no calendar is checked, prevent submitting empty value (which would mean "sync all") 3. Discovery cache: Cache discover_collections() results for 60s to avoid repeated PROPFIND requests on settings page interactions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
again, rereview please @claude |
|
Claude finished @gerfru's task in 3m 15s —— View job PR Re-Review (Pass 4)Tasks:
All three remaining findings from the previous review are resolved. Two new minor observations below. ✅ All previous findings — fully resolved
🟡 Minor: Save button disabled state not initialised on htmx fragment load
document.body.addEventListener("change", function(evt) {
if (evt.target.name !== "cal") return;
...
btn.disabled = checked === 0;
});
Recommendation: Add an document.body.addEventListener("htmx:afterSettle", function() {
var form = document.getElementById("calendar-form");
if (!form) return;
var btn = form.querySelector("[data-calendar-save]");
if (!btn) return;
btn.disabled = form.querySelectorAll("input[name=cal]:checked").length === 0;
});🟡 Minor: Null dereference risk in
|
1. SSRF: Reject CalDAV hrefs with foreign origins (non-matching base URL) 2. Response size: Cap CalDAV REPORT/PROPFIND responses at 10 MB 3. Prompt injection: Sanitize event fields (strip control chars, truncate) before passing to LLM context 4. Value validation: Reject settings values exceeding 4096 characters 5. Timezone validation: Verify IANA timezone in SettingsStore.set() and graceful fallback in build_system_prompt() 6. XML parsing: Replace all regex-based XML parsing with xml.etree.ElementTree for robustness against comments, CDATA, and namespace variations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. CSP: Move inline onclick handler in calendars.html to event-delegated handler in app.js (inline scripts blocked by CSP) 2. Test: Add test_filters_by_caldav_calendars_setting for calendar selection filter path 3. API: Rename _allowed_collections to allowed_collections (public, called from web.py) 4. Style: Remove unnecessary f-prefix in prompts.py, fix extra blank line in caldav.py 5. Docker: Revert postgres port exposure to commented-out default Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cache 1. Stale config: Update caldav.config after runtime settings change so allowed_collections() reads fresh caldav_calendars without restart 2. Empty selection UX: Disable save button when no calendar is checked, prevent submitting empty value (which would mean "sync all") 3. Discovery cache: Cache discover_collections() results for 60s to avoid repeated PROPFIND requests on settings page interactions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Feat: CalDAV REPORT sync, multi-calendar & agent datetime
Summary
time-rangequeries. Reduces ~2,900 HTTP requests to ~6, sync time from ~10 min to ~2 sec. Only syncs events from -30 days to +365 days.Changes
src/niles/sync/caldav.pysrc/niles/sources/web.pycaldav_enabledflagsrc/niles/config.pycaldav_calendarssettingsrc/niles/settings_store.pycaldav_calendarsin editable whitelistsrc/niles/main.pyapp.state.caldavsrc/niles/agent/prompts.pysrc/niles/agent/core.pybuild_system_promptconfig/soul.mdsrc/niles/templates/settings.htmlsrc/niles/templates/fragments/calendars.htmltests/test_caldav.pytests/test_settings_store.pyTest plan
python -m pytest tests/ -v— all 174 tests passpython -m ruff check src/ tests/— no lint errorsfind_eventinstead of hallucinating🤖 Generated with Claude Code