design+feat: ADR-0029 + per-repo blob membership index substrate (consolidates #88 + #89)#91
Open
jamestexas wants to merge 2 commits into
Open
design+feat: ADR-0029 + per-repo blob membership index substrate (consolidates #88 + #89)#91jamestexas wants to merge 2 commits into
jamestexas wants to merge 2 commits into
Conversation
…y for OCI registry surface (cloister-7c0a0b) Drafts the architectural fix for the P0 finding from cloister-667ea6 adversarial review: the OCI pull surface has no per-repo membership boundary, so an unauthenticated caller can probe HEAD/GET against any digest and learn whether *any* tenant pushed it (then pull the bytes). Architectural constraint that shapes the design: per src/blob-store.ts docstring (lines 14-20), the singleton BlobStore is LOAD-BEARING for ADR-0003 — partitioning storage per-repo would break the content- addressed monoid axiom that the cross-DO orchestrator (BlobStore.put → BeadStore.bead_create → TrustStore.applyAttestation) depends on. The proposal therefore lifts the boundary ORTHOGONAL to storage: a `(repo, blob_digest)` membership index on TrustStore, consulted by the OCI pull surface before BlobStore.get. Pulls without membership return constant-shape 404 (matches the §9.4 "exists but not yours" precedent from threat-model.md / ADR-0024). Pushes write membership as a side effect under the ADR-0012 cross-DO handoff discipline. The ADR also enumerates the four follow-up beads (lazy backfill, manifest-walk grant, peerFp propagation, catalog auth-gate) that together close the cluster of related findings. Status: Proposed. No code changes — this PR is the design pass that PR #87 recommended. Implementation lands as separate beads once the ADR is reviewed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…bstrate (cloister-7c0a0b, ADR-0029)
First implementation slice of ADR-0029 (per-repo membership boundary for
the OCI registry surface). This PR ships the storage layer ONLY — the
membership table, helpers, and TrustStore RPC surface. Two follow-ups:
the pull-side gate (consult before BlobStore.get) and the push-side
write (record after BlobStore.put) land as separate beads.
Why split into slices: per ADR-0029, the design has multiple coupling
seams (pull-side check, push-side record, lazy backfill for pre-existing
blobs, manifest-walk transitive grant, peerFp propagation, _catalog
auth-gate). Each is independently testable and reviewable. Substrate
first; the seams that consume it land next.
Changes:
1. **src/storage/registry-membership.ts** — pure-function helpers over
an injected SQL executor, mirroring src/storage/registry-tags.ts:
- SCHEMA_REGISTRY_BLOB_MEMBERSHIP — table DDL with (repo, digest,
kind) PK + idx_membership_digest secondary index
- recordMembership(sql, repo, digest, kind, nowMs, peerFp?) —
UPSERT-on-conflict refreshes recorded_at + recorded_by in place
- hasMembership(sql, repo, digest, kind): boolean — the pull-side
probe; must return constant-shape 404 when false per §9.4 / ADR-0024
- listReposWithMembership(sql, digest) — diagnostic helper for GC
reachability scans and operator forensics
2. **src/trust-store.ts** — wires the table into the TrustStore schema
migration and exposes three RPC methods: recordRegistryMembership,
hasRegistryMembership, listRegistryReposWithMembership. RPC surface
thin-wraps the helpers. Sibling shape to upsertRegistryTag et al.
3. **test/storage/registry-membership.test.ts** — 10 tests covering:
- round-trip (record then has)
- empty-substrate has-check returns false
- **CROSS-TENANT GUARANTEE** — recording under REPO_A does NOT grant
REPO_B membership (the core ADR-0029 invariant; if this ever
accidentally passes, the multi-tenant story is broken)
- blob/manifest kinds independent
- idempotency on (repo, digest, kind) — no duplicate-row error
- UPSERT refreshes recorded_at + recorded_by
- dev-mode null peerFp accepted
- listReposWithMembership lex-ordered, no double-count across kinds,
empty-when-empty
Tests exercise real workerd SqlStorage (via runInDurableObject on a
BEAD_STORE stub) — same pattern as test/storage/workerd.test.ts —
so PRIMARY KEY, ON CONFLICT, ORDER BY semantics are validated against
the actual engine, not an in-memory shim.
Verification: task lint → 1136/1136 tests pass (was 1126 on main, +10
from this PR's new file).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 24, 2026
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.
Consolidates #88 (ADR-0029) + #89 (membership substrate) into one unit. Will close those PRs.
Two commits, one unit of work
Commit 1 — ADR-0029: per-repo membership boundary design.
Surfaced by the cloister-667ea6 adversarial review (PRs #83-#87): the OCI pull surface has no per-repo membership boundary, so any anonymous caller can probe digest existence cross-tenant against the singleton
BlobStore. The architectural constraint that shapes the fix:src/blob-store.ts:14-20declares the singleton BlobStore load-bearing for ADR-0003's content-addressed monoid axiom — partitioning storage per-repo would break the cross-DO orchestrator. ADR-0029 therefore lifts the boundary orthogonal to storage: a(repo, blob_digest, kind)membership index on TrustStore, consulted by the OCI pull surface before BlobStore.get; constant-shape 404 on miss (matches §9.4 / ADR-0024 precedent).Commit 2 — substrate layer: the storage + RPC for that index.
src/storage/registry-membership.ts— pure-function helpers (recordMembership,hasMembership,listReposWithMembership) + DDL. Mirrorsregistry-tags.ts.src/trust-store.ts— wires the table into the schema migration; exposes 3 RPC methods.test/storage/registry-membership.test.ts— 10 tests against real workerd SqlStorage. Includes the CROSS-TENANT GUARANTEE test (recording under REPO_A does NOT grant REPO_B membership) — the core ADR-0029 invariant.What lands next (separate beads)
This PR is slice 1 of 5 per ADR-0029 §"Implementation plan":
hasRegistryMembershipbefore BlobStore.get/has.registry_tagsfor pre-existing blobs./v2/_catalog+/v2/<name>/tags/list.Each is independently shippable. This PR establishes the model that makes them coherent.
Test plan
task lint→ 1136/1136 (main baseline) — the membership test file is included; the +10 tests are present🤖 Generated with Claude Code