Commit Graph
855 Commits
Author SHA1 Message Date
zeekay ad606c7611 The browser tool takes a seat on the embedded ZAP router
hanzo-mcp said HELLO as consumer:hanzo-mcp/<pid> to a separate zapd
daemon, which the router that replaced it refuses as bad_id. Every
process that speaks ZAP now embeds the router (zapd 1.1 on PyPI) and the
kernel lock elects one; hanzo-mcp stands for it at startup and takes the
seat mcp/hanzo-<pid>. A browser is a browser/<host>/<name> node, found
by engine or name and reached with Node.call, and the extension's
command codec is unchanged.

hanzo-mcp pair prints the code the extension's popup takes. The daemon
spawn and socket probing are gone, and so is the zap-proto dependency.
2026-09-30 06:56:52 -07:00
zeekay 379600adc8 client: regenerate from hanzo-inc/cloud@2774987ccc (8.5.164)
The client lane checks the committed client against the document
api.hanzo.ai serves now, not against the ref .spec-lock pins. Cloud
v1.833.257 (2774987c) went live after 8.5.163 was cut and rewrote the
POST /v1/decisions description (cad76e1a0: Jev by OpenRouter's vendor
ids), so the served document moved from 296b1f84 to 783f66a3 and every
push since has failed the check on ai_api.py's docstring. The drift
reproduces identically at e34ab7e1; nothing in this tree caused it.

This is the lane's own move, done by hand: .spec-lock names the served
revision and digest, the generator reruns over those bytes, uv.lock
takes what `uv run` wrote, and the version takes the next patch.
2026-09-29 13:53:29 -07:00
zeekay 1b4cfa4c16 devserver passes browser options through args
The browser tool types only its core fields; everything else rides in
`args`. navigate's state and console's level sat at the top level,
where the schema drops them, so errors opened the app without waiting
for networkidle and read every console message rather than errors.
The test's stand-in browser now has the real tool's shape.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-29 13:06:50 -07:00
zeekay 450f65932d browser teaches the snapshot loop in its description and the rest on help
The tool put ~120 actions in a Literal enum and ~60 parameters in its
schema, all of it in every agent's context, and most of it Playwright
locator builders that returned a count. Three hand-kept lists (the enum,
extension_actions, _zap_method_for) disagreed: snapshot, wait and
select_option routed to the extension under names it did not answer.

ACTIONS is now the one table: each action once, its topic, its usage
line, and the extension method that serves it. The schema carries the
core loop (navigate, snapshot, click, fill, type, press, read,
screenshot, evaluate, wait, tabs, help) with an open `action` string;
help prints the rest by topic, and their parameters travel in `args`.

snapshot, click-by-ref and friends go to the extension's page engine
(hanzo.snapshot / hanzo.act / hanzo.read / hanzo.wait / hanzo.navigate,
screenshot annotate=true to hanzo.annotate). A tree or a page answers as
text; a refusal ("Stale ref @e4 … run snapshot again", "covered by
<div#consent>") is the error, not a success wrapping "ERR:", and is
never retried on Playwright, which has no refs to act on.

Aliases and no-ops are gone: select_option (select), get_inner_text and
get_value (get_text), clear (fill), content (get_html), new_page
(new_tab), press_key, mouse_wheel (scroll), list_browsers and
list_mcp_instances (browsers), use_browser, set_default_browser,
claim/release_browser, the locator/get_by_*/first/last/nth/filter/all
builders, is_hidden, frame, main_frame, on and off.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-29 13:05:21 -07:00
zeekay 82a090d0ba hanzo-tools and hanzo-mcp stay on mcp 1.x
hanzo_tools.core imports mcp.server.FastMCP, which mcp 2 removed, so a
fresh resolve that picks mcp 2.2.0 fails at import. The published
hanzo-mcp 0.15.17 already declares mcp<2; the tree now says the same,
and hanzo-tools, which owns the import, says it too.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-29 12:41:52 -07:00
zeekay af78255a39 devserver: running dev servers as MCP nodes
An agent editing a web app needs the dev server's own view of it: which
routes exist, and whether the last edit broke the build. Next 16 serves
that over MCP at /_next/mcp, and Vite-based frameworks can with
vite-plugin-mcp, but nothing here found those endpoints.

index finds Next, Vite, Nuxt, Remix, React Router, Astro and SvelteKit
dev servers from each process's argv, cwd and listening ports, and lists
the tools of the MCP each serves. call runs one. errors returns current
build and runtime errors: Next's get_errors, after opening the app in the
browser tool, since Next reports only what a connected session reported;
otherwise Vite's error overlay, console errors and page errors, read
through the same browser tool.

discover() returns the servers as plain records, the shape a devserver
node will take. hanzo-mcp depends on the package so the tool ships.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-29 12:37:53 -07:00
zeekay 351b1a2b1b mcp_proxy reaches running servers over HTTP and SSE
MCPServerConnection only spawned stdio servers, so an endpoint a process
already serves - Next 16's /_next/mcp, vite-plugin-mcp's /__mcp/sse - had
no client in this repo.

MCPServerConfig now takes a url. connect lists the tools and call_tool
calls one through the SDK's streamable HTTP or SSE client, a session per
request, and returns the same JSON-RPC result the stdio path returns. A
loopback URL skips TLS verification, since a local dev certificate is
self-signed. A failed connect says why in .error.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
2026-09-29 12:37:53 -07:00
zeekayandHanzo Dev e34ab7e14c client: regenerate from hanzo-inc/cloud@efc1644bfe (8.5.163)
Co-authored-by: Hanzo Dev <[email protected]>
v8.5.163
2026-09-29 17:17:07 +00:00
zeekay d49582ec35 Pin the CI caller to hanzoai/ci 909203f, whose PyPI lane seeds its clean venv with pip. 2026-09-29 10:13:16 -07:00
zeekay 4dac77fe06 hanzo-train 0.1.1 drives the training wire at /v1/train/clients. 2026-09-28 22:11:20 -07:00
zeekayandHanzo Dev 875469edc7 client: regenerate from hanzo-inc/cloud@20e58292d2 (8.5.162)
Co-authored-by: Hanzo Dev <[email protected]>
v8.5.162
2026-09-29 01:37:51 +00:00
zeekay 0336582777 Pin the CI caller to hanzoai/ci 4b737d1, whose tools checkout fetches a pinned sha. 2026-09-28 18:33:30 -07:00
zeekay 68d560c316 The store and agent examples name the document's ProvisioningProvisionRequest and AgentCreateAgentIn. 2026-09-28 18:33:24 -07:00
zeekayandHanzo Dev d044022152 client: regenerate from hanzo-inc/cloud@8eb2e12e26 (8.5.162)
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-29 01:18:01 +00:00
zeekay 7d85ade686 Pin the CI caller to hanzoai/ci 58fd5da, whose cut pushes to github.com with a GitHub token. 2026-09-28 18:14:06 -07:00
zeekay fb29d854fa The versioned-id fixture asks /v1/decisions only to accept the id, and /v1/systemone to answer with it 2026-09-28 18:10:48 -07:00
zeekay 8102dad40b sdk_jev asks about a March invoice, as the other examples do. 2026-09-28 18:05:58 -07:00
zeekay 169648c43d hanzo_kai.jev lists the decision models of data in Jev's shape, and the conformance fixtures name every reach refusal and pin Kai's versioned id 2026-09-28 17:37:54 -07:00
zeekay 70cb092e67 hanzo-kai's examples are the programs behind every Python page of the Kai docs. 2026-09-28 16:35:51 -07:00
zeekay 15c3a6ea95 Bring hanzo-kai to the frozen decision contract, with the Jev-compatible hanzo_kai.jev and a conformance fixture per rule 2026-09-28 14:57:06 -07:00
zeekay 75771bfe7f Add hanzo-kai, the Python client for Kai decisions 2026-09-28 14:32:02 -07:00
zeekayandHanzo Dev 862f504cb4 LLM.md is the one committed guide; CLAUDE.md, AGENTS.md and GEMINI.md are local links to it
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 15:48:10 -07:00
zeekayandHanzo Dev 2e271b5168 Install hanzoai>=8, so an older Python refuses instead of installing 2.1
Hanzo CI/CD / test (linux-amd64) (push) Successful in 2m42s
Hanzo CI/CD / test (linux-arm64) (push) Successful in 5m16s
Hanzo CI/CD / cicd (push) Failing after 2m2s
CI/CD / cicd (push) Failing after 9m50s
hanzoai 8 requires Python 3.12, and PyPI serves 2.1.3 to 3.9-3.11. An
unpinned `pip install hanzoai` there installed the 2.1 client without a word,
and every example here failed on `from hanzoai.cloud import ...`. Every install
line now carries the floor, which pip 21 on Python 3.9 answers with "No
matching distribution found for hanzoai>=8", and the quickstart no longer says
Python 3.9.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 11:11:59 -07:00
zeekayandHanzo Dev 02fbcb91bb Drop the install-from-git detour
Hanzo CI/CD / cicd (push) Failing after 2m5s
CI/CD / cicd (push) Failing after 2m6s
PyPI has shipped Client since 8.5.161, so a new user following the README
was sent to a git install they do not need.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:04:24 -07:00
zeekayandHanzo Dev 3335b2fa21 hanzoai 8.5.161: the import report runs under the interpreter its siblings fetch
Hanzo CI/CD / cicd (push) Successful in 2m21s
CI/CD / cicd (push) Successful in 2m21s
v8.5.160 regenerated the client and its release run passed the client check,
then stopped at declared-imports: the step called bare `python`, the runner has
none on its PATH, and exit 127 from a step that is report only ended the run
before anything was published. It now runs through uv like the three steps
beside it. Verified by running the whole test: block with no python on PATH.

Co-authored-by: Hanzo Dev <[email protected]>
v8.5.161
2026-09-18 22:23:29 -07:00
zeekayandHanzo Dev 40cb597c2f client: regenerate from hanzo-inc/cloud@2314af8b90 (8.5.160)
Hanzo CI/CD / cicd (push) Failing after 2m6s
CI/CD / cicd (push) Failing after 2m7s
The committed client had fallen behind the API it projects: 116 modules the
document now names were missing, 13 it no longer names were still here, and
30 had moved. The release lane refuses to publish a projection that does not
match the document, so v8.5.158 and v8.5.159 were tagged and never reached
the index.

Regenerated from the document production serves (sha256 aa1b79d0797704608e25a3898adb2ca69fec82a03f74c09671c372b63066cafe),
with the driver at hanzoai/openapi@88ea26b — the same inputs the lane reads,
and the same +116 -13 ~30 it reported. The repo's own gate ran over exactly
these bytes: 121 api and 2675 model modules import, no class declares a field
twice across 2804 modules, and every example flow resolves its method names.

Co-authored-by: Hanzo Dev <[email protected]>
v8.5.160
2026-09-18 17:28:47 -07:00
zeekayandHanzo Dev ea1940e3e3 hanzoai 8.5.159
Co-authored-by: Hanzo Dev <[email protected]>
v8.5.159
2026-09-11 13:44:15 -07:00
zeekayandHanzo Dev fb564b4d49 test_smoke follows the tools tag to tool
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 13:44:15 -07:00
zeekayandHanzo Dev a2652a6979 Stop a rate-limited call from sleeping for hours
urllib3 sleeps whatever Retry-After says, clamped at 6 h in 2.7, and retries three times. api.hanzo.ai sends ~23,000 s for an exhausted quota, so one GET held its caller for 18 h. Client sleeps through at most 60 s; past that the answer returns at once, as ApiException with the header for generated operations and as Fault.retry_after for the six.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 13:44:15 -07:00
zeekayandHanzo Dev 321aa75a5c Say what pip installs, and fix two examples that no longer import
pip install hanzoai on 3.12 gets 8.5.156, which has no Client; on 3.9 to
3.11 it gets the 2.1 line, a different client. The README said neither.
It showed KeysApi and MemoryApi, which the generated client does not
have, and mapped hanzo-agent and hanzo-agents to copies here that PyPI
does not publish.

examples/agent and examples/tools imported AgentsApi and ToolsApi, which
the document now names AgentApi and ToolApi. Both flows ran against a
local server after the change. The per-agent runs read is generated
without its ref, so the agent flow reads /v1/agent/runs and keeps its
own rows.

The status table and the Retry-After lines come from runs against a
local server: a capability call is one request, two on a 401; a
generated operation sleeps for Retry-After on 429 or 503 and retries
three times.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 10:47:40 -07:00
zeekayandHanzo Dev e6f6ff34d7 Map the imports no package declares
hanzo-agent 0.1.0 went to PyPI importing numpy and httpx and declaring
neither. In this repo the workspace supplies every sibling, so its own tests
all passed; `pip install hanzo-agent` then failed at import for everybody else.
An outside adopter hit that, gave up on three of our packages, and wrote their
own clients — the most expensive kind of bug we ship, because it is invisible
from inside the tree.

This reports every import across the 64 packages that its own manifest does not
declare. 90 of them.

It does NOT fail, and the reason is a limit of reading rather than leniency:
hanzo-zap imports `zap` and declares `zap-proto`, which provides it;
hanzo-tools-iam imports `mcp` and declares a sibling that depends on it.
Neither is broken and both look identical to a parser. Failing 90 rows on day
one is how a gate gets switched off.

It still earns its place. Relying on a transitive dependency is a break waiting
for the day that sibling drops it, and 90 rows is a map of where that sits.

The authoritative check — install each wheel alone in a clean venv and import
it — belongs in the shared publish lane rather than a copy here, which is the
point dd889c29 already made when it removed this repo's own lane.

The `zap` row was mine: the first run said 92 because the alias table did not
know zap-proto provides it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 23:04:17 -07:00
zeekayandHanzo Dev ca850e5cc9 Guard the install check against an empty catalogue
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 21:56:10 -07:00
zeekayandHanzo Dev 39bc72bc09 Give the install check a form the wheel carries
The line under `pip install hanzoai` was `python -m examples.models`, and
examples/ is in the sdist only — hatch's wheel target is pkg/hanzoai. So
the first command in the README answered ModuleNotFoundError to everyone
who installed the package the way the line above told them to.

Co-authored-by: Hanzo Dev <[email protected]>
v8.5.158
2026-09-10 21:50:23 -07:00
zeekayandHanzo Dev 30a465e537 Release through the shared lane, not a copy of it
publish-pypi.yml was a per-repo spelling of a rule the whole estate
needs, and it was the estate's only one: a Python repo without a copy
of this file could not publish at all. `pypi:` in hanzo.yml now names
what ships and hanzoai/ci ships it, so the rule has one home.

The hardcoded package list goes with it. A glob over pkg/ cannot fail
to notice a package added beside it, and the tag is matched against
each pyproject's own name rather than against a case list, so all four
tag shapes already in this repo's history resolve to the same
distributions they did before.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 21:44:51 -07:00
zeekayandHanzo Dev 234c2d8cf1 Prefer tgrep where the tree carries an index
Trigram index prunes files a query cannot match: 23ms against ripgrep's 78ms
for an absent symbol, which is most of what a search asks. It loses where many
files match (128ms against 78ms), and with no index it scans everything and is
slower than plain grep — so the index is the condition, not the binary.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:24:51 -07:00
zeekayandHanzo Dev 2b0401db54 ci: confirm a push still schedules a run
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:24:51 -07:00
zeekayandHanzo Dev 206be715d8 ci: verify the GitHub->forge inbound leg delivers
No file changes: this exercises the ingest path only, so the forge head
must advance to this commit without a build being cut.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:24:51 -07:00
zeekayandHanzo Dev 0da7a4185c fix(six): one word, one body, one shape across the three SDKs
Thirteen places where this SDK and the other two answered the same question
differently. Every one is now what the served handler reads, or what HIP-0044
says where the handler is silent.

Where this SDK was already right, the other two moved: search normalizes the
knowledge kind in both directions, `Page.total` stays the server's own count,
and `put` picks its method from the name. Where it was not:

- kb.Import carries what the handler answers. `/v1/knowledge/import` writes
  {format, imported, pages} (knowledge/import.go); this modelled `imported`
  alone, so a caller could not see which pages landed.
- Fact carries the server's three. `GET /v1/graph` serves twelve members
  (graph/typed.go, wireFact) and no other route hands back an assertion's
  content address, so projecting the nine an asserter states made a read lossy
  with nothing to recover it from. `id`, `by` and `knowable` arrive read-only
  and `write()` never sends them.
- graph.extract and graph.ingest take a Source. Both routes bind the same
  {source, subject, text, at} (graph/semantic.go, graphSourceIn), so it is one
  value — and the first positional argument is now the same thing here as it is
  in the other two.
- import_(format, data, project) — the order the contract writes and the other
  two take. It was (export, *, format).
- A body carries what somebody set. `send` dropped None from the query and left
  it in the body, so `search.find` posted {"index": null, "offset": null,
  "project": null} where the other two omit the key: three requests for one
  call, and cloud asked to read a value where there is none. Audit's pageSize
  and p go the same way — the route's own defaults are the same 100 and 1, so
  an unset term is one the route decides identically, and `all` pins the size it
  walks at because it needs to know the page length.
- A write body is field data and nothing else. `url` belongs to a source, and an
  empty title is left out so the doctype names the required field that is
  missing rather than being handed a blank one.
- kb.list takes order and kb.links takes a project. `/v1/framework/{doctype}`
  serves order_by and `/v1/knowledge/graph` serves ?project=.
- policy_denied and entitlement_required do not exist. Neither string appears
  anywhere in hanzoai/cloud, so REFUSED is the two codes it does emit. A 403
  read as denied on an invented code would hand an unauthenticated caller a cure
  for a refusal nobody made.
- One refusal reader, by named fallback: code from `code` else `error`, reason
  from `detail` else `message`, cures from `cure`. The gate's own `reason` —
  "unpaid", "unresolved" — names the admit leg that failed rather than
  explaining anything, so it is dropped, and `title` is not a refusal member.
- Money counts minor units, not cents: hundredths for USD, whole yen for JPY,
  thousandths for KWD.
- Balance.reserved, not held. The wire says `holds`; `held` is an arm.
- Vocabulary.rules, because it is a list. The wire member stays `rule`.
- The fourth outcome is Fault, not the generated client's ApiException. It is
  the one shared concept whose name a reader could not carry between languages,
  and an exception from the generated layer is not this layer's vocabulary.
- A client with no credential builds. It raised at construction, which is a
  process that cannot start where the other two are a process whose first call
  fails — and the first call is where the credential is actually needed.

HIP-0044 is amended where the code was right and the document was not.

ruff check, ruff format --check and mypy clean on every changed file; 109
passed. The two test_smoke failures are pre-existing (ToolsApi is absent from
the generated tree) and reproduce on a stashed working copy.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 16:26:15 -07:00
zeekayandHanzo Dev c2c8b8fd11 fix(six): ask the policy question about an org-rooted object, as the other two do
The example asked about "graph:hanzoai/python-sdk". The route parses `path` and
refuses one whose first segment is not the caller's own org, so that question
could only ever come back refused. It now reads the org off the wallet account
the budget step already named — the same construction go-sdk and js-sdk use —
and asks about "<org>/graph".

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 15:31:43 -07:00
zeekayandHanzo Dev 0a8e3272a0 fix(six): send the body /v1/authz/check reads, and stop stamping instants nobody set
policy.check was built from the route's OpenAPI description, which is stale.
The handler cloud pins — hanzoai/authz v1.10.37, serve/use.go — binds
{subject, verb, path} and answers {allow, subject, verb, path}; a body of
{sub, obj, act} gets 400 "subject, verb, path required". go-sdk and js-sdk both
send the served spelling. The caller-facing surface is unchanged: check(sub,
act, obj) -> Decision{allow, sub, act, obj, reason}.

graph left an unset instant on the body as "". Cloud reads an absent `seen` as
`at` and an absent `as_of` as now, but reads "" as a timestamp it refuses:
POST /v1/graph/ingest with at:"" answers 400 timestamp "" is not RFC 3339. An
instant nobody set is now left off.

kb.Edge takes PEP 8's keyword escape, from_ and to, rather than renaming the
pair to source and target — the word is `from`, which is what the other two
spell it. Document names and connector providers are escaped into their path
segment, as they already are in Go and TypeScript.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 15:28:12 -07:00
zeekayandHanzo Dev 7824191215 feat: budget, policy, audit, search, kb and graph on one credential
Six capabilities hang off the client as first-class methods rather than as
generated operations a caller assembles for itself: budget (allowance, wallet,
tier, charged ledger), policy (one authorization question), audit (the org's
trail, read-only by construction), search (one ranked set over every corpus),
kb (the corpus you write and the connectors that fill it), and graph
(assertions with provenance and time). The same six words, the same method
names and the same answer type as the Go and TypeScript clients.

A refused budget and a refused policy are answers. Answer is a union of three
frozen types with a discriminant — Ok, Denied, Held — so a caller reads
"denied, and here is why" without catching, and reading .value off a refusal
raises it rather than handing back a half-answer. One rule maps HTTP to an arm:
a 202 whose body says held is a hold, 402 and the four refusal codes on 403 are
refusals, and a status that decided nothing raises. Every arm carries the
x-request-id, which is the word audit.Event.request uses too — the join that
lets a caller find the row the server wrote for the call it just made.

Auth is IAM and nothing else. The client exchanges its own clientId and
clientSecret for a short-lived access token (client_credentials,
client_secret_basic, RFC 8707 resource), holds it until shortly before expiry,
and re-mints once on a 401. It no longer accepts a bearer, so HANZO_API_KEY,
Configuration(access_token=) construction and result() are gone rather than
deprecated: two ways to hold one credential is the defect this removes. as_
still scopes to a subject and now mints from the client's own token.

hanzoai resolves its public names on first use, and the capability layer is
written over the wire rather than over the generated client, so importing the
six costs milliseconds instead of 2,700 generated modules.

ruff and ruff format are clean, mypy is clean over the thirteen new and changed
modules, and 106 tests pass. The two failures in test_smoke are the ToolsApi
name the current document no longer carries, and they fail at HEAD too. The
mint was exercised against the live hanzo.id, which answers invalid_client for
a credential it does not know; the six were exercised against a stub asserting
the exact request and decoding the shapes cloud's own schemas declare.

examples/six runs the whole thing as a caller would.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 15:01:41 -07:00
zeekayandHanzo Dev e450125523 Drop the sync workflow that never ran
It has produced no run on the forge or on GitHub since it was added.
A workflow file that never fires still evaluates on every replayed
ref, so it is a hazard rather than a lane.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 00:27:03 -07:00
zeekayandHanzo Dev 792933d473 Move the workflows to .github/workflows
The forge reads .github/workflows natively and github.com reads nothing else,
so one directory serves both planes instead of two conventions that disagree
about which one is live. The reusable in hanzoai/ci already lives there, so a
caller naming that path now resolves to a file that exists.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 19:01:40 -07:00
zeekayandHanzo Dev d291f0f321 ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:08:07 -07:00
zeekayandHanzo Dev 4ba314672d fix: call the pipeline at the address it actually lives
hanzoai/ci keeps the reusable at .github/workflows/build.yml — the one path
both planes resolve, since github.com reads a reusable only from there and the
forge reads it too. The v1 tag moved with it, so a caller still naming
.hanzo/workflows/build.yml asks for a blob that is not in the tree:

  InsertRun: read hanzoai/ci@v1:.hanzo/workflows/build.yml: object does not exist

The caller itself stays at .hanzo/workflows/cicd.yml on purpose: github.com has
no runner for these labels, so a caller placed there could never be scheduled.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 01:14:30 -07:00
zeekay 9e93ffbeb2 Name the rest of the stack without importing it
`import hanzo` ran the CLI's module body, so it needed click, rich, httpx and
everything else in the list before it could tell you its own version — on a
bare interpreter it did not import at all. It also read `__version__` back off
this module from `cli`, a cycle that only worked because the assignment
happened to sit above the import.

Both go away by naming things instead of importing them. `__getattr__`
(PEP 562) resolves a member on first touch and caches it, so `hanzo.cloud`
costs nothing until somebody wants the cloud client, and the second touch is a
dict lookup. A member that is not installed says which distribution to install
rather than raising ModuleNotFoundError from somewhere inside us.

This is the shape the npm package and the Rust crate already had: one front
door, everything behind it, nothing loaded that was not asked for. One table
maps a name to its distribution, so adding a member is one line.
2026-09-05 18:15:02 -07:00
zeekay 7f4e176d57 Regenerate against the document the deployment answers for
The projection these were generated from was a month old and predated the
compute migration, so every one of them described /v1/visor — which the API
serves at zero paths now. That is why a hand-written console client and a
generated one had drifted the same way at the same time.

No /v1/visor path survives here. The word still appears as a source-plane
value and inside gvisor and supervisor, which are not this rename.
2026-09-05 18:07:44 -07:00
zeekayandHanzo Dev eb953b9fd5 test_smoke: billing takes the seat crm no longer holds
The document at the pinned ref names no `crm` tag and serves no /v1/crm,
so `hanzoai.cloud.api.crm_api` is a module the generator had nothing to
write — the import has raised ModuleNotFoundError since the emission that
dropped it.

This is the document's call, not a generation defect. The client emits one
module per tag and its 120 modules are exactly the 120 tags api.hanzo.ai
declares, with no difference in either direction; cli's spec/cloud.json,
projected from this same ref and digest, likewise carries neither the tag
nor the routes. So the expectation moves to the spec rather than the
client moving to the expectation.

Unlike tracker, analytics and chat before it, crm did not fold into
another product: contacts, companies, opportunities and applications have
no successor address anywhere in the document. billing replaces it because
it has been in every emission since the first generated client, which is
the property the test is really asserting — that whole products, not just
the AI routes, reach the SDK.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-04 00:09:01 -07:00
zeekayandHanzo Dev e740bb318b test_tenant: the keys operation lives on IamApi now
The regenerated client folded KeysApi into IamApi, so the tenant test
serializes `_get_iam_keys_serialize` to read the Authorization header it
asserts on. Same path under test — mint, cache, auth_settings — one class
name later.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-03 23:49:01 -07:00
zooqueenandzeekay 9492c358cc a uv source says where, not that anything depends on it
The hanzo-tools-core suite imports ten sibling tool distributions and
asserts an exact TOOLS count, but they were only ever named in
[tool.uv.sources]. A source resolves a package; it does not make anything
require one, so `uv sync --dev` uninstalled all ten and left the suite at
22 failed / 2 passed. Naming them in the dev dependency-group restores
24 passed, 12 skipped from a clean sync.

pkg/hanzo-dev-py never existed — the directory is pkg/hanzo-dev — so the
eight excludes and workspace members spelling it were inert. mypy.ini is
the config that actually loads, and with the name corrected it stops
type-checking a package it was written to skip (18 errors -> none).

cloud.py cited APIKeyPrefixes as the authority on key prefixes; no such
symbol exists. isAPIKey is the real gate and it admits hk-, sk-, pk-, fw_
and hz_, so NO_KEY was telling callers the wrong two.

(cherry picked from commit b2024662400895e456d4b3811fd286565e4e7109)
2026-09-03 23:39:20 -07:00