Files
python-sdk/hanzo.yml
T
zeekayandHanzo Dev 3a09eb4319 regenerate from cloud@5126939 — the client leaves the hand-merged lineage
pkg/hanzoai/cloud goes +2186 -2126 ~175: 1700 paths / 2354 operations / 2186
schemas, 182 api modules and 2172 model modules, against the 239/1116 it carried
from hanzo.yaml. The document is hanzoai/cloud's own emission now, pinned in a
new .spec-lock by commit and sha256, and generate.py reaches it by value —
--skip-validate-spec means the 1012 missing-`responses` errors no longer write
zero files, so a client can be cut from the authority instead of a projection of
it.

Two renamings come with that, and both are the fix rather than the damage. IAM's
types are namespace-qualified (iam.Role, iam.Application, 95 of them) because a
bare `Role` was two unrelated shapes wearing one name. And the <svc>_ prefix is
gone from every operationId, so every generated method lost it.

The prefix is what broke the examples, and the gate saw only two thirds of it.
Four flows failed on their imports; `money` and `tools` PASSED while every call
in them named a method that no longer existed — an import resolves the names in
the `from … import` line and a method is looked up at call time. All five flows
now name operations that exist, checked by resolving each one as an attribute,
and hanzo.yml records that ceiling so the next reader does not trust the gate
for more than it says.

`chat` is removed, which is a measurement and not a preference: cloud declares
POST /v1/chat/completions with no requestBody and no responses at all, so the
generated method takes no body and returns None — the one call a chat example
exists to make cannot be expressed. Inventing the type, or hand-rolling the HTTP
inside a generated client, is the drift these SDKs exist to prevent; js-sdk
dropped its own chat flow at 2.0.7 for exactly this. It returns the release
cloud gives that route a body.

Two smoke assertions were already red before this regeneration and are now true
again: they pinned AIApi/APIKeysApi/MCPApi and AdminApi.plugin_admin_*, names
from the retired lineage. The surface is all still there under AiApi/KeysApi/
McpApi and AdminApi.admin_plugins.

tests/test_zap_transport.py still errors, exactly as it did before: it imports
`Hanzo` from hanzoai, which the hand-written package does not define. That is
not this document's business and is untouched.

Minor rather than patch: every generated method changed name.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 11:00:03 -07:00

115 lines
5.8 KiB
YAML

# Hanzo CI — the ONE way this repo is gated, read by hanzoai/ci.
#
# No `images:` and no `deploy:`: these are libraries. A consumer installs them
# from PyPI at a tag, so what CI owes is a gate, not an artifact. Publishing
# stays where it already is — .hanzo/workflows/publish-pypi.yml on our own
# runners, which is the canonical path because it reads the PyPI token from KMS
# like every other publish credential in the fleet. Adding a second publish here
# would be one too many; this file only gates.
#
# Scope is the generated cloud client and the flows that exercise it. The other
# 64 packages under pkg/ are hand-written and carry their own tests; pulling
# them in here would make a red gate mean "something, somewhere" instead of
# "the client the spec just produced is broken".
test:
- name: cloud-client
# Import every generated module. For generated code this IS the build step:
# there is no compiler to catch a bad $ref, a name collision or a model that
# references a class the generator declined to emit — the import is what
# turns those into a failure instead of an AttributeError in a user's app
# six months later.
#
# The arc runner image promises no interpreter, so provision one with uv
# when it is missing. A gate that silently no-ops is worse than no gate.
run: |
set -e
if command -v uv >/dev/null 2>&1; then UV=uv; else
curl -fsSL https://astral.sh/uv/install.sh | sh
UV="$HOME/.local/bin/uv"
fi
PYTHONPATH=pkg "$UV" run --no-project --python 3.12 \
--with pydantic --with python-dateutil --with urllib3 --with typing-extensions \
python -c '
import pkgutil, importlib, sys
import hanzoai.cloud.api as A, hanzoai.cloud.models as M
bad = []
for pkg, label in ((A, "api"), (M, "models")):
n = 0
for m in pkgutil.iter_modules(pkg.__path__):
try:
importlib.import_module(f"{pkg.__name__}.{m.name}"); n += 1
except Exception as e:
bad.append(f"{pkg.__name__}.{m.name}: {e!r}")
print(f"{label}: {n} modules imported")
for b in bad[:20]: print("FAIL", b, file=sys.stderr)
sys.exit(1 if bad else 0)
'
- name: examples
# The canonical flows, imported against the client that was just generated.
# Each `from hanzoai.cloud import X` is an assertion that the symbol still
# exists, so a spec change that renames or drops a MODEL or an API class
# goes red here rather than in a user's app.
#
# Importing is enough BECAUSE each flow guards its call behind
# `if __name__ == "__main__"` — the module body resolves every name without
# opening a socket, so the gate needs no API key and no network.
#
# KNOWN CEILING, so nobody reads this gate for more than it says: an import
# resolves the names in the `from … import` line and NOTHING ELSE. A method
# name is looked up at call time, so a renamed OPERATION passes here and
# fails in a user's app. Measured: when the `<svc>_` prefix left every
# operationId, `money` and `tools` went on passing this gate while every
# call in them named a method that no longer existed. Four flows failed on
# their imports and two were silently broken — the gate saw four.
#
# `chat` is NOT in this list, and its absence is a measurement rather than a
# decision. cloud declares POST /v1/chat/completions with no `requestBody`
# and no `responses` at all, so the generated method takes no body and
# returns None: the one call a chat example exists to make cannot be
# expressed. Inventing the type here, or hand-rolling the HTTP inside a
# generated client, is the exact drift these SDKs exist to prevent, and
# hanzoai/js-sdk dropped its own chat flow at 2.0.7 for this same reason.
# Restoring it is one step and the test is one line: when
# paths['/v1/chat/completions'] in cloud's openapi.yaml carries a
# requestBody, add examples/chat back and put "chat" back in the tuple.
run: |
set -e
if command -v uv >/dev/null 2>&1; then UV=uv; else
curl -fsSL https://astral.sh/uv/install.sh | sh
UV="$HOME/.local/bin/uv"
fi
PYTHONPATH=pkg "$UV" run --no-project --python 3.12 \
--with pydantic --with python-dateutil --with urllib3 --with typing-extensions \
python -c '
import compileall, importlib, sys
ok = compileall.compile_dir("examples", quiet=1, maxlevels=3)
bad = []
for flow in ("hello", "money", "store", "agent", "tools"):
try:
importlib.import_module(f"examples.{flow}.__main__")
print(f"examples.{flow}: OK")
except Exception as e:
bad.append(f"examples.{flow}: {e!r}")
for b in bad: print("FAIL", b, file=sys.stderr)
sys.exit(1 if (bad or not ok) else 0)
'
# THE CLIENT LANE — this repo is a PROJECTION of one API document at one version.
#
# hanzoai/cloud's release sends `repository_dispatch: spec-update` carrying
# (version, sha, spec_sha256); hanzoai/ci fetches openapi.yaml AT THAT SHA,
# REFUSES if the bytes hash to anything else, runs `generate:`, writes
# `.spec-lock` beside the code, and — only after the `test:` block below has
# passed over exactly those bytes — commits and cuts a patch.
#
# The document moved from hanzoai/openapi `hanzo.yaml` (hand-merged, 1742 paths,
# fed by nothing, and NOTHING HAS EVER SENT the spec-update this repo listens
# for) to hanzoai/cloud `openapi.yaml` — smaller (1058 paths), emitted from the
# code by each app's own router, and gated on every cloud release. A smaller true
# document beats a larger unverified one.
client:
generate: ./scripts/generate.sh
version: "pyproject.toml:grep -m1 '^version' pyproject.toml | sed -E 's/.*\"(.*)\".*/\\1/'"