Skip to content

fix(dwn): port the enbox replication-feed Messages wire protocol - #270

Open
LiranCohen wants to merge 2 commits into
mainfrom
fix/messages-feed-wire
Open

fix(dwn): port the enbox replication-feed Messages wire protocol#270
LiranCohen wants to merge 2 commits into
mainfrom
fix/messages-feed-wire

Conversation

@LiranCohen

Copy link
Copy Markdown
Contributor

Fixes #266

What changed

enbox replaced its sync architecture: the speculative B1/B2 projection-root sync (MessagesSync, merkle StateIndex) was deleted upstream (enbox #996/#1035) in favor of replication-log feed sync. This PR ports the read-side wire protocol to meshd's Go DWN client so the daemon can speak it:

  • Messages interface (internal/dwn/messages.go): MessagesQuery (paged feed — filters[], cursor ProgressToken, limit, cidsOnly; reply entries{seq, messageCid, isLatestBaseState, protocol, message?, encodedData?} + cursor/drained/fingerprint, 410 ProgressGap), MessagesRead (by CID, data via the dwn-response header/body split), and MessagesSubscribe through the existing WebSocket subscription manager — including the new reply head + fingerprint snapshot and EOSE/cursor-resume semantics.
  • Plural permissionGrantIds: canonical (deduped, UTF-16 code-unit sorted) grant lists mirrored in both descriptor and signature payload, with the upstream error semantics (ambiguous / empty / non-canonical). Records/Protocols keep the singular path untouched.
  • Shared scope matcher (permission_scope.go): protocol exact-match; protocolPath and contextId both boundary-aware subtree matches; fail-closed on invalid combinations.
  • Feed-grant selection (SelectFeedGrants): active Messages.Read grants only, full scope requires an unscoped grant, protocol-set scope requires protocol-root coverage per protocol, typed error naming any uncovered protocol.
  • Link identity (link_identity.go): ComputeProjectionID (replication-log-feed-v1) and authorization-epoch hashing (messages-read-grants-v1) with byte-exact canonical JSON, plus BuildLinkKey.
  • Terminal subscription error contract: MessagesSubscribeDeliveryAuthorizationFailed maps to an exported ErrSubscriptionAuthorizationRevoked sentinel so callers can react to grant revocation/expiry specifically.

Out of scope by design: dwn.applyReplicatedMessage/push replication and local fingerprint folding — meshd is a remote-reading client with no local DWN store.

Parity testing

Golden fixtures in internal/dwn/testdata/ were generated by executing the actual TypeScript implementation (bun scripts importing enbox sources; fixed Ed25519 seed): fully-signed MessagesQuery/Read/Subscribe messages with byte-identical descriptors + signature payloads and identical CIDs, projection-ID/epoch hash vectors (incl. unicode edge cases that pinned UTF-16 vs bytewise ordering), and grant-ID canonicalization vectors. The independent review regenerated all fixtures from TS and confirmed byte-identical output. Existing Records subscribe contract/gap/hardening tests pass unchanged.

Verification

  • go build ./... — clean
  • go vet ./... — clean
  • go test ./... -count=1 -race — all packages pass, no data races

🤖 Generated with Claude Code

LiranCohen and others added 2 commits July 26, 2026 15:13
Add the Messages interface (Query/Read/Subscribe) to the Go DWN client,
matching the enbox dwn-sdk-js wire shapes at HEAD:

- MessagesFilter with exact wire keys (interface, method, protocol,
  protocolPathPrefix, contextIdPrefix, messageTimestamp{from,to}), empty
  filters rejected, filters field always present on the wire.
- Plural permissionGrantIds invocation: canonical form is deduplicated and
  sorted by UTF-16 code-unit order (JavaScript string order, NOT UTF-8 byte
  order — astral characters sort before U+E000..U+FFFF); the same canonical
  list is mirrored in descriptor and signature payload
  {descriptorCid, permissionGrantIds}. Ambiguous/empty/not-canonical error
  semantics mirror the SDK. The singular permissionGrantId path for
  Records/Protocols is untouched; Messages ops never use
  authorDelegatedGrant.
- Client.MessagesQuery with typed reply (entries incl. seq/messageCid/
  isLatestBaseState/protocol/encodedData, ProgressToken cursor, drained,
  fingerprint), 410 replies surfaced as ProgressGapError (now exported and
  shared with the subscription machinery), 429 mapped to ErrRateLimited.
- Client.MessagesRead with the dwn-response header/body data split, 404
  mapped to ErrMessagesEntryNotFound.
- SubscriptionManager.SubscribeMessages[WithLifecycle]: MessagesSubscribe
  descriptor (filters + permissionGrantIds + cursor) over the existing
  rpc.subscribe.dwn.processMessage / rpc.ack / rpc.subscribe.close
  plumbing; subscribe-reply head + fingerprint surfaced on the established
  lifecycle event; terminal error frames with code
  MessagesSubscribeDeliveryAuthorizationFailed or
  MessagesSubscribeDeliveryFailed wrap the new
  ErrSubscriptionAuthorizationRevoked sentinel so callers can react to
  grant revocation/expiry specifically.
- Shared permission scope matcher ported exactly from the SDK
  (PermissionScopeMatcher.matches + boundary-aware matchesSubtree,
  fail-closed combinations).
- Feed-grant selection (SelectFeedGrants) per the agent rules: active
  Messages/Read grants only, full scope needs an unscoped grant,
  protocol-set scope needs protocol-root coverage per protocol, invoked
  set is all participating grants sorted by ID; typed
  FeedGrantCoverageError names the uncovered protocol.
- Link identity hashing: ComputeProjectionID
  (replication-log-feed-v1), ComputeOwner/DelegateAuthorizationEpoch
  (messages-read-grants-v1) as base64url-no-pad SHA-256 over canonical
  alphabetical-key JSON without HTML escaping, and BuildLinkKey joined
  with '^'.

Refs #266

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tests

Golden fixtures under internal/dwn/testdata were generated by executing the
ACTUAL enbox TypeScript implementation (dwn-sdk-js and agent sources at
~/src/enboxorg/enbox, main) with bun, using a fixed Ed25519 key (seed
0x01..0x20) and fixed message timestamps; each fixture file records the
generation command in its generatedBy field.

Parity assertions:
- Fully-signed MessagesQuery/MessagesRead/MessagesSubscribe fixtures: the Go
  builders produce byte-identical descriptors and signature payloads and the
  same descriptor CIDs; Go's dag-cbor CID discipline reproduces the TS
  message CID over each TS-signed message. (Full-JWS bytes are not compared:
  the SDK serializes the protected header as {"kid","alg"} while meshd emits
  {"alg","kid"} — both valid, the server never compares header bytes.)
- permissionGrantIds canonicalization vectors incl. UTF-16 vs UTF-8 ordering
  edge (astral vs U+FFFD) and empty/ambiguous error codes.
- projectionId/authorizationEpoch/linkKey vectors incl. unicode protocol
  URIs, HTML-escaping hazards (& < >), and optional dateGranted omission.

Contract tests mirror the existing fake-server idiom: Messages subscribe
handshake (canonical grant IDs in descriptor AND payload, cursor in the
signed descriptor, reply head/fingerprint on the established lifecycle
event) and terminal delivery error-code mapping to
ErrSubscriptionAuthorizationRevoked. Client tests cover query reply parsing
(drained/fingerprint/cidsOnly), the 410 ProgressGap shape, rate limiting,
and the MessagesRead dwn-response data split. Table tests mirror the SDK's
scope matcher behavior and the agent's feed-grant selection rules.

Refs #266

Co-Authored-By: Claude Fable 5 <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.

Port the enbox replication-feed wire protocol to internal/dwn (Messages interface, plural permissionGrantIds, scope matcher)

1 participant