The client lane checks the committed client against the document
api.hanzo.ai serves now, not against the ref .spec-lock pins. Cloud
v1.833.257 (2774987c) went live after 8.5.163 was cut and rewrote the
POST /v1/decisions description (cad76e1a0: Jev by OpenRouter's vendor
ids), so the served document moved from 296b1f84 to 783f66a3 and every
push since has failed the check on ai_api.py's docstring. The drift
reproduces identically at e34ab7e1; nothing in this tree caused it.
This is the lane's own move, done by hand: .spec-lock names the served
revision and digest, the generator reruns over those bytes, uv.lock
takes what `uv run` wrote, and the version takes the next patch.
v8.5.160 regenerated the client and its release run passed the client check,
then stopped at declared-imports: the step called bare `python`, the runner has
none on its PATH, and exit 127 from a step that is report only ended the run
before anything was published. It now runs through uv like the three steps
beside it. Verified by running the whole test: block with no python on PATH.
Co-authored-by: Hanzo Dev <[email protected]>
The committed client had fallen behind the API it projects: 116 modules the
document now names were missing, 13 it no longer names were still here, and
30 had moved. The release lane refuses to publish a projection that does not
match the document, so v8.5.158 and v8.5.159 were tagged and never reached
the index.
Regenerated from the document production serves (sha256 aa1b79d0797704608e25a3898adb2ca69fec82a03f74c09671c372b63066cafe),
with the driver at hanzoai/openapi@88ea26b — the same inputs the lane reads,
and the same +116 -13 ~30 it reported. The repo's own gate ran over exactly
these bytes: 121 api and 2675 model modules import, no class declares a field
twice across 2804 modules, and every example flow resolves its method names.
Co-authored-by: Hanzo Dev <[email protected]>
Six capabilities hang off the client as first-class methods rather than as
generated operations a caller assembles for itself: budget (allowance, wallet,
tier, charged ledger), policy (one authorization question), audit (the org's
trail, read-only by construction), search (one ranked set over every corpus),
kb (the corpus you write and the connectors that fill it), and graph
(assertions with provenance and time). The same six words, the same method
names and the same answer type as the Go and TypeScript clients.
A refused budget and a refused policy are answers. Answer is a union of three
frozen types with a discriminant — Ok, Denied, Held — so a caller reads
"denied, and here is why" without catching, and reading .value off a refusal
raises it rather than handing back a half-answer. One rule maps HTTP to an arm:
a 202 whose body says held is a hold, 402 and the four refusal codes on 403 are
refusals, and a status that decided nothing raises. Every arm carries the
x-request-id, which is the word audit.Event.request uses too — the join that
lets a caller find the row the server wrote for the call it just made.
Auth is IAM and nothing else. The client exchanges its own clientId and
clientSecret for a short-lived access token (client_credentials,
client_secret_basic, RFC 8707 resource), holds it until shortly before expiry,
and re-mints once on a 401. It no longer accepts a bearer, so HANZO_API_KEY,
Configuration(access_token=) construction and result() are gone rather than
deprecated: two ways to hold one credential is the defect this removes. as_
still scopes to a subject and now mints from the client's own token.
hanzoai resolves its public names on first use, and the capability layer is
written over the wire rather than over the generated client, so importing the
six costs milliseconds instead of 2,700 generated modules.
ruff and ruff format are clean, mypy is clean over the thirteen new and changed
modules, and 106 tests pass. The two failures in test_smoke are the ToolsApi
name the current document no longer carries, and they fail at HEAD too. The
mint was exercised against the live hanzo.id, which answers invalid_client for
a credential it does not know; the six were exercised against a stub asserting
the exact request and decoding the shapes cloud's own schemas declare.
examples/six runs the whole thing as a caller would.
Co-authored-by: Hanzo Dev <[email protected]>
The hanzo-tools-core suite imports ten sibling tool distributions and
asserts an exact TOOLS count, but they were only ever named in
[tool.uv.sources]. A source resolves a package; it does not make anything
require one, so `uv sync --dev` uninstalled all ten and left the suite at
22 failed / 2 passed. Naming them in the dev dependency-group restores
24 passed, 12 skipped from a clean sync.
pkg/hanzo-dev-py never existed — the directory is pkg/hanzo-dev — so the
eight excludes and workspace members spelling it were inert. mypy.ini is
the config that actually loads, and with the name corrected it stops
type-checking a package it was written to skip (18 errors -> none).
cloud.py cited APIKeyPrefixes as the authority on key prefixes; no such
symbol exists. isAPIKey is the real gate and it admits hk-, sk-, pk-, fw_
and hz_, so NO_KEY was telling callers the wrong two.
(cherry picked from commit b2024662400895e456d4b3811fd286565e4e7109)
GET /v1/models declares a response schema, so the generator emits
AiApi.get_models() -> AiModelList and no longer emits ModelsApi at all.
The example imported the class that folded and failed at import, which is
the whole examples gate.
It reads the typed object now instead of the raw body and its own JSON —
the change its docstring said would arrive the day the schema landed.
One product, one number: the API and every client cut from it answer to the
version cloud's release train mints, so a reader holding 8.5.156 can name the
release it came from. The clients had drifted to their own counters -- 8.5.89,
8.5.91, 0.1.0, 0.1.0-alpha.5 -- because the fanout that carries the number can
only run in check mode on this forge and never writes.
Hanzo 8 is one product on one number. The API, the six generated clients, the
CLI and the console all answer to 8.5.89, so a client version names the API it
was cut from without a compatibility table.
8.5.89 is the first cut past the highest 8.x anyone had reached (@hanzo/console
8.5.88). Every register moves FORWARD into it; nothing is renumbered backwards.
Co-authored-by: Hanzo Dev <[email protected]>
RELEASE.md described GitHub Actions, GitHub secrets and three bin/ scripts
that each took a PyPI token off the environment. None of that publishes this
repo — .hanzo/workflows/publish-pypi.yml does, reading the token from KMS —
so the scripts are gone and the page describes the lane, its tag mapping, and
what to check before a tag makes a version permanent.
LLM.md carries the published state: hanzoai 3.2.13 with get_keys/get_tools,
and the KMS route that a 404 will otherwise misreport as a missing secret.
The job asked for /v1/kms/orgs/hanzo/secrets/<path>/<key> and parsed
.secret.value. KMS serves GET /v1/kms/secrets/<path>/<key>?env=<env> and
answers {"env","name","value"}, taking the org from the token's own claim,
so every read 404'd and every release reported the credential unseeded —
including hanzo/prod/python-sdk-publish/PYPI_TOKEN, which is there.
Root to 3.2.13, README install line names it. hanzo-research declares its
readme, license and authors, so its page carries them.
hanzoai/cloud@113cf7d6 declares components.securitySchemes.bearer and applies it
document-wide, so regenerating writes a populated Configuration.auth_settings()
and auth_settings=['bearer'] into 2498 call sites; ApiClient._apply_auth_params
sets Authorization: Bearer <token> from access_token, which was read by nothing
before. Four call sites carry an empty list — exactly the operations the document
marks security: [] (get_models, get_models_providers, get_commands,
get_openapi_json) — so the public routes still go out with no credential.
examples/client.py drops the header_name/header_value compensation and passes
access_token. examples/models is a new flow on the public catalog: it runs before
a reader has a key, which makes the install checkable in one command.
Proven on api.hanzo.ai, not by reading: examples.models prints 112 models with no
credential; examples.hello returns the caller's keys with a real sk- and 403
"sign in to manage API keys" with a bogus one.
README: PyPI serves 3.2.1 and this tree is 3.2.11, so the install line is the
git one. Between the two the default version left the operation ids — 3.2.1
carries get_v1_keys/get_v1_tools (182 api modules, 2172 models) and this carries
get_keys/get_tools — so `pip install hanzoai` above a `get_keys()` quickstart
hands the reader a client that cannot run it. Counts are measured off the pinned
document: 1814 paths, 2479 operations, 191 tags + default_api = 192 API classes
over 2460 models, 2659 importable modules.
Auth was wrong everywhere and it was the same root cause: the document declares
no securityScheme, so every operation got an empty auth_settings and
Configuration(access_token=…) is read by nothing. Serializing get_keys from such
a Configuration produces a request with no Authorization header at all — the
five flows have been calling anonymously and reading the refusal as a bad key.
examples/client.py now passes the bearer through ApiClient's header_name/value
pair, verified against a local server that echoes what it received, and a smoke
test pins both halves so the day cloud emits the scheme, the test fails and the
header moves back into Configuration.
The examples gate resolved class names and stopped there, which is how `money`
and `tools` once passed it while every call in them named a method that no
longer existed. It now walks the syntax tree for attribute accesses on anything
bound to a *Api and asks the class: ten names resolve, and a stale get_v1_tools
fails in the same second the import passes.
Deleted five example scripts that imported hanzoai.Hanzo, hanzoai.cluster,
hanzoai.agents and hanzoai.completion — none of which exist — or were dict
literals captioned "in practice this would be called through the MCP interface";
compileall passed all five, because syntax is not a claim. Deleted three docs
that were fabrication rather than drift and that nothing rendered: FEATURES.md,
QUICKSTART.md, GPT5_ORCHESTRATION.md.
pkg/hanzo-agent said `pip install hanzoai` and `from hanzoai import Agent,
Runner`; the client has no Agent. Its own `full` extra resolved to
hanzoai[web3,tee,marketplace,cli], extras the client does not declare, so it
installed the client and none of the extensions. pyproject said BSD-3-Clause
while LICENSE is the Apache 2.0 text. test_smoke imported tracker_api and no
/v1/tracker path is emitted any more, so the suite carried a failing test naming
a module the client does not have; it reads analytics_api now, off the client.
A regeneration on 2026-08-09 claimed v3.2.10 and tagged it, but that commit
never reached main, so main went on declaring 3.2.10 too and I bumped onto
the same number. Two trees cannot share a version: the tag names the other
one.
The next free patch is what the version should have been. Read the TAGS, not
just pyproject.toml — the tag is where a version is claimed.
Regenerated from hanzoai/cloud@6a719cdc (.spec-lock carries the ref and the
digest). 3.2.9 was a projection of a document 343 commits old, and in between
the operationId stopped carrying the default version: 5196 method names the
client published — `get_v1_tools`, `post_v1_agent`, every one of them — name
operations the document no longer declares. None remain.
The id is one identity, not four. An operationId is the SDK method, the MCP
tool name, the CLI spelling and the by-name key, so a client that keeps the
old spelling does not merely look stale — it teaches a name nothing answers to.
Shape: 1813 paths / 2479 operations -> 192 api modules + 2460 model modules,
all 2659 importable.
The regeneration onto hanzoai/[email protected] landed without moving this, because
the bump it carried named a version two releases behind: another line had already
cut 3.2.7 and 3.2.8 while it was in flight. The tree is right and the number was
not.
The document types 1495 of its 2459 operations, up from 1242. 87 new model
modules carry the response shapes ai, eval, functions, domain, experiments,
security and benchmark now declare, so those calls return a typed object
instead of a dict. 2524 generated modules import clean.
.spec-lock moves from v1.801.497 to @ffbc86064, which is v1.801.498.
This distribution shipped two complete generated clients under one import root.
`hanzoai.cloud` is the current one, generated from hanzoai/cloud's openapi.yaml at
the ref .spec-lock names. `hanzoai.api` / `hanzoai.models` was another, and the
package root re-exported its 2,202 names flat — so the obvious spelling,
`from hanzoai import ApiClient`, reached the second client, while
`hanzoai.cloud.ApiClient` reached the first. Nine core names existed twice as
distinct classes bound to distinct documents, and which one a caller got depended
on which import line they happened to write.
The second lineage was not an older copy of the first. It was a projection of a
document that no longer exists: of the 1,695 routes it binds, 243 are still in the
document at v1.801.495 and 1,452 are not. Eighty-six percent of it builds a
request the server answers with a 404. It had been broken for as long as it had
been shipping, and nothing said so, because a client only fails at the call.
So it goes, along with its runtime — api_client, api_response, configuration,
exceptions, rest — which `hanzoai.cloud` carries its own copies of. The package
root now exports `cloud` and nothing else. Nothing is re-exported flat on purpose:
a name that appears in two places is a name that can disagree with itself, and the
namespace already says which document a symbol came from.
The smoke tests pinned the deleted surface — they asserted more than 300 api
modules and imported `hanzoai.api.tracker_projects_api` by name, which is how a
suite can stay green over a client that 404s. They now assert the surviving one,
plus the invariant that made this possible in the first place: exactly one
generated package under `hanzoai`, and none of the nine core names re-exported
flat beside it.
hanzoai 3.2.0 is on PyPI as of 18:16:29Z, and its wheel is byte-identical to the
one built here before the o11y fix landed — so the published artifact still has
o11y.GettableAgentCheckIn declaring six fields where the document declares
eight, silently dropping an old AWS agent's `removed_at`.
This tree has that fix and the zap suite, and it was still calling itself 3.2.0.
One version naming two different sets of bytes is the failure this repo already
learned once from npm; the number moves so it cannot happen quietly. Nothing is
published here — 3.2.1 goes out with the operationId unification, or sooner if
the shipped data loss is judged to warrant its own release.
Co-authored-by: Hanzo Dev <[email protected]>
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]>
README-only. pypi.org/project/hanzoai renders the long description, so the
install line, the zen5-coder model id and the dropped operation counts only
reach the registry page on a release.
Co-authored-by: Hanzo Dev <[email protected]>
The document grew: cloud's woven spec was merged and won, taking hanzo.yaml from
1132 paths / 1519 operations / 779 schemas to 1737 / 2452 / 1798.
pkg/hanzoai/cloud/ is now 263 api + 2031 model modules, all importing.
`generate.py python --check` reports [python] clean.
The resync also RENAMED nearly every operationId to cloud_<method>_<path>, which
renames every generated method: billing_billingBalance became
cloud_get_v1_billing_balance, cloud_AgentsController.Create became
cloud_post_v1_agents. Examples written against the old names stop resolving —
exactly what the examples gate exists to catch. It went red; this is the fix.
Every flow was re-probed against api.hanzo.ai unauthenticated and with a bogus
key, because a spec says what SHOULD be served and only a request says what IS.
All eleven operations answer 401/403 — routed and identity-gated. Two moved:
store to the PROVISIONING plane (POST /v1/kv, GET|DELETE /v1/kv/{name}). The
per-key data plane the spec also describes is mounted nowhere: GET
/v1/kv/keys/{key} 404s, PUT and DELETE 405, kv.hanzo.ai 404s the whole
prefix. A round-trip on keys could not run.
tools to GET /v1/tools, the catalog. A live JSON-RPC door at POST /v1/mcp
answers tools/list with 730 tools but is absent from hanzo.yaml, so the
generator emits no method and an example would have to bypass the SDK
to reach it. Of the declared MCP routes /v1/automations/mcp returns 405.
hello stays on bot_authMe: /v1/ai/account answers 200 with
type="anonymous-user" to a request with NO Authorization header, so a hello
built on it certifies a key that would 401 everywhere else.
All six run and report the server's own refusal for a bogus key:
hello 403 no validated principal store 403 X-Org-Id required
chat 401 API key validation failed agent 403 X-Org-Id required
money 401 sign in to view billing tools 403 a validated principal required
money goes through the generated *_without_preload_content variant, because its
two operations are declared with a `default` response and no content so the
typed methods return None though the server sends JSON — a spec gap, and not a
small one: 696 of 2425 operations model no response body. That raw variant does
NOT raise on 4xx (the check lives in the typed deserialization these lack), so
the example checks status itself; without it a 401 body printed as the balance.
3.1.5 -> 3.1.6. 3.1.5 is live on PyPI, so this is a real patch above it.
Co-authored-by: Hanzo Dev <[email protected]>
REGENERATED pkg/hanzoai/cloud/ from hanzo.yaml @ 07783f5 via the canonical
driver (hanzoai/openapi generate.py python) — 1132 paths, 1519 operations, 779
schemas, landing as 239 api + 1116 model modules. `generate.py python --check`
reports `[python] clean`: the committed tree is byte-identical to a fresh
generation, with no local strip of any kind.
The path count fell from 1885 because the spec deleted 18 products it authored
and served nowhere. Nothing here was lost to a collapse — LLM.md carried an open
defect saying 127 of 411 operations went missing to 23 case-variant tag groups
(AI/ai, Users/users). That is now fixed upstream, and this regeneration proves
it: 239 distinct tags produce 239 api modules, exactly 1:1.
Generating at all required two spec fixes. Both landed in hanzoai/openapi first,
neither is patched here:
- fc0c17a 35 /v1/platform operations carried no `responses`. OAS 3.x requires
it and openapi-generator aborts the whole document, so hanzo.yaml
was producing no client in ANY language.
- 07783f5 ChatCompletionResponse.choices was `items: {type: object}` — so
choices[0].message.content came out List[object], unusable without
a cast, on the most-called route in the API.
SIX EXAMPLES under examples/{hello,chat,money,store,agent,tools}, plus
examples/client.py as the single place a base URL or an env var is resolved.
Same six, same names, same order as the TypeScript set, so a reader who knows
one can navigate the other. They import from hanzoai.cloud — the generated
surface new work targets — not the frozen pkg/hanzoai/{api,models}.
Each flow's call sits behind `if __name__ == "__main__":` deliberately: that is
what lets the gate IMPORT all six to prove every `from hanzoai.cloud import X`
still resolves, with no API key and no socket. A spec change that renames or
drops an operation goes red in CI instead of in a user's app.
CI is the fleet convention and this repo had none: root hanzo.yml + a 7-line
cicd.yml importing hanzoai/ci. The gate is two blocks — import every generated
module (for generated code that IS the build step; there is no compiler to catch
a bad $ref or a model referencing a class the generator declined to emit), then
import the six flows. Both provision an interpreter with uv, because the arc
runner image promises none and a gate that silently no-ops is worse than none.
Scope is deliberate: the cloud client and its flows, not all 65 packages, so a
red gate means "the client the spec just produced is broken" rather than
"something, somewhere". Publishing is untouched — .hanzo/workflows/publish-pypi.yml
stays canonical because it reads the PyPI token from KMS. Two publish paths is
one too many.
3.1.4 -> 3.1.5. PyPI still serves 3.1.1; the tree has been ahead since the KMS
secret at hanzo/prod/python-sdk-publish went unseeded.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/openapi beb4ea2 collapses an operation to its primary tag. Seven
commerce/pricing operations were being emitted twice under one identifier —
2381 lines of duplicate methods on CheckoutApi, CloudApi and InfrastructureApi.
The 21 deletions are the more interesting half. Generating over the existing
tree rather than into an empty one left files behind that no current generation
produces, and api/__init__.py had drifted into importing three of them:
from hanzoai.cloud.api.ai_api import AiApi # the module defines AIApi
which is an ImportError on "import hanzoai.cloud" — the package did not load at
all. A fresh generation is self-consistent; the accumulated one was not. Same
root cause as js-sdk 2.0.2.
Tests: 6 passed. tests/test_zap_transport.py fails to collect on main too
("cannot import name 'Hanzo' from 'hanzoai'") — unrelated and pre-existing,
verified by stashing this change and re-running.
Spec: hanzoai/openapi f9dbb2b (cloud 8143fc0e).
Co-authored-by: Hanzo Dev <[email protected]>
Regenerated pkg/hanzoai/cloud from hanzo.yaml. Consumed: cloud 8143fc0e,
openapi 2861089 (was f581a0e when 3.1.2 was cut).
openapi 3300cda took {org} out of the KMS secrets contract — the org is read from
the token now — and 3.1.2 shipped from before it, so its KMS methods addressed a
path the server no longer serves:
/v1/kms/orgs/{org}/secrets -> /v1/kms/secrets
/v1/kms/orgs/{org}/secrets/{rest} -> /v1/kms/secrets/{rest}
kms_get_v1_kms_orgs_org_secrets -> kms_get_v1_kms_secrets
kms_post_v1_kms_orgs_org_secrets -> kms_post_v1_kms_secrets
Five request/response models renamed with them. Nothing else in the 1885-path
surface moved: 5 models added, 5 removed, 6 files changed.
3.1.2 never reached PyPI (the tag cannot get to git.hanzo.ai — sync-from-github
fast-forwards main only, never tags), so this is fix-forward, not a replacement.
PyPI still serves 3.1.1; 3.1.2 stays a tag.
The upstream tag-casing collision came back exactly as LLM.md said it would: the
regen rmtree'd the tree and `import hanzoai.cloud` raised ImportError again on
AiApi/ApiKeysApi/McpApi. Stripped the same 9 dead lines. It will return on every
regen until one tag spelling per service lands in the per-service specs — that
also recovers the 127 of 411 operations those 23 colliding groups still drop.
No test pins the KMS route: asserting spec content here would make this repo a
second source of truth for routes and fight the pull-only contract. test_smoke
pins structure instead — import, version, surface breadth. 6 passed.
Version 3.1.2 -> 3.1.3, patch only.
Co-authored-by: Hanzo Dev <[email protected]>
pkg/hanzoai/cloud/ (2163 files) has existed on disk untracked and never shipped.
This commits it, so the operator surface cloud already serves is finally callable:
AdminApi.plugin_admin_plugins / _enable_plugin / _disable_plugin / _reload_plugin
Generated by hanzoai/openapi `generate.py python` from hanzo.yaml (1885 paths,
69 merged per-service specs). Consumed: cloud 8143fc0e, openapi f581a0e.
One driver, so delete the second one. scripts/generate.sh fetched the spec itself
and then did `rm -rf pkg/hanzoai` + copied generator output over it — which would
have deleted the hand-written config/mcp/protocols/session/zap/api_response
modules AND the new cloud/ tree. Nothing referenced it. hanzoai/openapi's
generate.py + sdks.yaml is the only way now; LLM.md records the pipeline.
`import hanzoai.cloud` raised ImportError as generated. hanzo.yaml has 23 tag
groups differing only by case; openapi-generator maps both spellings to one
module, and for AI/ai, API Keys/api-keys and MCP/mcp it then emits imports for
classes it never wrote (AiApi vs AIApi). Stripped those 9 dead lines. The same
collision silently drops 127 of the 411 operations in those groups — that one is
upstream and only a single tag spelling per service fixes it; noted in LLM.md.
Version: 3.1.1 -> 3.1.2, patch only. pkg/hanzoai/__init__.py said 1.0.0 (the
generator default) — now resolved from the installed distribution, same pattern
as hanzo_cli/hanzo_iam in f453dae8. uv.lock refreshed to match committed pkg
versions. test_smoke.py locks the cloud import, the four plugin ops, and that
__version__ equals the distribution: 6 passed.
Co-authored-by: Hanzo Dev <[email protected]>
Flags were natively supported in Rust (the hanzo-flags crate — the evaluation
core, pub-exported for Rust callers and FFI-exported for the rest), Go (in-process
via CGO to that core), and TypeScript (@hanzo/flags over /v1/flags). Python was the
gap. This closes it.
hanzo-flags POSTs an evaluation context to cloud /v1/flags — the same
PostHog-compatible endpoint every other client speaks — and answers is_enabled /
variant / payload from the response. It mirrors @hanzo/flags exactly: a HanzoFlags
client bound to one host, load(distinct_id, person_properties=, groups=), the same
three-field result, an async twin, and a one-shot evaluate().
Two invariants match the family: fail-open (a transport error yields the last good
or empty result with errors_while_computing set, never a raise on the hot path)
and cache-by-context+TTL. Zero runtime dependencies — stdlib urllib only, so a
flag check never drags httpx/pydantic into a service.
Tests stand up a local /v1/flags stub and prove the request shape is
PostHog-compatible, the accessors resolve booleans/variants/payloads, the TTL
cache holds, and both a 5xx and an unreachable host fail open. 7/7 green.
Hand-written package for the engine /v1/training API: ServiceClient +
TrainingClient (create/list/get/delete/forward_backward/optim_step/
sample/save_weights), dataclasses, sync httpx, completed-future
.result() wrapper so tinker fut.result() code ports 1:1. 22 pytest
tests over httpx.MockTransport, no network.
Wired like siblings: publish-pypi hanzo-train-* tag case, rye+uv
workspace member, uv source, pyright exclude. uv.lock relock also
catches the lockfile up to on-disk truth (hanzoai 3.1.1,
hanzo-tools-browser 0.5.10) -- no package versions changed.
zap.py is an httpx.BaseTransport; httpx is not a base dep (client core is urllib3).
3.1.0 imported zap.py eagerly in __init__, so `import hanzoai` crashed on missing
httpx in any clean install. Guard the import (try/except ImportError) and declare
httpx under the [zap] extra. import hanzoai now works with or without extras.
Add hanzoai.zap: an httpx.BaseTransport / AsyncBaseTransport that routes the
SAME typed client calls through hanzo_zap.CloudClient over the ZAP binary wire.
Opt in with Hanzo(http_client=hanzoai.zap_http_client(...)); default stays HTTPS.
- hanzo-zap is an optional extra [zap], imported lazily so "import hanzoai"
still works when it (and zap-proto) are not installed.
- Public __all__ is byte-identical; new names are additive and kept out of __all__.
- Request to ZAP mapping: path to dotted method, api-key header to Bearer auth,
body forwarded verbatim.
- Tests: back-compat lock + request-translation via mock CloudClient (no network).
Co-authored-by: hanzo-dev <[email protected]>
The uv workspace listed only 10 of 61 packages as members, so `uv sync`
never installed fs/shell/agent/reasoning/lsp/refactor/todo/jupyter/memory —
and hanzo-tools-core's integration suite (which asserts each imports with an
exact TOOLS count) failed 20 tests against the absent packages. Add the 9
required tool packages as members + [tool.uv.sources] workspace pins so the
real toolset installs and the suite validates live code. Honest fix — install
what the test asserts, don't weaken the assertions. Suite: 24 passed / 12
skipped (was 20 failed / 4 passed). Lock resolves clean (tree-sitter, tornado,
nbformat added). No regressions: hanzoai 3/3, browser 11/11, hanzo-mcp boots.
hanzo-tools-core was a [tool.uv.sources] gap: not a workspace member, so uv
resolved it from PyPI 0.3.0 — the OLD package that still ships the duplicate
hanzo_tools/core/ WITHOUT ToolImage, shadowing canonical hanzo-tools.core and
breaking 'from hanzo_tools.core import ToolImage' (browser tests: 6 fail/5 error).
Add hanzo-tools + hanzo-tools-core as workspace members AND [tool.uv.sources]
= { workspace = true } so both resolve on-disk (0.3.1 empty shim), never the
shadowing wheel. Purge the stale untracked pkg/hanzo-tools-core/hanzo_tools/
build artifact. Finishes the decomplect 711eb498 started. Browser tests: 11/11 green.
#45 regenerated hanzoai as a pure OpenAPI client and DELETED the hand-written
core-SDK modules (config/mcp/session/protocols/…) that live in the same package
but aren't part of API generation. hanzo-mcp imports all four → the published
hanzoai 3.0.0 crashed EVERY hanzo-mcp consumer on startup ('No module named
hanzoai.protocols') → the MCP server wouldn't boot → agent couldn't
reconnect. Restore the 4 self-contained modules hanzo-mcp needs (verified: zero
imports of the removed Stainless internals); they coexist with the generated
api/models client. Left agents/auth/cluster/llm_client OUT — they import the
removed ._client and need migration to the new client (separate follow-up, no
reconnect-path consumer). Bump 3.0.0→3.0.1. Boot-proven: hanzo-mcp starts,
26 tool packages / 29 tools, MCP initialize OK.