Skip to content

feat(sync): 10.3c wrap fan-out core + the receiver's not-for-me guard - #466

Merged
th3-br41n merged 1 commit into
mainfrom
feat/10-3c-wrap-fanout
Aug 3, 2026
Merged

th3-br41n merged 1 commit into
mainfrom
feat/10-3c-wrap-fanout

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

The producer half 10.3b never built. Pure + injected like the rest of main/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 sender and so learns nothing new.

That premise is wrong. PipelineContext.devicePub is session.identity.publicKey — the sovereign user key. Every device of one identity sends under the same sender, 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 — MemberWrapPayload carries recipientPubB64, so it is one string compare.

The guard is load-bearing, not hygiene

installWrap called unwrapMemberWrapWithType with 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. installEntityDek already 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

  • Wiring the ongoing producer into installEntityWrap (still hard-codes the local device as sole recipient)
  • The pairing-completion backfill with IE-11 yield/progress/abort
  • Both declared gates — <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

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
th3-br41n merged commit 5ba1b42 into main Aug 3, 2026
3 checks passed
@th3-br41n
th3-br41n deleted the feat/10-3c-wrap-fanout branch August 3, 2026 12:11
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant