--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.
178 lines
9.4 KiB
YAML
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.
|