Skip to content
ocx
install

index.ocx.sh

This page explains how OCX resolves packages that are published through the index.ocx.sh public index. It matters because the index, not the physical registry, decides which tags exist.

An OCI registry path is a physical detail: a hostname and a repository path. The identity of a package is logical, for example “the cmake package published under ocx.sh”. A maintainer may migrate the backing registry, say from a self-hosted GHCR org to Docker Hub. Every consumer that pinned the physical ghcr.io/… path directly then breaks, although the package itself did not change.

index.ocx.sh is a pointer index, not a registry. It carries no /v2 API and stores no blobs. It maps a stable logical identifier (ocx.sh/<namespace>/<package>) to the physical registry that hosts the package. It also records the per-platform content digests held there.

OCX resolves ocx.sh/kitware/cmake:3.28 in two steps. It asks the index for the current physical location and digest. It then fetches the actual manifest from that physical registry.

The OCI image-index hop of a direct registry resolve is served by the index instead of the registry. It is not skipped. The hop count is the same, and only the side that answers it changes.

The HTTP client of the index ships its own bundled CA root set, the same source the main OCI client uses. Root and index-object fetches therefore work on a minimal container with no system CA store.

For any ocx.sh/<namespace>/<package> identifier, the public index is consulted before the OCI registry. It is never the other way around. A logical reference always resolves through the verified two-hop path below. It never resolves through a registry that happens to serve a repository under the same name. Identifiers on any other registry are unaffected, and the index is never consulted for them.

This wiring ships in the binary. ocx.sh names https://index.ocx.sh in the compiled-defaults tier. Any config tier can point it elsewhere. Setting index = "" resolves ocx.sh as a plain OCI registry instead.

logical id (ocx.sh/<ns>/<pkg>[:tag])
→ index resolve : GET p/<ns>/<pkg>.json (root) → tags[tag].content (image-index digest)
→ GET p/<ns>/<pkg>/o/<algo>/<hex>.json (verify sha256 of bytes)
→ manifests[]: select the platform matching this host
→ physical : root.repository, e.g. "oci://ghcr.io/ocx-contrib/cmake"
→ mirror_map : rewritten through [mirrors]'s registry role, if configured for this host
→ fetch : GET physical-registry /v2/.../manifests/<leaf-digest> (verify OCI CAS)

index.ocx.sh yields only pointers. The platform manifest and its layers always come from the physical registry named by repository. The repository field carries an oci:// scheme marker. The marker identifies a physical, transport-only reference. It is never used as a storage key.

Locally, OCX keys everything on the logical identifier. That covers the local index path, ocx.lock and the garbage-collection roots. A registry migration therefore never orphans a local copy or breaks a committed team lock.

A configured index owns its whole registry

Section titled “A configured index owns its whole registry”

config.json carries the wire-format version, and nothing the client reads decides scope:

{ "format_version": 1 }

A namespace may name an index in [registries.<name>]. Then only what that index holds is discoverable through it. A reference for which the index has no root is a hard miss. OCX does not fall back to the plain OCI registry that the index points at. That holds even when the registry serves a repository under the same name.

The refusal names the index that was consulted. A package that has not been announced yet therefore reads as exactly that:

'ocx.sh/go-task:3.44' is not in the index at https://index.ocx.sh, which is authoritative
for every name in registry 'ocx.sh'; announce it there with `ocx package announce`, or take
the namespace off the index with `[registries."ocx.sh"] index = ""`

The scope is the registry, not a name shape. The root schema of index.ocx.sh pins a logical name to ocx.sh/<namespace>/<package>. A flat ocx.sh/<name> can therefore never hold a root there.

That is a constraint of the index operator, enforced when a package is announced. It is not a rule the client applies. An index that serves a root for a flat name resolves it normally.

This is what makes the yank and deprecation gate reachable at all. A tag that the index yanks cannot be obtained by asking the registry underneath instead. It also means completeness matters. A package missing from the index is unresolvable through it, by design.

An absent config.json is the same case, not a different one. It is read as {"format_version": 1} at every reader (see Serve an index). The name_segments field plays no part either way, whether present or absent. It never decides whether a name is in the jurisdiction of the index.

ObjectVolatilityOCX behaviour
p/<ns>/<pkg>.json rootVolatile. The repository pointer can move by maintainer PR and tags are curated.Copy-first. Never auto-refreshed under the default mode. A live re-fetch happens only on an explicit ocx index update or a --remote resolve. That re-fetch rewrites the local copy and bumps observed.
o/<algo>/<hex>.json, the OCI image index this tag resolved to, verbatimImmutableFetched once, verified against its own SHA-256 filename and cached forever.
c/index.json catalogVolatileNever copied. The local c/index.json is authored. Each entry is the hash of the local root document beside it, written in the same step that writes that root. The catalog of the site is fetched live only when you ask for it (ocx index catalog --remote), and nothing from it is stored. The local tree stays free of per-machine bookkeeping, so there is no record of remote state in it to go stale.

A registry alias can drift between announces. The registry-side digest then moves past what the index still has committed. The first row above is built to tolerate this case.

ocx package cascade check flags that drift from the publisher’s side. It compares the live registry tags of a package against its committed root and reports the mismatch as index staleness. ocx package cascade repair fixes a drifted registry alias itself. The follow-up ocx package announce --tags-file re-observes the repaired tags. It then re-publishes the moved digests into the index.

Neither command touches the local copy of any machine. That hop is still ocx index update, run whenever a particular machine wants the newly announced root.

The local index uses the identical p/<ns>/<pkg>.json and o/<algo>/<hex>.json shape for an index.ocx.sh source as for any other. It is one wire format, not a special case:

$OCX_HOME/index/ocx.sh/
├── config.json
├── c/index.json
└── p/kitware/
├── cmake.json root doc, assembled locally from snapshotted tags, not a verbatim copy
└── cmake/o/sha256/
└── <index-digest>.json OCI image index, verbatim (immutable)

The stored object is the OCI image index of the registry. Its manifests[].digest entries are the per-platform manifest digests that OCX needs. Each one is verified independently against the OCI content-addressed storage of the physical registry when it is fetched.

The index repository specifies the file format of the index: wire format.

The root document of a package carries status fields set by the publisher. OCX surfaces them but never silently acts on them:

FieldBehavior
yanked (per tag)A tag resolve against a yanked entry prints a warning and is refused by default. A yank is a publisher signal, not a delete. It surfaces identically whether the root was just fetched live or read from a committed or shipped local copy with zero network. It is the same field and the same refusal. Set OCX_ALLOW_YANKED to opt in and resolve it anyway. A digest-pinned resolve of the same content never needs the opt-in, because immutable content cannot itself be “yanked”.
deprecated + messageResolve prints a warning. The message is surfaced in ocx package description pull.
superseded_byShown as advisory information in ocx package description pull and in resolve diagnostics. OCX never auto-follows it, because that would silently substitute a different package than the one you asked for.
  • Indices: the shared wire format, the active index and shipped copies.
  • Local index: the layout of the local collection and how updates grow it.
  • Serve an index: serve a snapshot over HTTPS or from disk.
  • Corporate mirror: route index traffic through a mirror.