40 Commits
Author SHA1 Message Date
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 9c0d8b259a Stop naming a generator we do not use
The SDK generation heading carried "(Stainless RETIRED, 2026-07)". Retiring
something is worth recording once; carrying the name in a section title keeps it
on the page forever, and this was the last mention of it anywhere in the clients
or the driver.

The section says what the one way IS — hanzo.yaml, openapi-generator, one
invocation in generate.py. That is the whole fact.
2026-08-23 11:38:08 -07:00
zeekayandHanzo Dev 170d925a6e the record says why the gate could not run
The gates table's `needs` column carried a diagnosis that had been overtaken by
events, and a wrong diagnosis in the durable record is worse than none: it
blamed a GitHub plan tier and a repo secret named SDK_DISPATCH_TOKEN for
`publish.py --check` never running. Neither is why. The workflow asked
hanzoai/cloud, which is the re-rooted OSS core — twelve commits, no
`openapi.yaml` — and being PUBLIC, the checkout succeeded and the failure
surfaced a step later, where the step's own canned message relabelled it as a
hand edit of hanzo.yaml.

What is still open is the credential, and it is a different sentence.
hanzo-inc/cloud is private and in another org, 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 and again on this repo's own spec sync,
which now names HANZO_GIT_TOKEN and stops there in a second.

Two smaller ones follow the same rule. `--current`'s paragraph explains that the
remote is discovered and never named, then names one; it says which host now.
And `generate.py`'s read takes the fleet's one name for a credential that reads
git.hanzo.ai, so a hand run and hanzoai/ci's client lane ask for the same thing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-19 23:41:03 -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 32c5f43022 the retired rust script was 155 lines, and both figures for it were wc -l on no commit
Same defect the go figure had, in the rust row: prose that states a measurement
nobody can reproduce. The rust repo said 250 and this repo said 138; the script
is 155 lines at hanzo-rs/sdk@365f06f, the commit whose output the row reproduces
byte-for-byte, and that is also its maximum across its whole history. It was 144
on main's tip before the row landed and 139 when it wrote the earlier client.

The number is the only thing that moves. The claim it decorates — regenerate at
cloud@5126939 through the driver and --check reports [rust] clean — reproduces.
2026-08-14 20:23:26 -07:00
zeekay f58e3755a1 the rust note counts the script it replaced
hanzo-rs/sdk's scripts/generate.sh is 138 lines on main and never exceeded 139
across its four commits. The row and LLM.md both called it 250 — the same wrong
figure that stood against go's, which was 196.
2026-08-14 20:11:10 -07:00
zeekay 670c02764f rust is a row again: a template can sit beside its flags without leaving
Rust left sdks.yaml because it needs a reqwest/api.mustache override and a
template is a FILE that has to sit beside the flags it goes with, or the pair
drifts apart. That was right about the risk and wrong about the address —
nothing requires the file to live in the SDK repo. `templates:` names a
directory under this checkout, so both halves land in one commit and one review.

The departure is what drifted, in exactly the direction it was meant to prevent:
hanzo-rs/sdk's own script defaulted to SPEC_REPO=hanzoai/openapi, hanzo.yaml, at
the branch `main`, while the .spec-lock beside it named hanzoai/cloud,
openapi.yaml, at a commit. One repo, two answers to which document the client is
a projection of, and only one of them reachable by a hand run. That script also
re-derived the jar fetch, the YAML-to-JSON conversion, --skip-validate-spec and
the drift check — the same four copies of this driver's logic that retired go's.

The row reproduces it exactly: regenerated at the ref that lock named,
`generate.py rust --check` reports clean, byte-for-byte the tree the 250-line
script produced. templates/rust/reqwest/api.mustache is a 7-line delta over the
generator's stock copy, wrapping .body() in `if let Some(..)` for the 16
operations whose binary body is optional, where reqwest::Body has no
From<Option<_>> and no type mapping reaches the Option.

Templates also stop being a reason cpp cannot be a row; its generator-version
floor still is.
2026-08-14 19:52:41 -07:00
zeekay 7e446df39c say which document each consumer actually reads
The consumer table described a fleet that no longer exists in two places. The doc
site has left this document for hanzoai/cloud's own, and so has hanzo-rs/sdk;
both needed only paths, tags, prose and schemas, none of which the six rules
supply. A row here is earned by needing what publish.py adds — codegen does,
prose does not — and a table that says otherwise is the same defect this repo was
built to end, one level up.

The fetch host is stated too, because the old one was a trap rather than a
detail: reading api.github.com for a document only git.hanzo.ai carries produced
a 404 that the code reported as a missing credential.
2026-08-08 06:43:05 -07:00
zeekayandHanzo Dev 353b50d2b6 tools: a tool name is the id the door answers to, not codegen's rename
`publish.py` rule 6 suffixes an operationId when two collide under the identity
a GENERATOR uses — punctuation stripped, case folded — so that Go does not
declare one request type twice. `deleteSession` (POST /v1/iam/sessions/delete)
and `DeleteSession` (DELETE /v1/o11y/sessions) are that collision, and the
second is published as `DeleteSession_2`.

An operationId is not only a codegen identifier. It is the WIRE NAME of an MCP
tool, and `POST /v1/mcp` answers to no `DeleteSession_2` — measured against the
live door, which lists `DeleteSession` and `deleteSession` and nothing else. The
suffix shipped verbatim in the tool catalogue of three client distributions:
npm @hanzo/mcp, PyPI hanzo-mcp, crates.io hanzo-mcp, all three byte-identical at
d6d49a17, 2279 tools. A catalogue that names a tool nothing can be told to run is
the one defect the catalogue exists to remove.

So rule 6 records what it renamed. `x-id` is the id hanzoai/cloud emitted,
written only where the projection had to change it — one operation in 2279, so
its meaning is its presence — and `tools.py`'s `wire()` is the ONE place a tool
takes its name. Nothing else moves: same rules, same drops, same count.

MEASURED, catalogue against the live door's 1323 tools:
  before  1299/1323
  after   1300/1323   and the remaining 23 are exactly the compat-tagged IAM
                      operations rule 2 drops on purpose, each of which has a
                      canonical twin in the published document
                      (add-user -> POST /v1/iam/users, and so on for all 23).

test_tools.py pins it from the document alone, offline: where `x-id` is present
the catalogue must carry it and must NOT carry the codegen name. hanzo.yml runs
the file, because a gate that is not in the `test:` block does not run.

openapi-generator 7.14.0 is indifferent to the new key — python, typescript,
java and kotlin each emit a full client tree from the document that carries it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 18:31:39 -07:00
zeekayandHanzo Dev a40884ab95 publish: cloud's main is the remote that CARRIES the document, not the one called origin
`--current` resolved `origin/main`, and `origin` is not a fact about a checkout —
it is the name a clone happened to use. hanzoai/cloud answers on three remotes
here and they are not one lineage: `origin` is the GitHub OSS mirror, whose main
holds no openapi.yaml at its root at all, so `git show origin/main:openapi.yaml`
fatalled before the pin was ever compared. The one question this file asks on a
clock could only die or agree by accident, and it died — silently, for 24
releases, while nothing went red. A gate that cannot fail is not a gate.

The remote is DISCOVERED now, never named: fetch every one, keep those whose
`main` holds the document, take the one that contains all the others. A checkout
with a `forge` remote gets no special case — forge wins on this box because it
carries the file, `origin` wins on a normal clone, and neither is written down.
The same resolution is the default when re-pinning, so `python3 publish.py` with
no `--ref` can no longer publish from the wrong lineage either. Unreachable is
not fatal, because a mirror nobody can read decides nothing; stale is, so every
remote is fetched before any is read.

MEASURED, all four verdicts, against this checkout:

  pin v1.801.452 (a real older release)   -> forge/main is bab535c2 with a
                                             different document, exit 1
  pin 292e2fa8 (one commit back)          -> forge/main has moved (bab535c2) but
                                             the document has not, exit 0
  pin bab535c2 (the tip that carries it)  -> hanzoai/cloud@bab535c2 - forge/main,
                                             exit 0
  --cloud pointed at a checkout carrying   -> "no remote carries openapi.yaml on
  no openapi.yaml                            `main`", exit 1, not a git fatal

The committed artifact does not move: `--check` re-derives hanzo.yaml to the same
bytes at v1.801.476, `--served` refutes all 2305 published operations against
api.hanzo.ai and finds none missing, and 17 tests pass. Nothing here changes what
is published, only which repository gets asked whether it is still current — so
`info.version` stays 8.0.0, which is the shape of the publication and the shape
did not change.

Prose follows the code: the `--current` and default-re-pin help, the LLM.md
command block and the gates table all said "cloud's origin/main" and now say what
runs.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 13:23:16 -07:00
zeekayandHanzo Dev 0cc961012e LLM.md: the forge mirror is read-only, not absent
`git.hanzo.ai/hanzoai/openapi` was recorded as a 404. It answers now — main at
d1f8954, two commits behind — and refuses a push with `mirror repository is
read-only` (403). The conclusion is unchanged and stronger: it PULLS from
github, so it can never collect a workflow from a push, and `.hanzo/workflows`
would have been collected by nobody however long it sat there.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 10:52:04 -07:00
zeekayandGitHub d1f8954dda legal: merge legal/dual-mit-apache into main (HIP-0137) 2026-08-04 14:16:56 -07:00
zeekayandHanzo Dev c63f667452 gate: refute the published document against the deployment, in the lane that runs
The drift gate this repo already had could not run, so nothing did.

`publish.py --check` is the right question — is hanzo.yaml what its own pinned
input projects to — and it needs a hanzoai/cloud CHECKOUT. hanzoai/cloud is
private, hanzoai is on the GitHub Free plan (where an org secret resolves to the
empty string inside a private repo), and this repo's one repo-level secret is
SDK_DISPATCH_TOKEN. On top of that, 3cd60cb moved both callers to
.hanzo/workflows, which only git.hanzo.ai collects, and
git.hanzo.ai/hanzoai/openapi is a 404 — no mirror. `gh workflow list` has
returned NOTHING since 2026-07-30: not CI/CD, not `spec sync`, no `test:` block.

So on main, --check exited 1, --current exited 1 and test_publish.py failed —
three red gates, none of them running anywhere, while the document they guard
shipped 19 unserved operations to seven SDKs.

  publish.py --served   every operation this repo PUBLISHES must be one
                        api.hanzo.ai answers for. Cloud serves its own emission
                        unauthenticated at /v1/openapi.json, from the binary
                        that is running, so the deployment refutes a dead route
                        by itself. No checkout, no token, no ref to trust.

Deliberately the WEAKER question, because it needs nothing and therefore runs.
One direction only: served-and-unpublished means the pin is behind, which is
--current's question and not a lie about the API; published-and-unserved is the
lie, and it is what reaches an agent as a SKILL.md instructing it to call a dead
endpoint. Never fails on an unreachable API — same rule as hanzo.ai's
scripts/audit-catalog.mjs, which gates the same document: a gate that goes red
for somebody else's outage gets switched off, and then it gates nothing.

Wired into hanzo.yml's `test:` block, the one file both hanzoai/ci and
platform.hanzo.ai read, and test_publish.py now asserts that wiring — deleting
the gate has to delete its own test, in the same diff, on purpose.

The callers move back to .github/workflows, which is the path the forge this
repo LIVES ON collects, with the `uses:` path following them; `spec sync` gets
a token on its hanzoai/cloud checkout, since the automatic one reads a private
repo as an anonymous caller and gets a 404 that reads like a missing repo.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 01:44:33 -07:00
zeekayandHanzo Dev 13fa22c9fe legal: relicense to MIT OR Apache-2.0 (HIP-0137)
HIP-0137 "One License" (hanzoai/hips, HIPs/hip-0137-one-license.md) puts
BSD-3-Clause out of scope for hanzoai: original Hanzo work takes the standard
MIT OR Apache-2.0 dual licence, and only forks keep an upstream licence.

LICENSE is now the dual declaration; LICENSE-APACHE and LICENSE-MIT carry the
two texts beside it. The existing copyright holder and years are preserved
verbatim.

Provenance: the history reaches back to 2016 and is Hanzo own; no third-party
source survives in HEAD.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 01:20: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 3bafef9ac6 sdk: the four clients project cloud's document, not this repo's master
`hanzo.yaml` is a SECOND AUTHORITY ON WHAT EXISTS and that is what took it out
of the client path. Measured at hanzoai/[email protected]: master 2093
operations, cloud's emitted document 2333, shared 1908 — and 185 master
operations cloud's document does not have, every one of which reached python,
typescript, java and kotlin as a callable method.

Triaged those 185 at api.hanzo.ai with each operation's OWN method and a
nonsense-sibling CONTROL under the same prefix: 164 unfalsifiable (the control
answered identically — /v1/bot and /v1/dns 403 everything, /v1/vector and
/v1/search gate before routing, /v1/s3 and /v1/kv 404 both), 0 decidably absent,
19 decidably present-and-undescribed. A door is not a list. The master asserted
164 endpoints on evidence that cannot tell them from an invented path.

sdks.yaml loses `spec:`; there is no document in this repo a generator falls
back to. generate.py resolves the document from the CLIENT's `.spec-lock` — the
receipt hanzoai/ci's client: lane writes when a cloud release dispatches — and
refuses if the bytes behind that pinned ref hash to anything else. `--spec`
stays: the same document by value, which is what the lane passes.

The 19 live-and-undescribed are hanzoai/cloud's to fix and are recorded in
LLM.md. Nine are /v1/ai/*, the apps/ai seam projecting from a committed
plugin/ai/openapi.json subset rather than the mounted plugin's registry — a
second authority inside cloud, the same disease one level down.

merge.py, the authored specs and hanzo.yaml stay for skills.py, audit.py,
console and cloud-pulse. Nothing that generates a client reads them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 20:10:19 -07:00
zeekayandHanzo Dev 9713f29e7c iam: the dead store's document, and a path that 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 printing that
gap the whole time — 217 declared-and-unserved, 77 served-and-undeclared — and
nothing acted on it.

The 38 verb-noun addresses 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.
No binary in this fleet answers at any of them. 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, carrying the handlers' own doc comments, 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 left three (`get_v1_iam_users`,
`listOrganizations`, `addProvider`) — those are SDK method names, so the spelling
is the customer's business, not the emitter's.

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 — 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 beside
it: 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. audit.py reads it identically, so
`undeclared` stops counting 23 old spellings of operations every SDK already has.

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 on iam.hanzo.ai 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, each the same
handler as its /v1/iam twin.

audit.py after: undeclared 77 -> 0, prose lost 16 -> 0. `missing` is 58 and is not
a defect — it is iam's untyped surface, which the typed emission cannot see, and
it is now the honest measure of how much of iam is still raw handlers.

Gates: test_placement 4/4, test_flows 3/3, test_audit, test_skills 6/6, every
spec and the master parse.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 13:01:06 -07:00
zeekayandHanzo Dev 17f1234d52 placement: the catch-all stops claiming to be the inference API
`ai/openapi.yaml` opened with "NAMESPACE EXCEPTION — these routes are
TOP-LEVEL /v1/*, NOT /v1/ai/*" while 108 of its 109 paths were `/v1/ai/*`.
The sentence was true when it was written and stopped being true one
commit ago, when the inference edge moved to `gateway/openapi.yaml` — the
binary that answers it. A header that describes a surface the file no
longer holds is how a spec becomes the bucket everything unclaimed falls
into, and this one holds chats, stores, vectors, files, records, tasks,
workflows, videos, forms, graphs, scales, nodes, scans and templates.

The header now says what the file is, and states the two placement facts
about it plainly, because neither can be fixed in this repo:

  1. `ai` CANNOT BE REFUTED BY ANYTHING. apps/ai mounts the subsystem as
     one greedy `app.All("/v1/*")` — cloud's own comment calls that
     address "a FALLBACK, not a front door" — so the route table
     publishes ZERO /v1/ai paths and the registry has no opinion on any
     of the 108. Every other product is checked against that table; this
     one against nothing. Measured: all 85 GETs answer live, so nothing
     there is dead today. The point is that no one would know.

  2. EIGHT NOUNS ARE OWNED TWICE. /v1/ai/{users,applications,sessions,
     permissions,records,forms,providers,system} describe nouns
     /v1/iam/* already owns, from a different binary and a different
     store — which is why `hanzo ai users` and `hanzo iam users` both
     exist. Rule 1 of test_placement does not see it, because the
     PRODUCTS differ even though the capability does not. The fix is a
     route move upstream; deleting a served route here would only lose
     the capability.

The vendor-as-standard prose went with the surface it described. This
file no longer names two other companies' SDKs as the contract — that
belongs to gateway/, which already lost the same prose when it took the
routes.

AND THE ORPHANS THE LAST COMMIT LEFT. 27 tag declarations named buckets
no operation carried any more. That is not cosmetic: the tag namespace is
GLOBAL in hanzo.yaml and spellings fold case-insensitively, so `search`'s
leftover `Logs: "Configure logging"` — declared after
/v1/search/logs/stream was refuted — was being merged as the description
of cloud's `/v1/logs` OBSERVABILITY product. It reads as a sentence about
logging either way, which is exactly why nobody caught it. Dropping it
takes `logs` back to undescribed, where its own package doc can claim it.

`test_no_orphan_tag_declarations` is the gate, on the same principle as
`test_no_stale_exceptions` beside it: a declaration that names nothing is
a comment pretending to be a rule, and it goes when the thing it named
does. 28 -> 29 gates, all green.

LLM.md records what is NOT gated and why — the two `ai` facts above, the
18 tags two authored specs still both claim, the four served-but-
undescribed paths (three /health plus POST /v1/mcp), and the 39 under
search/kv/vector that auth-gate before routing so no unauthenticated
probe can decide them. A stated gap beats a silent one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 11:32:47 -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 11a70e6c5d sdks: the ruby row declared a client that exists in no registry
`sdks.yaml` carried a `ruby` row — repo `ruby-sdk`, `gemName: hanzoai_cloud`,
`gemVersion: 8.0.0` — with nothing on either end of it.
github.com/hanzoai/ruby-sdk answers a hard 404 with ZERO redirects, and that is
the distinguishing fact rather than a guess: every SDK repo that was actually
renamed answers a 301 instead (hanzoai/go-sdk -> hanzo-go/sdk, rust-sdk ->
hanzo-rs/sdk, cpp-sdk -> hanzo-cpp/sdk, kotlin-sdk -> hanzo-kotlin/sdk). So this
is absence, not a move the row failed to follow. rubygems has no gem under
hanzoai_cloud — the name the row itself declared — nor hanzoai, hanzoai-cloud,
hanzo_cloud, hanzo-ai, hanzo-client or hanzo-sdk. The gem literally named
`hanzo` there is mirego's Heroku deploy tool, not ours.

A row here is not inert. generate.py derives its whole language set from this
file, and README.md and LLM.md render it as the SDK matrix, so the row WAS the
claim — it is the only thing in the repo that asserted a Ruby client, and an
install line follows a matrix row. Removed from all three, with the reason
recorded in sdks.yaml beside the rust and cpp absences so the next reader does
not re-add it from the same wrong premise.

Ruby is NOT a departure in the sense that file defines, and the comment says so:
go, rust and cpp leave because their invocation needs a template or a layout
`take` cannot express, and they ship anyway. There is no Ruby client to ship. A
visible gap is honest; a row that generates into nothing is not. The `drop` list
keeps Gemfile, Rakefile and *.gemspec deliberately — it is a language-agnostic
denylist of build metadata and already carries go.mod/go.sum with go absent.

Gates unchanged and green: 25 passed, same set as before the edit, and
`generate.py ruby` now exits on an invalid choice instead of generating into a
repo that is not there.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 07:39:21 -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 23919e067d merge: an empty field from the winner must not delete a populated one
merge.py took the whole operation OBJECT from cloud wherever cloud took a route.
An untyped route's emission is an address and nothing else, so that replaced
described operations with undescribed ones: 47 request bodies and 135 response
sets (100 of them reduced to the synthesized `default`) 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, which is how it was caught: typed-flag
operations 574 -> 515, raw `--data` fallbacks 187 -> 378, five failing tests,
and `hanzo kms secrets create` missing the `value` field whose stdin-only guard
then had nothing to guard. A typed command degrading to `--data '{...}'` is the
raw escape hatch this whole pipeline exists to remove.

The mistake was mine and it is one word: I read cloud's SILENCE about an untyped
route as EMPTY when it means UNKNOWN — the same distinction the synthesized
`default` response exists to express, applied everywhere except here. Twice I
measured the damage and twice I measured only `parameters`, which is why 47
bodies stayed invisible: I was counting the field I had thought about instead of
the fields that changed.

fuse() overlays field by field now. TRUTH still wins existence, operationId,
tags and prose unconditionally, and wins any field it POPULATES. It cannot
delete one by being silent. A method it does not mention is left alone rather
than dropped, since a wildcard mount can serve what an emission never names.

requestBody and responses are kept; query parameters are not, and the asymmetry
is the rule rather than an exception to it. 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
quietly do nothing: a wrong answer rather than a missing method. Those 53 stay
dropped and counted.

Measured after: 0 operations still missing a body d4c7dbd had, 0 still reduced
to the synthesized default, parameters-or-body 1687 -> 1767 of 2454. All 47 body
losses were routes cloud serves UNTYPED and zero were typed-with-no-body, so
there is no emission bug to chase — only untyped routes to type.

And the response-schema gap this repo kept reporting was partly its own damage:
728 -> 637, with 91 of the "missing" schemas simply deleted by the merge. Every
figure published for it (754, 728, 696) was measuring this defect alongside the
real one, and /v1/billing/balance, /v1/billing/usage and /v1/agents/{ref}/run —
the three canonical flow operations two SDK lanes hand-wrote raw-decode helpers
for — are out of the gap entirely. Those helpers can be deleted.

Gate: validate 0 errors 0 warnings (recommendations 346 -> 240, since restored
schemas are referenced again); go 2335 files and compiles; typescript-axios and
java both generate; flows/skills/audit tests green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 10:04:04 -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 2831663903 docs: the published URL is private — a 404 there is a missing header
`https://raw.githubusercontent.com/hanzoai/openapi/main/hanzo.yaml` is the URL
every SDK generator already points at, and it answers 404 unauthenticated and
200 with `Authorization: Bearer $GITHUB_TOKEN`, because the repo is private.
GitHub returns 404 rather than 403 for a private path, so the failure reads as
a file that moved rather than a header that is missing, and under `curl -f` +
`set -e` it surfaces as a build that stopped with nothing naming the cause.

`hanzoai/js-sdk`'s `scripts/generate.sh:22` fetches it unauthenticated today.
Named here rather than fixed here — that repo owns its call site — and the
in-repo path (`generate.py`, reading the local file) needs no network at all.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-31 08:51:49 -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 d4c7dbd371 spec: delete the 18 products authored here and served nowhere
Each 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 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 telling 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/
    TOTAL              655   849

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 needed 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, 401 with 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 catch-all (/__total_nonsense_xyz__ returns the same page), so they were
never API routes, and 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 instead of 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 (2368 -> 1519 ops) and the removed set is exactly the
products above — no other path key moved. A product returns to this repo the
day it is actually served.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 20:51:08 -07:00
zeekayandHanzo Dev 2818cd9542 docs: name the five consumers, and mark paas superseded by platform
skills.py has NO liveness filter — it opens <svc>/openapi.yaml directly and
emits a SKILL.md per authored operation, so an unserved route ships as a live
instruction to call a dead endpoint. The CLI is protected by refutation; the
skills plane is protected by nothing but this file being true. LLM.md now names
all five consumers and which of them filter.

paas: cloud main (b87a38df) mounts no /v1/paas at all, and the deployed build
answers only the fleet subset that main renamed to /v1/platform/fleet/*. This
spec's org/project/env/container tree belongs to the standalone PaaS service the
unified binary replaced; /v1/paas/cluster/info 404s live. Annotated, not deleted.

platform: recorded a real debt rather than papering it. The 33 REST routes cloud
actually serves under /v1/platform (projects/apps, sites, fleet) are authored
NOWHERE, while the 131 paths in platform/openapi.yaml are the older RPC-style
surface and are served nowhere. /v1/platform/projects 403s (auth-gated, route
exists) and /v1/platform/health 200s, so the plane is live — it just cannot
reach an SDK or a `hanzo` command until it is authored.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 10:22:38 -07:00
zeekayandHanzo Dev edc2106821 feat(generated): publish hanzo.json — the cloud binary's own spec, measured against the master
The 69 per-service specs are hand-written, so they drift in both directions at
once: hanzo.yaml declares 2106 operations nothing serves, and the binary serves
1012 no SDK can see. Neither number was knowable before, which is why the last
note about it in LLM.md ("/v1/memory and /v1/videos are LIVE") was wrong in the
other direction — neither is in the router at all.

generated/hanzo.json is what the binary says about itself: 959 paths / 1358
operations / 153 schemas / 138 product tags, emitted by `make openapi
OPENAPI_DIR=<this repo>` in hanzoai/cloud. One document folded from two
readings of ONE router — the live route table gives every operation its address
and product, zip's typed-op registry gives the 157 typed ones their JSON
Schema, parameters, responses and the prose cmd/zipdoc lifts out of their doc
comments. A route that is not a typed op carries its address and nothing
invented.

It is named for hanzo.yaml, not for cloud/: the binary serves the whole fused
/v1 surface, so it is the generated counterpart of the MASTER, and audit.py now
finds a contract at the root as readily as in a directory — one script measures
a service and the master, rather than two that disagree.

Nothing is overwritten. `derived:` is the ratchet, and it is empty: a generated
spec replaces a contract only once audit.py shows it covering that contract,
and from then on --check fails the build if it stops.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:46:01 -07:00
zeekayandHanzo Dev 11b7f58d22 spec: fold the held branches' forward content into the master
- capabilities.yaml: record six more collapses (featuregate, ads, cms, cron,
  connectorruntime, gojabase) so every LIVE name resolves to one canonical
  capability.
- kms: define SdkEnvelopeIdentity. The schema was referenced but not declared,
  so the master carried a dangling pointer and every generated SDK lost the
  identity block on the envelope.
- git/o11y/research/world: info.version 8.0.0, the generation the rest of the
  specs already declare.
- LLM.md: state the V8 generation and the bare-operationId convention merge.py
  actually enforces.
- regenerate hanzo.yaml + CAPABILITIES.md from the sources.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 19:38:49 -07:00
zeekayandGitHub 5659db6574 refactor(openapi): collapse 3 capability registries to ONE source (capabilities.yaml) (#28)
Three hand-maintained capability registries drifted (8 vs 9 vs hardcoded) — a
one-and-one-way violation. Collapse to ONE machine-readable source, everything
else derived.

Before: 3 sources
  - capabilities.yaml     (structured manifest, read by nothing)
  - CAPABILITIES.md       (10KB hand-written index, wrongly called canonical)
  - merge.py GROUPS dict  (hardcoded, "mirrors CAPABILITIES.md by hand" — the
                           only one that actually drove x-tagGroups)

After: 1 source + 2 derivations
  - capabilities.yaml is the ONE source of truth
  - merge.py READS it (GROUPS dict deleted) → hanzo.yaml x-tagGroups (domain
    titles + Core) AND generates CAPABILITIES.md (now GENERATED — DO NOT EDIT)

Build invariant enforced (fail loud, was only promised): every present
<svc>/openapi.yaml dir maps to exactly one entry across domains∪core (orphan /
unlisted / double-listed → sys.exit); collapsed name must have no spec dir;
internal excluded from master + x-tagGroups.

Reconciled capabilities.yaml so the build PASSES without losing capability,
grounded in `ls */openapi.yaml`:
  - app → applications, do → platform (grouped, not folded; review notes kept)
  - world → applications (dir exists, was unlisted)
  - s3 → data, observe → observability (dirs exist → moved out of `collapsed`,
    which requires no dir; fold-later intent preserved in `review`)
  - team/wallets/treasury: no spec dir → dropped from domains, kept in `pending`

hanzo.yaml aggregation unchanged in substance: 1793 paths / 999 schemas / 150
responses / 39 params / 68 tags — zero added, zero removed. Only x-tagGroups
names change (8 → 9 groups, using domain titles).

Also: gitignore __pycache__/ (was polluting the tree).
2026-07-08 14:01:38 -07:00
55cfba4d7e spec: canonical capability manifest + category grouping (#25)
Add CAPABILITIES.md — the authoritative index of every public Hanzo
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 as non-public.

merge.py now emits x-tagGroups so the unified hanzo.yaml is navigable by
category, and fails closed if any present spec is ungrouped (drift guard).
Regenerated hanzo.yaml (content identical; +8 x-tagGroups). README + LLM.md
point at the manifest as the source of truth.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-08 08:43:40 -07:00
d4ddab9921 spec: codegen-clean hanzo.yaml + retire Stainless (openapi-generator for all langs) (#24)
* fix(spec): add SPDX license identifier (openapi-generator 3.1 requires it)

openapi-generator 7.14.0 errors 'info.license.identifier is missing' for 3.1
specs. Add the SPDX custom identifier LicenseRef-Hanzo-Proprietary alongside the
Proprietary name. Last spec-validation blocker for the openapi-generator path.

* fix(spec): make hanzo.yaml fully codegen-clean for python/go/ts

Retire Stainless: python/go/js SDKs now generate from hanzo.yaml via
openapi-generator (like cpp/dart). These spec fixes make the merged
surface compile clean across the go, python (pydantic v2) and
typescript-axios generators — all verified locally at 0 errors.

- pubsub ack_wait: type integer default 30000000000 (ns) overflows int32
  -> format: int64 (Go build fix).
- platform DeployJob/CancelDeploymentJob: oneOf of inline anonymous
  objects + `const` emitted an empty TS union type. Extract each variant
  into a named component schema referenced by $ref, and use enum:[x]
  instead of const:x for cross-generator support.
- merge.py: namespace operation tags per-service (svc_Tag), same
  discipline as schemas/operationIds. Cross-service tag collisions
  (even case-only, MCP/mcp, API Keys/api-keys) were fusing unrelated
  ops into one cross-contaminated API class and colliding on
  case-insensitive filesystems. Collapse to a single primary tag so one
  op maps to exactly one API class (no duplicate request models). Also
  namespace discriminator.mapping pointers, which prefix() missed.

* ci: retire Stainless — dispatch spec-update to python/go/js SDK repos

python/go/js now generate from hanzo.yaml with openapi-generator (like
cpp/dart), each repo owning its scripts/generate.sh + generate.yml. The
regenerate workflow replaces the Stainless build job with a
repository_dispatch(spec-update) fan-out to the three repos. LLM.md SDK
section rewritten: Stainless retired, per-language generators + publish,
remaining /v1/memory + /v1/videos spec-coverage gaps recorded.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 20:07:24 -07:00
01c59d173f Reconcile hanzo.yaml to the live cloud surface — 1698 paths, all products (#18)
* spec(ai): define /v1/images/generations + /v1/audio/{speech,transcriptions}

The ai spec's description already advertised /v1/images/* and /v1/audio/*
but never defined the operations. Add the OpenAI-compatible image generation,
text-to-speech, and speech-to-text (Whisper) operations + schemas so the spec
matches the SDK image/audio resources and what hanzo.chat consumes.

* spec(app): add hanzo.app projects & deployments surface (/v1/projects/*)

hanzo.app calls GET/POST /v1/projects, GET/PATCH/DELETE /v1/projects/{slug},
POST /v1/projects/{slug}/deploy (tar artifact or git build), and
GET /v1/projects/{slug}/deployments — none of which were in any spec.
New self-contained app service spec, top-level /v1/projects/* (path is the
contract, same rationale as top-level inference). Org is resolved server-side
from the IAM JWT owner claim, never sent by the browser.

* spec(base): add Hanzo Base records surface (/v1/collections/{collection}/records)

hanzo.app persists each site's working draft as a row in the Base site_drafts
collection (gated by VITE_BASE_URL) via GET(list+filter)/POST/PATCH on
/v1/collections/{collection}/records. Add a self-contained base service spec
covering the full record CRUD so the frontend contract is complete.

* spec(cloud): scrub literal '/api/' from product-surface comment

The only '/api/' occurrence left in any spec was a documentation comment.
Reword so 'grep -rn /api/' returns zero across the repo; meaning is unchanged
(every route is /v1/*).

* feat(openapi): regenerate unified hanzo.yaml (38 services, 1590 paths)

Regenerated via merge.py after adding app + base and the ai image/audio ops.
Verified: zero /api/ paths and zero /api/ literals across all specs; the only
non-/v1 routes are the unversioned infra probes /health, /healthz, /metrics.
README service index gains app + base rows.

* fix(merge): namespace operationIds by service in unified master

Two services can each define a unique-within-themselves operationId that
collides in the merged master (e.g. base.listRecords vs another service).
Redocly flagged 23 operation-operationId-unique errors, making the master
invalid as a single generatable document. Prefix operationIds with the
service tag — the same namespacing already applied to schemas/responses/
params — so the master is a globally-valid OpenAPI doc every client can
derive from. Per-service specs (Stainless inputs) keep their bare
operationIds. Master lint errors: 369 -> 100 (remainder is pre-existing
per-service nullable/summary debt, unrelated to this change).

* docs(sdk): document the one canonical regeneration path per SDK

hanzo.yaml is the source of truth; JS/Python/Go regenerate via the Stainless
'hanzo-ai' project (push spec -> PR), Rust is hand-written crates (not
spec-generated). Records the exact 'stl builds create' command + KMS-sourced
key for the hosted step.

* docs(openapi): document /v1/sdk enveloped secrets+sign surface (kms)

Add POST /v1/sdk/secrets — the SDK-facing native secrets plane served by
kmsd. Single RPC endpoint: the operation is the SIGNED op field, not the
URL; the ML-DSA-65 envelope IS the credential (no bearer, security: []).
Adds SdkEnvelope + SdkEnvelopeIdentity schemas. Regenerated hanzo.yaml.

* docs(sdk): record the real generation drift + the one-way

The published python/go/js SDKs derive from the LEGACY LLM-gateway spec, not
hanzo.yaml — they cover inference + gateway-admin only, none of the cloud
products. README implied otherwise; correct it. LLM.md now records: repoint
Stainless hanzo-ai -> hanzo.yaml, the 28 live /v1 prefixes with no spec, the 4
prefix-mismatch re-bases, and the stale monolith specs (cloud/nexus). Add cpp +
dart rows (openapi-generator on ARC) — one repo per language, one spec source.

* spec(agents,functions): add /v1/agents + /v1/functions surfaces from clients/*

Route-accurate to hanzoai/cloud/clients/{agents,functions}. agents covers
the agent CRUD + metered run + live session tree; functions covers
serverless CRUD + metered invoke + triggers/deployments/secrets/metrics.
Self-contained; regenerated hanzo.yaml (0 dup operationIds, 0 dangling refs).

* spec(framework,tracker,crm): add business-app surfaces from clients/*

Route-accurate to hanzoai/cloud/clients/{framework,tracker,crm}: framework
doctype/module/role + generic document CRUD, tracker projects+issues,
crm companies/contacts/opportunities.

* spec(automations,integrations,admin,plan): add orchestration + admin surfaces

Route-accurate to hanzoai/cloud/clients/{automations,integrations,admin,plan}:
automations flows/versions/runs/pieces/mcp, integrations provider-connect +
Slack, admin god-view aggregates, plans catalog/resolve/entitlements (the
plan spec expands the fixed-binding route loop, not just literal app.Get calls).

* spec(visor,zt): re-base onto live flat prefixes from clients/{visor,zt}

* spec(kb,prompts,templates): add /v1/{kb,prompts,templates} from clients/*

* spec(git,graph,do): add /v1/git + /v1/{indexers,oracles} + /v1/{vpcs,load-balancers} from clients/*

* spec(billing,security): add /v1/billing + /v1/security from clients/*

* spec(exec,websearch,tasks): add /v1/{exec,websearch,tasks} proxy surfaces; regen hanzo.yaml (58 services); changelog

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 15:26:21 -07:00
zooqueenandHanzo Dev 89213ac511 docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-authored-by: Hanzo Dev <[email protected]>
2026-06-17 10:01:30 -07:00
zeekay cba26d8e46 v1.0.0 lock-in
Master discovery spec, README, CHANGELOG, shared schemas, and LLM.md
brought to v1.0.0 final shape alongside the per-service commits in this
series.

- hanzo.yaml: rewritten as a thin /v1/discovery + /v1/health index. The
  prior 1202-line union-of-services dump is gone; per-service detail
  lives in each service's own self-contained spec.
- README.md: service index with hostname and gateway prefix per service.
- CHANGELOG.md: 2026-05-31 v1.0.0 entry documenting routing, IAM REST
  migration, security scheme consolidation, and coverage table.
- shared/auth.yaml: legacy /api/ reference replaced with /v1/iam/* path.
- LLM.md: forward-only conventions for future spec edits.

Verifications (zero results expected):
- grep "/api/"                in yaml: 0
- grep "deprecated:.*true"    in yaml: 0
- grep cross-brand word-bound in yaml: 0
- openapi-spec-validator      on all 35 specs: all OK
2026-06-01 12:44:10 -07:00
zeekay 45aa8a1010 chore: symlink AGENTS.md + CLAUDE.md → LLM.md 2026-04-06 15:07:24 -07:00
zeekay 882730d359 chore: remove stale LLM.md 2026-03-03 14:00:00 -08:00
zeekay fb2a3f5f61 docs: add LLM.md project guide 2026-03-11 10:32:25 -07:00