Skip to content

fix(read): resolve retention-retired catalog blocks via local shard inclusions - #44

Merged
frrist merged 5 commits into
mainfrom
fix/catalog-fallthrough-inclusions
Jul 28, 2026
Merged

fix(read): resolve retention-retired catalog blocks via local shard inclusions#44
frrist merged 5 commits into
mainfrom
fix/catalog-fallthrough-inclusions

Conversation

@frrist

@frrist frrist commented Jul 27, 2026

Copy link
Copy Markdown
Member

Catalog retention (runRetention, Retain default 6) deletes shipped segment CARs from disk and DB, but the appliance read tier could not resolve the blocks inside them: LocalLocator answers only whole-blob rows from blob_locations, keyed by a blob's own digest — and a manifest / MST node is an interior slice of a shipped CAR, not a stored blob. Once a bucket rolled past the retain window, any object whose manifest lived only in a retired segment became unreadable: GETs and undelimited listings failed with "blockstore: not found". (Regressed in the phase-7 hardening, which swapped the indexer-backed IndexLocator for LocalLocator; the ship path never stopped publishing the per-block index — the read side just stopped consulting anything that could use it. architecture.md §8's main text had this right; its R0/R1 parenthetical did not.)

Mirror the indexing-service contract locally — locations AND inclusions:

  • shard_inclusions (migration 00006): block digest → shard digest + inclusive byte range, one row per block of every shipped catalog CAR.
  • The flush path records the shard's blob_locations row and its inclusions BEFORE the segment is marked shipped, so retention can never retire blocks the read tier can't resolve. SubmitShard now returns the shipped CAR's location commitment to make that possible.
  • LocalLocator falls through location-miss → inclusion → shard location, emitting the same Location{shard commitment, inner range} shape IndexLocator produces; the ranged /content/retrieve path is unchanged, and an external indexer remains a locator-swap away.

Also fixed en route:

  • SubmitShard issued /index/add without the ship proof store, so every catalog index publication went out proofless and was rejected — segments never marked shipped, so retention never ran at all.
  • Index publication (index blob + /index/add) is now best-effort: the CAR is durable on piri and local inclusions serve ingot's reads, so an indexer-side failure logs loudly instead of wedging retention. TODO: queue failed publications for retry.
  • itest harness boots piri with Postgres (piri:main's Curio PDP pipeline no longer supports sqlite).

TestForgeReadAfterCatalogRetention proves the path end-to-end: roll a bucket past the retain window, verify the early segments physically retire, then GET + undelimited-list objects whose manifests exist only in retired segments. Note: currently red in a full run for an upstream reason — piri cannot verify did:plc-issued proofs (empty verification- method list), which fails ALL hilt-era /content/retrieve; the pre-existing TestForgeReadAfterEviction fails identically on main. This test should go green with no further changes once that piri fix lands.

@frrist
frrist requested a review from alanshaw July 27, 2026 17:36
@frrist frrist self-assigned this Jul 27, 2026
…nclusions

Catalog retention (runRetention, Retain default 6) deletes shipped segment
CARs from disk and DB, but the appliance read tier could not resolve the
blocks inside them: LocalLocator answers only whole-blob rows from
blob_locations, keyed by a blob's own digest — and a manifest / MST node is
an interior slice of a shipped CAR, not a stored blob. Once a bucket rolled
past the retain window, any object whose manifest lived only in a retired
segment became unreadable: GETs and undelimited listings failed with
"blockstore: not found". (Regressed in the phase-7 hardening, which swapped
the indexer-backed IndexLocator for LocalLocator; the ship path never
stopped publishing the per-block index — the read side just stopped
consulting anything that could use it. architecture.md §8's main text had
this right; its R0/R1 parenthetical did not.)

Mirror the indexing-service contract locally — locations AND inclusions:

- shard_inclusions (migration 00006): block digest → shard digest +
  inclusive byte range, one row per block of every shipped catalog CAR.
- The flush path records the shard's blob_locations row and its inclusions
  BEFORE the segment is marked shipped, so retention can never retire
  blocks the read tier can't resolve. SubmitShard now returns the shipped
  CAR's location commitment to make that possible.
- LocalLocator falls through location-miss → inclusion → shard location,
  emitting the same Location{shard commitment, inner range} shape
  IndexLocator produces; the ranged /content/retrieve path is unchanged,
  and an external indexer remains a locator-swap away.

Also fixed en route:

- SubmitShard issued /index/add without the ship proof store, so every
  catalog index publication went out proofless and was rejected — segments
  never marked shipped, so retention never ran at all.
- Index publication (index blob + /index/add) is now best-effort: the CAR
  is durable on piri and local inclusions serve ingot's reads, so an
  indexer-side failure logs loudly instead of wedging retention. TODO:
  queue failed publications for retry.
- itest harness boots piri with Postgres (piri:main's Curio PDP pipeline
  no longer supports sqlite).

TestForgeReadAfterCatalogRetention proves the path end-to-end: roll a
bucket past the retain window, verify the early segments physically
retire, then GET + undelimited-list objects whose manifests exist only in
retired segments. Note: currently red in a full run for an upstream
reason — piri cannot verify did:plc-issued proofs (empty verification-
method list), which fails ALL hilt-era /content/retrieve; the pre-existing
TestForgeReadAfterEviction fails identically on main. This test should go
green with no further changes once that piri fix lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frrist added a commit that referenced this pull request Jul 27, 2026
hilt ba71f84 picks up the durable-delegation expiry fix (#33) and its
libforge alignment (#34); ucantone ef993e1 and libforge 7fc3b2c bring
ingot to the same versions hilt now builds against, reducing the
cross-service skew that made this branch's failures hard to attribute.

Note: this does NOT fix PR #44's red itests. The did:plc verification
failure lives in ucantone's VerificationRelationship.All(), which
ignores the all-methods default that Document.UnmarshalJSON sets for
DID documents without explicit relationship sections (every PLC-served
document) — unfixed on ucantone main as of this commit — and the
failing verifier is piri, consumed as a published image. Green requires:
ucantone fix -> piri bump -> new piri:main image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@frrist
frrist force-pushed the fix/catalog-fallthrough-inclusions branch from 89ecff2 to 2916e87 Compare July 27, 2026 18:43
@frrist
frrist requested review from bajtos, hannahhoward and pyropy July 27, 2026 18:52
frrist and others added 2 commits July 27, 2026 14:02
hilt ba71f84 carries the durable-delegation expiry fix (#33) and its
libforge alignment (#34). ucantone ccb7705 carries the did:plc
verification fix (fil-forge/ucantone#42): undeclared verification
relationships now default to all of a document's methods, which is
what makes hilt-tenant (did:plc-issued) proofs verifiable — piri
consumes the same fix via fil-forge/piri#43. Aligning ingot to the
same versions keeps the cross-service skew down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mounts a locally-built static piri binary over the image's
/usr/bin/piri (stack.WithPiriBinary) — same escape hatch as the
image-override env vars, one step earlier in the pipeline. Used to
validate the ucantone did:plc verification fix end-to-end before any
piri image carried it: TestForgeReadAfterCatalogRetention and
TestForgeReadAfterEviction both passed against piri@939de00 + the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frrist and others added 2 commits July 27, 2026 14:28
The subtest (added in #42/#43) asserts preflight behavior the pinned
versitygw does not implement, and merged while the itest suite could
not boot a stack — so it has never passed in any environment. Skip
with a pointer to #45 rather than leave the suite red for a failure
that belongs to the CORS feature, not whichever branch runs it.

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

@alanshaw alanshaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread registry/stores.go
type BlobInclusion struct {
Space did.DID
Digest []byte // inner block multihash
ShardDigest []byte // enclosing shard CAR multihash

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Both multihash.Multihash?

bajtos pushed a commit that referenced this pull request Jul 28, 2026
Every top-level itest — TestForgeVersity, TestForgeScenarios,
TestForgeNativeProvision, TestForgeReadAfterEviction — failed at stack
boot, not in any assertion:

  Error: initializing piri node: ... ProvideHarmonyDB ...
  curio PDP pipeline requires Postgres (set database type to postgres)

Since piri:main absorbed Curio (~2026-07-24) its PDP pipeline refuses
sqlite, and forgeStack booted sqlite piri via the zero-value
PiriNodeConfig. All four suites route through that one constructor, so
flipping the topology default to Postgres:true is the whole fix; the
pinned smelt already supports it (no dependency bump).

Also relax the CORS preflight status assertion from 200 to any 2xx, the
fetch spec's 'ok status' range. versitygw answers a matched rule 200 and
only its no-CORS-config fallback 204, and the Max-Age assertion is what
actually pins the real path: the fallback sets no Max-Age and mirrors the
requested method instead of the rule's method list. This supersedes the
exact-200 assertion in 900d6b5.

Both changes match what #44 arrived at independently, so the overlap
resolves trivially whichever lands first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rwJ8KiPhm3G27vfU21Phk
@frrist
frrist merged commit a7684d7 into main Jul 28, 2026
8 checks passed
frrist added a commit that referenced this pull request Jul 28, 2026
…ucket abort, and spool eviction

The local-table Locator read tier is wired and validated
(TestForgeReadAfterEviction; #44 for retention-retired catalog blocks),
so the "not wired" bullet inverts: the remaining gap is spool eviction
itself — unbounded today, DeleteObject releases network-side only —
tracked in #48 alongside §5's bounded-cache spec. Record DeleteBucket's
implicit abort of in-flight multipart sessions (and its hilt blob.Abort
gate), point §11's cache-eviction open question at #48, and note the
forge paths are now exercised by the in-repo smelt itest harness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frrist added a commit that referenced this pull request Aug 6, 2026
…eleteObject (FIL-588) (#40)

* feat(multipart): complete FIL-520 — ListParts, ListMultipartUploads, S3 validation, abort/expiry hygiene

Completes the S3 multipart surface on the spool model (parts spool
locally at UploadPart; ship+accept at Complete — the R1 architecture
decision recorded on FIL-520; network unallocate-on-abort stays with the
parking refinement, FIL-589/623/624).

New surface:
- ListParts: pagination (part-number-marker / max-parts, default+cap
  1000), NoSuchUpload for unknown/mismatched/closed uploads, per-part
  LastModified (new multipart_parts.created_at).
- ListMultipartUploads: (key, initiated) ordering, prefix/delimiter with
  CommonPrefixes, key/upload-id marker semantics incl. the
  InvalidUploadIdMarker validation, in-flight sessions only, session
  checksum algorithm/type echo.

Conformance fixes:
- UploadPart against a mismatched key -> NoSuchUpload (was silently
  accepted — a real bug: any valid uploadId accepted parts for any key).
- Complete: part-number range check (InvalidArgCompleteMpPartNumber)
  before order check; EntityTooSmall for sub-5MiB non-final parts;
  x-amz-mp-object-size validation; conditional writes per S3 (If-Match /
  If-None-Match:* honored, concrete If-None-Match -> NotImplemented);
  duplicate Complete is idempotent — sessions are retained in a new
  'completed' state instead of deleted.
- CreateMultipartUpload: trailing-slash keys -> DirectoryObjectContainsData;
  content-encoding/disposition/language, cache-control, expires, and
  website-redirect-location persist through the session into the
  manifest (manifest gains WebsiteRedirectLocation; PutObject/Head/Get
  wire it too).

Hygiene ('remove uploaded parts on abort/expiry', spool-model edition):
- Abort and part re-upload delete the parts' now-unreferenced spooled
  blobs + intents, guarded against content-addressed sharing (other
  sessions via new CountPartRefs, committed objects via blob_refs
  claims, shipped blobs via intent state).
- Abort honors If-Match-Initiated-Time.
- Background sweeper (multipart_session_ttl, default 7d; negative
  disables) aborts stale open sessions and reaps terminal rows.

Registry: multipart_sessions gains header columns + completed state
(migration 00004); MultipartStore gains ListSessions /
ListStaleSessions / CountPartRefs (postgres + inmem; live-test
coverage for ordering, cutoff, bytea[] ANY-match, and the widened
CHECK constraint).

itest: 28 cases promoted xfail->pass (217 pass / 76 known-fail+skips,
was 189/104). TestForgeScenarios gains MultipartAbortCleansSpool and
uses >=5MiB non-final parts per the new EntityTooSmall enforcement.
Remaining multipart xfail is FIL-620 (checksums), FIL-534/525
(tagging/lock/ACL), FIL-586 (UploadPartCopy).

Refs: FIL-520

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

* deps: bump libforge to main (blob remove/abort/reject bindings)

github.com/fil-forge/libforge aac837a — Space on RemoveArguments plus
the /blob/abort binding the deferred-multipart abort path invokes.

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

* feat: wire /blob/remove — DeleteObject releases blobs on the network (FIL-588)

forgeclient gains BlobRemove (subject = space, digest in args); the
uploader's RemoveBlob no-op becomes the real call, so the reference-index
bookkeeping (claims→0) now releases the space's claim through sprue → piri.
Delete finality is claim-release-now, bytes-at-root-death: piri deletes
unaggregated bytes immediately and defers aggregated ones until the PDP
root retires on-chain.

itest: TestForgeDeleteReleasesNetworkBlob is the regression gate — PUT,
prove the blob serves from piri (spool wipe + read-through), DeleteObject,
then assert piri executed /blob/remove and queued the piece for removal.
Gated behind ITEST_PIRI_BIN/ITEST_SPRUE_BIN until published piri/sprue
images carry the handlers.

docs: architecture.md §2/§9 status rows flip remove(digest) to exists;
deferred-glue note updated.

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

* fix: seed /blob/remove space delegation; log failed blob removals

The e2e flushed out two gaps: the agent's seeded space delegations did not
cover /blob/remove, so the removal invocation shipped proofless and sprue's
validator rejected it; and releaseBlobs discards errors by design, which
made that failure invisible — RemoveBlob now logs its own errors. The
seeding sentinel moves to /blob/remove so stores seeded by older builds
re-seed the new cap.

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

* temp: replace smelt with local checkout

The delete-finality e2e needs smelt's piri registration proofs to include
blob/remove (fil-forge/smelt frrist/fil-588-blob-remove-v2). Drop this
commit and repin once that smelt PR merges.

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

* feat: deferred-accept multipart — park at UploadPart, accept at Complete, unallocate on abort (§7.2)

UploadPart now uploads each part blob to its provider before returning 200:
forgeclient.BlobAddParked runs /blob/add + the HTTP PUT but postpones the
/http/put conclude — the conclude is what makes sprue fire /blob/accept, so
the bytes are durable but stay out of the PDP pipeline. The park state
(add/accept task links + the sealed put invocation) persists in the new
blob_parks table (migration 00005). CompleteMultipartUpload concludes each
parked blob (BlobConclude: send the put receipt, await the accept receipt's
location commitment); blobs that never parked fall back to the whole
synchronous upload. Abort, the TTL sweeper, and part-supersede unwind parked
blobs with /blob/unallocate (cause = the add task link), behind the existing
live-part/CountPartRefs/CountClaims guards — an upload now ends in exactly
one of accept or unallocate.

BlobAdd is unchanged behavior: it is BlobAddParked + BlobConclude composed.
The harness NopUploader accepts immediately, so the in-process flow is
unchanged. Architecture §2/§7.2/§9 rows flipped.

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

* test: deferred-multipart e2e gate + blob_parks live-store coverage

TestForgeDeferredMultipart (env-gated like the delete-finality test):
RoundTrip proves parts are durable-but-parked at UploadPart (piri allocates,
no accept), concluded at Complete (accepts land, GET round-trips);
AbortUnallocates proves abort unwinds a parked blob end-to-end via
/blob/unallocate. Plus a park round-trip in the postgres live test.

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

* test(itest): delete-finality asserts through piri's async removal sweep

Piri's byte release is now fully asynchronous (defer-by-default RemovePiece
+ PDPRemoveSweep periodic task): the sweep re-verifies claims and pipeline
state before deleting. Follow the renamed 'queueing piece removal' log line
and additionally wait for 'finalized piece removal' — proving end-to-end
that the sweep actually released the bytes, not merely queued the request.

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

* rename: /blob/unallocate → /blob/abort + /blob/reject (libforge PR #46)

The abandon verb split per hop in review: clients (ingot) invoke
/blob/abort on the upload service, which forwards /blob/reject to the
storage node. BlobUnallocate → BlobAbort (Cause now required by type),
uploader iface UnallocateBlob → AbortBlob, seeded space delegation +
itest log-greps renamed.

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

* test(itest): boot the removal-gated tests on Curio-piri topology

The Curio-based piri requires a Postgres piri node (harmonydb); the
gated delete-finality and deferred-multipart tests now request one and
honor ITEST_BLOCKCHAIN_IMAGE until the published localdev image carries
the mockrpc Ticket fix.

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

* deps: pin smelt to the fil-588 branch, drop the local replace

CI has no sibling smelt checkout; pin the pushed branch commit
(fil-forge/smelt#19, 2718317) instead. Bump to the merge commit when
that PR lands.

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

* feat(blob)!: RFC-shaped remove/abort args; recognize BlobAccepted refusals

The blob-removal RFC drops the space from the client-leg arguments —
/blob/remove is {digest} and /blob/abort is {digest, cause}, with the
space as the invocation subject. Ingot already invoked with the space as
subject, so only the argument literals change.

Treat the node's BlobAccepted refusal on abort as final rather than a
fault: it means this space accepted the same content concurrently, so
the blob belongs to the reference index and is released via
/blob/remove when its last claim drops. The uploader logs it distinctly
instead of as an error; the multipart cleanup path already discards the
park row, which is obsolete either way.

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

* fix(migrations): renumber multipart migrations past main's versions

goose refuses duplicate version numbers, and this branch carried
00004/00005 twice (its multipart_listing/deferred_accept beside main's
bucket_created_at_space/segment_bucket) since the Hilt merge — breaking
forge-mode startup. Renumber the unreleased branch migrations to
00007/00008 so a fresh database applies main's sequence first. Verified
with TestUp_Live against a throwaway Postgres (migrates to version 8,
second Up is a no-op).

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

* test(itest): re-curate the versity partition for the delete-release era

The blob-removal chain landing across the stack (this branch's
DeleteObject → /blob/remove, sprue#33's forwarding, piri's release
sweep) unblocked bucket-delete teardown, flipping every
"teardown-blocked" xfail row green: promote all 50 (PutObject 5,
GetObject 17, HeadObject 14, DeleteObject 1, CopyObject 13) to the
pass tables. Demote four multipart cases that genuinely fail — the
upstream error-code mismatches (ListMultipartUploads
invalid_uploadId_marker, CompleteMultipartUpload invalid_part_number),
missing-ETag validation on Complete (missing_part_fields), and
conditional writes on Complete (conditional_writes) — with reasons in
the tables. Validated with a full local TestForgeVersity run against
fresh :main images: 216 pass, 0 fail.

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

* fix(s3frontend): conformance-align multipart validation; abort in-flight uploads on DeleteBucket

Three upstream conformance cases return to the pass tables with
behavior fixes instead of xfail rows:
- CompleteMultipartUpload: a part entry missing PartNumber or ETag is
  MalformedXML, and a part number below 1 is InvalidArgument
  (PartNumber), mirroring versitygw's posix backend; the ETag match is
  now mandatory and the >10000 special case folds into the membership
  check (InvalidPart).
- ListMultipartUploads: upload-id-marker validation mirrors upstream's
  MultipartUploadLister — the marker must be a valid UUID naming an
  upload of the first key group at/after the key marker (else
  InvalidArgument, upload-id-marker), and listing resumes past it.

DeleteBucket now implicitly aborts the bucket's open multipart sessions
(upstream's teardown never aborts them and expects the delete to
succeed), releasing their parked part blobs before the hilt space
delete. CompleteMultipartUpload_conditional_writes stays xfail: its
conditional matrix passes, but the implicit abort's /blob/abort goes
out proofless — hilt's per-operation grants carry blob.Abort for the
S3 Abort operation only. Promote once hilt's s3perm map grants
blob.Abort on bucket delete.

Validated: full local TestForgeVersity, 219 pass / 0 fail.

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

* docs(architecture): refresh §11/§12 status for the read tier, DeleteBucket abort, and spool eviction

The local-table Locator read tier is wired and validated
(TestForgeReadAfterEviction; #44 for retention-retired catalog blocks),
so the "not wired" bullet inverts: the remaining gap is spool eviction
itself — unbounded today, DeleteObject releases network-side only —
tracked in #48 alongside §5's bounded-cache spec. Record DeleteBucket's
implicit abort of in-flight multipart sessions (and its hilt blob.Abort
gate), point §11's cache-eviction open question at #48, and note the
forge paths are now exercised by the in-repo smelt itest harness.

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

* review: stock-image gates, MinPartSize provenance, cleanup logging, drop ci-error.log

- The delete-finality and deferred-multipart gates run on the stock
  smelt-SDK images like every other itest: env gate and binary
  injection dropped. They need piri:main ≥ piri#30 and a hilt with
  hilt#36's blob.Abort grant; until those publish, the standard
  INGOT_ITEST_PIRI_IMAGE / (new) INGOT_ITEST_HILT_IMAGE overrides
  cover local runs. Validated end-to-end with branch-built piri+hilt
  images: both gates green, AbortRejects 0.12s.
- Pin smelt at the smelt#19 branch head — its generated stack proofs
  carry the /blob/release + /blob/reject node delegations the gates
  exercise; re-pin on merge.
- Complete's 5 MiB minimum-part floor now reads backend.MinPartSize:
  it is S3's protocol constant, not an operator knob.
- Log the previously discarded errors: the post-commit latch to
  'completed', and DeletePark / spool.Remove / DeleteIntent in
  cleanupPartBlobs.
- Restore the captured-store mask in abortOpenSession with the
  corrected rationale: s3:DeleteBucket delegates no blob commands, so
  DeleteBucket's implicit abort must run on the authority captured at
  UploadPart (blob.Abort rides the write set per hilt#36); fix the
  xfail and architecture-doc comments that misattributed the grant.
- Remove the stray ci-error.log.

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

* review(uploader,forgeclient): WithConclude(false) replaces the parked add/upload split

Fold the PR #40 API-shape review (r3674608706, r3674935223): the deferred
accept is an option on the one entry point, not a second entry point.

- forgeclient: ParkedBlob's fields (AddTask, AcceptTask, PutInvocation)
  fold into AddedBlob; Location == nil marks an unconcluded add. BlobAdd
  takes WithConclude (default true); BlobAddParked becomes the private
  durable half. BlobConclude takes the AddedBlob, no-ops when already
  concluded, and drops the spent PutInvocation from its result.
- uploader: UploadBlob gains UploadOption and returns UploadedBlob
  (nil-able *BlobLocation + the pending-accept state, populated only
  while parked); DeferredBodyUploader embeds BodyUploader and keeps
  ConcludeBlob/AbortBlob. Callers guard the "concluding upload always
  returns a location" contract explicitly instead of deref-panicking on
  a misbehaving impl.
- s3frontend: parkBlobs passes WithConclude(false); the dedup-vs-parked
  branch is a documented Location nil-check.

The park vocabulary stays in the registry layer (blob_parks, BlobPark,
ParkStore) and docs — it is the blob-removal RFC's lifecycle term for the
allocated-but-unaccepted state; only the redundant API split is gone.

Validated: unit suite; full itest partition green against piri#30 head
(fil-forge/piri@b5208a3) + hilt#36 images.

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

* itest: grant the hilt#35 multipart permissions to the test tenant

hilt#35 (which superseded hilt#36's bare blob.Abort grant) makes the
multipart operations first-class in the authorizer: DELETE ?uploadId now
classifies as s3:AbortMultipartUpload instead of falling through to
s3:DeleteObject. The harness's hiltAllPermissions list predates that, so
once hilt:main published, every client-issued S3 abort was denied at
request authorization — the whole AbortMultipartUpload versity group and
TestForgeDeferredMultipart/AbortRejects failed CI with 403 AccessDenied.

Add s3:AbortMultipartUpload plus the two multipart List permissions,
keeping the list's contract ("every S3 permission hilt recognizes").

Also refresh the conditional_writes xfail rationale: with blob.Abort now
granted its failure moved — the conditional matrix passes and teardown
fails with BucketNotEmpty on DeleteBucket, which needs its own diagnosis
before the row can promote. Validated against the published piri/hilt
:main images (post piri#30/hilt#35): deferred gate + all versity
multipart groups pass; conditional_writes still xfails (no unexpected
pass).

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

* build(itest): bump smelt for the piri blob/release + blob/reject delegations

smelt#19 seeds the piri→upload delegation with blob/release and
blob/reject, so sprue's forwards carry proofs and piri accepts them —
TestForgeDeleteReleasesNetworkBlob and TestForgeDeferredMultipart pass
against this pin (validated pre-merge via a local replace).

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

* itest: promote conditional_writes + checksums_success (delete-release chain complete)

Both rows were teardown-blocked on superseded blobs leaking in the
space: the write-path release went out proofless. hilt#37 grants
blob.Remove with the write set and smelt#19 seeds the piri blob/release
delegation, so the releases now carry proofs end-to-end and CI's
unexpected-pass ratchet flagged both rows for promotion.

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

---------

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.

3 participants