docs(specs): add S12 container/OCI local repository spec
Why
The Artifact Registry has no format-specific protocol spec yet. Without S12, an implementing agent has no authoritative source for how the OCI Distribution Spec v1.1 endpoints map to ADR-007's database schema, ADR-008's content- addressable storage, ADR-009's URL structure, S01's HTTP server middleware, and the configuration module. Every implementation decision would otherwise require cross-referencing four ADRs, the OCI spec, and two platform specs simultaneously.
Closes #6 (closed)
What (non-obvious parts)
The spec is phased into three implementation sessions (A: blobs, B: manifests, C: tags/referrers) so an agent can consume one phase per session without needing the full document in context.
Key design choices that differ from the Container Registry reference implementation or are not obvious from the ADRs:
OCI endpoint behavior
- Image auto-creation on POST, not PUT. The
container_imagesrow is created at blob-upload session initiation, not at manifest push. This lets every subsequent PATCH/PUT/GET/DELETE on the session resolve the image via normal name resolution. Abandoned sessions leave orphan image rows that ADR-011 reconciliation cleans. - POST dispatch priority.
?mount=wins over?digest=, which wins over session initiation. Preserves the mount path's no-error-leakage invariant (mount returns only 201 or 202) when clients send both sub-mode parameters. - Tags are hard-deleted; manifests and blobs are soft-deleted. DELETE by
tag reference removes the
container_tagsrow. Manifest DELETE hard- deletes the manifest's tag rows in the same transaction and soft-deletes the manifest. The manifest and other tags pointing to it stay untouched by a tag-only DELETE. Soft-delete-based tag recovery UX (undo, audit trail, "recently deleted tags") is deferred as OQ-11. - Manifest delete cascades soft-delete to direct referrers only. Single- level by design: a referrer of a referrer (for example, a counter-signature) is not cascaded. OCI permits dangling subject references; GC reclaims orphaned chains through the image-level cleanup path.
- Manifest pull reference routing. A
sha256:-prefixed reference is treated as a digest pull; otherwise it's a tag. No tag-format validation on pull (any non-matching name returns404 MANIFEST_UNKNOWN), but a malformedsha256:reference returns400 DIGEST_INVALID. - Content-Range on PATCH is optional. Required for compatibility with
go-containerregistry/crane, which omits it on single-chunk uploads. OCI-
specific format (
^[0-9]+-[0-9]+$), not RFC 9110.
Storage and data model
- Manifest push splits storage write and DB transaction. Step 6a writes to object storage outside the transaction to avoid holding locks during external I/O. Orphaned payloads (write succeeds, transaction fails) are cleaned by ADR-011 reconciliation.
- Upload sessions are database-backed with hash state in a BYTEA column, diverging from ADR-008's object-storage approach. Flagged as OQ-6.
- Cross-image blob digest index added to
container_blobsso blob mount can locate a source row without scanning by repository. subject_digestindex covers cursor and sort via(namespace_id, container_image_id, subject_digest, digest)so referrers pagination is a single index scan.- Manifest-delete relationship unlink splits into parent/child DELETEs.
Two separate statements, one per column, instead of a combined
OR. Each DELETE prunes a single partition; the combined form cannot. container_tagskeeps a full unique index on(namespace_id, container_image_id, name)— no partial predicate, nosoft_deleted_atcolumn. The UPSERT conflict target is the plain unique index, and the tag-listing read path needs no soft-delete filter.- Text columns use explicit
CHECK (char_length(col) <= 255)rather than the informal "limit 255" notation, so the constraint names and bounds are unambiguous for migration authoring.
S01 and configuration integration
- Per-endpoint body-size enforcement. S01 installs a global MaxBytesReader
at
server.max_body_size(5 MB). Blob upload handlers unwrap it viarequest.OriginalBody(r)and install a freshhttp.MaxBytesReaderatblob_max_size(default 50 GB). Manifest push wraps atmanifest_max_payload(default 250 KB).*http.MaxBytesErrorsurfaces as413 Payload Too Largeat the correct boundary. - Upload read deadline extension. S01's 10 s default read timeout is too
short for blob uploads. Handlers extend the deadline via
http.ResponseController.SetReadDeadlinebefore any body read. The extension value is the newupload_read_timeoutconfig (default 1h). ContainerConfiguses proto strings with units. Values that carry units (manifest_max_payload,blob_max_size, durations) arestringfields with protovalidatemin_lenconstraints and parsed at startup, matching docs/dev/configuration.md conventions. No_bytessuffix.
OCI deviations explicitly documented
- Manifest size limit is 250 KB, below the OCI v1.1 SHOULD-level 4 MB floor. ADR-004 drives the limit. Flagged as OQ-15.
- Referrers response is paginated. OCI defines a single Image Index
response; AR adds
n/lastwith aLinkheader. Default page size ispagination_max_size(1000). Clients that ignoreLinkheaders for images with more than 1000 referrers will silently see a truncated set. - Manifest delete returns
409 MANIFEST_REFERENCEDwhen the target is a child of an active Image Index. The OCI end-6 table lists 202/400/404/405, not 409. Necessary to preserve index integrity under soft-delete semantics (the CR avoids this case with an FK constraint, which soft-delete cannot). - Six AR-specific error codes extend the OCI 14-code baseline:
MANIFEST_REFERENCED,MANIFEST_LIMIT_EXCEEDED,TAG_LIMIT_EXCEEDED,PAGINATION_NUMBER_INVALID,UNAVAILABLE,INTERNAL. Conformant clients fall back to HTTP status code handling per the OCI spec's general guidance. - Digest canonical form is lowercase hex. Responses always use lowercase, regardless of the case the client used in the request URL.
14 open questions (OQ-1, OQ-3 through OQ-15) gate implementation. Seven are
ADR-007 amendments (artifact_type, annotations, hash_state, created_at
and updated_at on container tables, partitioning of mid-tier tables,
subject_digest, container_blobs.media_type modeling). The remainder cover
ADR-004 unit conventions and the 4 MB floor (OQ-4, OQ-15), ADR-011
reconciliation scope for orphaned manifest payloads (OQ-8), S08/ADR-020 auth
contract stability (OQ-9), S21 GC coordination interfaces (OQ-10), and one
cross-reference to OQ-3 (OQ-12). OQ-11 is a deferred soft-delete-based tag
recovery UX feature and does not block MVP.
Test plan
- Spec passes markdownlint-cli2 with zero errors
- Spec passes Vale with zero errors
- All internal link fragments resolve (MD051)
- Open questions are individually actionable (each names the ADR/spec that must be amended)
- Acceptance criteria cover every endpoint variant in the API Contracts section