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_images row 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_tags row. 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 returns 404 MANIFEST_UNKNOWN), but a malformed sha256: reference returns 400 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_blobs so blob mount can locate a source row without scanning by repository.
  • subject_digest index 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_tags keeps a full unique index on (namespace_id, container_image_id, name) — no partial predicate, no soft_deleted_at column. 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 via request.OriginalBody(r) and install a fresh http.MaxBytesReader at blob_max_size (default 50 GB). Manifest push wraps at manifest_max_payload (default 250 KB). *http.MaxBytesError surfaces as 413 Payload Too Large at 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.SetReadDeadline before any body read. The extension value is the new upload_read_timeout config (default 1h).
  • ContainerConfig uses proto strings with units. Values that carry units (manifest_max_payload, blob_max_size, durations) are string fields with protovalidate min_len constraints and parsed at startup, matching docs/dev/configuration.md conventions. No _bytes suffix.

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/last with a Link header. Default page size is pagination_max_size (1000). Clients that ignore Link headers for images with more than 1000 referrers will silently see a truncated set.
  • Manifest delete returns 409 MANIFEST_REFERENCED when 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

🤖 Generated with Claude Code

Edited by Hayley Swimelar

Merge request reports

Loading
Loading