Catalog API

Package discovery, install/uninstall, publishing, collections, and registry management. This is also how agents/workflows are provisioned — there is no direct "create agent" API; you install an AgentTemplate/WorkflowTemplate package via /catalog/install.

Endpoints

MethodPathActionDescription
GET/catalog/taxonomyCatalogReadCompiled-in category taxonomy tree
GET/catalog/searchCatalogReadSearch packages
GET/catalog/packages/{name}CatalogReadPackage details
GET/catalog/packages/{name}/versionsCatalogReadList versions
POST/catalog/installCatalogInstallInstall (and activate) a package
POST/catalog/uninstallCatalogManageUninstall a package
GET/catalog/installedCatalogReadList installed packages
POST/catalog/publishPackagePublishPublish a package archive
POST/catalog/yankCatalogManageYank (deprecate) a version
GET/catalog/registriesCatalogReadList configured registries
POST/catalog/registriesCatalogManageAdd a registry
GET/catalog/registries/{name}CatalogReadGet a registry
PUT/catalog/registries/{name}CatalogManageUpdate a registry
DELETE/catalog/registries/{name}CatalogManageRemove a registry
POST/catalog/registries/{name}/testCatalogManageTest connectivity
GET/catalog/collectionsCatalogReadList collections
POST/catalog/collectionsCatalogManageCreate a collection (proxied to registry)
GET/catalog/collections/{name}CatalogReadGet collection details
PUT/catalog/collections/{name}CatalogManageUpdate a collection
DELETE/catalog/collections/{name}CatalogManageDelete a collection
POST/catalog/collections/{name}/installCatalogInstallInstall every package in a collection

CatalogInstall is admin/operator/developer; CatalogManage (uninstall, registries, collections write, yank) is admin/operator only; CatalogRead is open to everyone including service/auditor. PackagePublish (admin/operator/developer) is checked in the handler for catalog_publish, and CatalogManage for catalog_yank — despite an older code comment above the route registrations in routes.rs claiming these two are "intentionally unprotected at the daemon level," the current handlers do call check_role(); treat the handler code, not the comment, as ground truth.


GET /catalog/taxonomy

Serves the compiled-in category taxonomy (same tree the registry's GET /taxonomy uses) so authors can browse valid [package.metadata] categories paths without a reachable registry.

curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  http://localhost:4200/catalog/taxonomy

Response (200)

{
  "version": "1.0.0",
  "domains": [
    {"id": "claims", "name": "Claims", "description": "...", "subdomains": [{"id": "claims.fnol", "name": "FNOL", "description": "..."}]}
  ]
}

GET /catalog/search

curl -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  "http://localhost:4200/catalog/search?q=claims&domain=claims&region=us&page=1&per_page=20"

Query Parameters

ParamTypeDescription
qstringFree-text query
domainstringTaxonomy domain filter
regionstringRegion filter
pageinteger1-indexed, default 1
per_pageintegerDefault 20

Note: type/package-type filtering exists in the underlying SearchQuery struct but is not currently exposed as a query param on this route (always None).

Response (200) — SearchResults

{
  "packages": [
    {
      "name": "claims-intake",
      "version": "1.2.0",
      "package_type": "Competency",
      "description": "FNOL processing for auto/property",
      "license": "MIT",
      "downloads": 412,
      "categories": ["claims.fnol"],
      "regions": ["us", "uk"],
      "signed": true
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 20
}

author is omitted entirely (issue #619 — authorship is now the authenticated publisher, not self-declared free text).

Error (502 — registry unreachable)

{"error": {"category": "CatalogError", "message": "catalog registry unreachable: ..."}}

GET /catalog/packages/{name}

Response (200) — PackageInfo

Same shape as one entry of /catalog/search's packages array.

Error (404)

{"error": {"category": "NotFound", "message": "package 'claims-intake' not found"}}

GET /catalog/packages/{name}/versions

Response (200) — array of PackageVersionInfo

[
  {
    "version": "1.2.0",
    "published_at": "2026-05-01T00:00:00Z",
    "content_hash": "sha256:...",
    "size_bytes": 48213,
    "signed": true,
    "dependencies": ["document-ocr@^1.0"],
    "yanked": false,
    "publisher_id": "insurance-corp",
    "registry_signature": "...",
    "registry_key_id": "...",
    "signed_at": "2026-05-01T00:00:05Z",
    "release_notes": "Initial release"
  }
]

POST /catalog/install

Downloads and activates a package in one call. Activation errors (post-download) surface as 500 with the partial install result attached, not a silent failure.

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"package": "claims-intake", "version": "1.2.0"}' \
  http://localhost:4200/catalog/install

Request Body

FieldTypeRequiredDescription
packagestringYesPackage name
versionstringNoSpecific version (latest if omitted)

Response (200) — InstalledPackage

{
  "name": "claims-intake",
  "version": "1.2.0",
  "package_type": "Competency",
  "installed_at": "2026-06-04T10:00:00Z",
  "content_hash": "sha256:...",
  "already_installed": false,
  "resource_id": "claims-intake",
  "resource_state": "registered"
}

resource_id/resource_state meaning depends on package_type — for AgentTemplate it's the kernel agent UUID and its lifecycle state ("running", "created", ...); for Competency/Skill/Integration it's the id and "registered"; for WorkflowTemplate it's the workflow store UUID and "hydrated". region_warning appears when the package's declared regions don't include the carrier's home_region (advisory only, install still proceeds).

Response (500 — activation failed after download)

{
  "error": {"category": "ActivationError", "message": "..."},
  "installed": { "...": "partial InstalledPackage with activation_error set" }
}

POST /catalog/uninstall

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"package": "claims-intake"}' \
  http://localhost:4200/catalog/uninstall

Response (200)

{"status": "uninstalled", "package": "claims-intake"}

GET /catalog/installed

{"packages": [ /* InstalledPackage[] */ ], "total": 3}

POST /catalog/publish

Publishes a local .hpkg archive to a registry. The archive path is validated (must be relative, no .. traversal) and its package name is read from the archive's own MANIFEST.toml — the request does not pass name/version directly.

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"archive_path": "dist/claims-intake-1.2.0.hpkg", "registry": "internal"}' \
  http://localhost:4200/catalog/publish

Request Body

FieldTypeRequiredDescription
archive_pathstringYesRelative path to the .hpkg archive, resolved server-side
registrystringNoRegistry name (defaults to the daemon's configured publish target)

Response (201) — PublishResult

{
  "name": "claims-intake",
  "version": "1.2.0",
  "content_hash": "sha256:...",
  "registry": "internal",
  "published_at": "2026-06-04T10:00:00Z"
}

POST /catalog/yank

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"package": "claims-intake", "version": "1.0.0"}' \
  http://localhost:4200/catalog/yank

Response (200) — YankResult

{"name": "claims-intake", "version": "1.0.0", "yanked": true}

GET /catalog/registries

{
  "registries": [
    {"name": "default", "url": "https://catalog.hoziron.com", "priority": 100, "enabled": true, "require_signatures": true, "status": "Unknown"}
  ]
}

status is always "Unknown" on the list endpoint (RegistryConnectivityReachable/Unreachable/Unknown) — call POST /catalog/registries/{name}/test for a live check.


POST /catalog/registries

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "internal",
    "url": "https://packages.internal.company.com",
    "priority": 50,
    "auth_token_env": "INTERNAL_REGISTRY_TOKEN"
  }' \
  http://localhost:4200/catalog/registries

priority defaults to 100, enabled/require_signatures default to true if omitted.

Response (201)

{"status": "added", "registry": {"name": "internal", "url": "https://packages.internal.company.com", "priority": 50, "enabled": true}}

Response (409 — duplicate name)

{"error": {"category": "Conflict", "message": "..."}}

PUT /catalog/registries/{name}

Body is a RegistryUpdate — every field optional, only supplied fields change:

curl -X PUT -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"priority": 10, "enabled": false}' \
  http://localhost:4200/catalog/registries/internal

Response (200)

{"status": "updated", "registry": "internal"}

DELETE /catalog/registries/{name}

Response (200)

{"status": "removed", "registry": "internal"}

POST /catalog/registries/{name}/test

Response (200) — RegistryTestResult

{"reachable": true, "authenticated": true, "latency_ms": 45, "server_version": "1.4.0"}

GET /catalog/collections

{"collections": [{"name": "claims-starter", "display_name": "Claims Starter Pack", "description": "...", "package_count": 4, "tags": ["insurance", "claims"], "featured": true}], "total": 1}

POST /catalog/collections/{name}/install

Installs every package in a collection in one call.

curl -X POST -H "Api-Version: v1" -H "Authorization: Bearer hzn_sk_..." \
  http://localhost:4200/catalog/collections/claims-starter/install

Response (200)

{"status": "installed", "collection": "claims-starter", "packages": [ /* per-package install results */ ]}

  • skills.md, competencies.md — packages of these types, installable individually via /skills/install and /competencies/install as well as through the catalog
  • agents.md — agents are activated as a side effect of installing an AgentTemplate package