Files
openapi/flows.yaml
T
zooqueen 6b8612eb74 two red gates, neither of them about the artifact
--check asks one question: is hanzo.yaml what its own pinned input projects to?
It answers that at the ref .spec-lock NAMES, so which remote is cloud's main
cannot change the answer — and it was resolving that remote anyway, before
anything read it, and dying there. Measured on main: it exited on `forge/main,
forgeinc/main, inc/main each carry openapi.yaml and have diverged` while the
artifact it was asked about was byte-for-byte correct. The branch is resolved
only where it is used now — `--current`, whose whole question is whether the tip
moved past the pin, and a re-pin with no `--ref` to say where.

The ambiguity itself is real and stays refused. Three remotes carry the document
and none contains the rest; guessing between them is how a projection gets
published from the wrong lineage.

test_flows.py named four operations the document does not have. The keys route
answers under the app that owns it, and the store plane moved again: this file
said /v1/instances/kv, and that address is now 404 with a 404 control, which is
this repo's own definition of refuted. /v1/provisioning/kv answers 403 with a
404 control — served. The notes carried the old probe as if it still held, so
they are re-measured rather than re-worded, and they now say what the bare
/v1/kv actually is: a separate bucket-keyed value plane whose root answers 200
to anyone, which is the one shape a round-trip must not be built on.

Every flow renders into every SDK's examples, so an id here that the document
lacks is a broken example in eight languages.

hanzo.yaml, CAPABILITIES.md and .spec-lock are untouched. The pin stays where it
is: at 22aedc87 the deployment fails to answer for 16 published operations, and
at cloud's main it is 351 — the pin is the newest release api.hanzo.ai answers
for, not the newest release.
2026-08-27 14:39:10 -07:00

178 lines
9.4 KiB
YAML

# The canonical example flows — data only, like sdks.yaml beside it.
#
# Every Hanzo SDK ships the SAME six examples under `examples/<flow>/`, in this
# order, under these names. A reader who knows one language's set can then find
# their way around another's without re-learning what the repo chose to
# demonstrate. This file is what makes "the same six" a fact rather than six
# repos independently remembering to agree.
#
# `operations` are operationIds in hanzo.yaml, in call order. That is the part a
# language cannot fudge: test_flows.py asserts every id here exists in the
# published document, so a route that stops being served breaks THIS gate — one
# place — instead of breaking six SDKs quietly and separately. Since hanzo.yaml
# is now derived from hanzo-inc/cloud's own emission, an id here is a served
# operation by construction, and there is no way to name one that is not.
#
# Existing in the spec is necessary and not sufficient: an id can resolve and
# still 404 on the wire. Every flow below was PROBED at api.hanzo.ai, and where
# the obvious route did not answer, `notes` says what answered instead and how
# the dead one failed.
#
# THE TEST, in two halves, because each half alone has been wrong here once:
#
# • 403 PROVES THE ROUTE EXISTS. Auth middleware runs before the handler, so a
# mounted route refuses (401/403) and an unmounted one is 404/405. That is
# the only reliable liveness signal — a 404 BODY proves nothing, since a
# live handler says "not found" too.
# • PROBE THE METHOD THE DOCUMENT DECLARES, not GET. A POST-only route also
# 404s on GET: POST /v1/mcp answers 200 with 796 tools while GET /v1/mcp is
# 404, and reading that 404 as absence is how the fleet's one MCP endpoint
# stayed undeclared. One verb is never a liveness probe.
#
# And a 200 is not proof either — see `hello`: /v1/ai/account answers 200 with
# type="anonymous-user" to a request carrying no key at all.
#
# Nothing here is executable and no SDK repo duplicates it. What each example
# prints is the language's business; WHICH routes it calls is not.
#
# Rules an example must hold to, in every language:
# • read the key from HANZO_API_KEY;
# • default to https://api.hanzo.ai, overridable by HANZO_BASE_URL;
# • resolve both in ONE shared place per repo, not once per flow;
# • be compiled or imported by that repo's CI gate, so it cannot rot.
flows:
hello:
summary: Identity — prove the key works and print who it belongs to.
# The route must FAIL on a bad key, which is the whole point of the flow, so
# this choice is made by probing api.hanzo.ai rather than by reading the
# document. Three candidates were disqualified for answering 200 to a caller
# with no credential at all — GET /v1/ai/account (type="anonymous-user"),
# GET /v1/iam/whoami and GET /v1/iam/account (both {"status":"error",
# "msg":"please sign in first"}, 200) — and a `hello` built on any of them
# prints a cheerful identity for a key that would 403 everywhere else.
#
# get_models comes FIRST and needs no key: the document now declares a
# `bearer` scheme, applies it to every operation, and exempts exactly four
# with `security: []` — get_models, get_models_providers, get_commands,
# get_openapi.json. So "which routes are open" is a fact an SDK can read
# rather than a probe result recorded in this file, and hello can open with
# a call that SUCCEEDS for a reader who has no credential at all. It is a
# reachability line, not the identity proof; get_account_keys is still that.
operations: [get_models, get_account_keys]
notes: >-
The keys route answers under the app that owns it — /v1/account/keys — and
the bare /v1/keys this file used to name is a 404 today. Re-probed with the
nonsense-sibling control the moment it moved: GET /v1/account/keys answers
403 {"code":"forbidden","detail":"sign in to manage API keys"} with NO key
and with a bogus one, while GET /v1/keys-zzq9 answers 404 — so the 403 is
this route refusing, not a wildcard relay. With a real key it returns the
caller's own keys. An SDK's hello must be the call that says no.
Both halves are what makes the flow readable: an unreachable gateway and a
rejected credential both end in "no", and only the open call separates
them.
This replaces bot_authMe (GET /v1/bot/auth/me), which no longer resolves:
cloud relays all of /v1/bot through one `app.All("/v1/bot/*")`, so the
document has `/v1/bot/{wildcard1}` and no operation named that address. The
old id existed only in bot/openapi.yaml, and a hand-authored id vanishes
with the hand-authored spec — which is exactly why an id here must come
from the served document.
chat:
summary: One completion.
operations: [post_chat_completions]
notes: >-
POST /v1/chat/completions, the gateway's own inference route. The id lost
its `v1` when the default version left the operationId; the address did
not move.
The document states that address and NOT its shape — no requestBody and no
responses — so a generated method takes no prompt and hands back an opaque
body. No language can ship this as an example that runs, and python,
typescript and go each ship five. The flow keeps its place here because
the operation is served: the day cloud's handler declares its In and Out
types, the sixth example returns everywhere at once with nothing left to
decide.
Non-streaming when it does. Streaming is a different transport (SSE) that
the generated clients return as an opaque body, so demonstrating it here
would teach the wrong shape. Print choices[0].message.content.
money:
summary: Balance, then the usage that moved it.
operations: [get_billing_balance, get_billing_usage]
notes: >-
Neither takes an org: both derive the tenant server-side from the JWT
`owner` claim, so a key can only read its own money. Both ids are cloud's
cloud's, and so is the prose: it comes out of the handler's doc comment.
/v1/billing/usage takes `start` and `end`, and both operations carry a
typed 2xx body, so an SDK reads a struct rather than decoding by hand.
Those are cloud's own declarations, from the handlers' `In`/`Out` types —
not a shape anybody wrote here on their behalf.
store:
summary: KV round-trip — create a store, read it back, delete it.
operations: [post_provisioning_kv, get_provisioning_kv_by_name, delete_provisioning_kv_by_name]
notes: >-
The delete belongs in the language's finally/defer, so a failed read still
cleans up instead of leaving the store for the next run to collide with.
This is the provisioning plane — create a KV STORE, read it, drop it — at
its served address, /v1/provisioning/kv.
This file named /v1/instances/kv before, and that address is GONE: probed
at api.hanzo.ai it answers 404 and so does its /v1/instances/kv-zzq9
control, which is this repo's own definition of refuted. The store plane
answers under the app that registers it — GET /v1/provisioning/kv answers
403 {"code":"forbidden","detail":"a validated principal is required"} while
/v1/provisioning/kv-zzq9 answers 404, the existence proof this file
requires (auth runs before the handler, so only a mounted route can
refuse).
The bare /v1/kv spellings are a SEPARATE value plane keyed by bucket
(/v1/kv/{bucket}/{key}); the bare /v1/kv itself answers 200 with a package
descriptor to anyone, which is exactly the shape a round-trip must not be
built on.
agent:
summary: Create an agent, run it, read the run back.
operations:
- post_agents
- post_agents_by_ref_run
- get_agents_by_ref_runs
notes: >-
`ref` accepts the public id (agent_...) OR the org-unique name, which is
why run and read can both use the name just created without waiting for an
id. Names are org-unique, so an example must not hardcode one. The last
step is the RUN list, not the agent read: a run is asynchronous, so the
example polls it until the run it started is terminal.
tools:
summary: List the MCP tools this key can reach.
operations: [get_tools]
notes: >-
GET /v1/tools is the catalogue read: every tool the caller's org and
project can reach, from every source, each flagged with whether it is
activated. Assert the count is greater than zero and print three names.
This flow named `mcp_rpc` (POST /v1/mcp, the JSON-RPC endpoint) until the
document became the only authority on what exists. That endpoint IS live —
POST /v1/mcp answers 200 with the tool list while POST /v1/mcp-zzq9
answers 404, so the address is real and the control refutes nothing — but
cloud's emission does not declare it, so it has no operationId and no
generated method. An example may not reach for an address the document
does not carry: that is a hand-rolled HTTP call inside a generated client,
the exact drift these SDKs exist to prevent. `POST /v1/mcp` returns here
the release cloud declares it (LLM.md, "Handed to hanzo-inc/cloud").
Note for whoever writes that declaration: JSON-RPC reports failure INSIDE a
200, so a client reads `error` before `result` and `result.isError` before
`result.content`. The same endpoint with method=tools/call runs one.