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:
+8
-5
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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).
|
||||
@@ -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 }
|
||||
|
||||
@@ -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).
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+2604
-2601
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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' }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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' }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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' }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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] }
|
||||
|
||||
Reference in New Issue
Block a user