Folders, bundles and OCI
For: tenant administrators and plugin authors.
Install the same plugin from Git, a local folder, an archive, a console upload or an OCI registry. After fetching its bytes, the edge applies plugin lint, contract checks, the quality gate, canary rollouts and tool snapshots.
A Git version is locked to a commit. Bundles and OCI artifacts are locked to the SHA-256 digest of their bytes.
| Source | Install with | Locked to |
|---|---|---|
| Git | plugin install https://github.com/acme/tools | the commit |
| A folder | plugin install ./tools | the bundle's digest |
| An archive | plugin install tools-1.0.0.tar.gz | the bundle's digest |
| An upload | the console's Plugins page, Install, Upload a bundle | the bundle's digest |
| An OCI registry | plugin install oci://ghcr.io/acme/tools@sha256:… | the manifest's digest |
plugin list and the catalog show each plugin's kind and digest.
Pack a bundle
oz0 plugin pack ./ops-tools
Packed ops-tools 0.2.0 into ops-tools-0.2.0.tar.gz: 8 files, 30.4 KB, sha256:1d0a49ba0aa1…
A bundle is a gzipped tar with oz0-plugin.yaml at its root: the same artifact the edge builds from a Git commit. pack checks the bundle it wrote as plugin lint checks a folder and refuses one with errors, so a file the manifest names that stays out of the bundle (for example because .gitignore lists it) fails here, not at install. A symlink to a folder packs that folder.
A symbolic link inside the folder, or another special file, is refused, by pack and by plugin lint, plugin dev, dev and an install from Git alike, before anything is parsed: error: prompts/desk.md: a symbolic link or special file, which plugins may not contain, with the fix to copy the file into the folder in its place. A link could otherwise make the loader read, and the bundle carry, a file from outside the plugin on whichever machine builds it.
Folders that packing a folder leaves out (.git, .venv, node_modules, __pycache__, .oz0-tools, dist, .pytest_cache, .ruff_cache) are not looked into, so a virtual environment's links are fine; a link with one of those names is refused like any other. A build from Git packs what the commit holds, those folders too when they are committed, so it also refuses a link committed inside one of them, and plugin inspect refuses an artifact that holds a link. --out names the file (default <name>-<version>.tar.gz) and --json prints the digest and the files.
Packing is deterministic: entries are sorted and their times, owners and modes fixed, so the same files always give the same digest, on your laptop and in CI. These stay out of a bundle:
.git(a folder, or the.gitfile of a submodule or worktree),.venv,node_modules,__pycache__,distand the test and lint caches;.envand.env.*files, and.pycfiles;- whatever the
.gitignorefiles of the folder's Git repository ignore, when the folder is in one. Your global Git excludes (core.excludesFile,~/.config/git/ignore) and.git/info/excludedo not count, since they differ from machine to machine; - links and special files.
A file that the checks read (the manifest, agents, flows, schemas, instructions, skills and evals) may be at most 4 MB; code and data files may be larger, up to 512 MB unpacked and 50,000 files in all.
Install from a folder or an archive
oz0 plugin install ./ops-tools
oz0 plugin install ops-tools-0.2.0.tar.gz
The CLI packs the folder, or reads the archive, uploads it to the server, and installs it by its digest. This works in dev mode and in a cluster: the server never reads your disk. An archive that is not a bundle as it is (a .zip, a plain .tar, or a release .tar.gz that keeps the plugin in one top folder) is repacked as one first, and the CLI says so.
An upgrade is a new upload. plugin update <name> packs the same folder or file again when it is still where it was installed from; otherwise install the new file.
Upload in the console
On the Plugins page, choose Install, then Upload a bundle, and drop a .tar.gz from plugin pack (64 MB at most). Before anything is installed, the page shows:
- the bundle's digest;
- its tools, agents and flows;
- the checks' findings, the same as
plugin lint; - whether a quality gate will run: a plugin with evals runs them on a test node before any other node gets it;
- the installed version it replaces, if any.
Install then installs it, with the canary and breaking-change options of a Git install, and lists the contract changes an upgrade makes. Tenant admins may upload; the bundle is kept for that tenant only, and another tenant cannot install it by its digest.
An upload waits at most 24 hours to be installed, and a tenant may have 20 uploads (512 MB in all) waiting; above that an upload is refused with 429 until one is installed or expires. Installing an upload uses it up: to install the same bundle again later, upload it again (the CLI always does).
Scripts can do the same over HTTP: POST /v1/plugins/uploads?tenant=<tenant> on the management port with the bundle as an application/gzip body returns its digest and checks as JSON, and InstallPlugin with bundle set to that digest installs it.
OCI registries
A registry that holds container images can hold plugins too. Publish a bundle, or a folder, with plugin push:
echo "$REGISTRY_TOKEN" | oz0 plugin push ./ops-tools oci://ghcr.io/acme/ops-tools:v0.2.0 --username acme-ci --password-stdin
Pushed ops-tools 0.2.0 to oci://ghcr.io/acme/ops-tools:v0.2.0: sha256:74e28a9dfad3…
Install it with: oz0 plugin install oci://ghcr.io/acme/ops-tools@sha256:74e28a9dfad3…
OZ0_REGISTRY_AUTH=user:password works instead of the flags. Pushing the same bundle again gives the same digest.
Install it by that digest. The edge pulls it, never the nodes, with the registry's credentials from a tenant secret:
printf 'acme-ci:%s' "$REGISTRY_TOKEN" | oz0 secret set REGISTRY_AUTH
oz0 plugin install oci://ghcr.io/acme/ops-tools@sha256:74e28a9dfad3… --secret REGISTRY_AUTH
The secret holds user:password, or a token alone. A public repository needs no secret.
The edge checks that the manifest and the bundle match their digests and that the artifact is a plugin. plugin update does nothing for an OCI plugin: install a new digest instead.
The edge reaches registries over HTTPS only and follows no redirect to plain HTTP. It does not connect to its own machine or to link-local addresses (such as a cloud's metadata service), whatever the registry, its token service or a redirect names; a registry on a private address inside your network is reached once its network is listed with oz0 server start --plugin-source-allow-net (where the edge fetches from). plugin push from your machine reaches a registry on localhost or 127.0.0.1 over plain HTTP, as Docker does; a server does that only in dev mode.
The edge checks and builds at most four bundles at once; further uploads and pulls wait their turn.
The artifact's media types
For registries, scanners and other tools, a plugin is an OCI image manifest with:
| Part | Media type |
|---|---|
artifactType | application/vnd.oz0.plugin.v1 |
| config | application/vnd.oz0.plugin.config.v1+json, with the plugin's name, version and description |
| the one layer | application/vnd.oz0.plugin.bundle.v1.tar+gzip, the bundle |
The manifest carries the title, version and description as annotations and no creation time, so the same bundle always gives the same manifest digest. The edge accepts a manifest whose artifactType or config media type is one of these.
Section on this page