main
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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]> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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
|
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
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]>
|