Reconcile hanzo.yaml to the live cloud surface — 1698 paths, all products (#18)
* spec(ai): define /v1/images/generations + /v1/audio/{speech,transcriptions}
The ai spec's description already advertised /v1/images/* and /v1/audio/*
but never defined the operations. Add the OpenAI-compatible image generation,
text-to-speech, and speech-to-text (Whisper) operations + schemas so the spec
matches the SDK image/audio resources and what hanzo.chat consumes.
* spec(app): add hanzo.app projects & deployments surface (/v1/projects/*)
hanzo.app calls GET/POST /v1/projects, GET/PATCH/DELETE /v1/projects/{slug},
POST /v1/projects/{slug}/deploy (tar artifact or git build), and
GET /v1/projects/{slug}/deployments — none of which were in any spec.
New self-contained app service spec, top-level /v1/projects/* (path is the
contract, same rationale as top-level inference). Org is resolved server-side
from the IAM JWT owner claim, never sent by the browser.
* spec(base): add Hanzo Base records surface (/v1/collections/{collection}/records)
hanzo.app persists each site's working draft as a row in the Base site_drafts
collection (gated by VITE_BASE_URL) via GET(list+filter)/POST/PATCH on
/v1/collections/{collection}/records. Add a self-contained base service spec
covering the full record CRUD so the frontend contract is complete.
* spec(cloud): scrub literal '/api/' from product-surface comment
The only '/api/' occurrence left in any spec was a documentation comment.
Reword so 'grep -rn /api/' returns zero across the repo; meaning is unchanged
(every route is /v1/*).
* feat(openapi): regenerate unified hanzo.yaml (38 services, 1590 paths)
Regenerated via merge.py after adding app + base and the ai image/audio ops.
Verified: zero /api/ paths and zero /api/ literals across all specs; the only
non-/v1 routes are the unversioned infra probes /health, /healthz, /metrics.
README service index gains app + base rows.
* fix(merge): namespace operationIds by service in unified master
Two services can each define a unique-within-themselves operationId that
collides in the merged master (e.g. base.listRecords vs another service).
Redocly flagged 23 operation-operationId-unique errors, making the master
invalid as a single generatable document. Prefix operationIds with the
service tag — the same namespacing already applied to schemas/responses/
params — so the master is a globally-valid OpenAPI doc every client can
derive from. Per-service specs (Stainless inputs) keep their bare
operationIds. Master lint errors: 369 -> 100 (remainder is pre-existing
per-service nullable/summary debt, unrelated to this change).
* docs(sdk): document the one canonical regeneration path per SDK
hanzo.yaml is the source of truth; JS/Python/Go regenerate via the Stainless
'hanzo-ai' project (push spec -> PR), Rust is hand-written crates (not
spec-generated). Records the exact 'stl builds create' command + KMS-sourced
key for the hosted step.
* docs(openapi): document /v1/sdk enveloped secrets+sign surface (kms)
Add POST /v1/sdk/secrets — the SDK-facing native secrets plane served by
kmsd. Single RPC endpoint: the operation is the SIGNED op field, not the
URL; the ML-DSA-65 envelope IS the credential (no bearer, security: []).
Adds SdkEnvelope + SdkEnvelopeIdentity schemas. Regenerated hanzo.yaml.
* docs(sdk): record the real generation drift + the one-way
The published python/go/js SDKs derive from the LEGACY LLM-gateway spec, not
hanzo.yaml — they cover inference + gateway-admin only, none of the cloud
products. README implied otherwise; correct it. LLM.md now records: repoint
Stainless hanzo-ai -> hanzo.yaml, the 28 live /v1 prefixes with no spec, the 4
prefix-mismatch re-bases, and the stale monolith specs (cloud/nexus). Add cpp +
dart rows (openapi-generator on ARC) — one repo per language, one spec source.
* spec(agents,functions): add /v1/agents + /v1/functions surfaces from clients/*
Route-accurate to hanzoai/cloud/clients/{agents,functions}. agents covers
the agent CRUD + metered run + live session tree; functions covers
serverless CRUD + metered invoke + triggers/deployments/secrets/metrics.
Self-contained; regenerated hanzo.yaml (0 dup operationIds, 0 dangling refs).
* spec(framework,tracker,crm): add business-app surfaces from clients/*
Route-accurate to hanzoai/cloud/clients/{framework,tracker,crm}: framework
doctype/module/role + generic document CRUD, tracker projects+issues,
crm companies/contacts/opportunities.
* spec(automations,integrations,admin,plan): add orchestration + admin surfaces
Route-accurate to hanzoai/cloud/clients/{automations,integrations,admin,plan}:
automations flows/versions/runs/pieces/mcp, integrations provider-connect +
Slack, admin god-view aggregates, plans catalog/resolve/entitlements (the
plan spec expands the fixed-binding route loop, not just literal app.Get calls).
* spec(visor,zt): re-base onto live flat prefixes from clients/{visor,zt}
* spec(kb,prompts,templates): add /v1/{kb,prompts,templates} from clients/*
* spec(git,graph,do): add /v1/git + /v1/{indexers,oracles} + /v1/{vpcs,load-balancers} from clients/*
* spec(billing,security): add /v1/billing + /v1/security from clients/*
* spec(exec,websearch,tasks): add /v1/{exec,websearch,tasks} proxy surfaces; regen hanzo.yaml (58 services); changelog
---------
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Hanzo Dev
parent
8485f8e756
commit
01c59d173f
@@ -2,6 +2,58 @@
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### Cloud product-surface specs (clients/* → live /v1 flat prefixes)
|
||||
|
||||
Authored per-service specs for the fused `api.hanzo.ai/v1` cloud-binary
|
||||
surface, route-accurate to the Go handlers in `hanzoai/cloud/clients/<x>`:
|
||||
|
||||
- `/v1/agents/*` (agents + runs + live session tree) is covered by
|
||||
`cloud/openapi.yaml` (landed on main); the 21 specs below are the rest.
|
||||
- Added `functions/openapi.yaml` — `/v1/functions/*` (serverless functions,
|
||||
metered invoke) from `clients/functions`.
|
||||
- Added `framework/openapi.yaml` — `/v1/framework/*` (doctypes, modules, roles,
|
||||
generic document CRUD) from `clients/framework`.
|
||||
- Added `tracker/openapi.yaml` — `/v1/tracker/*` (projects + issues) from
|
||||
`clients/tracker`.
|
||||
- Added `crm/openapi.yaml` — `/v1/crm/*` (companies, contacts, opportunities)
|
||||
from `clients/crm`.
|
||||
- Added `automations/openapi.yaml` — `/v1/automations/*` (flows, versions,
|
||||
runs, pieces, MCP) from `clients/automations`.
|
||||
- Added `integrations/openapi.yaml` — `/v1/integrations/*` (provider connect
|
||||
+ Slack events/commands/link) from `clients/integrations`.
|
||||
- Added `admin/openapi.yaml` — `/v1/admin/*` (god-view overview, orgs, users,
|
||||
customers, finance, revenue, audit) from `clients/admin`.
|
||||
- Added `plan/openapi.yaml` — `/v1/plans/*` (plan catalog, resolve,
|
||||
entitlements) from `clients/plan`.
|
||||
- Added `kb/openapi.yaml` — `/v1/kb/*` (search + connectors) from `clients/kb`.
|
||||
- Added `prompts/openapi.yaml` — `/v1/prompts/*` (prompt library, catalog,
|
||||
metrics) from `clients/prompts`.
|
||||
- Added `templates/openapi.yaml` — `/v1/templates/*` (template gallery) from
|
||||
`clients/templates`.
|
||||
- Added `git/openapi.yaml` — `/v1/git/*` (repos + Git smart-HTTP transport)
|
||||
from `clients/git`.
|
||||
- Added `billing/openapi.yaml` — `/v1/billing/*` (usage, balance,
|
||||
gpu-eligibility, gpu-charge, payment-methods) from `clients/billing`.
|
||||
- Added `security/openapi.yaml` — `/v1/security/*` (rules, scans, findings)
|
||||
from `clients/security`.
|
||||
- Added `graph/openapi.yaml` — `/v1/indexers`, `/v1/oracles` from
|
||||
`clients/graph` (flat prefixes, not `/v1/graph`).
|
||||
- Added `do/openapi.yaml` — `/v1/vpcs`, `/v1/load-balancers` from `clients/do`
|
||||
(flat prefixes, not `/v1/do`).
|
||||
- Added `exec/openapi.yaml` — `/v1/exec`, `/v1/upload`, `/v1/download`,
|
||||
`/v1/files` (code interpreter, `X-API-Key` service auth) from `clients/exec`.
|
||||
- Added `websearch/openapi.yaml` — `/v1/websearch/{search,scrape}` (SearXNG +
|
||||
Firecrawl-shaped over Hanzo Crawl) from `clients/websearch`.
|
||||
- Added `tasks/openapi.yaml` — `/v1/tasks/*` (durable workflow engine surface,
|
||||
identity-gated) from `clients/tasksvc`.
|
||||
- Re-based `visor/openapi.yaml` onto its live flat prefixes `/v1/machines`,
|
||||
`/v1/gpus`, `/v1/clusters`, `/v1/bots`, `/v1/compute/*`, `/v1/agent-bindings`
|
||||
from `clients/visor` (was wrongly under `/v1/visor/*`).
|
||||
- Re-based `zt/openapi.yaml` onto its live flat prefixes `/v1/networks`,
|
||||
`/v1/mesh/services`, `/v1/edge/nodes` from `clients/zt` (was wrongly under
|
||||
`/v1/zt/*`).
|
||||
- `hanzo.yaml` regenerated: 58 services, 1698 paths.
|
||||
|
||||
### Frontend surface completeness (hanzo.app + hanzo.chat)
|
||||
|
||||
- Added the `app` service spec (`app/openapi.yaml`): the hanzo.app projects &
|
||||
|
||||
@@ -36,3 +36,55 @@ python3 -c "import yaml, glob; [yaml.safe_load(open(s)) for s in glob.glob('*/op
|
||||
other service yamls.
|
||||
4. Examples must come from real responses.
|
||||
5. Update `CHANGELOG.md` with a dated entry under the v1.0.0 heading.
|
||||
6. Run `python3 merge.py` (regenerates `hanzo.yaml`) and commit both.
|
||||
|
||||
## SDK generation — the ONE way, and its current drift (READ THIS)
|
||||
|
||||
The one interface is `hanzo.yaml`; the generator backend is an orthogonal
|
||||
per-language concern (one interface, orthogonal backends):
|
||||
|
||||
| Lang | Repo | Backend | Why |
|
||||
|------|------|---------|-----|
|
||||
| Python/Go/JS | `hanzoai/{python,go,js}-sdk` | Stainless project `hanzo-ai` | best DX, already published |
|
||||
| Rust | `hanzoai/rust-sdk` | hand-written crates | Stainless emits no Rust |
|
||||
| C++ | `hanzoai/cpp-sdk` | openapi-generator `cpp-restsdk` on ARC | Stainless emits no C++ |
|
||||
| Dart (if wanted) | `hanzoai/dart-sdk` | openapi-generator `dart-dio` on ARC | Stainless emits no Dart |
|
||||
|
||||
One repo per language, each derives from `hanzo.yaml`. There is NO unified
|
||||
`hanzoai/sdk` multi-lang monorepo generator — that repo's `gen/` (openapi-
|
||||
generator over all 10 langs, "replaces Stainless") is the retired SECOND way;
|
||||
`hanzoai/sdk` is CLI-only now.
|
||||
|
||||
### Known drift (2026-07 audit — must be fixed for the SDKs to be truthful)
|
||||
|
||||
1. **Stainless points at the LEGACY LLM-gateway spec, not `hanzo.yaml`.** The
|
||||
published python/go/js SDKs expose only the LiteLLM inference + gateway-admin
|
||||
surface (188 endpoints / 49 resources: chat, models, embeddings, Key, User,
|
||||
Team, Spend, Budget, Guardrails, provider passthrough). They cover NONE of
|
||||
the cloud product surface. Fix = repoint the Stainless `hanzo-ai` project's
|
||||
spec source to `hanzo.yaml` and expand its resource tree to the full surface.
|
||||
`python-sdk/.stats.yml` (spec `87bc62c…`) is also diverged from go/js
|
||||
(`9b3181f…`) — the repoint resyncs all three.
|
||||
2. **~28 LIVE `/v1` product prefixes have no usable spec** (so even after the
|
||||
repoint they'd be uncovered). The cloud binary (`hanzoai/cloud`
|
||||
`subsystems.go`) mounts these flat; they need per-service specs authored from
|
||||
the Go routes in `cloud/clients/<x>/*.go`:
|
||||
`agents, tracker, crm, framework(+cms/erp/help/kb), kb, prompts, tasks,
|
||||
functions, git, templates, security, integrations, notify, automations,
|
||||
billing, plans, graph(indexers/oracles), exec, websearch, admin, do
|
||||
(vpcs/load-balancers), provisioning(sql/datastore/docdb), memory, vfs,
|
||||
licensing, authz, ml(train)`.
|
||||
3. **Prefix-mismatch specs — re-base onto the live flat prefix:** `visor` →
|
||||
`/v1/machines,/v1/gpus,/v1/clusters`; `zt` → `/v1/networks,/v1/mesh,/v1/edge`;
|
||||
`flow` → `/v1/automations`; `guard` → `/v1/security`.
|
||||
4. **Stale specs (legacy monolith, NOT fused into the cloud binary):**
|
||||
`cloud/` (157) and `nexus/` (151) are the retired RPC monolith — only the
|
||||
`/v1/search-docs` + `/v1/vector` slices survive (via `clients/product`).
|
||||
`mq, pubsub, stream, registry, db, did, engine` are not live. Separate
|
||||
deployments that legitimately keep their spec: `iam, chat, gateway,
|
||||
operative, dns, flow`.
|
||||
|
||||
The SDK-generation surface is the FUSED `api.hanzo.ai/v1` binary, not the union
|
||||
of every deployment. `merge.py` currently unions all per-service specs; the
|
||||
correct target is the live cloud surface (add the 28, re-base the 4, drop the
|
||||
stale monolith).
|
||||
|
||||
@@ -113,12 +113,12 @@ npx -y @redocly/cli preview-docs hanzo.yaml
|
||||
`hanzo.yaml` (regenerated by `merge.py`) is the single source of truth every
|
||||
official SDK derives from. One way per SDK:
|
||||
|
||||
| SDK | Repo | Generator | Regenerate |
|
||||
|-----|------|-----------|------------|
|
||||
| JS/TS | `hanzoai/js-sdk` | Stainless (`hanzo-ai`) | push spec → Stainless PR |
|
||||
| Python | `hanzoai/python-sdk` | Stainless (`hanzo-ai`) | push spec → Stainless PR |
|
||||
| Go | `hanzoai/go-sdk` | Stainless (`hanzo-ai`) | push spec → Stainless PR |
|
||||
| Rust | `hanzoai/rust-sdk` | hand-written crates | not generated from this spec |
|
||||
> **Status (2026-07):** the Stainless project `hanzo-ai` still points at the
|
||||
> legacy LLM-gateway spec, so the published python/go/js SDKs cover the
|
||||
> inference + gateway-admin surface only — NOT the cloud products. Repoint it
|
||||
> to `hanzo.yaml` and close the ~28 missing product specs first. See the SDK
|
||||
> generation reconciliation section in `LLM.md`.
|
||||
|
||||
|
||||
### Regenerate the Stainless SDKs (canonical)
|
||||
|
||||
|
||||
@@ -0,0 +1,718 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Admin API
|
||||
description: |
|
||||
The god-mode admin surface (`/v1/admin/*`) the Hanzo Admin Console
|
||||
(admin.hanzo.ai / apps/operator) calls — served by the cloud `admin`
|
||||
subsystem (`hanzoai/cloud/clients/admin`).
|
||||
|
||||
It is an AGGREGATOR, not a store: identity (orgs/users/roles/applications/
|
||||
audit/me) is read from IAM, the money panels (spend/tokens/credits, revenue,
|
||||
finance/COGS) from commerce, System Health from o11y, and the compute/o11y
|
||||
fleet lenses from the shared datastore. Every route is GLOBAL-ADMIN ONLY and
|
||||
fail-closed: a non-global-admin principal is refused 403 before any upstream
|
||||
is touched.
|
||||
|
||||
Reads use the `/v1` envelope `{ status, msg, data, data2 }` — `data` is the
|
||||
payload, `data2` the total for list endpoints.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Identity
|
||||
description: Operator identity, orgs, users, roles, applications
|
||||
- name: Overview
|
||||
description: Fleet overview, usage, products, sync
|
||||
- name: Customers
|
||||
description: Customer cockpit — list, detail, credit, suspend/reactivate
|
||||
- name: Money
|
||||
description: Revenue, finance (COGS/margin), analytics
|
||||
- name: Observability
|
||||
description: Fleet compute and o11y aggregates
|
||||
- name: Audit
|
||||
description: Tamper-evident audit trail
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/admin/me:
|
||||
get:
|
||||
tags: [Identity]
|
||||
summary: Validated operator identity
|
||||
operationId: adminMe
|
||||
responses:
|
||||
'200': { description: Operator identity, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/AdminMe' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/overview:
|
||||
get:
|
||||
tags: [Overview]
|
||||
summary: Fleet overview tiles
|
||||
operationId: adminOverview
|
||||
responses:
|
||||
'200': { description: Overview, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/OverviewData' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/orgs:
|
||||
get:
|
||||
tags: [Identity]
|
||||
summary: Tenant directory
|
||||
operationId: adminListOrgs
|
||||
responses:
|
||||
'200': { description: Org rows, content: { application/json: { schema: { $ref: '#/components/schemas/OrgList' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/users:
|
||||
get:
|
||||
tags: [Identity]
|
||||
summary: Cross-org user directory
|
||||
operationId: adminListUsers
|
||||
parameters:
|
||||
- { name: org, in: query, schema: { type: string }, description: Filter to one org (owner) }
|
||||
- { name: p, in: query, schema: { type: integer }, description: 1-based page }
|
||||
- { name: pageSize, in: query, schema: { type: integer } }
|
||||
- { name: q, in: query, schema: { type: string }, description: Free-text name filter }
|
||||
responses:
|
||||
'200': { description: User rows, content: { application/json: { schema: { $ref: '#/components/schemas/UserList' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/roles:
|
||||
get:
|
||||
tags: [Identity]
|
||||
summary: IAM roles (verbatim passthrough)
|
||||
operationId: adminListRoles
|
||||
parameters:
|
||||
- { name: owner, in: query, schema: { type: string } }
|
||||
- { name: p, in: query, schema: { type: integer } }
|
||||
- { name: pageSize, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200': { description: Role rows (raw IAM shape), content: { application/json: { schema: { $ref: '#/components/schemas/RawList' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/applications:
|
||||
get:
|
||||
tags: [Identity]
|
||||
summary: IAM applications (verbatim passthrough)
|
||||
operationId: adminListApplications
|
||||
parameters:
|
||||
- { name: owner, in: query, schema: { type: string } }
|
||||
- { name: p, in: query, schema: { type: integer } }
|
||||
- { name: pageSize, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200': { description: Application rows (raw IAM shape), content: { application/json: { schema: { $ref: '#/components/schemas/RawList' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/audit:
|
||||
get:
|
||||
tags: [Audit]
|
||||
summary: Query the tamper-evident audit trail
|
||||
operationId: adminListAudit
|
||||
parameters:
|
||||
- { name: org, in: query, schema: { type: string } }
|
||||
- { name: sub, in: query, schema: { type: string } }
|
||||
- { name: action, in: query, schema: { type: string } }
|
||||
- { name: resource, in: query, schema: { type: string } }
|
||||
- { name: result, in: query, schema: { type: string } }
|
||||
- { name: since, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: until, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: pageSize, in: query, schema: { type: integer, default: 100 } }
|
||||
- { name: p, in: query, schema: { type: integer, default: 1 } }
|
||||
responses:
|
||||
'200': { description: Audit rows, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { type: array, items: { $ref: '#/components/schemas/AuditRow' } }, data2: { type: integer }, integrity: { $ref: '#/components/schemas/AuditIntegrity' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/audit/verify:
|
||||
get:
|
||||
tags: [Audit]
|
||||
summary: Verify audit-chain integrity
|
||||
operationId: adminVerifyAudit
|
||||
responses:
|
||||
'200': { description: Integrity result, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/AuditIntegrity' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/usage:
|
||||
get:
|
||||
tags: [Overview]
|
||||
summary: Fleet usage roll-up
|
||||
operationId: adminUsage
|
||||
parameters:
|
||||
- { name: org, in: query, schema: { type: string }, description: Scope to one org (default fleet) }
|
||||
responses:
|
||||
'200': { description: Usage data, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/UsageData' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/products:
|
||||
get:
|
||||
tags: [Overview]
|
||||
summary: Product / workload registry
|
||||
operationId: adminProducts
|
||||
responses:
|
||||
'200': { description: Product rows, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { type: array, items: { $ref: '#/components/schemas/ProductRow' } }, data2: { type: integer } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/finance:
|
||||
get:
|
||||
tags: [Money]
|
||||
summary: COGS / gross-margin / runway dashboard
|
||||
operationId: adminFinance
|
||||
responses:
|
||||
'200': { description: Finance data, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/FinanceData' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/compute:
|
||||
get:
|
||||
tags: [Observability]
|
||||
summary: Cross-tenant compute analytics
|
||||
operationId: adminCompute
|
||||
parameters:
|
||||
- { name: kind, in: query, schema: { type: string }, description: 'bot | machine | cluster | nodepool | container | function | …' }
|
||||
- { name: org, in: query, schema: { type: string } }
|
||||
- { name: range, in: query, schema: { type: string, enum: [24h, 7d, 30d], default: 30d } }
|
||||
responses:
|
||||
'200': { description: Compute leaves, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { type: array, items: { $ref: '#/components/schemas/ComputeLeaf' } }, data2: { type: integer } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/o11y:
|
||||
get:
|
||||
tags: [Observability]
|
||||
summary: Fleet-wide observability board
|
||||
operationId: adminO11y
|
||||
parameters:
|
||||
- { name: range, in: query, schema: { type: string, enum: [24h, 7d, 30d], default: 30d } }
|
||||
responses:
|
||||
'200': { description: Fleet o11y, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/O11yGlobal' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/sync:
|
||||
post:
|
||||
tags: [Overview]
|
||||
summary: Trigger a fleet re-read
|
||||
operationId: adminSync
|
||||
responses:
|
||||
'200': { description: Started, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { type: object, properties: { started: { type: boolean } } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/customers:
|
||||
get:
|
||||
tags: [Customers]
|
||||
summary: Fleet customer list
|
||||
operationId: adminListCustomers
|
||||
responses:
|
||||
'200': { description: Customer rows, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { type: array, items: { $ref: '#/components/schemas/CustomerRow' } }, data2: { type: integer } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/customers/{org}:
|
||||
get:
|
||||
tags: [Customers]
|
||||
summary: One customer's detail
|
||||
operationId: adminCustomerDetail
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Customer detail, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/CustomerDetail' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Customer not found }
|
||||
|
||||
/v1/admin/customers/{org}/credit:
|
||||
post:
|
||||
tags: [Customers]
|
||||
summary: Grant credit (commerce deposit)
|
||||
operationId: adminGrantCredit
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreditRequest' }
|
||||
responses:
|
||||
'200': { description: Grant result, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/CreditResult' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Customer not found }
|
||||
|
||||
/v1/admin/customers/{org}/suspend:
|
||||
post:
|
||||
tags: [Customers]
|
||||
summary: Suspend a customer (IAM isForbidden=true)
|
||||
operationId: adminSuspendCustomer
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Suspend result, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/AccessResult' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Customer not found }
|
||||
|
||||
/v1/admin/customers/{org}/reactivate:
|
||||
post:
|
||||
tags: [Customers]
|
||||
summary: Reactivate a customer (IAM isForbidden=false)
|
||||
operationId: adminReactivateCustomer
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Reactivate result, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/AccessResult' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Customer not found }
|
||||
|
||||
/v1/admin/revenue:
|
||||
get:
|
||||
tags: [Money]
|
||||
summary: Fleet revenue aggregate
|
||||
operationId: adminRevenue
|
||||
responses:
|
||||
'200': { description: Revenue data, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/RevenueData' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/admin/analytics:
|
||||
get:
|
||||
tags: [Money]
|
||||
summary: Native SaaS analytics (retention/growth/churn)
|
||||
operationId: adminAnalytics
|
||||
parameters:
|
||||
- { name: range, in: query, schema: { type: string, enum: [7d, 30d, 90d, all], default: 30d } }
|
||||
responses:
|
||||
'200': { description: Analytics data, content: { application/json: { schema: { type: object, properties: { status: { type: string }, msg: { type: string }, data: { $ref: '#/components/schemas/AnalyticsData' } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
RawList:
|
||||
type: object
|
||||
description: Verbatim IAM list envelope — `data` carries the raw upstream rows.
|
||||
properties:
|
||||
status: { type: string }
|
||||
msg: { type: string }
|
||||
data: { type: array, items: { type: object, additionalProperties: true } }
|
||||
data2: { type: integer }
|
||||
AdminMe:
|
||||
type: object
|
||||
properties:
|
||||
owner: { type: string }
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
displayName: { type: string }
|
||||
isGlobalAdmin: { type: boolean }
|
||||
SourceStatus:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
ok: { type: boolean }
|
||||
rows: { type: integer }
|
||||
error: { type: string }
|
||||
at: { type: string }
|
||||
OverviewData:
|
||||
type: object
|
||||
properties:
|
||||
orgs: { type: integer }
|
||||
users: { type: integer }
|
||||
products: { type: integer }
|
||||
activeProducts: { type: integer }
|
||||
drift: { type: integer }
|
||||
spendCents30d: { type: integer, format: int64 }
|
||||
tokens30d: { type: integer, format: int64 }
|
||||
creditsCents: { type: integer, format: int64 }
|
||||
lastSync: { type: string }
|
||||
sources: { type: array, items: { $ref: '#/components/schemas/SourceStatus' } }
|
||||
OrgRow:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
display: { type: string }
|
||||
users: { type: integer }
|
||||
products: { type: integer }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
creditsCents: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
created: { type: string }
|
||||
OrgList:
|
||||
type: object
|
||||
properties:
|
||||
status: { type: string }
|
||||
msg: { type: string }
|
||||
data: { type: array, items: { $ref: '#/components/schemas/OrgRow' } }
|
||||
data2: { type: integer }
|
||||
OperatorUser:
|
||||
type: object
|
||||
properties:
|
||||
owner: { type: string }
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
displayName: { type: string }
|
||||
isAdmin: { type: boolean }
|
||||
isGlobalAdmin: { type: boolean }
|
||||
tag: { type: string }
|
||||
created: { type: string }
|
||||
lastSignin: { type: string }
|
||||
forbidden: { type: boolean }
|
||||
UserList:
|
||||
type: object
|
||||
properties:
|
||||
status: { type: string }
|
||||
msg: { type: string }
|
||||
data: { type: array, items: { $ref: '#/components/schemas/OperatorUser' } }
|
||||
data2: { type: integer }
|
||||
UsageTotals:
|
||||
type: object
|
||||
properties:
|
||||
spendCents: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
requests: { type: integer, format: int64 }
|
||||
UsagePoint:
|
||||
type: object
|
||||
properties:
|
||||
date: { type: string }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
requests: { type: integer, format: int64 }
|
||||
UsageByProduct:
|
||||
type: object
|
||||
properties:
|
||||
product: { type: string }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
UsageData:
|
||||
type: object
|
||||
properties:
|
||||
totals: { $ref: '#/components/schemas/UsageTotals' }
|
||||
series: { type: array, items: { $ref: '#/components/schemas/UsagePoint' } }
|
||||
byProduct: { type: array, items: { $ref: '#/components/schemas/UsageByProduct' } }
|
||||
ProductRow:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
kind: { type: string }
|
||||
org: { type: string }
|
||||
cluster: { type: string }
|
||||
declaredTag: { type: string }
|
||||
runningTag: { type: string }
|
||||
health: { type: string }
|
||||
drift: { type: boolean }
|
||||
updated: { type: string }
|
||||
CustomerRow:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
display: { type: string }
|
||||
ownerEmail: { type: string }
|
||||
plan: { type: string }
|
||||
status: { type: string, enum: [active, suspended] }
|
||||
users: { type: integer }
|
||||
balanceCents: { type: integer, format: int64 }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
mrrCents: { type: integer, format: int64 }
|
||||
created: { type: string }
|
||||
lastActive: { type: string }
|
||||
CustomerUser:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
email: { type: string }
|
||||
isAdmin: { type: boolean }
|
||||
forbidden: { type: boolean }
|
||||
hasApiKey: { type: boolean }
|
||||
lastSignin: { type: string }
|
||||
created: { type: string }
|
||||
CustomerTxn:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
type: { type: string, enum: [deposit, withdraw] }
|
||||
cents: { type: integer, format: int64 }
|
||||
currency: { type: string }
|
||||
notes: { type: string }
|
||||
time: { type: string }
|
||||
CustomerDetail:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
display: { type: string }
|
||||
ownerEmail: { type: string }
|
||||
plan: { type: string }
|
||||
status: { type: string }
|
||||
created: { type: string }
|
||||
balanceCents: { type: integer, format: int64 }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
mrrCents: { type: integer, format: int64 }
|
||||
apiKeys: { type: integer }
|
||||
users: { type: array, items: { $ref: '#/components/schemas/CustomerUser' } }
|
||||
transactions: { type: array, items: { $ref: '#/components/schemas/CustomerTxn' } }
|
||||
CreditRequest:
|
||||
type: object
|
||||
required: [amountCents]
|
||||
properties:
|
||||
amountCents: { type: integer, format: int64, description: Positive credit to add (capped at 10,000,000 cents per grant) }
|
||||
currency: { type: string, default: usd }
|
||||
reason: { type: string, description: Operator justification (recorded in the audit trail) }
|
||||
CreditResult:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
grantedCents: { type: integer, format: int64 }
|
||||
currency: { type: string }
|
||||
balanceCents: { type: integer, format: int64 }
|
||||
transactionId: { type: string }
|
||||
AccessResult:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
suspended: { type: boolean }
|
||||
affected: { type: array, items: { type: string } }
|
||||
failed: { type: array, items: { type: string } }
|
||||
VendorCost:
|
||||
type: object
|
||||
properties:
|
||||
vendor: { type: string }
|
||||
service: { type: string }
|
||||
amountCents: { type: integer, format: int64 }
|
||||
source: { type: string, enum: [actual, estimated] }
|
||||
note: { type: string }
|
||||
DoHistoryPoint:
|
||||
type: object
|
||||
properties:
|
||||
date: { type: string }
|
||||
amountCents: { type: integer, format: int64 }
|
||||
type: { type: string }
|
||||
description: { type: string }
|
||||
DoCost:
|
||||
type: object
|
||||
properties:
|
||||
configured: { type: boolean }
|
||||
error: { type: string }
|
||||
creditRemainingCents: { type: integer, format: int64 }
|
||||
monthToDateSpendCents: { type: integer, format: int64 }
|
||||
avgDailyBurnCents: { type: integer, format: int64 }
|
||||
accountBalanceCents: { type: integer, format: int64 }
|
||||
generatedAt: { type: string }
|
||||
history: { type: array, items: { $ref: '#/components/schemas/DoHistoryPoint' } }
|
||||
FinanceCost:
|
||||
type: object
|
||||
properties:
|
||||
configured: { type: boolean }
|
||||
error: { type: string }
|
||||
period: { type: string }
|
||||
totalCents: { type: integer, format: int64 }
|
||||
vendors: { type: array, items: { $ref: '#/components/schemas/VendorCost' } }
|
||||
digitalocean: { $ref: '#/components/schemas/DoCost' }
|
||||
FinanceRevenue:
|
||||
type: object
|
||||
properties:
|
||||
configured: { type: boolean }
|
||||
totalRevenueCents: { type: integer, format: int64 }
|
||||
mrrCents: { type: integer, format: int64 }
|
||||
creditsConsumedCents: { type: integer, format: int64 }
|
||||
FinanceDerived:
|
||||
type: object
|
||||
properties:
|
||||
grossMarginCents: { type: integer, format: int64 }
|
||||
grossMarginPct: { type: number, format: double }
|
||||
runwayDays: { type: number, format: double, nullable: true }
|
||||
profitable: { type: boolean }
|
||||
FinanceData:
|
||||
type: object
|
||||
properties:
|
||||
cost: { $ref: '#/components/schemas/FinanceCost' }
|
||||
revenue: { $ref: '#/components/schemas/FinanceRevenue' }
|
||||
derived: { $ref: '#/components/schemas/FinanceDerived' }
|
||||
generatedAt: { type: string }
|
||||
sources: { type: array, items: { $ref: '#/components/schemas/SourceStatus' } }
|
||||
SeriesPoint:
|
||||
type: object
|
||||
properties:
|
||||
t: { type: string }
|
||||
value: { type: integer, format: int64 }
|
||||
RevenueCustomer:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
display: { type: string }
|
||||
plan: { type: string }
|
||||
balanceCents: { type: integer, format: int64 }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
mrrCents: { type: integer, format: int64 }
|
||||
RevenueData:
|
||||
type: object
|
||||
properties:
|
||||
totalBalancesCents: { type: integer, format: int64 }
|
||||
totalSpendCents: { type: integer, format: int64 }
|
||||
mrrCents: { type: integer, format: int64 }
|
||||
customers: { type: integer }
|
||||
payingCustomers: { type: integer }
|
||||
arpuCents: { type: integer, format: int64 }
|
||||
perCustomer: { type: array, items: { $ref: '#/components/schemas/RevenueCustomer' } }
|
||||
spendTrend: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
generatedAt: { type: string }
|
||||
sources: { type: array, items: { $ref: '#/components/schemas/SourceStatus' } }
|
||||
RetentionCohort:
|
||||
type: object
|
||||
properties:
|
||||
cohort: { type: string }
|
||||
size: { type: integer }
|
||||
values: { type: array, items: { type: number, format: double } }
|
||||
RetentionGrid:
|
||||
type: object
|
||||
properties:
|
||||
interval: { type: string }
|
||||
periods: { type: integer }
|
||||
cohorts: { type: array, items: { $ref: '#/components/schemas/RetentionCohort' } }
|
||||
AnalyticsSlice:
|
||||
type: object
|
||||
properties:
|
||||
label: { type: string }
|
||||
value: { type: integer, format: int64 }
|
||||
hint: { type: string }
|
||||
AnalyticsData:
|
||||
type: object
|
||||
properties:
|
||||
range: { type: string }
|
||||
interval: { type: string }
|
||||
generatedAt: { type: string }
|
||||
signups: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
cumulativeCustomers: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
totalCustomers: { type: integer }
|
||||
newCustomers: { type: integer }
|
||||
growthRatePct: { type: number, format: double }
|
||||
activeCustomers: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
dau: { type: integer }
|
||||
wau: { type: integer }
|
||||
mau: { type: integer }
|
||||
retention: { $ref: '#/components/schemas/RetentionGrid' }
|
||||
churn: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
churnRatePct: { type: number, format: double }
|
||||
mrrCents: { type: integer, format: int64 }
|
||||
revenue: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
arpuCents: { type: integer, format: int64 }
|
||||
ltvCents: { type: integer, format: int64, nullable: true }
|
||||
nrrPct: { type: number, format: double, nullable: true }
|
||||
usage: { type: array, items: { $ref: '#/components/schemas/SeriesPoint' } }
|
||||
topCustomers: { type: array, items: { $ref: '#/components/schemas/AnalyticsSlice' } }
|
||||
computed: { type: object, additionalProperties: { type: boolean } }
|
||||
sources: { type: array, items: { $ref: '#/components/schemas/SourceStatus' } }
|
||||
ComputeLeaf:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
app: { type: string }
|
||||
project: { type: string }
|
||||
kind: { type: string }
|
||||
machines: { type: integer, format: int64 }
|
||||
active: { type: integer, format: int64 }
|
||||
spendCents: { type: integer, format: int64 }
|
||||
lastTs: { type: string }
|
||||
O11yTotals:
|
||||
type: object
|
||||
properties:
|
||||
requests: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
promptTokens: { type: integer, format: int64 }
|
||||
completionTokens: { type: integer, format: int64 }
|
||||
costCents: { type: integer, format: int64 }
|
||||
errors: { type: integer, format: int64 }
|
||||
orgs: { type: integer, format: int64 }
|
||||
models: { type: integer, format: int64 }
|
||||
traceCount: { type: integer, format: int64 }
|
||||
latencyP50Ms: { type: number, format: double }
|
||||
latencyP95Ms: { type: number, format: double }
|
||||
latencyP99Ms: { type: number, format: double }
|
||||
traceErrorRate: { type: number, format: double }
|
||||
services: { type: integer, format: int64 }
|
||||
logVolume: { type: integer, format: int64 }
|
||||
O11ySeries:
|
||||
type: object
|
||||
properties:
|
||||
ts: { type: string }
|
||||
requests: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
costCents: { type: integer, format: int64 }
|
||||
errors: { type: integer, format: int64 }
|
||||
O11yLogPoint:
|
||||
type: object
|
||||
properties:
|
||||
ts: { type: string }
|
||||
count: { type: integer, format: int64 }
|
||||
O11yOrgStat:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
requests: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
costCents: { type: integer, format: int64 }
|
||||
O11yModelStat:
|
||||
type: object
|
||||
properties:
|
||||
model: { type: string }
|
||||
requests: { type: integer, format: int64 }
|
||||
tokens: { type: integer, format: int64 }
|
||||
costCents: { type: integer, format: int64 }
|
||||
O11ySvcStat:
|
||||
type: object
|
||||
properties:
|
||||
service: { type: string }
|
||||
requests: { type: integer, format: int64 }
|
||||
errorRate: { type: number, format: double }
|
||||
latencyP95Ms: { type: number, format: double }
|
||||
O11yLLM:
|
||||
type: object
|
||||
properties:
|
||||
generations: { type: integer, format: int64 }
|
||||
costUsd: { type: number, format: double }
|
||||
O11yGlobal:
|
||||
type: object
|
||||
properties:
|
||||
range: { type: string }
|
||||
start: { type: string }
|
||||
end: { type: string }
|
||||
totals: { $ref: '#/components/schemas/O11yTotals' }
|
||||
series: { type: array, items: { $ref: '#/components/schemas/O11ySeries' } }
|
||||
logSeries: { type: array, items: { $ref: '#/components/schemas/O11yLogPoint' } }
|
||||
topOrgs: { type: array, items: { $ref: '#/components/schemas/O11yOrgStat' } }
|
||||
topModels: { type: array, items: { $ref: '#/components/schemas/O11yModelStat' } }
|
||||
topServices: { type: array, items: { $ref: '#/components/schemas/O11ySvcStat' } }
|
||||
llm: { $ref: '#/components/schemas/O11yLLM' }
|
||||
AuditRow:
|
||||
type: object
|
||||
properties:
|
||||
seq: { type: integer, format: int64 }
|
||||
time: { type: string }
|
||||
org: { type: string }
|
||||
sub: { type: string }
|
||||
email: { type: string }
|
||||
action: { type: string }
|
||||
resource: { type: string }
|
||||
resourceId: { type: string }
|
||||
method: { type: string }
|
||||
path: { type: string }
|
||||
result: { type: string }
|
||||
status: { type: integer }
|
||||
reason: { type: string }
|
||||
sourceIp: { type: string }
|
||||
userAgent: { type: string }
|
||||
requestId: { type: string }
|
||||
isAdmin: { type: boolean }
|
||||
authMethod: { type: string }
|
||||
hash: { type: string }
|
||||
prevHash: { type: string }
|
||||
AuditIntegrity:
|
||||
type: object
|
||||
description: Hash-chain verification summary.
|
||||
properties:
|
||||
ok: { type: boolean }
|
||||
count: { type: integer, format: int64 }
|
||||
head: { type: string }
|
||||
brokenAt: { type: integer, format: int64 }
|
||||
@@ -0,0 +1,414 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Automations API
|
||||
description: |
|
||||
Connectors + Automations engine for Hanzo Cloud — the `/v1/automations/*`
|
||||
surface served by the cloud `automations` subsystem
|
||||
(`hanzoai/cloud/clients/automations`, HIP-0106). An org authors a
|
||||
trigger→action flow tree, versions it, and runs it durably on the shared
|
||||
in-process tasks engine; connector actions are also exposed as MCP tools at
|
||||
`/v1/automations/mcp`. Every flow runs in the OWNER org's namespace; the org
|
||||
is always server-derived from the validated principal, never a request body.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Pieces
|
||||
description: The connector/piece catalogue
|
||||
- name: Flows
|
||||
description: Automation flows and their versions
|
||||
- name: Runs
|
||||
description: Durable flow runs
|
||||
- name: MCP
|
||||
description: JSON-RPC tool surface over connector actions
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/automations/pieces:
|
||||
get:
|
||||
tags: [Pieces]
|
||||
summary: List the connector/piece catalogue
|
||||
operationId: listPieces
|
||||
responses:
|
||||
'200': { description: Catalogue, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/automations/flows:
|
||||
get:
|
||||
tags: [Flows]
|
||||
summary: List flows
|
||||
operationId: listFlows
|
||||
parameters:
|
||||
- { name: limit, in: query, schema: { type: integer, default: 200, maximum: 1000 } }
|
||||
responses:
|
||||
'200': { description: Flows, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Flow' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Flows]
|
||||
summary: Create a flow (with an initial draft version)
|
||||
operationId: createFlow
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreateFlowRequest' }
|
||||
responses:
|
||||
'201': { description: Flow created, content: { application/json: { schema: { $ref: '#/components/schemas/PopulatedFlow' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'422': { description: Invalid trigger }
|
||||
|
||||
/v1/automations/flows/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Flows]
|
||||
summary: Get a flow and its latest version
|
||||
operationId: getFlow
|
||||
responses:
|
||||
'200': { description: Flow, content: { application/json: { schema: { $ref: '#/components/schemas/PopulatedFlow' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found }
|
||||
patch:
|
||||
tags: [Flows]
|
||||
summary: Update flow metadata
|
||||
operationId: updateFlow
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PatchFlowRequest' }
|
||||
responses:
|
||||
'200': { description: Updated flow, content: { application/json: { schema: { $ref: '#/components/schemas/Flow' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found }
|
||||
'422': { description: publishedVersionId must name a version of this flow }
|
||||
delete:
|
||||
tags: [Flows]
|
||||
summary: Delete a flow (with its versions and runs)
|
||||
operationId: deleteFlow
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found }
|
||||
|
||||
/v1/automations/flows/{id}/versions:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Flows]
|
||||
summary: List a flow's versions
|
||||
operationId: listVersions
|
||||
parameters:
|
||||
- { name: limit, in: query, schema: { type: integer, default: 200, maximum: 1000 } }
|
||||
responses:
|
||||
'200': { description: Versions, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/FlowVersion' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Flows]
|
||||
summary: Create a draft version
|
||||
operationId: createVersion
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreateVersionRequest' }
|
||||
responses:
|
||||
'201': { description: Version created, content: { application/json: { schema: { $ref: '#/components/schemas/FlowVersion' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found }
|
||||
'422': { description: Invalid trigger }
|
||||
|
||||
/v1/automations/flows/{id}/operations:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Flows]
|
||||
summary: Apply a flow operation to the latest version
|
||||
operationId: applyOperation
|
||||
description: A CHANGE_STATUS op enables/disables the flow (returns the Flow); every other op mutates the latest version's step tree (returns the FlowVersion).
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/FlowOperation' }
|
||||
responses:
|
||||
'200': { description: Updated version or flow, content: { application/json: { schema: { $ref: '#/components/schemas/FlowVersion' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow, version, or step not found }
|
||||
'422': { description: Unsupported or invalid operation }
|
||||
|
||||
/v1/automations/flows/{id}/run:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Runs]
|
||||
summary: Start a durable run of a flow's runnable version
|
||||
operationId: runFlow
|
||||
responses:
|
||||
'201': { description: Run started, content: { application/json: { schema: { $ref: '#/components/schemas/FlowRun' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found or has no runnable version }
|
||||
'429': { description: Too many concurrent automation requests for this org }
|
||||
'503': { description: Automation engine not ready }
|
||||
|
||||
/v1/automations/flows/{id}/enable:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Flows]
|
||||
summary: Enable a flow (POLLING triggers create a schedule)
|
||||
operationId: enableFlow
|
||||
responses:
|
||||
'200': { description: Flow enabled, content: { application/json: { schema: { $ref: '#/components/schemas/Flow' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found }
|
||||
'503': { description: Engine required for a POLLING enable and not ready }
|
||||
|
||||
/v1/automations/flows/{id}/disable:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Flows]
|
||||
summary: Disable a flow (removes any POLLING schedule)
|
||||
operationId: disableFlow
|
||||
responses:
|
||||
'200': { description: Flow disabled, content: { application/json: { schema: { $ref: '#/components/schemas/Flow' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Flow not found }
|
||||
|
||||
/v1/automations/runs:
|
||||
get:
|
||||
tags: [Runs]
|
||||
summary: List runs
|
||||
operationId: listRuns
|
||||
parameters:
|
||||
- { name: flowId, in: query, schema: { type: string } }
|
||||
- { name: limit, in: query, schema: { type: integer, default: 200, maximum: 1000 } }
|
||||
responses:
|
||||
'200': { description: Runs, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/FlowRun' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/automations/runs/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Runs]
|
||||
summary: Get a run (non-terminal status is refreshed from the engine)
|
||||
operationId: getRun
|
||||
responses:
|
||||
'200': { description: Run, content: { application/json: { schema: { $ref: '#/components/schemas/FlowRun' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Run not found }
|
||||
|
||||
/v1/automations/runs/{id}/resume:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Runs]
|
||||
summary: Resume a paused run
|
||||
operationId: resumeRun
|
||||
description: The JSON body (max 64 KiB) is delivered verbatim as the waitpoint's output.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema: { description: Arbitrary JSON payload delivered to the paused waitpoint }
|
||||
responses:
|
||||
'200': { description: Resumed, content: { application/json: { schema: { type: object, properties: { resumed: { type: boolean } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Run not found }
|
||||
'413': { description: Resume payload exceeds the 64 KiB limit }
|
||||
'503': { description: Automation engine not ready }
|
||||
|
||||
/v1/automations/mcp:
|
||||
post:
|
||||
tags: [MCP]
|
||||
summary: JSON-RPC 2.0 tool surface over connector actions
|
||||
operationId: mcp
|
||||
description: |
|
||||
HIP-0300 JSON-RPC 2.0. Methods: `initialize`, `ping`, `tools/list`,
|
||||
`tools/call`. Every connector action is a tool named `<connector>_<action>`.
|
||||
JSON-RPC errors are returned with HTTP 200 and an `error` member.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/McpRequest' }
|
||||
responses:
|
||||
'200': { description: JSON-RPC response, content: { application/json: { schema: { $ref: '#/components/schemas/McpResponse' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Catalog:
|
||||
type: object
|
||||
properties:
|
||||
pieceCount: { type: integer }
|
||||
pieces: { type: array, items: { $ref: '#/components/schemas/PieceMetadata' } }
|
||||
PieceMetadata:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
displayName: { type: string }
|
||||
description: { type: string }
|
||||
logoUrl: { type: string }
|
||||
version: { type: string }
|
||||
categories: { type: array, items: { type: string } }
|
||||
auth: { $ref: '#/components/schemas/PieceAuth' }
|
||||
actions: { type: array, items: { $ref: '#/components/schemas/PieceAction' } }
|
||||
triggers: { type: array, items: { $ref: '#/components/schemas/PieceTrigger' } }
|
||||
PieceAuth:
|
||||
type: object
|
||||
properties:
|
||||
type: { type: string, description: 'none | oauth2 | bot_token' }
|
||||
required: { type: boolean }
|
||||
PieceAction:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
displayName: { type: string }
|
||||
description: { type: string }
|
||||
props: { type: array, items: { $ref: '#/components/schemas/PropSpec' } }
|
||||
PieceTrigger:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
displayName: { type: string }
|
||||
description: { type: string }
|
||||
strategy: { type: string }
|
||||
props: { type: array, items: { $ref: '#/components/schemas/PropSpec' } }
|
||||
PropSpec:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
displayName: { type: string }
|
||||
type: { type: string, enum: [string, number, boolean, object, array] }
|
||||
required: { type: boolean }
|
||||
description: { type: string }
|
||||
Flow:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
projectId: { type: string, description: The owner org (server-derived) }
|
||||
externalId: { type: string }
|
||||
folderId: { type: string }
|
||||
status: { type: string, enum: [ENABLED, DISABLED] }
|
||||
publishedVersionId: { type: string }
|
||||
metadata: { description: Opaque builder metadata }
|
||||
created: { type: integer, format: int64, description: Unix ms }
|
||||
updated: { type: integer, format: int64, description: Unix ms }
|
||||
PopulatedFlow:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/Flow' }
|
||||
- type: object
|
||||
properties:
|
||||
version: { $ref: '#/components/schemas/FlowVersion' }
|
||||
FlowVersion:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
flowId: { type: string }
|
||||
displayName: { type: string }
|
||||
trigger: { $ref: '#/components/schemas/FlowTrigger' }
|
||||
valid: { type: boolean }
|
||||
state: { type: string, enum: [DRAFT, LOCKED] }
|
||||
schemaVersion: { type: string }
|
||||
created: { type: integer, format: int64 }
|
||||
updated: { type: integer, format: int64 }
|
||||
StepSettings:
|
||||
type: object
|
||||
properties:
|
||||
pieceName: { type: string }
|
||||
pieceVersion: { type: string }
|
||||
actionName: { type: string }
|
||||
triggerName: { type: string }
|
||||
input: { type: object, additionalProperties: true }
|
||||
FlowTrigger:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string, enum: [PIECE_TRIGGER, EMPTY] }
|
||||
displayName: { type: string }
|
||||
valid: { type: boolean }
|
||||
strategy: { type: string, enum: [POLLING, WEBHOOK, APP_WEBHOOK, MANUAL] }
|
||||
settings: { $ref: '#/components/schemas/StepSettings' }
|
||||
nextAction: { $ref: '#/components/schemas/FlowAction' }
|
||||
FlowAction:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string, enum: [PIECE, CODE, ROUTER, LOOP_ON_ITEMS, BRANCH] }
|
||||
displayName: { type: string }
|
||||
valid: { type: boolean }
|
||||
skip: { type: boolean }
|
||||
settings: { $ref: '#/components/schemas/StepSettings' }
|
||||
nextAction: { $ref: '#/components/schemas/FlowAction' }
|
||||
CreateFlowRequest:
|
||||
type: object
|
||||
properties:
|
||||
displayName: { type: string }
|
||||
externalId: { type: string }
|
||||
folderId: { type: string }
|
||||
trigger: { $ref: '#/components/schemas/FlowTrigger' }
|
||||
PatchFlowRequest:
|
||||
type: object
|
||||
properties:
|
||||
folderId: { type: string }
|
||||
externalId: { type: string }
|
||||
publishedVersionId: { type: string }
|
||||
metadata: { description: Opaque builder metadata }
|
||||
CreateVersionRequest:
|
||||
type: object
|
||||
properties:
|
||||
displayName: { type: string }
|
||||
trigger: { $ref: '#/components/schemas/FlowTrigger' }
|
||||
FlowOperation:
|
||||
type: object
|
||||
required: [type, request]
|
||||
properties:
|
||||
type: { type: string, enum: [ADD_ACTION, UPDATE_ACTION, DELETE_ACTION, MOVE_ACTION, UPDATE_TRIGGER, CHANGE_NAME, CHANGE_STATUS] }
|
||||
request: { description: Per-type operation payload }
|
||||
FlowRun:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
flowId: { type: string }
|
||||
flowVersionId: { type: string }
|
||||
status: { type: string, enum: [RUNNING, SUCCEEDED, FAILED, PAUSED, QUEUED, CANCELED, TIMEOUT] }
|
||||
startTime: { type: integer, format: int64 }
|
||||
finishTime: { type: integer, format: int64 }
|
||||
created: { type: integer, format: int64 }
|
||||
updated: { type: integer, format: int64 }
|
||||
McpRequest:
|
||||
type: object
|
||||
required: [jsonrpc, method]
|
||||
properties:
|
||||
jsonrpc: { type: string, example: '2.0' }
|
||||
id: { description: Request id (string, number, or null) }
|
||||
method: { type: string, enum: [initialize, ping, tools/list, tools/call] }
|
||||
params: { description: Method params }
|
||||
McpResponse:
|
||||
type: object
|
||||
properties:
|
||||
jsonrpc: { type: string, example: '2.0' }
|
||||
id: { description: Echoed request id }
|
||||
result: { description: Method result (present on success) }
|
||||
error: { type: object, properties: { code: { type: integer }, message: { type: string } } }
|
||||
@@ -0,0 +1,161 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Billing API
|
||||
description: |
|
||||
The customer-facing, org-scoped billing surface (`/v1/billing/*`) served by
|
||||
the cloud `billing` subsystem (`hanzoai/cloud/clients/billing`). It is the
|
||||
per-org READ twin of the admin god-view: a thin service-to-service proxy to
|
||||
commerce, scoped to the caller's OWN org (the validated IAM owner claim). The
|
||||
billing subject is pinned server-side — a client can never widen scope to
|
||||
another tenant's wallet. Bodies + status codes are forwarded from commerce
|
||||
verbatim (e.g. `402` on `gpu-charge` when a card is required or prepaid is
|
||||
insufficient).
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Billing
|
||||
description: Per-org usage, balance, GPU launch gate, and saved cards
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/billing/usage:
|
||||
get:
|
||||
tags: [Billing]
|
||||
summary: Per-request usage ledger
|
||||
operationId: billingUsage
|
||||
description: The raw per-request billing ledger (one row per billed call) for the caller's own org.
|
||||
parameters:
|
||||
- { name: start, in: query, schema: { type: string }, description: Optional server-side window start }
|
||||
- { name: end, in: query, schema: { type: string }, description: Optional server-side window end }
|
||||
responses:
|
||||
'200': { description: Usage ledger, content: { application/json: { schema: { $ref: '#/components/schemas/UsageLedger' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/billing/balance:
|
||||
get:
|
||||
tags: [Billing]
|
||||
summary: Prepaid credit balance
|
||||
operationId: billingBalance
|
||||
description: The org's prepaid credit balance in USD cents — the same wallet the gateway debits.
|
||||
parameters:
|
||||
- { name: currency, in: query, schema: { type: string }, description: Optional currency filter (default usd) }
|
||||
responses:
|
||||
'200': { description: Balance, content: { application/json: { schema: { $ref: '#/components/schemas/Balance' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/billing/gpu-eligibility:
|
||||
get:
|
||||
tags: [Billing]
|
||||
summary: GPU launch eligibility gate
|
||||
operationId: billingGpuEligibility
|
||||
description: Read-only launch gate — reports prepaid availability and card-on-file so the launch UI can show the exact remedy.
|
||||
parameters:
|
||||
- { name: amountCents, in: query, schema: { type: integer, format: int64 }, description: Immediate charge to test against }
|
||||
- { name: minPrepaidCents, in: query, schema: { type: integer, format: int64 }, description: 24h-minimum prepaid floor }
|
||||
- { name: currency, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Eligibility, content: { application/json: { schema: { $ref: '#/components/schemas/GpuEligibility' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/billing/gpu-charge:
|
||||
post:
|
||||
tags: [Billing]
|
||||
summary: Prepay-only GPU charge
|
||||
operationId: billingGpuCharge
|
||||
description: |
|
||||
The prepay-only, card-required GPU debit against the caller's own org
|
||||
wallet. Commerce enforces both gates server-side; the subject is pinned
|
||||
server-side, so only the charge params ride the body. Commerce's status is
|
||||
forwarded verbatim.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/GpuChargeRequest' }
|
||||
responses:
|
||||
'201': { description: Charge accepted, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'402': { description: 'Payment required — card_required or insufficient_prepaid', content: { application/json: { schema: { type: object, properties: { reason: { type: string, enum: [card_required, insufficient_prepaid] } }, additionalProperties: true } } } }
|
||||
|
||||
/v1/billing/payment-methods:
|
||||
get:
|
||||
tags: [Billing]
|
||||
summary: Saved payment methods (masked)
|
||||
operationId: billingPaymentMethods
|
||||
description: The org's saved cards as commerce's masked descriptor (brand + last4 + expiry) — never a PAN/CVV/token.
|
||||
responses:
|
||||
'200': { description: Saved cards, content: { application/json: { schema: { $ref: '#/components/schemas/PaymentMethods' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
UsageRecord:
|
||||
type: object
|
||||
properties:
|
||||
transactionId: { type: string }
|
||||
amount: { type: integer, format: int64, description: Billed amount in USD cents }
|
||||
metadata: { type: object, additionalProperties: true }
|
||||
createdAt: { type: string }
|
||||
additionalProperties: true
|
||||
UsageLedger:
|
||||
type: object
|
||||
properties:
|
||||
usage: { type: array, items: { $ref: '#/components/schemas/UsageRecord' } }
|
||||
additionalProperties: true
|
||||
Balance:
|
||||
type: object
|
||||
description: Prepaid wallet balance in USD cents.
|
||||
properties:
|
||||
balance: { type: integer, format: int64 }
|
||||
holds: { type: integer, format: int64 }
|
||||
available: { type: integer, format: int64 }
|
||||
additionalProperties: true
|
||||
GpuEligibility:
|
||||
type: object
|
||||
properties:
|
||||
eligible: { type: boolean }
|
||||
reason: { type: string }
|
||||
prepaidAvailable: { type: integer, format: int64 }
|
||||
cardOnFile: { type: boolean }
|
||||
requiredCents: { type: integer, format: int64 }
|
||||
additionalProperties: true
|
||||
GpuChargeRequest:
|
||||
type: object
|
||||
properties:
|
||||
amountCents: { type: integer, format: int64 }
|
||||
currency: { type: string }
|
||||
requestId: { type: string }
|
||||
tag: { type: string }
|
||||
PaymentMethod:
|
||||
type: object
|
||||
description: Masked card descriptor — brand + last4 + expiry only.
|
||||
properties:
|
||||
brand: { type: string }
|
||||
last4: { type: string }
|
||||
expMonth: { type: integer }
|
||||
expYear: { type: integer }
|
||||
additionalProperties: true
|
||||
PaymentMethods:
|
||||
type: object
|
||||
properties:
|
||||
data: { type: array, items: { $ref: '#/components/schemas/PaymentMethod' } }
|
||||
additionalProperties: true
|
||||
@@ -0,0 +1,298 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo CRM API
|
||||
description: |
|
||||
Per-org CRM for Hanzo Cloud — the `/v1/crm/*` surface served by the cloud
|
||||
`crm` subsystem (`hanzoai/cloud/clients/crm`). A native-Go port of the
|
||||
Twenty CRM core model (company / person / opportunity) on Base/SQLite, NOT a
|
||||
proxy to a NestJS backend. Tenant isolation is enforced server-side on every
|
||||
request from the validated bearer owner claim; one tenant can never read or
|
||||
mutate another's records.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Companies
|
||||
description: Account records (Twenty `company` standard object)
|
||||
- name: Contacts
|
||||
description: Person records (Twenty `person` standard object)
|
||||
- name: Opportunities
|
||||
description: Deal records (Twenty `opportunity` standard object)
|
||||
- name: Summary
|
||||
description: Per-org row counts
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/crm/summary:
|
||||
get:
|
||||
tags: [Summary]
|
||||
summary: Per-org row counts (companies / contacts / opportunities)
|
||||
operationId: crmSummary
|
||||
responses:
|
||||
'200': { description: Counts, content: { application/json: { schema: { $ref: '#/components/schemas/Summary' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/crm/companies:
|
||||
get:
|
||||
tags: [Companies]
|
||||
summary: List companies
|
||||
operationId: listCompanies
|
||||
parameters:
|
||||
- { name: limit, in: query, schema: { type: integer, default: 200, maximum: 1000 } }
|
||||
responses:
|
||||
'200': { description: Companies, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Company' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Companies]
|
||||
summary: Create a company
|
||||
operationId: createCompany
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CompanyInput' }
|
||||
responses:
|
||||
'201': { description: Company created, content: { application/json: { schema: { $ref: '#/components/schemas/Company' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/crm/companies/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Companies]
|
||||
summary: Company detail
|
||||
operationId: getCompany
|
||||
responses:
|
||||
'200': { description: Company, content: { application/json: { schema: { $ref: '#/components/schemas/Company' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Company not found }
|
||||
put:
|
||||
tags: [Companies]
|
||||
summary: Update a company
|
||||
operationId: updateCompany
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CompanyInput' }
|
||||
responses:
|
||||
'200': { description: Company updated, content: { application/json: { schema: { $ref: '#/components/schemas/Company' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Company not found }
|
||||
delete:
|
||||
tags: [Companies]
|
||||
summary: Delete a company (clears dangling contact/opportunity refs)
|
||||
operationId: deleteCompany
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Company not found }
|
||||
|
||||
/v1/crm/contacts:
|
||||
get:
|
||||
tags: [Contacts]
|
||||
summary: List contacts
|
||||
operationId: listContacts
|
||||
parameters:
|
||||
- { name: companyId, in: query, schema: { type: string }, description: Filter to one company }
|
||||
- { name: limit, in: query, schema: { type: integer, default: 200, maximum: 1000 } }
|
||||
responses:
|
||||
'200': { description: Contacts, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Contact' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Contacts]
|
||||
summary: Create a contact
|
||||
operationId: createContact
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ContactInput' }
|
||||
responses:
|
||||
'201': { description: Contact created, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'422': { description: Referenced company not found in org }
|
||||
|
||||
/v1/crm/contacts/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Contacts]
|
||||
summary: Contact detail
|
||||
operationId: getContact
|
||||
responses:
|
||||
'200': { description: Contact, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Contact not found }
|
||||
put:
|
||||
tags: [Contacts]
|
||||
summary: Update a contact
|
||||
operationId: updateContact
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ContactInput' }
|
||||
responses:
|
||||
'200': { description: Contact updated, content: { application/json: { schema: { $ref: '#/components/schemas/Contact' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Contact not found }
|
||||
delete:
|
||||
tags: [Contacts]
|
||||
summary: Delete a contact (clears opportunity point-of-contact refs)
|
||||
operationId: deleteContact
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Contact not found }
|
||||
|
||||
/v1/crm/opportunities:
|
||||
get:
|
||||
tags: [Opportunities]
|
||||
summary: List opportunities
|
||||
operationId: listOpportunities
|
||||
parameters:
|
||||
- { name: stage, in: query, schema: { $ref: '#/components/schemas/Stage' }, description: Filter to one pipeline stage }
|
||||
- { name: limit, in: query, schema: { type: integer, default: 200, maximum: 1000 } }
|
||||
responses:
|
||||
'200': { description: Opportunities, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Opportunity' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Opportunities]
|
||||
summary: Create an opportunity
|
||||
operationId: createOpportunity
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OpportunityInput' }
|
||||
responses:
|
||||
'201': { description: Opportunity created, content: { application/json: { schema: { $ref: '#/components/schemas/Opportunity' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'422': { description: Referenced company/contact not found in org }
|
||||
|
||||
/v1/crm/opportunities/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Opportunities]
|
||||
summary: Opportunity detail
|
||||
operationId: getOpportunity
|
||||
responses:
|
||||
'200': { description: Opportunity, content: { application/json: { schema: { $ref: '#/components/schemas/Opportunity' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Opportunity not found }
|
||||
put:
|
||||
tags: [Opportunities]
|
||||
summary: Update an opportunity
|
||||
operationId: updateOpportunity
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/OpportunityInput' }
|
||||
responses:
|
||||
'200': { description: Opportunity updated, content: { application/json: { schema: { $ref: '#/components/schemas/Opportunity' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Opportunity not found }
|
||||
delete:
|
||||
tags: [Opportunities]
|
||||
summary: Delete an opportunity
|
||||
operationId: deleteOpportunity
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Opportunity not found }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Stage:
|
||||
type: string
|
||||
description: Opportunity pipeline stage (default NEW).
|
||||
enum: [NEW, SCREENING, MEETING, PROPOSAL, CUSTOMER]
|
||||
Summary:
|
||||
type: object
|
||||
properties:
|
||||
companies: { type: integer }
|
||||
contacts: { type: integer }
|
||||
opportunities: { type: integer }
|
||||
CompanyInput:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
domainName: { type: string }
|
||||
employees: { type: integer, format: int64 }
|
||||
city: { type: string }
|
||||
country: { type: string }
|
||||
arr: { type: integer, format: int64, description: Annual recurring revenue in minor units (cents) of currency }
|
||||
currency: { type: string, default: USD }
|
||||
idealCustomerProfile: { type: boolean }
|
||||
linkedinLink: { type: string }
|
||||
xLink: { type: string }
|
||||
Company:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CompanyInput'
|
||||
- type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
createdAt: { type: integer, format: int64, description: Unix seconds }
|
||||
updatedAt: { type: integer, format: int64, description: Unix seconds }
|
||||
ContactInput:
|
||||
type: object
|
||||
description: One of firstName, lastName, or email is required.
|
||||
properties:
|
||||
firstName: { type: string }
|
||||
lastName: { type: string }
|
||||
email: { type: string }
|
||||
phone: { type: string }
|
||||
jobTitle: { type: string }
|
||||
city: { type: string }
|
||||
companyId: { type: string, description: Optional in-org relation to a Company }
|
||||
linkedinLink: { type: string }
|
||||
xLink: { type: string }
|
||||
Contact:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ContactInput'
|
||||
- type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
createdAt: { type: integer, format: int64, description: Unix seconds }
|
||||
updatedAt: { type: integer, format: int64, description: Unix seconds }
|
||||
OpportunityInput:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
amount: { type: integer, format: int64, description: Deal size in minor units (cents) of currency }
|
||||
currency: { type: string, default: USD }
|
||||
stage: { $ref: '#/components/schemas/Stage' }
|
||||
closeDate: { type: integer, format: int64, description: Unix seconds (0 = unset) }
|
||||
companyId: { type: string, description: Optional in-org relation to a Company }
|
||||
pointOfContactId: { type: string, description: Optional in-org relation to a Contact }
|
||||
Opportunity:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/OpportunityInput'
|
||||
- type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
createdAt: { type: integer, format: int64, description: Unix seconds }
|
||||
updatedAt: { type: integer, format: int64, description: Unix seconds }
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo DigitalOcean Infra API
|
||||
description: |
|
||||
DigitalOcean-native infra surface served by `hanzoai/cloud/clients/do`:
|
||||
VPCs (`/v1/vpcs`) and Load Balancers (`/v1/load-balancers`), a thin
|
||||
org-scoped facade over the `digitalocean/godo` SDK. DigitalOcean is a single
|
||||
account, so the org boundary is enforced by this subsystem: a resource's
|
||||
physical DO name is derived from the caller's validated org, LIST filters to
|
||||
the caller's namespace, and a cross-tenant id is reported 404 (an
|
||||
existence-oracle guard, never 403).
|
||||
|
||||
Fail-closed: absent `DO_API_TOKEN` the surface mounts but every op returns
|
||||
503; it never fabricates a resource.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: VPCs
|
||||
description: Virtual Private Cloud networks
|
||||
- name: Load Balancers
|
||||
description: Regional/global load balancers
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/vpcs:
|
||||
get:
|
||||
tags: [VPCs]
|
||||
summary: List the caller's VPCs
|
||||
operationId: listVpcs
|
||||
responses:
|
||||
'200': { description: VPCs, content: { application/json: { schema: { type: object, properties: { vpcs: { type: array, items: { $ref: '#/components/schemas/Vpc' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'502': { description: DigitalOcean upstream error }
|
||||
'503': { description: DigitalOcean not configured }
|
||||
post:
|
||||
tags: [VPCs]
|
||||
summary: Create a VPC
|
||||
operationId: createVpc
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/VpcCreate' }
|
||||
responses:
|
||||
'201': { description: VPC created, content: { application/json: { schema: { $ref: '#/components/schemas/Vpc' } } } }
|
||||
'400': { description: Invalid name or missing region }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'409': { description: A VPC with that name already exists }
|
||||
'502': { description: DigitalOcean upstream error }
|
||||
'503': { description: DigitalOcean not configured }
|
||||
|
||||
/v1/vpcs/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string }, description: DO VPC id }
|
||||
get:
|
||||
tags: [VPCs]
|
||||
summary: Get one VPC (owned)
|
||||
operationId: getVpc
|
||||
responses:
|
||||
'200': { description: VPC, content: { application/json: { schema: { $ref: '#/components/schemas/Vpc' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: VPC not found }
|
||||
'503': { description: DigitalOcean not configured }
|
||||
delete:
|
||||
tags: [VPCs]
|
||||
summary: Delete one VPC (owned)
|
||||
operationId: deleteVpc
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'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:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
VpcCreate:
|
||||
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 }
|
||||
ip_range: { type: string, description: 'CIDR (empty → DO auto-assigns)' }
|
||||
Vpc:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
cidr: { type: string }
|
||||
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,146 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Exec API
|
||||
description: |
|
||||
Code Interpreter ("Run Code") surface on the unified cloud-api /v1 plane
|
||||
(`hanzoai/cloud/clients/exec`), per HIP-0106. A thin reverse proxy that
|
||||
forwards each request UNCHANGED to a sandboxed executor upstream — no code
|
||||
runs in the cloud binary. The contract is the LibreChat code-interpreter
|
||||
contract: POST `{lang, code, files?}` to `/v1/exec`, with sibling paths for
|
||||
programmatic execution, uploads, downloads and per-session file listing.
|
||||
|
||||
AUTH: this surface is called server-side by the chat node process with a
|
||||
shared service key on the `X-API-Key` header (NOT a user JWT — the gateway
|
||||
bypasses these paths). An unset key fails closed (503); a wrong key is 401.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Exec
|
||||
description: Sandboxed code execution
|
||||
- name: Files
|
||||
description: Session file upload / download / listing
|
||||
security:
|
||||
- serviceKey: []
|
||||
paths:
|
||||
/v1/exec:
|
||||
post:
|
||||
tags: [Exec]
|
||||
summary: Execute code in a sandboxed session
|
||||
operationId: execCode
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ExecRequest' }
|
||||
responses:
|
||||
'200': { description: Execution result, content: { application/json: { schema: { $ref: '#/components/schemas/ExecResult' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
/v1/exec/programmatic:
|
||||
post:
|
||||
tags: [Exec]
|
||||
summary: Programmatic code execution (sibling of /v1/exec, same executor contract)
|
||||
operationId: execProgrammatic
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ExecRequest' }
|
||||
responses:
|
||||
'200': { description: Execution result, content: { application/json: { schema: { $ref: '#/components/schemas/ExecResult' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
/v1/upload:
|
||||
post:
|
||||
tags: [Files]
|
||||
summary: Upload a file into a session
|
||||
operationId: uploadFile
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file: { type: string, format: binary }
|
||||
session_id: { type: string }
|
||||
responses:
|
||||
'200': { description: Uploaded, content: { application/json: { schema: { type: object, properties: { session_id: { type: string }, files: { type: array, items: { $ref: '#/components/schemas/ExecFile' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
/v1/download/{id}:
|
||||
get:
|
||||
tags: [Files]
|
||||
summary: Download a produced file by id
|
||||
operationId: downloadFile
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: File contents, content: { application/octet-stream: { schema: { type: string, format: binary } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
/v1/files/{session_id}:
|
||||
get:
|
||||
tags: [Files]
|
||||
summary: List the files produced in a session
|
||||
operationId: listSessionFiles
|
||||
parameters:
|
||||
- { name: session_id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Files, content: { application/json: { schema: { type: object, properties: { files: { type: array, items: { $ref: '#/components/schemas/ExecFile' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
serviceKey:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
description: Shared service key (KMS-sourced) presented server-side by the chat node process. Not a user JWT.
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: integer }, error: { type: string } } }
|
||||
NotConfigured:
|
||||
description: Code execution not configured on this deployment (service key unset — fail-closed)
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: integer }, error: { type: string } } }
|
||||
schemas:
|
||||
ExecRequest:
|
||||
type: object
|
||||
required: [lang, code]
|
||||
properties:
|
||||
lang: { type: string, description: Language / runtime (e.g. python, node) }
|
||||
code: { type: string }
|
||||
files: { type: array, description: Optional input files, items: { $ref: '#/components/schemas/ExecFile' } }
|
||||
ExecFile:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
ExecResult:
|
||||
type: object
|
||||
properties:
|
||||
session_id: { type: string }
|
||||
stdout: { type: string }
|
||||
stderr: { type: string }
|
||||
files: { type: array, items: { $ref: '#/components/schemas/ExecFile' } }
|
||||
@@ -0,0 +1,392 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Framework API
|
||||
description: |
|
||||
Metadata-driven DocType engine — the `/v1/framework/*` surface served by the
|
||||
cloud `framework` subsystem (`hanzoai/cloud/clients/framework`). A native-Go,
|
||||
Base/SQLite successor to Frappe's DocType/metadata core: define a DocType
|
||||
(fields, permissions, lifecycle), then CRUD schemaless documents against it
|
||||
through ONE generic surface. CMS content-types, ERP DocTypes, Helpdesk, and
|
||||
the KB knowledge model are all "just DocTypes" installed as modules.
|
||||
|
||||
- Register a DocType and its fields, roles, and per-role permissions.
|
||||
- Create/read/update/delete/submit/cancel documents of any DocType.
|
||||
- Assign and revoke per-org roles; install app-lane module fixtures.
|
||||
|
||||
Everything is org-scoped: the org is the validated tenant, so one org's
|
||||
schema and data are invisible to another.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: DocTypes
|
||||
description: DocType registry (metadata definitions)
|
||||
- name: Documents
|
||||
description: Generic metadata-driven document CRUD + lifecycle
|
||||
- name: Roles
|
||||
description: Per-org role assignments
|
||||
- name: Modules
|
||||
description: App-lane module fixtures (install a content model into an org)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/framework/summary:
|
||||
get:
|
||||
tags: [DocTypes]
|
||||
summary: Org summary (doctype + document counts)
|
||||
operationId: frameworkSummary
|
||||
responses:
|
||||
'200':
|
||||
description: Summary
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { doctypes: { type: integer }, documents: { type: integer } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/framework/doctypes:
|
||||
get:
|
||||
tags: [DocTypes]
|
||||
summary: List DocType definitions
|
||||
operationId: listDocTypes
|
||||
responses:
|
||||
'200': { description: DocType list, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/DocType' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [DocTypes]
|
||||
summary: Define a DocType
|
||||
operationId: createDocType
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/DocType' }
|
||||
responses:
|
||||
'201': { description: DocType created, content: { application/json: { schema: { $ref: '#/components/schemas/DocType' } } } }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/framework/doctypes/{name}:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [DocTypes]
|
||||
summary: Get a DocType definition
|
||||
operationId: getDocType
|
||||
responses:
|
||||
'200': { description: DocType, content: { application/json: { schema: { $ref: '#/components/schemas/DocType' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
put:
|
||||
tags: [DocTypes]
|
||||
summary: Replace a DocType definition
|
||||
operationId: replaceDocType
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/DocType' }
|
||||
responses:
|
||||
'200': { description: DocType replaced, content: { application/json: { schema: { $ref: '#/components/schemas/DocType' } } } }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
delete:
|
||||
tags: [DocTypes]
|
||||
summary: Delete a DocType (and its documents)
|
||||
operationId: deleteDocType
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/v1/framework/roles:
|
||||
get:
|
||||
tags: [Roles]
|
||||
summary: List per-org role assignments
|
||||
operationId: listRoles
|
||||
responses:
|
||||
'200': { description: Role assignments, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Role' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Roles]
|
||||
summary: Assign a role to a user
|
||||
operationId: assignRole
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Role' }
|
||||
responses:
|
||||
'201': { description: Role assigned, content: { application/json: { schema: { $ref: '#/components/schemas/Role' } } } }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/framework/roles/{user}/{role}:
|
||||
parameters:
|
||||
- { name: user, in: path, required: true, schema: { type: string } }
|
||||
- { name: role, in: path, required: true, schema: { type: string } }
|
||||
delete:
|
||||
tags: [Roles]
|
||||
summary: Revoke a role from a user
|
||||
operationId: revokeRole
|
||||
responses:
|
||||
'204': { description: Revoked }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/v1/framework/modules:
|
||||
get:
|
||||
tags: [Modules]
|
||||
summary: List registered app-lane modules
|
||||
operationId: listModules
|
||||
responses:
|
||||
'200':
|
||||
description: Modules
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
module: { type: string }
|
||||
doctypes: { type: array, items: { type: string } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/framework/modules/{module}:
|
||||
parameters:
|
||||
- { name: module, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Modules]
|
||||
summary: Inspect a module and its install state in this org
|
||||
operationId: getModule
|
||||
responses:
|
||||
'200':
|
||||
description: Module detail
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
module: { type: string }
|
||||
doctypes: { type: array, items: { type: string } }
|
||||
installed: { type: array, items: { type: string } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/v1/framework/modules/{module}/install:
|
||||
parameters:
|
||||
- { name: module, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Modules]
|
||||
summary: Install a module's DocType fixtures into this org (idempotent)
|
||||
operationId: installModule
|
||||
responses:
|
||||
'200':
|
||||
description: Install result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
module: { type: string }
|
||||
created: { type: array, items: { type: string } }
|
||||
existing: { type: array, items: { type: string } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/v1/framework/{doctype}:
|
||||
parameters:
|
||||
- { name: doctype, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Documents]
|
||||
summary: List documents of a DocType
|
||||
operationId: listDocuments
|
||||
parameters:
|
||||
- { name: filters, in: query, schema: { type: string }, description: 'JSON object of field→value filters (declared fields, name, or docstatus)' }
|
||||
- { name: fields, in: query, schema: { type: string }, description: 'Comma list or JSON array of field names to project' }
|
||||
- { name: order_by, in: query, schema: { type: string }, description: 'field [asc|desc]' }
|
||||
- { name: limit, in: query, schema: { type: integer, default: 100 } }
|
||||
responses:
|
||||
'200': { description: Documents, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Document' } } } } } } }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
post:
|
||||
tags: [Documents]
|
||||
summary: Create a document
|
||||
operationId: createDocument
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Document' }
|
||||
responses:
|
||||
'201': { description: Document created, content: { application/json: { schema: { $ref: '#/components/schemas/Document' } } } }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/v1/framework/{doctype}/{name}:
|
||||
parameters:
|
||||
- { name: doctype, in: path, required: true, schema: { type: string } }
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Documents]
|
||||
summary: Get a document
|
||||
operationId: getDocument
|
||||
responses:
|
||||
'200': { description: Document, content: { application/json: { schema: { $ref: '#/components/schemas/Document' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
put:
|
||||
tags: [Documents]
|
||||
summary: Update a draft document
|
||||
operationId: updateDocument
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Document' }
|
||||
responses:
|
||||
'200': { description: Document updated, content: { application/json: { schema: { $ref: '#/components/schemas/Document' } } } }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { description: Document is not a draft }
|
||||
delete:
|
||||
tags: [Documents]
|
||||
summary: Delete a document
|
||||
operationId: deleteDocument
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { description: Submitted document must be cancelled first }
|
||||
|
||||
/v1/framework/{doctype}/{name}/submit:
|
||||
parameters:
|
||||
- { name: doctype, in: path, required: true, schema: { type: string } }
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Documents]
|
||||
summary: Submit a document (docstatus 0→1)
|
||||
operationId: submitDocument
|
||||
responses:
|
||||
'200': { description: Document submitted, content: { application/json: { schema: { $ref: '#/components/schemas/Document' } } } }
|
||||
'400': { description: DocType is not submittable }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { description: Illegal docstatus transition }
|
||||
|
||||
/v1/framework/{doctype}/{name}/cancel:
|
||||
parameters:
|
||||
- { name: doctype, in: path, required: true, schema: { type: string } }
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Documents]
|
||||
summary: Cancel a submitted document (docstatus 1→2)
|
||||
operationId: cancelDocument
|
||||
responses:
|
||||
'200': { description: Document cancelled, content: { application/json: { schema: { $ref: '#/components/schemas/Document' } } } }
|
||||
'400': { description: DocType is not submittable }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'409': { description: Illegal docstatus transition }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
BadRequest:
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Fieldtype:
|
||||
type: string
|
||||
description: DocField type (closed set the engine validates).
|
||||
enum: [Data, Int, Float, Currency, Check, Date, Datetime, Text, SmallText, LongText, RichText, Select, Link, Table, Attach, JSON, Password]
|
||||
DocField:
|
||||
type: object
|
||||
required: [fieldname, fieldtype]
|
||||
properties:
|
||||
fieldname: { type: string, description: snake_case identifier }
|
||||
fieldtype: { $ref: '#/components/schemas/Fieldtype' }
|
||||
label: { type: string }
|
||||
reqd: { type: boolean }
|
||||
options: { type: string, description: 'Select → newline choices; Link → target DocType; Table → child DocType' }
|
||||
default: { type: string }
|
||||
unique: { type: boolean }
|
||||
readOnly: { type: boolean }
|
||||
hidden: { type: boolean }
|
||||
inListView: { type: boolean }
|
||||
fetchFrom: { type: string, description: '"link_field.source_field" — auto-populate from a linked document' }
|
||||
DocPerm:
|
||||
type: object
|
||||
required: [role]
|
||||
properties:
|
||||
role: { type: string }
|
||||
read: { type: boolean }
|
||||
write: { type: boolean }
|
||||
create: { type: boolean }
|
||||
delete: { type: boolean }
|
||||
submit: { type: boolean }
|
||||
cancel: { type: boolean }
|
||||
DocType:
|
||||
type: object
|
||||
required: [name, fields]
|
||||
properties:
|
||||
name: { type: string }
|
||||
module: { type: string }
|
||||
isSingle: { type: boolean, description: Exactly one document exists }
|
||||
isSubmittable: { type: boolean, description: Has the submit/cancel lifecycle }
|
||||
autoname: { type: string, description: '"" or hash → random id; "field:x"; "prompt"; or a series pattern e.g. "INV-.YYYY.-.#####"' }
|
||||
titleField: { type: string }
|
||||
fields: { type: array, items: { $ref: '#/components/schemas/DocField' } }
|
||||
permissions: { type: array, items: { $ref: '#/components/schemas/DocPerm' } }
|
||||
createdAt: { type: integer, format: int64 }
|
||||
updatedAt: { type: integer, format: int64 }
|
||||
Role:
|
||||
type: object
|
||||
required: [user, role]
|
||||
properties:
|
||||
user: { type: string }
|
||||
role: { type: string }
|
||||
Document:
|
||||
type: object
|
||||
description: |
|
||||
A schemaless document. Field data is carried at the top level alongside the
|
||||
envelope keys (name/doctype/docstatus/createdAt/updatedAt). Password fields
|
||||
are redacted in responses. On write, only the field data is read.
|
||||
additionalProperties: true
|
||||
properties:
|
||||
name: { type: string }
|
||||
doctype: { type: string }
|
||||
docstatus: { type: integer, enum: [0, 1, 2], description: '0=draft, 1=submitted, 2=cancelled' }
|
||||
createdAt: { type: integer, format: int64 }
|
||||
updatedAt: { type: integer, format: int64 }
|
||||
@@ -0,0 +1,259 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Functions API
|
||||
description: |
|
||||
Org-scoped serverless functions on the unified cloud binary — the
|
||||
`/v1/functions/*` surface served by the cloud `functions` subsystem
|
||||
(`hanzoai/cloud/clients/functions`). Define a function (runtime + code or
|
||||
image + env), invoke it through the sandboxed code-execution runtime (each
|
||||
invocation is real, metered compute against the org's balance), and read
|
||||
its triggers, deployments, secrets, invocations, and metrics. Tenant
|
||||
isolation is enforced server-side from the validated bearer `owner` claim
|
||||
(HIP-0026).
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Functions
|
||||
description: Function definitions and invocations
|
||||
- name: Metrics
|
||||
description: Invocation histograms and status breakdown
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/functions:
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: List functions
|
||||
operationId: listFunctions
|
||||
responses:
|
||||
'200': { description: Functions, content: { application/json: { schema: { type: object, properties: { functions: { type: array, items: { $ref: '#/components/schemas/Function' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Functions]
|
||||
summary: Create a function
|
||||
operationId: createFunction
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreateFunctionRequest' }
|
||||
responses:
|
||||
'201': { description: Function created, content: { application/json: { schema: { $ref: '#/components/schemas/Function' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/functions/metrics:
|
||||
get:
|
||||
tags: [Metrics]
|
||||
summary: Invocation histogram + status breakdown
|
||||
operationId: functionMetrics
|
||||
parameters:
|
||||
- { name: range, in: query, schema: { type: string, enum: ['24H', '7D', '30D'], default: '24H' } }
|
||||
responses:
|
||||
'200': { description: Metrics, content: { application/json: { schema: { $ref: '#/components/schemas/Metrics' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/functions/triggers:
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: List triggers across functions
|
||||
operationId: listTriggers
|
||||
responses:
|
||||
'200': { description: Triggers, content: { application/json: { schema: { type: object, properties: { triggers: { type: array, items: { $ref: '#/components/schemas/Trigger' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/functions/deployments:
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: List deployed functions
|
||||
operationId: listDeployments
|
||||
responses:
|
||||
'200': { description: Deployments, content: { application/json: { schema: { type: object, properties: { functions: { type: array, items: { $ref: '#/components/schemas/Function' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/functions/secrets:
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: List function secrets (names only)
|
||||
operationId: listFunctionSecrets
|
||||
responses:
|
||||
'200': { description: Secrets, content: { application/json: { schema: { type: object, properties: { secrets: { type: array, items: { $ref: '#/components/schemas/Secret' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/functions/{name}:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: Get a function (with triggers, recent invocations, secrets)
|
||||
operationId: getFunction
|
||||
responses:
|
||||
'200': { description: Function detail, content: { application/json: { schema: { $ref: '#/components/schemas/FunctionDetail' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Function not found }
|
||||
delete:
|
||||
tags: [Functions]
|
||||
summary: Delete a function
|
||||
operationId: deleteFunction
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Function not found }
|
||||
|
||||
/v1/functions/{name}/invocations:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: List a function's invocations
|
||||
operationId: listInvocations
|
||||
parameters:
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200': { description: Invocations, content: { application/json: { schema: { type: object, properties: { invocations: { type: array, items: { $ref: '#/components/schemas/Invocation' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Function not found }
|
||||
|
||||
/v1/functions/{name}/logs:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Functions]
|
||||
summary: Get the latest invocation logs
|
||||
operationId: getFunctionLogs
|
||||
responses:
|
||||
'200': { description: Logs, content: { application/json: { schema: { type: object, properties: { logs: { type: string } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Function not found }
|
||||
|
||||
/v1/functions/{name}/invoke:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [Functions]
|
||||
summary: Invoke a function (metered compute)
|
||||
operationId: invokeFunction
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/InvokeRequest' }
|
||||
responses:
|
||||
'200': { description: Invocation result, content: { application/json: { schema: { $ref: '#/components/schemas/Invocation' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'402': { description: Insufficient balance }
|
||||
'404': { description: Function not found }
|
||||
'502': { description: Sandbox execution failed (invocation recorded with error status), content: { application/json: { schema: { $ref: '#/components/schemas/Invocation' } } } }
|
||||
'503': { description: Code execution runtime not configured }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Function:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
namespace: { type: string }
|
||||
environment: { type: string }
|
||||
status: { type: string }
|
||||
image: { type: string }
|
||||
endpoint: { type: string }
|
||||
envCount: { type: integer }
|
||||
timeoutSec: { type: integer }
|
||||
memoryLimit: { type: string }
|
||||
invocations7d: { type: integer, nullable: true }
|
||||
successRate: { type: number, nullable: true }
|
||||
avgDurationMs: { type: number, nullable: true }
|
||||
errors7d: { type: integer, nullable: true }
|
||||
createdAt: { type: string }
|
||||
lastDeployedAt: { type: string }
|
||||
FunctionDetail:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/Function' }
|
||||
- type: object
|
||||
properties:
|
||||
triggers: { type: array, items: { $ref: '#/components/schemas/Trigger' } }
|
||||
recentInvocations: { type: array, items: { $ref: '#/components/schemas/Invocation' } }
|
||||
secrets: { type: array, items: { type: string } }
|
||||
CreateFunctionRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string }
|
||||
environment: { type: string }
|
||||
runtime: { type: string }
|
||||
namespace: { type: string }
|
||||
image: { type: string }
|
||||
code: { type: string }
|
||||
handler: { type: string }
|
||||
timeoutSec: { type: integer }
|
||||
memoryLimit: { type: string }
|
||||
envNames: { type: array, items: { type: string } }
|
||||
Trigger:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
type: { type: string }
|
||||
enabled: { type: boolean }
|
||||
target: { type: string }
|
||||
functionName: { type: string }
|
||||
Invocation:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
statusCode: { type: integer }
|
||||
status: { type: string, enum: [ok, error] }
|
||||
method: { type: string }
|
||||
time: { type: string, description: RFC3339 UTC }
|
||||
durationMs: { type: integer, format: int64 }
|
||||
Secret:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
namespace: { type: string }
|
||||
mountedBy: { type: string }
|
||||
InvokeRequest:
|
||||
type: object
|
||||
properties:
|
||||
input: { type: string }
|
||||
Metrics:
|
||||
type: object
|
||||
properties:
|
||||
series:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key: { type: string }
|
||||
points:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
t: { type: string }
|
||||
v: { type: integer }
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
success: { type: integer }
|
||||
timeout: { type: integer }
|
||||
error: { type: integer }
|
||||
costCents: { type: integer, format: int64, nullable: true }
|
||||
@@ -0,0 +1,192 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Git API
|
||||
description: |
|
||||
S3-backed Git hosting native to the Hanzo Cloud binary — the `/v1/git/*`
|
||||
surface served by `hanzoai/cloud/clients/git`. A repo is the Git layer
|
||||
(source code) scoped under an org (gateway-minted `X-Org-Id`, HIP-0026) and
|
||||
an optional project sub-scope (`X-Project-Id`); tenancy is enforced on every
|
||||
query so one tenant can never read, clone, push to, or delete another's.
|
||||
|
||||
The control plane (`/v1/git/repos`, `/v1/git/usage`) is JSON. The
|
||||
smart-HTTP transport routes (`.../info/refs`, `.../git-upload-pack`,
|
||||
`.../git-receive-pack`) speak the native Git wire protocol so
|
||||
`git clone` / `git fetch` / `git push` work against
|
||||
`https://api.hanzo.ai/v1/git/<org>/<repo>.git`.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Repos
|
||||
description: Repository lifecycle (create, list, detail, delete)
|
||||
- name: Usage
|
||||
description: Per-repo + total storage bytes (metering)
|
||||
- name: Smart HTTP
|
||||
description: Native Git wire protocol (clone / fetch / push)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/git/repos:
|
||||
post:
|
||||
tags: [Repos]
|
||||
summary: Create a bare repo
|
||||
operationId: createRepo
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreateRepo' }
|
||||
responses:
|
||||
'201': { description: Repo created, content: { application/json: { schema: { $ref: '#/components/schemas/Repo' } } } }
|
||||
'400': { description: Invalid name/project/description }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'409': { description: Repo name already exists in this scope }
|
||||
get:
|
||||
tags: [Repos]
|
||||
summary: List the tenant's repos
|
||||
operationId: listRepos
|
||||
responses:
|
||||
'200': { description: Repos, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Repo' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/git/usage:
|
||||
get:
|
||||
tags: [Usage]
|
||||
summary: Per-repo + total storage bytes for the tenant
|
||||
operationId: getGitUsage
|
||||
responses:
|
||||
'200': { description: Usage rollup, content: { application/json: { schema: { $ref: '#/components/schemas/Usage' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/git/repos/{name}:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string }, description: 'Repo name (a trailing ".git" is stripped)' }
|
||||
get:
|
||||
tags: [Repos]
|
||||
summary: Repo detail (branches + resolved HEAD)
|
||||
operationId: getRepo
|
||||
responses:
|
||||
'200': { description: Repo, content: { application/json: { schema: { $ref: '#/components/schemas/Repo' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Repo not found }
|
||||
delete:
|
||||
tags: [Repos]
|
||||
summary: Delete a repo and purge its storage
|
||||
operationId: deleteRepo
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Repo not found }
|
||||
|
||||
/v1/git/{org}/{repo}/info/refs:
|
||||
get:
|
||||
tags: [Smart HTTP]
|
||||
summary: Git smart-HTTP ref advertisement
|
||||
description: The ref-advertisement phase. `?service=` selects git-upload-pack (fetch) or git-receive-pack (push).
|
||||
operationId: gitInfoRefs
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
- { name: repo, in: path, required: true, schema: { type: string } }
|
||||
- { name: service, in: query, required: true, schema: { type: string, enum: [git-upload-pack, git-receive-pack] } }
|
||||
responses:
|
||||
'200':
|
||||
description: pkt-line ref advertisement
|
||||
content:
|
||||
application/x-git-upload-pack-advertisement: { schema: { type: string, format: binary } }
|
||||
application/x-git-receive-pack-advertisement: { schema: { type: string, format: binary } }
|
||||
'400': { description: service must be git-upload-pack or git-receive-pack }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Repo not found }
|
||||
|
||||
/v1/git/{org}/{repo}/git-upload-pack:
|
||||
post:
|
||||
tags: [Smart HTTP]
|
||||
summary: Git upload-pack (clone / fetch)
|
||||
operationId: gitUploadPack
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
- { name: repo, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-git-upload-pack-request: { schema: { type: string, format: binary } }
|
||||
responses:
|
||||
'200': { description: Packfile result, content: { application/x-git-upload-pack-result: { schema: { type: string, format: binary } } } }
|
||||
'400': { description: Malformed upload-pack request }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Repo not found }
|
||||
'413': { description: Request body exceeds 256 MiB }
|
||||
|
||||
/v1/git/{org}/{repo}/git-receive-pack:
|
||||
post:
|
||||
tags: [Smart HTTP]
|
||||
summary: Git receive-pack (push)
|
||||
operationId: gitReceivePack
|
||||
parameters:
|
||||
- { name: org, in: path, required: true, schema: { type: string } }
|
||||
- { name: repo, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-git-receive-pack-request: { schema: { type: string, format: binary } }
|
||||
responses:
|
||||
'200': { description: report-status, content: { application/x-git-receive-pack-result: { schema: { type: string, format: binary } } } }
|
||||
'400': { description: Malformed receive-pack request }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Repo not found }
|
||||
'413': { description: Request body exceeds 256 MiB }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
CreateRepo:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string, description: 'Repo name; must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' }
|
||||
project: { type: string, description: Optional project sub-scope (else the X-Project-Id header) }
|
||||
description: { type: string, maxLength: 4096 }
|
||||
Repo:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
org: { type: string }
|
||||
project: { type: string }
|
||||
name: { type: string }
|
||||
description: { type: string }
|
||||
defaultBranch: { type: string, example: main }
|
||||
branches: { type: array, items: { type: string } }
|
||||
head: { type: string, description: Resolved HEAD commit hash }
|
||||
cloneUrl: { type: string, format: uri }
|
||||
sizeBytes: { type: integer, format: int64 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
UsageRepo:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
project: { type: string }
|
||||
sizeBytes: { type: integer, format: int64 }
|
||||
Usage:
|
||||
type: object
|
||||
properties:
|
||||
org: { type: string }
|
||||
totalBytes: { type: integer, format: int64 }
|
||||
repos: { type: array, items: { $ref: '#/components/schemas/UsageRepo' } }
|
||||
@@ -0,0 +1,87 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Graph API
|
||||
description: |
|
||||
Chain-data surface served by `hanzoai/cloud/clients/graph`: the deployment's
|
||||
blockchain indexing (`/v1/indexers`) and on-chain oracle feeds
|
||||
(`/v1/oracles`), re-shaped as principal-gated REST off the unified cloud
|
||||
binary. It fronts the Lux chain-data plane — `luxfi/indexer` (per-network
|
||||
block/event indexer) and `luxfi/graph` (GraphQL oracle price-feed registry).
|
||||
|
||||
The subsystem owns no chain state; it is a thin translator that never
|
||||
fabricates a row. An unreachable upstream surfaces an honest 502; a
|
||||
reachable-but-empty upstream returns an empty list. Every route requires a
|
||||
validated IAM principal (HIP-0026).
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Indexers
|
||||
description: Per-network chain indexers + status
|
||||
- name: Oracles
|
||||
description: On-chain price/data oracle feeds
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/indexers:
|
||||
get:
|
||||
tags: [Indexers]
|
||||
summary: List the deployment's chain indexer(s)
|
||||
operationId: listIndexers
|
||||
responses:
|
||||
'200': { description: Indexers, content: { application/json: { schema: { type: object, properties: { indexers: { type: array, items: { $ref: '#/components/schemas/Indexer' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'502': { description: Indexer upstream unreachable }
|
||||
|
||||
/v1/oracles:
|
||||
get:
|
||||
tags: [Oracles]
|
||||
summary: List on-chain price/data oracles
|
||||
operationId: listOracles
|
||||
responses:
|
||||
'200': { description: Oracles, content: { application/json: { schema: { type: object, properties: { oracles: { type: array, items: { $ref: '#/components/schemas/Oracle' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'502': { description: Graph upstream unreachable }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Indexer:
|
||||
type: object
|
||||
description: 'One chain indexer row. `lag` is omitted when the chain HEAD is not derivable (never fabricated).'
|
||||
properties:
|
||||
id: { type: string }
|
||||
chain: { type: string }
|
||||
network: { type: string, description: mainnet | testnet | devnet }
|
||||
height: { type: string, description: Latest indexed block height }
|
||||
lag: { type: string }
|
||||
status: { type: string, enum: [active, degraded] }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
Oracle:
|
||||
type: object
|
||||
description: One on-chain price feed.
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string, description: Trading pair (e.g. LUX/USD) }
|
||||
feed: { type: string }
|
||||
value: { type: string, description: Feed price verbatim }
|
||||
source: { type: string, example: O-Chain }
|
||||
status: { type: string, enum: [active] }
|
||||
updatedAt: { type: string, format: date-time }
|
||||
+7976
-3665
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,217 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Integrations API
|
||||
description: |
|
||||
Generic, provider-agnostic OAuth connector plane for Hanzo Cloud — the
|
||||
`/v1/integrations/*` surface served by the cloud `integrations` subsystem
|
||||
(`hanzoai/cloud/clients/integrations`). One framework, N providers: an org
|
||||
connects a third-party account (Slack today; GitHub scaffolded), the callback
|
||||
seals the resulting per-org tokens into KMS custody, and the store keeps only
|
||||
non-secret connection metadata. Also hosts the Slack `@hanzo` agent front-door
|
||||
(events + slash commands) and the per-user account-link browser flow — those
|
||||
legs authenticate INSIDE the handler (HMAC-signed body / `__Host-` cookies +
|
||||
state), not with a bearer token.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Providers
|
||||
description: Connectable third-party providers and per-org connection status
|
||||
- name: OAuth
|
||||
description: OAuth connect/callback/disconnect flow
|
||||
- name: Slack
|
||||
description: Slack agent webhooks and per-user account link
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/integrations:
|
||||
get:
|
||||
tags: [Providers]
|
||||
summary: List providers with this org's connection status
|
||||
operationId: listProviders
|
||||
responses:
|
||||
'200': { description: Providers, content: { application/json: { schema: { type: object, properties: { providers: { type: array, items: { $ref: '#/components/schemas/ProviderView' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/integrations/slack/events:
|
||||
post:
|
||||
tags: [Slack]
|
||||
summary: Slack Events API webhook
|
||||
operationId: slackEvents
|
||||
description: Public at the JWT layer; authenticated inside the handler by HMAC-SHA256 over the raw body with the Slack signing secret. Handles the `url_verification` challenge and routes `app_mention`/`message` events to an on-behalf-of agent run.
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SlackEventEnvelope' }
|
||||
responses:
|
||||
'200': { description: Acknowledged (challenge echo, or empty ack) }
|
||||
|
||||
/v1/integrations/slack/commands:
|
||||
post:
|
||||
tags: [Slack]
|
||||
summary: Slack slash command webhook
|
||||
operationId: slackCommands
|
||||
description: Public at the JWT layer; HMAC-verified inside the handler. Accepts the Slack `application/x-www-form-urlencoded` slash-command payload.
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
command: { type: string }
|
||||
text: { type: string }
|
||||
team_id: { type: string }
|
||||
user_id: { type: string }
|
||||
channel_id: { type: string }
|
||||
response_url: { type: string }
|
||||
responses:
|
||||
'200': { description: Slash command acknowledged }
|
||||
|
||||
/v1/integrations/slack/link:
|
||||
get:
|
||||
tags: [Slack]
|
||||
summary: Begin the per-user account link (leg 1 — Slack sign-in)
|
||||
operationId: slackLinkStart
|
||||
description: Sets a `__Host-` init cookie and redirects to Slack sign-in. State-authed (no bearer).
|
||||
security: []
|
||||
parameters:
|
||||
- { name: state, in: query, required: true, schema: { type: string }, description: Signed, single-use link state }
|
||||
responses:
|
||||
'302': { description: Redirect to Slack sign-in }
|
||||
'400': { description: Invalid or expired link }
|
||||
'503': { description: Account linking not configured }
|
||||
|
||||
/v1/integrations/slack/link/slack:
|
||||
get:
|
||||
tags: [Slack]
|
||||
summary: Slack sign-in callback (leg 2)
|
||||
operationId: slackLinkSlack
|
||||
description: Requires the leg-1 init cookie to match the sign-in state before exchanging the code; sets the `__Host-` link cookie and redirects to hanzo.id OIDC.
|
||||
security: []
|
||||
parameters:
|
||||
- { name: code, in: query, schema: { type: string } }
|
||||
- { name: state, in: query, schema: { type: string } }
|
||||
- { name: error, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'302': { description: Redirect to hanzo.id OIDC authorize }
|
||||
'400': { description: Invalid state, session mismatch, or sign-in failed }
|
||||
'503': { description: Account linking not configured }
|
||||
|
||||
/v1/integrations/slack/link/callback:
|
||||
get:
|
||||
tags: [Slack]
|
||||
summary: hanzo.id OIDC callback — bind Slack↔Hanzo (leg 3)
|
||||
operationId: slackLinkCallback
|
||||
description: Reads the Slack-verified (team,user) from the `__Host-` link cookie, exchanges the OIDC code, and seals the refresh token into the workspace org's KMS namespace. Returns an HTML confirmation page.
|
||||
security: []
|
||||
parameters:
|
||||
- { name: code, in: query, schema: { type: string } }
|
||||
- { name: state, in: query, schema: { type: string } }
|
||||
- { name: error, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Account linked, content: { text/html: { schema: { type: string } } } }
|
||||
'400': { description: Missing/mismatched state or workspace not connected }
|
||||
'503': { description: Secret store unavailable or linking not configured }
|
||||
|
||||
/v1/integrations/{provider}:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string }, description: Provider slug (e.g. slack, github) }
|
||||
get:
|
||||
tags: [Providers]
|
||||
summary: Get one provider with this org's connection status
|
||||
operationId: getProvider
|
||||
responses:
|
||||
'200': { description: Provider, content: { application/json: { schema: { $ref: '#/components/schemas/ProviderView' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Unknown provider }
|
||||
|
||||
/v1/integrations/{provider}/connect:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [OAuth]
|
||||
summary: Begin an OAuth flow (returns the provider authorize URL)
|
||||
operationId: connectProvider
|
||||
responses:
|
||||
'200': { description: Authorize URL, content: { application/json: { schema: { type: object, properties: { authorizeUrl: { type: string } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Unknown provider }
|
||||
'503': { description: Provider not configured, or KMS not ready }
|
||||
|
||||
/v1/integrations/{provider}/callback:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [OAuth]
|
||||
summary: OAuth return — seal tokens and redirect to console
|
||||
operationId: providerCallback
|
||||
description: Public, state-authed. The org comes only from the signed, single-use state. Always 302s back to the console (success or labeled failure).
|
||||
security: []
|
||||
parameters:
|
||||
- { name: state, in: query, required: true, schema: { type: string } }
|
||||
- { name: code, in: query, schema: { type: string } }
|
||||
- { name: error, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'302': { description: Redirect to console (connected or error) }
|
||||
|
||||
/v1/integrations/{provider}/disconnect:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string } }
|
||||
post:
|
||||
tags: [OAuth]
|
||||
summary: Revoke and forget an org's connection (idempotent)
|
||||
operationId: disconnectProvider
|
||||
responses:
|
||||
'200': { description: Disconnected, content: { application/json: { schema: { type: object, properties: { disconnected: { type: boolean } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Unknown provider }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
ProviderView:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
description: { type: string }
|
||||
category: { type: string }
|
||||
available: { type: boolean, description: App credentials configured on this deployment }
|
||||
connected: { type: boolean, description: This org has a connection }
|
||||
connection: { $ref: '#/components/schemas/ConnectionView' }
|
||||
ConnectionView:
|
||||
type: object
|
||||
properties:
|
||||
account: { type: string, description: Human account label (e.g. Slack team name) }
|
||||
externalId: { type: string, description: Provider account id (e.g. Slack team id) }
|
||||
scopes: { type: array, items: { type: string } }
|
||||
connectedAt: { type: string, format: date-time }
|
||||
SlackEventEnvelope:
|
||||
type: object
|
||||
description: Slack Events API outer envelope (subset).
|
||||
properties:
|
||||
type: { type: string, description: 'e.g. url_verification | event_callback' }
|
||||
challenge: { type: string, description: Present on url_verification }
|
||||
team_id: { type: string }
|
||||
event: { type: object, additionalProperties: true }
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo KB API
|
||||
description: |
|
||||
Knowledge Base retrieval + ingestion control-plane — the `/v1/kb/*` surface
|
||||
served by the cloud `kb` subsystem (`hanzoai/cloud/clients/kb`). It is the thin
|
||||
RAG + connector layer on top of the framework DocType store (KB content lives as
|
||||
`kb-page`/`kb-memory`/`kb-source` documents at `/v1/framework/*`) and the per-org
|
||||
vector index.
|
||||
|
||||
- Semantic search over the caller org's own knowledge namespace (the RAG entry
|
||||
point an agent/chat calls) — human wiki + AI memory in one store.
|
||||
- Per-org app connectors (GitHub/Slack/Google/Notion): OAuth in, sync external
|
||||
documents INTO the same store + index.
|
||||
|
||||
Every route is org-scoped to the validated tenant — a caller can only search or
|
||||
connect its own knowledge.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Search
|
||||
description: Org-scoped semantic retrieval (RAG)
|
||||
- name: Connectors
|
||||
description: Per-org app connectors (OAuth ingestion)
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/kb/search:
|
||||
post:
|
||||
tags: [Search]
|
||||
summary: Semantic search over the org's knowledge
|
||||
operationId: kbSearch
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SearchRequest' }
|
||||
responses:
|
||||
'200':
|
||||
description: Hits (an unreachable/disabled index returns an empty degraded result, never a 5xx)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
hits: { type: array, items: { $ref: '#/components/schemas/Hit' } }
|
||||
degraded: { type: boolean, description: true when the index was unreachable and no context was retrieved }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/kb/connectors:
|
||||
get:
|
||||
tags: [Connectors]
|
||||
summary: List this org's connectors and connection state
|
||||
operationId: listConnectors
|
||||
responses:
|
||||
'200':
|
||||
description: Connectors
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { connectors: { type: array, items: { $ref: '#/components/schemas/ConnectorState' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/kb/connectors/catalog:
|
||||
get:
|
||||
tags: [Connectors]
|
||||
summary: List every connectable source (native + long-tail pieces)
|
||||
operationId: listCatalog
|
||||
responses:
|
||||
'200':
|
||||
description: Catalog
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { connectors: { type: array, items: { $ref: '#/components/schemas/CatalogEntry' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/kb/connectors/{provider}/connect:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string, enum: [github, slack, google, notion] } }
|
||||
get:
|
||||
tags: [Connectors]
|
||||
summary: Begin an OAuth connection (returns the provider authorize URL)
|
||||
operationId: connectStart
|
||||
responses:
|
||||
'200':
|
||||
description: Authorize URL for the console to open
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { authorizeUrl: { type: string, format: uri } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'503': { description: Connector not configured on this deployment }
|
||||
|
||||
/v1/kb/connectors/{provider}/callback:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string, enum: [github, slack, google, notion] } }
|
||||
get:
|
||||
tags: [Connectors]
|
||||
summary: OAuth callback (provider redirect — org recovered from signed state)
|
||||
operationId: connectCallback
|
||||
security: []
|
||||
parameters:
|
||||
- { name: code, in: query, schema: { type: string } }
|
||||
- { name: state, in: query, schema: { type: string } }
|
||||
- { name: error, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Connected
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
provider: { type: string }
|
||||
status: { type: string, example: connected }
|
||||
account: { type: string }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'403': { description: Invalid OAuth state }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
/v1/kb/connectors/{provider}/sync:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string, enum: [github, slack, google, notion] } }
|
||||
post:
|
||||
tags: [Connectors]
|
||||
summary: Sync the provider's documents into this org's knowledge store
|
||||
operationId: syncConnector
|
||||
responses:
|
||||
'200':
|
||||
description: Sync result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
provider: { type: string }
|
||||
ingested: { type: integer }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
'502': { description: Sync failed }
|
||||
|
||||
/v1/kb/connectors/{provider}:
|
||||
parameters:
|
||||
- { name: provider, in: path, required: true, schema: { type: string, enum: [github, slack, google, notion] } }
|
||||
delete:
|
||||
tags: [Connectors]
|
||||
summary: Disconnect a connector (tombstone token, purge its vector points)
|
||||
operationId: disconnectConnector
|
||||
responses:
|
||||
'200':
|
||||
description: Disconnected
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
provider: { type: string }
|
||||
status: { type: string, example: disconnected }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { $ref: '#/components/responses/NotFound' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
BadRequest:
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
SearchRequest:
|
||||
type: object
|
||||
required: [query]
|
||||
properties:
|
||||
query: { type: string, description: Natural-language question }
|
||||
limit: { type: integer, default: 10, maximum: 50 }
|
||||
project: { type: string, description: Optional project scope }
|
||||
doctypes:
|
||||
type: array
|
||||
items: { type: string, enum: [kb-page, kb-memory, kb-source] }
|
||||
description: Optional restriction to a subset of indexed knowledge doctypes
|
||||
Hit:
|
||||
type: object
|
||||
properties:
|
||||
doctype: { type: string }
|
||||
name: { type: string }
|
||||
title: { type: string }
|
||||
project: { type: string }
|
||||
provider: { type: string }
|
||||
url: { type: string }
|
||||
score: { type: number, format: double }
|
||||
ConnectorState:
|
||||
type: object
|
||||
properties:
|
||||
provider: { type: string }
|
||||
configured: { type: boolean, description: Deployment has OAuth credentials for it }
|
||||
status: { type: string, enum: [disconnected, connected, syncing, error] }
|
||||
docCount: { type: integer, description: Ingested kb-source documents from this provider }
|
||||
kind: { type: string, enum: [native, piece] }
|
||||
account: { type: string }
|
||||
lastSync: { type: string }
|
||||
error: { type: string }
|
||||
CatalogEntry:
|
||||
type: object
|
||||
properties:
|
||||
provider: { type: string }
|
||||
displayName: { type: string }
|
||||
description: { type: string }
|
||||
kind: { type: string, enum: [native, piece] }
|
||||
configured: { type: boolean }
|
||||
@@ -0,0 +1,186 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Plans API
|
||||
description: |
|
||||
The `@hanzo/plans` catalog mounted into the unified cloud binary under
|
||||
`/v1/plans/*` by `hanzoai/cloud/clients/plan`. A thin Go wrapper loads the
|
||||
`hanzoai/plans` bundle into a goja runtime and dispatches each route through
|
||||
it — the entitlement transforms (fromLegacy / toLicenseFeatures /
|
||||
resolvePlan) run as real JS, not a Go reimplementation.
|
||||
|
||||
The catalog is read-only public content. Every route reads the
|
||||
gateway-minted identity (`X-Org-Id`, HIP-0026) as the tenant so a reseller
|
||||
org sees its own catalog overrides; an anonymous or forged org falls back to
|
||||
the public `hanzo` default. Responses are the bundle's catalog JSON.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Plans
|
||||
description: Plan catalog + entitlement resolution
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/plans/health:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Health check
|
||||
operationId: plansHealth
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: Service healthy, content: { application/json: { schema: { type: object, properties: { status: { type: string, example: ok }, service: { type: string, example: plans } } } } } }
|
||||
|
||||
/v1/plans:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: The full plan catalog
|
||||
operationId: listPlans
|
||||
responses:
|
||||
'200': { description: Plan catalog, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/subscriptions:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Subscription plans
|
||||
operationId: listSubscriptionPlans
|
||||
responses:
|
||||
'200': { description: Subscription plans, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/cloud:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Cloud plans
|
||||
operationId: listCloudPlans
|
||||
responses:
|
||||
'200': { description: Cloud plans, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/blockchain:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Blockchain plans
|
||||
operationId: listBlockchainPlans
|
||||
responses:
|
||||
'200': { description: Blockchain plans, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/dns:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: DNS plans
|
||||
operationId: listDnsPlans
|
||||
responses:
|
||||
'200': { description: DNS plans, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/gpu:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: GPU plans
|
||||
operationId: listGpuPlans
|
||||
responses:
|
||||
'200': { description: GPU plans, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/regions:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Regions
|
||||
operationId: listPlanRegions
|
||||
responses:
|
||||
'200': { description: Regions, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/storage:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Storage plans
|
||||
operationId: listStoragePlans
|
||||
responses:
|
||||
'200': { description: Storage plans, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/tools:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Tools catalog
|
||||
operationId: listPlanTools
|
||||
responses:
|
||||
'200': { description: Tools, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/policy:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Plan policy
|
||||
operationId: getPlanPolicy
|
||||
responses:
|
||||
'200': { description: Policy, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/schema:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Entitlement schema
|
||||
operationId: getPlanSchema
|
||||
responses:
|
||||
'200': { description: Schema, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/vocab:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Entitlement vocabulary
|
||||
operationId: getPlanVocab
|
||||
responses:
|
||||
'200': { description: Vocabulary, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/resolve/{id}:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Resolve a plan by id
|
||||
operationId: resolvePlan
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string }, description: Plan id }
|
||||
responses:
|
||||
'200': { description: Resolved plan, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/plans/entitlements/{id}:
|
||||
get:
|
||||
tags: [Plans]
|
||||
summary: Entitlements for a plan
|
||||
operationId: getPlanEntitlements
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string }, description: Plan id }
|
||||
responses:
|
||||
'200': { description: Entitlements, content: { application/json: { schema: { $ref: '#/components/schemas/Catalog' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Catalog:
|
||||
type: object
|
||||
description: Opaque plan-catalog JSON emitted by the @hanzo/plans bundle (shape varies per route).
|
||||
additionalProperties: true
|
||||
@@ -0,0 +1,156 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Prompts API
|
||||
description: |
|
||||
Per-org, versioned prompt library for Hanzo Cloud — the `/v1/prompts`
|
||||
surface served by the cloud `prompts` subsystem
|
||||
(`hanzoai/cloud/clients/prompts`). Every prompt belongs to exactly one org
|
||||
(the validated bearer owner claim, HIP-0026); tenant isolation is enforced on
|
||||
every query. Creating a prompt whose name already exists appends a new
|
||||
version — real, inspectable history. The store holds template text + taxonomy
|
||||
only, never a secret.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Prompts
|
||||
description: Named, versioned prompt templates
|
||||
- name: Catalog
|
||||
description: Read-only Hanzo starter prompt library
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/prompts:
|
||||
get:
|
||||
tags: [Prompts]
|
||||
summary: List current prompts for the org
|
||||
operationId: listPrompts
|
||||
responses:
|
||||
'200': { description: Prompts, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/PromptMeta' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Prompts]
|
||||
summary: Create a prompt or append a new version
|
||||
operationId: createPrompt
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreatePrompt' }
|
||||
responses:
|
||||
'201': { description: Prompt detail (post-write), content: { application/json: { schema: { $ref: '#/components/schemas/PromptDetail' } } } }
|
||||
'400': { description: Invalid name / content too large }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/prompts/metrics:
|
||||
get:
|
||||
tags: [Prompts]
|
||||
summary: Real per-prompt statistics
|
||||
operationId: promptMetrics
|
||||
responses:
|
||||
'200': { description: Metrics, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/PromptMetric' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/prompts/catalog:
|
||||
get:
|
||||
tags: [Catalog]
|
||||
summary: Read-only starter prompt library
|
||||
operationId: promptCatalog
|
||||
responses:
|
||||
'200': { description: Catalog, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/CatalogEntry' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/prompts/{name}:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$' } }
|
||||
get:
|
||||
tags: [Prompts]
|
||||
summary: Prompt detail + version history
|
||||
operationId: getPrompt
|
||||
responses:
|
||||
'200': { description: Prompt detail, content: { application/json: { schema: { $ref: '#/components/schemas/PromptDetail' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Prompt not found }
|
||||
delete:
|
||||
tags: [Prompts]
|
||||
summary: Delete a prompt and its version history
|
||||
operationId: deletePrompt
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Prompt not found }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
CreatePrompt:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string, description: 'Org-unique handle, ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ (not "metrics"/"new"/"catalog")' }
|
||||
type: { type: string, default: text }
|
||||
prompt: { type: string, description: Template body (max 64KiB) }
|
||||
labels: { type: array, items: { type: string } }
|
||||
tags: { type: array, items: { type: string } }
|
||||
PromptMeta:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string }
|
||||
versions: { type: array, items: { type: integer }, description: Version numbers, newest first }
|
||||
labels: { type: array, items: { type: string } }
|
||||
tags: { type: array, items: { type: string } }
|
||||
lastUpdatedAt: { type: string, format: date-time }
|
||||
VersionMeta:
|
||||
type: object
|
||||
description: Version history metadata only — no per-version content.
|
||||
properties:
|
||||
version: { type: integer }
|
||||
type: { type: string }
|
||||
createdAt: { type: string, format: date-time }
|
||||
PromptDetail:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string }
|
||||
prompt: { type: string, description: Current version content }
|
||||
version: { type: integer, description: Current version number }
|
||||
labels: { type: array, items: { type: string } }
|
||||
tags: { type: array, items: { type: string } }
|
||||
versionHistory: { type: array, items: { $ref: '#/components/schemas/VersionMeta' } }
|
||||
createdAt: { type: string, format: date-time }
|
||||
lastUpdatedAt: { type: string, format: date-time }
|
||||
PromptMetric:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string }
|
||||
versions: { type: integer, description: True version count }
|
||||
currentVersion: { type: integer }
|
||||
createdAt: { type: string, format: date-time }
|
||||
lastUpdatedAt: { type: string, format: date-time }
|
||||
CatalogEntry:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string }
|
||||
type: { type: string }
|
||||
prompt: { type: string }
|
||||
tags: { type: array, items: { type: string } }
|
||||
labels: { type: array, items: { type: string } }
|
||||
@@ -0,0 +1,166 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Security API
|
||||
description: |
|
||||
Secret / vulnerability scanning (`/v1/security/*`) served by the cloud
|
||||
`security` subsystem (`hanzoai/cloud/clients/security`). Submit source files,
|
||||
the engine runs its detection ruleset over them in memory (raw content is
|
||||
never stored), and persists a per-org scan summary plus redacted findings.
|
||||
Every route is tenant-scoped to the caller's validated org.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Scans
|
||||
description: Submit and read scans
|
||||
- name: Findings
|
||||
description: Redacted findings
|
||||
- name: Rules
|
||||
description: Detection ruleset
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/security/health:
|
||||
get:
|
||||
tags: [Rules]
|
||||
summary: Health check
|
||||
operationId: securityHealth
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: Service healthy, content: { application/json: { schema: { type: object, properties: { status: { type: string, example: ok }, rules: { type: integer } } } } } }
|
||||
|
||||
/v1/security/rules:
|
||||
get:
|
||||
tags: [Rules]
|
||||
summary: List the detection ruleset
|
||||
operationId: securityListRules
|
||||
responses:
|
||||
'200': { description: Rules, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Rule' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/security/scans:
|
||||
post:
|
||||
tags: [Scans]
|
||||
summary: Submit a scan
|
||||
operationId: securitySubmitScan
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ScanRequest' }
|
||||
responses:
|
||||
'201': { description: Scan summary, content: { application/json: { schema: { $ref: '#/components/schemas/Scan' } } } }
|
||||
'400': { description: Missing files, too many files, or scan too large }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
get:
|
||||
tags: [Scans]
|
||||
summary: List scans
|
||||
operationId: securityListScans
|
||||
parameters:
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200': { description: Scans, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Scan' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/security/findings:
|
||||
get:
|
||||
tags: [Findings]
|
||||
summary: List findings
|
||||
operationId: securityListFindings
|
||||
parameters:
|
||||
- { name: scanId, in: query, schema: { type: string } }
|
||||
- { name: minSeverity, in: query, schema: { type: string, enum: [critical, high, medium, low] } }
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200': { description: Findings, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Finding' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/security/scans/{id}:
|
||||
get:
|
||||
tags: [Scans]
|
||||
summary: Get a scan with its findings
|
||||
operationId: securityGetScan
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Scan detail, content: { application/json: { schema: { type: object, properties: { scan: { $ref: '#/components/schemas/Scan' }, findings: { type: array, items: { $ref: '#/components/schemas/Finding' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Scan not found }
|
||||
|
||||
/v1/security/findings/{id}:
|
||||
get:
|
||||
tags: [Findings]
|
||||
summary: Get a finding
|
||||
operationId: securityGetFinding
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200': { description: Finding, content: { application/json: { schema: { $ref: '#/components/schemas/Finding' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Finding not found }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Rule:
|
||||
type: object
|
||||
description: One detection rule in the catalog.
|
||||
properties:
|
||||
ID: { type: string }
|
||||
Name: { type: string }
|
||||
Severity: { type: string, enum: [critical, high, medium, low] }
|
||||
Description: { type: string }
|
||||
FileInput:
|
||||
type: object
|
||||
required: [path, content]
|
||||
properties:
|
||||
path: { type: string }
|
||||
content: { type: string }
|
||||
ScanRequest:
|
||||
type: object
|
||||
required: [files]
|
||||
properties:
|
||||
project: { type: string, description: 'Optional sub-scope, ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' }
|
||||
files: { type: array, description: 'At least one file; max 500 files / 8 MiB total', items: { $ref: '#/components/schemas/FileInput' } }
|
||||
Scan:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
project: { type: string }
|
||||
files: { type: integer }
|
||||
findings: { type: integer }
|
||||
critical: { type: integer }
|
||||
high: { type: integer }
|
||||
medium: { type: integer }
|
||||
low: { type: integer }
|
||||
createdAt: { type: integer, format: int64, description: Unix milliseconds }
|
||||
Finding:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
scanId: { type: string }
|
||||
ruleId: { type: string }
|
||||
ruleName: { type: string }
|
||||
severity: { type: string, enum: [critical, high, medium, low] }
|
||||
path: { type: string }
|
||||
line: { type: integer }
|
||||
preview: { type: string, description: Redacted preview — never the raw secret }
|
||||
fingerprint: { type: string }
|
||||
createdAt: { type: integer, format: int64, description: Unix milliseconds }
|
||||
@@ -0,0 +1,137 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Tasks API
|
||||
description: |
|
||||
Hanzo Tasks — the durable workflow/activity engine (event-sourced,
|
||||
exactly-once, crash-recovering) mounted natively on the unified cloud binary
|
||||
(`hanzoai/cloud/clients/tasksvc`), per HIP-0106. The subsystem mounts the ONE
|
||||
shared in-process engine's HTTP surface under `/v1/tasks/*`; the Tasks
|
||||
product reads the same durable state as the AI durable-ingest path.
|
||||
|
||||
Capability/probe routes (`/settings`, `/cluster`) are open; the data surface
|
||||
(the engine JSON API, `/mcp`, `/events`) is identity-gated — a request
|
||||
without a gateway-validated principal (X-User-Id) is refused (403). The
|
||||
engine is resolved lazily, so requests before it is live return 503.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Tasks
|
||||
description: Durable workflow/activity engine JSON API
|
||||
- name: System
|
||||
description: Liveness, settings and cluster probes
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/tasks/health:
|
||||
get:
|
||||
tags: [System]
|
||||
summary: Liveness probe
|
||||
operationId: tasksHealth
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: Service healthy, content: { application/json: { schema: { type: object, properties: { status: { type: string, example: ok } } } } } }
|
||||
|
||||
/v1/tasks/settings:
|
||||
get:
|
||||
tags: [System]
|
||||
summary: Capability flags (open bootstrap)
|
||||
operationId: tasksSettings
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: Capability flags, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
|
||||
/v1/tasks/cluster:
|
||||
get:
|
||||
tags: [System]
|
||||
summary: Cluster status (open probe)
|
||||
operationId: tasksCluster
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: Cluster status, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
|
||||
/v1/tasks/cluster/health:
|
||||
get:
|
||||
tags: [System]
|
||||
summary: Cluster health (open probe)
|
||||
operationId: tasksClusterHealth
|
||||
security: []
|
||||
responses:
|
||||
'200': { description: Cluster health, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
|
||||
/v1/tasks/mcp:
|
||||
post:
|
||||
tags: [Tasks]
|
||||
summary: Tasks MCP tool surface (JSON-RPC, identity-gated)
|
||||
operationId: tasksMcp
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true, description: JSON-RPC 2.0 request }
|
||||
responses:
|
||||
'200': { description: JSON-RPC response, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
'403': { $ref: '#/components/responses/IdentityRequired' }
|
||||
'503': { $ref: '#/components/responses/EngineNotReady' }
|
||||
|
||||
/v1/tasks/events:
|
||||
get:
|
||||
tags: [Tasks]
|
||||
summary: Realtime event stream (SSE, identity-gated)
|
||||
operationId: tasksEvents
|
||||
responses:
|
||||
'200': { description: Server-sent event stream, content: { text/event-stream: { schema: { type: string } } } }
|
||||
'403': { $ref: '#/components/responses/IdentityRequired' }
|
||||
'503': { $ref: '#/components/responses/EngineNotReady' }
|
||||
|
||||
/v1/tasks/{resource}:
|
||||
get:
|
||||
tags: [Tasks]
|
||||
summary: Engine JSON API (namespaces, workflows, activities, …), identity-gated
|
||||
operationId: tasksGet
|
||||
parameters:
|
||||
- { name: resource, in: path, required: true, schema: { type: string }, description: 'Engine resource path (e.g. namespaces, nexus)' }
|
||||
responses:
|
||||
'200': { description: Engine JSON, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
'403': { $ref: '#/components/responses/IdentityRequired' }
|
||||
'503': { $ref: '#/components/responses/EngineNotReady' }
|
||||
post:
|
||||
tags: [Tasks]
|
||||
summary: Engine JSON API write (identity-gated)
|
||||
operationId: tasksPost
|
||||
parameters:
|
||||
- { name: resource, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
responses:
|
||||
'200': { description: Engine JSON, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
|
||||
'403': { $ref: '#/components/responses/IdentityRequired' }
|
||||
'503': { $ref: '#/components/responses/EngineNotReady' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key. The gateway mints X-User-Id/X-Org-Id from the validated token.
|
||||
responses:
|
||||
IdentityRequired:
|
||||
description: Identity required — no gateway-validated principal on the request
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { error: { type: string }, code: { type: integer, example: 403 } } }
|
||||
EngineNotReady:
|
||||
description: Tasks engine not ready (resolved lazily; retry once it is wired)
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { error: { type: string }, code: { type: integer, example: 503 } } }
|
||||
@@ -0,0 +1,75 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Templates API
|
||||
description: |
|
||||
Read-only starter-kit gallery for Hanzo Cloud — the `/v1/templates` surface
|
||||
served by the cloud `templates` subsystem
|
||||
(`hanzoai/cloud/clients/templates`). Deployable app/site scaffolds (source of
|
||||
truth: `hanzoai/gallery`), vendored so the unified cloud binary ships the
|
||||
catalog with no external dependency. Reference content, not org data — a user
|
||||
forks a template into a project via the deploy flow, not by mutating this
|
||||
catalog.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Templates
|
||||
description: Deployable starter-kit scaffolds
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/templates:
|
||||
get:
|
||||
tags: [Templates]
|
||||
summary: List the starter-kit catalog
|
||||
operationId: listTemplates
|
||||
responses:
|
||||
'200': { description: Catalog, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/Template' } } } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
|
||||
/v1/templates/{slug}:
|
||||
parameters:
|
||||
- { name: slug, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Templates]
|
||||
summary: One template by slug
|
||||
operationId: getTemplate
|
||||
responses:
|
||||
'200': { description: Template, content: { application/json: { schema: { $ref: '#/components/schemas/Template' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Template not found }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
Template:
|
||||
type: object
|
||||
properties:
|
||||
slug: { type: string }
|
||||
title: { type: string }
|
||||
category: { type: string }
|
||||
description: { type: string }
|
||||
framework: { type: string }
|
||||
features: { type: array, items: { type: string } }
|
||||
useCase: { type: string }
|
||||
tier: { type: integer, description: Optional pricing tier }
|
||||
rating: { type: number, format: double, description: Optional gallery rating }
|
||||
source: { type: string, description: Live gallery fork/deploy URL }
|
||||
preview: { type: string, description: Live gallery screenshot/preview URL }
|
||||
@@ -0,0 +1,222 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Tracker API
|
||||
description: |
|
||||
Native-Go, per-org issue tracker (projects + issues) for Hanzo Cloud —
|
||||
the `/v1/tracker/*` surface served by the cloud `tracker` subsystem
|
||||
(`hanzoai/cloud/clients/tracker`). Linear-style projects with an
|
||||
org-unique uppercase key that prefixes every issue identifier
|
||||
(`KEY-<number>`). Tenant isolation is enforced server-side on every
|
||||
request from the validated bearer `owner` claim (HIP-0026) — never a
|
||||
client-supplied header.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Projects
|
||||
description: Org-scoped tracker projects (Linear-style teams)
|
||||
- name: Issues
|
||||
description: Work items inside a project
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/tracker/projects:
|
||||
get:
|
||||
tags: [Projects]
|
||||
summary: List projects
|
||||
operationId: listProjects
|
||||
responses:
|
||||
'200': { description: Projects, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Project' } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
post:
|
||||
tags: [Projects]
|
||||
summary: Create a project
|
||||
operationId: createProject
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreateProjectRequest' }
|
||||
responses:
|
||||
'201': { description: Project created, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'409': { description: Project key already exists in this org }
|
||||
|
||||
/v1/tracker/projects/{key}:
|
||||
parameters:
|
||||
- { name: key, in: path, required: true, schema: { type: string }, description: 'Project key (uppercase, ^[A-Z][A-Z0-9]{1,7}$)' }
|
||||
get:
|
||||
tags: [Projects]
|
||||
summary: Get a project
|
||||
operationId: getProject
|
||||
responses:
|
||||
'200': { description: Project, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Project not found }
|
||||
patch:
|
||||
tags: [Projects]
|
||||
summary: Update a project
|
||||
operationId: updateProject
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/UpdateProjectRequest' }
|
||||
responses:
|
||||
'200': { description: Project updated, content: { application/json: { schema: { $ref: '#/components/schemas/Project' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Project not found }
|
||||
delete:
|
||||
tags: [Projects]
|
||||
summary: Delete a project and all its issues
|
||||
operationId: deleteProject
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Project not found }
|
||||
|
||||
/v1/tracker/projects/{key}/issues:
|
||||
parameters:
|
||||
- { name: key, in: path, required: true, schema: { type: string }, description: Project key }
|
||||
get:
|
||||
tags: [Issues]
|
||||
summary: List issues (board/list)
|
||||
operationId: listIssues
|
||||
parameters:
|
||||
- { name: status, in: query, schema: { $ref: '#/components/schemas/IssueStatus' }, description: Filter to one board column }
|
||||
responses:
|
||||
'200': { description: Issues, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/Issue' } } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Project not found }
|
||||
post:
|
||||
tags: [Issues]
|
||||
summary: Create an issue
|
||||
operationId: createIssue
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/CreateIssueRequest' }
|
||||
responses:
|
||||
'201': { description: Issue created, content: { application/json: { schema: { $ref: '#/components/schemas/Issue' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Project not found }
|
||||
|
||||
/v1/tracker/projects/{key}/issues/{num}:
|
||||
parameters:
|
||||
- { name: key, in: path, required: true, schema: { type: string }, description: Project key }
|
||||
- { name: num, in: path, required: true, schema: { type: integer }, description: Per-project issue number }
|
||||
get:
|
||||
tags: [Issues]
|
||||
summary: Get an issue
|
||||
operationId: getIssue
|
||||
responses:
|
||||
'200': { description: Issue, content: { application/json: { schema: { $ref: '#/components/schemas/Issue' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Issue not found }
|
||||
patch:
|
||||
tags: [Issues]
|
||||
summary: Update an issue
|
||||
operationId: updateIssue
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/UpdateIssueRequest' }
|
||||
responses:
|
||||
'200': { description: Issue updated, content: { application/json: { schema: { $ref: '#/components/schemas/Issue' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Issue not found }
|
||||
delete:
|
||||
tags: [Issues]
|
||||
summary: Delete an issue
|
||||
operationId: deleteIssue
|
||||
responses:
|
||||
'204': { description: Deleted }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'404': { description: Issue not found }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: IAM-issued JWT or `hk-` Cloud API key.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: string }, msg: { type: string } } }
|
||||
schemas:
|
||||
IssueStatus:
|
||||
type: string
|
||||
enum: [backlog, todo, in_progress, done, canceled]
|
||||
description: Board column. Empty defaults to backlog.
|
||||
IssuePriority:
|
||||
type: string
|
||||
enum: [none, urgent, high, medium, low]
|
||||
description: Priority. Empty defaults to none.
|
||||
Project:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
org: { type: string }
|
||||
key: { type: string }
|
||||
name: { type: string }
|
||||
description: { type: string }
|
||||
createdAt: { type: integer, format: int64, description: Unix seconds }
|
||||
updatedAt: { type: integer, format: int64, description: Unix seconds }
|
||||
CreateProjectRequest:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
key: { type: string, description: 'Optional; derived from name when omitted. ^[A-Z][A-Z0-9]{1,7}$' }
|
||||
name: { type: string, maxLength: 256 }
|
||||
description: { type: string }
|
||||
UpdateProjectRequest:
|
||||
type: object
|
||||
properties:
|
||||
name: { type: string, maxLength: 256 }
|
||||
description: { type: string }
|
||||
Issue:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: string }
|
||||
identifier: { type: string, description: 'KEY-<number>, the human handle' }
|
||||
projectKey: { type: string }
|
||||
number: { type: integer }
|
||||
title: { type: string }
|
||||
description: { type: string }
|
||||
status: { $ref: '#/components/schemas/IssueStatus' }
|
||||
priority: { $ref: '#/components/schemas/IssuePriority' }
|
||||
assignee: { type: string }
|
||||
labels: { type: array, items: { type: string } }
|
||||
createdAt: { type: integer, format: int64 }
|
||||
updatedAt: { type: integer, format: int64 }
|
||||
CreateIssueRequest:
|
||||
type: object
|
||||
required: [title]
|
||||
properties:
|
||||
title: { type: string, maxLength: 512 }
|
||||
description: { type: string }
|
||||
status: { $ref: '#/components/schemas/IssueStatus' }
|
||||
priority: { $ref: '#/components/schemas/IssuePriority' }
|
||||
assignee: { type: string }
|
||||
labels: { type: array, items: { type: string } }
|
||||
UpdateIssueRequest:
|
||||
type: object
|
||||
properties:
|
||||
title: { type: string, maxLength: 512 }
|
||||
description: { type: string }
|
||||
status: { $ref: '#/components/schemas/IssueStatus' }
|
||||
priority: { $ref: '#/components/schemas/IssuePriority' }
|
||||
assignee: { type: string }
|
||||
labels: { type: array, items: { type: string } }
|
||||
+352
-1432
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Hanzo Web Search API
|
||||
description: |
|
||||
Hanzo-native Web Search + Scrape on the unified cloud-api /v1 plane
|
||||
(`hanzoai/cloud/clients/websearch`), per HIP-0106 — so chat's web_search
|
||||
agent tool runs entirely on Hanzo infrastructure with NO external SaaS
|
||||
provider. Two contracts under `/v1/websearch`, backed by Hanzo's own
|
||||
services: a SearXNG-shaped search proxied to a Hanzo-operated metasearch
|
||||
instance, and a Firecrawl-shaped scrape backed by Hanzo Crawl.
|
||||
|
||||
AUTH: both surfaces require the shared service key (KMS-sourced) and fail
|
||||
closed — search via the `X-API-Key` header, scrape via `Authorization:
|
||||
Bearer`. An unset key returns 503; a missing/mismatched key returns 401.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Hanzo AI
|
||||
url: https://hanzo.ai
|
||||
email: [email protected]
|
||||
license:
|
||||
name: Proprietary
|
||||
servers:
|
||||
- url: https://api.hanzo.ai
|
||||
description: Hanzo Gateway
|
||||
tags:
|
||||
- name: Search
|
||||
description: SearXNG-shaped metasearch
|
||||
- name: Scrape
|
||||
description: Firecrawl-shaped URL scrape over Hanzo Crawl
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/v1/websearch/search:
|
||||
get:
|
||||
tags: [Search]
|
||||
summary: Search the web (SearXNG JSON contract, proxied verbatim)
|
||||
operationId: webSearch
|
||||
security:
|
||||
- serviceKey: []
|
||||
parameters:
|
||||
- { name: q, in: query, required: true, schema: { type: string }, description: Search query }
|
||||
- { name: format, in: query, required: false, schema: { type: string, enum: [json], default: json } }
|
||||
responses:
|
||||
'200': { description: Search results (SearXNG JSON), content: { application/json: { schema: { $ref: '#/components/schemas/SearchResponse' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
/v1/websearch/v1/scrape:
|
||||
post:
|
||||
tags: [Scrape]
|
||||
summary: Scrape a URL to markdown (Firecrawl response shape)
|
||||
operationId: webScrape
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ScrapeRequest' }
|
||||
responses:
|
||||
'200': { description: 'Scrape result (success may be false with an error; HTTP still 200)', content: { application/json: { schema: { $ref: '#/components/schemas/ScrapeResponse' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
/v1/websearch/scrape:
|
||||
post:
|
||||
tags: [Scrape]
|
||||
summary: Scrape a URL to markdown (bare alias of /v1/websearch/v1/scrape)
|
||||
operationId: webScrapeBare
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ScrapeRequest' }
|
||||
responses:
|
||||
'200': { description: Scrape result, content: { application/json: { schema: { $ref: '#/components/schemas/ScrapeResponse' } } } }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'503': { $ref: '#/components/responses/NotConfigured' }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: 'Shared service key presented as an Authorization Bearer header (scrape). Not a user JWT.'
|
||||
serviceKey:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
description: Shared service key presented on `X-API-Key` (search). Not a user JWT.
|
||||
responses:
|
||||
Unauthorized:
|
||||
description: Missing or invalid credentials
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: integer }, error: { type: string } } }
|
||||
NotConfigured:
|
||||
description: Web search not configured on this deployment (service key unset — fail-closed)
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, properties: { status: { type: integer }, error: { type: string } } }
|
||||
schemas:
|
||||
SearchResponse:
|
||||
type: object
|
||||
description: SearXNG JSON, forwarded verbatim.
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
url: { type: string }
|
||||
title: { type: string }
|
||||
content: { type: string }
|
||||
img_src: { type: string }
|
||||
ScrapeRequest:
|
||||
type: object
|
||||
required: [url]
|
||||
properties:
|
||||
url: { type: string }
|
||||
ScrapeResponse:
|
||||
type: object
|
||||
properties:
|
||||
success: { type: boolean }
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
markdown: { type: string }
|
||||
metadata: { type: object, additionalProperties: true }
|
||||
error: { type: string }
|
||||
+92
-2457
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user