WIP: feat: sharing slice 1 PR 2: shared data and recovery #829
Closed
kayg
wants to merge 1 commit from
feat/sharing-s1-pr2 into feat/sharing-s1-pr1
pull from: feat/sharing-s1-pr2
merge into: kayg:feat/sharing-s1-pr1
kayg:main
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:t3/paged-search-reconcile-final
kayg:feat/pass8
kayg:review/calendar-2
kayg:fix/shared-row-swipes
kayg:feat/contrast
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:notes-archive-hide
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:perf/executor-hot-path
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:feat/tiering-761-a-migration
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:feat/carddav-writes
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:docs/deploy-plan
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
kayg/calternal!829
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/sharing-s1-pr2"
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?
Part 2 of the sharing slice, built on approved PR #827 (
08876c76) and targetingfeat/sharing-s1-pr1.This draft retains the
AuthorizedResourceresolver, server admission fences and durable per-Installation controls, including per-note sequence fences and re-grant baselines. It stays dormant: cross-owner access is refused unconditionally and sharing actions are not registered, so a grant cannot be created through the API.Live room delivery moved to stacked PR #835. That isolates the send-time sequence/fence check, recipient epoch, and revoke eviction while this PR remains safe without UI registration.
Changed lines: 3,463 (2,956 additions, 507 deletions).
Gates on dev-01
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -j4 -- -D warningscargo test --workspace -j4(89 successful test-result groups; perf-only tests remain ignored)Independent review, round 1
Reviewed head:
f00c26249872f44d4846049b978acb56e55610ce.Findings
P0: none.
P1: Purge does not fence already-open room deliveries or a prepared response.
Notes::joinchecks access before and after joining, then returns a room stream with no later access check.sharing.revokecommits the purge control but does not detach or filter existing room sessions. Failure scenario: a recipient joinsowner/note, the owner revokes the grant, then the owner edits; the recipient's existing room still receives the update even though a newnotes.getis denied. I reproduced this with the temporary dev-01 probeprobe_shared_room_still_delivers_after_revoke.get_authorizedalso performs its final resolver check before returning the response, so a prepared read can finish after purge commits. Recheck at delivery and response completion, and fence active room sessions.P1: This split does not keep sharing unavailable. The sharing plugin is registered unconditionally, the action contract exposes grant/revoke/control actions, and the new isolation test asserts that an owner can grant.
notes.getacceptsowner_id, and room joins acceptowner/id; the probe reads the shared text throughnotes.get. API clients can therefore share and read before the follow-up sync, fan-out, device recovery, search, history and lifecycle work is complete. No UI does not block these server actions. Add a server-side gate for share creation and cross-owner reads until the follow-ups are complete, or keep the core unlanded.P2: Durable control and fan-out storage is not bounded over time.
sharing.controlslimits one response to 256 rows, but there is no control/ack cleanup path. If an Installation never returns, its controls remain pending indefinitely; even after all current Installations acknowledge, the rows remain. Also, each edit on a note with anote_sharesrow inserts ashare_fanoutintent, and this branch has no worker to drain them. The response limit does not bound either table. Define retirement/compaction for absent Installations and land the fan-out worker before enabling sharing.P3: Controls are not returned in per-note sequence order. The pending query sorts by
created_at,id, while IDs are random and timestamps have millisecond resolution. A quick re-grant/revoke can return a baseline and purge out of sequence; applying that list order can let the older purge clear the newer baseline. The 2C client path should apply controls monotonically bynote_seqand cover equal-timestamp ordering.Split and verdict
The 2B → 2C → UI split is sensible by dependency. The core is not safe to land on its own while sharing is meant to remain unavailable: its API is already callable, and live room delivery survives revoke. Verdict: request changes before merge; gate the feature and enforce the committed fence on every delivery path before enabling it.
Probe results
auth.db.Round-one review status at head
2bb35086(code gates ran ate4f6a934;2bb35086records the gate results).notes.getrechecks access immediately before returning. Tests:revoked_shared_room_is_evicted_and_receives_no_later_updatesandprepared_shared_read_rechecks_its_sequence_before_returningpass.contracts/actions.json; the generic route inventory is unchanged. Tests:sharing_actions_are_not_registered_before_the_ui_prand the route inventory isolation tests pass.note_seq; cleanup retains controls and fan-out intents for known Installations, retires work when an Installation has been inactive for 30 days, and requires a current authorized reset on return. Tests:pending_controls_use_note_sequence_when_timestamps_matchandcleanup_keeps_known_work_and_resets_dormant_installationspass.Dev-01 gates:
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -j4 -- -D warnings, the targeted migration regression,cargo test -j4 -p calternal-auth --test migration -- --nocapture, andcargo test --workspace -j4pass. Workspace result: 91 groups, 1,278 passed, 0 failed, 20 ignored. Focused auth, prepared-read and isolation gates pass.PR #829 remains at the server core (3,229 changed lines against PR #827). Direct bytes, attachments, sync fan-out, offline recovery, search, history and lifecycle remain in the sequenced follow-ups. The owner-to-recipient app-password API flow stays unavailable until PR 3 registers sharing actions.
Independent review, round 2
Reviewed head:
2bb3508623.Findings
Verification
The focused gates ran on dev-01 through ~/bin/slot nix develop -c bash -c:
cargo test -j4 -p calternal-auth --lib sharing::tests -- --nocapture— 13 passed.cargo test -j4 -p calternal --bin calternal isolation::sharing_actions_are_not_registered_before_the_ui_pr -- --nocapture— passed.cargo test -j4 -p calternal --bin calternal isolation::every_registered_action_and_direct_route_has_a_capability_classification -- --nocapture— passed.cargo test -j4 -p calternal --bin calternal isolation::revoked_shared_room_is_evicted_and_receives_no_later_updates -- --nocapture— passed.cargo test -j4 -p calternal-notes --lib prepared_shared_read_rechecks_its_sequence_before_returning -- --nocapture— passed.cargo test -j4 -p calternal-core room_queue::tests::abort_discards_messages_waiting_for_a_room_client -- --nocapture— passed.Verdict: request changes.
Round 3 status at
7857ace57184b51eacb92691531a85315215a39c.dequeued_room_frame_is_checked_after_revoke_under_random_schedulescovers 16 seeded yield schedules, anddequeued_room_frame_rechecks_committed_fence_and_grant_epochchecks the real auth database across purge and re-grant.notes.get(owner_id)could read through a stored grant. Fix: cross-owner resolution remains disabled by the compile-timeCROSS_OWNER_SHARING_ENABLEDconstant, and sharing actions remain unregistered for PR 2. Test:a_direct_grant_row_does_not_enable_cross_owner_read_pathsinserts a grant row, verifies Bob's valid Notes Read app password can read his own note, then verifies Alice's note is refused throughnotes.get, direct bytes, the resolver, room join, and sync.reset_ack_keeps_controls_and_fanout_after_its_note_cursor.Dev-01 gates passed under Nix/
~/bin/slot:cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings, andcargo test -j4 --workspace(1,280 passed, 0 failed, 20 ignored).Independent review, round 3 (final)
Reviewed head:
7857ace57184b51eacb92691531a85315215a39c.note_shares/share_grants,notes.get, direct note bytes, resolver, room join and sync returned no owner data. List, search, backlinks and history also returned no owner ID or marker. A Notes Read scope was refused by the DAV gate.CROSS_OWNER_SHARING_ENABLEDis false, sharing actions remain unregistered, and the existing unregistered-actions test passes.Findings
writer_lock(owner, note_id), shared by every recipient, and holds it throughsocket.send(up to the existing 15-second write timeout). A slow peer can serialize same-note deliveries and delay revoke, which uses the same lock. This path is unreachable while cross-owner access stays disabled; resolve it before PR 3 removes the gate.Split recommendation
Yes, split into two stacked PRs. Keep #829 focused on the resolver and hard-off gate, action registration remaining off, per-note admission fences, durable controls and reset ordering, plus the committed direct-grant matrix. Put the room frame sequence/epoch plumbing and final websocket send check in a new PR stacked on #829. Keep the gate false until that room PR is merged; resolve the P2 before enabling sharing in PR 3.
Verdict
The reviewed head is safe to land only as a dormant slice. I recommend the split before merge because the PR is 1.7 times the planned size and combines auth/control storage with room transport. No P0/P1 remains.
Verification
On dev-01 through
~/bin/slot nix develop -c bash -c, these focused probes passed. Probe bodies were temporary and restored after the run:cargo test -j4 -p calternal-auth --lib review_probe_dequeued_frame_is_refused_after_db_revoke -- --nocapturecargo test -j4 -p calternal-auth --lib review_probe_reset_cursor_preserves_a_later_purge -- --nocapturecargo test -j4 -p calternal-server --lib review_probe_dequeued_frame_holds_fence_through_socket_write -- --nocapturecargo test -j4 -p calternal --bin calternal isolation::a_direct_grant_row_does_not_enable_cross_owner_read_paths -- --nocapturecargo test -j4 -p calternal --bin calternal isolation::sharing_actions_are_not_registered_before_the_ui_pr -- --nocaptureSplit complete: live room send-time checks, recipient epochs, revoke eviction, and their regressions moved to stacked #835 so the socket-delivery boundary can be gated separately. This PR retains the resolver, admission fences, and durable controls; cross-owner reads remain refused and sharing actions remain unregistered. dev-01 fmt, Clippy with warnings denied, and workspace tests pass.
kayg referenced this pull request2026-11-02 08:59:09 +00:00
BM14 follow-up: both auth failures are deterministic integration mismatches, not timing failures. Dev added the tests after this branch diverged; #829's
CROSS_OWNER_SHARING_ENABLED=falserejects the recipient operations they still unwrap as successful. The baseline has no dormant gate. I merged the pinned BM14 base41f2efa3into this owning branch to bring those tests into scope.The fix preserves the dormant gate and every cache/sequence check. It additionally requires recipient admission to return NotFound without consuming a sequence, then verifies owner admission still receives sequence 6. The replay test polls the authorization future under the held per-note mutex, requiring Pending for both owner and recipient, then requires owner success and recipient refusal after release. No timeout or access restriction is relaxed. Before/after 30-run merged-tree auth results under one ordinary slot build will follow here. No train rerun or merge into dev.
BM14 auth fix pushed:
97d790f6(pinned dev integration merged95b705cis its parent). Both original failures reproduced in 30/30 runs, and both pass in 30/30 after the fix on BM14's merged source tree. Each run executes the entire auth library suite with its normal parallel test threads, not just the named case. Before: 62 passed / 2 failed per run. After: 64 passed / 0 failed per run.Cause: the dev baseline admits recipient writes, but #829 deliberately hard-disables cross-owner resolution. The new dev tests' success unwraps therefore fail consistently when the branches are combined. There is no evidence of a production access race in these two failures.
Fix: retain the dormant access gate; verify recipient admission returns NotFound and consumes no sequence, then verify the next owner write receives sequence 6. All owner cache and DB-operation assertions remain. The writer-lock regression now polls the actual authorization future under the held per-note mutex, requires Pending for both identities, then requires owner success and recipient refusal after release. No timeout is extended and no access assertion is removed to bypass a race.
Dev-01 commands, under Nix and
~/bin/slot,CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 CARGO_INCREMENTAL=0:Before loops overlapped the ordinary
~/bin/slot ... cargo build -j4preparation job. After loops overlapped one ordinary~/bin/slot env CARGO_TARGET_DIR=$PWD/target/load cargo build -p calternal-auth -j4job. Both use normal slots, no train lock or artificial CPU load.cargo fmt --allran on dev-01. Browser work remains in progress. No full train or merge into dev was run.Additional targeted validation on the merged tree with #829 fix
97d790f6:cargo fmt --all --check: PASS.cargo clippy -p calternal-auth --lib --tests -j4 -- -D warnings: PASS (dev-01, Nix, normal slot).The recorded 30/30 before failures and 30/30 after passes remain unchanged. No full train was run.
bm14 follow-up complete. head
97d790f69259b0308598ca0aafd2407ef1a1226e. both named rust tests: 0/30 before, 30/30 after, from 30 complete 64-test auth-library runs with a concurrent ordinary slot build. fmt and targeted auth clippy pass. detailed rust evidence: #829 (comment)final cross-case results, commands, limitations and cleanup: #850 (comment)
no train run or merge. own VM jobs finished and large temporary artifacts removed.
Perf fix pushed to this branch (head
72eb0eb1): the room-delivery check for an owner's own note no longer reads the sharing db per frame.e859b19cauth:authorize_room_deliveryfor the owner with a session/development credential runs only the note-ID size and read-scope checks (whatresolve_resource_inreduces to for an owner). App passwords (revocable) keep the full check.08d11203room:Home::owner_deliveryanswers the owner's connection in place (no caller clone, no sharing call).aeda1a4fNotes::resolve_resourceanswers owner Read/Edit with the same size + scope checks and anOwnerresult (join used to make two reads); sequence is passed through as given.95c6ed00test: the shared-read recheck probe now reads as a recipient (an owner never needs that second read); same assertions.Access logic touched, stated plainly: owner Read/Edit of own note only; same allow/deny outcomes (size, scope, Forbidden); recipients, app passwords, Snapshot and ManageSharing take the unchanged path. No check was relaxed. Gates:
cargo test -p calternal-auth -p calternal-notes -p calternal-core -p calternal-server --libpass (65/551/17/16),cargo fmt --all --checkclean.Method: release binaries built on dev-01, relayed to the perf VMs with matching sha256 (dev
de99cfbf…, train+fixes68dd30a0…, same devcalternal-perffor every run). Every run under~/bin/perf-lock, seed 1, 10k Home, alternating pairs. Medians of per-run p50 / p95, [min–max of p50]. The perf VMs were shared with other agents' long lock holds, so runs spread over several windows; dev-vs-dev spread alone is 0.20–0.37 ms on round trip.sync.round_trip bisect (10k, perf-02, ms)
41f2efa3ef284146)efa2bf37)72eb0eb1) vs dev same windowCulprits: #829 (
Auth::authorize_room_delivery, called per delivered frame, does a sharing-db read even for the owner's own note) and #835 (the same check per queued frame in the room, again per written frame inlive.rs, plus two more reads per room join and a clone of the caller). Both are the same cause: an owner reading their own note never depends on a grant row, but every frame paid a database hop.Independent review: owner fast path (#829 head
72eb0eb1, #835 head46be23e3)Scope:
e859b19c,08d11203,aeda1a4f,95c6ed00on #829, plusfd01b6d8,a7d8cac3,85d02dfb,231e4457and the merges on #835. I only read code. Nothing was built or run.Verdict: safe to train. A non-owner can't reach the fast path. Neither can an app password. Snapshot and ManageSharing still take the full path. There are no P1s. The two P2s are hardening and test gaps, not open holes today. Security is not the merge gate, though: the 100k
sync.round_trip(+17% p50) and the 10kopen.notenumbers on #835 are not shown flat yet, so under the no-regression rule those still decide the merge.P1
None.
P2
kind != AppPasswordatcrates/notes/src/home.rs:233and:263,crates/auth/src/sharing.rs:996(#829) and:984(#835).Credential::has_scope(crates/core/src/action.rs:82) istruefor every kind except AppPassword, so the "scope check" on the fast path does nothing. Today only Session, AppPassword and Development exist, so nothing is exposed now. The problem comes with the next kind. If a scoped Cal or assistant key (#839) or a link token is added as a newCredentialKind, it gets owner Read and Edit with no scope check and no revocation check, and no test will fail. Fix: one helper, e.g.Credential::is_session_like(), written as an exhaustivematchwith no_arm (Session | Development => true, AppPassword => false). Use it at all four places and at thecacheable_ownercheck (sharing.rs:884on #829,:1069on #835). Adding a kind then forces someone to decide.other_people_and_app_passwords_take_the_full_pathandowner_resource_answers_owner_for_read_and_edit_onlyonly show that the routing function returnsNone. No test wiresAuthintoNotes, revokes an owner's app password, and then checks thatNotes::get_authorized,Notes::join, roomsend_to_authorizedand the live writer (Notes::authorize_delivery,lib.rs:965on #835) all refuse. The four call sites now pick their path separately (lib.rs:522/540,sync.rssend_to_authorized,home.rs:469,lib.rs:965). If one of them later calls the fast path without the kind check, nothing catches it. I found no such test undercrates/*/testseither (app_passwords.rscovers the session store only). Suggested test: owner app password withnotes.read/notes.writejoins and receives one frame, then gets revoked. The next frame,get_authorizedand a newjoinare all refused.P3
owner_room_delivery_keeps_size_and_scope_checks_without_a_db_hop(sharing.rs:2211on #835) never asserts thatauth.inner.db.run_count()stays unchanged, even though its name says so. That assertion is the cheap guard against this perf regression coming back.owner_unshared_admission_is_cached…already shows the pattern.owner_read_precheck(sharing.rs:451/:470),home::owner_delivery(home.rs:228) andhome::owner_resource(home.rs:255), each with its own copy of the 256-byte limit and error strings. They could drift. Keep one copy incalternal_coreand call it from all three.Noneon join) instead oflast_note_seq, so an owner join no longer raisesroom.delivery_sequence(sync.rs:90). I checked and it does no harm. The frame sequence never reaches the client (live.rs:235sends only the bytes). The owner's writer check ignores it. A recipient's own join still resolves the current sequence and raises it, and so do admitted edits. Worth one comment atNotes::join.Notes::authorize_delivery(lib.rs:965) runs beforeself.enter(owner). An owner frame that is already queued is now written even while that Home is shutting down. Only the owner gets their own bytes, so nothing leaks. It is a small behaviour change.Checked, no finding
caller.usercomes only from the authenticator.DevUseris a single fixed user (server/src/auth.rs:78-87). In aowner/idreference the owner is parsed, and the fast path only runs when it equalscaller.user.self.home.userin the room,owner == caller.userin Notes), so an id can only name the caller's own note.watch::valid_idand the size check still run first.access_inreturnsOwnerwhenowner == person,sharing.rs:378).Home.useris fixed when the Home opens. I found no "A13" in code or docs.ensure_credential). A session owner was never rechecked on the old path either (ensure_credentialreturnsOkfor anything except app passwords), so session sign-out on a live socket is not a regression here.isolation.rs:380-420refuses bob through the action,get_authorized,resolve_resourceandjoin. The fence, epoch, slow-recipient, revoke and regrant tests insharing.rsuse realAuthwith recipient callers, so the change doesn't touch them.95c6ed00(the test the author asked about): coverage is not weaker. The probe now drives a real recipient throughget_authorized, and the second read is still refused with the same assertions. The mock only lets a non-owner in whenreject_second_readis set (pipeline.rsmemory_tests), so the other probes keep their owner-only behaviour. The owner case is covered byowner_resource_answers_owner_for_read_and_edit_onlyandowner_session_passes_and_keeps_size_checks. Those are unit-level, which is what P2.2 is about.Security/access review: #829 (
72eb0eb1) + #835 (46be23e3), follow-up #909 (98afda51), against dev41f2efa3Review only, nothing changed. Read:
crates/auth/src/{sharing,db,app_password,session,lib}.rs,crates/core/src/{sharing,action}.rs,crates/notes/src/{lib,actions,home}.rs,crates/notes/src/room/{sync,pipeline,actor,mod}.rs,crates/server/src/{live,rooms,auth}.rs,crates/calternal/src/isolation.rs, plan 0409. Line numbers are from #835 unless marked #909.Prod exposure in BM14e: cross-owner access is off in two places,
CROSS_OWNER_SHARING_ENABLED=false(sharing.rs:15, enforced atsharing.rs:492and:962), andsharing.*is unregistered (removed fromcontracts/actions.json, testisolation.rs:267). So no grant row can exist and no recipient path can be reached. In prod, what changes is the owner path (fast path, per-frame check,notes.getresolver) and the auth migrations v12/v13.P1: none
P2: fix before PR 3 enables sharing (not reachable in BM14e)
cleanup_sharingsweeps every user's sharing tables inside each shared-note write.sharing.rs:762-878runs five globalINSERT…SELECT/DELETEstatements overinstallations,recipient_control,share_fanoutandshare_fanout_acksfor all owners. It runs inside anIMMEDIATEauth.db transaction fromadmit(:1121),grant(:1680),revoke(:1813),acknowledge_control(:1290,:1326) andacknowledge_fanout(:1379,:1400). Sequence after PR 3: A shares a note with B; every room edit batch from A callsadmit, which runscleanup_sharing. Cost is O(all sharing rows on the server) per keystroke batch, and it holds the auth.db writer, so other auth writes (sign-in, session/app-passwordtouch) wait behind it. Todayadmitreturns at the!sharedcheck before reaching it. Ask: move it to a bounded periodic task keyed by share, and add a perf-VM gate for "many shares + one typing owner".room/sync.rs:300-349:broadcast_updateawaitsauthorize_room_deliveryfor each non-owner connection, which takes the note writer lock and does one auth.dbrun(sharing.rs:1006-1023), all inside the room actor. With 32 recipients, each update costs 32 sequential DB hops before the actor takes its nextCmd, including the owner's next keystroke. The server then rechecks every dequeued frame again (live.rs:257-283,sharing.rs:1009), so that is about 2 hops per frame per recipient. The owner-only path is unaffected (home::owner_delivery). Ask: read the room state once per update for all recipients, and measure on perf VMs before PR 3 (no-regression rule).CROSS_OWNER_SHARING_ENABLEDenablesnotes.get{owner_id}and room joinowner/id. History, attachments, search, backlinks, link previews, export and sync still read onlycaller.user's Home (plan 2B/2C). Nothing leaks today. The risk is that someone flips the const in PR 3 before 2B/2C land, or a new entry point that accepts an owner reference skipsresolve_resource. Ask: an inventory test (like the action/route inventory) that lists every Notes entry point taking a note ID and asserts it either goes throughNotes::resolve_resourceor rejects owner references. Make it a precondition for the flip.P3
core/src/action.rsCredentialKind::owner_fast_path(#909) says Session/Development "cannot be revoked mid-session". Sessions can be revoked. The fast path is still equivalent to the full path for two reasons. First,ensure_credentialreturnsOkfor every non-app-password credential (sharing.rs:264-267), so the full path never rechecked sessions either. Second, live sockets close onaccount.installation-revoked(live.rs:150,:419). Please reword to "kinds whose access the sharing layer never rechecks; revocation is enforced by authentication and the socket sign-out". Otherwise a future scoped key or link token may be markedtrueby analogy. The same wording appears inhome.rs:222-250.notes/src/lib.rs(authorize_delivery, #909),_workis dropped on return, beforelive.rswrites the frame. It refuses frames once shutdown has started but does not hold shutdown off during the write. The non-fast path behaves the same, so this is a comment fix only.notes.getwith an owner app password now makes 2 extra auth.db round trips.get_authorizedcallsresolve_resource(full path,ensure_credentialquery) and thenauthorize_room_delivery, which for app passwords repeatsresolve_resource_in(sharing.rs:986-1001). One check is enough because the read already happened under the first. No perf-VM numbers were posted for app-passwordnotes.get. Collapse it, or measure it per the no-regression rule.socket.send(live.rs:276-280,sharing.rs:1009-1012), andrevoketakes the same lock first (:1756). A stalled recipient socket delays that recipient's revoke by up toWRITE_TIMEOUT= 15 s (live.rs:40). This is bounded and tested for other recipients (:2341). It is a PR 3 UX note: the revoke button must not look stuck.grantdoesn't check the note exists in the owner's Home (sharing.rs:1556-1560). Delete/purge (Q14) doesn't removenote_shareseither. Effect: orphan share rows in the owner's own namespace only. No leak.Checked and OK
notes.get{owner_id}(actions.rs:67-79) and roomowner/id(lib.rs:883-895) give the sameNotFound "no such note"for a bad UUID, another user, a missing note or no grant. The old"no note {id:?}"echo is gone. The resolver runs beforeenter(owner)in both join andget_authorized, so no other user's Home is opened.watch::valid_idexcludes/, so the split can't change what an existing ID means.access_innever reads note existence, titles or counts. Every refusal is the samenot_found()with no extra work, so there's no timing difference between "note exists" and "no grant"._arm), AppPassword takes the full path, andcheck_owner_notekeeps one size/scope rule.Developmentis reachable only through--dev-user(main.rs:106,server/auth.rs:78). Owner app passwords are rechecked on every call, with installation binding plusinstallation_notes_scopes(sharing.rs:264-291). Testisolation.rs:1777(#909) covers the real call sites.sharing.rs:570-641,live.rs:257), and rechecks prepared reads (get_authorized). Recipient app passwords are rechecked in the same transaction. Offline devices get durable controls with per-Installation acks, plus a reset for installations idle more than 30 days. I found no lock cycle: grant/revoke take recipient lock then note lock, the frame writer takes only the recipient lock, the room actor takes only the note lock, and eviction runs after the note lock is dropped.acknowledge_controlfilters onrc.recipient_id=caller(:1305),acknowledge_fanoutrunsaccess_infirst, and a non-ownerrevokereturns the samenot_foundas a missing grant.deny_unknown_fieldson every input,roles::can_seebefore granting.Not in these PRs, so not reviewed
Named/link tokens and QR codes (no token generation, hashing, compare, expiry or Referer handling exists yet), folder/tag-scoped grants, and the web share UI. Search, backlinks, previews/unfurls, history, trash, attachments by URL, export, IMAP, webhooks and notifications are not shared-aware yet; they still use only
caller.user, so there is nothing to bypass today. All of these need their own review in 2B/2C/PR 3. Link tokens especially need: ≥128-bit random, stored hashed, constant-time lookup by hash, expiry,Referrer-Policy: no-referrer, and redaction from logs.Verdict: P1 0 / P2 3 / P3 5. Safe to ship in BM14e: yes (P2s are unreachable while the flag is off and actions are unregistered). #909 is fine for the next train after the comment fixes in P3 4–5.
kayg referenced this pull request2026-11-05 23:21:11 +00:00
Landed in dev via
12103f1c97— closing.Pull request closed