Repository navigation
feat(sync): 10.3c wrap fan-out core + the receiver's not-for-me guard - #466
Merged
Merged
Conversation
The producer half 10.3b never built. Pure + injected like the rest of main/sync, so every rule is testable without a vault. CORRECTS THE SETTLED DESIGN. The rung chose a per-DEVICE inbox channel on the premise that the relay already sees device keys as the frame `sender`, so a device-scoped channel leaks nothing new. Reading the code says otherwise: PipelineContext.devicePub is session.identity.publicKey, the sovereign USER key, so every device of one identity sends under the SAME sender and the relay cannot currently tell them apart. A per-device channel would hand a blind relay a brand-new identifier revealing how many devices an identity has and which frames are whose. That is a real privacy regression, and it buys only the bandwidth of a sibling dropping a frame addressed to someone else. So: sibling wraps ride the existing identity inbox, and the receiver drops what is not addressed to it. The drop is EXACT, not a decryption failure — MemberWrapPayload carries recipientPubB64, so it is one string compare. That guard is also the load-bearing fix here. installWrap called unwrapMemberWrapWithType with no recipient check at all, so a sibling's wrap would have thrown once per entity per device. It doubles as defence in depth on the cross-user path, where a wrap addressed to another member was likewise never ours to open. Ordinal rule, inverted from what the rung anticipated: the hazard is not that a real wrap loses to a placeholder, but that fanning out an entity still ON the placeholder ships version 0, which every receiver rejects forever. Refused before any emit. Never throws: one unreachable device must not abort the rest, or a backfill dies on its first failure and strands every later entity. REMAINING (this is the core, not the whole rung): wiring the ongoing producer into installEntityWrap, the pairing-completion backfill with IE-11 yield/progress/abort, and BOTH declared gates. Not to be scheduled ahead of rotate-on-revoke: every device this reaches keeps every DEK it is handed. 9 tests written failing first. 781 pass across sync + collab; typecheck and lint green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
th3-br41n
added a commit
that referenced
this pull request
Aug 3, 2026
…ices (#467) The core landed in #466 with nothing calling it. This wires it, because a mechanism with no caller is the exact bug class this repo keeps filing (Tool-5's approval that could never be minted, Tool-8's prompt with no receiver) — so it ships wired or not at all. SharingEngine.fanOutEntityWrapToSiblings owns the seam: it already holds the relay and the pipeline ctx. It resolves the active device roster, seals the DEK for every sibling, and emits on the IDENTITY inbox per #466's corrected addressing. Returns null when there is nothing to do (no relay, no siblings) so the caller can tell "not delivered" from "nothing to deliver". installEntityWrap then calls it after the local install. Two deliberate choices there: - Not awaited into the caller's failure path. A sibling that is offline must not fail a local write; the pairing backfill is the catch-up path. - The version comes from the DEK store, not the caller. The retro-wrap pass calls installEntityWrap with a PLACEHOLDER dek, and fanning that out would ship version 0 — which every receiver rejects forever. Reading the real ordinal lets fanOutEntityWrap refuse it, and a test pins exactly that. Found while testing: makeCtx reads the DEK store, and this method can be the first thing to touch sharing on a session, so it now calls ensureDekStore rather than assuming. That was a real defect, not a test accommodation — an entity created before anything else touched sharing would have thrown. Three integration tests over a real VaultSession + loopback relay, so they would have failed against the pre-#466 state rather than passing the way a pure-module test would. 1208 pass across collab/sync/entities/integration; typecheck and lint green. REMAINING on the rung: the pairing-completion backfill (entities that existed BEFORE pairing are still stranded) and both declared gates. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The producer half
10.3bnever built. Pure + injected like the rest ofmain/sync, so every rule is testable without a vault.This corrects the settled design
The rung chose a per-device inbox channel, on the premise that the relay already sees device keys as the frame
senderand so learns nothing new.That premise is wrong.
PipelineContext.devicePubissession.identity.publicKey— the sovereign user key. Every device of one identity sends under the samesender, so the relay currently cannot tell them apart. A per-device channel would hand a blind relay a brand-new identifier revealing how many devices an identity has and which frames belong to which. That is a real privacy regression, and it buys only the bandwidth of a sibling dropping a frame addressed to someone else.So sibling wraps ride the existing identity inbox, and the receiver drops what is not addressed to it. The drop is exact, not a decryption failure —
MemberWrapPayloadcarriesrecipientPubB64, so it is one string compare.The guard is load-bearing, not hygiene
installWrapcalledunwrapMemberWrapWithTypewith no recipient check at all. A sibling's wrap would have thrown once per entity per device. It also hardens the cross-user path, where a wrap addressed to another member was likewise never ours to open.Ordinals — inverted from what the rung anticipated
The hazard is not that a real wrap loses to a placeholder.
installEntityDekalready takes strictly-newer only, so a real ordinal wins. The hazard is fanning out an entity that is itself still on the placeholder: that ships version 0, which every receiver rejects forever, leaving the entity looking fanned-out while staying unreadable everywhere. Refused before any emit.Never throws
One unreachable device must not abort the rest, or a backfill dies on its first failure and strands every later entity. Failures are returned, not raised.
Explicitly remaining — this is the core, not the whole rung
installEntityWrap(still hard-codes the local device as sole recipient)IE-11yield/progress/abort<gates: security-review, pentest>And the standing caveat: do not schedule this ahead of rotate-on-revoke. Every device this reaches keeps every DEK it is handed.
Verification
9 tests written failing first · 781 pass across sync + collab · typecheck + lint green.
🤖 Generated with Claude Code