Skip to content

feat(sync): snapshot + retention wire format and hub snapshot plane (P4-SYNC-02 part 1, P6-HUB-04 format)#65

Merged
Reederey87 merged 4 commits into
mainfrom
feat/snapshot-plane
Jul 4, 2026
Merged

feat(sync): snapshot + retention wire format and hub snapshot plane (P4-SYNC-02 part 1, P6-HUB-04 format)#65
Reederey87 merged 4 commits into
mainfrom
feat/snapshot-plane

Conversation

@Reederey87

@Reederey87 Reederey87 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

PR 1 of the compaction + snapshot-exchange wave (P4-SYNC-02/P4-HUB-11). Library + backends only — no CLI behavior change, no migration.

What

  • internal/sync/snapshot.go (new): the snapshot.v1 wire format — snapshot document (namespace entries with source-event coords, draft pointers, tombstones, per-device chain anchors, floor map) sealed under the current-epoch WCK with an enc.v2-style XChaCha20-Poly1305 AEAD (AAD binds workspace/producer/kid/HLC/epoch; kid field stays an unauthenticated routing hint). Content-addressed by sha256 of the sealed bytes. Signed retention manifest (per-device floors + snapshot ref + prev_sha256 chain) under the new devstrap:retention:v1 domain.
  • Hub interface grows the retention/snapshot plane: GetRetention/PutRetention (CAS: create-only or If-Match; lost race = ErrRetentionConflict), content-addressed snapshot objects, CompactEventsBelow (strictly below floor; never Seq<=0). Implemented by FileHub, R2Hub, EncryptedHub (pass-through).
  • S3Client grows GetObjectWithETag + PutObjectIfMatch (If-Match CAS — R2-supported S3 extension; sdk v1.104.1 models PutObjectInput.IfMatch); memS3 simulates etags/CAS.
  • Pull now reads the manifest floors (unverified — backends hold no device registry; an unverified floor can only force the snapshot path, where fail-closed import verification lives). A garbled manifest is a hard error, never "no floor".

Trust model (spec/07 + spec/15 updated)

One Ed25519 signature on the manifest covers both objects (manifest names the snapshot by sha256; AEAD binds the plaintext to its carrier). Import verification is fail-closed with no pre-enrollment window — lands in PR 2. A malicious hub can only DoS, never inject state.

Tests

Seal/unseal + per-field AAD tamper matrix (kid-relabel-harmless mirrors enc.v2), manifest sign/verify + tamper matrix + canonical re-parse pin, FileHub/memS3 CAS conflict matrices, floor gates (exact at-floor boundary; fresh device forced to snapshot), both-layouts compaction with unparseable-legacy-keys-KEPT.

Validation

gofmt; golangci-lint run (0 issues); spec-drift (20 specs, 15 files); go test -race ./... green (one pre-existing internal/git flake, passes on rerun).

Follow-up PRs in this wave: import/recovery + migration 00020 (PR 2), hub compact (PR 3), sync acks + tombstone GC (PR 4), hub migrate-events + sweep lock (PR 5).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added snapshot-based sync with sealed, content-addressed snapshot objects and a signed, mutable retention manifest enforcing per-device retention floors.
    • Introduced ETag/CAS conditional updates for retention and snapshot-plane storage to prevent silent overwrites.
    • Added snapshot object lifecycle (put/get/list/delete) plus per-device floor–based event compaction across both layouts.
  • Bug Fixes
    • Fail-closed handling for malformed or hollow retention manifests to prevent incorrect or partial sync.
  • Tests / Documentation
    • Expanded coverage for CAS conflicts, sealing/unsealing, wrong-key verification, snapshot semantics, and compaction; updated sync/security/spec and test plan documentation.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e3846ed-4bc4-4139-900f-a04b53a8dd73

📥 Commits

Reviewing files that changed from the base of the PR and between 4cbfb71 and ddeb0d9.

📒 Files selected for processing (3)
  • internal/hub/r2.go
  • internal/sync/hub.go
  • internal/sync/snapshot.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/sync/snapshot.go
  • internal/sync/hub.go
  • internal/hub/r2.go

📝 Walkthrough

Walkthrough

Adds snapshot sealing, retention-manifest CAS, snapshot-object storage, floor-based pull and compaction behavior, plus supporting S3/R2, encrypted-hub, test, and spec updates.

Changes

Snapshot Exchange and Retention Plane

Layer / File(s) Summary
Snapshot wire format and retention manifest signing
internal/sync/snapshot.go, internal/sync/snapshot_test.go
Defines snapshot and retention-manifest types, sealing/unsealing and parsing helpers, and verification/tamper tests.
Hub contract and FileHub retention plane
internal/sync/hub.go, internal/sync/snapshot_hub_test.go
Extends the Hub interface and FileHub implementation with retention CAS, snapshot object operations, floor-aware Pull, and compaction, with tests.
S3 client CAS/ETag support
internal/hub/s3client_awssdk.go, internal/hub/mems3_test.go
Adds ETag-aware read/write operations to the S3 adapter and in-memory test double.
R2Hub retention manifest, snapshot objects, and compaction
internal/hub/r2.go, internal/hub/r2_snapshot_test.go
Implements retention-manifest CAS, snapshot-object storage/listing, merged floor handling in Pull, and event compaction across both key layouts, with tests.
EncryptedHub pass-through
internal/sync/encryptedhub.go, internal/sync/encryptedhub_test.go
Adds retention/snapshot/compaction forwarding and matching test-double behavior.
Spec and work-log updates
spec/07_NAMESPACE_AND_SYNC_MODEL.md, spec/15_SECURITY_THREAT_MODEL.md, spec/16_TEST_PLAN.md, spec/18_WORK_LOG.md, spec/19_CLOUD_PROVISIONING_GUIDE.md
Documents the snapshot, retention, and R2 storage model changes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • Reederey87/DevStrap#59: Shares the same per-device cursor model and pull-path floor handling that this PR extends with retention manifests and snapshot exchange.
  • Reederey87/DevStrap#25: Both PRs extend internal/sync/encryptedhub.go with additional EncryptedHub pass-through methods.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: snapshot/retention wire format and hub snapshot-plane support for sync.
Description check ✅ Passed The description is detailed and relevant, though it doesn't follow the exact template sections or checklist formatting.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/snapshot-plane

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/hub/r2.go`:
- Around line 548-552: The PutSnapshotObject path in R2Hub currently only
validates the key format and then stores the bytes, so it can accept content
that does not match sha256Hex. Update PutSnapshotObject to compute the SHA-256
of body and compare its hex encoding against sha256Hex before calling
h.retry().do / h.S3.PutObject, returning dssync.ErrInvalidBlobKey on mismatch.
Use the existing R2Hub, PutSnapshotObject, and snapshotKey symbols to locate the
change, and add crypto/sha256 plus encoding/hex imports if needed.
- Around line 594-598: ListSnapshotObjects is returning keys that may not
actually include the required .json suffix because it strips the suffix before
validating the key. Update the object filtering in internal/hub/r2.go so only
objects whose key matches the snapshot JSON filename format are returned, and
keep using the existing symbols ListSnapshotObjects, isValidHexKey, and
dssync.BlobInfo to locate the logic. Ensure bare snapshots/<sha> objects are
excluded and only snapshots/<sha>.json entries are advertised.

In `@internal/sync/hub.go`:
- Around line 371-391: The FileHub retention update in PutRetention is using a
non-atomic read/check/write flow, so concurrent writers can both pass the
etag/absence check and overwrite each other. Update PutRetention to perform the
compare-and-swap under an exclusive lock or equivalent atomic file-CAS
mechanism, using the retentionPath/metaDir handling in FileHub, and keep the
write itself atomic by writing to a temp file and renaming it after the
validation step.
- Around line 349-351: The retention override merge in h.RetentionSeqs is
currently replacing each device floor directly, which can lower an existing
manifest floor and allow incremental pulls across compacted gaps. Update the
merge logic in the retention/floor handling path so the floor for each device is
set to the maximum of the existing value in floors and the override seq,
preserving the stricter floor while still allowing tests to tighten it. Use the
h.RetentionSeqs loop and the floors map as the main reference points when
applying the change.
- Around line 399-410: PutSnapshotObject currently accepts any body for a valid
key and treats any existing file as deduped without checking content, so add a
sha256(body) verification against sha256Hex before writing in
FileHub.PutSnapshotObject. Also, when os.Stat(dst) finds an existing snapshot,
read/verify the stored bytes match the requested hash before returning nil;
otherwise return an error so mismatched content cannot be silently reused.

In `@internal/sync/snapshot.go`:
- Around line 285-292: The UnsealSnapshot flow currently decrypts and unmarshals
the snapshot but does not verify that the plaintext document matches the
authenticated carrier fields. Update UnsealSnapshot, after json.Unmarshal into
Snapshot and before returning, to compare Snapshot.WorkspaceID, ProducedBy, HLC,
Epoch, and the candidate-key KID against the authenticated envelope/carrier
values, and return ErrSnapshotVerification on any mismatch.
- Around line 421-426: ParseRetentionFloors currently returns m.Floors even when
the manifest omits floors, which lets ParseRetentionManifest accept empty or
partial manifests as valid. Update ParseRetentionFloors to fail closed by
rejecting syntactically valid manifests that do not include floors, and make
sure the check is done after ParseRetentionManifest so Pull cannot treat a
malformed retention marker as “no floor.” Use the ParseRetentionFloors and
ParseRetentionManifest symbols to locate the validation path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8ab8aa2-deaf-49f0-9535-c64e3d84eeda

📥 Commits

Reviewing files that changed from the base of the PR and between ca9056a and b020477.

📒 Files selected for processing (15)
  • internal/hub/mems3_test.go
  • internal/hub/r2.go
  • internal/hub/r2_snapshot_test.go
  • internal/hub/s3client_awssdk.go
  • internal/sync/encryptedhub.go
  • internal/sync/encryptedhub_test.go
  • internal/sync/hub.go
  • internal/sync/snapshot.go
  • internal/sync/snapshot_hub_test.go
  • internal/sync/snapshot_test.go
  • spec/07_NAMESPACE_AND_SYNC_MODEL.md
  • spec/15_SECURITY_THREAT_MODEL.md
  • spec/16_TEST_PLAN.md
  • spec/18_WORK_LOG.md
  • spec/19_CLOUD_PROVISIONING_GUIDE.md

Comment thread internal/hub/r2.go
Comment thread internal/hub/r2.go
Comment thread internal/sync/hub.go
Comment thread internal/sync/hub.go Outdated
Comment thread internal/sync/hub.go
Comment thread internal/sync/snapshot.go
Comment thread internal/sync/snapshot.go
@Reederey87

Copy link
Copy Markdown
Owner Author

Codex review findings addressed in the follow-up commit: (P1) structural fail-closed manifest parsing — {}/null-floors/wrong-version/negative-floors are hard errors, never "no floor" (TestParseRetentionManifestStructuralFailClosed, TestFileHubPullFailsClosedOnHollowManifest); (P2) R2Hub.PutRetention disambiguates a 412 via read-back byte comparison so a retry after an ambiguous failure can't lose to its own commit (TestR2PutRetentionOwnCommitReadBack, both If-Match and create-only modes); (P2) FileHub.PutRetention serializes read-check-write under an O_EXCL lock file with atomic temp+rename (TestFileHubPutRetentionConcurrentCAS). Lint 0 issues, sync/hub suites green.

@Reederey87

Copy link
Copy Markdown
Owner Author

Opus review verdict: SHIP — its four findings map 1:1 onto the Codex trio already fixed on the branch. Its remaining nit (AAD domain separation between the snapshot and enc.v2 planes, which share the per-epoch WCK) is now also fixed in the latest commit: snapshotAAD leads with a devstrap:snapshot-aead:v1 length-prefixed tag — free while no sealed snapshot exists anywhere; envelopeAAD deliberately untouched (enc.v2 ciphertext is live). Second nit (memoize the per-Pull retention GET) deferred to the compact/producer PR. Dual review complete: coordinator line-by-line + Codex (gpt-5.5) + opus + CodeRabbit.

@Reederey87 Reederey87 enabled auto-merge (squash) July 4, 2026 02:25
Reederey87 and others added 3 commits July 4, 2026 00:22
…P4-SYNC-02 part 1, P6-HUB-04 format)

- snapshot.v1 sealed document + signed retention manifest (devstrap:retention:v1)
- Hub interface retention/snapshot plane: CAS PutRetention, content-addressed
  snapshot objects, CompactEventsBelow (FileHub + R2Hub + EncryptedHub)
- S3Client GetObjectWithETag/PutObjectIfMatch (If-Match CAS)
- Pull reads manifest floors unverified (DoS-only); garbled manifest fails closed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rse, CAS self-race read-back, FileHub lock-file CAS

Codex review P1: {} / null-floors / wrong-version manifests are now hard
errors, never "no floor". P2s: a retried conditional PutRetention that 412s
against its own commit classifies as success by byte comparison; FileHub's
read-check-write runs under an O_EXCL lock file with atomic install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Snapshots and enc.v2 events share the per-epoch WCK; a leading
devstrap:snapshot-aead:v1 tag makes the AAD separation unconditional
rather than structural. Free now — no sealed snapshot exists yet.
envelopeAAD deliberately untouched (enc.v2 ciphertext is live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Reederey87 Reederey87 force-pushed the feat/snapshot-plane branch from 2a13ee0 to 4cbfb71 Compare July 4, 2026 04:22
…uffix listing, monotonic test override, UnsealSnapshot carrier cross-check

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

Copy link
Copy Markdown
Owner Author

All five CodeRabbit findings applied in the latest commit: PutSnapshotObject on BOTH backends now refuses a body that doesn't hash to its key; ListSnapshotObjects requires the .json suffix before advertising a key; the RetentionSeqs test override may only TIGHTEN a floor (max-merge); and UnsealSnapshot itself now cross-checks the plaintext document's identity fields against the AAD-authenticated carrier + the sealing key's kid, so every caller gets the check.

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