18 Commits
Author SHA1 Message Date
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
zeekay 0b132a8881 Name the thing instead of calling it a door
One word stood for several: the MCP server, one address that accepts requests,
the single way in, and the boundary a credential is validated at. Each site now
names its own sense — endpoint, entry point, the MCP server, the edge, address,
relay — or, where none of those fit, takes the word the surrounding code already
uses.

Prose and comments only, 1:1 line for line. Identifiers keep their names: CSS
classes, DOM ids, struct fields, enum variants, config values and the doc
comments that must open with the identifier they document.
2026-08-23 13:49:39 -07:00
zeekay fb3721e91c the vocabulary is the capability — held for the release that serves it
capabilities.yaml groups the 116 capabilities (the apps, HIP-0139) into the
nine domains, replacing the 191 path-segment names; the store journey moves
to its served address (/v1/instances/kv, probed 403-vs-404 against the
-zzq9 control). Inert until the pin moves: the both-ways gate compares this
file to the pinned document, and the pinned release still tags by segment.
The re-derive lands when --served accepts the release that tags by app.
2026-08-20 17:47:28 -07:00
zeekayandHanzo Dev 0df9fb2432 spec sync asks the repository that holds the document
hanzoai/cloud is the re-rooted OSS core: a twelve-commit line sharing no
ancestor with the product and carrying no openapi.yaml at all. It is PUBLIC, so
the workflow's checkout succeeded and publish.py failed one step later with "no
remote of .cloud carries openapi.yaml on `main`" — which the step then threw
away, printing instead its own fixed sentence, that hanzo.yaml had been
hand-edited. It had not: re-derived at its own pinned ref, the committed
artifact is byte-for-byte what that ref projects to. Every push and every hourly
run has reported a drift that never existed.

So the address is the value it always should have been. SPEC_REPO names
hanzo-inc/cloud — where every ref this repo and every SDK has pinned resolves,
and hashes to the digest its lock already carries — and .spec-lock, info.x-spec
and CAPABILITIES.md's header follow it rather than restating it. The receipt was
repointed on its own once and the artifact, the receipt and the test then named
three different repositories; test_the_artifact_names_the_release_it_came_from
now asserts the three AGREE instead of repeating the string a fourth time.

Both canned ::error:: wrappers are gone. A step that replaces a precise
diagnosis with a fixed sentence is worse than a step with no message: publish.py
already says what is wrong and which command fixes it, and `set -e` fails the
step.

hanzo-inc/cloud is private, and the per-job token Hanzo Git mints reads the
run's OWN repository and anything public — nothing else. Measured on
hanzoai/python-sdk run 91449, which named the right repo and still took
`curl: (22) ... 404`. The checkout therefore takes HANZO_GIT_TOKEN, the one name
in this fleet for a credential that reads git.hanzo.ai, and the job says which
credential is missing before actions/checkout can report a private repo as
"repository not found". generate.py's own fetch takes the same name, so one read
needs one credential.

Cloud declares its own `bearer` scheme now, and the projection's addition
stopped adding and started corrupting: setdefault no longer fired while the
document-level `security` it wrote still did, so the published document required
`bearerAuth` and defined nothing by that name. Generated typescript, measured:
2498 bearer call sites from cloud's document, 0 from the projection, leaving 191
files importing a credential helper none of them called. The rule is gone —
cloud says both halves — and test_a_client_can_authenticate refuses a document
that requires a scheme it does not define, whoever supplies it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-19 23:33:15 -07:00
zeekay 0317330d8e hello opens on the route the document declares open
The document now states which operations need the credential: a `bearer`
scheme over everything, exempting four with `security: []`. So "which routes
are open" stopped being a probe result recorded in this file and became a fact
an SDK reads, and `hello` can lead with a call that succeeds for a reader
carrying nothing.

get_models first, get_keys still the identity proof. An unreachable gateway and
a rejected credential both end in "no"; only the open call tells them apart.
2026-08-14 17:30:29 -07:00
zeekay a053643e88 the chat flow names the id the document declares
post_v1_chat_completions lost its v1 when the default version left the
operationId, and test_flows.py has been red on it since — the manifest named an
operation hanzo.yaml does not have, which is exactly the drift that test exists
to catch.

The note also promised something no language can print. The document states the
address and not the shape: no requestBody, no responses, so a generated method
takes no prompt and returns an opaque body. python, typescript and go each ship
five examples. The flow keeps its place because the operation is served — the
sixth example returns everywhere at once the day cloud's handler declares its
In and Out types.
2026-08-14 14:02:29 -07:00
zooqueen 5b35c13cce flows: follow the ids upstream renamed, route for route
cloud@5e2b8eb1 dropped the `v1_` every operationId carried — the segment is in
every path already, so `get_v1_billing_balance` is `get_billing_balance`. The
document the last commit published is that document, so the ten ids flows.yaml
names stopped resolving and test_flows went red: exactly the one place this
manifest exists to fail in, instead of six SDKs' examples failing separately.

Each id was remapped by the route it names, not by its spelling — old id to its
(method, path), that pair to the new id. All ten resolved to the same route and
nothing was dropped, which is the difference between a rename and a removal.
2026-08-12 20:48:18 -07:00
zeekay eb2e3bd370 re-pin to cloud main: the published document was 82 paths behind what is served
The pin sat at v1.801.476 while the deployment moved on, so hanzo.yaml — the
file every SDK, the docs site and the skills catalog regenerate from — described
1680 paths against 1782 served. Client methods for real endpoints did not exist,
and methods for seven retired ones did. One of the missing was
/v1/audio/transcriptions, which is part of the PUBLIC contract: a product we
publish had no SDK method.

Three capabilities the document now carries had nobody to place them, and
publish.py refused the run rather than default them into a bucket — which is the
gate doing its job. Placed by what the operations are, not by what the names
sound like:

  sandboxes (15 ops: /fs, /terminal, lease, delete) -> intelligence, beside exec
            and code. A sandbox is the workspace those two are given; filing it
            under compute would separate the room from the thing standing in it.
  coding    (POST /v1/coding) -> intelligence, same family.
  tags      (GET /v1/tags) -> streams, beside destinations. /v1/tags is
            destinations' BROWSER half — the read track.js makes on every page
            load — which is the same fact that put /v1/tags on the destinations
            row in cloud's manifest.

Verified with this repo's own two gates rather than by eye:

  publish.py --check   the file IS the projection of cloud@35543313, no hand edit
  publish.py --served  all 2429 published operations are answered by the live
                       deployment; the 23 served-and-unpublished are the compat
                       aliases the projection drops on purpose

So every method the SDKs are about to generate can be called, and none of them
describes a route the binary does not serve.
2026-08-07 05:42:09 -07:00
zeekayandHanzo Dev a96449bcc6 publish: the document is generated, so a route nobody serves cannot be described
hanzo.yaml stops being where the API is DECIDED and becomes where hanzoai/cloud's
emission is PUBLISHED. `publish.py` derives it from `cloud@<ref>:openapi.yaml`;
`publish.py --check` regenerates and diffs. An operation no code declares is now
structurally impossible to publish, in this repo and in everything downstream.

MEASURED at hanzoai/[email protected], both documents loaded and keyed by
(METHOD, path): the hand-merged master carried 2093 operations, cloud's emission
2333, shared 1908. So the master described 185 operations nothing serves under
that name and MISSED 425 that are served — 8% invented, 18% blind — while being
the input to seven SDKs, the doc site, the MCP catalogue and the agent-skills
plane, which has no liveness filter at all.

THE 185, each probed with its OWN method and a nonsense-sibling CONTROL:
36 ABSENT (real 404, control 404), 44 unfalsifiable behind a prefix auth gate,
66 absorbed by a {wildcardN} relay door, 39 SERVED and undescribed. A 404 on both
sides IS a refutation and is the only way absence can ever be shown; the control
exists to stop a 401/403/200 being read as existence. The 39 are hanzoai/cloud's
to fix and every one is itemised in LLM.md.

DELETED: 52 `<svc>/openapi.yaml`, shared/, cloud/, generated/, merge.py, sync.py,
audit.py, test_audit.py, test_placement.py — and 14 Swagger 2.0 documents for
`api.hanzo.io`, which is a parked domain: `/account`, `/store` and the control
`/account-zzq9` all return the same registrar lander, and HTTPS has no
certificate for the name.

THE PROJECTION IS SIX RULES AND NOTHING ELSE — no operation added, none removed
that is served, no prose written. It exists because openapi-generator 7.14.0
refuses cloud's emission with 1012 errors (one per route published with an
address and no `responses`) and writes ZERO files in every language; against
hanzo.yaml it reports 0 errors and go/python/typescript-axios/kotlin all emit.
Every rule is a candidate to move upstream into cloud's emitter, and rule 5 alone
is 90% of the file.

BLOCKING, and new with this release rather than with the projection: three o11y
schemas cannot be projected into Go. `o11y.GettableAgentCheckIn` declares
`integration_config` AND `integrationConfig`, `removed_at` AND `removedAt`;
`o11y.O11yPodOnboarding` has `hasClusterName` beside `clusterName`;
`o11y.PostableProfile` the same. `go build ./...` fails on those three and on
NOTHING else — renaming the keys in a scratch copy and regenerating gives exit 0.
This repo must not rename a field, because a field name is the wire, so
test_publish.py pins them as a ceiling instead.

BREAKING: operationIds lose the `<svc>_` prefix merge.py applied — it existed to
keep 52 authored specs from colliding and there is exactly ONE collision in 2284
operations now. Two flows.yaml journeys moved with the hand-authored ids that
vanished: `hello` is get_v1_keys (403 with no key AND with a bogus one, control
404), `tools` is get_v1_tools.

capabilities.yaml keeps one job — which DOMAIN a capability is shown under — and
is gated both ways: a served capability it does not group fails the publish, and
a name it groups that the document does not carry fails too.

generate.py's fallback to this checkout's hanzo.yaml returns, superseding
3953d66's removal of it. That removal was right about authority and wrong about
consequence: pointed at cloud's raw emission the four clients generate nothing.
The file it refused to fall back to no longer exists — this one is derived.

  publish.py --check           hanzo.yaml is the projection of [email protected]
  pytest (16 tests)            passed
  openapi-generator validate   0 errors (was 1012 against the raw emission)
  go build ./... on the client exit 0, with the three o11y schemas patched

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 20:10:20 -07:00
zeekayandHanzo Dev a2a49658e5 placement: one product, one owning spec — and 31 paths nothing serves
capabilities.yaml states the law and nothing enforced it. Measured against
api.hanzo.ai: 103 authored paths outside their owner's prefix, and four
products (kv, s3, search, vector) described by TWO specs at once.

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

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

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

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

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

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 10:29:43 -07:00
zeekayandHanzo Dev a758c61361 merge: parameters union too, and the comment now says what the code does
Two findings, and the first is the uncomfortable one.

THE RULE I DOCUMENTED WAS NOT THE RULE THE CODE RAN. The previous commit's
comment declared that query parameters were deliberately dropped while bodies
and responses were kept. Reading the published document back showed
GET /v1/kms/secrets still carrying `path` and `env`: the overlay starts from the
authored operation and replaces only what cloud populates, so it had never
dropped them. A rule stated in a comment that the code does not implement is
worse than no rule — it is the thing the next reader trusts instead of reading.

The claim was also wrong on the merits, and the four routes that DID lose query
parameters show why. Cloud declaring ONLY the path parameters is not a typed
struct saying "no query parameters exist" — the weave derives those from the
route template, so `[{id}]` is what an untyped route emits whether it accepts
twenty query parameters or none. Treating that as a complete list is the same
silence-for-emptiness error as before, one level down, and it cost
GET /v1/integrations/{provider}/callback its `code` and `state` — the entire
OAuth handshake — while a sibling route with no path template kept everything.
Same evidence, opposite outcome, decided by whether the URL had a brace in it.

So parameters union by name, cloud's definition winning any name it defines.

The union then needed two corrections that the gate could not supply:

- a `$ref` parameter has no `name`, so comparing on the missing key made eight
  /v1/admin/** routes declare `{id}` twice. openapi-generator caught that one.
- uniqueness spans BOTH levels — a path item's parameters apply to every
  operation under it — and the two sides disagree about where a path parameter
  belongs: authored specs hoist it to the item, the weave emits it per
  operation. 115 operations declared `{id}` twice. The validator does NOT
  resolve $ref parameters, so it reported 0 errors on an invalid document.
  Green and wrong simultaneously, which is why this is measured here and not
  left to the gate.

Verified against d4c7dbd: 0 parameters gone, 0 request bodies gone, 0 duplicate
(name, in) pairs — against 0 duplicates before the resync, so the document is
back to being clean by that measure rather than merely no worse.

The build counter is one honest number now: 234 authored shapes still
load-bearing, meaning bodies, responses and parameters cloud took a route
without declaring. It falls to 0 as the typing lane converts those routes, since
a typed zip.Get[In, Out] carries all three from the code itself. The old
`dropped` half is gone with the `inputs()` helper that computed it — it measured
a difference the overlay no longer permits, and printing a number that is always
zero teaches nothing.

Bucket (a), for the typing lane: all 47 body losses are routes cloud serves
UNTYPED, zero are typed-with-no-body, so there is no emission bug to chase. The
list is in LLM.md, highest value first — POST /v1/authz/check,
POST /v1/agents/{ref}/run, the five agent-session controls, POST /v1/kms/secrets,
POST /v1/exec, POST /v1/upload — because no body means no call.

Gate: validate 0 errors 0 warnings (240 recommendations); go generates and
compiles; typescript-axios and java both generate; flows/skills/audit green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 10:32:46 -07:00
zeekayandHanzo Dev 2577c27834 spec: measure the parameters the handover drops; rust leaves the matrix
/v1/billing/usage lost `start` and `end` when cloud's document took the route,
so flows.yaml was telling every SDK to ask for a window no generated method can
express. Measured across all 375 handover routes rather than fixed one at a
time: 28 routes lost 53 parameters, and EVERY ONE IS A QUERY PARAMETER — zero
path parameters were lost.

That asymmetry is the mechanism, not a coincidence. A path parameter is
structural, so the weave reads it off the route template; a query parameter is
only knowable from a typed In struct, and an untyped route has none to give. 27
of the 28 routes are untyped in cloud (route-derived operationId, no responses)
— the same routes as the response-schema gap, one cause with two symptoms. The
28th is its own small bug and is called out as such: GET /v1/ml/models IS typed,
and its In struct is simply missing `stage` and `search`.

The rule is do not put them back. A parameter re-authored here is a claim that a
handler which accepts nothing will honour it, and a generated method that takes
start and end and silently drops them is a lie the caller cannot see — the same
invention the `default` response exists to refuse. They belong in cloud's typed
input, where sync.py will pick them up. LLM.md carries the full list as the
handover; three OAuth callbacks lost `code` and `state`, and /v1/o11y/vm/query
lost `query`, which are worse than a missing filter.

merge.py prints the count on every build so it cannot drift back unseen. It
observes only: hanzo.yaml is byte-identical across this change.

sdks.yaml: rust deleted, and the boundary written down so it is a rule rather
than a mood. A row exists while the WHOLE invocation is expressible as data; a
language leaves when it needs something that is not. rust needs a
reqwest/api.mustache override for 14 operations whose binary body is OPTIONAL —
Option<Vec<u8>>, which no type mapping reaches, because the problem is the
Option — and a template is a FILE that must sit beside the invocation, as must
the --type-mappings=file=Vec<u8> it works with. The row also carried
useSingleRequestParameter (changes every call signature), packageVersion 8.0.0
(the spec's version; the crate shipped 0.1.0) and crates/hanzo-cloud, which
hanzo-rs/sdk does not have. It could not have produced a compiling client.

The other five rows stay, and the same test is why: hanzoai/js-sdk and
hanzoai/java-sdk carry NO flags in their own scripts — they exec generate.py —
and js-sdk deleted its copy only after the two disagreed about modelPackage and
about src/ vs src/cloud/, which built an orphan second copy of all 2143 files.
A row here AND flags there is the lie waiting to happen. A row here and a bare
call site there is one declaration. Stripping those five to registry-only would
recreate the exact drift js-sdk already paid for.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 09:54:30 -07:00
zeekayandHanzo Dev 4551c778c2 sdks: two rows pointed at a client that does not exist; go cannot be a row here
Both were verified against a LOCAL checkout, and both local checkouts are behind
their canonical remotes, so neither row would have updated a client — each would
have written a SECOND one beside the shipped one.

`rust` said crates/hanzo-cloud; hanzo-rs/sdk ships crates/hanzo-client
("Generated client for the Hanzo API — every service, one crate") and has no
hanzo-cloud. Corrected.

`go` said take {.: cloud} with packageName cloud; hanzo-go/sdk ships `package
hanzoai` at the MODULE ROOT and has no cloud/ directory. Deleted rather than
corrected, because the mismatch is structural: every `take` is a promise that
the generator OWNS a directory — sdk() rmtree's it and copies the fresh tree in
— and there is no such directory when the client is the module root beside
go.mod, LICENSE and .git. `take: {.: .}` would delete the repository. Go
regenerates from its own scripts/generate.sh instead: same generator, same
7.14.0 pin, same hanzo.yaml pulled from here, with the private-repo token
fallback this repo documented. That is the pull model working, not a gap.

Three SDK repos answer through a rename redirect, which is exactly how a stale
name keeps resolving and hides the move: hanzoai/go-sdk → hanzo-go/sdk,
hanzoai/rust-sdk → hanzo-rs/sdk, hanzoai/cpp-sdk → hanzo-cpp/sdk. Go's module
path stays github.com/hanzoai/go-sdk — the proxy has it, consumers require it.
The doc tables now name the canonical repos and what each actually ships.

Also recorded, both measured rather than remembered:

operationId renamed 249 SDK methods. Where cloud's document took a route an
authored spec also had (366 ops), 117 of cloud's ids are the handler's own name
and 249 are synthesized from the route, so adminListAffiliates became
CloudGetV1AdminAffiliates in every language. It fell out of the resync; on
review it stands, because a route-derived id is a total function of the
immutable /v1 path while a hand-authored one vanishes with the spec that owns
it — the same break again, later, silently — and it is the name the MCP door
already uses for the tool.

754 of 2479 operations declare no 2xx schema, and what that costs is
language-dependent: Go returns *http.Response and the body survives, Rust
returns Result<(), _> and DROPS it. billing/balance, billing/usage and
agents/{ref}/run are all in the set and all on canonical flow paths, which is
why two lanes hand-wrote raw-decode helpers. The lever is typed ops in
hanzoai/cloud; anything done here would be inventing shapes.

flows.yaml's probe rule gains the half that makes it conclusive: a 403 proves
the route EXISTS, because auth middleware runs before the handler. /v1/kv,
/v1/kv/keys and the control /v1/agents/{ref} answer 403; /v1/kv/keys/x answers
404. The parent collection routes and the child does not.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 09:36:30 -07:00
zeekayandHanzo Dev 16792cbd52 spec: declare the MCP door — the probe was the wrong verb, not a missing route
`POST /v1/mcp` answers 200 with 796 tools and no Authorization header. It was in
no spec, and this repo had written down that it did not exist, because the probe
was a GET: the route is POST-only, so GET is 404, and the document held only
/v1/mcp/servers — the registry of external servers — which made that 404 look
confirmed. Two wrong readings agreeing is not evidence.

mcp/openapi.yaml declares one operation, `mcp_rpc`, with the JSON-RPC 2.0
envelope typed from the door's own answers rather than from the MCP spec:
initialize (protocolVersion 2025-06-18, serverInfo, capabilities), tools/list
(Tool with name, description, inputSchema), tools/call (content[] + isError),
and the two failure channels that both live INSIDE a 200 — `error` for -32601,
`result.isError` for a tool that ran and refused. `capabilities.yaml` gains
`mcp` in Intelligence.

The probe rule that hid it is corrected in flows.yaml and LLM.md, where I had
written the sloppy version: probe the METHOD THE DOCUMENT DECLARES, not GET. A
POST-only route and an absent route are the same 404 from a GET. The two
surfaces flows.yaml routes around survive the corrected test — every method
their specs declare was probed — but one verb is never a liveness probe.

What the door exposes is recorded because no one can derive it: 796 tools
against 2479 operations, so two thirds of the surface is not a tool. The naming
rule is mechanical — operationId minus the leading `<service>_` — and holds for
795 of 796. The exception is this document's own doing:
`cloud_get_v1_pricing_policy_2` carries the suffix that keeps
/v1/pricing-policy and /v1/pricing/policy from becoming one Go type, so its tool
is still named `get_v1_pricing_policy`.

A tag whose key is a service NAME now belongs to that service's domain instead
of to whichever spec used it first. `MCP` is tagged by four specs; the earliest
alphabetically is `automations`, which filed the fleet's MCP door under Streams.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 09:26:38 -07:00
zeekayandHanzo Dev 56db388280 flows: the JSON-RPC door is absent from the document, not dead
The tools note retired /v1/mcp on the strength of a GET probe: "not routed
(404 page not found)". Measured against api.hanzo.ai:

    POST /v1/mcp   200  {"jsonrpc":"2.0","result":{"tools":[...]}}   730 tools
    GET  /v1/mcp   404  404 page not found
    GET  /v1/tools 403  {"status":403,"error":"a validated principal is required"}

The route is POST-only and answers unauthenticated. This file's own probe rule
— a route the binary lacks replies 404 to GET and 405 to everything else —
cannot separate that case from an absent route on a GET alone, because a
POST-only route also 404s on GET. Method matters; one GET cannot retire a route.

cloud_get_v1_tools is still the right pick, for the reason that actually holds:
POST /v1/mcp is not in hanzo.yaml, so an example built on it would be a
hand-rolled HTTP call inside a generated client — the drift these SDKs exist to
prevent. Recorded so the next lane does not "fix" the flow onto an undocumented
route on the strength of a 200.

The automations door is genuinely retired and now says so precisely: it replies
405 to POST, the method it documents, not "404 to GET and 405 to POST".

No operation changed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 09:04:00 -07:00
zeekayandHanzo Dev 50ca3fcbda flows: pick the routes the server actually serves, not the ones that read best
Probed every flow operation against api.hanzo.ai unauthenticated and with a
bogus key. A route that answers 401/403 is routed and gated; 404/405 means
nothing is mounted. Two of the six were wrong, and both were wrong in the
direction that hurts most — they looked fine.

hello: ai_getAccount -> bot_authMe. ai_getAccount answers 200 with
type="anonymous-user" to a request carrying NO Authorization header at all. A
hello flow exists to fail on a bad key; that one prints a cheerful synthetic
identity for a key that would 401 everywhere else, which is worse than having no
check, because it reads as proof. (No credential exposure: 169 of the 176 fields
come back empty, accessKey/accessSecret/accessToken among them.) bot_authMe
returns 403 {"error":"no validated principal"}.

tools: automations_mcp -> tasks_tasksMcp. POST /v1/automations/mcp returns 405
Method Not Allowed; /v1/tasks/mcp returns 403. The one I had was chosen for
having typed McpRequest/McpResponse with a `method` enum — and that is exactly
the trap: the better-typed door is the one that does not open. The swap costs an
untyped body, which is the spec's gap to close rather than the SDK's to paper
over.

store keeps its operations and gains a DRIFT note: kv_setKey/getKey/deleteKey
are the only flow operations still unserved — 404/405 at api.hanzo.ai, and
kv.hanzo.ai 404s the whole prefix, while sibling reads GET /v1/kv/keys and
GET /v1/kv/clusters correctly 403. The spec declares a data plane nothing
mounts. Left in place because a round-trip IS the right shape for `store`;
it resolves by mounting the routes or by dropping them from kv/openapi.yaml the
way the 18 unserved products were dropped.

The lesson is the general one: a spec says what SHOULD be served, and only a
request says what IS. Both SDKs' examples now run against real routes and both
now reject a bogus key with a clean error instead of pretending.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 08:52:46 -07:00
zeekayandHanzo Dev 1f93702023 spec: publish one document — cloud's woven spec merged, and it wins
`cloud/openapi.yaml` was a hand-written contract for a product with five served
routes and 151 that answer `not found`. It is now a verbatim copy of hanzoai/cloud's
own woven document, pulled by `sync.py` from that repo's origin/main (34663c49).
Cloud regenerates that document from its router and fails its own build on any
diff, so it cannot describe a route the binary does not serve — the property no
spec here has — which is why `merge.py` merges it LAST and lets it WIN every
route an authored spec also claims (298), explicitly rather than by where
`cloud` falls in the alphabet.

The document that every SDK, doc site and tool generates from goes 1132 -> 1737
paths, 1519 -> 2478 operations, and 699 -> 1217 operations carrying a
description — prose lifted from the handlers' Go doc comments, not written
twice. `tags` and `x-tagGroups` now describe the tags OPERATIONS carry (263, of
which 213 described, cloud's being the owning package's synopsis) instead of the
55 spec directories, four of which any operation carried. The registry still
decides the movements: a tag's group is the domain of the service that
introduced it.

Three defects that reached generators are fixed where codegen identity already
lives. An operation with no `responses` (668, cloud's untyped routes) gets a
`default` saying exactly that, rather than a schema nobody knows. An operation
with no tag takes its service's name, rather than `DefaultApi`. And
`cloud_get_v1_pricing-policy` and `cloud_get_v1_pricing_policy` — two real
routes, distinct strings, one Go type once the generator strips punctuation —
are separated. openapi-generator 7.14.0: 0 errors, 0 warnings (was 35 errors,
68 warnings); go and typescript-axios both generate; the Go client compiles.

Six route collisions surfaced the moment the merge stopped resolving them by
alphabet, and every one was duplicate authoring. `/health` was declared by nine
specs and answers with the SPA at api.hanzo.ai, while `/healthz` answers JSON:
it is gateway's alone now. `observe`, `app` and `product` declared only other
services' routes — o11y's, projects', vector's and search-docs' — and are
collapsed. Every route they held is still in the document, from cloud.

`generated/hanzo.json` is deleted. Measuring hanzo.yaml against a document
hanzo.yaml now contains is tautology, and a second, older copy of one emission
is the drift this change exists to end.

`flows.yaml` is new: hello, chat, money, store, agent, tools — six journeys
naming the exact operationIds a client calls and what each step asserts, so
every SDK's examples are the SAME journeys. merge.py refuses to write the
document if a flow names an operation it does not have, and three flows record
a substitution where the obvious route is not the served one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 08:50:17 -07:00
zeekayandHanzo Dev 80ab4245c3 flows: name the six canonical example flows, and gate them
Every Hanzo SDK is supposed to ship the SAME six examples — hello, chat, money,
store, agent, tools. Until now that was six repos independently remembering to
agree, and nothing checked. Nothing could: the flows existed in no manifest, in
no repo, in any language. The TypeScript and Python sets are the first two, and
writing them is what made the gap obvious.

flows.yaml is data only, like sdks.yaml beside it: per flow, the operationIds in
hanzo.yaml in call order, plus why THAT route and not a neighbour. Those choices
are the part worth writing down —

  hello  ai_getAccount, not bot_authMe (bot-service scoped) and not the IAM OIDC
         userinfo endpoint, which is on a different ORIGIN; using it would force
         every example set to know two base URLs, and IAM answers 200 text/html
         for an unregistered path, so a typo there is silent breakage, not a 404.
  tools  automations_mcp, not tasks_tasksMcp: only the former has typed
         request/response schemas, so `method` is a checked enum in every
         generated client instead of a free string.
  store  the data plane (/v1/kv/keys/*), not the provisioning plane (/v1/kv,
         which creates stores) — the two are easy to confuse by name alone.

test_flows.py asserts every id named here still exists in hanzo.yaml. Verified it
fails on a typo, not just passes on the truth:

    FAIL test_every_operation_exists_in_the_spec: flows.yaml names operations
         hanzo.yaml does not have: [('hello', 'ai_getAccount_TYPO')]

So deleting a route breaks ONE gate here rather than six SDKs' examples quietly
and separately.

Also gives this repo a `test:` block, which it did not have. 25 tests lived here
and not one had ever gated a commit — and that is not academic: hanzo.yaml spent
this entire period unable to generate a client in ANY language, because 35
/v1/platform operations carried no `responses` and openapi-generator refuses the
whole document over it. Nothing here noticed, because nothing here ever tried.
hanzo.yaml is the single input to seven language SDKs; a defect merged here is a
defect merged into all seven at once.

    25 passed in 19.99s

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 23:13:29 -07:00