---
title: "Manage plugin indexes"
description: "Configure trusted indexes or publish an index of your own."
canonical_url: "https://docs.oz0.ai/build/plugins/indexes/manage"
---
# 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.

```bash [Terminal]
oz0 index add acme https://plugins.example.com/index.json
```

```text [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:

```bash [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.

```bash [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:

```bash [Terminal]
oz0 index refresh acme --file index.json
```

A cluster that should never fetch an index at all imports it instead ([offline install](https://docs.oz0.ai/fleet/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):

```bash [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](https://docs.oz0.ai/fleet/backups#back-up).

## 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:

```bash [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:

```bash [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:

```bash [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](https://docs.oz0.ai/build/parts/tools/snapshots#the-measured-listing), its [SBOM](https://docs.oz0.ai/build/plugins/indexes/version-records#sboms), 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](https://docs.oz0.ai/build/plugins/limits/documentation#the-readme-its-images-and-its-diagrams), 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:

```bash [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](https://docs.oz0.ai/build/plugins/indexes/version-records#version-states), 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](https://docs.oz0.ai/improve/evals/coverage#policy-path-coverage): the figure and the counts per kind, never the points.

```bash [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`:

```json [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](https://docs.oz0.ai/build/plugins/limits/artifacts#index-size); the compact entries keep it far below that.

```bash [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
```

```text [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](https://docs.oz0.ai/govern/reviewed-changes/changesets#where-a-change-came-from)) 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:

```json
"provenance": {"job": "job-0192...", "agent": "improver", "plugin": "plugin-improver@0.1.0", "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](https://docs.oz0.ai/build/plugins/vulnerabilities) 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:

```json [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](https://docs.oz0.ai/build/plugins/list-a-plugin): `hub submit` writes the listing file a hub builds its index from.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
