fix: patch TLS advisories, cap event results, pin tool surface (v0.3.2) #47

Merged
jlxq0 merged 8 commits from fix/t_3bf096eb-audit into main 2026-10-24 03:20:02 +00:00
Owner

Closes the findings of the caldav-mcp audit (fleet t_38eeb901) under t_3bf096eb, which Julian approved directly.

What changes

  • Security, lockfile only. Six crates are patched: rustls 0.23.46 (RUSTSEC-2026-0346 to -0349), rustls-webpki 0.103.16 (-0382), webpki-roots 1.0.11 (-0381), tokio 1.54.1 (-0386, unsound), quinn-proto 0.11.19 (-0338 to -0342, present in the lockfile only, not compiled in) and chacha20 0.10.2 (the old version was yanked). Before this, cargo audit on v0.3.1 reported 11 vulnerabilities; it now reports 0, and cargo deny passes on advisories, bans, licenses and sources.
  • Result cap. list_events, search_events and free_busy now refuse a result of more than 5000 event instances. They refuse rather than truncate, because a truncated free_busy would read as free time. The refusal returns JSON-RPC code -32027 and is audited as result_cap. search_events counts matches, not the instances it scans. free_busy checks the running total after each calendar.
  • Tests that pin the tool surface. The exact tool set and each tool's read-only and destructive hints are pinned, including the case where a hint is dropped. Every tool must have a README row.
  • Docs. The README documents all 10 tools (get_event_raw and delete_occurrence were missing). deployment.md now says smoke tests run as a fixture identity, never in a person's real calendar. AGENTS.md notes that a green status from before an advisory was published is not a clean audit.
  • CI. Push CI's cargo deny now checks advisories too. A new daily Advisories workflow runs cargo audit and cargo deny check advisories. It builds and publishes nothing, and it restores CI's cache without writing to it.
  • Release 0.3.2.

Verification at 500177ac2f

  • fmt, and clippy with -D warnings on Rust 1.93.0 and 1.99.0, both clean. Tests: 164 + 9 passing.
  • The new tests were checked by mutation: each of 9 deliberate code breaks made a test fail.
  • Independent review ran two rounds. Round 1 found 3 should-fix items and 6 nits, all addressed in 500177a. Round 2 found no blockers.
  • Live client proof (t_0286abfa, Clark). The official Python MCP SDK, authenticated through OAuth as a dedicated fixture identity, ran create, update, raw read and delete against a local build of this exact head, in a disposable calendar. No fixture carried ATTENDEE or ORGANIZER. Another principal's home returned 403. The event and the calendar returned 404 after cleanup. No personal calendar was touched.

Not in this PR

  • The sharing branches (feat/delegate-sharing, feat/owner-dav-share) need a decision from Julian.
  • A red scheduled run notifies nobody yet; AGENTS.md records this.
  • Beta and production rollout happen after the tag: fast-forward the beta branch, and promote the production pin in oddie-apps/platform (Clark).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM

Closes the findings of the caldav-mcp audit (fleet t_38eeb901) under t_3bf096eb, which Julian approved directly. ## What changes - **Security, lockfile only.** Six crates are patched: rustls 0.23.46 (RUSTSEC-2026-0346 to -0349), rustls-webpki 0.103.16 (-0382), webpki-roots 1.0.11 (-0381), tokio 1.54.1 (-0386, unsound), quinn-proto 0.11.19 (-0338 to -0342, present in the lockfile only, not compiled in) and chacha20 0.10.2 (the old version was yanked). Before this, `cargo audit` on v0.3.1 reported 11 vulnerabilities; it now reports 0, and `cargo deny` passes on advisories, bans, licenses and sources. - **Result cap.** `list_events`, `search_events` and `free_busy` now refuse a result of more than 5000 event instances. They refuse rather than truncate, because a truncated `free_busy` would read as free time. The refusal returns JSON-RPC code -32027 and is audited as `result_cap`. `search_events` counts matches, not the instances it scans. `free_busy` checks the running total after each calendar. - **Tests that pin the tool surface.** The exact tool set and each tool's read-only and destructive hints are pinned, including the case where a hint is dropped. Every tool must have a README row. - **Docs.** The README documents all 10 tools (`get_event_raw` and `delete_occurrence` were missing). `deployment.md` now says smoke tests run as a fixture identity, never in a person's real calendar. AGENTS.md notes that a green status from before an advisory was published is not a clean audit. - **CI.** Push CI's `cargo deny` now checks advisories too. A new daily `Advisories` workflow runs `cargo audit` and `cargo deny check advisories`. It builds and publishes nothing, and it restores CI's cache without writing to it. - **Release 0.3.2.** ## Verification at 500177ac2fa86be9f834f9ed32540fe109199bc1 - fmt, and clippy with `-D warnings` on Rust 1.93.0 and 1.99.0, both clean. Tests: 164 + 9 passing. - The new tests were checked by mutation: each of 9 deliberate code breaks made a test fail. - Independent review ran two rounds. Round 1 found 3 should-fix items and 6 nits, all addressed in 500177a. Round 2 found no blockers. - **Live client proof (t_0286abfa, Clark).** The official Python MCP SDK, authenticated through OAuth as a dedicated fixture identity, ran create, update, raw read and delete against a local build of this exact head, in a disposable calendar. No fixture carried ATTENDEE or ORGANIZER. Another principal's home returned 403. The event and the calendar returned 404 after cleanup. No personal calendar was touched. ## Not in this PR - The sharing branches (`feat/delegate-sharing`, `feat/owner-dav-share`) need a decision from Julian. - A red scheduled run notifies nobody yet; AGENTS.md records this. - Beta and production rollout happen after the tag: fast-forward the `beta` branch, and promote the production pin in `oddie-apps/platform` (Clark). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
cargo audit on a6db434 reported 11 vulnerabilities and 2 warnings that
were published after v0.3.1's CI ran:

- rustls 0.23.45 -> 0.23.46: RUSTSEC-2026-0346, -0347, -0348, -0349
- rustls-webpki 0.103.14 -> 0.103.16: RUSTSEC-2026-0382
- webpki-roots 1.0.9 -> 1.0.11: RUSTSEC-2026-0381
- quinn-proto 0.11.16 -> 0.11.19: RUSTSEC-2026-0338..0342 (lockfile only,
  not in the normal build graph)
- tokio 1.53.1 -> 1.54.1: RUSTSEC-2026-0386 (unsound)
- chacha20 0.10.1 -> 0.10.2: yanked

Each is a single-package update that still resolves on rust-version
1.93; no manifest change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
The 16 MiB response limit bounds what is read from one calendar, not
what a tool hands back. free_busy reads up to 256 calendars and
returned every interval it found: a live week across two calendars was
about 70 intervals, so a year is thousands, all paid for in the
client's context, and the accumulated Vec had no bound at all.

list_events, search_events and free_busy now refuse a result above
MAX_RESULT_EVENTS (5000) and tell the caller to narrow the window. They
refuse rather than truncate because a truncated free_busy reads as free
time. search_events caps its matches, not the instances it scans, so a
dense calendar can still be searched by year; free_busy checks the
running total after each calendar, so a refusal stops further reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
The README listed eight tools; get_event_raw and delete_occurrence
shipped in v0.3.0 without an entry. The capabilities list is now a
table with one row per tool, and the descriptions of list_events,
search_events and free_busy state the result cap.

Two regression tests: the router's tool set and its read-only and
destructive hints must match an explicit catalogue, because clients
decide whether to confirm a call from those hints; and every catalogued
tool must have a README row.

The README also says beta images build only from pushes to the beta
branch, which is why beta has run 0.2.2 since 2026-08-27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
v0.3.1's CI ran cargo audit green on 2026-10-07. Seven advisories
landed against its lockfile between 2026-10-13 and 2026-10-20, four in
the rustls production uses for every outbound request, and nothing ran
to notice because nothing was pushed.

A separate Advisories workflow runs cargo audit and cargo deny check
advisories daily and on demand. It builds nothing and publishes
nothing, and restores CI's cache without saving to it, so ci.yml stays
the only writer of that key. Every pinned action revision was fetched
from the Forge mirror before use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
The crate's clippy policy denies panic! and expect() outside
production code too, so the new cap and catalogue tests failed
clippy -D warnings. They now assert with matches! and compare Options
directly; what they check is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
AGENTS.md: a push-triggered audit is only as fresh as the last push;
read the daily Advisories run, not the commit status.

deployment.md: the authenticated smoke test writes events, so it runs
as a fixture identity in a calendar created for the run, never in a
person's real calendar, where a test event syncs and can notify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
fix: review round 1 — audit class for the cap, exact hints, true docs
All checks were successful
CI / cargo (pull_request) Successful in 2m37s
CI / docker (pull_request) Successful in 1m7s
500177ac2f
From an independent pre-PR review (no blockers):

- The cap refusal was InvalidInput, so the audit line recorded
  invalid_params, identical to a bad date: a user hitting the cap was
  invisible. It is now CaldavError::ResultCapExceeded with JSON-RPC
  code -32027, audited as result_cap, and each tool names its own
  remedy (search: narrow the query; list: the window; free_busy: the
  window or the calendars).
- The hints test defaulted a missing destructive_hint to false, but
  under MCP an absent hint on a write means destructive, so dropping
  `destructive_hint = false` from create_event stayed green. It now
  pins the exact Option.
- Push CI's cargo-deny ran bans/licenses/sources only, so the new
  daily job's "same checks" comment was false and a yanked crate would
  fail only there. ci.yml, README and CONTRIBUTING now include
  advisories.
- The daily job force-reinstalls cargo-audit so a cached binary older
  than the advisory-db format cannot fail every run until a push.
- delete_occurrence's README row and tool description now say
  send_scheduling_messages=true proceeds past the attendee refusal.
- Advisory counts corrected: twelve advisories were added against the
  v0.3.1 lockfile by 2026-10-23, six in the rustls stack (an earlier
  commit message on this branch says seven). The lockfile update also
  moved dev-only tempfile's getrandom 0.4.3 -> 0.3.4, now recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmS62rgeTQ7RcRAt9kNaCM
jlxq0 merged commit 2fd6cbfc2d into main 2026-10-24 03:20:02 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jlxq0/caldav-mcp!47
No description provided.