Commit Graph
643 Commits
Author SHA1 Message Date
zeekay 7aa204ed16 ci: go native — 13 GitHub workflows out, publish moves to git.hanzo.ai
git.hanzo.ai is the canonical forge and GitHub is a mirror, so this repo's CI
had no business living in .github/workflows. It also did not work: Actions is
disabled at the ACCOUNT level for the identity that pushes here —

  gh workflow run publish-pypi.yml
  HTTP 422: Actions has been disabled for this user

— and a tag push produced no run for the same reason. Thirteen workflows (ci,
test, test-windows, test-hanzo-mcp, test-hanzo-tools, quality-gate, docs,
generate, generate-api-providers, hanzo-packages-ci, publish-external-dists,
publish-pypi, test-auto-publish) looked like CI and could not run. That is worse
than having none, and it is how `hanzo` 0.4.4 sat tagged-but-unpublished while
PyPI kept serving 0.4.3 — a build whose `hanzo auth login` cannot complete,
because Cloudflare rejects urllib's default User-Agent with `error code: 1010`
and the token exchange 403s after the user has already signed in.

Depending on GitHub to ship a fix for our own CLI was the actual defect.

Now:
  .hanzo/workflows/publish-pypi.yml     — native, runs on hanzo-build-linux-amd64
  .hanzo/workflows/sync-from-github.yml — the proven fast-forward-only pull
  .github/workflows/sync.yml            — a mirror nudge, nothing else

The tag → package mapping is carried over intact and checked against real tag
shapes: hanzo-v0.4.4 → hanzo, hanzo-dev-0.1.0 → hanzo-dev, hanzoai-3.1.1 →
hanzoai, hanzo-tools-vcs-v0.1.2 → the tools set, v1.0.0 → all. The package list
is auto-discovered from pkg/ rather than hardcoded, because the old hardcoded
lists silently dropped newly added packages.

Both native workflows parse as valid YAML. The PyPI token must exist as a secret
on git.hanzo.ai for the publish to run; the job fails loudly with that message
rather than half-publishing.
2026-07-26 13:27:33 -07:00
zeekay d319c6b0bc release(cli): hanzo 0.4.4 — auth login actually works
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.
2026-07-26 11:26:33 -07:00
zeekay 50d442eb7d fix(cli): auth login was broken end-to-end — Cloudflare 1010, no PKCE, legacy paths
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.
2026-07-26 11:09:23 -07:00
zeekayandHanzo Dev 9a13066b24 ci: allow manual runs of Hanzo Packages CI
Push-event dispatch in this repo is intermittent — the two commits before
this one produced zero workflow runs — so there was no way to exercise a
package's test job on demand. Both publish jobs are already gated
(auto-publish on github.event_name == 'push', publish-to-pypi on a tag ref),
so a manual run can only test.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 08:33:17 -07:00
zeekayandHanzo Dev f3cb1892eb kms: migrate hanzo-kms off Infisical paths onto the luxfi/kms surface
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]>
2026-07-26 08:29:59 -07:00
zeekay 0a5b2a1f4e refactor(iam): route every OIDC call through the canonical path constants
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.
2026-07-26 01:55:28 -07:00
zeekay 3030b2a152 docs: modernize + LLM.md + cross-links (one-way SDK model) 2026-07-24 12:29:42 -07:00
zeekay 039fc7b1b1 docs: modernize + LLM.md + cross-links (one-way SDK model) 2026-07-24 12:29:41 -07:00
zeekay f99cf2a19a research-sdk: rename the running-log method note()->log() (fix method/field shadow)
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.
2026-07-22 22:11:38 -07:00
zeekay 53cc3979d5 research-sdk: structure runs as falsifiable tests — hypothesis, notes, verdict
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.
2026-07-22 21:56:07 -07:00
zeekay a0599c1fee research SDK: content-address artifacts server-side; drop X-User-Id minting (Red L1/M1)
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.
2026-07-22 19:57:31 -07:00
zeekay 6bb89b86cc research: hanzo-research SDK — ergonomic, zero-config, auto-instrumenting client
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.
2026-07-22 18:24:12 -07:00
zeekay 0698c2866d merge(feat/browser-zapd-autospawn): consolidate onto main 2026-07-21 17:18:50 -07:00
zeekay 22aa49321c chore(hanzo-flags): commit uv.lock 2026-07-19 20:02:48 -07:00
zeekay 363c35bafe feat(tools): hybrid local+cloud tools -- HanzoCloud client, cloud code/vector actions, vision
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.
2026-07-19 20:02:48 -07:00
11022ccfac fix(generate): authenticated spec fetch — hanzoai/openapi is private (#49)
raw.githubusercontent.com/hanzoai/openapi/main/hanzo.yaml 404s (openapi is a
private repo), so every spec-update regen failed at the fetch. Fetch hanzo.yaml
through the GitHub Contents API with a SPEC_TOKEN (repo/contents:read on
hanzoai/openapi) instead. Local SPEC=... override unchanged.

Co-authored-by: zeekay <[email protected]>
2026-07-18 12:31:17 -07:00
zeekay 82ed2626cb hanzo-flags: the Python client for the native flags engine
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.
2026-07-17 13:00:33 -07:00
zeekay 5489e2c560 feat(hanzo-train): add Tinker-shaped training client (0.1.0)
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.
2026-07-15 18:16:32 -07:00
zeekay cd1c4c359c ci: route linux/amd64 jobs to hanzo-build-linux-amd64 ARC scale set 2026-07-10 21:51:56 -07:00
zeekay df2e3160bf release(hanzo-tools-browser): 0.5.10 — zapd connect-or-spawn auto-start 2026-07-08 08:50:48 -07:00
zeekayandGitHub 49ad34a2c9 feat(browser): auto-start zapd when absent (connect-or-spawn) (#47)
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.
2026-07-08 08:49:21 -07:00
zeekay e53ba1917c feat(browser): auto-start zapd when absent (connect-or-spawn)
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.
2026-07-08 08:36:14 -07:00
zeekay df0becc3cc fix(hanzoai): 3.1.1 — guard optional ZAP import so import hanzoai works without the [zap] extra
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.
2026-07-05 19:49:03 -07:00
zeekay 98a9a2bfa1 release(hanzoai): 3.1.0 — opt-in ZAP-native transport 2026-07-05 19:20:31 -07:00
a777df1efb feat(hanzoai): optional ZAP-native transport (opt-in, public API unchanged) (#46)
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]>
2026-07-05 19:18:09 -07:00
zeekay 65b6557bf2 fix(workspace): complete member set — install real toolset, fix 20 integration failures
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.
2026-07-05 12:11:35 -07:00
zeekay 4d6d68a6e0 fix(workspace): pin hanzo-tools + hanzo-tools-core to on-disk source (kill PyPI shadow)
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.
2026-07-05 12:07:52 -07:00
zeekay 7902881446 fix(hanzoai): restore core SDK modules wiped by #45 regen — unbreaks hanzo-mcp (3.0.1)
#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.
2026-07-05 09:50:51 -07:00
zeekay 04741734d5 fix(browser): route MCP register through _result_to_mcp — ToolImage→ImageContent
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.
2026-07-05 04:29:07 -07:00
bcd56c6910 feat(hanzoai): regenerate client from unified OpenAPI spec (retire Stainless) (#45)
Replace the Stainless-generated `hanzoai` client (188 legacy LLM-gateway
endpoints) with an openapi-generator `python` (urllib3 + pydantic v2)
client built from hanzoai/openapi `hanzo.yaml` — the full unified
api.hanzo.ai/v1 surface (493 API groups, 1710 models, all /v1 products).

- scripts/generate.sh: the one-way generator; emits only pkg/hanzoai
  (other pkg/hanzo-* packages untouched).
- pyproject: deps -> urllib3 / python-dateutil / pydantic>=2 /
  typing-extensions; version 2.2.3 -> 3.0.0 (breaking surface change).
- tests/: Stainless client tests replaced with a surface smoke test.
- .github/workflows/generate.yml: regenerate on openapi spec-update.
- Base URL https://api.hanzo.ai, Bearer hk- / JWT auth.

Verified: wheel hanzoai-3.0.0 builds, installs into a clean venv, and
`import hanzoai` + product API construction succeed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 20:07:32 -07:00
zeekay 565426aa84 Merge branch 'chore/ui-rag-v1-paths'
# Conflicts:
#	pkg/hanzo-tools-browser/hanzo_tools/browser/browser_tool.py
2026-07-04 13:59:42 -07:00
zeekay 61d932766c fix(browser): screenshots/pdf save-to-disk + return path, never inline huge base64
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.
2026-07-04 12:33:52 -07:00
zeekayandHanzo Dev 93330d3091 fix(billing): drop extraneous /api prefix — api.hanzo.ai/v1/billing
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]>
2026-07-03 12:31:09 -07:00
zeekayandHanzo Dev 3da02092db browser/zapd_consumer: self-heal a dead cached socket
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]>
2026-07-03 12:31:09 -07:00
zeekay 711eb4987a feat(mcp): native MCP ImageContent — tools return images the client can SEE
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.
2026-07-03 00:45:49 -07:00
zeekay 5bfc8f9399 test(hanzo-mcp): guard every hanzo.tools entry point imports cleanly
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.
2026-07-02 23:57:21 -07:00
zeekay babfd616fa release(hanzo-tools): 0.3.1 — ship core error-class re-exports
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.
2026-07-02 23:55:25 -07:00
zeekay 421af5170a fix(hanzo-mcp): 'tool list' crash on @property descriptions (v0.15.11)
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.
2026-07-02 23:00:52 -07:00
zeekay 590a1bd88e Merge remote-tracking branch 'origin/rip/api-callers-to-v1' 2026-07-02 13:18:52 -07:00
zeekayandHanzo Dev c3ac3efd06 rip: migrate KMS universal-auth login to canonical /v1/kms/auth/login
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]>
2026-07-02 13:16:42 -07:00
zeekay 3076f1587f release: bump auth-fixed packages for publish
Ship the canonical hanzo-app client_id + /v1/iam/get-account fixes:
- hanzoai 2.2.2 → 2.2.3
- hanzo-cli 0.2.1 → 0.2.2
- hanzo 0.4.2 → 0.4.3
- hanzo-tools-auth 0.1.0 → 0.1.1
2026-06-30 22:21:53 -07:00
zeekay 73774a1b51 auth: fix get-account endpoint (bare /v1/get-account 404s)
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).
2026-06-30 17:56:31 -07:00
zeekay 133472997a auth: align all SDK packages to canonical <org>-<app> client ids
IAM seeds apps as <org>-<app> (iam cmd/iam/cli/init_apps.go:57-59):
hanzo-app, zoo-app, lux-app — AppName == ClientID. Several packages used the
inverted 'app-hanzo'/'app-hanzobot' or placeholder '*-client-id', none of which
IAM knows — device/PKCE login would fail. One value, everywhere:

- hanzoai/auth.py: login_with_pkce default -> IAM_CLIENT_ID (hanzo-app)
- hanzo-cli/auth.py: DEFAULT_APP, DEFAULT_CLIENT_ID -> hanzo-app
- hanzo/commands/auth.py: IAM_CLIENT_ID -> hanzo-app
- hanzo-tools-auth/session.py: DEFAULT_APP, DEFAULT_CLIENT_ID -> hanzo-app
- hanzo/commands/iam.py: --app default + doc example -> hanzo-app
- hanzo-cli/bot/commands.py: BOT_IAM_APP/CLIENT_ID app-hanzobot -> hanzo-bot
2026-06-30 17:23:10 -07:00
zeekay 5ecb393e8a auth: fix device login + API-key flow to real hanzo.id/IAM contract
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).
2026-06-30 17:23:10 -07:00
44dab1e8a5 chore(tools-ui): RAG client paths /api/* -> /v1/* (#43)
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]>
2026-06-30 17:06:30 -07:00
zeekay d6d3d1b07d chore(tools-ui): RAG client paths /api/* -> /v1/*
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.
2026-06-30 16:58:58 -07:00
f2ca58ca6c Point cloud clients at canonical api.hanzo.ai front door (#42)
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]>
2026-06-30 16:49:00 -07:00
0ea7627203 refactor(iam): drop Casdoor branding — Hanzo IAM only (white-label) (#41)
Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 14:56:32 -07:00
23ab981c47 fix(ci): PyPI upload token-fallback for per-project scope (#40)
hanzo-iam is owned on PyPI by HANZO_AI_PYPI_TOKEN, not PYPI_TOKEN, so it 403'd.
Each upload now tries PYPI_TOKEN then falls back to HANZO_AI_PYPI_TOKEN, so any
hanzo-* package publishes regardless of which token owns its project.

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 14:36:47 -07:00
6c90b9d9ea fix(ci): auto-discover packages + retry PyPI uploads on 429 (#39)
The hardcoded MAIN/TOOLS package lists silently dropped hanzo-zap and the
newer packages, so tag-triggered 'publish all' never shipped them. Derive
the list from pkg/ so it can never go stale again. Also retry uploads with
backoff: PyPI 429-rate-limits rapid multi-package bursts, which dropped the
tail of a large publish (hanzo-hooks, hanzo-tools-gimp, hanzo-tools-s3).

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 14:04:13 -07:00