Our SDKs are generated by openapi-generator on our own CI. The docs said a
hosted third-party service, which we retired: hanzoai/openapi's LLM.md and its
regenerate-sdks.yml both already said so, while its README still called that
service "canonical" and told a reader to fetch an API key for it out of KMS.
Two sources disagreeing is how somebody follows the wrong one, and the wrong one
here sends them looking for a credential that should not exist.
The same claim had spread to what customers and agents read: five pages on
docs.hanzo.ai and six agent skills advertised a vendor we do not use, and
hip-0040 -- the standard for multi-language SDKs -- argued FOR it by name. The
argument survives without the brand, because it was never about the vendor: one
spec beats N hand-written clients. python-sdk's mock script also pulled that
vendor's fork of prism-cli; it takes upstream @stoplight/prism-cli now.
What is deliberately left alone is every occurrence that is not about us: a
filament material in a 3D-printer integration, a browser of that name in three
user-agent parsers, tokenizer vocabulary where the string is a token id, and
citations recording where Mux, Perigon and Stigg publish their own specs.
Renaming any of those breaks something real.
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 → Claude Code 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 (8b9e82d9) 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
(4406ef32) 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 e1a38363 but
unreleased. 2.2.1 is taken on PyPI; bump to 2.2.2 (x.x.x+1).
hanzo-tools-iam built URLs as {base}/api/{path} — forbidden per HIP-0111
(no /api/ prefix; one way). Now {base}/v1/iam/{path}; health probe is
/v1/iam/healthz. Asserts JSON shape, not bare 200, to dodge the IAM SPA
catch-all that serves 200 text/html for unregistered paths. Adds a test
for _iam_url + IAM_BASE_URL. (Test harness wiring for this non-member
package is a separate packaging task — code verified: 0 /api/ remain.)
hanzoai/s3 (SeaweedFS fork w/ consensus+ZAP) is the canonical object
store; the minio pip package and its branding are gone. hanzo-s3 is now
a thin native boto3 adapter exposing a stable surface (S3Client + Bucket/
Object/Stat) so the hanzo s3 CLI and s3 MCP tool are unchanged. Removed
the dead MinioAdmin re-export (no consumer) and all MinIO naming.
CLI (7) + MCP S3Tool (14) tests green; ruff clean.
Closes the last gap in the one-way Hanzo control surface. IAM/KMS/PaaS
already had both CLI subcommands and MCP tools; S3 had neither.
- hanzo s3: buckets/mb/rb/ls/stat/rm/presign over hanzo_s3 (MinIO) client,
creds from env (inject from KMS, never plaintext). Registered in the
unified hanzo CLI alongside iam/kms/paas.
- hanzo-tools-s3: S3Tool MCP tool (same action-dispatch pattern as PaaSTool),
exported via the hanzo.tools entry point; s3 added to loader prefix map.
- uv workspace: map hanzo-cli/iam/kms/s3 + tools-s3 to local sources so the
unified CLI resolves on-disk packages (fixes hanzo_iam ImportError from a
stale published 1.29.0 wheel shadowing local 1.30.0).
Tests: 7 CLI + 14 MCP, all green.
Root cause of dead shell-exec: stale installs froze hanzo-tools-core 0.2.0 (no ToolError)
before 0.3.0 published, shadowing hanzo_tools.core and crashing the shell package import.
Floors now exclude it; a fresh/upgraded install always lands the consistent set.
hanzo-tools-browser: the zapd router envelope (frame.rs mirror) is no
longer reimplemented — zapd_consumer now uses zap.frame + zap.ZapClient
(dep zap-proto>=1.2.0). The end-to-end browser command codec stays local
and untagged to match the extension's decodeCmd (frame.encode_cmd is
tagged and would corrupt it); a new test proves the byte layout.
hanzo-zap: wire.py now delegates the zero-copy codec (Message/Builder/
ObjectBuilder/object readers) to canonical zap.wire instead of a 3rd
hand-rolled copy; the hanzo cloud-service schema + handshake + frame I/O
remain as a thin layer. dep zap-proto>=1.2.0. 114 tests pass.
When the firefox backend is targeted and Firefox exposes a WebDriver BiDi remote agent (--remote-debugging-port=9222), drive it directly via bidi_client (real navigation + trusted input) instead of the half-implemented extension path or a dead headless Playwright. Additive + gated: only fires for target firefox with live BiDi and a BiDi-mapped action; otherwise falls through to the existing extension/Playwright routing unchanged. Takes effect after the MCP server reloads.
Firefox 129+ runs WebDriver BiDi-only by default (CDP off), so /json/version 404s and the socket is served directly at /session. Four fixes make the BiDi client actually work against it:
- probe(): fall back to a TCP check + ws://host:port/session when /json/version isn't advertised (don't open a probe WebSocket — that establishes session state and conflicts with session.new).
- websockets>=13 compat: ClientConnection dropped .closed; _ws_open() reads .state/.close_code across versions.
- session lifecycle: session.end on close() so a session never orphans (Firefox keeps one at a time, and an orphan blocks every later session.new); retry session.new once after ending.
- add create_context/close_context/get_url so automation runs in a fresh tab without disturbing the user's tabs.
* fix(browser-mcp): migrate CDP bridge -> zapd consumer; make disk authoritative
The MCP runtime kept pulling the buggy published wheel (hanzo-tools-browser
0.5.7) from PyPI instead of the migrated on-disk source, so every restart
re-broke the `cdp` tool. Reinstate `cdp` as a zapd-native tool, finish the
bridge->zapd migration (delete dead bridge/server modules + their tests), bump
to 0.5.8 and pin hanzo-mcp >=0.5.8, add the package to the uv workspace/sources
so disk is authoritative. 9/9 browser tests pass.
Pairs with the hanzoai/extension bridge->zapd migration (separate, still
uncommitted). The ~/.claude.json MCP invocation was also pointed at the local
editable source (machine config, not in this repo).
(cherry picked from commit 11b4d4ea8a5570d6fb0e51079fa445dba3713f71)
* fix(browser): drop dead lifecycle.py (in-process zap_server/cdp_bridge are gone)
lifecycle.py only wrapped the removed in-process ZAP server + CDP bridge; nothing
imports it under the zapd-consumer model. One way: transport is zapd_consumer,
tools are browser/cdp/playwright.
---------
Co-authored-by: hanzo-dev <[email protected]>
Co-authored-by: Antje Worring <[email protected]>
Co-authored-by: Hanzo Dev <[email protected]>
The `memory` service never set REDIS_URL and never wired itself to the
Redis container. Runtime config (src/hanzo_memory/config.py) defaults
redis_url=None — caching via Redis is optional and unconfigured. The
`redis:` block was a dead, profile-gated (with-cache) declaration with
no consumer.
Per "no redis, no kv" policy: remove the orphaned block and its volume.
No consumer migration needed (nothing consumed it).
Published 0.2.0 predates 10da71bd: its ConfigTool passed
permission_manager positionally into BaseTool.__init__(self), so the
'config' tool failed registration on every server start (and 0.2.0
lacked WorkspaceTool entirely). The fix has sat unreleased on main —
same stranded-fix pattern as hanzoai.protocols. hanzo-mcp now floors
hanzo-tools-config>=0.2.1.
Floor hanzoai at 2.2.1 — the first release that actually ships
hanzoai.protocols, which permissions.py imports.
Drop the stderr→devnull redirect in stdio mode. The MCP stdio protocol
rides stdout alone; stderr is the channel clients log for diagnostics.
Silencing it turned the missing-module crash into a zero-byte mystery —
the client log literally suggests printing to stderr while the server
was throwing tracebacks into devnull. Run-loop failures also log with
exc_info so the traceback reaches the client log.
protocols.py landed 2026-03-31 (1fff6dea) but 2.2.0 was published before
it, so the module never reached PyPI. hanzo-mcp imports
hanzoai.protocols at startup; resolving against the published 2.2.0
crashed every stdio launch with ModuleNotFoundError before the
initialize response — surfacing as 'Failed to connect' in Claude
Desktop/Code.
Adds bidi_client.py — a WebDriver BiDi client over WebSocket to
Firefox 129+ / Chrome 124+ at --remote-debugging-port=9222.
This is the "trusted input" backend that complements the existing
WebExtension scripting backend (the cdp_bridge_server WS to the
extension). The extension produces synthetic events with
isTrusted=false, which strict frameworks (Drupal AJAX, certain React
libraries, security-aware sites) reject. BiDi via input.performActions
produces real browser input with isTrusted=true that ALL frameworks
honor because they ARE the real browser events.
Methods implemented:
- probe() auto-detect at /json/version
- connect() / close() WebSocket session lifecycle
- list_contexts() browsingContext.getTree
- find_context_by_url() locate tab by URL substring
- navigate(ctx, url) browsingContext.navigate
- capture_screenshot(ctx) browsingContext.captureScreenshot
- script_evaluate(ctx, js) script.evaluate in page context
- input_mouse_click(ctx,x,y) input.performActions — TRUSTED click
- input_double_click double-click composite
- input_key_press input.performActions key
- input_insert_text trusted keyboard typing
- click_selector(ctx, sel) composite: eval rect + trusted click
Architecture: decomplected, three layers. Layer 1 = wire transport
(WebSocket JSON-RPC). Layer 2 = canonical primitives
(input.performActions / browsingContext.* / script.evaluate). Layer 3
= ergonomic aliases (click_selector composite).
Integration into cdp_bridge_server.py — auto-detect on startup, route
BiDi.* methods, optionally route Input.dispatchMouseEvent and
hanzo.click to BiDi backend when available — is the next step.
To enable BiDi in a Firefox session:
open -a "Firefox Developer Edition" --args --remote-debugging-port=9222
The bridge will probe http://localhost:9222/json/version on startup;
if BiDi is available it advertises BiDi.* in its capabilities.