feat(gallery): galleries published as OCI artifacts - #12167
Merged
Merged
Conversation
Galleries published to an OCI registry arrive as an ORAS artifact whose layers carry their tree path in org.opencontainers.image.title. Nothing in pkg/oci could read a non-image manifest, so add PullArtifact: it lays the layers out under a destination directory and returns the manifest digest so callers can pin and verify signatures on it. A registry is remote input and this writes files, so the manifest is validated before any byte is fetched. A title that is absolute, escapes the destination or is missing is refused, an artifact of an unexpected artifactType is refused, and both the layer count and the total size are capped with caller-settable limits. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-5 [Claude Code]
A gallery whose URL is oci://host/repo:tag is now pulled as an ORAS artifact and its index.yaml read from the unpacked tree, alongside the existing HTTP and file sources. Mirrors, the per-candidate timeout, the failure cooldown and the last known good copy work the same for both schemes, so a gallery can mirror an OCI primary with an HTTP fallback. When the gallery declares a verification policy the artifact reference is resolved to its manifest digest, the publisher signature is checked against that digest, and only then is the same digest pulled. Nothing unverified is written to disk or parsed. With strict integrity mode on, an OCI gallery without a verification policy is refused instead of warned about, which is what the flag already does for backend installs. The pull lands in a staging directory that is renamed into the cache only once the whole tree is on disk and the index reads back as an index, so a failed or interrupted pull leaves nothing a later fetch would serve. A fetch that finds a fresh cache entry does not contact the registry. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-5 [Claude Code]
A gallery published as a self-contained tree, which is what an OCI gallery is, names its base configs by their place in that tree. Such a url reached the HTTP client verbatim and no entry of the gallery could be installed. A scheme-less entry url now resolves against the root the index came from: the unpacked artifact for an oci:// gallery, and the directory of the index URL otherwise. The relative path may not climb out of that root, reusing the guard the artifact puller already applies to layer titles, and an entry that does is dropped from the listing rather than failing the whole gallery. An entry that names its own scheme is untouched, so .ref indirection and local github: resolution behave as before. Reading an entry of an OCI gallery also needs the unpacked artifact directory as its trusted root, since the cache is deliberately a sibling of the models directory the downloader otherwise confines a file read to. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-5 [Claude Code]
Create artifact files with owner-only permissions to satisfy G302. Document the existing path validation for the G304 scanner finding. Check the permissions of downloaded layers in the artifact test. Assisted-by: Codex:gpt-6 gosec
Use root-relative filesystem operations to reject escapes through existing directory symlinks. Retain owner-only permissions and add a symlink escape regression test, without suppressing the path traversal finding. Assisted-by: Codex:gpt-6
Owner
|
@localai-org-maint-bot prepare a followup so our CI publishes as well galleries, and let's add it as a fallback |
2 tasks
Collaborator
Author
|
Prepared the follow-up in #12182. It publishes signed model and backend gallery artifacts, bundles repository-local base configurations, and adds OCI after the existing HTTP and GitHub fallbacks. Gallery artifact verification stays separate from backend image verification. Targeted config, gallery, and packaging tests plus go vet pass. Registry publication remains untested locally; the workflow uses the existing Quay repository and credentials. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A gallery can now live in a container registry:
url: oci://host/repo:tag.LocalAI pulls the artifact, verifies the publisher signature before unpacking
anything, and reads
index.yamlfrom the unpacked tree. Entries may refer totheir base configuration by a path inside the same artifact, so a gallery and
the configs it needs travel as one object.
This is what makes a self-contained, privately distributed gallery possible.
Today an index must be a URL on a web server and every entry must name an
absolute URL, so a gallery cannot be shipped as a unit.
What is here
pkg/oci.PullArtifact: a generic ORAS artifact pull that lays layers outby their
org.opencontainers.image.title. A registry is remote input andthis writes files, so the whole manifest is validated before a byte is
fetched: a title that is absolute, empty or escapes the destination is
refused, an unexpected
artifactTypeis refused, and layer count and totalsize are capped. Blobs are size-limited and digest-verified while being
written, so a lying registry cannot exceed the bound.
oci://gallery fetch, with mirrors, cooldown and last-known-good intact.With a
verificationpolicy: resolve the tag to a digest, verify thesignature on that digest, pull that same digest. The pull stages in a temp
directory and is renamed into the cache only once the tree is complete and
the index parses, so an interrupted pull leaves nothing a later fetch would
serve. Under
LOCALAI_REQUIRE_BACKEND_INTEGRITYanoci://gallery withno policy is refused rather than warned about, which is what the flag
already does for backend installs.
artifact for
oci://, the index URL's directory otherwise) and confined toit by the same guard the puller uses. An entry that climbs out is dropped
with a logged reason rather than failing the whole gallery.
Behaviour change to be aware of
Strict integrity mode now refuses an
oci://gallery without averificationblock at listing time, not only at install. It is called out in a warning
box in the docs.
Tests
Ginkgo, against an in-process registry throughout: the tree with subdirectories
and its digest,
..and absolute and missing titles, wrong artifactType, sizeand layer caps, index read, cache hit with zero registry traffic, an
unverifiable artifact refused with nothing cached and zero blob fetches,
verification pinned to the digest rather than the tag, strict-mode refusal, a
failed pull leaving nothing servable, relative entries resolving for OCI and
HTTP galleries, and an escaping entry dropped.
Signature verification sits behind a test seam so the suite does not reach the
public Sigstore mirror. An end-to-end check against a really signed artifact is
worth doing once #12165 lands, since that PR fixes the discovery path this
would exercise.
Docs updated in the same change:
docs/content/features/model-gallery.md.🤖 Generated with Claude Code
https://claude.ai/code/session_012ELNaYBdvugwkePWCQokNX