Use a plugin index

Manage plugin indexes

Configure trusted indexes or publish an index of your own.

For: tenant administrators and plugin authors.

Configure trusted indexes or publish an index of your own.

Manage the indexes a cluster reads

Cluster admins add, trust, refresh and remove indexes; they are the cluster's, for every tenant. Tenant admins install from them.

Terminal
oz0 index add acme https://plugins.example.com/index.json
Output
INDEX  STATE        VERSION  EXPIRES  PLUGINS  ROOT                                     URL
acme   not trusted  -        -        0        offered SHA256:0fbd772d9a665eae19f6f2bf  https://plugins.example.com/index.json

Index acme is not used until you trust it. Its copy names root key pyz0ZO4P... (SHA256:0fbd772d9a665eae19f6f2bf09d26512);
if that is the key its publisher gave you: oz0 index trust acme --fingerprint SHA256:0fbd772d9a665eae19f6f2bf09d26512

An index other than ours is used only once you trust it. Compare the fingerprint of the root key its copy names with the one its publisher gives you, then pin the key, by that fingerprint or by the key itself:

Terminal
oz0 index trust acme --fingerprint SHA256:0fbd772d9a665eae19f6f2bf09d26512
oz0 index trust acme --root-key pyz0ZO4Pazv63lUxmfv2BUX+SeciSKzoGTJXOjmhy8w=
oz0 index add acme https://plugins.example.com/index.json --root-key pyz0ZO4P...   # add and trust at once

The server pins what the fetched copy offers only when its fingerprint is the one you give: whoever controls what the URL serves (its host, the DNS, the network) controls what it offers. Once an index is trusted:

  • only --root-key replaces its root, for when its publisher really replaced theirs; a copy signed under another root is refused, and index list keeps showing the pinned one;
  • index trust <name> --allow-withdrawn and --refuse-withdrawn change only whether withdrawn and quarantined versions install with a warning or are refused (the default).

Ours is trusted through the root keys built into the server, so index trust oz0 takes only those two flags. To read a mirror of it under a key you trust, add the mirror under another name with --root-key.

Every signed-in user sees index URLs, and the audit log records them, so an index URL may not carry a user name or password, and should carry no other secret either.

From then on the server takes a copy of the index only when:

  • an online key that the pinned root key certified for this index signed it;
  • neither the copy nor the key's certificate has expired;
  • its version is higher than the highest the cluster has seen, or the same version with the same contents. A lower one is refused as a rollback.

index list shows each index's state (ready, not trusted, no copy, expired, no root key), the version the cluster has and when it expires, and the last check's error. Installs fetch a copy older than 10 minutes again, and use the copy they have when the index cannot be reached, until it expires.

Terminal
oz0 index refresh            # every trusted index
oz0 index refresh acme
oz0 index remove oz0         # stop listing ours

Offline

A cluster that cannot reach an index takes a mirrored copy instead. Copy the signed index.json across and upload it; it is checked exactly as a fetched copy:

Terminal
oz0 index refresh acme --file index.json

A cluster that should never fetch an index at all imports it instead (offline install): index add with --file adds an index whose URL is imported:, which nothing fetches, in any mode; each later copy comes from index refresh --file, with the same checks (a lower version is refused as a rollback):

Terminal
oz0 index add airgap --file index.json --root-key <root key>
oz0 index refresh airgap --file index-next.json

An index URL is https://; in dev mode it may also be http:// or file://. index add refuses one the server could never fetch, before it adds anything, on an all-in-one server and on a management server whose edges fetch its indexes alike.

Removing an index forgets the highest version seen of it: adding it again starts over. Indexes and the highest versions seen are part of a backup.

Run an index of your own

An index is one file, index.json, which any web server can serve. Its format is a JSON Schema in the engine's repository, schemas/plugin-index/v1: index.schema.json for the index, listing.schema.json for what plugin inspect --json prints, and the envelope and certificate it is signed in.

The schema files currently require repository access. Public source downloads are coming soon.

Keys

An index has two Ed25519 keys. The root key stays offline; its public key is what admins pin. It certifies an online key, which signs each version of the index, for a time:

Terminal
oz0 index keygen ~/keys/acme-root.key      # prints the public key to give to admins
oz0 index keygen ~/keys/acme-online.key
oz0 index certify ~/keys/acme-online.key --root ~/keys/acme-root.key --index acme --valid 90d -o acme-online.cert

Keep both keys outside any repository; keygen writes them readable only by you and never overwrites one. When the online key's certificate nears its expiry, or the key leaks, certify a new one: admins need not change anything.

Listings

plugin inspect --json describes a plugin at a pinned commit or digest. From a Git URL it resolves --ref to a commit, packs the plugin as the edge does, asks the forge for the repository's ID (GitHub, GitLab and Gitea, Forgejo or Codeberg; --forge names it for other hosts, --forge none skips the check), and prints the listing's source and generated part, sorted, so the same commit always gives the same bytes:

Terminal
oz0 plugin inspect --json https://github.com/acme/team --ref v0.1.0 > listing.json

A listing of a bundle or an OCI artifact is made from the pinned bytes themselves, so that its generated part describes what servers install: give inspect the bundle file that plugin pack built, with --url where you will publish it, or an oci:// reference with its digest (pulled anonymously). The source then pins the bundle's SHA-256 or the manifest digest:

Terminal
oz0 plugin inspect --json team-0.1.0.tar.gz --url https://files.example.com/team-0.1.0.tar.gz
oz0 plugin inspect --json oci://ghcr.io/acme/team@sha256:4f1c...

Without --json it prints a summary: its tools' functions with an estimate of each definition's tokens and each agent's capability card, when the plugin carries a measured listing, its SBOM, its docs and its licence. It runs no plugin code. It checks the README and the licence as plugin lint does, and fails on a README past its limits, so a listing never describes a page the hub cannot show.

--docs-dir <dir> also writes the files the listing's docs name (the README, the images it shows, every licence file and the graph of the plugin's parts) into a folder, each named by the 64 hex characters of its SHA-256, for an index's site to serve:

Terminal
oz0 plugin inspect --json https://github.com/acme/team --ref v0.1.0 --docs-dir docs > listing.json

To make the listed version validated, run its evals and attach the report: plugin test --report report.json on exactly the files you list, then plugin inspect --test-report report.json. Inspect refuses a report of other files or one that skipped an eval. A report of a run with --coverage also carries its policy-path coverage: the figure and the counts per kind, never the points.

Terminal
oz0 plugin test ./team --report report.json
oz0 plugin inspect --json https://github.com/acme/team --ref v0.1.0 --test-report report.json > listing.json

The index

The index's payload lists each plugin with its name, its publisher (whose ID is the namespace), what its authors write (a summary, maintainers, a homepage, issues and security link, tags), and its versions, each with a status, a source and the whole generated part from plugin inspect:

index-payload.json
{
  "format": "oz0.plugin-index.v1",
  "name": "acme",
  "version": 1,
  "plugins": [{
    "name": "com.example/team",
    "publisher": {"id": "com.example", "verified": "dns"},
    "author": {"summary": "Greets people", "maintainers": [{"name": "Ada"}]},
    "versions": [{"version": "0.1.0", "status": "active", "source": {"git": {...}}, "generated": {...}}]
  }]
}

Sign it with the online key. --version must grow with every publication, and --expires (default 72 hours) is the longest a withdrawal takes to reach every server, so keep it short and sign often. Signing also fills in each version's clashes from the generated parts and its trust facets (publisher-proven, custody, no-advisories, and verified for the project's own plugins), replacing any the payload had, resolves each version's uses: to the index's listings, and names on each plugin the listings that use it.

index sign writes each version's full listing as a file named by its SHA-256 into --detail-dir and signs a compact index that pins it; publish the folder beside index.json if your site shows plugin pages (servers never fetch it), and keep it: the next sign reads a compact version back from it. index verify --detail-dir checks the folder against the index. A server reads an index of at most 32 MiB; the compact entries keep it far below that.

Terminal
oz0 index sign index-payload.json --key ~/keys/acme-online.key --cert acme-online.cert --version 2 --expires 72h \
  --detail-dir detail -o index.json
oz0 index verify index.json --root-key ~/keys/acme-root.key --detail-dir detail
Output
Index acme version 2 verifies: 1 plugins, 1 versions; expires 2026-10-09T08:44:22Z.
Signed by SHA256:2671940a799c3b425482885bd3952d53, certified by root SHA256:916ec2110d8cc16ffe83dcd0daeced35 until 2026-11-05T08:44:21Z.
Each version's detail document is in detail and matches the index (1).

A version an agent made carries its provenance beside its custody: plugin inspect --json of a Git source reads the inspected commit's OZ trailers (the ones push_changes writes) into the listing's provenance, and the index puts it on the version.

index sign keeps of it only what anyone may read: the job and the agent as opaque text, the agent's plugin and version, the model alias, and the evidence counted by kind (read, finding, eval, comparison), never an evidence ID or a link into the publisher's tenant.

index sign works offline and reads no commit itself. An index signed before carries none, and servers before this release ignore the key:

"provenance": {"job": "job-0192...", "agent": "improver", "plugin": "[email protected]", "model": "smart",
               "evidence": [{"kind": "finding", "count": 2}, {"kind": "comparison", "count": 1}]}

To take a version back, set its status to withdrawn (or quarantined, while you look into it), with a reason, and publish a new version of the index. Never give a withdrawn name to someone else.

Advisories

An index may carry known vulnerabilities of the packages plugins lock, for every cluster that trusts it to scan its installed plugins with. They are an advisories section of the payload in the hub's format, oz0-hub.advisories.v1 (defined in schemas/plugin-index/v1/advisories.schema.json), signed with the rest:

index-payload.json
{
  "format": "oz0.plugin-index.v1",
  "name": "acme",
  "plugins": [...],
  "advisories": {
    "format": "oz0-hub.advisories.v1",
    "updated": "2026-10-12T00:00:00Z",
    "advisories": [{
      "id": "GHSA-xxxx-xxxx-xxxx", "aliases": ["CVE-2026-1234"],
      "ecosystem": "PyPI", "package": "httpx",
      "ranges": [{"introduced": "0.20", "fixed": "0.28.1"}],
      "summary": "One line", "severity": "high",
      "references": ["https://example.com/advisory"]
    }]
  }
}

versions lists affected versions exactly; a range runs from introduced (0 for every version) up to, not including, fixed, or up to and including last_affected. To take an advisory back, publish a new version of the index without it (servers skip an entry that carries OSV's withdrawn, and an index's entry never hides another index's). index sign refuses a PyPI or npm entry that carries withdrawn or does not validate, with how many and why, rather than sign an index whose advisories no server would apply.

Servers keep PyPI and npm entries, spelled exactly PyPI and npm, and skip the rest, so a newer index may carry other ecosystems; index sign refuses either name in another spelling (pypi, NPM), which no server would read as it. Servers before this release ignore the section. index sign keeps it as it is.

To list a plugin on a hub rather than run one, see List a plugin: hub submit writes the listing file a hub builds its index from.

Copyright © 2026