> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Publish a plugin

> How a plugin reaches the registry: the descriptor, the release, the ingest, and how advisories are filed.

How a plugin gets into the [marketplace](/concepts/marketplace): what the
registry reads, what a release has to look like, and what happens between
pushing a tag and the listing going live.

<Warning>
  The registry is curated. There is no publisher account, no submission form, and
  no self-serve claim: a namespace exists because an operator seeded a listing
  under it, and every listing today sits under the `tai42` namespace at the
  `official` trust tier. Adding a repository to the registry is an administrative
  action on the registry itself.
</Warning>

## The descriptor

Every plugin describes itself in a `tai-plugin.yml` file at the repository root,
shipped inside the built wheel as well. It is the single artifact the registry
reads — the registry never downloads or unpacks the package to learn what it
contains.

```yaml tai-plugin.yml theme={null}
spec_version: 1
namespace: tai42
name: storage-s3
package: tai42-storage-s3
version: 0.1.0
description: "S3 storage backend for the TAI ecosystem — text, binary, and media content over an S3 bucket."
license: Apache-2.0
homepage: https://tai42.ai
repository: https://github.com/tai42ai/tai-storage-s3
contract: ">=0.1,<0.2"
categories: [storage]
tags: [storage, s3, aws]
permissions:
  network: true
  subprocess: false
  filesystem: false
provides:
  - kind: storage
    name: s3
    module: tai42_storage_s3
    description: "Storage backend serving text, binary, and media content over an S3 bucket."
    tags: [s3]
```

| Field                    | Required | Rule                                                                                |
| ------------------------ | -------- | ----------------------------------------------------------------------------------- |
| `spec_version`           | yes      | Exactly `1`.                                                                        |
| `namespace`, `name`      | yes      | Lowercase slug, `a`–`z` first. Together they form the listing ref `namespace/name`. |
| `package`                | yes      | The normalised distribution name on PyPI.                                           |
| `version`                | yes      | A PEP 440 version.                                                                  |
| `description`            | yes      | One line, no control characters.                                                    |
| `license`                | yes      | An SPDX identifier.                                                                 |
| `contract`               | yes      | A PEP 440 specifier set naming the `tai42-contract` range the plugin supports.      |
| `categories`             | yes      | One to three category slugs from the registry's vocabulary.                         |
| `provides`               | yes      | At least one item; each `(kind, name)` pair unique.                                 |
| `display_name`           | no       | One line, 80 characters at most.                                                    |
| `icon`                   | no       | An `https://` URL, or a packaged image path relative to the package root.           |
| `homepage`, `repository` | no       | http(s) URLs.                                                                       |
| `tags`                   | no       | Up to ten lowercase slugs.                                                          |
| `permissions`            | no       | `network`, `subprocess`, `filesystem` booleans.                                     |

Each entry in `provides` carries a `kind` (`tool`, `agent`, `extension`,
`connector`, `channel`, `backend`, `storage`, `monitoring`, `webhook-verifier`,
`config`, `identity`, `studio-plugin`, `router`, or `middleware`), a `name`, the
`module` that registers it, and a one-line `description`. The kind is what tells
an installer which manifest field the item wires into — see
[the plugin descriptor](/guides/authors/plugin-descriptor).

<Note>
  The parser is strict on purpose: unknown keys, YAML anchors, duplicate keys, a
  file over 256 KiB, or a non-UTF-8 file are all rejected rather than
  interpreted. `permissions` is declarative metadata shown to installers — it is
  not a sandbox.
</Note>

## Cut a release

The registry resolves a version's artifact from PyPI when the distribution is
there, so the release order is: publish the distribution, then push the tag the
registry watches.

1. Bump `version` in both `pyproject.toml` and `tai-plugin.yml` to the same value.
2. Push tag `v<version>`. The shipped release workflow asserts the tag matches
   the project version, builds with `uv build`, and uploads to PyPI.
3. The registry normalises the tag by stripping one leading `v`, so `v0.2.0` must
   match `version: 0.2.0` in the descriptor.

## What the registry does with it

A registered repository's tag push reaches the registry as a GitHub webhook, and
the whole publish happens inside that one request — there is no queue, no worker,
and no scheduled crawl.

<Steps>
  <Step title="Verify the delivery">
    The HMAC signature is checked against the registry's webhook secret before
    anything else is read. A push that is not a tag is acknowledged and ignored.
  </Step>

  <Step title="Resolve the listing">
    The repository URL is matched to a registered listing. An unknown repository
    is rejected — this is where "curated" is enforced.
  </Step>

  <Step title="Read the descriptor at the tag">
    `tai-plugin.yml` is fetched at the pushed tag and validated. The registry
    then cross-checks that its `namespace`, `name`, and `package` still match the
    listing, so a registered repository cannot publish into someone else's
    namespace.
  </Step>

  <Step title="Resolve the artifact">
    The distribution is looked up on PyPI. When it is there, the version's
    artifact reference and its SHA-256 are recorded and the listing's source is
    `pypi`; when it is not, the source is `github` and the version carries no
    artifact pointer.
  </Step>

  <Step title="Render and store">
    The README is rendered to sanitised HTML, the version row is written with its
    items and its full descriptor, and the listing's displayed metadata follows
    if this version is now the latest.
  </Step>
</Steps>

Ingest refuses, loudly, when the descriptor fails validation, when a category is
outside the registry's vocabulary, when `contract` is not a parseable specifier
set, when the tag does not normalise to the descriptor's version, or when that
version has been killed. A killed version is terminal: re-pushing the tag does
not resurrect it. A distribution absent from PyPI is not a refusal — the version
is simply recorded as a `github` source.

## Advisories

An advisory marks a version range of a listing as unsafe. Filing one is a
registry-administrator action, and there is no reporting route for publishers or
users — report a problem to the registry's maintainers.

An advisory carries the listing ref, an `affected_versions` specifier set, a
severity of `low`, `medium`, `high`, or `critical`, and a one-line summary.
Withdrawing one keeps the row and stamps it withdrawn, so consumers that already
saw it also see the withdrawal.

Servers poll the public advisories feed and surface matches against what they
have installed — [Advisories](/marketplace/advisories) covers the client side and
its poll setting. A kill switch is the sharper tool: it marks a version
unpublishable rather than merely warning about it.

## See also

* [The marketplace model](/concepts/marketplace) — listings, items, versions, advisories.
* [The plugin descriptor](/guides/authors/plugin-descriptor) — writing `tai-plugin.yml`.
* [Install a plugin](/marketplace/install) — the consumer side.
