the owner's scenario: any chat, "I'm having dinner Saturday with my wife, add that", "cancel it", "add Peter Griffin, this number" — and it happens. reading already worked through the JSON api. writing is the DAV door, and the credential question is what took the thinking: every option meant a NEW secret and then somewhere to keep it — a file, the secret store, one per commander — each a password to distribute, rotate and eventually leak. $OFFICER_AGENT_KEY is already in every agent's environment, already bound to one user, already revoked when the session ends. so the DAV door accepts it as the basic-auth password, and no new secret exists anywhere. it cannot reach another account, and that is structural rather than a check: resolveApiKey says whose the key is and that id becomes X-Remote-User. the URL is never consulted, radicale refuses any other principal, and the username in the header must AGREE with the key rather than select the account. verified — own principal 207/201, another account 403, spoofed username 401. the permission check is isApiRequestAllowed against this plugin's own prefix, so the answer is exactly what the gate would give the same credential at /api/caldav. a second copy of that rule is the copy that stops being updated. DAV methods are mapped to read/write with PROPFIND and REPORT as reads — unmapped they counted as writes and a member with read could not list their own calendar. anything unrecognised defaults to write, which is the safe direction. the skill is written from recipes that were RUN, not recalled: create 201, move 204 (same UID, same href, SEQUENCE bumped), delete 200, contact create and delete, and the json read for lookups. the trap it exists to prevent, found while testing rather than by reading: `date -u -d 'saturday 20:00'` does not convert. -u parses AND prints as UTC, so 8pm becomes 20:00Z, which is 21:00 in lisbon. every event an hour out, nothing errors, and it is invisible until someone is stood up. the two-step through epoch is the only form that works and the skill leads with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
Calendar
Calendars and contacts on your own machine, synced to your phone over CalDAV and CardDAV.
- Radicale, not a container. A ~2MB Python package, spawned on loopback by this plugin's sidecar. It was a Docker service only because the old app store made everything one.
- Per user. The sync door resolves an app password to one account and tells Radicale that account's id
is the principal;
[rights] type = owner_onlydoes the rest. Every user gets their own calendars and their own contacts, in their own directory, invisible to everyone else. - Phones work. DAVx5, iOS and Thunderbird, including RFC 6764 autodiscovery from a bare domain.
Two screens, one plugin
/calendar and /contacts. They are two places you go rather than two sections of one page, which is why
this is the first plugin to declare screens in its manifest — one sidecar, one permission, one install,
two routes and two dock tiles.
What it claims from the platform, and why
Four paths outside /api, above the account gate:
| Path | Why it cannot sit behind the session |
|---|---|
/dav/* |
DAVx5, iOS and Thunderbird send HTTP Basic on every request and have nowhere to put a JWT |
/dav/provision/*.mobileconfig |
Safari is handed a URL by the phone app and has no credential; the one-shot token in the path IS the authentication |
/.well-known/caldav |
RFC 6764 — probed with a bare domain before the client has any credential |
/.well-known/carddav |
the same, for contacts |
"Above the account gate" is not "open". The sync door demands a scoped DAV app password and resolves it to a user before anything reaches Radicale.
Every one of those is in the platform's RESERVED_ROOT, claimable only by a first-party plugin. A
third-party plugin is confined to /p/<publisher>/ so it can never shadow a core route.
Extracted from the platform on 2026-08-27
It was core from 2026-08-19. What moved: the sidecar, the four routers, the panel apps, the app-password table, and the settings form — which became a modal on the Calendar screen, because a plugin ships panels and cannot contribute a settings section.
Two things deliberately did not change. The table is still dav_app_passwords rather than
caldav_app_passwords: installs already hold real rows, and the platform's prefix convention is not worth
a data migration on a live machine. And the panel names still resolve through the platform's translation
catalogs, so the six locales they had as core screens survive.