placement: one product, one owning spec — and 31 paths nothing serves

capabilities.yaml states the law and nothing enforced it. Measured against
api.hanzo.ai: 103 authored paths outside their owner's prefix, and four
products (kv, s3, search, vector) described by TWO specs at once.

provisioning was the bucket — it claimed /v1/{sql,vector,datastore,kv,search,
s3,docdb}, seven other products' roots, and its own header says why it could:
"these routes are keyed by RESOURCE KIND at top-level /v1/, NOT under
/v1/provisioning/." There is no /v1/provisioning route. Each kind's spec owns
its own quartet now; sql/datastore/docdb became their own specs under `data`;
provisioning is collapsed.

The inference edge (chat/completions, completions, embeddings, rerank, models,
messages, images/generations, audio/speech) moved from ai/ to gateway/, the
binary that answers it, and lost the vendor-as-standard prose on the way — a
tag named for another company's product is not a description of ours. The
/v1/admin/* operations authored in affiliates/, authors/, referrals/ and
plugin/ moved to admin/; /v1/edge/nodes moved from zt/ to edge/. plan/ ->
plans/ and plugin/ -> plugins/, because the routes are /v1/plans and
/v1/plugins and the route is the part that cannot move.

31 paths deleted, each probed live and absent from the route table: the whole
/v1/gateway/* subtree (23 — a phantom second copy of the inference edge, and
the reason three operationIds needed a _2 suffix to stay distinct in codegen),
/v1/load-balancers x2 (served product is /v1/balancers), /v1/bots/launch|{id}|
{id}/{action} (served at /v1/compute/bots/*), /v1/machines/{id}/bind-agent|
agent-binding (served at /v1/machines/{id}/agent), /v1/audio/transcriptions.
hanzo.yaml 1742 -> 1711 paths: 31 removed, 0 added, nothing served lost.

test_placement.py is the gate. Rule 1 — no two authored specs claim one
product — is absolute. Rule 2 — a spec claims its own prefix — carries
OFF_PREFIX, 48 declared exceptions each measured live, each a binary answering
at a noun that is not its service's name; the fix for those is a route move in
the serving repo, and a stated gap beats a silent one.

flows.yaml's chat journey follows the operation it names: ai_createChatCompletion
-> gateway_createChatCompletion. That gate caught the move, which is what it is
for.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-01 10:29:43 -07:00
co-authored by Hanzo Dev
parent 11a70e6c5d
commit a2a49658e5
29 changed files with 6072 additions and 5874 deletions
+8 -5
View File
@@ -46,6 +46,9 @@ One canonical name per capability = one `/v1/<name>` route = one `<name>/openapi
| `base` | `/v1/base` | ✓ |
| `kv` | `/v1/kv` | ✓ |
| `vector` | `/v1/vector` | ✓ |
| `sql` | `/v1/sql` | ✓ |
| `datastore` | `/v1/datastore` | ✓ |
| `docdb` | `/v1/docdb` | ✓ |
## Streams
@@ -75,7 +78,7 @@ One canonical name per capability = one `/v1/<name>` route = one `<name>/openapi
| `commerce` | `/v1/commerce` | ✓ |
| `billing` | `/v1/billing` | ✓ |
| `pricing` | `/v1/pricing` | ✓ |
| `plan` | `/v1/plan` | ✓ |
| `plans` | `/v1/plans` | ✓ |
| `crm` | `/v1/crm` | ✓ |
| `referrals` | `/v1/referrals` | ✓ |
| `affiliates` | `/v1/affiliates` | ✓ |
@@ -89,7 +92,6 @@ One canonical name per capability = one `/v1/<name>` route = one `<name>/openapi
|---|---|---|
| `cloud` | `/v1/cloud` | ✓ |
| `platform` | `/v1/platform` | ✓ |
| `provisioning` | `/v1/provisioning` | ✓ |
| `gateway` | `/v1/gateway` | ✓ |
| `visor` | `/v1/visor` | ✓ |
| `edge` | `/v1/edge` | ✓ |
@@ -121,7 +123,7 @@ One canonical name per capability = one `/v1/<name>` route = one `<name>/openapi
| Capability | Route | Spec |
|---|---|---|
| `admin` | `/v1/admin` | ✓ |
| `plugin` | `/v1/plugin` | ✓ |
| `plugins` | `/v1/plugins` | ✓ |
## Collapsed
@@ -131,8 +133,9 @@ Resolved duplicates / consolidations — removed, no spec dir. The capability no
|---|---|
| `eval` | evals |
| `operative` | bot + visor |
| `plans` | plan |
| `plugins` | plugin |
| `plan` | plans |
| `plugin` | plugins |
| `provisioning` | sql + vector + datastore + kv + search + s3 + docdb |
| `featuregate` | featureflags |
| `ads` | marketing |
| `cms` | content |
+47
View File
@@ -2,6 +2,53 @@
## v1.0.0
### One product, one owning spec — placement, and 31 dead paths deleted
`capabilities.yaml` has always stated the law — "the route IS the identity: one
capability = one name = one /v1/<name> = one <name>/openapi.yaml" — and nothing
enforced it. Measured against the live route table at api.hanzo.ai: **103
authored paths** sat outside their owner's prefix and **four products** (`kv`,
`s3`, `search`, `vector`) were described by TWO specs at once.
`provisioning/openapi.yaml` was the bucket: it claimed `/v1/sql`, `/v1/vector`,
`/v1/datastore`, `/v1/kv`, `/v1/search`, `/v1/s3` and `/v1/docdb` — seven other
products' roots — and its own header says why it could: "these routes are keyed
by RESOURCE KIND at top-level /v1/, NOT under /v1/provisioning/." There is no
`/v1/provisioning` route and never was. Each kind's spec owns its own quartet
now; `sql`, `datastore` and `docdb` became their own specs under `data`;
`provisioning` is `collapsed`.
The inference edge — `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`,
`/v1/rerank`, `/v1/models`, `/v1/models/{model}`, `/v1/messages`,
`/v1/images/generations`, `/v1/audio/speech` — moved from `ai/` to `gateway/`,
the binary that answers it. The `/v1/admin/*` operations authored in
`affiliates/`, `authors/`, `referrals/` and `plugin/` moved to `admin/`;
`/v1/edge/nodes` moved from `zt/` to `edge/`. `plan/` → `plans/` and `plugin/` →
`plugins/`, because the routes are `/v1/plans` and `/v1/plugins` and the route is
the part that cannot move.
**31 paths deleted**, each probed at api.hanzo.ai and absent from the live route
table: the whole `/v1/gateway/*` subtree (23 — a phantom second copy of the
inference edge, which is also why three operationIds needed a `_2` suffix to
stay distinct in codegen and no longer do), `/v1/load-balancers` ×2 (the served
product is `/v1/balancers`), `/v1/bots/launch|{id}|{id}/{action}` (served at
`/v1/compute/bots/*`), `/v1/machines/{id}/bind-agent|agent-binding` (served at
`/v1/machines/{id}/agent`), and `/v1/audio/transcriptions`. Nothing served was
removed: hanzo.yaml went 1742 → 1711 paths, 31 removed and 0 added.
Fifteen more authored paths 404 at the server while the live route table lists
them as keys — `/v1/admin/{affiliates,authors}/*` writes, `/v1/admin/plugins/*`
control, `/v1/sites/deploy`, `/v1/clusters/{clusterId}/pools*`. Those are
hanzoai/cloud describing routes whose handlers are not reachable, not authoring
defects, so they stay until cloud fixes them — this repo cannot refute the
registry it trusts.
`test_placement.py` is the gate, in CI via `hanzo.yml`. Rule 1 (no two authored
specs claim one product) is absolute. Rule 2 (a spec claims its own prefix)
carries `OFF_PREFIX`, 48 declared exceptions each measured live, each a binary
answering at a top-level noun that is not its service's name — the fix for those
is a route move in the serving repo, and a stated gap beats a silent one.
### An empty field from the winner was deleting a populated one — 182 shapes back
The resync's worst defect, and it outlived two rounds of measurement here
+39 -2
View File
@@ -25,7 +25,9 @@ prefixes, no cross-brand references.
`hanzoai/js-sdk`'s `scripts/generate.sh:22` fetches it unauthenticated today
and dies on `curl -f`. The in-repo path (`generate.py`, which reads the local
file) needs no network at all and is the one to prefer.
- `<service>/openapi.yaml` — one self-contained spec per service.
- `<service>/openapi.yaml` — one self-contained spec per service. The route IS
the identity: `<svc>/openapi.yaml` describes `/v1/<svc>` and nothing else, and
`test_placement.py` is its gate (below).
- `cloud/openapi.yaml` — the ONE spec NOT authored here: hanzoai/cloud's own
woven document, copied verbatim by `sync.py`. Source-true, and it wins (below).
- `sync.py` — the resync, one command: pull cloud's document, then merge.
@@ -117,6 +119,41 @@ service's name rather than landing in `DefaultApi`, and an operation with no
invented — gets a `default` that says exactly that, so the document validates
without anyone inventing a schema.
## Placement — one product, one owning spec
`capabilities.yaml` states it: "the route IS the identity: one capability = one
name = one `/v1/<name>` = one `<name>/openapi.yaml`." Nothing enforced it, so it
drifted three ways at once, and `test_placement.py` now gates all three.
**Two specs described one product.** `provisioning/openapi.yaml` claimed
`/v1/sql`, `/v1/vector`, `/v1/datastore`, `/v1/kv`, `/v1/search`, `/v1/s3` and
`/v1/docdb` — seven other products' roots — while `vector/`, `kv/`, `s3/` and
`search/` claimed the same prefixes. Asking "who owns `/v1/vector`" got two
answers and every consumer picked one. There was never a `/v1/provisioning`
route: `Mount` registers one CRUD quartet per KIND, so each kind's spec owns its
own quartet now and `provisioning` is `collapsed`. `sql`, `datastore` and
`docdb` are their own specs, in `data`, because they are their own products.
**A spec's name disagreed with its route.** `plan/` served `/v1/plans` and
`plugin/` served `/v1/plugins`. The route is immutable and the directory is not,
so the directories moved: `plans/`, `plugins/`. The old `collapsed` entries said
the singular was canonical — backwards, and the law above is why.
**One binary's surface sat in another binary's spec.** The inference edge
(`/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/rerank`,
`/v1/models`, `/v1/messages`, `/v1/images/generations`, `/v1/audio/speech`) was
authored in `ai/openapi.yaml`, which describes hanzoai/ai. The gateway serves
them; they live in `gateway/openapi.yaml` now. The same move deleted the phantom
`/v1/gateway/*` copy of that surface — 23 paths, all route-level 404 and none in
the live route table — which is also why three operationIds had to be suffixed
to stay distinct in codegen and no longer do.
Where a binary really does answer at a noun that is not its service's name, the
path is declared in `OFF_PREFIX` in `test_placement.py` with the reason. That
table is the honest statement of the remaining 48; the fix for each is a route
move in the serving repo, not a re-file here. Everything not in it fails the
gate.
## Validate
```bash
@@ -139,7 +176,7 @@ python3 -c "import yaml, glob; [yaml.safe_load(open(s)) for s in glob.glob('*/op
## Generated specs — a spec cannot describe a route its service does not serve
The 69 `<service>/openapi.yaml` files are hand-written, and a hand-written spec
The `<service>/openapi.yaml` files are hand-written, and a hand-written spec
drifts silently in both directions: it declares operations nothing serves, and
it misses operations that are served. `generated/` is the other reading — what
a binary emits from its OWN route table — and `audit.py` is the measurement
+1047
View File
File diff suppressed because it is too large Load Diff
-306
View File
@@ -51,8 +51,6 @@ security:
tags:
- name: Affiliate
description: Customer-facing affiliate program (org-scoped).
- name: Admin
description: Global-admin affiliate management, payouts, and accrual sweep.
paths:
/v1/affiliates:
get:
@@ -153,147 +151,6 @@ paths:
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/v1/admin/affiliates:
get:
tags:
- Admin
summary: List all affiliates
description: |
Returns every affiliate (org exposed) plus a fleet summary. Global-admin
only. Wrapped in the admin `{ status, msg, data }` envelope.
operationId: adminListAffiliates
parameters:
- name: limit
in: query
required: false
description: Max rows to return (default 500, max 1000).
schema:
type: integer
default: 500
maximum: 1000
responses:
'200':
description: Affiliate directory and summary.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminListEnvelope'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/v1/admin/affiliates/sweep:
post:
tags:
- Admin
summary: Run the accrual sweep
description: |
The periodic accrual path (cron/o11y hits it, or an operator on demand). It
folds over every approved affiliate's referred orgs and accrues this period's
commission, at-most-once per period. Global-admin only.
operationId: adminSweepAffiliates
responses:
'200':
description: Sweep result (edges checked and accruals created).
content:
application/json:
schema:
$ref: '#/components/schemas/AdminSweepEnvelope'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalError'
/v1/admin/affiliates/{id}/approve:
post:
tags:
- Admin
summary: Approve an affiliate and mint its code
description: |
Approves the affiliate and mints its code. The body may carry an explicit
`code` override; else the requested vanity code; else a derived slug.
Global-admin only.
operationId: adminApproveAffiliate
parameters:
- $ref: '#/components/parameters/AffiliateId'
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/ApproveRequest'
responses:
'200':
description: The approved affiliate.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAffiliateEnvelope'
'400':
$ref: '#/components/responses/BadRequest'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'500':
$ref: '#/components/responses/InternalError'
/v1/admin/affiliates/{id}/suspend:
post:
tags:
- Admin
summary: Suspend an affiliate
description: Suspends the affiliate. Global-admin only.
operationId: adminSuspendAffiliate
parameters:
- $ref: '#/components/parameters/AffiliateId'
responses:
'200':
description: The suspended affiliate.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAffiliateEnvelope'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
/v1/admin/affiliates/{id}/payout:
post:
tags:
- Admin
summary: Record a payout
description: |
Records a payout of accrued commission. A `credits` method issues a commerce
grant into the affiliate's wallet; a cash method (wire/paypal/…) is
record-only. The amount can never exceed pending (accrued − paid), reserved
atomically before any grant. Global-admin only.
operationId: adminPayoutAffiliate
parameters:
- $ref: '#/components/parameters/AffiliateId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PayoutRequest'
responses:
'200':
description: The recorded payout and the updated affiliate.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminPayoutEnvelope'
'400':
$ref: '#/components/responses/BadRequest'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalError'
components:
securitySchemes:
BearerAuth:
@@ -301,14 +158,6 @@ components:
scheme: bearer
bearerFormat: JWT
description: Bearer JWT issued by Hanzo IAM (https://hanzo.id). The same token authenticates every Hanzo service.
parameters:
AffiliateId:
name: id
in: path
required: true
description: The affiliate id (e.g. `aff_<hex>`).
schema:
type: string
responses:
BadRequest:
description: Invalid request.
@@ -328,12 +177,6 @@ components:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: The requested code is already taken.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalError:
description: Internal server error.
content:
@@ -458,31 +301,6 @@ components:
type: integer
format: int64
description: Unix timestamp (seconds).
ApproveRequest:
type: object
description: Optional body for POST /v1/admin/affiliates/{id}/approve.
properties:
code:
type: string
description: Explicit code override. If omitted, the requested vanity code or a derived slug is used.
PayoutRequest:
type: object
required:
- amountCents
- method
description: Body for POST /v1/admin/affiliates/{id}/payout.
properties:
amountCents:
type: integer
format: int64
description: Payout amount in USD cents (must be positive; cannot exceed pending commission).
method:
type: string
description: Payout method. `credits` issues a commerce grant; any other value (wire/paypal/check/…) is record-only.
example: credits
reference:
type: string
description: Optional external reference for a cash payout.
AffiliateStatus:
type: string
description: Affiliate lifecycle status. An affiliate advances applied → approved (and can be suspended).
@@ -490,64 +308,6 @@ components:
- applied
- approved
- suspended
AdminAffiliateView:
type: object
description: One row in the global-admin directory (org exposed).
properties:
id:
type: string
org:
type: string
code:
type: string
requestedCode:
type: string
status:
$ref: '#/components/schemas/AffiliateStatus'
rateBps:
type: integer
format: int64
referredCount:
type: integer
accruedCents:
type: integer
format: int64
pendingCents:
type: integer
format: int64
paidCents:
type: integer
format: int64
createdAt:
type: integer
format: int64
approvedAt:
type: integer
format: int64
suspendedAt:
type: integer
format: int64
AdminSummary:
type: object
description: Fleet tally for the admin directory.
properties:
total:
type: integer
applied:
type: integer
approved:
type: integer
suspended:
type: integer
accruedCents:
type: integer
format: int64
pendingCents:
type: integer
format: int64
paidCents:
type: integer
format: int64
Payout:
type: object
description: One recorded disbursement of accrued commission.
@@ -567,69 +327,3 @@ components:
createdAt:
type: integer
format: int64
AdminListEnvelope:
type: object
description: Admin `{ status, msg, data }` envelope for the affiliate directory.
properties:
status:
type: string
example: ok
msg:
type: string
data:
type: object
properties:
affiliates:
type: array
items:
$ref: '#/components/schemas/AdminAffiliateView'
summary:
$ref: '#/components/schemas/AdminSummary'
AdminAffiliateEnvelope:
type: object
description: Admin envelope wrapping a single affiliate.
properties:
status:
type: string
example: ok
msg:
type: string
data:
type: object
properties:
affiliate:
$ref: '#/components/schemas/AdminAffiliateView'
AdminPayoutEnvelope:
type: object
description: Admin envelope wrapping a recorded payout and the updated affiliate.
properties:
status:
type: string
example: ok
msg:
type: string
data:
type: object
properties:
payout:
$ref: '#/components/schemas/Payout'
affiliate:
$ref: '#/components/schemas/AdminAffiliateView'
AdminSweepEnvelope:
type: object
description: Admin envelope wrapping the accrual sweep result.
properties:
status:
type: string
example: ok
msg:
type: string
data:
type: object
properties:
swept:
type: integer
description: Total referred-org edges checked across all approved affiliates.
accrued:
type: integer
description: Total new accrual events created this sweep.
-435
View File
@@ -35,237 +35,7 @@ servers:
security:
- BearerAuth: []
tags:
- name: OpenAI Compatible
description: Drop-in OpenAI Chat/Completions/Embeddings/Models surface.
- name: Claude Compatible
description: Drop-in Anthropic Messages surface.
- name: Models
description: Model discovery.
paths:
/v1/chat/completions:
post:
tags:
- OpenAI Compatible
summary: Create chat completion
description: |
OpenAI-compatible chat completions. Set `stream: true` for an SSE token
stream (`text/event-stream`), otherwise a single JSON response. `model`
accepts any model id from `GET /v1/models` (including the Zen ladder).
operationId: createChatCompletion
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ChatCompletionRequest'
responses:
'200':
description: 'Completion (JSON) or SSE stream when `stream: true`.'
content:
application/json:
schema:
$ref: '#/components/schemas/ChatCompletionResponse'
text/event-stream:
schema:
type: string
'401': { $ref: '#/components/responses/Unauthorized' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'429': { $ref: '#/components/responses/RateLimited' }
/v1/completions:
post:
tags:
- OpenAI Compatible
summary: Create completion (legacy text)
operationId: createCompletion
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CompletionRequest'
responses:
'200':
description: 'Completion (JSON) or SSE stream when `stream: true`.'
content:
application/json:
schema: { type: object }
text/event-stream:
schema: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/embeddings:
post:
tags:
- OpenAI Compatible
summary: Create embeddings
operationId: createEmbeddings
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmbeddingRequest'
responses:
'200':
description: Embedding vectors.
content:
application/json:
schema:
$ref: '#/components/schemas/EmbeddingResponse'
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/rerank:
post:
tags:
- OpenAI Compatible
summary: Rerank documents against a query
operationId: rerank
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [model, query, documents]
properties:
model: { type: string }
query: { type: string }
documents:
type: array
items: { type: string }
top_n: { type: integer }
responses:
'200':
description: Ranked results.
content:
application/json:
schema: { type: object }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/models:
get:
tags:
- Models
summary: List available models
description: OpenAI-compatible model list across every routable provider.
operationId: listModels
responses:
'200':
description: Model list.
content:
application/json:
schema:
$ref: '#/components/schemas/ModelList'
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/models/{model}:
get:
tags:
- Models
summary: Retrieve a model
operationId: retrieveModel
parameters:
- name: model
in: path
required: true
schema: { type: string }
responses:
'200':
description: Model object.
content:
application/json:
schema:
$ref: '#/components/schemas/Model'
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/v1/messages:
post:
tags:
- Claude Compatible
summary: Create a message (Anthropic-compatible)
description: |
Anthropic Messages-compatible endpoint. Set `stream: true` for an SSE
event stream. Drop-in for the Anthropic SDK and Claude Code's
`ANTHROPIC_BASE_URL`.
operationId: createMessage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageRequest'
responses:
'200':
description: 'Message (JSON) or SSE stream when `stream: true`.'
content:
application/json:
schema:
$ref: '#/components/schemas/MessageResponse'
text/event-stream:
schema: { type: string }
'401': { $ref: '#/components/responses/Unauthorized' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'429': { $ref: '#/components/responses/RateLimited' }
/v1/images/generations:
post:
tags:
- OpenAI Compatible
summary: Create image
description: OpenAI-compatible text-to-image generation.
operationId: createImage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ImageGenerationRequest'
responses:
'200':
description: Generated image(s).
content:
application/json:
schema:
$ref: '#/components/schemas/ImageResponse'
'401': { $ref: '#/components/responses/Unauthorized' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'429': { $ref: '#/components/responses/RateLimited' }
/v1/audio/speech:
post:
tags:
- OpenAI Compatible
summary: Create speech (text-to-speech)
description: OpenAI-compatible text-to-speech. Returns binary audio.
operationId: createSpeech
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SpeechRequest'
responses:
'200':
description: Audio stream.
content:
application/octet-stream:
schema: { type: string, format: binary }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/audio/transcriptions:
post:
tags:
- OpenAI Compatible
summary: Create transcription (speech-to-text)
description: OpenAI-compatible speech-to-text (Whisper interface).
operationId: createTranscription
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/TranscriptionRequest'
responses:
'200':
description: Transcription result.
content:
application/json:
schema:
$ref: '#/components/schemas/TranscriptionResponse'
'401': { $ref: '#/components/responses/Unauthorized' }
# BEGIN generated: resource surface (go run ./cmd/openapi) — DO NOT EDIT BY HAND
/v1/ai/account:
get:
@@ -5852,212 +5622,7 @@ components:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
PaymentRequired:
description: Insufficient balance — add credits.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
RateLimited:
description: Rate limit exceeded.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
NotFound:
description: Resource not found.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
schemas:
ChatCompletionRequest:
title: ChatCompletionRequest
type: object
required: [model, messages]
properties:
model: { type: string }
messages:
type: array
items: { $ref: '#/components/schemas/ChatMessage' }
stream: { type: boolean, default: false }
temperature: { type: number }
top_p: { type: number }
max_tokens: { type: integer }
tools:
type: array
items: { type: object }
tool_choice: {}
ChatMessage:
title: ChatMessage
type: object
required: [role]
properties:
role:
type: string
enum: [system, user, assistant, tool]
content: {}
name: { type: string }
tool_calls:
type: array
items: { type: object }
ChatChoice:
title: ChatChoice
# The one field of the one route every SDK user calls first. As a bare
# `type: object` it generated `Array<object>` / `List[object]`, so reading
# the reply — choices[0].message.content, the whole point of the call —
# needed a cast in every language, on the endpoint we most want to feel
# typed. This is not a guess about the wire: the route is
# OpenAI-compatible (that is its tag), and `message` is the ChatMessage
# already defined above and already accepted on the request.
type: object
properties:
index: { type: integer }
message: { $ref: '#/components/schemas/ChatMessage' }
finish_reason:
type: string
examples: [stop, length, tool_calls, content_filter]
ChatCompletionResponse:
title: ChatCompletionResponse
type: object
properties:
id: { type: string }
object: { type: string, examples: [chat.completion] }
created: { type: integer }
model: { type: string }
choices:
type: array
items: { $ref: '#/components/schemas/ChatChoice' }
usage: { $ref: '#/components/schemas/Usage' }
CompletionRequest:
title: CompletionRequest
type: object
required: [model, prompt]
properties:
model: { type: string }
prompt: {}
stream: { type: boolean, default: false }
max_tokens: { type: integer }
temperature: { type: number }
EmbeddingRequest:
title: EmbeddingRequest
type: object
required: [model, input]
properties:
model: { type: string }
input: {}
encoding_format:
type: string
enum: [float, base64]
EmbeddingResponse:
title: EmbeddingResponse
type: object
properties:
object: { type: string, examples: [list] }
data:
type: array
items: { type: object }
model: { type: string }
usage: { $ref: '#/components/schemas/Usage' }
ModelList:
title: ModelList
type: object
properties:
object: { type: string, examples: [list] }
data:
type: array
items: { $ref: '#/components/schemas/Model' }
Model:
title: Model
type: object
properties:
id: { type: string }
object: { type: string, examples: [model] }
created: { type: integer }
owned_by: { type: string }
MessageRequest:
title: MessageRequest
type: object
required: [model, messages, max_tokens]
properties:
model: { type: string }
messages:
type: array
items: { type: object }
max_tokens: { type: integer }
stream: { type: boolean, default: false }
system: {}
temperature: { type: number }
tools:
type: array
items: { type: object }
MessageResponse:
title: MessageResponse
type: object
properties:
id: { type: string }
type: { type: string, examples: [message] }
role: { type: string, examples: [assistant] }
model: { type: string }
content:
type: array
items: { type: object }
stop_reason: { type: string }
usage: { type: object }
Usage:
title: Usage
type: object
properties:
prompt_tokens: { type: integer }
completion_tokens: { type: integer }
total_tokens: { type: integer }
ImageGenerationRequest:
title: ImageGenerationRequest
type: object
required: [prompt]
properties:
model: { type: string }
prompt: { type: string }
n: { type: integer, default: 1 }
size: { type: string, examples: ['1024x1024'] }
quality: { type: string, enum: [standard, hd] }
response_format: { type: string, enum: [url, b64_json] }
ImageResponse:
title: ImageResponse
type: object
properties:
created: { type: integer }
data:
type: array
items:
type: object
properties:
url: { type: string }
b64_json: { type: string }
revised_prompt: { type: string }
SpeechRequest:
title: SpeechRequest
type: object
required: [model, input, voice]
properties:
model: { type: string }
input: { type: string }
voice: { type: string }
response_format: { type: string, enum: [mp3, opus, aac, flac, wav, pcm] }
speed: { type: number }
TranscriptionRequest:
title: TranscriptionRequest
type: object
required: [file, model]
properties:
file: { type: string, format: binary }
model: { type: string }
language: { type: string }
prompt: { type: string }
response_format: { type: string, enum: [json, text, srt, verbose_json, vtt] }
temperature: { type: number }
TranscriptionResponse:
title: TranscriptionResponse
type: object
properties:
text: { type: string }
Envelope:
title: Envelope
description: |
-248
View File
@@ -51,8 +51,6 @@ tags:
description: Author enrollment, repo verification, and dashboard (org surface).
- name: Deploys
description: Deploy-attribution recording (deploying-org surface).
- name: Admin
description: Author directory, accrual sweep, approval, suspension, and payout (global-admin only).
paths:
/v1/authors:
get:
@@ -190,123 +188,6 @@ paths:
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalServerError' }
/v1/admin/authors:
get:
tags:
- Admin
summary: List all authors
description: Every author (org exposed) plus a fleet summary. Global-admin only.
operationId: adminListAuthors
parameters:
- name: limit
in: query
description: Max rows (default 500, capped at 1000).
schema: { type: integer, minimum: 1, maximum: 1000, default: 500 }
responses:
'200':
description: Author directory and summary.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminListEnvelope'
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalServerError' }
/v1/admin/authors/sweep:
post:
tags:
- Admin
summary: Run accrual sweep
description: |
The periodic accrual path. Folds over every approved author's distinct
deploying orgs and accrues this period's royalty, at-most-once per period.
Global-admin only.
operationId: adminSweepAuthors
responses:
'200':
description: Sweep counts.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminSweepEnvelope'
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalServerError' }
/v1/admin/authors/{id}/approve:
post:
tags:
- Admin
summary: Approve an author
description: |
Admits an author to earning. An optional `{shareBps}` body overrides the
royalty share (0–10000 basis points). Global-admin only.
operationId: adminApproveAuthor
parameters:
- $ref: '#/components/parameters/AuthorId'
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/ApproveRequest'
responses:
'200':
description: Approved author.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAuthorEnvelope'
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalServerError' }
/v1/admin/authors/{id}/suspend:
post:
tags:
- Admin
summary: Suspend an author
description: Suspends an author. Global-admin only.
operationId: adminSuspendAuthor
parameters:
- $ref: '#/components/parameters/AuthorId'
responses:
'200':
description: Suspended author.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminAuthorEnvelope'
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalServerError' }
/v1/admin/authors/{id}/payout:
post:
tags:
- Admin
summary: Record a payout
description: |
Records a payout of accrued royalty. A `credits` method issues a commerce
grant into the author's wallet; a cash method (wire, paypal, check, …) is
record-only. The amount can never exceed pending (accrued − paid), reserved
atomically before any grant. Global-admin only.
operationId: adminPayoutAuthor
parameters:
- $ref: '#/components/parameters/AuthorId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PayoutRequest'
responses:
'200':
description: Recorded payout and updated author.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminPayoutEnvelope'
'400': { $ref: '#/components/responses/BadRequest' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalServerError' }
components:
securitySchemes:
BearerAuth:
@@ -316,13 +197,6 @@ components:
description: 'Bearer JWT issued by Hanzo IAM (https://hanzo.id) or an hk- API
key. Discovery: https://hanzo.id/.well-known/openid-configuration. Admin
routes require a global-admin identity.'
parameters:
AuthorId:
name: id
in: path
required: true
description: Author id (e.g. aut_...).
schema: { type: string }
responses:
BadRequest:
description: Invalid input parameters.
@@ -334,11 +208,6 @@ components:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
NotFound:
description: Author not found.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
InternalServerError:
description: Internal server error.
content:
@@ -383,33 +252,6 @@ components:
project:
type: string
description: The published project id.
ApproveRequest:
title: ApproveRequest
type: object
description: Optional royalty-share override.
properties:
shareBps:
type: integer
minimum: 0
maximum: 10000
description: Royalty share in basis points (500 = 5%).
PayoutRequest:
title: PayoutRequest
type: object
required: [amountCents, method]
properties:
amountCents:
type: integer
format: int64
minimum: 1
description: Payout amount in USD minor units (cents). Must be positive and not exceed pending.
method:
type: string
description: Payout method. 'credits' issues a commerce grant; any other value (wire, paypal, check, …) is record-only.
examples: [credits]
reference:
type: string
description: Optional external reference for a cash disbursement.
NotEnrolled:
title: NotEnrolled
type: object
@@ -523,93 +365,3 @@ components:
type: string
description: Commerce transaction id for a credits payout.
createdAt: { type: integer, format: int64 }
AdminAuthorView:
title: AdminAuthorView
type: object
description: One row in the global-admin directory (org exposed).
properties:
id: { type: string }
org: { type: string }
githubLogin: { type: string }
status:
type: string
enum: [connected, approved, suspended]
verified: { type: boolean }
shareBps: { type: integer, format: int64 }
repoCount: { type: integer }
deployCount: { type: integer }
accruedCents: { type: integer, format: int64 }
pendingCents: { type: integer, format: int64 }
paidCents: { type: integer, format: int64 }
createdAt: { type: integer, format: int64 }
approvedAt: { type: integer, format: int64 }
suspendedAt: { type: integer, format: int64 }
AdminSummary:
title: AdminSummary
type: object
description: Fleet tally for the admin directory.
properties:
total: { type: integer }
connected: { type: integer }
approved: { type: integer }
suspended: { type: integer }
accruedCents: { type: integer, format: int64 }
pendingCents: { type: integer, format: int64 }
paidCents: { type: integer, format: int64 }
AdminEnvelope:
title: AdminEnvelope
type: object
description: The { status, msg, data } envelope wrapping every admin response.
properties:
status: { type: string, examples: [ok] }
msg: { type: string }
data: { type: object }
AdminListEnvelope:
title: AdminListEnvelope
allOf:
- $ref: '#/components/schemas/AdminEnvelope'
- type: object
properties:
data:
type: object
properties:
authors:
type: array
items: { $ref: '#/components/schemas/AdminAuthorView' }
summary: { $ref: '#/components/schemas/AdminSummary' }
AdminSweepEnvelope:
title: AdminSweepEnvelope
allOf:
- $ref: '#/components/schemas/AdminEnvelope'
- type: object
properties:
data:
type: object
properties:
swept:
type: integer
description: Deploying-org checks performed across all approved authors.
accrued:
type: integer
description: Accruals newly created.
AdminAuthorEnvelope:
title: AdminAuthorEnvelope
allOf:
- $ref: '#/components/schemas/AdminEnvelope'
- type: object
properties:
data:
type: object
properties:
author: { $ref: '#/components/schemas/AdminAuthorView' }
AdminPayoutEnvelope:
title: AdminPayoutEnvelope
allOf:
- $ref: '#/components/schemas/AdminEnvelope'
- type: object
properties:
data:
type: object
properties:
payout: { $ref: '#/components/schemas/PayoutView' }
author: { $ref: '#/components/schemas/AdminAuthorView' }
+7 -6
View File
@@ -39,7 +39,7 @@ domains:
- id: data
title: "Data"
role: "the stores every capability writes to and reads from"
services: [storage, s3, base, kv, vector]
services: [storage, s3, base, kv, vector, sql, datastore, docdb]
- id: streams
title: "Streams"
@@ -54,12 +54,12 @@ domains:
- id: commerce
title: "Commerce"
role: "the economy — meter · price · bill · reward"
services: [commerce, billing, pricing, plan, crm, referrals, affiliates, licensing]
services: [commerce, billing, pricing, plans, crm, referrals, affiliates, licensing]
- id: platform
title: "Platform"
role: "the cloud fabric — deploy · provision · route · host"
services: [cloud, platform, provisioning, gateway, visor, edge, dns, do]
services: [cloud, platform, gateway, visor, edge, dns, do]
- id: applications
title: "Applications"
@@ -67,7 +67,7 @@ domains:
services: [projects, git, templates, integrations, bot, kb, search, websearch, world, authors, sites, tracker, framework]
# Core — governance that owns no single domain.
core: [admin, plugin]
core: [admin, plugins]
# Internal — power the music, are not instruments. Excluded from /v1, SDKs, docs.
internal: [principal, goja, mpc, controlplane, metrics, audit]
@@ -84,8 +84,9 @@ derived: []
collapsed:
eval: evals # pure dup — same /v1/evals
operative: bot + visor # computer-using-agent split: bot = the CUA (@hanzo/bot), visor = the VM it runs on
plans: plan # singular canonical
plugins: plugin
plan: plans # the route is /v1/plans; the route IS the identity
plugin: plugins # the route is /v1/plugins; the route IS the identity
provisioning: sql + vector + datastore + kv + search + s3 + docdb # never had a /v1/provisioning route — it mounted one CRUD quartet per KIND, so each kind's spec now owns its own
featuregate: featureflags # policy toggles → one feature-flags surface (Policy)
ads: marketing # ad surfaces fold into marketing
cms: content # content management = content
+281
View File
@@ -0,0 +1,281 @@
openapi: 3.1.0
info:
title: Hanzo Datastore API
summary: Managed analytics datastore (ClickHouse wire) — a dedicated per-org instance.
description: |
Provisioning turns "create a datastore" into a real logical resource inside an
already-live shared backend, or launches the org's OWN isolated instance for
the dedicated kinds (HIP-0106). Served by the `clients/provisioning`
subsystem of the unified cloud binary and reached through the gateway at
`api.hanzo.ai`; the kind string is the Hanzo product name, never the upstream
OSS backend name.
Tenancy: every request is scoped to the gateway-minted org (`X-Org-Id` /
validated principal). A request without a validated principal, or with an
empty org and no admin claim, is refused 403. The physical backend resource
is namespaced by a fixed-width org hash so two tenants can never fold onto
one resource.
Secrets: generated per-resource passwords are sealed in Hanzo KMS and only a
`secret_ref` is persisted. When KMS is not configured the service degrades
safely — it returns the password ONCE in the create response and stores
nothing in plaintext.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
version: 8.0.0
contact:
name: Hanzo AI
url: https://hanzo.ai
email: [email protected]
license:
name: BSD 3-Clause License
identifier: BSD-3-Clause
servers:
- url: https://api.hanzo.ai
description: Hanzo Gateway Production
tags:
- name: datastore
security:
- BearerAuth: []
paths:
/v1/datastore:
post:
tags: [datastore]
operationId: createDatastore
summary: Provision a datastore resource (dedicated per-org instance)
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [datastore]
operationId: listDatastore
summary: List datastore resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/datastore/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [datastore]
operationId: getDatastore
summary: Get one datastore resource (reconciles live instance status)
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [datastore]
operationId: deleteDatastore
summary: Deprovision a datastore instance
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
components:
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
required: [status, msg]
properties:
status:
type: string
enum: [error]
msg:
type: string
description: Human-readable error message.
code:
type: integer
requestId:
type: string
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Bearer JWT issued by Hanzo IAM (https://hanzo.id).
-74
View File
@@ -25,8 +25,6 @@ servers:
tags:
- name: VPCs
description: Virtual Private Cloud networks
- name: Load Balancers
description: Regional/global load balancers
security:
- bearerAuth: []
paths:
@@ -79,54 +77,7 @@ paths:
'404': { description: VPC not found }
'503': { description: DigitalOcean not configured }
/v1/load-balancers:
get:
tags: [Load Balancers]
summary: List the caller's load balancers
operationId: listLoadBalancers
responses:
'200': { description: Load balancers, content: { application/json: { schema: { type: object, properties: { loadBalancers: { type: array, items: { $ref: '#/components/schemas/LoadBalancer' } } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'502': { description: DigitalOcean upstream error }
'503': { description: DigitalOcean not configured }
post:
tags: [Load Balancers]
summary: Create a load balancer
operationId: createLoadBalancer
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/LoadBalancerCreate' }
responses:
'201': { description: Load balancer created, content: { application/json: { schema: { $ref: '#/components/schemas/LoadBalancer' } } } }
'400': { description: Invalid name or missing region }
'401': { $ref: '#/components/responses/Unauthorized' }
'409': { description: A load balancer with that name already exists }
'502': { description: DigitalOcean upstream error }
'503': { description: DigitalOcean not configured }
/v1/load-balancers/{id}:
parameters:
- { name: id, in: path, required: true, schema: { type: string }, description: DO load balancer id }
get:
tags: [Load Balancers]
summary: Get one load balancer (owned)
operationId: getLoadBalancer
responses:
'200': { description: Load balancer, content: { application/json: { schema: { $ref: '#/components/schemas/LoadBalancer' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Load balancer not found }
'503': { description: DigitalOcean not configured }
delete:
tags: [Load Balancers]
summary: Delete one load balancer (owned)
operationId: deleteLoadBalancer
responses:
'204': { description: Deleted }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Load balancer not found }
'503': { description: DigitalOcean not configured }
components:
securitySchemes:
@@ -157,28 +108,3 @@ components:
region: { type: string }
subnets: { type: array, items: { type: string } }
status: { type: string, example: active }
ForwardingRule:
type: object
properties:
entry_protocol: { type: string }
entry_port: { type: integer }
target_protocol: { type: string }
target_port: { type: integer }
LoadBalancerCreate:
type: object
required: [name, region]
properties:
name: { type: string, description: 'Friendly name; must match ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$' }
region: { type: string, description: DO region slug }
type: { type: string, description: 'empty → DO default (REGIONAL)' }
size: { type: string, description: DO size slug }
forwarding_rules: { type: array, items: { $ref: '#/components/schemas/ForwardingRule' }, description: 'empty → default http 80→80' }
LoadBalancer:
type: object
properties:
id: { type: string }
name: { type: string }
type: { type: string }
targets: { type: integer, description: Count of attached backend droplets }
ip: { type: string }
status: { type: string }
+281
View File
@@ -0,0 +1,281 @@
openapi: 3.1.0
info:
title: Hanzo DocDB API
summary: Managed document database (MongoDB wire) — a dedicated per-org instance.
description: |
Provisioning turns "create a document database" into a real logical resource inside an
already-live shared backend, or launches the org's OWN isolated instance for
the dedicated kinds (HIP-0106). Served by the `clients/provisioning`
subsystem of the unified cloud binary and reached through the gateway at
`api.hanzo.ai`; the kind string is the Hanzo product name, never the upstream
OSS backend name.
Tenancy: every request is scoped to the gateway-minted org (`X-Org-Id` /
validated principal). A request without a validated principal, or with an
empty org and no admin claim, is refused 403. The physical backend resource
is namespaced by a fixed-width org hash so two tenants can never fold onto
one resource.
Secrets: generated per-resource passwords are sealed in Hanzo KMS and only a
`secret_ref` is persisted. When KMS is not configured the service degrades
safely — it returns the password ONCE in the create response and stores
nothing in plaintext.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
version: 8.0.0
contact:
name: Hanzo AI
url: https://hanzo.ai
email: [email protected]
license:
name: BSD 3-Clause License
identifier: BSD-3-Clause
servers:
- url: https://api.hanzo.ai
description: Hanzo Gateway Production
tags:
- name: docdb
security:
- BearerAuth: []
paths:
/v1/docdb:
post:
tags: [docdb]
operationId: createDocdb
summary: Provision a document-database resource (dedicated per-org instance)
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [docdb]
operationId: listDocdb
summary: List document-database resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/docdb/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [docdb]
operationId: getDocdb
summary: Get one document-database resource (reconciles live instance status)
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [docdb]
operationId: deleteDocdb
summary: Deprovision a document-database instance
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
components:
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
required: [status, msg]
properties:
status:
type: string
enum: [error]
msg:
type: string
description: Human-readable error message.
code:
type: integer
requestId:
type: string
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Bearer JWT issued by Hanzo IAM (https://hanzo.id).
+23
View File
@@ -44,6 +44,7 @@ tags:
description: Environment secrets for functions
- name: Domains
description: Custom domain management
- name: Edge
security:
- BearerAuth: []
components:
@@ -60,6 +61,14 @@ components:
description: 'Bearer JWT issued by Hanzo IAM (https://hanzo.id). Discovery: https://hanzo.id/.well-known/openid-configuration.
Obtain via /v1/iam/auth/login, /oauth/token, or the API key UI on hanzo.id.'
schemas:
EdgeNodeView:
type: object
description: One row per ZT edge-router.
properties:
id: { type: string }
name: { type: string }
region: { type: string, description: From a "region-<slug>" role attribute; omitted when none }
status: { type: string, enum: [online, disabled, offline] }
Error:
type: object
properties:
@@ -291,6 +300,11 @@ components:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotConfigured:
description: Networking is not configured on this deployment (ZT credential absent — fail-closed)
content:
application/json:
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
Unauthorized:
description: Unauthorized
content:
@@ -677,6 +691,15 @@ paths:
$ref: '#/components/schemas/FunctionMetrics'
'404':
$ref: '#/components/responses/NotFound'
/v1/edge/nodes:
get:
tags: [Edge]
summary: List the org's ZT edge-routers
operationId: listEdgeNodes
responses:
'200': { description: Edge nodes, content: { application/json: { schema: { type: object, properties: { nodes: { type: array, items: { $ref: '#/components/schemas/EdgeNodeView' } } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'503': { $ref: '#/components/responses/NotConfigured' }
/v1/edge/secrets:
get:
tags:
+5 -5
View File
@@ -57,12 +57,12 @@ flows:
chat:
summary: One completion.
operations: [ai_createChatCompletion]
operations: [gateway_createChatCompletion]
notes: >-
OpenAI-compatible request and response. Non-streaming on purpose —
streaming is a different transport (SSE) that the generated clients return
as an opaque body, so demonstrating it here would teach the wrong shape.
Print choices[0].message.content.
POST /v1/chat/completions, the gateway's own inference route. Non-streaming
on purpose — streaming is a different transport (SSE) that the generated
clients return as an opaque body, so demonstrating it here would teach the
wrong shape. Print choices[0].message.content.
money:
summary: Balance, then the usage that moved it.
+268 -797
View File
File diff suppressed because it is too large Load Diff
+2604 -2601
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -29,6 +29,11 @@ test:
# flows.yaml still exists in hanzo.yaml, so deleting a route breaks one gate
# here instead of six SDKs' examples quietly and separately.
#
# test_placement.py enforces the law capabilities.yaml states and nothing
# checked: one product, one owning spec, one /v1/<name>. It went in the day
# four products (kv, s3, search, vector) were each described by two specs at
# once and 103 authored paths sat outside their owner's prefix.
#
# uv rather than a bare python3 — the arc runner image promises no
# interpreter, let alone one with PyYAML.
run: |
@@ -38,4 +43,4 @@ test:
UV="$HOME/.local/bin/uv"
fi
"$UV" run --no-project --python 3.12 --with pyyaml --with pytest \
python -m pytest test_audit.py test_skills.py test_flows.py -q
python -m pytest test_audit.py test_skills.py test_flows.py test_placement.py -q
+206
View File
@@ -54,9 +54,28 @@ tags:
description: Managed KV cluster lifecycle
- name: Health
description: Service health
- name: kv
security:
- BearerAuth: []
components:
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
securitySchemes:
bearerAuth:
type: http
@@ -70,6 +89,51 @@ components:
description: 'Bearer JWT issued by Hanzo IAM (https://hanzo.id). Discovery: https://hanzo.id/.well-known/openid-configuration.
Obtain via /v1/iam/auth/login, /oauth/token, or the API key UI on hanzo.id.'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
properties:
@@ -77,6 +141,29 @@ components:
type: string
code:
type: integer
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
KeyValue:
type: object
properties:
@@ -99,6 +186,28 @@ components:
size:
type: integer
description: Size in bytes
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
Namespace:
type: object
properties:
@@ -247,12 +356,67 @@ components:
type: string
format: date-time
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized
content:
@@ -272,6 +436,28 @@ components:
schema:
$ref: '#/components/schemas/Error'
paths:
/v1/kv:
post:
tags: [kv]
operationId: createKv
summary: Provision a key-value resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [kv]
operationId: listKv
summary: List key-value resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/kv/clusters:
get:
tags:
@@ -1243,3 +1429,23 @@ paths:
$ref: '#/components/schemas/StreamEntry'
groups:
type: integer
/v1/kv/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [kv]
operationId: getKv
summary: Get one key-value resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [kv]
operationId: deleteKv
summary: Deprovision a key-value resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
-511
View File
@@ -1,511 +0,0 @@
openapi: 3.1.0
info:
title: Hanzo Plugin API
summary: Runtime plugin loader and introspection for the unified cloud binary (hanzoai/cloud).
description: |
The plugin subsystem of the unified Hanzo cloud binary. `cloud` is a thin
host: its native Go subsystems are compiled in, but everything else mounts at
RUNTIME from a JSON manifest (`CLOUD_PLUGINS`) — no cloud rebuild to add or
update a service.
Two plugin kinds, both reduced to "produce an `http.Handler`, then
`app.Mount(prefix, h)`":
- `wasm` — a polyglot service (Rust/WASM, or Python/TS via goa) loaded
in-process through `github.com/hanzoai/goa`.
- `proxy` — a standalone server reached over a pluggable transport
(`http` default, or `zap`).
Because plugins mount at arbitrary manifest-defined prefixes, their routes are
NOT part of this fixed contract. The static HTTP surface this subsystem
registers is the introspection endpoint `GET /v1/plugins`, which lists what is
currently mounted, plus the operator surface under `/v1/admin/plugins` that
reads each host's running processes and swaps, stops and starts them. If
`CLOUD_PLUGINS` is unset, zero plugins mount and the list is empty.
Introspection and operation answer different questions. `GET /v1/plugins`
reports the MANIFEST view — name, kind, prefix — for the host that answered.
`/v1/admin/plugins` reports the PROCESS view — pid, uptime, reload and restart
counts, measured CPU/RSS/thread/fd cost — per host across the fleet, plus the
drift roll-up that says whether a rollout finished.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
version: 8.0.0
contact:
name: Hanzo AI
url: https://hanzo.ai
email: [email protected]
license:
name: Proprietary
url: https://hanzo.ai/terms
servers:
- url: https://api.hanzo.ai
description: Hanzo Gateway Production
security:
- BearerAuth: []
tags:
- name: Plugins
description: Runtime plugin introspection.
- name: Admin
description: Fleet plugin operation — list, enable, disable, reload.
paths:
/v1/plugins:
get:
tags:
- Plugins
summary: List mounted plugins
description: |
Returns every plugin currently mounted from the runtime manifest, as a
flat list of `{name, kind, prefix}`. This is introspection only — it does
not expose the plugins' own routes, which live at each plugin's `prefix`.
The list is empty when no manifest is configured (`CLOUD_PLUGINS` unset).
operationId: listPlugins
responses:
'200':
description: The set of mounted plugins.
content:
application/json:
schema:
$ref: '#/components/schemas/PluginList'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins:
get:
tags:
- Admin
summary: List what each host is running
description: |
list reports what each host is actually running: every loaded plugin with its
version, pid, uptime, reload and restart counts, and its measured CPU, RSS,
thread and fd cost — read from the kernel, which is only answerable at all
because a plugin is a process.
Reading this from deployment config would answer what was INTENDED. Only the
process knows what is TRUE, and during a rolling upgrade the two disagree on
purpose.
operationId: adminPlugins
parameters:
- name: scope
in: query
required: false
description: |
Scope "host" answers for THIS host only. Default "fleet" fans out to every
live peer. A peer answers a host-scoped read, which is what stops the
fan-out recursing.
schema:
type: string
responses:
'200':
description: Per-host plugin inventory plus the fleet drift roll-up.
content:
application/json:
schema:
$ref: '#/components/schemas/ListOut'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins/{name}/enable:
post:
tags:
- Admin
summary: Enable a stopped or disabled plugin
description: |
enable brings a stopped or disabled plugin back on the artifact it already
has: the zero Plugin names no new artifact, so Reload reuses the loaded spec
and clears the disabled flag. Named for what an operator means by it.
operationId: adminEnablePlugin
parameters:
- $ref: '#/components/parameters/PluginName'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NameIn'
responses:
'200':
description: Per-host result of the enable.
content:
application/json:
schema:
$ref: '#/components/schemas/ActionOut'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins/{name}/disable:
post:
tags:
- Admin
summary: Disable a plugin — its routes answer 503, not 404
description: |
disable stops the plugin. Its routes STAY REGISTERED and answer 503 — not 404.
That is zip's choice and this keeps it. Removing the routes would mutate the
route table, and re-adding them on enable would grow it without bound across
repeated cycles, which is the invariant that makes reloads flat in memory. It
is also the better answer: 404 says "no such API" and a client may cache it
and stop retrying, while 503 says "this API exists and is down right now",
which is true and retryable. Which of the two 503s this is — deliberate stop
or crash — is what the status's disabled flag reports.
operationId: adminDisablePlugin
parameters:
- $ref: '#/components/parameters/PluginName'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NameIn'
responses:
'200':
description: Per-host result of the disable.
content:
application/json:
schema:
$ref: '#/components/schemas/ActionOut'
'401':
$ref: '#/components/responses/Unauthorized'
/v1/admin/plugins/{name}/reload:
post:
tags:
- Admin
summary: Reload a plugin onto another build
description: |
reload swaps a plugin for another build without dropping a request. The
replacement is started and proven to be LISTENING before any traffic moves to
it, so a bad build leaves the old one serving and returns an error rather
than a hole; the old process then drains before it is killed.
With a version or url+sum it pins; naming a digest this host has run before is
the rollback, and costs no network because the digest IS the cache key. With
neither it restarts what is already loaded.
Fleet scope applies it to one host at a time and STOPS at the first failure,
so a build that cannot come up reaches exactly one host.
operationId: adminReloadPlugin
parameters:
- name: name
in: path
required: true
description: Name is the app, from the path. It must be one the manifest declares.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReloadIn'
responses:
'200':
description: Per-host result of the reload, with the digest now running.
content:
application/json:
schema:
$ref: '#/components/schemas/ActionOut'
'401':
$ref: '#/components/responses/Unauthorized'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT access token issued by Hanzo IAM (https://hanzo.id). The same token
authenticates every Hanzo service.
parameters:
PluginName:
name: name
in: path
required: true
description: Name is the app, from the path.
schema:
type: string
responses:
Unauthorized:
description: Missing or invalid bearer token.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
PluginList:
type: object
description: The set of plugins mounted from the runtime manifest.
required:
- plugins
properties:
plugins:
type: array
items:
$ref: '#/components/schemas/PluginInfo'
PluginInfo:
type: object
description: |
Introspection view of one mounted plugin. Derived from the manifest
`Plugin` entry; only the three public identity fields are surfaced.
properties:
name:
type: string
description: Plugin name from the manifest entry.
kind:
type: string
description: Plugin kind selecting how it was built and mounted.
enum:
- wasm
- proxy
prefix:
type: string
description: Mount point at which the plugin's routes are served.
example: /v1/pricing
ListOut:
type: object
description: The fleet board — one entry per host, plus the drift roll-up.
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/Host'
drift:
type: array
items:
$ref: '#/components/schemas/Drift'
data2:
type: integer
Host:
type: object
description: |
One host's own account of what it is running. Reported per host rather than
merged, because during a rollout the hosts disagree BY DESIGN and a merged
view hides exactly the state an operator is watching for.
properties:
host:
type: string
description: |
Host is the pod's stable id, and Addr where it was reached. Self is true
for the host that answered the request.
addr:
type: string
self:
type: boolean
error:
type: string
description: |
Err is set when a peer could not be reached. Its plugins are then
unknown, which is NOT the same as none, so the list stays empty and the
drift below refuses to conclude anything from it.
plugins:
type: array
items:
$ref: '#/components/schemas/PluginStatus'
PluginStatus:
type: object
description: |
One plugin's process view on one host — what `GET /v1/plugins` cannot
answer, because that reports the manifest and this reports the kernel.
properties:
name:
type: string
prefix:
type: string
description: |
Prefix is the FIRST subtree this plugin answers — the one a log line
names it by. Prefixes is every subtree, and a plugin may own several.
Reporting only the first would understate the blast radius of taking
this plugin down, which is the question a fleet view exists to answer.
prefixes:
type: array
items:
type: string
source:
type: string
description: |
Source is where the binary came from: "embedded", "path", "url", or
"remote" for an instance this host did not start.
enum:
- embedded
- path
- url
- remote
version:
type: string
description: |
Version is the artifact's SHA-256 when it was installed from a URL —
the only version identifier that cannot drift from the bits actually
running, since it IS the bits. Empty for the other sources.
addr:
type: string
description: Addr is the socket or address serving it.
pid:
type: integer
description: PID is the child process, or 0 when this host did not start it.
running:
type: boolean
description: |
Running is false after Unload, or after a child exited and no Reload has
replaced it. Its routes stay registered and answer 503, so a false here
is the difference between "not deployed" and "deployed but down".
disabled:
type: boolean
description: |
Disabled is true when Unload stopped it deliberately, as opposed to it
having crashed. Both answer 503, so without this an operator cannot tell
a maintenance window from an outage — and would page for the former.
since:
type: string
format: date-time
description: |
Since is when the CURRENT instance started — it resets on Reload, so it
reports the age of what is running, not of the mount.
reloads:
type: integer
description: |
Reloads counts successful swaps since Load. A climbing number on one
host and not its peers is the signal that a rollout is uneven.
restarts:
type: integer
description: |
Restarts counts times the supervisor brought this plugin back after it
died on its own. Distinct from Reloads, which are deliberate: a nonzero
Restarts is a plugin crashing, and a climbing one is a crash loop.
usage:
$ref: '#/components/schemas/PluginUsage'
PluginUsage:
type: object
description: |
A plugin's resource cost, measured rather than self-reported.
This exists because a plugin is a process. A linked-in subsystem cannot be
measured this way at any price — one process has one RSS and one CPU clock,
and nothing inside it can be attributed to a subsystem. Running a service as
its own process makes "which app is eating the memory" a question with an
exact answer instead of an argument.
Zero means not measured (no /proc, or nothing running), not measured-as-zero.
properties:
cpuNs:
type: integer
format: int64
description: |
CPU is total user+system time this instance has consumed since it
started, in nanoseconds. It only ever climbs, so a rate is the
interesting derivative.
rssBytes:
type: integer
format: int64
description: |
RSS is resident memory in bytes — the honest number for "what does this
plugin cost", as opposed to virtual size.
threads:
type: integer
description: |
Threads and FDs are the two limits a busy service hits first, and both
are leaks when they climb without bound.
fds:
type: integer
Drift:
type: object
description: |
One plugin's agreement across the fleet. Versions holds every distinct
digest seen running; more than one means a rollout is incomplete or stuck,
which is the single question this whole view exists to answer.
properties:
name:
type: string
versions:
type: array
items:
type: string
running:
type: integer
down:
type: integer
disabled:
type: integer
drifted:
type: boolean
ActionOut:
type: object
description: |
The envelope every mutation answers with. Data is per host and in the order
applied, so a halted rollout reads as the prefix that succeeded followed by
the one that did not.
properties:
status:
type: string
msg:
type: string
data:
type: array
items:
$ref: '#/components/schemas/Result'
Result:
type: object
description: One host's outcome for one action.
properties:
host:
type: string
ok:
type: boolean
version:
type: string
msg:
type: string
NameIn:
type: object
description: |
Addresses one plugin by name, for the operations that take nothing else.
properties:
name:
type: string
description: Name is the app, from the path.
scope:
type: string
description: |
Scope "host" applies here only; default "fleet" applies everywhere.
ReloadIn:
type: object
description: |
Names an artifact to run. Exactly one of Version or URL+Sum, or neither to
restart the artifact already loaded — which is how a wedged plugin is
bounced without changing what it runs.
properties:
name:
type: string
description: |
Name is the app, from the path. It must be one the manifest declares.
version:
type: string
description: |
Version is a release tag, resolved to a URL and digest through the
origin's binaries.json index — the same index CI publishes, so there is
no second table mapping versions to digests.
url:
type: string
description: |
URL is the artifact directly, for an origin with no index. Sum is its hex
SHA-256 and is REQUIRED with it: zip refuses an unverified download, and
so does this.
sum:
type: string
scope:
type: string
description: |
Scope "host" applies here only. Default "fleet" rolls it out one host at
a time, halting on the first host that fails to come up.
Error:
type: object
description: Standard error envelope.
properties:
error:
type: object
properties:
message:
type: string
type:
type: string
code:
type: string
+128
View File
@@ -0,0 +1,128 @@
openapi: 3.1.0
info:
title: Hanzo Plugin API
summary: Runtime plugin loader and introspection for the unified cloud binary (hanzoai/cloud).
description: |
The plugin subsystem of the unified Hanzo cloud binary. `cloud` is a thin
host: its native Go subsystems are compiled in, but everything else mounts at
RUNTIME from a JSON manifest (`CLOUD_PLUGINS`) — no cloud rebuild to add or
update a service.
Two plugin kinds, both reduced to "produce an `http.Handler`, then
`app.Mount(prefix, h)`":
- `wasm` — a polyglot service (Rust/WASM, or Python/TS via goa) loaded
in-process through `github.com/hanzoai/goa`.
- `proxy` — a standalone server reached over a pluggable transport
(`http` default, or `zap`).
Because plugins mount at arbitrary manifest-defined prefixes, their routes are
NOT part of this fixed contract. The static HTTP surface this subsystem
registers is the introspection endpoint `GET /v1/plugins`, which lists what is
currently mounted, plus the operator surface under `/v1/admin/plugins` that
reads each host's running processes and swaps, stops and starts them. If
`CLOUD_PLUGINS` is unset, zero plugins mount and the list is empty.
Introspection and operation answer different questions. `GET /v1/plugins`
reports the MANIFEST view — name, kind, prefix — for the host that answered.
`/v1/admin/plugins` reports the PROCESS view — pid, uptime, reload and restart
counts, measured CPU/RSS/thread/fd cost — per host across the fleet, plus the
drift roll-up that says whether a rollout finished.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
version: 8.0.0
contact:
name: Hanzo AI
url: https://hanzo.ai
email: [email protected]
license:
name: Proprietary
url: https://hanzo.ai/terms
servers:
- url: https://api.hanzo.ai
description: Hanzo Gateway Production
security:
- BearerAuth: []
tags:
- name: Plugins
description: Runtime plugin introspection.
paths:
/v1/plugins:
get:
tags:
- Plugins
summary: List mounted plugins
description: |
Returns every plugin currently mounted from the runtime manifest, as a
flat list of `{name, kind, prefix}`. This is introspection only — it does
not expose the plugins' own routes, which live at each plugin's `prefix`.
The list is empty when no manifest is configured (`CLOUD_PLUGINS` unset).
operationId: listPlugins
responses:
'200':
description: The set of mounted plugins.
content:
application/json:
schema:
$ref: '#/components/schemas/PluginList'
'401':
$ref: '#/components/responses/Unauthorized'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT access token issued by Hanzo IAM (https://hanzo.id). The same token
authenticates every Hanzo service.
responses:
Unauthorized:
description: Missing or invalid bearer token.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
PluginList:
type: object
description: The set of plugins mounted from the runtime manifest.
required:
- plugins
properties:
plugins:
type: array
items:
$ref: '#/components/schemas/PluginInfo'
PluginInfo:
type: object
description: |
Introspection view of one mounted plugin. Derived from the manifest
`Plugin` entry; only the three public identity fields are surfaced.
properties:
name:
type: string
description: Plugin name from the manifest entry.
kind:
type: string
description: Plugin kind selecting how it was built and mounted.
enum:
- wasm
- proxy
prefix:
type: string
description: Mount point at which the plugin's routes are served.
example: /v1/pricing
Error:
type: object
description: Standard error envelope.
properties:
error:
type: object
properties:
message:
type: string
type:
type: string
code:
type: string
-580
View File
@@ -1,580 +0,0 @@
openapi: 3.1.0
info:
title: Hanzo Provisioning API
summary: Managed-resource control plane (hanzoai/cloud clients/provisioning, HIP-0106).
description: |
The Hanzo Cloud provisioning control plane. It turns "create a database" into
a real logical resource inside an already-live, shared product backend — or,
for the dedicated kinds, launches the org's OWN isolated instance via the
cluster operator (HIP-0106). Served by the `clients/provisioning` subsystem of
the unified cloud binary, reached through the gateway at `api.hanzo.ai`.
NAMESPACE NOTE — these routes are keyed by RESOURCE KIND at top-level `/v1/`,
NOT under `/v1/provisioning/`. `Mount` registers one CRUD quartet per kind:
`/v1/sql`, `/v1/vector`, `/v1/datastore`, `/v1/kv`, `/v1/search`, `/v1/s3`,
`/v1/docdb`. The kind string is the Hanzo product name, never the upstream OSS
backend name (so `sql`/`s3`, not `postgres`/`minio`). This spec documents the
surface exactly as the Go code registers it.
Seven kinds, one Provisioner each:
- sql -> Postgres CREATE DATABASE + ROLE
- vector -> Qdrant collection
- datastore -> Datastore dedicated per-org instance
- kv -> Redis ACL SETUSER (keyspace scope)
- search -> Meilisearch index
- s3 -> S3/MinIO bucket
- docdb -> FerretDB dedicated per-org instance (MongoDB wire)
Tenancy: every request is scoped to the gateway-minted org (`X-Org-Id` /
validated principal). A request without a validated principal, or with an
empty org and no admin claim, is refused 403. The physical backend resource is
namespaced by a fixed-width org hash so two tenants can never fold onto one
resource.
Secrets: generated per-resource passwords are sealed in Hanzo KMS and only a
`secret_ref` is persisted. When KMS is not configured the service degrades
safely — it returns the password ONCE in the create response and stores
nothing in plaintext.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
version: 8.0.0
contact:
name: Hanzo AI
url: https://hanzo.ai
email: [email protected]
license:
name: Proprietary
url: https://hanzo.ai/terms
servers:
- url: https://api.hanzo.ai
description: Hanzo Gateway Production
security:
- BearerAuth: []
tags:
- name: sql
description: Managed SQL (Postgres) database resources.
- name: vector
description: Managed vector (Qdrant) collection resources.
- name: datastore
description: Managed analytics datastore (Hanzo Datastore, ClickHouse-compatible wire) — dedicated per-org instance.
- name: kv
description: Managed key-value (Redis) resources.
- name: search
description: Managed search (Meilisearch) index resources.
- name: s3
description: Managed object storage (S3/MinIO) bucket resources.
- name: docdb
description: Managed document database (FerretDB / MongoDB wire) — dedicated per-org instance.
paths:
/v1/sql:
post:
tags: [sql]
operationId: createSql
summary: Provision a SQL resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [sql]
operationId: listSql
summary: List SQL resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/sql/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [sql]
operationId: getSql
summary: Get one SQL resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [sql]
operationId: deleteSql
summary: Deprovision a SQL resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
/v1/vector:
post:
tags: [vector]
operationId: createVector
summary: Provision a vector resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [vector]
operationId: listVector
summary: List vector resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/vector/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [vector]
operationId: getVector
summary: Get one vector resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [vector]
operationId: deleteVector
summary: Deprovision a vector resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
/v1/datastore:
post:
tags: [datastore]
operationId: createDatastore
summary: Provision a datastore resource (dedicated per-org instance)
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [datastore]
operationId: listDatastore
summary: List datastore resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/datastore/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [datastore]
operationId: getDatastore
summary: Get one datastore resource (reconciles live instance status)
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [datastore]
operationId: deleteDatastore
summary: Deprovision a datastore instance
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
/v1/kv:
post:
tags: [kv]
operationId: createKv
summary: Provision a key-value resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [kv]
operationId: listKv
summary: List key-value resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/kv/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [kv]
operationId: getKv
summary: Get one key-value resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [kv]
operationId: deleteKv
summary: Deprovision a key-value resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
/v1/search:
post:
tags: [search]
operationId: createSearch
summary: Provision a search resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [search]
operationId: listSearch
summary: List search resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/search/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [search]
operationId: getSearch
summary: Get one search resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [search]
operationId: deleteSearch
summary: Deprovision a search resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
/v1/s3:
post:
tags: [s3]
operationId: createS3
summary: Provision an object-storage bucket
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [s3]
operationId: listS3
summary: List object-storage buckets for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/s3/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [s3]
operationId: getS3
summary: Get one object-storage bucket
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [s3]
operationId: deleteS3
summary: Deprovision an object-storage bucket
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
/v1/docdb:
post:
tags: [docdb]
operationId: createDocdb
summary: Provision a document-database resource (dedicated per-org instance)
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [docdb]
operationId: listDocdb
summary: List document-database resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/docdb/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [docdb]
operationId: getDocdb
summary: Get one document-database resource (reconciles live instance status)
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [docdb]
operationId: deleteDocdb
summary: Deprovision a document-database instance
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
JWT access token issued by Hanzo IAM (https://hanzo.id). The same token
authenticates every Hanzo service. The org is resolved from the validated
principal (X-Org-Id); a request without a validated principal is refused.
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
Deleted:
description: Resource deprovisioned. No content.
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
Error:
type: object
required: [status, msg]
properties:
status:
type: string
enum: [error]
msg:
type: string
description: Human-readable error message.
code:
type: integer
requestId:
type: string
-174
View File
@@ -29,8 +29,6 @@ security:
tags:
- name: Referrals
description: Customer-facing referral surface, scoped to the caller's org.
- name: Admin
description: Global-admin referral directory and qualify sweep.
- name: Health
description: Liveness probe.
paths:
@@ -108,68 +106,6 @@ paths:
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/admin/referrals:
get:
operationId: adminListReferrals
tags:
- Admin
summary: List every referral with a fleet summary (global-admin)
description: 'Returns every referral (both orgs exposed) plus a fleet summary. Global-admin
only. Wrapped in the `{ status, msg, data }` admin envelope.
'
security:
- bearerAuth: []
parameters:
- name: limit
in: query
required: false
description: 'Max rows to return. Defaults to 500 when absent/invalid/<=0; capped at 1000.
'
schema:
type: integer
minimum: 1
maximum: 1000
default: 500
responses:
'200':
description: The referral directory and summary, in the admin envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminListEnvelope'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/admin/referrals/sweep:
post:
operationId: adminSweepReferrals
tags:
- Admin
summary: Qualify-check every pending referral (global-admin)
description: 'The periodic qualify path (cron/operator on demand). Qualify-checks every
pending referral (bounded to 500 per sweep) and grants the ones that now
qualify. Global-admin only. Returns counts in the admin envelope.
'
security:
- bearerAuth: []
responses:
'200':
description: Sweep result counts, in the admin envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminSweepEnvelope'
'403':
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/referrals/health:
get:
operationId: referralsHealth
@@ -292,99 +228,6 @@ components:
type: integer
format: int64
description: Unix timestamp.
AdminReferralView:
type: object
description: One row in the global-admin directory (both orgs exposed).
properties:
id:
type: string
referrerOrg:
type: string
refereeOrg:
type: string
code:
type: string
status:
$ref: '#/components/schemas/ReferralStatus'
referrerGrantCents:
type: integer
format: int64
refereeGrantCents:
type: integer
format: int64
referrerTxn:
type: string
description: Ledger transaction id for the referrer bonus (omitted if empty).
refereeTxn:
type: string
description: Ledger transaction id for the referee bonus (omitted if empty).
createdAt:
type: integer
format: int64
qualifiedAt:
type: integer
format: int64
creditedAt:
type: integer
format: int64
AdminSummary:
type: object
description: The fleet tally for the admin directory.
properties:
total:
type: integer
signedUp:
type: integer
qualified:
type: integer
credited:
type: integer
grantedCents:
type: integer
format: int64
description: Total promo credit granted across both sides of all referrals.
AdminListData:
type: object
properties:
referrals:
type: array
items:
$ref: '#/components/schemas/AdminReferralView'
summary:
$ref: '#/components/schemas/AdminSummary'
AdminListEnvelope:
type: object
description: The `{ status, msg, data }` admin envelope.
properties:
status:
type: string
enum:
- ok
msg:
type: string
data:
$ref: '#/components/schemas/AdminListData'
AdminSweepData:
type: object
properties:
swept:
type: integer
description: Number of pending referrals examined this sweep.
credited:
type: integer
description: Number newly advanced to credited this sweep.
AdminSweepEnvelope:
type: object
description: The `{ status, msg, data }` admin envelope.
properties:
status:
type: string
enum:
- ok
msg:
type: string
data:
$ref: '#/components/schemas/AdminSweepData'
Error:
type: object
required:
@@ -424,23 +267,6 @@ components:
status: error
msg: 'Invalid parameter: email format is incorrect'
code: 400
Unauthorized:
description: Unauthorized - Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
missingToken:
value:
status: error
msg: Authentication required
code: 401
invalidToken:
value:
status: error
msg: Invalid or expired token
code: 401
Forbidden:
description: Forbidden - Insufficient permissions
content:
+219
View File
@@ -54,9 +54,96 @@ tags:
description: Administrative operations
- name: Health
description: Service health
- name: s3
security:
- BearerAuth: []
components:
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
s3Auth:
type: apiKey
@@ -79,6 +166,51 @@ components:
description: 'Bearer JWT issued by Hanzo IAM (https://hanzo.id). Discovery: https://hanzo.id/.well-known/openid-configuration.
Obtain via /v1/iam/auth/login, /oauth/token, or the API key UI on hanzo.id.'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
properties:
@@ -105,6 +237,51 @@ components:
objects:
type: integer
description: Number of objects
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
ObjectInfo:
type: object
properties:
@@ -263,6 +440,28 @@ components:
buckets:
type: integer
paths:
/v1/s3:
post:
tags: [s3]
operationId: createS3
summary: Provision an object-storage bucket
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [s3]
operationId: listS3
summary: List object-storage buckets for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/s3/:
get:
operationId: listBuckets
@@ -842,3 +1041,23 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ServiceAccount'
/v1/s3/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [s3]
operationId: getS3
summary: Get one object-storage bucket
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [s3]
operationId: deleteS3
summary: Deprovision an object-storage bucket
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
+225
View File
@@ -91,7 +91,30 @@ tags:
description: Export index data
- name: Webhooks
description: Event webhook management
- name: search
paths:
/v1/search:
post:
tags: [search]
operationId: createSearch
summary: Provision a search resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [search]
operationId: listSearch
summary: List search resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/search/version:
get:
operationId: getVersion
@@ -1291,7 +1314,34 @@ paths:
application/json:
schema:
type: object
/v1/search/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [search]
operationId: getSearch
summary: Get one search resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [search]
operationId: deleteSearch
summary: Deprovision a search resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
components:
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
securitySchemes:
BearerAuth:
type: http
@@ -1308,7 +1358,78 @@ components:
type: string
pattern: ^[a-zA-Z0-9_-]+$
description: Unique index identifier
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Missing or invalid authorization header
content:
@@ -1322,6 +1443,88 @@ components:
schema:
$ref: '#/components/schemas/ResponseError'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
required: [status, msg]
properties:
status:
type: string
enum: [error]
msg:
type: string
description: Human-readable error message.
code:
type: integer
requestId:
type: string
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
HealthResponse:
type: object
properties:
@@ -1329,6 +1532,28 @@ components:
type: string
enum:
- available
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
VersionResponse:
type: object
properties:
+281
View File
@@ -0,0 +1,281 @@
openapi: 3.1.0
info:
title: Hanzo SQL API
summary: Managed SQL databases (Postgres wire) — one logical database plus role per resource.
description: |
Provisioning turns "create a database" into a real logical resource inside an
already-live shared backend, or launches the org's OWN isolated instance for
the dedicated kinds (HIP-0106). Served by the `clients/provisioning`
subsystem of the unified cloud binary and reached through the gateway at
`api.hanzo.ai`; the kind string is the Hanzo product name, never the upstream
OSS backend name.
Tenancy: every request is scoped to the gateway-minted org (`X-Org-Id` /
validated principal). A request without a validated principal, or with an
empty org and no admin claim, is refused 403. The physical backend resource
is namespaced by a fixed-width org hash so two tenants can never fold onto
one resource.
Secrets: generated per-resource passwords are sealed in Hanzo KMS and only a
`secret_ref` is persisted. When KMS is not configured the service degrades
safely — it returns the password ONCE in the create response and stores
nothing in plaintext.
Authentication: Bearer JWT issued by Hanzo IAM (`https://hanzo.id`). The same
token authenticates every Hanzo service.
version: 8.0.0
contact:
name: Hanzo AI
url: https://hanzo.ai
email: [email protected]
license:
name: BSD 3-Clause License
identifier: BSD-3-Clause
servers:
- url: https://api.hanzo.ai
description: Hanzo Gateway Production
tags:
- name: sql
security:
- BearerAuth: []
paths:
/v1/sql:
post:
tags: [sql]
operationId: createSql
summary: Provision a SQL resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [sql]
operationId: listSql
summary: List SQL resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/sql/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [sql]
operationId: getSql
summary: Get one SQL resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [sql]
operationId: deleteSql
summary: Deprovision a SQL resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
components:
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
required: [status, msg]
properties:
status:
type: string
enum: [error]
msg:
type: string
description: Human-readable error message.
code:
type: integer
requestId:
type: string
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Bearer JWT issued by Hanzo IAM (https://hanzo.id).
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Placement — every authored operation under the app that owns its route.
Runnable standalone (`python3 test_placement.py`, non-zero exit on failure) AND
under pytest, like test_flows.py beside it.
capabilities.yaml states the law this file enforces: "the route IS the identity:
one capability = one name = one /v1/<name> = one <name>/openapi.yaml." Nothing
enforced it, so 103 authored paths had drifted out of their owner's prefix and
four products (`kv`, `s3`, `search`, `vector`) were each described by TWO specs
at once — the namesake and a `provisioning` bucket that squatted on seven other
products' roots. A reader asking "who owns /v1/vector" got two answers, and the
CLI, the SDKs and the agent-skills catalog each picked one.
Two assertions:
1. ONE OWNER PER PRODUCT. No two authored specs may claim paths under the same
`/v1/<product>` prefix. This is absolute — there is no exception list,
because a second claimant is never a fact about a server, it is always two
descriptions of one thing.
2. THE ROUTE IS THE IDENTITY. `<svc>/openapi.yaml` claims `/v1/<svc>` paths.
Every exception is declared BELOW with the reason it is true, so a new one
fails here instead of arriving silently.
`cloud/openapi.yaml` is exempt from both: it is not authored here. It is
hanzoai/cloud's own woven document, regenerated from that binary's router and
drift-gated there, and merge.py merges it LAST so it wins every route an
authored spec also claims. It describes whatever cloud serves, wherever cloud
serves it, and this repo does not get a vote.
"""
import glob
import os
import sys
import yaml
ROOT = os.path.dirname(os.path.abspath(__file__))
TRUTH = "cloud"
# The declared exceptions to rule 2, each measured against api.hanzo.ai.
#
# Every path here is SERVED at the address given — none is a placeholder. What
# they have in common is a BINARY that answers at a top-level noun which is not
# its service's name, so moving the operation into a same-named spec would
# either invent a route nobody serves or split one service across many specs.
# The fix for each is a route move in the serving repo; until then this table is
# the honest statement of where the surface actually lives.
OFF_PREFIX = {
# IAM additionally exposes the OIDC discovery + OAuth endpoints, which the
# standards place at fixed unprefixed addresses. Documented in LLM.md.
"iam": ["/.well-known/jwks", "/.well-known/openid-configuration",
"/.well-known/webfinger", "/.well-known/{application}/jwks",
"/.well-known/{application}/openid-configuration",
"/.well-known/{application}/webfinger", "/oauth/callback",
"/oauth/introspect", "/oauth/token", "/oauth/token/refresh",
"/oauth/userinfo"],
# The inference edge answers at the top of /v1 because a model call names a
# model and nothing else — there is no product noun between the caller and
# the model. The gateway is the binary that serves them.
"gateway": ["/healthz", "/v1/audio/speech", "/v1/chat/completions",
"/v1/completions", "/v1/embeddings", "/v1/images/generations",
"/v1/messages", "/v1/models", "/v1/models/{model}", "/v1/rerank"],
# Liveness/scrape endpoints, outside /v1 by convention.
"search": ["/metrics"],
# The compute plane answers at the resource nouns, not under /v1/visor.
"visor": ["/v1/bots", "/v1/clusters", "/v1/clusters/{clusterId}/pools",
"/v1/clusters/{clusterId}/pools/{poolId}",
"/v1/clusters/{clusterId}/pools/{poolId}/scale",
"/v1/compute/regions", "/v1/compute/sizes", "/v1/gpus",
"/v1/gpus/alerts", "/v1/machines", "/v1/machines/{id}"],
# The zero-trust fabric answers at the fabric nouns, not under /v1/zt.
"zt": ["/v1/mesh/services", "/v1/networks", "/v1/networks/{id}"],
# Base serves its record CRUD at /v1/collections/*; /v1/base is its health.
"base": ["/v1/collections/{collection}/records",
"/v1/collections/{collection}/records/{id}"],
# The DigitalOcean provider serves VPCs at the resource noun.
"do": ["/v1/vpcs", "/v1/vpcs/{id}"],
# A session's file transfer is addressed by the verb, not by /v1/exec.
"exec": ["/v1/download/{id}", "/v1/files/{session_id}", "/v1/upload"],
# The graph service serves these two registries at their own nouns.
"graph": ["/v1/indexers", "/v1/oracles"],
# Sites are served at /v1/sites; /v1/projects is the same service's other
# noun. (cloud also serves /v1/platform/sites — that collision is cloud's to
# settle, and cloud's document is the one that wins it.)
"projects": ["/v1/sites", "/v1/sites/deploy"],
# Routing data export/erase, served by the ai binary at /v1/router.
"ai": ["/v1/router/data"],
}
def specs():
out = {}
for f in sorted(glob.glob(os.path.join(ROOT, "*", "openapi.yaml"))):
svc = os.path.basename(os.path.dirname(f))
if svc == TRUTH:
continue
out[svc] = list((yaml.safe_load(open(f)) or {}).get("paths") or {})
return out
def product(path):
seg = path.split("/")
return seg[2] if len(seg) > 2 and seg[1] == "v1" else None
def test_one_owner_per_product():
owners = {}
for svc, paths in specs().items():
for p in paths:
prod = product(p)
if prod:
owners.setdefault(prod, set()).add(svc)
clashes = {k: sorted(v) for k, v in owners.items() if len(v) > 1}
assert not clashes, (
"two authored specs describe one product — one of them must die:\n" +
"\n".join(f" /v1/{k} <- {v}" for k, v in sorted(clashes.items())))
def test_the_route_is_the_identity():
stray = {}
for svc, paths in specs().items():
allowed = set(OFF_PREFIX.get(svc, []))
bad = [p for p in paths
if not (p == f"/v1/{svc}" or p.startswith(f"/v1/{svc}/"))
and p not in allowed]
if bad:
stray[svc] = sorted(bad)
assert not stray, (
"an authored spec claims a route outside its own product. Move the "
"operation into the spec that owns the prefix, or — if a binary really "
"does answer it there — add it to OFF_PREFIX in this file with the "
"reason:\n" +
"\n".join(f" {k}/openapi.yaml claims {v}" for k, v in sorted(stray.items())))
def test_no_stale_exceptions():
"""An exception that no longer names a real path is a comment pretending to
be a rule — it must go when the path does."""
have = specs()
stale = {}
for svc, paths in OFF_PREFIX.items():
if svc not in have:
stale[svc] = ["(no such spec)"]
continue
gone = [p for p in paths if p not in have[svc]]
if gone:
stale[svc] = gone
assert not stale, (
"OFF_PREFIX declares paths that no longer exist — delete the entries:\n" +
"\n".join(f" {k}: {v}" for k, v in sorted(stale.items())))
if __name__ == "__main__":
fails = 0
for name, fn in sorted(globals().items()):
if name.startswith("test_") and callable(fn):
try:
fn()
print(f"ok {name}")
except AssertionError as e:
fails += 1
print(f"FAIL {name}\n{e}")
sys.exit(1 if fails else 0)
+233
View File
@@ -42,9 +42,96 @@ tags:
description: Cluster management
- name: Service
description: Service operations
- name: vector
security:
- BearerAuth: []
components:
parameters:
Name:
name: name
in: path
required: true
description: |
The user-supplied resource name (slug). Lowercased and trimmed server-side;
must match `^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$`.
schema:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
requestBodies:
CreateResource:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRequest'
responses:
BadGateway:
description: The backend provisioner or dedicated-instance operation failed.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
BadRequest:
description: Invalid name or malformed request body.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: A resource with this name (or physical name) already exists.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Created:
description: Resource provisioned. Password (if any) is returned ONCE here.
content:
application/json:
schema:
$ref: '#/components/schemas/CreateResponse'
Deleted:
description: Resource deprovisioned. No content.
Forbidden:
description: No validated principal, or empty org without admin claim.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
PaymentRequired:
description: Org balance gate failed (unfunded org).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Resource:
description: Resource metadata (never a password).
content:
application/json:
schema:
$ref: '#/components/schemas/GetResponse'
ResourceList:
description: The caller's org resources of this kind (never a password).
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ListItem'
ServiceUnavailable:
description: |
Kind is honestly gated (cannot grant a per-tenant-safe credential), or the
cluster orchestrator is unavailable for a dedicated kind, or commerce is
unreachable in the fail-closed posture.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
@@ -101,6 +188,110 @@ components:
$ref: '#/components/schemas/OptimizerConfig'
wal_config:
$ref: '#/components/schemas/WalConfig'
CreateRequest:
type: object
required: [name]
properties:
name:
type: string
pattern: '^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$'
description: Resource name (lowercased/trimmed server-side).
example: analytics
CreateResponse:
type: object
description: |
Returned on 201. `password` and `username` are present only for secretful
kinds (sql, kv, and the dedicated datastore/docdb admin credential). The
password is returned ONCE and is never retrievable again.
required: [id, kind, name, status, host, port, database, connectionString]
properties:
id:
type: string
description: Server-generated resource id.
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
name:
type: string
status:
type: string
description: Resource status (e.g. "ready", or "provisioning" for a dedicated instance still coming up).
example: ready
host:
type: string
description: Customer-facing host (public gateway host, or dedicated instance service).
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
connectionString:
type: string
description: Public, routable DSN (internal admin host is remapped out).
password:
type: string
description: Present only for secretful kinds; returned ONCE.
Error:
type: object
required: [status, msg]
properties:
status:
type: string
enum: [error]
msg:
type: string
description: Human-readable error message.
code:
type: integer
requestId:
type: string
GetResponse:
type: object
required: [id, name, kind, status, host, port, database]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
username:
type: string
description: Present only for secretful kinds.
database:
type: string
ListItem:
type: object
required: [id, name, kind, status, host, port, createdAt]
properties:
id:
type: string
name:
type: string
kind:
type: string
enum: [sql, vector, datastore, kv, search, s3, docdb]
status:
type: string
example: ready
host:
type: string
port:
type: integer
createdAt:
type: integer
format: int64
description: Unix seconds.
PointId:
description: A point identifier — an integer or a UUID string.
oneOf:
@@ -366,6 +557,28 @@ components:
size:
type: integer
paths:
/v1/vector:
post:
tags: [vector]
operationId: createVector
summary: Provision a vector resource
requestBody:
$ref: '#/components/requestBodies/CreateResource'
responses:
'201': { $ref: '#/components/responses/Created' }
'400': { $ref: '#/components/responses/BadRequest' }
'402': { $ref: '#/components/responses/PaymentRequired' }
'403': { $ref: '#/components/responses/Forbidden' }
'409': { $ref: '#/components/responses/Conflict' }
'502': { $ref: '#/components/responses/BadGateway' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [vector]
operationId: listVector
summary: List vector resources for the caller's org
responses:
'200': { $ref: '#/components/responses/ResourceList' }
'403': { $ref: '#/components/responses/Forbidden' }
/v1/vector/collections:
get:
tags:
@@ -816,3 +1029,23 @@ paths:
application/json:
schema:
type: object
/v1/vector/{name}:
parameters:
- $ref: '#/components/parameters/Name'
get:
tags: [vector]
operationId: getVector
summary: Get one vector resource
responses:
'200': { $ref: '#/components/responses/Resource' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
delete:
tags: [vector]
operationId: deleteVector
summary: Deprovision a vector resource
responses:
'204': { $ref: '#/components/responses/Deleted' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'502': { $ref: '#/components/responses/BadGateway' }
-110
View File
@@ -27,8 +27,6 @@ tags:
description: DOKS clusters and node pools
- name: Catalog
description: Global compute regions and sizes
- name: Bindings
description: Agent↔machine bindings
- name: Bots
description: Bots (kind=bot machine + agent binding)
security:
@@ -78,43 +76,7 @@ paths:
'204': { description: Terminated }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/machines/{id}/bind-agent:
post:
tags: [Bindings]
summary: Bind a cloud Agent to a machine
operationId: bindMachineAgent
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BindAgentRequest' }
responses:
'200': { description: Agent binding, content: { application/json: { schema: { $ref: '#/components/schemas/AgentBinding' } } } }
'400': { description: agentName is required }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/machines/{id}/agent-binding:
get:
tags: [Bindings]
summary: Get a machine's agent binding
operationId: getMachineAgentBinding
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200': { description: Agent binding, content: { application/json: { schema: { $ref: '#/components/schemas/AgentBinding' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: No agent binding for machine }
delete:
tags: [Bindings]
summary: Unbind the agent from a machine
operationId: unbindMachineAgent
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'204': { description: Unbound }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/gpus:
get:
@@ -219,64 +181,8 @@ paths:
'200': { description: Bots, content: { application/json: { schema: { type: object, properties: { bots: { type: array, items: { $ref: '#/components/schemas/BotView' } } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/bots/launch:
post:
tags: [Bots]
summary: Launch a bot (machine + agent binding), or dryRun for a quote
operationId: launchBot
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/BotLaunchRequest' }
responses:
'200': { description: Price quote (dryRun), content: { application/json: { schema: { type: object } } } }
'201': { description: Bot launched, content: { application/json: { schema: { $ref: '#/components/schemas/BotView' } } } }
'400': { description: size / name is required }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/bots/{id}:
get:
tags: [Bots]
summary: Get one bot by id
operationId: getBot
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'200': { description: Bot, content: { application/json: { schema: { $ref: '#/components/schemas/BotView' } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { description: Bot not found }
delete:
tags: [Bots]
summary: Terminate a bot (unbind agent + delete machine)
operationId: deleteBot
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
responses:
'204': { description: Terminated }
'401': { $ref: '#/components/responses/Unauthorized' }
/v1/bots/{id}/{action}:
post:
tags: [Bots]
summary: Act on a bot (stop, pause, or message)
description: |
`stop` and `pause` both halt the bot's agent runtime (one honest
capability). `message` runs the bot's bound agent via the agent runner
and returns that run's output verbatim.
operationId: botAction
parameters:
- { name: id, in: path, required: true, schema: { type: string } }
- { name: action, in: path, required: true, schema: { type: string, enum: [stop, pause, message] } }
requestBody:
required: false
content:
application/json:
schema: { type: object, description: 'For message: the agent run body forwarded to /v1/agents/{ref}/run' }
responses:
'200': { description: 'Action result — {id,status} for stop/pause, or the agent run output for message', content: { application/json: { schema: { type: object } } } }
'400': { description: unknown bot action / bot has no bound agent }
'401': { $ref: '#/components/responses/Unauthorized' }
components:
securitySchemes:
@@ -389,19 +295,3 @@ components:
properties:
provider: { type: string }
count: { type: integer, minimum: 0 }
BindAgentRequest:
type: object
required: [agentName]
properties:
agentName: { type: string }
botVersion: { type: string }
BotLaunchRequest:
type: object
properties:
name: { type: string, description: Required for a real launch (not dryRun) }
agent: { type: string, description: The cloud Agent the bot runs (defaults to the bot name) }
size: { type: string, description: Machine size slug (or use instanceType) }
instanceType: { type: string }
region: { type: string }
botVersion: { type: string }
dryRun: { type: boolean }
-19
View File
@@ -24,8 +24,6 @@ tags:
description: The org's ZT overlay network(s)
- name: Mesh
description: ZT edge services (service mesh)
- name: Edge
description: ZT edge-routers (edge nodes)
security:
- bearerAuth: []
paths:
@@ -62,15 +60,6 @@ paths:
'401': { $ref: '#/components/responses/Unauthorized' }
'503': { $ref: '#/components/responses/NotConfigured' }
/v1/edge/nodes:
get:
tags: [Edge]
summary: List the org's ZT edge-routers
operationId: listEdgeNodes
responses:
'200': { description: Edge nodes, content: { application/json: { schema: { type: object, properties: { nodes: { type: array, items: { $ref: '#/components/schemas/EdgeNodeView' } } } } } } }
'401': { $ref: '#/components/responses/Unauthorized' }
'503': { $ref: '#/components/responses/NotConfigured' }
components:
securitySchemes:
@@ -106,11 +95,3 @@ components:
service: { type: string }
mtls: { type: string, enum: [enabled, required] }
status: { type: string }
EdgeNodeView:
type: object
description: One row per ZT edge-router.
properties:
id: { type: string }
name: { type: string }
region: { type: string, description: From a "region-<slug>" role attribute; omitted when none }
status: { type: string, enum: [online, disabled, offline] }