Commit Graph
3 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 0faf9183ad sign with -binary, or iOS refuses the profile it just verified
every signed profile was rejected with "Invalid Profile". `openssl smime -verify`
reported success the whole time, which is exactly why it took so long to see:
the signature was valid over the WRONG BYTES.

without `-binary`, openssl treats the input as TEXT — it canonicalises line
endings and adds MIME headers before signing — so what iOS unwraps is no longer
the plist that was written.

this was already found and written down in officer-oas/enroll.ts on 2026-08-20,
four wrong endings into a hunt that was never about the ending. this file was
written without reading that one, and repeated it exactly.

also reads OFFICERDEV_DAV_PROFILE_SIGN_* as well as the bare names, which is the
convention every other consumer follows. on a converted install the signing
certificate could be configured correctly and silently ignored, and the only
symptom is a profile that says "Not Signed" for no visible reason.

verified on a real iPhone (iOS 26.6): the profile installs, and the signed inner
payload starts with <?xml with no MIME headers and no CRLF mangling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-27 09:49:55 +00:00
pastilhasandClaude Opus 5 a161e2ac5f agents can manage the calendar and the address book
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
2026-08-27 06:08:25 +00:00
pastilhasandClaude Opus 5 dcbc9b102c calendar and contacts, extracted from the officer platform
one sidecar, one permission, one install — and two screens, because Calendar and
Contacts are two places you go rather than two sections of one page. the first
plugin to declare `screens`, which the platform gained for this.

what moved: the radicale sidecar, the four routers, the panel apps, the
app-password table and the settings form. the form became a modal on the
calendar screen — a plugin ships panels and cannot contribute a settings
section, which is the same wall the email plugin hit and the same answer.

the routes are /calendar and /contacts, not /caldav/calendar. they were core
routes and keeping them is not cosmetic: a bookmark, a dock tile someone
arranged, and a persisted layout all address them. only a first-party plugin may
choose its own segment; a third-party one is confined to /p/<publisher>/.

four paths outside /api come with it, above the account gate, because phones are
not browsers: DAVx5, iOS and thunderbird send HTTP basic on every request and
have nowhere to put a JWT. that is not "open" — the sync door demands a scoped
app password and resolves it to one user before radicale sees anything.

two things deliberately unchanged. the table is still dav_app_passwords, not
caldav_app_passwords: installs hold real rows and a naming convention is not
worth a data migration on a live machine. and the panel names still resolve
through the platform's catalogs, so the six locales they had as core screens
survive rather than dropping to english to make the refactor tidier.

per-user was already true and is worth writing down because the config said
otherwise: the sync door resolves an app password to an account, tells radicale
that id is the principal, and owner_only does the rest. the comment claiming
"the owner is the only principal that exists" had been stale since multi-user
landed on 2026-08-07.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-27 04:53:45 +00:00