# Registry

URL: https://docs.hanzo.ai/docs/openapi/registry

> Your container and package registry: push images, pull them back, see what you store.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/registry) · [MCP](https://docs.hanzo.ai/docs/mcp-tools/registry) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Your container and package registry: push images, pull them back, see what you store.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **Base URL**   | `https://api.hanzo.ai`                 |
| **Operations** | 6                                      |
| **Auth**       | `Authorization: Bearer $HANZO_API_KEY` |

## Specification

> **HIP-1144 · Registry — The Artifact Control Plane** — Draft · [read the specification →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1144-registry-artifact-control-plane.md)

`/v1/registry` is the management plane over the platform's two running artifact
registries — the OCI registry at `oci.hanzo.ai` and the npm registry at
`pkg.hanzo.ai`: list projects, images, tags and packages, and mint scoped pull
tokens. It is implemented in `hanzoai/cloud` at `apps/registry` and
reimplements neither registry: every op is a typed read of what those services
genuinely answer, plus one token mint through the same IAM realm the docker CLI
uses (`apps/registry/registry.go:8-16`).

### Motivation

The registries are shared platform deployments with their own wire protocols
and one platform credential. A tenant needs to answer "what do I store, and who
may pull it" through the unified `/v1` plane — with IAM identity, the org
boundary, and the document/SDK/MCP projections — without cloud becoming a
second registry or a byte relay.

### Specification

The key words MUST, MUST NOT and SHOULD are to be interpreted as in RFC 2119.

#### The store, and there is none

This capability owns no store. The registries own all artifact data; the
subsystem holds only the parsed token challenge and a short-lived token cache,
in memory, pruned by expiry (`apps/registry/registry.go:127-142`).

#### Control plane only

The OCI wire — manifests, blobs, push, pull — stays on `oci.hanzo.ai` and MUST
NOT be proxied here: a data path through the API host would double-move every
image byte and break the content-addressed client protocol
(`apps/registry/registry.go:18-23`). `/v1/registry` answers the questions
AROUND the wire and hands out the address of the wire itself.

#### Addresses

Six operations under `/v1/registry`, all typed ops
(`apps/registry/registry.go:170-184`): `GET /status` (a live probe of both
halves), `GET /projects`, `GET /images`, `GET /tags`, `GET /packages`, and
`POST /token` — a short-lived, pull-only token for exactly one org-owned image,
its scope pinned server-side to `repository:<org>/<image>:pull`.

#### Tenancy

The org is the validated principal's (`principal.Org`), NEVER an In field. The
registries are shared, so the boundary is enforced HERE on their own namespace
conventions: an org's images are the catalog entries under `<org>/…` and its
packages are `<org>` and `@<org>/…`. Filtering happens before any response
shape exists — foreign names are dropped, never serialized
(`apps/registry/registry.go:649-652`) — and the npm scope filter runs on the
results, so a caller's query cannot widen it. No validated principal is 403
before any upstream byte.

Every repository segment MUST match the OCI distribution path-component grammar
before it is folded into an upstream URL or a token scope
(`apps/registry/registry.go:107-125`), so a hostile value can never smuggle
path or scope structure into the wire.

#### Money, events, telemetry

Free, said in those words: `plugin/registry/main.go` declares `cloud.Free`. It
publishes nothing to the bus, so a customer's webhooks receive nothing from it.
It emits nothing beyond the request span every route gets.

#### Failure posture

The platform catalog credential (`REGISTRY_CLIENT_ID`/`REGISTRY_CLIENT_SECRET`,
an IAM application's service credential, KMS-synced) rides only as Basic auth
to the token realm the registry's own 401 challenge advertises. An upstream
that refuses it MUST surface 503 — a deployment fault, never a caller-auth bug
— and an unreachable upstream is 503 (`apps/registry/registry.go:36-39`).

#### Stage

`ga`: the registries are developer-tools core of the self-service cloud, and
the manifest row (`manifest/apps.go:421`) declares no stage.

#### Upstream

The registries this plane manages are forks the platform runs, not code this
package embeds: `hanzoai/registry` is CNCF Distribution (Apache-2.0), S3-backed
with Hanzo IAM token auth, and `hanzoai/pkg` is Verdaccio (MIT) on S3. The app
imports neither — it speaks the OCI distribution HTTP API (including its token
challenge and RFC 5988 Link paging) and the npm registry search dialect.

### Rationale

The alternative is proxying the registries whole, which buys one hostname and
costs the content-addressed protocol: docker and npm clients already speak the
registries' own wire, with digests verified end to end, and a relay in the
middle is a second copy of every byte plus a place for the two to disagree. The
narrower design — reads plus a pinned token mint — gives the tenant boundary a
single enforcement site without touching the data path. The token being
pull-only and single-repository is the same argument at the credential layer:
the mint's job is to let a workload pull one image, not to delegate the
platform credential.

### Security Considerations

The dangerous object is the platform credential, which can read the whole
shared catalog. The wrong implementation leaks it in either direction: echoed
or logged outright, or laundered through an over-scoped minted token — a token
scope built from an unvalidated name (`repository:a/b:pull,push` smuggled via a
crafted "image") is the concrete case the segment grammar closes. Cross-tenant
listing is the other exposure: filtering on the response shape instead of
before it, or letting the caller's search query bypass the org prefix, turns a
shared catalog into everyone's. And the refusal split is load-bearing — an
upstream refusing the PLATFORM credential must read 503, because reporting it
as the caller's 401 trains callers to retry with more privilege.

## Four surfaces

| Surface  | Reaches this capability as                       | Coverage                                                                  |
| -------- | ------------------------------------------------ | ------------------------------------------------------------------------- |
| **REST** | `registry` at its own prefix                     | 6 operations                                                              |
| **CLI**  | `hanzo registry …`                               | 6 of 6                                                                    |
| **SDK**  | `RegistryApi` in every published client          | 6 methods                                                                 |
| **MCP**  | tool `registry` on `https://api.hanzo.ai/v1/mcp` | 5 operations, 1 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/registry/tags`, operation `get_registry_tags`:

**CLI**

```bash
hanzo registry tags
```

**SDK**

**.ts**

```ts
import { Configuration, RegistryApi } from 'hanzoai';

const api = new RegistryApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getRegistryTags();
```

**.py**

```python
from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import RegistryApi

client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = RegistryApi(client).get_registry_tags()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.RegistryAPI.GetRegistryTags(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::apis::{configuration::Configuration, registry_api};

let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();

let result = registry_api::get_registry_tags(&cfg, Default::default()).await?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.RegistryApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new RegistryApi(client).getRegistryTags();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/registry/tags \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **registry** through the `registry` tool, which names its 5 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
curl -X POST https://api.hanzo.ai/v1/mcp \
  -H "Content-Type: application/json" \
  -d '{
       "jsonrpc": "2.0",
       "id": 1,
       "method": "tools/call",
       "params": {
         "name": "describe",
         "arguments": {
           "op": "list_registry_images"
         }
       }
     }'
```

Answers `200` with `object` — ok.

## Endpoints

| Endpoint                                                                                         | What it does                                                                                                                                                                                                      |
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`GET /v1/registry/images`](https://docs.hanzo.ai/docs/openapi/registry/get-registry-images)     | Lists the org's container repositories, read live from the OCI catalog and filtered server-side to the org's namespace — the page can only ever hold the caller's own images.                                     |
| [`GET /v1/registry/packages`](https://docs.hanzo.ai/docs/openapi/registry/get-registry-packages) | Lists the org's npm packages — `&lt;org&gt;` and `@&lt;org&gt;/…` — from the npm registry's search index, optionally narrowed by a query within that scope.                                                       |
| [`GET /v1/registry/projects`](https://docs.hanzo.ai/docs/openapi/registry/get-registry-projects) | Lists the namespaces the caller can see with what each holds: the org's slug, its repository count on the OCI catalog, and its package count on the npm registry.                                                 |
| [`GET /v1/registry/status`](https://docs.hanzo.ai/docs/openapi/registry/get-registry-status)     | Reports whether the OCI and npm registries are reachable and, when the OCI half is auth-gated, which token realm its challenge advertises — an honest lens for "is the registry plane up", never a fabricated ok. |
| [`GET /v1/registry/tags`](https://docs.hanzo.ai/docs/openapi/registry/get-registry-tags)         | Lists one org-owned repository's tags, read live from the OCI registry.                                                                                                                                           |
| [`POST /v1/registry/token`](https://docs.hanzo.ai/docs/openapi/registry/post-registry-token)     | Mints a short-lived, pull-only registry token for exactly one of the org's images, through the same IAM realm the docker CLI authenticates against.                                                               |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
