Skip to content

P36a: WAL bucket remotes — objio leaf crate + BucketTransport (sc+wal://, sc+s3://) - #126

Merged
tonibergholm merged 17 commits into
mainfrom
p36-wal-bucket-spec
Aug 26, 2026
Merged

P36a: WAL bucket remotes — objio leaf crate + BucketTransport (sc+wal://, sc+s3://)#126
tonibergholm merged 17 commits into
mainfrom
p36-wal-bucket-spec

Conversation

@tonibergholm

Copy link
Copy Markdown
Member

Summary

First slice of P36 (spec: docs/superpowers/specs/2026-08-26-wal-bucket-backend-design.md, ADR-0046): an S3-compatible bucket — or a local directory standing in for one — becomes an sc remote. Immutable content-addressed packs + a parent-linked log + one compare-and-swapped manifest are the entire coordination model: no server, no database, no leader.

  • New leaf crate objio (quarantines the S3 SDK exactly like tlsio quarantines rustls): Bucket trait (conditional get / create-only put / CAS put), DirBucket (local, tests/demos), S3Bucket (aws-sdk-s3 behind a sync facade; credentials via the SDK's standard chain — sc gains no credential surface).
  • walfmt versioned strict codec (manifest + log entries; fail-closed on unknown versions, every length capped).
  • BucketTransport implements the existing Transport trait, so clone/fetch/push work through the untouched sync.rs entry points; sc remote add origin sc+s3://bucket/prefix (or sc+wal://<dir>). Readers trust only the parent chain from the CAS'd manifest — orphan/crash debris is invisible. Partial-clone filter is refused loudly.
  • Hardening: zstd-bomb cap for untrusted bucket reads via new scl_core::pack::read_object_at_bounded (trusted local path stays unbounded per ADR-0039's carve-out); MAX_OBJECT_SIZE caps on all WAL metadata; ref-name validation on log contents.
  • Proofs: two-writer race (one winner, one clean NonFastForward), 8-thread fleet hammer (all land via CAS retry, no coordinator), crash-debris invisibility + seq step-over, sealed-content-stays-ciphertext-in-bucket grep test, CLI integration test.
  • Docs: ADR-0046, CLAUDE.md (dependency rule, capability map P36 row, standing boundary), ROADMAP Deferred (compaction/gc, leases, P36b checkpoints, P36c bucket-backed serve, GCS, streaming, incremental refresh, batched negotiation), THREAT-MODEL bucket trust boundary.

Design provenance: the WAL + manifest-CAS coordination layer is borrowed from walgit / Cursor's Continuity design (evaluated in docs/research/walgit-evaluation.md); sc keeps its end-to-end sealing, which that model lacks.

Test plan

  • cargo test --workspace: 756 passed, 0 failed
  • cargo clippy --workspace --all-targets: clean
  • sc demo --agents 4: zero residue confirmed
  • Live S3 parity: env-gated contract suite (SC_OBJIO_S3_BUCKET + MinIO) — not run in CI; first follow-on

…t asserts (P36a review)

Advisor review of Task 5 caught: staged objects were dropped from the
RefCell before upload, so a transient bucket failure lost them silently
instead of leaving them for a retry; bucket_transport.rs wasn't
rustfmt-clean; and the three new tests were missing the
!broot.exists() cleanup assertion the read-half tests already use.
…review)

pending_packs was only cleared on update_ref's success paths, so a
call that failed (NonFastForward or CAS exhaustion) left its uploaded
pack hashes in the field. A later, unrelated update_ref on the same
long-lived transport (wire::serve keeps one instance per session) then
committed a log entry citing a pack it never actually needed, violating
the WAL's per-entry "packs support these ref updates" invariant.

Fix: take pending_packs out of the shared field into a call-local Vec
at the top of update_ref. It survives that call's internal CAS
retries (unchanged) but is simply dropped on every exit — success or
failure — instead of being explicitly cleared only on success. The
abandoned pack itself stays in the bucket as inert, content-addressed
orphan garbage, which is correct (compaction is a later phase).

Adds a regression test that stages a pack, fails NonFastForward with
it pending, then succeeds an update_ref for an unrelated branch on the
same instance, and decodes the committed log entry straight from the
bucket to assert it references only its own pack.
@tonibergholm
tonibergholm merged commit e0e1619 into main Aug 26, 2026
8 checks passed
@tonibergholm
tonibergholm deleted the p36-wal-bucket-spec branch August 26, 2026 16:41
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.

2 participants