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.
45 KiB
Changelog
v1.0.0
The repo stops being where the API is DECIDED
hanzo.yaml is now OUTPUT: publish.py derives it from hanzoai/cloud's own
emitted openapi.yaml at the release .spec-lock pins, and publish.py --check
regenerates and diffs on every push. An operation no code declares is now
structurally impossible to publish.
DELETED — 52 hand-authored <service>/openapi.yaml specs, 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 (a parked
domain: real path and nonsense control both return the registrar's lander).
MEASURED at hanzoai/[email protected]: the hand-merged master carried 2093
operations against cloud's 2333, sharing 1908 — so it described 185 operations
nothing serves under that name and missed 425 that are served. Of the 185, 36 are
refuted outright (real 404, nonsense-sibling control 404), 44 are unfalsifiable
behind a prefix auth gate, 66 sit under a {wildcardN} relay route, and 39 are
live and undescribed — every one a hanzoai/cloud defect, itemised in LLM.md.
The projection is six codegen rules and nothing else. It exists because
openapi-generator validate refuses cloud's emission with 1012 errors (one
per route published with an address and no responses) and generate writes
zero files in every language; against hanzo.yaml it reports 0 errors. Each rule
is a candidate to move upstream into cloud's emitter, and rule 5 alone is 90% of
this file.
BREAKING — operationIds lose the <svc>_ prefix merge.py applied:
cloud_get_v1_billing_balance is get_v1_billing_balance. The prefix 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 ids that vanished:
hello is get_v1_keys (403 with no key and with a bogus one, control 404) and
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.
IAM: the dead store's document is gone, and a path names a thing
iam/openapi.yaml was the entity store this fleet replaced, imported whole. It
declared 234 operations; hanzoai/iam serves 94. audit.py had been
reporting that gap the whole time — 217 declared-and-unserved, 77
served-and-undeclared — and nothing acted on it.
The 38 verb-noun addresses in it were the loudest half: run-casbin-command,
all-actions, all-objects, all-roles, global-users, global-certs,
sorted-users, ldap-users, filtered-policies, remove-policy,
is-session-duplicated, impersonation-user, pay-order, place-order,
invoice-payment, user-orders, user-payments, permissions-by-roles,
records-filters, version-infos, table-infos and the rest. None is served by
any binary in the fleet. They reached a customer as hanzo iam all-actions and as
a method in every generated SDK. 223 summaries were a title-cased Go
controller method — "Api Controller Get All Actions", "Api Controller Get
Ldapser" — and one operation was named after an outside vendor's policy engine.
The file is now built from what the binary serves: the 75 typed operations
App.OpenAPISpec() emits, with the handlers' own doc comments as their prose,
plus the OIDC/OAuth, front-door, SCIM 2.0 and credential surface authored at the
addresses measured off app.Fiber().GetRoutes(). 112 paths, 133 operations, 0
verb-noun segments, 0 undescribed, 0 controller names. operationIds are one
shape — <kind>_<verb> — where the emitter had left three (get_v1_iam_users,
listOrganizations, addProvider); those are SDK method names, so the spelling
is the customer's business.
test_placement.py's OFF_PREFIX for iam went from eleven unprefixed paths to
three. /oauth/token, /oauth/userinfo, /oauth/introspect, /oauth/callback,
/oauth/token/refresh, /.well-known/webfinger and the two
/.well-known/{application}/… were the dead store's addresses; iam serves the
protocol endpoints under /v1/iam/oauth/ and its own discovery document says so.
Probing the old ones returns 200 text/html — the portal's catch-all,
byte-identical to what a nonsense path returns. A 200 is not liveness. What
remains is discovery at the three addresses the standards fix.
compat leaves the published document. An operation the SERVING binary tags
compat is a legacy address it keeps reachable for consumers pinned to it, not
something the contract teaches. hanzoai/iam declares 51 of them — the entity
verbs (get-users, add-application, set-preferred-mfa) and the singular
application address from before that kind was pluralized — each on the SAME
handler as its canonical noun twin. merge.py drops them, the same call as
dropping TRACE: the served surface is unchanged and the published one says each
thing once. This repo is not taking a vote about what a server serves; the
serving repo made the declaration and this respects it.
One product, one owning spec — placement, and 31 dead paths deleted
capabilities.yaml has always stated the law — "the route IS the identity: one
capability = one name = one /v1/ = one /openapi.yaml" — and nothing
enforced it. Measured against the live route table at api.hanzo.ai: 103
authored paths sat outside their owner's prefix and four products (kv,
s3, search, vector) were described by TWO specs at once.
provisioning/openapi.yaml was the bucket: it claimed /v1/sql, /v1/vector,
/v1/datastore, /v1/kv, /v1/search, /v1/s3 and /v1/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 and never was. Each kind's spec owns its own quartet
now; sql, datastore and docdb became their own specs under data;
provisioning is collapsed.
The inference edge — /v1/chat/completions, /v1/completions, /v1/embeddings,
/v1/rerank, /v1/models, /v1/models/{model}, /v1/messages,
/v1/images/generations, /v1/audio/speech — moved from ai/ to gateway/,
the binary that answers it. 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 at api.hanzo.ai and absent from the live route
table: the whole /v1/gateway/* subtree (23 — a phantom second copy of the
inference edge, which is also why three operationIds needed a _2 suffix to
stay distinct in codegen and no longer do), /v1/load-balancers ×2 (the 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), and /v1/audio/transcriptions. Nothing served was
removed: hanzo.yaml went 1742 → 1711 paths, 31 removed and 0 added.
Fifteen more authored paths 404 at the server while the live route table lists
them as keys — /v1/admin/{affiliates,authors}/* writes, /v1/admin/plugins/*
control, /v1/sites/deploy, /v1/clusters/{clusterId}/pools*. Those are
hanzoai/cloud describing routes whose handlers are not reachable, not authoring
defects, so they stay until cloud fixes them — this repo cannot refute the
registry it trusts.
test_placement.py is the gate, in CI via hanzo.yml. 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 top-level 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.
An empty field from the winner was deleting a populated one — 182 shapes back
The resync's worst defect, and it outlived two rounds of measurement here
because both measured parameters and never looked at requestBody.
merge.py took the whole operation OBJECT from cloud wherever it took a route,
and an untyped route's emission is an address and nothing else — so described
operations were replaced by undescribed ones. 47 request bodies and 135
response sets (100 of them reduced to the synthesized default) had left the
document: POST /v1/authz/check, POST /v1/agents/{ref}/run,
POST /v1/kms/secrets, the five agent-session control ops, and more. The CLI
measured it downstream: typed-flag operations 574 → 515, raw --data fallbacks
187 → 378, and hanzo kms secrets create lost the value field whose
stdin-only guard then had nothing to guard.
The mistake was reading cloud's silence about an untyped route as EMPTY when it
means UNKNOWN. fuse() overlays field by field now: cloud still wins existence,
operationId, tags and prose unconditionally, and wins any field it POPULATES. It
simply cannot delete one by being silent.
requestBody and responses are kept; query parameters are not, and the
asymmetry is the rule rather than an exception. A body IS the operation — silence
there does not prevent a lie, it prevents the CALL. A response describes what
comes back and cannot corrupt a request. A query parameter is the one a client
SENDS to a handler that may ignore it, so restoring it asserts a filter that may
silently do nothing — a wrong answer rather than a missing method.
Restored: 0 operations still missing a body d86248f had, 0 still reduced to the
synthesized default. Operations carrying parameters-or-body 1687 → 1767 of
2454. And the response-schema gap this repo had been reporting was partly its
own damage: 728 → 637, with 91 of the "missing" schemas simply deleted by
the merge. Every figure published for that gap (754, 728, 696) was measuring
this defect alongside the real one. A number that only ever went up should have
been suspicious.
All 47 body losses were routes cloud serves UNTYPED; zero were typed-with- no-body, so there is no emission bug to chase — only untyped routes to type. LLM.md carries the 47 as a handover list, prioritised by what they cost: no body means no call.
merge.py prints one number every build — 242 authored shapes still
load-bearing — designed to fall to zero as the typing lane converts those
routes, since a typed zip.Get[In, Out] carries body, parameters and response
from the code itself.
A rule stated in a comment that the code did not implement
The first version of this fix kept requestBody and responses and declared
that query parameters were deliberately dropped. Reading the published
document back showed GET /v1/kms/secrets still carrying path and env: the
code had never dropped them, because the overlay starts from the authored
operation and replaces only what cloud populates.
The claim was wrong on the merits too. 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 either
way. Treating that as complete was the same silence-for-emptiness error one
level down, and it cost GET /v1/integrations/{provider}/callback its code
and state — the whole OAuth handshake — while a sibling route with no path
template kept everything. Same evidence, opposite outcome, decided by whether
the URL contained a brace.
union() merges parameters by name now, cloud's definition winning any name it
defines, and the comment says what the code does. Verified against d86248f:
0 parameters and 0 request bodies that existed pre-resync are missing.
The union needed two corrections the gate could not supply. A $ref parameter
has no name, so comparing on the key that was not there made eight
/v1/admin/** routes declare {id} twice — that one openapi-generator caught.
Then uniqueness turned out to span both levels: a path item's parameters apply
to every operation under it, the two sides disagree about where the path
parameter belongs (authored specs hoist it, the weave emits it per operation),
and 115 operations ended up declaring {id} twice. The validator does not
resolve $ref parameters, so it reported zero errors on an invalid document.
Green and wrong at the same time; both are measured here now.
The SDK matrix pointed at two clients that do not exist
sdks.yaml's go and rust rows were verified against local checkouts, and
both local checkouts are behind their canonical remotes. Running either would
not have updated a client — it would have written a SECOND one beside the
shipped one. go said take: {.: cloud} / packageName: cloud while
hanzo-go/sdk ships package hanzoai at the module root and has no cloud/
at all; rust said crates/hanzo-cloud while hanzo-rs/sdk ships
crates/hanzo-client.
Both rows are now DELETED, and the boundary is written down rather than decided
per case: a row exists while the WHOLE invocation is expressible as data, and a
language leaves when it needs something that is not. go needs the client at
the module root, which take cannot express — it rmtree's what it owns, so
{.: .} deletes the repository. rust needs a reqwest/api.mustache override
for 14 operations whose binary body is optional (Option<Vec<u8>>, which no
type mapping reaches), and a template is a file that must sit beside the
invocation — as must the --type-mappings=file=Vec<u8> it works with. The old
rust row had neither, plus useSingleRequestParameter and the spec's version as
the crate's.
The remaining five rows stay, because the same test says so: hanzoai/js-sdk
and hanzoai/java-sdk carry NO flags in their own scripts — they exec
generate.py — and js-sdk removed its copy only after the two disagreed and
built an orphan second copy of all 2143 files. A row here AND flags there is the
lie; a row here and a bare call site there is one declaration.
Three SDK repos answer through a rename redirect, which is how a stale name
keeps resolving: 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 and consumers require it.
operationId renamed 249 SDK methods — the decision, stated
Where cloud's document took a route an authored spec also had (366 operations),
the document now carries cloud's operationId. 117 of those are the handler's own
name and lost nothing; 249 are synthesized from the route, so
affiliates_adminListAffiliates became cloud_get_v1_admin_affiliates — a
cross-language method rename, not cosmetics. It fell out of the resync rather
than being chosen, and on review it stands: a route-derived id is a total
function of the immutable /v1 path, while a hand-authored id is owned by a
spec that no longer describes the route and vanishes when that spec is deleted —
the same break again, later, silently. It is also the name the MCP server already
uses. The break is once, now, and cannot recur for these operations.
mcp_Request.id is a string — a scalar oneOf does not survive codegen
id was declared oneOf: [integer, string], which is what JSON-RPC 2.0 permits
and what no typed generator can project: openapi-generator emits an empty
carrier class for a scalar oneOf (Kotlin class McpRequestId() with no
members) that serializes to {} — not a legal id anywhere. Java and C++ produce
the same shape, so it blocked three languages on one declaration.
Verified on the live MCP server before changing it, because the fix is only safe if
the wire agrees: id: "1", id: 1 and id: "abc-123" are all accepted and all
echoed back with the TYPE they were sent; omitting id returns null. A string
is therefore always legal, and a client generated from this document always
reads back the string it sent.
Declared type: string on both Request and Response, with the polymorphism
recorded in the prose rather than in a shape — no second type, no alias, one
declaration. The generated field is now kotlin.String?, private String id,
Id *string; the McpRequestId/McpResponseId carriers are gone from every
client.
Declare the MCP endpoint — POST /v1/mcp, the one address an MCP client speaks
The fleet's JSON-RPC endpoint has been answering all along and was in no spec:
POST /v1/mcp returns 200 with 796 tools, unauthenticated. It read as missing
because the probe was a GET — the route is POST-only, so GET is 404 — and
because the document held only /v1/mcp/servers, the registry of external
servers, which made the 404 look confirmed. mcp/openapi.yaml now declares one
operation (mcp_rpc) with the JSON-RPC 2.0 request and response typed:
initialize, tools/list, tools/call, a Tool with its inputSchema, and
both failure channels that live INSIDE a 200 — error (-32601 for an unknown
method) and result.isError (a tool that ran and refused). Verified against the
live MCP server, not guessed: serverInfo, protocolVersion: 2025-06-18, and the
content[] shape are all copied from its own answers.
The tools flow moves onto it, and the probe rule that hid it is corrected
everywhere it was written: probe the method the document declares, not GET.
A POST-only route and an absent route are indistinguishable from a GET.
What the MCP server exposes is recorded because it cannot be derived: 796 tools
against 2479 operations, so about two thirds of the surface is not a tool, and
the split is the binary's decision. The naming rule is mechanical — a tool name
is its operationId minus the leading <service>_ — holding for 795 of 796. The
one exception is this document's own doing: cloud_get_v1_pricing_policy_2
carries a _2 because /v1/pricing-policy and /v1/pricing/policy collapse to
one identifier in every generator, while its tool is still get_v1_pricing_policy.
A tag whose key is a service NAME now belongs to that service's domain rather
than to whichever spec used it first: MCP was tagged by four specs, the
earliest alphabetically was automations, and the fleet's MCP endpoint was being
filed under Streams.
hanzo.yaml is THE published document — cloud's woven spec merged, and it wins
cloud/openapi.yaml stopped being a hand-written contract and became a verbatim
copy of hanzoai/cloud's own woven document, pulled by the new sync.py from that
repo's origin/main. 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. merge.py therefore merges it LAST and
lets it WIN every route an authored spec also claims (298), explicitly instead of
by where cloud falls in the alphabet.
1132 → 1720 paths, 1519 → 2457 operations, and the prose came with it:
699 → 1196 operations carry a description, lifted from the handlers' Go doc
comments. Tags too — tags/x-tagGroups now describe the tags OPERATIONS carry
(260, of which 210 described, cloud's being the owning package's synopsis)
instead of the 55 spec directories, 4 of which any operation carried. A tag's
group is still the domain of the service that introduced it, so capabilities.yaml
remains the only taxonomy.
Three defects that reached generators are fixed at the one place that owns
codegen identity: an operation with no responses (668, cloud's untyped routes)
gets a default that says exactly that rather than an invented schema; an
operation with no tag takes its service's name rather than DefaultApi; and
cloud_get_v1_pricing-policy vs cloud_get_v1_pricing_policy — two real routes,
distinct strings, one Go type — are separated. Without the first, cloud's
document alone would be 668 generator errors. openapi-generator 7.14.0 reports
0 errors, 0 warnings on the result (346 recommendations, all unused models),
go and typescript-axios both generate, and the generated Go client compiles.
Six collisions surfaced when the merge stopped resolving them by alphabet, and
all six were duplicate authoring: /health declared by nine specs (and answering
with the SPA at api.hanzo.ai, while /healthz answers JSON) is now gateway's
alone; observe, app and product declared only other services' routes and
are collapsed into o11y, projects and vector + search-docs. Every route
they held is in the document, from cloud.
generated/hanzo.json is deleted: measuring hanzo.yaml against a document it
now contains is tautology, and a second, older copy of one emission is the drift
this change exists to end.
flows.yaml — repointed at what the wire actually answers
The six journeys landed naming ids the merge then moved or that were never
served, so every one was re-resolved against the merged document AND probed.
money and agent move to cloud's ids (billing_* and
cloud_AgentsController.* no longer exist — the binary's document owns those
routes now). store moves from the KV value plane to the store itself, and
tools from the automations MCP endpoint to /v1/tools: both of the originals
reply 404 to GET and 405 to PUT/POST/DELETE, which is what a GET-only wildcard
answers when nothing is routed there. hello and chat were already right —
ai_getAccount returns 200 with the owner and name to print.
The method spread is the lesson: a bare 404 is ambiguous because a live handler says "not found" too, so one GET is not a liveness probe.
Delete the 18 products authored here and served nowhere — 655 paths, 849 operations
Every product below was probed on its OWN authored routes, at api.hanzo.ai and
at the hostname this repo claimed for it, and every one answered a route-level
404 — the router's own plaintext 404 page not found, not an empty result from a
live handler. hanzoai/cloud's openapi.yaml does not own them either.
Authoring a route nothing serves is not a harmless placeholder. hanzoai/cli's
genspec refutes an authored operation only where the live route table OWNS its
product, so a product the router has never heard of is refuted by NOTHING: it
survives every gate and ships as a hanzo command, an SDK method, and a
SKILL.md under /.well-known/agent-skills instructing an agent to call a dead
endpoint — skills.py has no liveness filter at all. Annotating the spec
UNSERVED was the previous answer; no generator reads a comment.
| product | paths | ops | source removed |
|---|---|---|---|
| chat | 171 | 206 | chat/ |
| nexus | 150 | 150 | nexus/ |
| flow | 87 | 120 | flow/ |
| auto | 50 | 69 | auto/ |
| console | 43 | 67 | console/ |
| paas | 28 | 42 | paas/ |
| mq | 28 | 41 | mq/ |
| engine | 22 | 35 | engine/ |
| db | 17 | 30 | db/ |
| pubsub | 19 | 29 | pubsub/ |
| registry | 13 | 22 | registry/ |
| did | 10 | 14 | did/ |
| stream | 8 | 13 | stream/ |
| guard | 5 | 7 | guard/ |
| chat-docs | 1 | 1 | path in cloud/ |
| index-docs | 1 | 1 | path in cloud/ |
| agent-bindings | 1 | 1 | path in visor/ |
| sdk | 1 | 1 | /v1/sdk/secrets in kms/ |
paas is also a standing ruling: paas and platform were two names for one deploy
plane, and /v1/platform is the one that answers. It moves to collapsed: in
capabilities.yaml, pointing at platform.
chat was the one case needing a scalpel rather than a verdict, because the
GATEWAY serves inference under /v1/chat/. All 157 /v1/chat/* paths in the
master were probed individually (GET for reads, POST + {} for writes; 180 of
181 requests returned the plaintext route-miss). Exactly one answered:
POST /v1/chat/completions, with 401 and a JSON body. That operation is authored
by ai/, not chat/ — so it survives untouched and chat/ goes whole. The 15
bare /oauth/{provider} paths chat/ also authored return 200 HTML from the
marketing SPA's catch-all (/__total_nonsense_xyz__ returns the same page), so
they were never API routes, and they were never under /v1/ either.
Removing /v1/sdk/secrets orphaned kms's entire components.schemas
(SdkEnvelope, SdkEnvelopeIdentity, Error — reachable from no surviving
path), so it goes with the operation rather than shipping as three dead models in
every SDK. kms itself is untouched and live: /v1/kms/secrets answers 403.
Unchanged and verified alive at the edge: audio, completions, embeddings,
images, messages, models, router, rerank, and /v1/chat/completions.
The master goes 1787 → 1132 paths; the removed set is exactly the products above.
A product returns to this repo the day it is actually served.
plugin/ — author the /v1/admin/plugins operator surface cloud already serves
generated/hanzo.json carried four operations the contract never named:
GET /v1/admin/plugins and POST /v1/admin/plugins/{name}/{enable,disable,reload},
served by hanzoai/cloud/clients/plugin as zip typed ops. Undeclared routes are
unreachable from every consumer of this repo — no SDK method, and no hanzo
command, because hanzoai/cli's genspec iterates the AUTHORED master and uses
the live route table only to REFUTE. A path absent here can never enter the CLI
spec no matter what the server serves.
They live in plugin/, not admin/: admin/ is the aggregator subsystem
(clients/admin), and the convention already established by affiliates/,
authors/ and referrals/ is that a subsystem authors its own
/v1/admin/<subsystem> operator paths beside its public ones.
Prose, operationIds and parameters are carried verbatim from the emitted spec.
The response schemas are named rather than inlined — ListOut → Host →
PluginStatus → PluginUsage, ActionOut → Result — with each field's
description lifted from the Go type it encodes, and since typed as a
date-time string rather than the empty object a time.Time renders as.
Add research/ — the /v1/research versioned R&D evidence surface (HIP-0512)
Adds the research service under the Intelligence domain: the R&D evidence plane
(experiments, attempts, diary artifacts) served by hanzoai/cloud/clients/research.
Versioned append-only records with canonical-vs-retained counts, first-class provenance
(git + lib_versions), private-by-default visibility + separate consent grants, and
content-hash-addressed diary artifacts. Seven operations: ingest, list experiments,
projects, totals, grants, record artifact, list artifacts.
Codegen-clean: dart-dio + typescript-axios crashes fixed; SDK regen unblocked
The Regenerate SDKs workflow failed on every merge for a week. Root causes,
all fixed here (openapi-generator 7.14.0, pinned):
notify/openapi.yaml— thesyncquery param on/v1/notify/send,/v1/notify/send/sms,/v1/notify/send/emaildeclaredtype: stringbutenum: [true](a YAML boolean). typescript-axios crashed withClassCastException: Boolean cannot be cast to Stringonnotify_notifySend. Enum value is now the string'true'(matches the declared type and the?sync=truewire form).vector/openapi.yaml— theidpath param on/v1/vector/collections/{collection_name}/points/{id}usedoneOf: [integer, string]. dart-dio crashed withClassCastException: JsonSchema cannot be cast to ComposedSchema(composed schemas are not supported on parameters). Path params serialize as strings on the wire, so it is nowtype: string(pass"42"or a UUID) — faithful and gives cleaner SDK signatures than anany/objectid..github/workflows/regenerate-sdks.yml— thedispatchjob authenticated withSDK_DISPATCH_TOKEN, which was never set (resolved empty → 401), so python/go/js were never regenerated; repointed to the canonical orgGH_PAT(visibility: all, repo scope) and setfail-fast: false. The cpp/dart job no longer uploads to GitHub artifact storage (that quota was exhausted and is a dependency we refuse per the own-CI directive) — generation itself is the codegen gate.
All five generators (cpp-restsdk, dart-dio, go, python, typescript-axios) now
generate clean from hanzo.yaml with zero exceptions.
Usage-cap + promo canonical types (HIP-0127 primitive algebra)
Curry-precise, code-faithful types for the spend-cap / promo money surface, aligned to the primitive algebra (Meter = usage measured, Policy = cap-deny verdict, Money = cents, Schedule = period window).
billing/openapi.yaml—/v1/billing/spend-alertsself-service CRUD (getlist,post,patch {id},delete {id}) plus/v1/billing/spend-alerts/authorize(the per-request cap verdict the cloud metering gate consumes). SchemasSpendAlert(scope +thresholdcap centsenforce+ derivedperiod/resetsAt/periodSpentCents/over/warn),SpendAlertCreate,SpendAlertUpdate,CapVerdict, andCapReason— wherespend_capandinsufficient_balanceare DISTINCT reasons. Shapes match commerceapi/billing/spend_alerts.go+spend_cap.goand the cloudclients/meteringverdict.
admin/openapi.yaml—/v1/admin/promosadmin-set discount CRUD with thePromo(percentOff,start,end,plans,active),PromoCreate, andPromoUpdateschemas.
request bodies — complete the write-op contract from real handlers
Filled the missing requestBody shapes on write ops (POST/PUT/PATCH), each
sourced from the real handler (hanzoai/cloud/clients/*, iam, chat,
commerce, world, auto, flow→auto, platform, and the hanzoai/ai
Beego controllers behind the cloud/nexus file/connection routes) — never
guessed. Coverage across the 68 specs: 832 → 871 of 955 write ops now carry
a requestBody.
The 84 write ops still without a body fall in two source-verified buckets, not fabricated:
- Body-less by design (61): path/query/session RPC actions that read no
body — admin suspend/reactivate/sweep; automations run/enable/disable (org is
the validated cred, "NEVER from the body"); commerce
capture/confirm/cancel/discard (captured amount is the stored order amount,
never client-supplied); the Neon/Harbor/Meilisearch/Qdrant-proxied
db/registry/search/vector actions; iam
device/impersonation-exit/sso-logout/identification-verify; and the
cloud/nexusquery-param file/connection + signin/signout actions. - No live handler (23) — flagged, not authored: iam user-keys/orders-cancel/invoice-payment/pay-order/place-order/refresh-engines (legacy swagger; billing moved to commerce); kms token-renew, token-auth-identity-tokens, webhook-test, secret-sync-trigger (absent from the Go KMS); framework install/submit/cancel (repo not in tree); flow solutions-apply, git-repos-pull (no such routes); bot skill undelete/stars (cloud proxies verbatim, upstream not located); analytics auth-logout/verify/sso + website-reset (Umami, no upstream in tree); paas doks-upgrade-ha, container-deploy (served by platform tRPC, not cloud).
Red-review fixes (fix-then-ship): world/classify-event corrected from POST to
GET with title (required) + variant query params, matching the handler
(internal/world/handlers_ai.go:167). The cloud/nexus add-file/delete-file
bodies now reference a dedicated request schema (object.FileInput / FileInput,
owner+name required) built from the real ai/object/file.go struct — instead
of the stale UI-tree response component object.File/File, which lacked
owner/name/filename/store and would have handed clients a wrong contract.
delete-connection's object.Connection/Connection was verified correct against
ai/object/connection.go and left unchanged. hanzo.yaml + CAPABILITIES.md
regenerated via merge.py (3802 $refs, 0 dangling).
2026-07-11 — git: SSH transport, client-less push, ZAP note, sshUrl
Extended git/openapi.yaml to match the new native-git surface in
hanzoai/cloud/clients/git:
/v1/git/keysCRUD (registerGitKey,listGitKeys,deleteGitKey) — per-user SSH public keys backinggit clone [email protected]:<org>/<repo>.git. A key is stored with its SHA256 fingerprint (the global unique handle) and resolves, at SSH auth time, to its owning org./v1/git/repos/{name}/push(gitPush) — client-less push: builds a commit from a set of posted files (UTF-8 or base64), advances the branch ref, and fires the git-push-to-deploy build exactly as a real receive-pack does. Creates the repo on first push.sshUrladded to theReposchema (and the newPushResult): every repo now advertises bothcloneUrl(HTTPS) andsshUrl(SSH, scp-style).- ZAP transport documented in the spec description: the git control plane is
also reachable over the shared ZAP-over-WebSocket plane at
/zap(proceduresgit/zap/{createRepo,listRepos,getRepo,deleteRepo,usage}), thin adapters over the SAME core the REST handlers call — one implementation, two transports.
New schemas: RegisterKey, Key, PushFile, PushRequest, PushResult.
hanzo.yaml + CAPABILITIES.md regenerated via merge.py.
2026-07-10 — o11y resynced to the flat, version-less live surface
Reconciled o11y/openapi.yaml to the live cloud routes in
hanzoai/cloud/clients/o11y (scope.go + query.go + vmproxy.go +
event_ingest.go). The prior spec modelled a fictional Loki/Tempo/Grafana
surface (LogQL logs/query, logs/push, logs/tail, Prom series/labels,
traces, dependencies, notification channels, dashboard/rule CRUD) that
was never served at these flat paths. The o11y subsystem also collapsed from
five registrations to one o11y concept; the spec now reads as a single O11y
surface (one tag).
The spec is now exactly the live public surface (12 flat, version-less paths —
no nested /v1/o11y/api/vN/* or /v1/o11y/vN/*):
POST /v1/o11y/query,POST /v1/o11y/query_range— the one canonical composite builder query (was wronglyGETPromQL); the upstream engine version stays internal, never in the route.GET /v1/o11y/logs,/v1/o11y/metrics,/v1/o11y/status— tenant-scoped, org-pinned reads (product logs, RED + LLM usage, live service health) with schemas grounded in the Go response structs.GET /v1/o11y/vm/query,GET /v1/o11y/vm/query_range— NEW. The SuperAdmin-only VictoriaMetrics read proxy backing the platform infrastructure-health board;queryallowlisted toup/sum(up)/count(up), returning the native Prometheus envelope verbatim.GET /v1/o11y/services,/v1/o11y/dashboards,/v1/o11y/rules,/v1/o11y/health— served by the embedded o11y runtime.POST /v1/o11y/ingestion— native-Go LLM-observability event ingest (traces/observations/scores).
hanzo.yaml regenerated via merge.py: 68 services, 1797 paths (−10 from the
removed fictional o11y paths).
Canonical capability manifest + category grouping
- Added
CAPABILITIES.md— the authoritative index of every public capability: one canonical name each, grouped into the eight categories (Identity, AI, Messaging, Observability, Commerce, Platform, Applications, Core), with/v1/<name>prefixes. Internal infra (principal, goja, mpc, controlplane) is explicitly excluded. merge.pynow emitsx-tagGroupsso the unifiedhanzo.yamlis navigable by category, and fails closed if any spec is ungrouped.
Cloud product-surface specs (clients/* → live /v1 flat prefixes)
Authored per-service specs for the fused api.hanzo.ai/v1 cloud-binary
surface, route-accurate to the Go handlers in hanzoai/cloud/clients/<x>:
/v1/agents/*(agents + runs + live session tree) is covered bycloud/openapi.yaml(landed on main); the 21 specs below are the rest.- Added
functions/openapi.yaml—/v1/functions/*(serverless functions, metered invoke) fromclients/functions. - Added
framework/openapi.yaml—/v1/framework/*(doctypes, modules, roles, generic document CRUD) fromclients/framework. - Added
tracker/openapi.yaml—/v1/tracker/*(projects + issues) fromclients/tracker. - Added
crm/openapi.yaml—/v1/crm/*(companies, contacts, opportunities) fromclients/crm. - Added
automations/openapi.yaml—/v1/automations/*(flows, versions, runs, pieces, MCP) fromclients/automations. - Added
integrations/openapi.yaml—/v1/integrations/*(provider connect- Slack events/commands/link) from
clients/integrations.
- Slack events/commands/link) from
- Added
admin/openapi.yaml—/v1/admin/*(god-view overview, orgs, users, customers, finance, revenue, audit) fromclients/admin. - Added
plan/openapi.yaml—/v1/plans/*(plan catalog, resolve, entitlements) fromclients/plan. - Added
kb/openapi.yaml—/v1/kb/*(search + connectors) fromclients/kb. - Added
prompts/openapi.yaml—/v1/prompts/*(prompt library, catalog, metrics) fromclients/prompts. - Added
templates/openapi.yaml—/v1/templates/*(template gallery) fromclients/templates. - Added
git/openapi.yaml—/v1/git/*(repos + Git smart-HTTP transport) fromclients/git. - Added
billing/openapi.yaml—/v1/billing/*(usage, balance, gpu-eligibility, gpu-charge, payment-methods) fromclients/billing. - Added
security/openapi.yaml—/v1/security/*(rules, scans, findings) fromclients/security. - Added
graph/openapi.yaml—/v1/indexers,/v1/oraclesfromclients/graph(flat prefixes, not/v1/graph). - Added
do/openapi.yaml—/v1/vpcs,/v1/load-balancersfromclients/do(flat prefixes, not/v1/do). - Added
exec/openapi.yaml—/v1/exec,/v1/upload,/v1/download,/v1/files(code interpreter,X-API-Keyservice auth) fromclients/exec. - Added
websearch/openapi.yaml—/v1/websearch/{search,scrape}(SearXNG + Firecrawl-shaped over Hanzo Crawl) fromclients/websearch. - Added
tasks/openapi.yaml—/v1/tasks/*(durable workflow engine surface, identity-gated) fromclients/tasks. - Re-based
visor/openapi.yamlonto its live flat prefixes/v1/machines,/v1/gpus,/v1/clusters,/v1/bots,/v1/compute/*,/v1/agent-bindingsfromclients/visor(was wrongly under/v1/visor/*). - Re-based
zt/openapi.yamlonto its live flat prefixes/v1/networks,/v1/mesh/services,/v1/edge/nodesfromclients/zt(was wrongly under/v1/zt/*). hanzo.yamlregenerated: 58 services, 1698 paths.
Frontend surface completeness (hanzo.app + hanzo.chat)
- Added the
appservice spec (app/openapi.yaml): the hanzo.app projects & deployments surface at top-level/v1/projects/*—GET/POST /v1/projects,GET/PATCH/DELETE /v1/projects/{slug},POST /v1/projects/{slug}/deploy(tar artifact or git build),GET /v1/projects/{slug}/deployments. - Added the
baseservice spec (base/openapi.yaml): the Hanzo Base records store at top-level/v1/collections/{collection}/records[/{id}](CRUD), backing hanzo.app'ssite_draftsworking-draft store. aispec: defined the image and audio operations its own description already advertised —POST /v1/images/generations,POST /v1/audio/speech,POST /v1/audio/transcriptions(OpenAI-compatible).- Confirmed zero
/api/paths across every spec; the only non-/v1routes are the unversioned infra probes/health,/healthz,/metrics. hanzo.yamlregenerated: 38 services, 1590 paths.
v1.0.0 — 2026-06-30
AI inference namespace
- Added the
aiservice spec (ai/openapi.yaml): the top-level OpenAI- and Claude-compatible inference surface served byhanzoai/ai—/v1/chat/completions,/v1/completions,/v1/embeddings,/v1/rerank,/v1/models,/v1/models/{model},/v1/messages(Anthropic). These are the ONE deliberate exception to the/v1/<service>/rule (SDK path-compatibility is the contract). - Un-nested inference from the cloud control plane: removed
/v1/cloud/chat/completions(moved to top-levelai)./v1/cloud/*is now the user-org control plane only. hanzo.yamldiscovery now listsai(gateway_route/v1/chat/completions) and correctscloudto/v1/cloud.
2026-07-03 — agents + sessions control plane documented at /v1
- Documented the previously-undocumented core agents surface in
cloud/openapi.yaml(grep/v1/agentsreturned nothing before this). Sourced from the real handlers inhanzoai/cloud(clients/agents/{agents,store, sessions,sessions_store,sessions_stream}.go), not guessed. Top-level per the lock-in (no legacy prefix):/v1/agents(list/create),/v1/agents/{ref}(get/patch/delete),/v1/agents/{ref}/run(+ credit drawdown metered product=agent),/v1/agents/{ref}/runs,/v1/agents/metrics,/v1/agents/activity. - Documented the live session/subagent control plane:
/v1/agents/sessions(register/list),/v1/agents/sessions/{id}(detail/patch),/v1/agents/sessions/{id}/tree(subagent tree),/v1/agents/sessions/{id}/events, the control commands.../{pause,resume,stop,message}, and the SSE feed/v1/agents/sessions/stream(streams over ZAP). - New tags:
Agents API,Agent Sessions API. New self-contained schemas undercomponents.schemas(agents.*): Agent, AgentDetail, Run, CreateAgentRequest, UpdateAgentRequest, RunRequest, Metrics, Activity, Session, SessionDetail, Event, TreeNode, RegisterSessionRequest, PatchSessionRequest, EventRequest, ControlRequest, ControlResult. Every route is org-scoped (X-Org-Id minted by the gateway from the validated IAM token, HIP-0026). info.versionstays1.0.0per the lock-in (this repo tracks changes here, not via per-spec version bumps). Whole-repo/api/sweep remains zero routes (the two residual mentions were#comments, reworded away).
v1.0.0 — 2026-05-31
Forward-only v1 lock-in. No prior versions, no backwards compatibility.
Scope
All 34 service specifications plus the master discovery spec
(hanzo.yaml) are now at info.version: 1.0.0.
Routing
- Every route is
/v1/<service>/<resource>. The same path resolves through the gateway atapi.hanzo.aiand through the service's own hostname. /api/prefixes are removed from every spec (794 routes rewritten).- IAM also exposes
/oauth/*(RFC 6749 / OIDC) and/.well-known/*(RFC 5785) at the root.
IAM
- 227 legacy RPC-style
/api/<verb>-<resource>routes rewritten to REST under/v1/iam/<resource>[/{id}]. Verb is encoded in the HTTP method. - 233 input paths consolidated to 150 REST paths after collision merging.
- OIDC and OAuth2 endpoints (
/oauth/token,/oauth/authorize,/oauth/userinfo,/oauth/introspect,/oauth/callback) moved to canonical paths per RFC 6749.
Security
- Every operation uses
BearerAuth(JWT issued by Hanzo IAM athttps://hanzo.id). - Bearer scheme cites the OIDC discovery URL
(
https://hanzo.id/.well-known/openid-configuration). - No service has its own login flow.
Cleanup
- 1
deprecated: trueoperation removed (console). - 1 cross-brand DID method reference removed from the did spec; only the
did:hanzomethod is supported. - Loki upstream paths (
/loki/api/v1/*) renamed to/v1/o11y/logs/*for consistency. - chat had two competing
bearerAuth/BearerAuthsecurity schemes; consolidated toBearerAuthonly. - Master
hanzo.yamlrewritten as a thin v1 discovery index. The previous 1202-line union-of-services dump is replaced by a 3-route document (/v1/discovery,/v1/health,/.well-known/openid-configuration) plus aServiceschema. Per-service detail lives in each service's own spec; there is no cross-file$ref.
2026-06-30 — cloud product surface documented at /v1
- Added the previously-undocumented cloud product/docs-RAG surface to
cloud/openapi.yaml, top-level per the lock-in (no/api/):/v1/search-docs,/v1/search-docs/indexes,/v1/search-docs/stats,/v1/chat-docs,/v1/index-docs,/v1/vector/collections,/v1/vector/stats. These are the console Search/Vector panels + docs-RAG clients (python-sdk, hanzo-docs). The routes served the residual/api/<route>form and were renamed to/v1/<route>in hanzoai/cloud (productsvc) + hanzoai/ai; the spec now matches. New tag:Search API. Whole-repo/api/sweep confirms zero remaining Hanzo paths.
Coverage
| Service | Paths | Routes |
|---|---|---|
| analytics | 63 | all /v1/analytics/* |
| auto | 50 | all /v1/auto/* |
| bot | 30 | all /v1/bot/* |
| chat | 171 | all /v1/chat/* |
| cloud | 151 | all /v1/cloud/* |
| commerce | 78 | all /v1/commerce/* |
| console | 43 | all /v1/console/* |
| db | 17 | all /v1/db/* |
| did | 11 | all /v1/did/* |
| dns | 10 | all /v1/dns/* |
| edge | 12 | all /v1/edge/* |
| engine | 22 | all /v1/engine/* |
| flow | 87 | all /v1/flow/* |
| gateway | 24 | all /v1/gateway/* |
| guard | 6 | all /v1/guard/* |
| iam | 150 | /v1/iam/* + /oauth/* + /.well-known/* |
| kms | 52 | all /v1/kms/* |
| kv | 22 | all /v1/kv/* |
| ml | 14 | all /v1/ml/* |
| mq | 28 | all /v1/mq/* |
| nexus | 151 | all /v1/nexus/* |
| o11y | 22 | all /v1/o11y/* |
| operative | 10 | all /v1/operative/* |
| paas | 29 | all /v1/paas/* |
| platform | 132 | all /v1/platform/* |
| pricing | 23 | all /v1/pricing/* |
| pubsub | 20 | all /v1/pubsub/* |
| registry | 14 | all /v1/registry/* |
| s3 | 12 | all /v1/s3/* |
| search | 34 | all /v1/search/* |
| stream | 9 | all /v1/stream/* |
| vector | 13 | all /v1/vector/* |
| visor | 41 | all /v1/visor/* |
| zt | 30 | all /v1/zt/* |
Master hanzo.yaml discovery spec: 3 paths.