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]>
0.15.13 shipped the 166-service surface switched off. `tools/list` against a
clean install returned 40 tools with no `hanzo`, and the server logged
"unified `hanzo` tool unavailable — keeping per-service tools enabled".
The import was fine; the mode gate was not. register_all_tools() asks
is_tool_enabled("hanzo", True), which returns tool_config["hanzo"] whenever the
key exists rather than falling back to the default. ModeLoader builds that
config by disabling every TOOL_REGISTRY key and re-enabling only the active
mode's tools. TOOL_REGISTRY has "hanzo"; the default personality's list did
not — so the "True" default was never reached and the tool was explicitly off.
Two things broke together, because the per-service cloud tools are retired only
once `hanzo` is enabled: the unified surface was absent AND the ten tools it
replaces were still mounted. The exact state 929e103e set out to end.
`hanzo` belongs in ESSENTIAL_TOOLS. It is an axis like fs or git — the single
seam to the platform — not a mode-specific extra. It stays out of
ESSENTIAL_SYSTEM_TOOLS so it remains disableable.
Verified in a clean uv venv from built wheels only (no editable, no PYTHONPATH):
tools/list now returns 31 tools including `hanzo`, and api/auth/billing/commerce/
iam/ingress/kms/mpc/paas/team are correctly gone (40 - 10 + 1). The tool answers
hanzo(service="services") with 166 services read live from
https://api.hanzo.ai/v1/openapi.json.
A test now pins the gate where it is actually decided. It fails against the
installed 0.15.13 (3 of 4) and passes against 0.15.14 — an import-level check
cannot catch this, because the import succeeds.
Co-authored-by: Hanzo Dev <[email protected]>
The unified `hanzo` tool imports HanzoCloud from hanzo_tools.core, which
hanzo-tools first shipped in 0.3.3 — but nothing in the dependency graph
required it. hanzo-tools-api asked for `hanzo-tools-core>=0.1.0`, an empty
shim whose own floor is hanzo-tools>=0.3.2, and 0.3.2 has no cloud.py at all.
A resolver was therefore free to satisfy every constraint and still produce an
install where `from hanzo_tools.core import HanzoCloud` raises ImportError.
That is exactly what happened: the legacy-tool gate logged "unified hanzo tool
not importable" and fell back, and the dev venv had to install hanzo-tools
editable to get a working `hanzo`.
Name the real dependency at the real floor:
- hanzo-tools-api: hanzo-tools-core>=0.1.0 -> hanzo-tools>=0.3.4. It imports
hanzo_tools.core directly, so it must depend on the package that owns it;
the >=0.1.0 shim floor also still admitted the shadowing duplicate.
- hanzo-tools-vector: hanzo-tools>=0.3.0 -> >=0.3.4 (cloud_vector.py).
- hanzo-mcp: hanzo-tools>=0.3.2 -> >=0.3.4, hanzo-tools-api>=0.3.1 -> >=0.3.2.
Metadata only, on versions not yet published. No version bumps.
Co-authored-by: Hanzo Dev <[email protected]>
The `hanzo` dispatcher hand-listed 10 services in SERVICE_TOOL_PATHS, so the
other ~150 cloud products were unreachable until someone cut a Python release.
Project the surface from cloud's /v1/openapi.json instead: cloud tags every
operation with its product (the first path segment after /v1/), so that document
already IS a service/action catalog. 166 services are now reachable, and a newly
mounted app is callable the moment cloud serves it.
Still exactly one MCP tool — agents degrade badly with hundreds — now shaped
hanzo(service, action, params, method), with `services` listing the catalog and
an empty action listing a service's actions.
Delete the local Infinity vector store (3,546 lines). It shipped no embedder, so
its only possible output was random vectors: it ranked "Bananas are yellow" above
an auth document for the query "authentication oauth jwt". A previous fix put the
mock behind HANZO_VECTOR_ALLOW_MOCK, but a fake that a flag can re-enable is
still a fake, and a tool that lies to an agent is worse than a missing one. The
package keeps only the cloud-backed VectorTool, and a test now fails if any
module in it imports `random`.
Make the legacy-tool gate honest: hide the per-service tools only once the
unified tool genuinely imports. It does not import against released hanzo-tools
(HanzoCloud is newer), and disabling them unconditionally would have left no
cloud tools at all.
- spec.py: catalog projection, cache keyed per spec source, stale cache beats
failing a call. HANZO_OPENAPI_URL decouples catalog from target so a partial
local host can be driven with the full registry.
- HanzoCloud.call: one generic seam for the verbs the spec names (PUT/PATCH/
DELETE); auth also resolves the hanzo CLI's IAM session, memoized.
- 40 new tests covering action naming, template binding, method inference,
param split, and cache fallback.
Co-authored-by: Hanzo Dev <[email protected]>
is_authenticated() was `load_token() is not None` — a string-presence check
that returned True for the literal "fake.not.a.real.jwt". Every permission
decision downstream of it was therefore a lie. This makes the client side of
Hanzo auth actually work, end to end. Builds on the /v1/iam prefix already on
main; the endpoint paths were necessary but not sufficient.
hanzo_iam.tokens — the ONE credential judge. Verifies the signature against
the issuer's published JWKS plus exp/iss/aud, and fails CLOSED: an unreachable
JWKS is not a pass, because a client that cannot check a signature does not
know the token is good. Reason codes distinguish "expired" from "offline".
`alg: none` is absent from the accepted list by construction. An opaque API
key cannot be judged offline, so it is confirmed against userinfo rather than
assumed valid for being a non-empty string.
hanzo_iam.oauth — a real login: authorization code + PKCE (S256) over a
loopback redirect, with a deadline. Not the device grant, which would be the
better CLI UX: iam implements RFC 8628 fully and advertises it, but no PUBLIC
client is registered, so POST /v1/iam/oauth/device answers 401 invalid_client
for hanzo-cli, hanzo-app, hanzo-cloud and hanzo-console alike. Registering one
public app is the whole server-side fix; shipping a device path that always
401s would be a lie in code. The password grant is out for the same reason
(401 invalid_client without a secret), so `--no-browser` now prints the URL
instead of prompting for a password it cannot use.
The loopback listener binds a REGISTERED redirect_uri. iam compares
redirect_uri by exact string and does not apply RFC 8252 §7.3 port-agnostic
loopback matching, so a CLI cannot pick a free ephemeral port — the previous
code bound 8399 (cli) and 8398 (bot), neither registered, and /authorize
refused both with a bare 400 before the user saw a login page. It listens on
127.0.0.1 and ::1 because the registered URIs spell "localhost", and sets
SO_REUSEADDR because the previous callback sits in TIME_WAIT for ~60s and
would otherwise EADDRINUSE a second login on a port nothing is using.
hanzo_iam.store — keyring first, else an atomic 0600 file. The old path did
write_text() then chmod(0600), so with umask 022 a bearer token sat at 0644
between the two calls. Now it is created private and renamed into place. The
PaaS session cache goes through the same writer.
Two definitions of IAMConfig existed; models.py's was the one every client
imported, so the endpoint properties main had just fixed on config.py's were
dead code. One now, in config.py, plus jwks_uri and device_endpoint.
The bot and CLI login copies are gone; both call the one flow. whoami and
`hanzo bot login` no longer print claims from an unverified decode — that
rendered an attacker-chosen identity as fact.
The Team tool refuses instead of pretending: api.hanzo.ai/team is the
marketing SPA (200 text/html), so raise_for_status() passed and .json() blew
up. There is no Team API; HANZO_TEAM_URL stays as the seam for when one ships.
PaaS token exchange reports its real diagnosis: POST /v1/auth/login 404s while
/v1/org and /v1/user 401, so the API is up and gated but the exchange route is
not deployed at that edge. No client-side workaround exists, so it says so.
Tests fail against the old behaviour, which is the point: 5 session tests fail
against the shipped is_authenticated and 4 store tests fail against
write-then-chmod. 134 pass alongside main's KMS suite.
Co-authored-by: Hanzo Dev <[email protected]>
The first pass left three classes of config that BOTH tsc 5.9 and tsc 7
reject. Each was proven against both compilers before changing:
TS5090 A `paths` target must be relative once `baseUrl` is gone. The
first pass skipped the "./" prefix wherever baseUrl pointed at
the config's own directory, reasoning it was semantically
equivalent. It is not — without baseUrl a non-relative target
is rejected outright, by 5.9 as well as 7.
TS5110 `moduleResolution: node16` requires `module: node16`. The first
pass mapped commonjs projects to node16 resolution alone, which
BROKE those configs for the current toolchain. Both are now set.
TS5102 `downlevelIteration` is also removed in TS7; it was missing from
the dead-flag list.
Verified: repos that tsc 7 previously refused (base-studio, js-sdk, kv-js)
now report zero config errors on tsc 7 AND tsc 5.9.
Co-authored-by: Hanzo Dev <[email protected]>
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.
`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).
`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.
The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.
Co-authored-by: Hanzo Dev <[email protected]>
test_version read `assert "0.1.0" in r.stdout` and passed only because
hanzo_cli.__version__ was stale at 0.1.0 while pyproject said 0.2.2. The one
test positioned to catch the version drift was asserting the drifted value, so
single-sourcing the version in f453dae8 is what finally made it fail. It now
compares against hanzo_cli.__version__ and cannot enshrine a wrong value again.
Co-authored-by: Hanzo Dev <[email protected]>
Same drift as the CLI, in all three packages: a hardcoded __version__ that no
longer matched pyproject.
hanzo_iam/__init__.py 1.1.1 vs pyproject 1.30.0 (29 minor versions apart)
hanzo_cli/__init__.py 0.1.0 vs pyproject 0.2.2
The hanzo_cli one was load-bearing: click's version_option reads it, so it is
the reason `hanzo --version` answered "0.1.0" — a release that never existed.
All three now resolve from the installed distribution, so there is one answer.
Patch bumps for the two packages changed in 68877409 (never a lazy major):
hanzo-cli 0.2.2 -> 0.2.3 entry point removed, __main__ added, PKCE, token path
hanzo-iam 1.30.0 -> 1.30.1 endpoint properties on the exported IAMConfig
Verified on a clean venv: __version__ == importlib.metadata.version for all
three, `hanzo --version` 0.4.4, `python -m hanzo_cli --version` 0.2.3, 58
commands. hanzo-iam 19/19; hanzo-cli's failures are unchanged live-service auth.
Co-authored-by: Hanzo Dev <[email protected]>
`hanzo` depends on `hanzo-cli`, and BOTH declared `[project.scripts] hanzo`.
Entry-point resolution is install-order dependent, so which CLI a user got was
a coin flip — and hanzo-cli kept winning. The result:
$ hanzo auth login
Error: No such command 'auth'.
So the "auth login is broken by Cloudflare 1010" story was real but unreachable:
users never got as far as the HTTP call, because the binary they had did not
have an `auth` command at all. Publishing that fix would have changed nothing.
One command, one owner:
- hanzo-cli drops its console script. `hanzo` is the ONE command.
- `paas` and `bot` are mounted into it — the only two groups with no equivalent.
`kms` is NOT mounted: `hanzo secrets` is a strict superset (audit/grant/
revoke/rollback/rotate/versions on top of the same get/list/set/delete), and
two commands for one concern is what we are removing.
- hanzo_cli grows a __main__ so it stays invocable as `python -m hanzo_cli`,
and its e2e suite targets that instead of whatever `hanzo` resolves to.
Endpoints, one definition:
- models.IAMConfig (the class the package EXPORTS) gains the HIP-0111 endpoint
properties. config.IAMConfig had them, but nothing imports that one — a
second same-named class nobody uses is how the legacy path survived.
- config.py + fastapi.py stop hand-assembling paths and read the constants.
- password_login posted to `/oauth/token`, which is not a 404: IAM serves a
200 text/html SPA catch-all for unregistered paths, so it received a login
PAGE and json() blew up on HTML. Verified: legacy 200 text/html vs canonical
401 application/json.
PKCE, because this is a public client:
- browser_login ships no client_secret, so the authorization code was the only
secret in the flow and it arrives over a plaintext loopback redirect. The SDK
already accepted code_challenge/code_verifier; the CLI simply never passed
them. RFC 8252 §8.1 / RFC 7636 S256.
Version, one source:
- pyproject said 0.4.4, __init__ said 0.3.47, cli.py said 0.3.48, and the
installed binary reported 0.1.0 — four answers, none right. Now resolved
from the installed distribution.
Verified on a clean venv: entry-point providers 2 -> 1, commands 10 -> 58,
`hanzo --version` 0.1.0 -> 0.4.4, all four endpoints canonical. hanzo-iam
19/19 pass; hanzo-cli's 20 failures are unchanged before and after and are all
live-service auth (no stored token in this environment).
Co-authored-by: Hanzo Dev <[email protected]>
Ships 50d442eb. 0.4.3 cannot complete a login at all: Cloudflare rejects
urllib's default User-Agent in front of hanzo.id with `error code: 1010`, so the
token exchange 403s AFTER the user has already signed in through the browser.
Also adds the PKCE S256 that flow never had, and moves off the legacy
/oauth/* + /api/device/code paths onto the canonical /v1/iam/oauth/* surface
hanzo.id advertises.
Patch bump from the last published version (0.4.3), per the release rule.
Three defects in `hanzo auth login`, found while auditing HIP-0111 compliance.
1. LOGIN DID NOT WORK AT ALL. urllib defaults its User-Agent to
`Python-urllib/3.x`, which Cloudflare refuses in front of hanzo.id with
`error code: 1010` — a 403 that never reaches IAM. The browser leg looked
fine, the user signed in, and THEN the token exchange died, so the failure
landed after the user had already authenticated. Measured on the live host:
POST /v1/iam/oauth/token UA=Python-urllib/3.11 -> 403 (CF 1010)
POST /v1/iam/oauth/token UA=hanzo-cli/python -> 400 (reaches IAM)
POST /v1/iam/oauth/device UA=Python-urllib/3.11 -> 403 (CF 1010)
POST /v1/iam/oauth/device UA=hanzo-cli/python -> 400 (reaches IAM)
Every outbound request now identifies itself. The device flow was dead the
same way.
2. NO PKCE. The flow had zero code_challenge/code_verifier — `state` only,
which is CSRF protection, not interception protection. HIP-0111 Security
Considerations makes PKCE S256 mandatory, and RFC 8252 §8.1 requires it for
native apps specifically: this redirects to a fixed loopback port (1456) that
any local process can bind or race, so an intercepted authorization code was
directly redeemable. Now S256, with the verifier bound into the exchange.
hanzo.id advertises `code_challenge_methods_supported: ["S256"]`.
3. LEGACY PATHS (HIP-0111 §4.4). It called `/oauth/authorize`, `/oauth/token`
and `/api/device/code` — the `/oauth/*` and `/api/*` spellings the standard
retired, the latter breaking the absolute "no /api/" rule. Replaced with the
canonical paths hanzo.id actually advertises in its discovery document, held
in ONE constant block so no call site spells a path itself (§4.3):
/v1/iam/oauth/authorize /v1/iam/oauth/token /v1/iam/oauth/device
`/api/device/code` was not merely non-canonical — it answers 401; the real
device endpoint is `/v1/iam/oauth/device`, per discovery.
Also drops a redundant function-local `import base64` now that the module
imports it.
kms.hanzo.ai runs luxfi/kms, which has never served an /api/* route. This
SDK was sending Infisical's: /api/v3/secrets/raw for every secret operation,
/api/v3/auth/login for user auth, /api/v1/auth/kubernetes-auth/login for k8s.
Only one call in the whole package — /v1/kms/auth/login — was ever real.
It stayed invisible because older luxfi/kms builds embedded a console SPA
behind a root catch-all that answered every unmatched path with 200
text/html. A wrong URL came back as a JSON decode error, which reads like a
parsing bug in the client rather than "this endpoint does not exist." That
catch-all is gone (cmd/kms/main.go ends in notFoundJSON), so those paths now
return honest JSON 404s and the SDK fails outright.
The route table is not the only thing that was wrong. The data model was
Infisical's too — project_id, workspace, secret version, secret comment,
shared-vs-personal type — and luxfi/kms has none of those concepts. It keys
one value by (org, path, name, env) in ZapDB at kms/secrets/{path}/{env}/{name}
and a write upserts it in place. Half-migrating would have left response
models that cannot validate what the server sends, so the vocabulary moves
with the paths:
list_secrets(path, env) -> names GET /v1/kms/orgs/{org}/secrets
get_secret(path, name, env) GET /v1/kms/orgs/{org}/secrets/{path}/{name}
put_secret(path, name, value, env) POST /v1/kms/orgs/{org}/secrets (create AND replace)
delete_secret(path, name, env) DELETE .../secrets/{path}/{name}
health() GET /v1/kms/healthz
Two server behaviors now have one home, hanzo_kms/routes.py, shared by the
sync and async clients so they stay mirror images:
- The server splits the trailing path at its LAST slash into (path, name),
so each segment is escaped individually. Escaping the joined string
encodes the separators away and the server reads one long name. A name
containing "/" is rejected outright: it would be written under one key
and read back under another, so the write looks like it succeeded and
the read never finds it.
- There is no versioned read. get_secret(version=N) raises
VersionUnsupportedError rather than quietly returning the current value.
org is new and required — it scopes both the URL and the JWT owner claim.
Constructor field, HANZO_KMS_ORG, defaults to "hanzo".
Auth collapses to what the server actually offers: client credentials
exchanged at /v1/kms/auth/login, or a pre-issued IAM bearer token. The AWS,
Azure, GCP, Kubernetes and SRP methods were Infisical's and none were served.
That also fixes a divergence where the async client silently ignored
HANZO_KMS_TOKEN — env parsing is now one function.
Callers migrated with it: hanzo-cli's `hanzo kms`, the hanzo-tools-kms MCP
tool, and hanzo-tools-auth's session client, all of which were building the
removed auth models and calling the removed create/update pair. Each now
constructs KMSClient() and lets it read the environment.
Tests: pkg/hanzo-kms/tests pins the wire shape — no request URL may contain
"/api/", per-segment escaping survives the server's last-slash split, sync
and async emit byte-identical request lines, and a decoder rejects the
200-HTML/JSON-404 shapes instead of reading them as empty results. Wired into
hanzo-packages-ci without `|| true`, so a regression turns CI red instead of
waiting for a decode error in production.
hanzo-kms 1.1.0 -> 1.1.1.
Co-authored-by: Hanzo Dev <[email protected]>
The client had OIDC paths written inline at each call site, so the canonical
route existed in as many places as it was used and drifted per method. Moves
them to named constants in models.py — OIDC_DISCOVERY_PATH, OIDC_JWKS_PATH,
OIDC_TOKEN_PATH, OIDC_INTROSPECT_PATH — and has both the sync and async
clients read those.
Values match the canonical IAM surface (HIP-0111): discovery
/.well-known/openid-configuration, jwks /v1/iam/.well-known/jwks, token
/v1/iam/oauth/token, introspect /v1/iam/oauth/introspect, authorize
/v1/iam/oauth/authorize. No legacy /oauth/* and no /api/ prefix.
One definition per route, so sync and async can no longer disagree.
The one-shot constructor note= sets self.note (a str), which shadowed the
note() method → 'str object is not callable' at call time. The running-log
accumulator is now log(); note= stays the one-shot note. Proven end-to-end
logging 9 kernel-perf experiments (4 proven / 5 refuted) to the live
api.hanzo.ai/v1/research with hypothesis+verdict+provenance.
Add the scientific-method frame to the experiment handle: state a hypothesis
+ prediction up front, .note() the running log, and .conclude(verdict ∈
proven|refuted|inconclusive, because=...) to seal it. A refutation is
recorded as clearly and durably as a proof. finish() defaults a stated
hypothesis to inconclusive — a finished run never silently reads as proof.
All fields serialize into the record's meta alongside the auto-captured
provenance (git sha + lib versions), so a verdict is queryable evidence.
Backward-compatible: every new param is optional.
snapshot/report now SUBMIT the bytes (base64 content); the server hashes them and owns
the sha256 + ref — the SDK no longer asserts a (poisonable) hash or a client ref. _headers
sends ONLY the per-org key (Bearer); it never mints X-User-Id/X-Org-Id (a cross-tenant
forge the gateway strips) — any dev bypass is server-side.
The ONE way producers record/query R&D evidence via /v1/research (HIP-0512),
generated-shape from openapi/research and wrapped in a tiny verb surface so
hand-rolling is the worse choice:
exp = research.experiment(kind, subject, task) # get/create → handle
exp.record(item, model, result) # attempt, idempotent by stable id
exp.snapshot(bytes) / exp.report(text) # diary artifacts (sha256-addressed)
exp.finish(value) # seal the run
research.query(project=, kind=) # read canonical
Zero-config auto-instrumentation (caller supplies no provenance): captures git
sha/branch/dirty, lib versions, host, and THREE self-documenting narrative sources —
the calling code's docstring (what this experiment is), the commit messages since this
experiment's last recorded run (what changed + why), and an optional note. The project
self-documents as a side effect of running.
Importer-agnostic: ingest() is one idempotent POST; any source (SQLite backfill, git
history, kernel-perf campaign) maps to stable ids and re-imports as a no-op. Stdlib
only (urllib); per-org key auth; private-by-default.
Adds a shared HanzoCloud client (core/cloud.py; lazy httpx, api.hanzo.ai /v1)
and wires the cloud half of the hybrid tools: code gains cross-repo
search/context/ask/index over the cloud index, vector gains a cloud-backed
store plus an infinity-embedded local option, net gains a vision tool. Bumps
hanzo-tools 0.3.2->0.3.3, -code 0.1.2->0.1.3, -net 0.1.2->0.1.3, -vector
0.2.0->0.2.1 and declares httpx where the cloud path is used. New tests cover
the code/net cloud actions. import hanzo_tools.core is green.
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.
The browser MCP tool no longer requires a human to have started zapd.
ZapdConsumer.connect() now dials the router and, on failure, auto-starts
the shared singleton (locate the zapd binary, spawn it detached, wait for
the socket) then dials once more — mirroring zapd's own host-mode
connect-or-spawn. No consumer-side lock: the singleton invariant lives in
zapd (advisory-lock bind), so concurrent spawns are safe. The OS unit is
the primary starter; this is the bare-machine fallback so the tool just
works.
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]>
#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.
The browser tool's bespoke register() returned execute()'s dict RAW, so FastMCP
flat-JSON-serialized a screenshot's ToolImage (base64 inline, 250K chars) →
overflowed the agent context and wedged the run — the SAME bug main's
ImageContent converter (711eb498) fixed for every OTHER tool via
BaseTool.register→_result_to_mcp, but the browser tool bypassed that path.
Now both registration paths share the one converter: nested ToolImage anywhere
in the result becomes a native MCP ImageContent block the client SEES. One way.
A full-page PNG returned as inline base64 (100K+ chars) overflows an agent's
context window and wedges the whole run — the #1 cause of 'the browser tool
hangs' (two mobile-QA agents died on 6.6MB transcripts). Both capture paths
(native-zap _extension_command + the playwright fallback) now persist to
~/.hanzo/screenshots (or a caller path) and return a compact {path,size};
base64 is inlined ONLY for a small PNG (<=40KB). Adds _save_capture + _extract_b64.
The billing tool hardcoded api.hanzo.ai/api/v1/billing (double prefix). Per
the /v1-only canonical API contract, the gateway serves everything under bare
/v1/* — never /api/. Scrub to api.hanzo.ai/v1/billing.
Co-authored-by: Hanzo Dev <[email protected]>
connect() trusted a cached ZapClient forever; if the router restarted, every
call raised BrokenPipeError and the consumer stayed wedged. list_providers/route
now drop the dead client and reconnect once.
Co-authored-by: Hanzo Dev <[email protected]>
HIP-0300 tools serialize results to JSON text, which flattened images into
useless base64-in-text. Add a ToolImage value type + one converter in
BaseTool.register: any ToolImage anywhere in a result becomes a real
mcp.types.ImageContent block; text-only results stay byte-for-byte JSON.
Emitters wired: fs read of image files, browser screenshots (bidi+playwright).
This gives hanzo-mcp vision parity with zai-mcp — an agent with native vision
(Claude) sees pixels directly, no per-task vision tool needed.
Also decomplect the core-package collision that caused this class of bug:
hanzo-tools-core shipped a DUPLICATE hanzo_tools/core/ that nondeterministically
shadowed canonical hanzo-tools (dropping ToolImage, the sophisticated register,
etc. — same shadow class as the 0.3.0 error-reexport gap). Finish the
'merge core into hanzo-tools' refactor: move id_tool into hanzo-tools, make
hanzo-tools-core a pure empty metapackage. Now ONE package owns hanzo_tools.core.
Versions: hanzo-tools 0.3.2, -core 0.3.1 (shim), -browser 0.5.9, -fs 0.3.4,
hanzo-mcp 0.15.12 (floors bumped). Verified: fs read PNG -> [text, image];
fs stat -> [text]; 29 tools intact; 9 tests pass.
Regression guard for the hanzo-tools 0.3.0 core re-export gap that silently
dropped fs/exec/code/git/fetch/agent, leaving hanzo-mcp with only 13 tools.
Fails loudly if any entry point stops loading or the core error contract
(ToolError/ConflictError/InvalidParamsError/NotFoundError) regresses.
PyPI hanzo-tools 0.3.0 shipped a core/__init__.py that never re-exported
ToolError/ConflictError/InvalidParamsError/NotFoundError from core.unified.
Six tool packages (fs, shell, agent, code, vcs, net) import those names at
entry-point load and silently failed, so hanzo-mcp exposed only 13 tools
instead of the full axis set. The re-export fix already landed in git
(0d8d52d9) but 0.3.0 was never republished. Bump to 0.3.1 to ship it.
Verified: entry points 20→26 loaded, hanzo-mcp tools/list 13→29.
DynamicToolRegistry stored getattr(tool_class, 'description'), which for a
class-level @property returns the property DESCRIPTOR (not a str). 'tool list'
then did len(config.description) → 'object of type property has no len()',
breaking the entire tool-catalog command. Add _extract_tool_description
(mirrors _extract_tool_name): resolve a @property via a cheap instance, else
coerce to str, always returns str. Plus a defensive isinstance guard in the
list display path. Regression test covers property/unevaluable-property/plain/
absent + asserts every registered entry's description is len()-safe. 2 passed.
hanzo-kms (sync + async): /api/v1/auth/universal-auth/login →
/v1/kms/auth/login. Body already canonical {clientId,clientSecret};
TokenResponse already parses {accessToken,expiresIn,tokenType}.
Scoped per CTO ruling: ONLY endpoints with a real canonical /v1/kms/*
target are migrated. The remaining /api/v3/secrets/raw/*,
/api/v1/auth/{kubernetes-auth,aws-auth}/login and /api/v3/auth/login
have NO canonical route (or a different API shape) on the kms.hanzo.ai
server and are left intact — rewriting them would mint dead /v1 paths.
They are flagged for a separate SDK-port task. Clean break, no aliases
(org rule: one way, /v1/, never /api/).
Co-authored-by: Hanzo Dev <[email protected]>
get_user_info hit {api_base_url}/v1/get-account (api.hanzo.ai/v1/get-account),
which 404s — the account read lives on the IAM issuer at /v1/iam/get-account
(same base as mint_api_key). Response is a {status,sub,name,data,data2}
envelope; accessKey is under data (get_api_key already reads it there).
Verified against iam (controllers/token.go, user.go, init_apps.go), ai
(controllers/account.go), and login (middleware.ts) sources:
- client_id hanzo-app (canonical brand CLI/desktop/mobile app seeded with the
device_code grant), not the unseeded hanzo-dev/app-hanzo.
- Device flow uses RFC paths hanzo.id proxies to IAM — POST /oauth/device and
/oauth/token — form-encoded (IAM reads device-grant params via Query(), skips
JSON), keyed off the response body so a proxy can't desync polling.
- get_user_info reads /v1/get-account (IAM Claims with accessKey inlined),
not the nonexistent /v1/user.
- Replace fictional named-key CRUD (/v1/api-keys) with Hanzo's real single
per-user hk- key model: get_api_key (read accessKey), mint_api_key (self via
/v1/iam/mint-user-keys), get_or_create_api_key (read-first), revoke_api_key.
Email/SMS/Google/GitHub/web3 all work since the device flow delegates login UI
to the browser approval page (providers wired per iam wire_providers.go).
The hanzo-tools-ui docs-RAG client targets api.hanzo.ai (host already
canonicalized on main). Move the paths off the residual /api/ prefix to
top-level /v1/, matching the cloud-api route rename and the openapi
v1.0.0 lock-in (no /api/ prefix):
/api/search-docs -> /v1/search-docs
/api/chat-docs -> /v1/chat-docs
/api/index-docs -> /v1/index-docs
/api/search-docs/stats -> /v1/search-docs/stats
Hard cutover, coordinated with hanzoai/cloud (productsvc + ai serve these
at /v1), universe cloud-api-v1 AUTH_PUBLIC_PATHS, hanzo-docs RAG clients,
and the openapi cloud spec.
Co-authored-by: Hanzo Dev <[email protected]>
The hanzo-tools-ui docs-RAG client targets api.hanzo.ai (host already
canonicalized on main). Move the paths off the residual /api/ prefix to
top-level /v1/, matching the cloud-api route rename and the openapi
v1.0.0 lock-in (no /api/ prefix):
/api/search-docs -> /v1/search-docs
/api/chat-docs -> /v1/chat-docs
/api/index-docs -> /v1/index-docs
/api/search-docs/stats -> /v1/search-docs/stats
Hard cutover, coordinated with hanzoai/cloud (productsvc + ai serve these
at /v1), universe cloud-api-v1 AUTH_PUBLIC_PATHS, hanzo-docs RAG clients,
and the openapi cloud spec.
The cloud API now sits behind one front door (ingress -> gateway -> cloud).
api.cloud.hanzo.ai / cloud-api.hanzo.ai / functions.hanzo.ai are redundant
aliases routing to the same backend, so migrate clients to api.hanzo.ai.
- hanzo-tools-ui: HANZO_CLOUD_API default cloud-api.hanzo.ai -> api.hanzo.ai
(docs RAG paths /api/search-docs|chat-docs|index-docs are served at /api on
the cloud backend per AUTH_PUBLIC_PATHS; kept as-is)
- hanzo base status: Functions remote host functions.hanzo.ai ->
api.hanzo.ai/v1/functions (the canonical gateway path)
Non-breaking: the aliases still resolve to the same backend.
Co-authored-by: Hanzo Dev <[email protected]>
Packaging metadata (twine check was PASSED-with-warnings -> clean PASS):
- config/llm/mcp/vector: add missing 'readme = README.md' ([project] key);
README files existed but were never wired into the dist metadata.
- net/plan/vcs: fill empty (0-byte) README.md files referenced by the
readme key, so long_description is no longer missing.
Tests (hanzo-tools-core/test_all_tools.py drifted from current sources):
- browser: 1 -> 3 tools (browser, cdp, playwright after the zapd split)
- mcp_tools: 4 -> 5 tools
- required-tool total: 21 -> 23
Patch release carrying the HIP-0111 IAM endpoint migration in
hanzoai.resources.iam (/iam/api/get-account + /iam/api/userinfo ->
canonical /v1/iam/oauth/userinfo), already in-tree since be917695 but
unreleased. 2.2.1 is taken on PyPI; bump to 2.2.2 (x.x.x+1).