v1 fetch layer, Go lane: resolve, trust, bytes, cache, lock - #396
Conversation
Implements go/internal/ocifetch, the Go half of the v1 OCI fetch layer against the frozen seam (lane 0A's constants_gen.go, docs/guides/fetch-v1.md): - http.go: retries with one shared table, Retry-After in both forms (and refused once it exceeds the retry schedule's own total budget), redirects with Authorization dropped cross-origin, the anonymous Bearer-token flow for mirrors, and the static CHTYPES_DOWNLOAD_TOKEN sent only to configured hosts. - oci.go: resolve an image index by tag, select the platform manifest, verify it by digest, and the tag/digest 404 policies (next-base-then- unpublished vs next-base-then-retry-on-last). - referrers.go / dsse.go: referrers-API-or-fallback-tag discovery, DSSE's pre-authentication encoding and ed25519 verification against the trusted key list, and the in-toto statement checks (abi, os, arch, version-within- request, subject-equals-layer). - unpack.go: zstd decode with the window-log cap honored before the decoder allocates for it, and tar extraction refusing anything but regular files and directories, with the unpacked-bytes cap enforced as it is written. - layout.go: the OCI image layout cache (temp-then-rename blobs, an index.json read-check-rename loop that survives a concurrent writer) and each unpacked directory's own verified.json record as the source of truth for offline reads. - lock.go: schema 3, refusing a v0 (schema 1/2) or wrong-ABI lock outright. - ensure.go: the seam's four entry points (Ensure, ResolveInstalled, ListInstalled, VerifyInstalled, FetchSigned) and --frozen/--offline/update. - errors.go: the ten shared v1 codes over the generated exit-code table. - conformance_test.go: TestConformanceV1, which skips loudly without CHTYPES_V1_CONFORMANCE; the fixtures and scripted server it depends on have not landed from the fixtures lane yet, so it is written against the documented case/report shape and will need a pass once they exist. go.mod/go.sum add github.com/klauspost/compress, pinned, for the zstd decoder (the zstd encoder used by fixture generation is a separate module). 64 unit tests exercise every piece directly, including a full fake-registry integration test of the happy path, an index.json concurrent-writer race, every tar refusal rule, and the retry/Retry-After/redirect/token behavior over real HTTP round trips — all green, including under -race. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two corrections from the delivery side's review: - Every GET …/manifests/<ref>, by tag and by digest, now sends Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json. Our host ignores it; a mirror may need it for content negotiation. Covers resolveIndex, fetchManifestByDigest, the referrers fallback tag's own manifests/ GET, fetchReferrerContent, FetchSigned and the --frozen path — every one of them, not only the tag-based resolve. - referrers.go's comment no longer frames the fallback-tag fallback as working around a cache lag on our own host (that endpoint is served no-store): it is for mirrors, which may serve only the fallback tag and never the referrers API at all. The fallback logic itself is unchanged. Also removes five dead citations of scripts/fetch-v1/server.py (lane 0B's scripted server, not yet on this branch) that lint-cited-paths.sh correctly flagged once the new test files were tracked — CI caught this on the first push; fixed and re-verified clean (lint-cited-paths, lint-public, lint-spelling, policy-merge-check --check-carve-out, check-selftests-wired.py, gen-constants.py --check). 67 tests pass (two new ones assert the Accept header is sent/absent as configured); go vet, golangci-lint and gofmt all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found by re-reading ensure.go end to end: resolveOptions already parsed Options.HookBeforeIndexRename into resolvedOptions.hookBeforeIndexRename, but both real addIndexEntry call sites in ensure.go hardcoded nil instead of using it, so the option had no effect on an actual Ensure call — only layout_test.go's own direct addIndexEntry test exercised the race-survival logic itself. session now carries hookBeforeIndexRename (set from resolvedOptions in newSession) and both installManifest and the already-installed shortcut pass it through. A new integration test, TestEnsureInvokesHookBeforeIndexRename, proves the hook actually fires during a real Ensure call — the gap existing tests could not have caught, since they called addIndexEntry directly rather than through the public entry point the conformance runner will use for the index-race-reapply case. 68 tests pass; go vet, golangci-lint and gofmt all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Two follow-up commits after review:
Also removed five dead citations of the fixtures lane's not-yet-landed scripted server that 68 unit tests pass ( |
- conformance_test.go: wire Options.SystemDirs from the fixture's own
system-dir copies, instead of leaving it unset (which silently fell
back to the real machine's SystemCacheDirs). This was the cause of
system-dir-readonly's only failure.
- ensure.go: ensureOnline's monotonic-build check now runs BEFORE the
layer is fetched/installed, using the signed predicate's own
version/build, and keeps the existing (newer) installed entry rather
than installing the registry's older one and only warning about it
after the fact. Matches monotonic-warning's documented intent ("the
existing (newer) install is kept, with a warning").
Also carried over from the previous round's review feedback: the Accept
header on every manifest GET, the referrers-empty-then-fallback-tag
trust rule, and the referrers.go comment reword — already in this
branch before this commit.
14 (case, transport) pairs still fail; all traced to fixture/script
defects outside this lane's scope (genfixtures/*, server.py) and
reported separately rather than fixed here, per brief.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lane 0B's round-3 fixes (merged #407) moved mirror-failover-5xx/ -digest-404 onto two genuinely independent bases ({base}, {base2}) served from server.py's two origins, each with its own response- sequence cursor. expandBase now also substitutes {base2} against the server's second port for http transport (docs/guides/fetch-v1.md's "{base2}" section). With #407's other fixes (real-version tags for existing-install-noop/ index-race-reapply, predicate-wrong-version's numeric spelling, an actually-oversize zstd window, preseed-oras dropped while its layout is absent, corrected redirect templates), the full local conformance run is now 139/139 passing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NdkF6p8Q3qkdxKCgbdjgb
…ssed 139/139 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NdkF6p8Q3qkdxKCgbdjgb
|
First live run of the Go conformance suite inside the no-network sandbox (run 37135773065, head 027b94d), leg
|
…he patch release Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NdkF6p8Q3qkdxKCgbdjgb
…port from v1-network, which does not run a suite yet Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NdkF6p8Q3qkdxKCgbdjgb
|
Update on Go enrollment (measured, runs 37136299058 and 37136947820):
|
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012NdkF6p8Q3qkdxKCgbdjgb
Summary
The Go lane of the v1 fetch layer:
go/internal/ocifetch, built against the frozen seam indocs/guides/fetch-v1.mdand lane 0A's generatedconstants_gen.go. It implements resolve, trust, bytes/unpack, the cache, the lock, and the seam's four entry points, with no edits togo/chtypes/*or the generated contract files.http.go— one retry table (constants-driven),Retry-Afterin both the delta-seconds and HTTP-date forms, redirects (capped,Authorizationdropped on any cross-origin hop), the anonymous Bearer-token flow for mirrors, and the staticCHTYPES_DOWNLOAD_TOKENsent only to configured hosts. An injectableClockmakes every retry/backoff path deterministic in tests.oci.go— spelling validation before any network call, resolving an image index by tag, selecting and verifying the platform manifest by digest, and the tag/digest 404 policies from the constants (next_base_then_unpublishedvsnext_base_then_retry_on_last). Base-URL construction is transport-aware:http(s)inserts/v2/right after the authority and carries the rest of the base's path through as the repository (so a test server's routing prefix and the real repository path both pass through unchanged);file://is used exactly as given, with no insertion, per the plan's "a file base names the repository path."referrers.go/dsse.go— referrers-API-or-fallback-tag discovery (filtered byartifactType, accepting the manifest once any candidate verifies), DSSE's pre-authentication encoding, ed25519 verification against the trusted key list (never the bundle's own untrustedpublicKey.hint), and the in-toto statement checks (abi— never a strayabi_revision— os, arch, version-within-request, subject-equals-layer, duplicate-JSON-key refusal).unpack.go— zstd decode with the window-log cap honored before the decoder allocates for it, multi-frame streams accepted, and tar extraction refusing anything but regular files and directories (symlink, hardlink, device, absolute path,.., duplicate entry all refused), with the unpacked-bytes cap enforced as the stream is written, not after.layout.go— the OCI image layout cache: content-addressed blobs by temp-then-rename, anindex.jsonread-check-rename loop that re-merges onto a concurrent writer's result rather than clobbering it, and each unpacked directory's ownverified.jsonrecord as the durable source of truth for offline reads (neverindex.json).lock.go— schema 3, refusing a v0 (schema 1/2) or wrong-ABI-generation lock outright, naming re-lock.ensure.go— the seam:Ensure,ResolveInstalled,ListInstalled,VerifyInstalled,FetchSigned, plus--frozen(by-digest only, no index lookup),--offline, andupdate(full lock rewrite from re-resolution, never a merge of stale and fresh entries).errors.go— the ten shared v1 codes over the generated exit-code table;CHTYPES_ARTIFACT_INCOMPATIBLEis reserved for the FFI/loader layer and this package never constructs one.conformance_test.go—TestConformanceV1, the commandv1.yml's conformance job runs. It skips loudly whenCHTYPES_V1_CONFORMANCEis unset.Spec commit used: the delivery side's layout-v2 design doc at commit
aa1980f521c791a296d31b68d30cafb4ff6ff2bb. No disagreement found between its §0/§4.1/§7 and what this PR implements.A mid-task correction folded in: on a staging registry, the referrers API can answer 200 with an empty (or goldens-only) list for a manifest that was just pushed, for up to five minutes, before its referrer cache catches up.
findReferrerstherefore always tries the fallback tag too whenever the referrers API's own match set for the artifact type being searched for comes up empty — regardless of whether that is because the API isn't served at all or because it is simply still catching up. This applies uniformly to signature-bundle discovery and to the genericFetchSignedpath (goldens, fixtures).Design points decided here (the spec is silent or ambiguous)
bundledigest is the bundle's own content (its blob/layer digest), not the wrapping referrer manifest's digest —lock3.schema.json's pin has nobundle_manifestfield, so--frozenfetches the bundle directly as a blob by that digest, never through the manifest-wrapper indirection the normal (non-frozen) discovery path uses.FetchSigned'srepositoryparameter is a suffix appended to every configured base (matching howfixtures_repository_suffixis named and shaped in the constants), not a full repository-path replacement.verified.jsonrecord,Ensurereturns immediately with zero further requests at all (referrers, bundle, and layer) — a superset of the documented "existing-install-noop: zero layer requests," on the reasoning that content at a given digest never changes, so nothing past that point needs re-checking.ConnectTimeoutSeconds + IdleReadTimeoutSeconds) rather than separately modeling a dial timeout and a stream-idle timeout;Options.ConnectTimeout/IdleReadTimeoutlet a caller (a test) override both to something short.oras copy -r --to-oci-layout) local verification inResolveInstalled. It is implemented against this lane's own best understanding of how such a copy represents a referrer in a plain OCI layout'sindex.json(filtering entries byartifactType), without having seen a reallayouts/oras-preseed/fixture — that is the fixtures lane's own job, and this will need a pass once it exists.Prose elsewhere claiming the Go module is dependency-free
This PR gives
go.modits firstrequire(pinnedgithub.com/klauspost/compress, for the zstd decoder). A search for prose asserting the opposite found these, all outside this lane's scope and left untouched:.github/dependabot.yml:15— "the module is stdlib-only (no require block)"CONTRIBUTING.md:75— "this tree has zero external Go dependencies today (norequireblock ingo/go.mod, sogo/go.sumdoes not exist yet either)"docs/reference/go.md:101— "The implementation is stdlib only"scripts/policy-merge-check.py:729— a comment making the same claim.github/workflows/ci.yml(9 occurrences) —cache: false # the module is stdlib-only: there is no go.sum to cacheNone of these are wrong about
go/chtypesitself (still stdlib-only); they become stale oncego.modcarries anyrequireblock at all, which happens the moment this PR lands onv1.Test plan
go vet ./internal/ocifetch/— clean.golangci-lint run ./internal/ocifetch/...— 0 issues.gofmt -l internal/ocifetch/— clean.go test ./internal/ocifetch/...(and again with-race) — 64 unit tests, all passing, covering every file directly: DSSE PAE against a hand-countable vector, bundle verification with this repository's own fixture test key, every tar refusal rule, the index.json concurrent-writer race, retry/Retry-After/redirect/token behavior over real HTTP round trips (httptest), and a full fake-registry integration test of the happy path (Ensureend to end, already-installed reuse, offline resolution, untrusted-key refusal, bad-spelling refusal).go test -count=1 -run '^TestConformanceV1$' ./internal/ocifetch/— skips loudly (CHTYPES_V1_CONFORMANCEunset), which is the correct, expected behavior in a bare copy and on this PR today: the fixtures lane'stests/fixtures/fetch-v1/{cases.json,trees/,layouts/}andscripts/fetch-v1/server.pyhave not landed onv1yet.conformance_test.gois written against the documented case/report schema shapes as precisely as they can be read today, but has not run against real fixtures — expect an iteration pass once that lane merges and this branch is updated.scripts/policy-merge-check.py --check-carve-out,scripts/check-selftests-wired.py,scripts/lint-public.sh,scripts/lint-spelling.sh,scripts/lint-cited-paths.sh,scripts/fetch-v1/gen-constants.py --check— all clean, run unpiped.Related issues
No tracking issue filed yet for the v1 fetch layer as a whole.
🤖 Generated with Claude Code