feat(sharing,sync): shared address books with owner and rights; sync_contacts over sync-collection (v0.4.0) #40
No reviewers
Labels
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jlxq0/carddav-mcp!40
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/t_8eb101f3-shared-sync"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Card t_8eb101f3 (Julian approved shared address books and incremental sync directly; per-agent identity, CalDAV, dependency migrations and any tool that changes other users' access are out of scope). Follows v0.3.0 (#39).
Why
Address books other accounts share with the caller were listed with no owner or rights, discovery broke on more than one
addressbook-home-sethref, and a client that wanted to stay current had to re-page the whole book.What
list_address_booksreturns the caller's own books and the ones shared with them, each withowner,shared,rights(owner,read-write,read,custom,none, classified from the server'scurrent-user-privilege-setas Stalwart spells it) and the rawprivileges; own books first. Every home-set href is read, so discovery works with assisted discovery off. Contact tools take shared hrefs unchanged; Stalwart's ACL decides, a refused write is a 403 that now names therightsfield, and nothing changes. No share-management tool, on purpose: this server acts as whoever presented the bearer, and an agent holding the owner's session must not be able to widen access. The unmergedfeat/delegate-sharingbranch (#33) carried both halves; only its read side is ported here (reworked onto the quick-xml parser, no new dependency).sync_contacts(addressbook_href, sync_token, limit, version)(src/contact_sync.rs): RFC 6578sync-collection, etags only, bodies viaaddressbook-multigetper page. Without a token: every card (initial pass). With one: cards added or changed, hrefs deleted, up tolimit(max 100) entries per call,complete,changes,initial,missing. The token is opaque, bound to the book and refused before any request when not issued here or issued for another book, with instructions to start over; a report the server truncates (507number-of-matches-within-limits) is refused the same way, never paged. Stalwart's ownlimitis not sent: read from its source, a limited report is paged byoffset = limit * seqover a set that moves under concurrent edits and a limited pass is stamped with its last page's state, so a low-id card changed mid-pass is lost; a limited initial report is a truncated list under a whole-book token. Here the next pass is anchored to the token of the pass's first report, so a change made during a multi-page pass is never lost; it may be delivered twice, which applying by href absorbs.version,given_name); concatenated home-set hrefs in discovery.src/fake_stalwart.rs(tests only): discovery, home listing with owner/privileges, read-only books (403 on PUT/DELETE), a change log andsync-collectionwith Stalwart's token shape and quirks (unknown token answered as initial;sync_max_results→ 507; the collection's own response).docs/sharing-and-sync.md(contract, no-loss argument, permission boundaries, client loop), AGENTS.md scope and seven pitfalls (stale tool schema on old mounts; never sendlimiton a Stalwart sync; unknown token = initial report; tombstones have nopropstat;AddItemshas no privilege spelling; no share tool here; the DAV change log is not purged by default).Evidence
missingnow and a tombstone next; another account's book refused without ending the session or leaking;@/%40spellings; the unknown-token-is-initial Stalwart behaviour pinned; listing with owner/rights/order; assisted discovery off with three home hrefs listing each book once; encoded owner href; writes to a read-only share refused by the server with no PUT/DELETE recorded and the ETag unchanged; parser units for owner, privileges, response-level status, truncation andsync-token; rights table; schema shapes.afterinclusive,initialconstant,missingconstant, status read frompropstat, read-write requiringwrite,sharedconstant, first home only, books not deduped, collection counted as a member,allnot owner; after the review: sibling tombstone counted as a wrong book, sibling tombstone delivered, probe skipped, probe on every page, behind-the-token check off, listing truncation ignored, secondary-home error propagated, secondary home not evicted, homes not deduped, sync bodies matched by raw href.cargo test --locked,cargo audit,cargo deny check, gitleaks: clean.aa47d8e): round 1 MUST-FIX on one item, fixed inc12234b: Stalwart reports the account's deleted paths for every address book in one book'ssync-collectionreport, and the first draft counted a sibling's tombstone as a member outside the book, refusing a quiet delta with a false "href does not name the book" error (the fake did not emulate it; 232 tests were green either way). Also from round 1: every pass now starts with aPROPFINDprobe of the href, because Stalwart answers async-collectionon an unshared book inside an account that shares another one with an empty 207 rather than a refusal (read from the source; to be confirmed live in AC4);list_contactsrefuses a listing Stalwart cut atmax-resultsinstead of returning it as the whole book; a revoked secondary home no longer fails the caller's own listing for the cache hour; a report whose Stalwart token id is behind the id asked about is refused with resync guidance; seven nits. Round 2 (confirmation): PASS, every finding closed, two wording nits fixed inebce484. The reviewer's unverified items for AC4: the 404-vs-403 shape of the probe on an unreadable book, andhighest_change_idmonotonicity across a cache rebuild. Follow-up outside this card:search_contactsandupcoming_birthdaysgo through the samemax-resultscut silently (pre-existing).Live proof (AC4) follows Clark's rollout, with fixture permissions Clark provisions on a disposable account: initial sync, create/update/delete and continued sync on a read-write share, read-only refusal, revoke and denied readback, conditional cleanup and absence; no real contact is written.
🤖 Generated with Claude Code
https://claude.ai/code/session_016ZfeS2nAr54h4afQ8ShfVE