Registry
Your container and package registry: push images, pull them back, see what you store.
Also for this capability: API · CLI · MCP · SDKs
Your container and package registry: push images, pull them back, see what you store.
| 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 →
/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
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/registry/tags, operation get_registry_tags:
hanzo registry tagsimport { Configuration, RegistryApi } from 'hanzoai';
const api = new RegistryApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getRegistryTags();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()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.RegistryAPI.GetRegistryTags(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::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?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.RegistryApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new RegistryApi(client).getRegistryTags();curl https://api.hanzo.ai/v1/registry/tags \
-H "Authorization: Bearer $HANZO_API_KEY"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:
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 | 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… |
GET /v1/registry/packages | Packages lists the org's npm packages — <org> and @<org>/… — from the npm registry's search index, optionally narrowed by a query within that… |
GET /v1/registry/projects | 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… |
GET /v1/registry/status | 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… |
GET /v1/registry/tags | Tags lists one org-owned repository's tags, read live from the OCI registry. |
POST /v1/registry/token | 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… |
How is this guide?