v0.3.164 - Object-Storage Upload Adapter (Stage 2): Real SigV4 R2/S3 Transport
v0.3.164 - Object-Storage Upload Adapter (Stage 2): Real SigV4 R2/S3 Transport
v0.3.164 lands Stage 2 of the WOM #11 live object-storage upload adapter: a real,
hand-rolled AWS SigV4 R2/S3-compatible upload transport. WOM is now
network-CAPABLE for an approved object-storage upload. Capable is not automatic:
a live --approve upload still fails closed without env-only credentials, a safe
--reviewed-by, a resolvable endpoint/bucket, and a met tiered tiny-first gate.
This release ships with unproven_against_live_provider: true on the release note
and every execution receipt, and stays that way until the tiny-first human runbook
below confirms the first live object.
What ships
- Real SigV4 transport (
S3CompatibleTransport). PUT and UploadPart sign the
real lowercase-hex payload sha256; HEAD/GET signUNSIGNED-PAYLOAD. The
canonical URI/query encoding is RFC-3986 correct (unreservedA-Za-z0-9-._~,
everything else uppercase%XX, content-addressedsha256/<first2>/<hex>
key separators preserved, path encoded once). Header canonicalization,
SignedHeaders, the 4-line string-to-sign, and the 4-stage signing key
(kDate→kRegion→kService→kSigning, raw-byte intermediates) are all
byte-exact. Region isautofor Cloudflare R2 (threaded into both the
credential scope andkRegion);generic-s3requires an explicit region.
The SigV4 core is pinned against the published AWS documented example — the
canonical request, string-to-sign, and final signature all match the third-party
values. - Whole-object verification by re-download-and-hash (the showstopper this stage
resolves). The executor compares a lowercase-hex sha256 at HEAD-after. Rather
than depend on a provider-stored SHA-256 — which is not reliably available: R2
does not implement GetObjectAttributes, marks thex-amz-checksum-*headers
"Feature Not Implemented", and a SHA-256 multipart checksum can only be COMPOSITE
(never the whole-object hash) — the transport verifies byHeadObject(presence- size) followed by
GetObjectand re-hashing the returned bytes to hex. Uploads
still sign the real payload SHA-256 in SigV4 (x-amz-content-sha256) for on-wire
integrity, but no provider checksum surface is trusted. The shipped executor
comparison is unchanged and now passes for a genuine upload.
- size) followed by
- Multipart. Objects at or above the 5 GiB threshold use a plain multipart
upload (create → part PUTs → complete). Nox-amz-checksum-type: FULL_OBJECTis
sent — that combination is unsupported for SHA-256 on both AWS S3 and R2. The
CompleteMultipartUpload request carries only the<Part>list and an explicit
text/xmlContent-Type (AWS rejectsapplication/x-www-form-urlencodedfor that
call). Whole-object integrity is verified by the same HEAD+GET-rehash path. If the
re-download does not hash to the content id, the object FAILS — it never passes on
ETag or size, and the completed-but-wrong object is deleted so no orphan accrues
storage cost. Arate_limitedfailure mid-multipart is retried by the bounded
loop to the attempt ceiling, exactly like the single-PUT path. - Bounded retry + cost ceilings. A per-object retry loop backs off exponentially
with jitter up to a hard attempt ceiling onrate_limited(429/503/SlowDown/
InternalError/conn-reset), then fails closed asfailed_rate_limited. Anauth
status (403/400/SignatureDoesNotMatch/InvalidAccessKeyId/RequestTimeTooSkewed)
fails closed immediately with ZERO retries — retrying a bad signature burns
Class-A ops and can never succeed. A HARD cumulative provider-PUT ceiling
(OBJECT_STORAGE_TOTAL_PUT_CEILING) bounds cost across the whole run,
independent of--max-objects.retry_summary.backoff_ms_totalis now a real
accumulated value. - Tiered tiny-first gate. A run may not exceed the live-acceptance tier the
store's prior durable execution receipts have proved. A bulk first-live run
REFUSES withtiered_gate_unmetuntil the single small object is proved. Tier
advancement is derived from durable receipt facts —result_status,
bytes_uploaded, and the count of distinct successful one-per-object receipts —
not a bare flag the caller sets. A batch (tier 3) requires a proven large-object
/ multipart upload (tier 2) plus enough distinct landed objects. Each live tier
stays a human--approvestep. - Single networking seam. Every transport method builds a fully-signed request
and hands it to an injectedsend. The only place stdlib networking is reachable
is one_default_urllib_sender()factory; the CLI wires it only for a real
--approverun. No dependency was added (still PyYAML only). - Secret discipline extended. The direct-value containment guard now also covers
the derived SigV4 signing key, as belt-and-suspenders over the primary structural
guarantee that signing material lives in transport locals only. No request
headers,Authorization,StringToSign,CanonicalRequest, or provider error
body is ever recorded.
Capable != automatic
live_object_upload_adapter_implemented and provider_api_call_implemented are
now true — the live adapter really ships. But archive object-storage-upload --approve still fails closed unless every gate is met: env-only credential refs,
a safe --reviewed-by, a resolvable non-secret endpoint/bucket, and a met tiered
gate. Read-only object-storage-upload-plan/-verify and the read-only MCP tools
are unaffected.
Residual (only a real endpoint proves these)
Carried as unproven_against_live_provider: true until the first live object:
signature ACCEPTANCE by R2's authorizer; real read-after-write consistency of the
HEAD+GET verification path; real 429/SlowDown timing; the ±15-minute clock-skew
window; and real Class-A/B billing (note the HEAD+GET verification adds one
GetObject read per uploaded object). Whole-object integrity itself no longer
depends on any provider checksum surface, so it is not a live unknown.
Human tiny-first runbook (tier 1, first live object)
Upload exactly one small object, verify it end-to-end by hand, then advance tiers.
WOM_R2_ACCESS_KEY_ID=<id> WOM_R2_SECRET_ACCESS_KEY=<secret> \
archive object-storage-upload <archive-root> \
--provider-kind cloudflare-r2 \
--store-ref storage:account:<label> \
--endpoint-host <account>.r2.cloudflarestorage.com \
--bucket <bucket-name> \
--access-key-id-ref env:WOM_R2_ACCESS_KEY_ID \
--secret-access-key-ref env:WOM_R2_SECRET_ACCESS_KEY \
--only sha256:<one-small-object-hex> \
--max-objects 1 \
--skip-uploaded \
--reviewed-by kim \
--approve
After it succeeds, verify by hand: the execution receipt under
receipts/providers/object-storage-executions/, the manifest transition to a
wom_uploaded location, and the remote after-HEAD. Then advance the tiers, each a
separate human --approve run:
- T1 one small object (above).
- T2 one object at or above the multipart threshold.
- T3 a small batch (~10) with a deliberate mid-batch abort proving the
manifest lock + resume ledger. - T4 a
--skip-uploadedre-run proving nodeclared_uploadedfalse-skip. - T5 the full set.
Only after the first live object is confirmed does the doc caveat flip to
proven_against_live_provider: cloudflare-r2, one object.
What is deliberately NOT in this release
No live object has been uploaded from this release — the tiny-first runbook is a
human step. Real signature acceptance, checksum surfacing, multipart, throttle
timing, and cost are validated live, tier by tier, behind explicit human approval.